summaryrefslogtreecommitdiff
path: root/android
diff options
context:
space:
mode:
authorTomaž Vajngerl <tomaz.vajngerl@collabora.com>2014-06-03 15:23:39 +0200
committerJan Holesovsky <kendy@collabora.com>2014-06-30 14:48:01 +0200
commit8339f8abc5470e5b9bad611e5577fccf34fef240 (patch)
treebb6bc5ff9213184990255f44cfc04476fab9f202 /android
parent71e176c0205f916f04a3d8fa3908da4e6dd20f50 (diff)
Initial commit of Android Viewer project
Project was created with Android Studio. Currently includes the base of Fennec's LayerView and dependencies. Change-Id: I5c3ae253d153f659eb92bd0ca17ef95372b71b23
Diffstat (limited to 'android')
-rw-r--r--android/experimental/LOAndroid/.gitignore4
-rw-r--r--android/experimental/LOAndroid/.idea/.name1
-rw-r--r--android/experimental/LOAndroid/.idea/compiler.xml23
-rw-r--r--android/experimental/LOAndroid/.idea/copyright/profiles_settings.xml3
-rw-r--r--android/experimental/LOAndroid/.idea/encodings.xml5
-rw-r--r--android/experimental/LOAndroid/.idea/gradle.xml18
-rw-r--r--android/experimental/LOAndroid/.idea/libraries/appcompat_v7_19_1_0.xml10
-rw-r--r--android/experimental/LOAndroid/.idea/libraries/support_v4_19_1_0.xml11
-rw-r--r--android/experimental/LOAndroid/.idea/misc.xml143
-rw-r--r--android/experimental/LOAndroid/.idea/modules.xml10
-rw-r--r--android/experimental/LOAndroid/.idea/scopes/scope_settings.xml5
-rw-r--r--android/experimental/LOAndroid/.idea/vcs.xml7
-rw-r--r--android/experimental/LOAndroid/LOAndroid.iml12
-rw-r--r--android/experimental/LOAndroid/app/.gitignore1
-rw-r--r--android/experimental/LOAndroid/app/app.iml77
-rw-r--r--android/experimental/LOAndroid/app/build.gradle24
-rw-r--r--android/experimental/LOAndroid/app/proguard-rules.txt17
-rw-r--r--android/experimental/LOAndroid/app/src/main/AndroidManifest.xml25
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/libreoffice/LOKitShell.java22
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/libreoffice/MainActivity.java36
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/libreoffice/MainLayerView.java26
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/TouchEventInterceptor.java14
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/ZoomConstraints.java46
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/Axis.java420
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/BitmapUtils.java368
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/BufferedCairoImage.java69
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/CairoGLInfo.java35
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/CairoImage.java28
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/CairoUtils.java51
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java777
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/DisplayPortMetrics.java78
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/DrawTimingQueue.java95
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/FloatSize.java54
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/GLController.java354
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java1000
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java374
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/InputConnectionHandler.java22
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/IntSize.java91
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/JavaPanZoomController.java1461
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/Layer.java207
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/LayerMarginsAnimator.java324
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/LayerRenderer.java722
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/LayerView.java692
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/Overscroll.java21
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java130
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java49
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/PanZoomTarget.java33
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java123
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/PointUtils.java51
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/ProgressiveUpdateData.java33
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/RectUtils.java126
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/RenderTask.java80
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/ScrollbarLayer.java297
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/SimpleScaleGestureDetector.java322
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/SingleTileLayer.java153
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java148
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TextLayer.java69
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TextureGenerator.java75
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TextureReaper.java51
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TileLayer.java177
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TouchEventHandler.java306
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/ViewTransform.java34
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/VirtualLayer.java36
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/mozglue/DirectBufferAllocator.java51
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/EventDispatcher.java115
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/FloatUtils.java43
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java55
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/GeckoEventListener.java14
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/GeckoEventResponder.java16
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/ThreadUtils.java169
-rw-r--r--android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/UiAsyncTask.java86
-rw-r--r--android/experimental/LOAndroid/app/src/main/res/drawable-hdpi/ic_launcher.pngbin0 -> 9397 bytes
-rw-r--r--android/experimental/LOAndroid/app/src/main/res/drawable-hdpi/ic_status_logo.pngbin0 -> 1864 bytes
-rw-r--r--android/experimental/LOAndroid/app/src/main/res/drawable-mdpi/ic_launcher.pngbin0 -> 5237 bytes
-rw-r--r--android/experimental/LOAndroid/app/src/main/res/drawable-mdpi/ic_status_logo.pngbin0 -> 1533 bytes
-rw-r--r--android/experimental/LOAndroid/app/src/main/res/drawable-xhdpi/ic_launcher.pngbin0 -> 14383 bytes
-rw-r--r--android/experimental/LOAndroid/app/src/main/res/drawable-xhdpi/ic_status_logo.pngbin0 -> 2049 bytes
-rw-r--r--android/experimental/LOAndroid/app/src/main/res/drawable-xxhdpi/ic_launcher.pngbin0 -> 19388 bytes
-rw-r--r--android/experimental/LOAndroid/app/src/main/res/layout/activity_main.xml15
-rw-r--r--android/experimental/LOAndroid/app/src/main/res/menu/main.xml9
-rw-r--r--android/experimental/LOAndroid/app/src/main/res/values-w820dp/dimens.xml6
-rw-r--r--android/experimental/LOAndroid/app/src/main/res/values/colors.xml95
-rw-r--r--android/experimental/LOAndroid/app/src/main/res/values/dimens.xml5
-rw-r--r--android/experimental/LOAndroid/app/src/main/res/values/strings.xml8
-rw-r--r--android/experimental/LOAndroid/app/src/main/res/values/styles.xml8
-rw-r--r--android/experimental/LOAndroid/build.gradle16
-rw-r--r--android/experimental/LOAndroid/gradle.properties18
-rw-r--r--android/experimental/LOAndroid/gradle/wrapper/gradle-wrapper.jarbin0 -> 49896 bytes
-rw-r--r--android/experimental/LOAndroid/gradle/wrapper/gradle-wrapper.properties6
-rw-r--r--android/experimental/LOAndroid/gradlew164
-rw-r--r--android/experimental/LOAndroid/gradlew.bat90
-rw-r--r--android/experimental/LOAndroid/settings.gradle1
92 files changed, 11066 insertions, 0 deletions
diff --git a/android/experimental/LOAndroid/.gitignore b/android/experimental/LOAndroid/.gitignore
new file mode 100644
index 000000000000..d6bfc95b184b
--- /dev/null
+++ b/android/experimental/LOAndroid/.gitignore
@@ -0,0 +1,4 @@
+.gradle
+/local.properties
+/.idea/workspace.xml
+.DS_Store
diff --git a/android/experimental/LOAndroid/.idea/.name b/android/experimental/LOAndroid/.idea/.name
new file mode 100644
index 000000000000..3300c569c980
--- /dev/null
+++ b/android/experimental/LOAndroid/.idea/.name
@@ -0,0 +1 @@
+LOAndroid \ No newline at end of file
diff --git a/android/experimental/LOAndroid/.idea/compiler.xml b/android/experimental/LOAndroid/.idea/compiler.xml
new file mode 100644
index 000000000000..217af471a9e6
--- /dev/null
+++ b/android/experimental/LOAndroid/.idea/compiler.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="CompilerConfiguration">
+ <option name="DEFAULT_COMPILER" value="Javac" />
+ <resourceExtensions />
+ <wildcardResourcePatterns>
+ <entry name="!?*.java" />
+ <entry name="!?*.form" />
+ <entry name="!?*.class" />
+ <entry name="!?*.groovy" />
+ <entry name="!?*.scala" />
+ <entry name="!?*.flex" />
+ <entry name="!?*.kt" />
+ <entry name="!?*.clj" />
+ </wildcardResourcePatterns>
+ <annotationProcessing>
+ <profile default="true" name="Default" enabled="false">
+ <processorPath useClasspath="true" />
+ </profile>
+ </annotationProcessing>
+ </component>
+</project>
+
diff --git a/android/experimental/LOAndroid/.idea/copyright/profiles_settings.xml b/android/experimental/LOAndroid/.idea/copyright/profiles_settings.xml
new file mode 100644
index 000000000000..e7bedf3377d4
--- /dev/null
+++ b/android/experimental/LOAndroid/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,3 @@
+<component name="CopyrightManager">
+ <settings default="" />
+</component> \ No newline at end of file
diff --git a/android/experimental/LOAndroid/.idea/encodings.xml b/android/experimental/LOAndroid/.idea/encodings.xml
new file mode 100644
index 000000000000..e206d70d8595
--- /dev/null
+++ b/android/experimental/LOAndroid/.idea/encodings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" />
+</project>
+
diff --git a/android/experimental/LOAndroid/.idea/gradle.xml b/android/experimental/LOAndroid/.idea/gradle.xml
new file mode 100644
index 000000000000..736c7b5cffcc
--- /dev/null
+++ b/android/experimental/LOAndroid/.idea/gradle.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="GradleSettings">
+ <option name="linkedExternalProjectsSettings">
+ <GradleProjectSettings>
+ <option name="distributionType" value="DEFAULT_WRAPPED" />
+ <option name="externalProjectPath" value="$PROJECT_DIR$" />
+ <option name="modules">
+ <set>
+ <option value="$PROJECT_DIR$" />
+ <option value="$PROJECT_DIR$/app" />
+ </set>
+ </option>
+ </GradleProjectSettings>
+ </option>
+ </component>
+</project>
+
diff --git a/android/experimental/LOAndroid/.idea/libraries/appcompat_v7_19_1_0.xml b/android/experimental/LOAndroid/.idea/libraries/appcompat_v7_19_1_0.xml
new file mode 100644
index 000000000000..970e5fa480a9
--- /dev/null
+++ b/android/experimental/LOAndroid/.idea/libraries/appcompat_v7_19_1_0.xml
@@ -0,0 +1,10 @@
+<component name="libraryTable">
+ <library name="appcompat-v7-19.1.0">
+ <CLASSES>
+ <root url="jar://$PROJECT_DIR$/app/build/exploded-aar/com.android.support/appcompat-v7/19.1.0/classes.jar!/" />
+ <root url="file://$PROJECT_DIR$/app/build/exploded-aar/com.android.support/appcompat-v7/19.1.0/res" />
+ </CLASSES>
+ <JAVADOC />
+ <SOURCES />
+ </library>
+</component> \ No newline at end of file
diff --git a/android/experimental/LOAndroid/.idea/libraries/support_v4_19_1_0.xml b/android/experimental/LOAndroid/.idea/libraries/support_v4_19_1_0.xml
new file mode 100644
index 000000000000..1ca1ac68ad96
--- /dev/null
+++ b/android/experimental/LOAndroid/.idea/libraries/support_v4_19_1_0.xml
@@ -0,0 +1,11 @@
+<component name="libraryTable">
+ <library name="support-v4-19.1.0">
+ <CLASSES>
+ <root url="jar://$USER_HOME$/Programs/android-sdk-linux/extras/android/m2repository/com/android/support/support-v4/19.1.0/support-v4-19.1.0.jar!/" />
+ </CLASSES>
+ <JAVADOC />
+ <SOURCES>
+ <root url="jar://$USER_HOME$/Programs/android-sdk-linux/extras/android/m2repository/com/android/support/support-v4/19.1.0/support-v4-19.1.0-sources.jar!/" />
+ </SOURCES>
+ </library>
+</component> \ No newline at end of file
diff --git a/android/experimental/LOAndroid/.idea/misc.xml b/android/experimental/LOAndroid/.idea/misc.xml
new file mode 100644
index 000000000000..d0225fc0c171
--- /dev/null
+++ b/android/experimental/LOAndroid/.idea/misc.xml
@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="DaemonCodeAnalyzer">
+ <disable_hints />
+ </component>
+ <component name="ProjectInspectionProfilesVisibleTreeState">
+ <entry key="Project Default">
+ <profile-state>
+ <expanded-state>
+ <State>
+ <id />
+ </State>
+ </expanded-state>
+ <selected-state>
+ <State>
+ <id>Abstraction issues</id>
+ </State>
+ </selected-state>
+ </profile-state>
+ </entry>
+ </component>
+ <component name="ProjectLevelVcsManager" settingsEditedManually="false">
+ <OptionsSetting value="true" id="Add" />
+ <OptionsSetting value="true" id="Remove" />
+ <OptionsSetting value="true" id="Checkout" />
+ <OptionsSetting value="true" id="Update" />
+ <OptionsSetting value="true" id="Status" />
+ <OptionsSetting value="true" id="Edit" />
+ <ConfirmationsSetting value="0" id="Add" />
+ <ConfirmationsSetting value="0" id="Remove" />
+ </component>
+ <component name="ProjectRootManager" version="2" languageLevel="JDK_1_6" assert-keyword="true" jdk-15="true" project-jdk-name="1.7" project-jdk-type="JavaSDK">
+ <output url="file://$PROJECT_DIR$/build/classes" />
+ </component>
+ <component name="RunManager">
+ <configuration default="true" type="Remote" factoryName="Remote">
+ <option name="USE_SOCKET_TRANSPORT" value="true" />
+ <option name="SERVER_MODE" value="false" />
+ <option name="SHMEM_ADDRESS" value="javadebug" />
+ <option name="HOST" value="localhost" />
+ <option name="PORT" value="5005" />
+ <method />
+ </configuration>
+ <configuration default="true" type="TestNG" factoryName="TestNG">
+ <module name="" />
+ <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
+ <option name="ALTERNATIVE_JRE_PATH" />
+ <option name="SUITE_NAME" />
+ <option name="PACKAGE_NAME" />
+ <option name="MAIN_CLASS_NAME" />
+ <option name="METHOD_NAME" />
+ <option name="GROUP_NAME" />
+ <option name="TEST_OBJECT" value="CLASS" />
+ <option name="VM_PARAMETERS" value="-ea" />
+ <option name="PARAMETERS" />
+ <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
+ <option name="OUTPUT_DIRECTORY" />
+ <option name="ANNOTATION_TYPE" />
+ <option name="ENV_VARIABLES" />
+ <option name="PASS_PARENT_ENVS" value="true" />
+ <option name="TEST_SEARCH_SCOPE">
+ <value defaultName="moduleWithDependencies" />
+ </option>
+ <option name="USE_DEFAULT_REPORTERS" value="false" />
+ <option name="PROPERTIES_FILE" />
+ <envs />
+ <properties />
+ <listeners />
+ <method />
+ </configuration>
+ <configuration default="true" type="Applet" factoryName="Applet">
+ <module name="" />
+ <option name="MAIN_CLASS_NAME" />
+ <option name="HTML_FILE_NAME" />
+ <option name="HTML_USED" value="false" />
+ <option name="WIDTH" value="400" />
+ <option name="HEIGHT" value="300" />
+ <option name="POLICY_FILE" value="$APPLICATION_HOME_DIR$/bin/appletviewer.policy" />
+ <option name="VM_PARAMETERS" />
+ <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
+ <option name="ALTERNATIVE_JRE_PATH" />
+ <method />
+ </configuration>
+ <configuration default="true" type="Application" factoryName="Application">
+ <option name="MAIN_CLASS_NAME" />
+ <option name="VM_PARAMETERS" />
+ <option name="PROGRAM_PARAMETERS" />
+ <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
+ <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
+ <option name="ALTERNATIVE_JRE_PATH" />
+ <option name="ENABLE_SWING_INSPECTOR" value="false" />
+ <option name="ENV_VARIABLES" />
+ <option name="PASS_PARENT_ENVS" value="true" />
+ <module name="" />
+ <envs />
+ <method />
+ </configuration>
+ <configuration default="true" type="JUnit" factoryName="JUnit">
+ <module name="" />
+ <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
+ <option name="ALTERNATIVE_JRE_PATH" />
+ <option name="PACKAGE_NAME" />
+ <option name="MAIN_CLASS_NAME" />
+ <option name="METHOD_NAME" />
+ <option name="TEST_OBJECT" value="class" />
+ <option name="VM_PARAMETERS" value="-ea" />
+ <option name="PARAMETERS" />
+ <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
+ <option name="ENV_VARIABLES" />
+ <option name="PASS_PARENT_ENVS" value="true" />
+ <option name="TEST_SEARCH_SCOPE">
+ <value defaultName="moduleWithDependencies" />
+ </option>
+ <envs />
+ <patterns />
+ <method />
+ </configuration>
+ <list size="0" />
+ <configuration name="&lt;template&gt;" type="#org.jetbrains.idea.devkit.run.PluginConfigurationType" default="true" selected="false">
+ <option name="VM_PARAMETERS" value="-Xmx512m -Xms256m -XX:MaxPermSize=250m -ea" />
+ </configuration>
+ <configuration name="&lt;template&gt;" type="WebApp" default="true" selected="false">
+ <Host>localhost</Host>
+ <Port>5050</Port>
+ </configuration>
+ </component>
+ <component name="masterDetails">
+ <states>
+ <state key="ScopeChooserConfigurable.UI">
+ <settings>
+ <splitter-proportions>
+ <option name="proportions">
+ <list>
+ <option value="0.2" />
+ </list>
+ </option>
+ </splitter-proportions>
+ </settings>
+ </state>
+ </states>
+ </component>
+</project>
+
diff --git a/android/experimental/LOAndroid/.idea/modules.xml b/android/experimental/LOAndroid/.idea/modules.xml
new file mode 100644
index 000000000000..f08135d5d6bf
--- /dev/null
+++ b/android/experimental/LOAndroid/.idea/modules.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ProjectModuleManager">
+ <modules>
+ <module fileurl="file://$PROJECT_DIR$/LOAndroid.iml" filepath="$PROJECT_DIR$/LOAndroid.iml" />
+ <module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
+ </modules>
+ </component>
+</project>
+
diff --git a/android/experimental/LOAndroid/.idea/scopes/scope_settings.xml b/android/experimental/LOAndroid/.idea/scopes/scope_settings.xml
new file mode 100644
index 000000000000..922003b8433b
--- /dev/null
+++ b/android/experimental/LOAndroid/.idea/scopes/scope_settings.xml
@@ -0,0 +1,5 @@
+<component name="DependencyValidationManager">
+ <state>
+ <option name="SKIP_IMPORT_STATEMENTS" value="false" />
+ </state>
+</component> \ No newline at end of file
diff --git a/android/experimental/LOAndroid/.idea/vcs.xml b/android/experimental/LOAndroid/.idea/vcs.xml
new file mode 100644
index 000000000000..def6a6a18457
--- /dev/null
+++ b/android/experimental/LOAndroid/.idea/vcs.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="VcsDirectoryMappings">
+ <mapping directory="" vcs="" />
+ </component>
+</project>
+
diff --git a/android/experimental/LOAndroid/LOAndroid.iml b/android/experimental/LOAndroid/LOAndroid.iml
new file mode 100644
index 000000000000..edb62a65fa47
--- /dev/null
+++ b/android/experimental/LOAndroid/LOAndroid.iml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
+ <exclude-output />
+ <content url="file://$MODULE_DIR$">
+ <excludeFolder url="file://$MODULE_DIR$/.gradle" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ </component>
+</module>
+
diff --git a/android/experimental/LOAndroid/app/.gitignore b/android/experimental/LOAndroid/app/.gitignore
new file mode 100644
index 000000000000..796b96d1c402
--- /dev/null
+++ b/android/experimental/LOAndroid/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/android/experimental/LOAndroid/app/app.iml b/android/experimental/LOAndroid/app/app.iml
new file mode 100644
index 000000000000..d3a780b91527
--- /dev/null
+++ b/android/experimental/LOAndroid/app/app.iml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="LOAndroid" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
+ <component name="FacetManager">
+ <facet type="android-gradle" name="Android-Gradle">
+ <configuration>
+ <option name="GRADLE_PROJECT_PATH" value=":app" />
+ </configuration>
+ </facet>
+ <facet type="android" name="Android">
+ <configuration>
+ <option name="SELECTED_BUILD_VARIANT" value="debug" />
+ <option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
+ <option name="COMPILE_JAVA_TASK_NAME" value="compileDebugJava" />
+ <option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugTest" />
+ <option name="SOURCE_GEN_TASK_NAME" value="generateDebugSources" />
+ <option name="ALLOW_USER_CONFIGURATION" value="false" />
+ <option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
+ <option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
+ <option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
+ <option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
+ </configuration>
+ </facet>
+ </component>
+ <component name="NewModuleRootManager" inherit-compiler-output="false">
+ <output url="file://$MODULE_DIR$/build/classes/debug" />
+ <exclude-output />
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/build/source/r/debug" isTestSource="false" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/source/aidl/debug" isTestSource="false" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/source/buildConfig/debug" isTestSource="false" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/source/rs/debug" isTestSource="false" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/res/rs/debug" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/build/source/r/test/debug" isTestSource="true" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/source/aidl/test/debug" isTestSource="true" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/source/buildConfig/test/debug" isTestSource="true" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/source/rs/test/debug" isTestSource="true" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/res/rs/test/debug" type="java-test-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/assets" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/assets" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
+ <excludeFolder url="file://$MODULE_DIR$/build/apk" />
+ <excludeFolder url="file://$MODULE_DIR$/build/assets" />
+ <excludeFolder url="file://$MODULE_DIR$/build/bundles" />
+ <excludeFolder url="file://$MODULE_DIR$/build/classes" />
+ <excludeFolder url="file://$MODULE_DIR$/build/dependency-cache" />
+ <excludeFolder url="file://$MODULE_DIR$/build/incremental" />
+ <excludeFolder url="file://$MODULE_DIR$/build/libs" />
+ <excludeFolder url="file://$MODULE_DIR$/build/manifests" />
+ <excludeFolder url="file://$MODULE_DIR$/build/res" />
+ <excludeFolder url="file://$MODULE_DIR$/build/symbols" />
+ <excludeFolder url="file://$MODULE_DIR$/build/tmp" />
+ </content>
+ <orderEntry type="jdk" jdkName="Android API 19 Platform" jdkType="Android SDK" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="library" exported="" name="appcompat-v7-19.1.0" level="project" />
+ <orderEntry type="library" exported="" name="support-v4-19.1.0" level="project" />
+ </component>
+</module>
+
diff --git a/android/experimental/LOAndroid/app/build.gradle b/android/experimental/LOAndroid/app/build.gradle
new file mode 100644
index 000000000000..7e98dd4c48c9
--- /dev/null
+++ b/android/experimental/LOAndroid/app/build.gradle
@@ -0,0 +1,24 @@
+apply plugin: 'android'
+
+android {
+ compileSdkVersion 19
+ buildToolsVersion "19.1.0"
+
+ defaultConfig {
+ minSdkVersion 15
+ targetSdkVersion 19
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ runProguard false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+ }
+ }
+}
+
+dependencies {
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ compile 'com.android.support:appcompat-v7:19.+'
+}
diff --git a/android/experimental/LOAndroid/app/proguard-rules.txt b/android/experimental/LOAndroid/app/proguard-rules.txt
new file mode 100644
index 000000000000..0b0be289afc1
--- /dev/null
+++ b/android/experimental/LOAndroid/app/proguard-rules.txt
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /home/quikee/Programs/android-sdk-linux/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#} \ No newline at end of file
diff --git a/android/experimental/LOAndroid/app/src/main/AndroidManifest.xml b/android/experimental/LOAndroid/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000000..3120a69b4247
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.libreoffice" >
+
+ <!-- App requires OpenGL ES 2.0 -->
+ <uses-feature android:glEsVersion="0x00020000" android:required="true" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:hardwareAccelerated="true"
+ android:theme="@style/AppTheme" >
+ <activity
+ android:name="org.libreoffice.MainActivity"
+ android:label="@string/app_name" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/libreoffice/LOKitShell.java b/android/experimental/LOAndroid/app/src/main/java/org/libreoffice/LOKitShell.java
new file mode 100644
index 000000000000..cd429d66a2ad
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/libreoffice/LOKitShell.java
@@ -0,0 +1,22 @@
+package org.libreoffice;
+
+
+import org.mozilla.gecko.gfx.LayerView;
+
+public class LOKitShell {
+ public static int getDpi() {
+ return 96;
+ }
+
+ public static int getScreenDepth() {
+ return 24;
+ }
+
+ public static LayerView getLayerView() {
+ return null;
+ }
+
+ public static float computeRenderIntegrity() {
+ return 0.0f;
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/libreoffice/MainActivity.java b/android/experimental/LOAndroid/app/src/main/java/org/libreoffice/MainActivity.java
new file mode 100644
index 000000000000..1963ad2c2b43
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/libreoffice/MainActivity.java
@@ -0,0 +1,36 @@
+package org.libreoffice;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+
+
+public class MainActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ }
+
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.main, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Handle action bar item clicks here. The action bar will
+ // automatically handle clicks on the Home/Up button, so long
+ // as you specify a parent activity in AndroidManifest.xml.
+ int id = item.getItemId();
+ if (id == R.id.action_settings) {
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/libreoffice/MainLayerView.java b/android/experimental/LOAndroid/app/src/main/java/org/libreoffice/MainLayerView.java
new file mode 100644
index 000000000000..5721df2806b3
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/libreoffice/MainLayerView.java
@@ -0,0 +1,26 @@
+package org.libreoffice;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.AttributeSet;
+
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.util.ThreadUtils;
+
+public class MainLayerView extends LayerView {
+
+ public MainLayerView(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public MainLayerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ private void init(Context context) {
+ ThreadUtils.setUiThread(Thread.currentThread(), new Handler());
+ }
+
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/TouchEventInterceptor.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/TouchEventInterceptor.java
new file mode 100644
index 000000000000..41a71dfa5f88
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/TouchEventInterceptor.java
@@ -0,0 +1,14 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import android.view.MotionEvent;
+import android.view.View;
+
+public interface TouchEventInterceptor extends View.OnTouchListener {
+ /** Override this method for a chance to consume events before the view or its children */
+ public boolean onInterceptTouchEvent(View view, MotionEvent event);
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/ZoomConstraints.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/ZoomConstraints.java
new file mode 100644
index 000000000000..40d1817c9301
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/ZoomConstraints.java
@@ -0,0 +1,46 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public final class ZoomConstraints {
+ private final boolean mAllowZoom;
+ private final float mDefaultZoom;
+ private final float mMinZoom;
+ private final float mMaxZoom;
+
+ public ZoomConstraints(boolean allowZoom) {
+ mAllowZoom = allowZoom;
+ mDefaultZoom = 0.0f;
+ mMinZoom = 0.0f;
+ mMaxZoom = 0.0f;
+ }
+
+ ZoomConstraints(JSONObject message) throws JSONException {
+ mAllowZoom = message.getBoolean("allowZoom");
+ mDefaultZoom = (float)message.getDouble("defaultZoom");
+ mMinZoom = (float)message.getDouble("minZoom");
+ mMaxZoom = (float)message.getDouble("maxZoom");
+ }
+
+ public final boolean getAllowZoom() {
+ return mAllowZoom;
+ }
+
+ public final float getDefaultZoom() {
+ return mDefaultZoom;
+ }
+
+ public final float getMinZoom() {
+ return mMinZoom;
+ }
+
+ public final float getMaxZoom() {
+ return mMaxZoom;
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/Axis.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/Axis.java
new file mode 100644
index 000000000000..103ee5173539
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/Axis.java
@@ -0,0 +1,420 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+//import org.mozilla.gecko.GeckoAppShell;
+//import org.mozilla.gecko.PrefsHelper;
+import org.libreoffice.LOKitShell;
+import org.mozilla.gecko.util.FloatUtils;
+
+import org.json.JSONArray;
+
+import android.util.Log;
+import android.view.View;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This class represents the physics for one axis of movement (i.e. either
+ * horizontal or vertical). It tracks the different properties of movement
+ * like displacement, velocity, viewport dimensions, etc. pertaining to
+ * a particular axis.
+ */
+abstract class Axis {
+ private static final String LOGTAG = "GeckoAxis";
+
+ private static final String PREF_SCROLLING_FRICTION_SLOW = "ui.scrolling.friction_slow";
+ private static final String PREF_SCROLLING_FRICTION_FAST = "ui.scrolling.friction_fast";
+ private static final String PREF_SCROLLING_MAX_EVENT_ACCELERATION = "ui.scrolling.max_event_acceleration";
+ private static final String PREF_SCROLLING_OVERSCROLL_DECEL_RATE = "ui.scrolling.overscroll_decel_rate";
+ private static final String PREF_SCROLLING_OVERSCROLL_SNAP_LIMIT = "ui.scrolling.overscroll_snap_limit";
+ private static final String PREF_SCROLLING_MIN_SCROLLABLE_DISTANCE = "ui.scrolling.min_scrollable_distance";
+
+ // This fraction of velocity remains after every animation frame when the velocity is low.
+ private static float FRICTION_SLOW;
+ // This fraction of velocity remains after every animation frame when the velocity is high.
+ private static float FRICTION_FAST;
+ // Below this velocity (in pixels per frame), the friction starts increasing from FRICTION_FAST
+ // to FRICTION_SLOW.
+ private static float VELOCITY_THRESHOLD;
+ // The maximum velocity change factor between events, per ms, in %.
+ // Direction changes are excluded.
+ private static float MAX_EVENT_ACCELERATION;
+
+ // The rate of deceleration when the surface has overscrolled.
+ private static float OVERSCROLL_DECEL_RATE;
+ // The percentage of the surface which can be overscrolled before it must snap back.
+ private static float SNAP_LIMIT;
+
+ // The minimum amount of space that must be present for an axis to be considered scrollable,
+ // in pixels.
+ private static float MIN_SCROLLABLE_DISTANCE;
+
+ private static float getFloatPref(Map<String, Integer> prefs, String prefName, int defaultValue) {
+ Integer value = (prefs == null ? null : prefs.get(prefName));
+ return (float)(value == null || value < 0 ? defaultValue : value) / 1000f;
+ }
+
+ private static int getIntPref(Map<String, Integer> prefs, String prefName, int defaultValue) {
+ Integer value = (prefs == null ? null : prefs.get(prefName));
+ return (value == null || value < 0 ? defaultValue : value);
+ }
+
+ static void initPrefs() {
+ final String[] prefs = { PREF_SCROLLING_FRICTION_FAST,
+ PREF_SCROLLING_FRICTION_SLOW,
+ PREF_SCROLLING_MAX_EVENT_ACCELERATION,
+ PREF_SCROLLING_OVERSCROLL_DECEL_RATE,
+ PREF_SCROLLING_OVERSCROLL_SNAP_LIMIT,
+ PREF_SCROLLING_MIN_SCROLLABLE_DISTANCE };
+
+ /*PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() {
+ Map<String, Integer> mPrefs = new HashMap<String, Integer>();
+
+ @Override public void prefValue(String name, int value) {
+ mPrefs.put(name, value);
+ }
+
+ @Override public void finish() {
+ setPrefs(mPrefs);
+ }
+ });*/
+ }
+
+ static final float MS_PER_FRAME = 1000.0f / 60.0f;
+ static final long NS_PER_FRAME = Math.round(1000000000f / 60f);
+ private static final float FRAMERATE_MULTIPLIER = (1000f/60f) / MS_PER_FRAME;
+ private static final int FLING_VELOCITY_POINTS = 8;
+
+ // The values we use for friction are based on a 16.6ms frame, adjust them to currentNsPerFrame:
+ static float getFrameAdjustedFriction(float baseFriction, long currentNsPerFrame) {
+ float framerateMultiplier = (float)currentNsPerFrame / NS_PER_FRAME;
+ return (float)Math.pow(Math.E, (Math.log(baseFriction) / framerateMultiplier));
+ }
+
+ static void setPrefs(Map<String, Integer> prefs) {
+ FRICTION_SLOW = getFloatPref(prefs, PREF_SCROLLING_FRICTION_SLOW, 850);
+ FRICTION_FAST = getFloatPref(prefs, PREF_SCROLLING_FRICTION_FAST, 970);
+ VELOCITY_THRESHOLD = 10 / FRAMERATE_MULTIPLIER;
+ MAX_EVENT_ACCELERATION = getFloatPref(prefs, PREF_SCROLLING_MAX_EVENT_ACCELERATION, /*GeckoAppShell.getDpi()*/ LOKitShell.getDpi() > 300 ? 100 : 40);
+ OVERSCROLL_DECEL_RATE = getFloatPref(prefs, PREF_SCROLLING_OVERSCROLL_DECEL_RATE, 40);
+ SNAP_LIMIT = getFloatPref(prefs, PREF_SCROLLING_OVERSCROLL_SNAP_LIMIT, 300);
+ MIN_SCROLLABLE_DISTANCE = getFloatPref(prefs, PREF_SCROLLING_MIN_SCROLLABLE_DISTANCE, 500);
+ Log.i(LOGTAG, "Prefs: " + FRICTION_SLOW + "," + FRICTION_FAST + "," + VELOCITY_THRESHOLD + ","
+ + MAX_EVENT_ACCELERATION + "," + OVERSCROLL_DECEL_RATE + "," + SNAP_LIMIT + "," + MIN_SCROLLABLE_DISTANCE);
+ }
+
+ static {
+ // set the scrolling parameters to default values on startup
+ setPrefs(null);
+ }
+
+ private enum FlingStates {
+ STOPPED,
+ PANNING,
+ FLINGING,
+ }
+
+ private enum Overscroll {
+ NONE,
+ MINUS, // Overscrolled in the negative direction
+ PLUS, // Overscrolled in the positive direction
+ BOTH, // Overscrolled in both directions (page is zoomed to smaller than screen)
+ }
+
+ private final SubdocumentScrollHelper mSubscroller;
+
+ private int mOverscrollMode; /* Default to only overscrolling if we're allowed to scroll in a direction */
+ private float mFirstTouchPos; /* Position of the first touch event on the current drag. */
+ private float mTouchPos; /* Position of the most recent touch event on the current drag. */
+ private float mLastTouchPos; /* Position of the touch event before touchPos. */
+ private float mVelocity; /* Velocity in this direction; pixels per animation frame. */
+ private float[] mRecentVelocities; /* Circular buffer of recent velocities since last touch start. */
+ private int mRecentVelocityCount; /* Number of values put into mRecentVelocities (unbounded). */
+ private boolean mScrollingDisabled; /* Whether movement on this axis is locked. */
+ private boolean mDisableSnap; /* Whether overscroll snapping is disabled. */
+ private float mDisplacement;
+
+ private FlingStates mFlingState = FlingStates.STOPPED; /* The fling state we're in on this axis. */
+
+ protected abstract float getOrigin();
+ protected abstract float getViewportLength();
+ protected abstract float getPageStart();
+ protected abstract float getPageLength();
+ protected abstract float getMarginStart();
+ protected abstract float getMarginEnd();
+ protected abstract boolean marginsHidden();
+
+ Axis(SubdocumentScrollHelper subscroller) {
+ mSubscroller = subscroller;
+ mOverscrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS;
+ mRecentVelocities = new float[FLING_VELOCITY_POINTS];
+ }
+
+ // Implementors can override these to show effects when the axis overscrolls
+ protected void overscrollFling(float velocity) { }
+ protected void overscrollPan(float displacement) { }
+
+ public void setOverScrollMode(int overscrollMode) {
+ mOverscrollMode = overscrollMode;
+ }
+
+ public int getOverScrollMode() {
+ return mOverscrollMode;
+ }
+
+ private float getViewportEnd() {
+ return getOrigin() + getViewportLength();
+ }
+
+ private float getPageEnd() {
+ return getPageStart() + getPageLength();
+ }
+
+ void startTouch(float pos) {
+ mVelocity = 0.0f;
+ mScrollingDisabled = false;
+ mFirstTouchPos = mTouchPos = mLastTouchPos = pos;
+ mRecentVelocityCount = 0;
+ }
+
+ float panDistance(float currentPos) {
+ return currentPos - mFirstTouchPos;
+ }
+
+ void setScrollingDisabled(boolean disabled) {
+ mScrollingDisabled = disabled;
+ }
+
+ void saveTouchPos() {
+ mLastTouchPos = mTouchPos;
+ }
+
+ void updateWithTouchAt(float pos, float timeDelta) {
+ float newVelocity = (mTouchPos - pos) / timeDelta * MS_PER_FRAME;
+
+ mRecentVelocities[mRecentVelocityCount % FLING_VELOCITY_POINTS] = newVelocity;
+ mRecentVelocityCount++;
+
+ // If there's a direction change, or current velocity is very low,
+ // allow setting of the velocity outright. Otherwise, use the current
+ // velocity and a maximum change factor to set the new velocity.
+ boolean curVelocityIsLow = Math.abs(mVelocity) < 1.0f / FRAMERATE_MULTIPLIER;
+ boolean directionChange = (mVelocity > 0) != (newVelocity > 0);
+ if (curVelocityIsLow || (directionChange && !FloatUtils.fuzzyEquals(newVelocity, 0.0f))) {
+ mVelocity = newVelocity;
+ } else {
+ float maxChange = Math.abs(mVelocity * timeDelta * MAX_EVENT_ACCELERATION);
+ mVelocity = Math.min(mVelocity + maxChange, Math.max(mVelocity - maxChange, newVelocity));
+ }
+
+ mTouchPos = pos;
+ }
+
+ boolean overscrolled() {
+ return getOverscroll() != Overscroll.NONE;
+ }
+
+ private Overscroll getOverscroll() {
+ boolean minus = (getOrigin() < getPageStart());
+ boolean plus = (getViewportEnd() > getPageEnd());
+ if (minus && plus) {
+ return Overscroll.BOTH;
+ } else if (minus) {
+ return Overscroll.MINUS;
+ } else if (plus) {
+ return Overscroll.PLUS;
+ } else {
+ return Overscroll.NONE;
+ }
+ }
+
+ // Returns the amount that the page has been overscrolled. If the page hasn't been
+ // overscrolled on this axis, returns 0.
+ private float getExcess() {
+ switch (getOverscroll()) {
+ case MINUS: return getPageStart() - getOrigin();
+ case PLUS: return getViewportEnd() - getPageEnd();
+ case BOTH: return (getViewportEnd() - getPageEnd()) + (getPageStart() - getOrigin());
+ default: return 0.0f;
+ }
+ }
+
+ /*
+ * Returns true if the page is zoomed in to some degree along this axis such that scrolling is
+ * possible and this axis has not been scroll locked while panning. Otherwise, returns false.
+ */
+ boolean scrollable() {
+ // If we're scrolling a subdocument, ignore the viewport length restrictions (since those
+ // apply to the top-level document) and only take into account axis locking.
+ if (mSubscroller.scrolling()) {
+ return !mScrollingDisabled;
+ }
+
+ // if we are axis locked, return false
+ if (mScrollingDisabled) {
+ return false;
+ }
+
+ // if there are margins on this axis but they are currently hidden,
+ // we must be able to scroll in order to make them visible, so allow
+ // scrolling in that case
+ if (marginsHidden()) {
+ return true;
+ }
+
+ // there is scrollable space, and we're not disabled, or the document fits the viewport
+ // but we always allow overscroll anyway
+ return getViewportLength() <= getPageLength() - MIN_SCROLLABLE_DISTANCE ||
+ getOverScrollMode() == View.OVER_SCROLL_ALWAYS;
+ }
+
+ /*
+ * Returns the resistance, as a multiplier, that should be taken into account when
+ * tracking or pinching.
+ */
+ float getEdgeResistance(boolean forPinching) {
+ float excess = getExcess();
+ if (excess > 0.0f && (getOverscroll() == Overscroll.BOTH || !forPinching)) {
+ // excess can be greater than viewport length, but the resistance
+ // must never drop below 0.0
+ return Math.max(0.0f, SNAP_LIMIT - excess / getViewportLength());
+ }
+ return 1.0f;
+ }
+
+ /* Returns the velocity. If the axis is locked, returns 0. */
+ float getRealVelocity() {
+ return scrollable() ? mVelocity : 0f;
+ }
+
+ void startPan() {
+ mFlingState = FlingStates.PANNING;
+ }
+
+ private float calculateFlingVelocity() {
+ int usablePoints = Math.min(mRecentVelocityCount, FLING_VELOCITY_POINTS);
+ if (usablePoints <= 1) {
+ return mVelocity;
+ }
+ float average = 0;
+ for (int i = 0; i < usablePoints; i++) {
+ average += mRecentVelocities[i];
+ }
+ return average / usablePoints;
+ }
+
+ void startFling(boolean stopped) {
+ mDisableSnap = mSubscroller.scrolling();
+
+ if (stopped) {
+ mFlingState = FlingStates.STOPPED;
+ } else {
+ mVelocity = calculateFlingVelocity();
+ mFlingState = FlingStates.FLINGING;
+ }
+ }
+
+ /* Advances a fling animation by one step. */
+ boolean advanceFling(long realNsPerFrame) {
+ if (mFlingState != FlingStates.FLINGING) {
+ return false;
+ }
+ if (mSubscroller.scrolling() && !mSubscroller.lastScrollSucceeded()) {
+ // if the subdocument stopped scrolling, it's because it reached the end
+ // of the subdocument. we don't do overscroll on subdocuments, so there's
+ // no point in continuing this fling.
+ return false;
+ }
+
+ float excess = getExcess();
+ Overscroll overscroll = getOverscroll();
+ boolean decreasingOverscroll = false;
+ if ((overscroll == Overscroll.MINUS && mVelocity > 0) ||
+ (overscroll == Overscroll.PLUS && mVelocity < 0))
+ {
+ decreasingOverscroll = true;
+ }
+
+ if (mDisableSnap || FloatUtils.fuzzyEquals(excess, 0.0f) || decreasingOverscroll) {
+ // If we aren't overscrolled, just apply friction.
+ if (Math.abs(mVelocity) >= VELOCITY_THRESHOLD) {
+ mVelocity *= getFrameAdjustedFriction(FRICTION_FAST, realNsPerFrame);
+ } else {
+ float t = mVelocity / VELOCITY_THRESHOLD;
+ mVelocity *= FloatUtils.interpolate(getFrameAdjustedFriction(FRICTION_SLOW, realNsPerFrame),
+ getFrameAdjustedFriction(FRICTION_FAST, realNsPerFrame), t);
+ }
+ } else {
+ // Otherwise, decrease the velocity linearly.
+ float elasticity = 1.0f - excess / (getViewportLength() * SNAP_LIMIT);
+ float overscrollDecelRate = getFrameAdjustedFriction(OVERSCROLL_DECEL_RATE, realNsPerFrame);
+ if (overscroll == Overscroll.MINUS) {
+ mVelocity = Math.min((mVelocity + overscrollDecelRate) * elasticity, 0.0f);
+ } else { // must be Overscroll.PLUS
+ mVelocity = Math.max((mVelocity - overscrollDecelRate) * elasticity, 0.0f);
+ }
+ }
+
+ return true;
+ }
+
+ void stopFling() {
+ mVelocity = 0.0f;
+ mFlingState = FlingStates.STOPPED;
+ }
+
+ // Performs displacement of the viewport position according to the current velocity.
+ void displace() {
+ // if this isn't scrollable just return
+ if (!scrollable())
+ return;
+
+ if (mFlingState == FlingStates.PANNING)
+ mDisplacement += (mLastTouchPos - mTouchPos) * getEdgeResistance(false);
+ else
+ mDisplacement += mVelocity * getEdgeResistance(false);
+
+ // if overscroll is disabled and we're trying to overscroll, reset the displacement
+ // to remove any excess. Using getExcess alone isn't enough here since it relies on
+ // getOverscroll which doesn't take into account any new displacment being applied.
+ // If we using a subscroller, we don't want to alter the scrolling being done
+ if (getOverScrollMode() == View.OVER_SCROLL_NEVER && !mSubscroller.scrolling()) {
+ float originalDisplacement = mDisplacement;
+
+ if (mDisplacement + getOrigin() < getPageStart() - getMarginStart()) {
+ mDisplacement = getPageStart() - getMarginStart() - getOrigin();
+ } else if (mDisplacement + getViewportEnd() > getPageEnd() + getMarginEnd()) {
+ mDisplacement = getPageEnd() - getMarginEnd() - getViewportEnd();
+ }
+
+ // Return the amount of overscroll so that the overscroll controller can draw it for us
+ if (originalDisplacement != mDisplacement) {
+ if (mFlingState == FlingStates.FLINGING) {
+ overscrollFling(mVelocity / MS_PER_FRAME * 1000);
+ stopFling();
+ } else if (mFlingState == FlingStates.PANNING) {
+ overscrollPan(originalDisplacement - mDisplacement);
+ }
+ }
+ }
+ }
+
+ float resetDisplacement() {
+ float d = mDisplacement;
+ mDisplacement = 0.0f;
+ return d;
+ }
+
+ void setAutoscrollVelocity(float velocity) {
+ if (mFlingState != FlingStates.STOPPED) {
+ Log.e(LOGTAG, "Setting autoscroll velocity while in a fling is not allowed!");
+ return;
+ }
+ mVelocity = velocity;
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/BitmapUtils.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/BitmapUtils.java
new file mode 100644
index 000000000000..9dba802f6d38
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/BitmapUtils.java
@@ -0,0 +1,368 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.UiAsyncTask;
+//import org.mozilla.gecko.util.GeckoJarReader;
+//import org.mozilla.gecko.R;
+
+import org.libreoffice.R;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.BitmapDrawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.util.Base64;
+import android.util.Log;
+import android.text.TextUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.lang.NoSuchFieldException;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public final class BitmapUtils {
+ private static final String LOGTAG = "GeckoBitmapUtils";
+
+ private BitmapUtils() {}
+
+ public interface BitmapLoader {
+ public void onBitmapFound(Drawable d);
+ }
+
+ public static void getDrawable(final Context context, final String data, final BitmapLoader loader) {
+ if (TextUtils.isEmpty(data)) {
+ loader.onBitmapFound(null);
+ return;
+ }
+
+ if (data.startsWith("data")) {
+ BitmapDrawable d = new BitmapDrawable(context.getResources(), getBitmapFromDataURI(data));
+ loader.onBitmapFound(d);
+ return;
+ }
+
+ if (data.startsWith("jar:") || data.startsWith("file://")) {
+ (new UiAsyncTask<Void, Void, Drawable>(ThreadUtils.getBackgroundHandler()) {
+ @Override
+ public Drawable doInBackground(Void... params) {
+ try {
+ //if (data.startsWith("jar:jar")) {
+ // return GeckoJarReader.getBitmapDrawable(context.getResources(), data);
+ //}
+
+ // Don't attempt to validate the JAR signature when loading an add-on icon
+ //if (data.startsWith("jar:file")) {
+ // return GeckoJarReader.getBitmapDrawable(context.getResources(), Uri.decode(data));
+ //}
+
+ URL url = new URL(data);
+ InputStream is = (InputStream) url.getContent();
+ try {
+ return Drawable.createFromStream(is, "src");
+ } finally {
+ is.close();
+ }
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Unable to set icon", e);
+ }
+ return null;
+ }
+
+ @Override
+ public void onPostExecute(Drawable drawable) {
+ loader.onBitmapFound(drawable);
+ }
+ }).execute();
+ return;
+ }
+
+ if(data.startsWith("-moz-icon://")) {
+ Uri imageUri = Uri.parse(data);
+ String resource = imageUri.getSchemeSpecificPart();
+ resource = resource.substring(resource.lastIndexOf('/') + 1);
+
+ try {
+ Drawable d = context.getPackageManager().getApplicationIcon(resource);
+ loader.onBitmapFound(d);
+ } catch(Exception ex) { }
+
+ return;
+ }
+
+ if(data.startsWith("drawable://")) {
+ Uri imageUri = Uri.parse(data);
+ int id = getResource(imageUri, R.drawable.ic_status_logo);
+ Drawable d = context.getResources().getDrawable(id);
+
+ loader.onBitmapFound(d);
+ return;
+ }
+
+ loader.onBitmapFound(null);
+ }
+
+ public static Bitmap decodeByteArray(byte[] bytes) {
+ return decodeByteArray(bytes, null);
+ }
+
+ public static Bitmap decodeByteArray(byte[] bytes, BitmapFactory.Options options) {
+ return decodeByteArray(bytes, 0, bytes.length, options);
+ }
+
+ public static Bitmap decodeByteArray(byte[] bytes, int offset, int length) {
+ return decodeByteArray(bytes, offset, length, null);
+ }
+
+ public static Bitmap decodeByteArray(byte[] bytes, int offset, int length, BitmapFactory.Options options) {
+ if (bytes.length <= 0) {
+ throw new IllegalArgumentException("bytes.length " + bytes.length
+ + " must be a positive number");
+ }
+
+ Bitmap bitmap = null;
+ try {
+ bitmap = BitmapFactory.decodeByteArray(bytes, offset, length, options);
+ } catch (OutOfMemoryError e) {
+ Log.e(LOGTAG, "decodeByteArray(bytes.length=" + bytes.length
+ + ", options= " + options + ") OOM!", e);
+ return null;
+ }
+
+ if (bitmap == null) {
+ Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned null");
+ return null;
+ }
+
+ if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
+ Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned "
+ + "a bitmap with dimensions " + bitmap.getWidth()
+ + "x" + bitmap.getHeight());
+ return null;
+ }
+
+ return bitmap;
+ }
+
+ public static Bitmap decodeStream(InputStream inputStream) {
+ try {
+ return BitmapFactory.decodeStream(inputStream);
+ } catch (OutOfMemoryError e) {
+ Log.e(LOGTAG, "decodeStream() OOM!", e);
+ return null;
+ }
+ }
+
+ public static Bitmap decodeUrl(Uri uri) {
+ return decodeUrl(uri.toString());
+ }
+
+ public static Bitmap decodeUrl(String urlString) {
+ URL url;
+
+ try {
+ url = new URL(urlString);
+ } catch(MalformedURLException e) {
+ Log.w(LOGTAG, "decodeUrl: malformed URL " + urlString);
+ return null;
+ }
+
+ return decodeUrl(url);
+ }
+
+ public static Bitmap decodeUrl(URL url) {
+ InputStream stream = null;
+
+ try {
+ stream = url.openStream();
+ } catch(IOException e) {
+ Log.w(LOGTAG, "decodeUrl: IOException downloading " + url);
+ return null;
+ }
+
+ if (stream == null) {
+ Log.w(LOGTAG, "decodeUrl: stream not found downloading " + url);
+ return null;
+ }
+
+ Bitmap bitmap = decodeStream(stream);
+
+ try {
+ stream.close();
+ } catch(IOException e) {
+ Log.w(LOGTAG, "decodeUrl: IOException closing stream " + url, e);
+ }
+
+ return bitmap;
+ }
+
+ public static Bitmap decodeResource(Context context, int id) {
+ return decodeResource(context, id, null);
+ }
+
+ public static Bitmap decodeResource(Context context, int id, BitmapFactory.Options options) {
+ Resources resources = context.getResources();
+ try {
+ return BitmapFactory.decodeResource(resources, id, options);
+ } catch (OutOfMemoryError e) {
+ Log.e(LOGTAG, "decodeResource() OOM! Resource id=" + id, e);
+ return null;
+ }
+ }
+
+ public static int getDominantColor(Bitmap source) {
+ return getDominantColor(source, true);
+ }
+
+ public static int getDominantColor(Bitmap source, boolean applyThreshold) {
+ if (source == null)
+ return Color.argb(255,255,255,255);
+
+ // Keep track of how many times a hue in a given bin appears in the image.
+ // Hue values range [0 .. 360), so dividing by 10, we get 36 bins.
+ int[] colorBins = new int[36];
+
+ // The bin with the most colors. Initialize to -1 to prevent accidentally
+ // thinking the first bin holds the dominant color.
+ int maxBin = -1;
+
+ // Keep track of sum hue/saturation/value per hue bin, which we'll use to
+ // compute an average to for the dominant color.
+ float[] sumHue = new float[36];
+ float[] sumSat = new float[36];
+ float[] sumVal = new float[36];
+ float[] hsv = new float[3];
+
+ int height = source.getHeight();
+ int width = source.getWidth();
+ int[] pixels = new int[width * height];
+ source.getPixels(pixels, 0, width, 0, 0, width, height);
+ for (int row = 0; row < height; row++) {
+ for (int col = 0; col < width; col++) {
+ int c = pixels[col + row * width];
+ // Ignore pixels with a certain transparency.
+ if (Color.alpha(c) < 128)
+ continue;
+
+ Color.colorToHSV(c, hsv);
+
+ // If a threshold is applied, ignore arbitrarily chosen values for "white" and "black".
+ if (applyThreshold && (hsv[1] <= 0.35f || hsv[2] <= 0.35f))
+ continue;
+
+ // We compute the dominant color by putting colors in bins based on their hue.
+ int bin = (int) Math.floor(hsv[0] / 10.0f);
+
+ // Update the sum hue/saturation/value for this bin.
+ sumHue[bin] = sumHue[bin] + hsv[0];
+ sumSat[bin] = sumSat[bin] + hsv[1];
+ sumVal[bin] = sumVal[bin] + hsv[2];
+
+ // Increment the number of colors in this bin.
+ colorBins[bin]++;
+
+ // Keep track of the bin that holds the most colors.
+ if (maxBin < 0 || colorBins[bin] > colorBins[maxBin])
+ maxBin = bin;
+ }
+ }
+
+ // maxBin may never get updated if the image holds only transparent and/or black/white pixels.
+ if (maxBin < 0)
+ return Color.argb(255,255,255,255);
+
+ // Return a color with the average hue/saturation/value of the bin with the most colors.
+ hsv[0] = sumHue[maxBin]/colorBins[maxBin];
+ hsv[1] = sumSat[maxBin]/colorBins[maxBin];
+ hsv[2] = sumVal[maxBin]/colorBins[maxBin];
+ return Color.HSVToColor(hsv);
+ }
+
+ /**
+ * Decodes a bitmap from a Base64 data URI.
+ *
+ * @param dataURI a Base64-encoded data URI string
+ * @return the decoded bitmap, or null if the data URI is invalid
+ */
+ public static Bitmap getBitmapFromDataURI(String dataURI) {
+ String base64 = dataURI.substring(dataURI.indexOf(',') + 1);
+ try {
+ byte[] raw = Base64.decode(base64, Base64.DEFAULT);
+ return BitmapUtils.decodeByteArray(raw);
+ } catch (Exception e) {
+ Log.e(LOGTAG, "exception decoding bitmap from data URI: " + dataURI, e);
+ }
+ return null;
+ }
+
+ public static Bitmap getBitmapFromDrawable(Drawable drawable) {
+ if (drawable instanceof BitmapDrawable) {
+ return ((BitmapDrawable) drawable).getBitmap();
+ }
+
+ int width = drawable.getIntrinsicWidth();
+ width = width > 0 ? width : 1;
+ int height = drawable.getIntrinsicHeight();
+ height = height > 0 ? height : 1;
+
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+
+ return bitmap;
+ }
+
+ public static int getResource(Uri resourceUrl, int defaultIcon) {
+ int icon = defaultIcon;
+
+ final String scheme = resourceUrl.getScheme();
+ if ("drawable".equals(scheme)) {
+ String resource = resourceUrl.getSchemeSpecificPart();
+ resource = resource.substring(resource.lastIndexOf('/') + 1);
+
+ try {
+ return Integer.parseInt(resource);
+ } catch(NumberFormatException ex) {
+ // This isn't a resource id, try looking for a string
+ }
+
+ try {
+ final Class<R.drawable> drawableClass = R.drawable.class;
+ final Field f = drawableClass.getField(resource);
+ icon = f.getInt(null);
+ } catch (final NoSuchFieldException e1) {
+
+ // just means the resource doesn't exist for fennec. Check in Android resources
+ try {
+ final Class<android.R.drawable> drawableClass = android.R.drawable.class;
+ final Field f = drawableClass.getField(resource);
+ icon = f.getInt(null);
+ } catch (final NoSuchFieldException e2) {
+ // This drawable doesn't seem to exist...
+ } catch(Exception e3) {
+ Log.i(LOGTAG, "Exception getting drawable", e3);
+ }
+
+ } catch (Exception e4) {
+ Log.i(LOGTAG, "Exception getting drawable", e4);
+ }
+
+ resourceUrl = null;
+ }
+ return icon;
+ }
+}
+
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/BufferedCairoImage.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/BufferedCairoImage.java
new file mode 100644
index 000000000000..307f41204256
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/BufferedCairoImage.java
@@ -0,0 +1,69 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import org.mozilla.gecko.mozglue.DirectBufferAllocator;
+
+import android.graphics.Bitmap;
+import android.util.Log;
+
+import java.nio.ByteBuffer;
+
+/** A Cairo image that simply saves a buffer of pixel data. */
+public class BufferedCairoImage extends CairoImage {
+ private ByteBuffer mBuffer;
+ private IntSize mSize;
+ private int mFormat;
+
+ private static String LOGTAG = "GeckoBufferedCairoImage";
+
+ /** Creates a buffered Cairo image from a byte buffer. */
+ public BufferedCairoImage(ByteBuffer inBuffer, int inWidth, int inHeight, int inFormat) {
+ setBuffer(inBuffer, inWidth, inHeight, inFormat);
+ }
+
+ /** Creates a buffered Cairo image from an Android bitmap. */
+ public BufferedCairoImage(Bitmap bitmap) {
+ setBitmap(bitmap);
+ }
+
+ private synchronized void freeBuffer() {
+ mBuffer = DirectBufferAllocator.free(mBuffer);
+ }
+
+ @Override
+ public void destroy() {
+ try {
+ freeBuffer();
+ } catch (Exception ex) {
+ Log.e(LOGTAG, "error clearing buffer: ", ex);
+ }
+ }
+
+ @Override
+ public ByteBuffer getBuffer() { return mBuffer; }
+ @Override
+ public IntSize getSize() { return mSize; }
+ @Override
+ public int getFormat() { return mFormat; }
+
+
+ public void setBuffer(ByteBuffer buffer, int width, int height, int format) {
+ freeBuffer();
+ mBuffer = buffer;
+ mSize = new IntSize(width, height);
+ mFormat = format;
+ }
+
+ public void setBitmap(Bitmap bitmap) {
+ mFormat = CairoUtils.bitmapConfigToCairoFormat(bitmap.getConfig());
+ mSize = new IntSize(bitmap.getWidth(), bitmap.getHeight());
+
+ int bpp = CairoUtils.bitsPerPixelForCairoFormat(mFormat);
+ mBuffer = DirectBufferAllocator.allocate(mSize.getArea() * bpp);
+ bitmap.copyPixelsToBuffer(mBuffer.asIntBuffer());
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/CairoGLInfo.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/CairoGLInfo.java
new file mode 100644
index 000000000000..472c1d29f792
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/CairoGLInfo.java
@@ -0,0 +1,35 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import javax.microedition.khronos.opengles.GL10;
+
+/** Information needed to render Cairo bitmaps using OpenGL ES. */
+public class CairoGLInfo {
+ public final int internalFormat;
+ public final int format;
+ public final int type;
+
+ public CairoGLInfo(int cairoFormat) {
+ switch (cairoFormat) {
+ case CairoImage.FORMAT_ARGB32:
+ internalFormat = format = GL10.GL_RGBA; type = GL10.GL_UNSIGNED_BYTE;
+ break;
+ case CairoImage.FORMAT_RGB24:
+ internalFormat = format = GL10.GL_RGB; type = GL10.GL_UNSIGNED_BYTE;
+ break;
+ case CairoImage.FORMAT_RGB16_565:
+ internalFormat = format = GL10.GL_RGB; type = GL10.GL_UNSIGNED_SHORT_5_6_5;
+ break;
+ case CairoImage.FORMAT_A8:
+ case CairoImage.FORMAT_A1:
+ throw new RuntimeException("Cairo FORMAT_A1 and FORMAT_A8 unsupported");
+ default:
+ throw new RuntimeException("Unknown Cairo format");
+ }
+ }
+}
+
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/CairoImage.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/CairoImage.java
new file mode 100644
index 000000000000..5a18a4bb1995
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/CairoImage.java
@@ -0,0 +1,28 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import java.nio.ByteBuffer;
+
+/*
+ * A bitmap with pixel data in one of the formats that Cairo understands.
+ */
+public abstract class CairoImage {
+ public abstract ByteBuffer getBuffer();
+
+ public abstract void destroy();
+
+ public abstract IntSize getSize();
+ public abstract int getFormat();
+
+ public static final int FORMAT_INVALID = -1;
+ public static final int FORMAT_ARGB32 = 0;
+ public static final int FORMAT_RGB24 = 1;
+ public static final int FORMAT_A8 = 2;
+ public static final int FORMAT_A1 = 3;
+ public static final int FORMAT_RGB16_565 = 4;
+}
+
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/CairoUtils.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/CairoUtils.java
new file mode 100644
index 000000000000..48c449f05e5c
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/CairoUtils.java
@@ -0,0 +1,51 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import android.graphics.Bitmap;
+
+/**
+ * Utility methods useful when displaying Cairo bitmaps using OpenGL ES.
+ */
+public class CairoUtils {
+ private CairoUtils() { /* Don't call me. */ }
+
+ public static int bitsPerPixelForCairoFormat(int cairoFormat) {
+ switch (cairoFormat) {
+ case CairoImage.FORMAT_A1: return 1;
+ case CairoImage.FORMAT_A8: return 8;
+ case CairoImage.FORMAT_RGB16_565: return 16;
+ case CairoImage.FORMAT_RGB24: return 24;
+ case CairoImage.FORMAT_ARGB32: return 32;
+ default:
+ throw new RuntimeException("Unknown Cairo format");
+ }
+ }
+
+ public static int bitmapConfigToCairoFormat(Bitmap.Config config) {
+ if (config == null)
+ return CairoImage.FORMAT_ARGB32; /* Droid Pro fix. */
+
+ switch (config) {
+ case ALPHA_8: return CairoImage.FORMAT_A8;
+ case ARGB_4444: throw new RuntimeException("ARGB_444 unsupported");
+ case ARGB_8888: return CairoImage.FORMAT_ARGB32;
+ case RGB_565: return CairoImage.FORMAT_RGB16_565;
+ default: throw new RuntimeException("Unknown Skia bitmap config");
+ }
+ }
+
+ public static Bitmap.Config cairoFormatTobitmapConfig(int format) {
+ switch (format) {
+ case CairoImage.FORMAT_A8: return Bitmap.Config.ALPHA_8;
+ case CairoImage.FORMAT_ARGB32: return Bitmap.Config.ARGB_8888;
+ case CairoImage.FORMAT_RGB16_565: return Bitmap.Config.RGB_565;
+ default:
+ throw new RuntimeException("Unknown CairoImage format");
+ }
+ }
+}
+
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java
new file mode 100644
index 000000000000..50d0a1c818a9
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java
@@ -0,0 +1,777 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+//import org.mozilla.gecko.GeckoAppShell;
+//import org.mozilla.gecko.PrefsHelper;
+import org.libreoffice.LOKitShell;
+import org.mozilla.gecko.util.FloatUtils;
+
+import org.json.JSONArray;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.FloatMath;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+
+final class DisplayPortCalculator {
+ private static final String LOGTAG = "GeckoDisplayPort";
+ private static final PointF ZERO_VELOCITY = new PointF(0, 0);
+
+ // Keep this in sync with the TILEDLAYERBUFFER_TILE_SIZE defined in gfx/layers/TiledLayerBuffer.h
+ private static final int TILE_SIZE = 256;
+
+ private static final String PREF_DISPLAYPORT_STRATEGY = "gfx.displayport.strategy";
+ private static final String PREF_DISPLAYPORT_FM_MULTIPLIER = "gfx.displayport.strategy_fm.multiplier";
+ private static final String PREF_DISPLAYPORT_FM_DANGER_X = "gfx.displayport.strategy_fm.danger_x";
+ private static final String PREF_DISPLAYPORT_FM_DANGER_Y = "gfx.displayport.strategy_fm.danger_y";
+ private static final String PREF_DISPLAYPORT_VB_MULTIPLIER = "gfx.displayport.strategy_vb.multiplier";
+ private static final String PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_vb.threshold";
+ private static final String PREF_DISPLAYPORT_VB_REVERSE_BUFFER = "gfx.displayport.strategy_vb.reverse_buffer";
+ private static final String PREF_DISPLAYPORT_VB_DANGER_X_BASE = "gfx.displayport.strategy_vb.danger_x_base";
+ private static final String PREF_DISPLAYPORT_VB_DANGER_Y_BASE = "gfx.displayport.strategy_vb.danger_y_base";
+ private static final String PREF_DISPLAYPORT_VB_DANGER_X_INCR = "gfx.displayport.strategy_vb.danger_x_incr";
+ private static final String PREF_DISPLAYPORT_VB_DANGER_Y_INCR = "gfx.displayport.strategy_vb.danger_y_incr";
+ private static final String PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_pb.threshold";
+
+ private static DisplayPortStrategy sStrategy = new VelocityBiasStrategy(null);
+
+ static DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+ return sStrategy.calculate(metrics, (velocity == null ? ZERO_VELOCITY : velocity));
+ }
+
+ static boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+ if (displayPort == null) {
+ return true;
+ }
+ return sStrategy.aboutToCheckerboard(metrics, (velocity == null ? ZERO_VELOCITY : velocity), displayPort);
+ }
+
+ static boolean drawTimeUpdate(long millis, int pixels) {
+ return sStrategy.drawTimeUpdate(millis, pixels);
+ }
+
+ static void resetPageState() {
+ sStrategy.resetPageState();
+ }
+
+ static void initPrefs() {
+ final String[] prefs = { PREF_DISPLAYPORT_STRATEGY,
+ PREF_DISPLAYPORT_FM_MULTIPLIER,
+ PREF_DISPLAYPORT_FM_DANGER_X,
+ PREF_DISPLAYPORT_FM_DANGER_Y,
+ PREF_DISPLAYPORT_VB_MULTIPLIER,
+ PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD,
+ PREF_DISPLAYPORT_VB_REVERSE_BUFFER,
+ PREF_DISPLAYPORT_VB_DANGER_X_BASE,
+ PREF_DISPLAYPORT_VB_DANGER_Y_BASE,
+ PREF_DISPLAYPORT_VB_DANGER_X_INCR,
+ PREF_DISPLAYPORT_VB_DANGER_Y_INCR,
+ PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD };
+
+ /*PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() {
+ private Map<String, Integer> mValues = new HashMap<String, Integer>();
+
+ @Override public void prefValue(String pref, int value) {
+ mValues.put(pref, value);
+ }
+
+ @Override public void finish() {
+ setStrategy(mValues);
+ }
+ });*/
+ }
+
+ /**
+ * Set the active strategy to use.
+ * See the gfx.displayport.strategy pref in mobile/android/app/mobile.js to see the
+ * mapping between ints and strategies.
+ */
+ static boolean setStrategy(Map<String, Integer> prefs) {
+ Integer strategy = prefs.get(PREF_DISPLAYPORT_STRATEGY);
+ if (strategy == null) {
+ return false;
+ }
+
+ switch (strategy) {
+ case 0:
+ sStrategy = new FixedMarginStrategy(prefs);
+ break;
+ case 1:
+ sStrategy = new VelocityBiasStrategy(prefs);
+ break;
+ case 2:
+ sStrategy = new DynamicResolutionStrategy(prefs);
+ break;
+ case 3:
+ sStrategy = new NoMarginStrategy(prefs);
+ break;
+ case 4:
+ sStrategy = new PredictionBiasStrategy(prefs);
+ break;
+ default:
+ Log.e(LOGTAG, "Invalid strategy index specified");
+ return false;
+ }
+ Log.i(LOGTAG, "Set strategy " + sStrategy.toString());
+ return true;
+ }
+
+ private static float getFloatPref(Map<String, Integer> prefs, String prefName, int defaultValue) {
+ Integer value = (prefs == null ? null : prefs.get(prefName));
+ return (float)(value == null || value < 0 ? defaultValue : value) / 1000f;
+ }
+
+ private static abstract class DisplayPortStrategy {
+ /** Calculates a displayport given a viewport and panning velocity. */
+ public abstract DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity);
+ /** Returns true if a checkerboard is about to be visible and we should not throttle drawing. */
+ public abstract boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort);
+ /** Notify the strategy of a new recorded draw time. Return false to turn off draw time recording. */
+ public boolean drawTimeUpdate(long millis, int pixels) { return false; }
+ /** Reset any page-specific state stored, as the page being displayed has changed. */
+ public void resetPageState() {}
+ }
+
+ /**
+ * Return the dimensions for a rect that has area (width*height) that does not exceed the page size in the
+ * given metrics object. The area in the returned FloatSize may be less than width*height if the page is
+ * small, but it will never be larger than width*height.
+ * Note that this process may change the relative aspect ratio of the given dimensions.
+ */
+ private static FloatSize reshapeForPage(float width, float height, ImmutableViewportMetrics metrics) {
+ // figure out how much of the desired buffer amount we can actually use on the horizontal axis
+ float usableWidth = Math.min(width, metrics.getPageWidth());
+ // if we reduced the buffer amount on the horizontal axis, we should take that saved memory and
+ // use it on the vertical axis
+ float extraUsableHeight = (float)Math.floor(((width - usableWidth) * height) / usableWidth);
+ float usableHeight = Math.min(height + extraUsableHeight, metrics.getPageHeight());
+ if (usableHeight < height && usableWidth == width) {
+ // and the reverse - if we shrunk the buffer on the vertical axis we can add it to the horizontal
+ float extraUsableWidth = (float)Math.floor(((height - usableHeight) * width) / usableHeight);
+ usableWidth = Math.min(width + extraUsableWidth, metrics.getPageWidth());
+ }
+ return new FloatSize(usableWidth, usableHeight);
+ }
+
+ /**
+ * Expand the given rect in all directions by a "danger zone". The size of the danger zone on an axis
+ * is the size of the view on that axis multiplied by the given multiplier. The expanded rect is then
+ * clamped to page bounds and returned.
+ */
+ private static RectF expandByDangerZone(RectF rect, float dangerZoneXMultiplier, float dangerZoneYMultiplier, ImmutableViewportMetrics metrics) {
+ // calculate the danger zone amounts in pixels
+ float dangerZoneX = metrics.getWidth() * dangerZoneXMultiplier;
+ float dangerZoneY = metrics.getHeight() * dangerZoneYMultiplier;
+ rect = RectUtils.expand(rect, dangerZoneX, dangerZoneY);
+ // clamp to page bounds
+ return clampToPageBounds(rect, metrics);
+ }
+
+ /**
+ * Expand the given margins such that when they are applied on the viewport, the resulting rect
+ * does not have any partial tiles, except when it is clipped by the page bounds. This assumes
+ * the tiles are TILE_SIZE by TILE_SIZE and start at the origin, such that there will always be
+ * a tile at (0,0)-(TILE_SIZE,TILE_SIZE)).
+ */
+ private static DisplayPortMetrics getTileAlignedDisplayPortMetrics(RectF margins, float zoom, ImmutableViewportMetrics metrics) {
+ float left = metrics.viewportRectLeft - margins.left;
+ float top = metrics.viewportRectTop - margins.top;
+ float right = metrics.viewportRectRight + margins.right;
+ float bottom = metrics.viewportRectBottom + margins.bottom;
+ left = Math.max(metrics.pageRectLeft, TILE_SIZE * FloatMath.floor(left / TILE_SIZE));
+ top = Math.max(metrics.pageRectTop, TILE_SIZE * FloatMath.floor(top / TILE_SIZE));
+ right = Math.min(metrics.pageRectRight, TILE_SIZE * FloatMath.ceil(right / TILE_SIZE));
+ bottom = Math.min(metrics.pageRectBottom, TILE_SIZE * FloatMath.ceil(bottom / TILE_SIZE));
+ return new DisplayPortMetrics(left, top, right, bottom, zoom);
+ }
+
+ /**
+ * Adjust the given margins so if they are applied on the viewport in the metrics, the resulting rect
+ * does not exceed the page bounds. This code will maintain the total margin amount for a given axis;
+ * it assumes that margins.left + metrics.getWidth() + margins.right is less than or equal to
+ * metrics.getPageWidth(); and the same for the y axis.
+ */
+ private static RectF shiftMarginsForPageBounds(RectF margins, ImmutableViewportMetrics metrics) {
+ // check how much we're overflowing in each direction. note that at most one of leftOverflow
+ // and rightOverflow can be greater than zero, and at most one of topOverflow and bottomOverflow
+ // can be greater than zero, because of the assumption described in the method javadoc.
+ float leftOverflow = metrics.pageRectLeft - (metrics.viewportRectLeft - margins.left);
+ float rightOverflow = (metrics.viewportRectRight + margins.right) - metrics.pageRectRight;
+ float topOverflow = metrics.pageRectTop - (metrics.viewportRectTop - margins.top);
+ float bottomOverflow = (metrics.viewportRectBottom + margins.bottom) - metrics.pageRectBottom;
+
+ // if the margins overflow the page bounds, shift them to other side on the same axis
+ if (leftOverflow > 0) {
+ margins.left -= leftOverflow;
+ margins.right += leftOverflow;
+ } else if (rightOverflow > 0) {
+ margins.right -= rightOverflow;
+ margins.left += rightOverflow;
+ }
+ if (topOverflow > 0) {
+ margins.top -= topOverflow;
+ margins.bottom += topOverflow;
+ } else if (bottomOverflow > 0) {
+ margins.bottom -= bottomOverflow;
+ margins.top += bottomOverflow;
+ }
+ return margins;
+ }
+
+ /**
+ * Clamp the given rect to the page bounds and return it.
+ */
+ private static RectF clampToPageBounds(RectF rect, ImmutableViewportMetrics metrics) {
+ if (rect.top < metrics.pageRectTop) rect.top = metrics.pageRectTop;
+ if (rect.left < metrics.pageRectLeft) rect.left = metrics.pageRectLeft;
+ if (rect.right > metrics.pageRectRight) rect.right = metrics.pageRectRight;
+ if (rect.bottom > metrics.pageRectBottom) rect.bottom = metrics.pageRectBottom;
+ return rect;
+ }
+
+ /**
+ * This class implements the variation where we basically don't bother with a a display port.
+ */
+ private static class NoMarginStrategy extends DisplayPortStrategy {
+ NoMarginStrategy(Map<String, Integer> prefs) {
+ // no prefs in this strategy
+ }
+
+ @Override
+ public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+ return new DisplayPortMetrics(metrics.viewportRectLeft,
+ metrics.viewportRectTop,
+ metrics.viewportRectRight,
+ metrics.viewportRectBottom,
+ metrics.zoomFactor);
+ }
+
+ @Override
+ public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "NoMarginStrategy";
+ }
+ }
+
+ /**
+ * This class implements the variation where we use a fixed-size margin on the display port.
+ * The margin is always 300 pixels in all directions, except when we are (a) approaching a page
+ * boundary, and/or (b) if we are limited by the page size. In these cases we try to maintain
+ * the area of the display port by (a) shifting the buffer to the other side on the same axis,
+ * and/or (b) increasing the buffer on the other axis to compensate for the reduced buffer on
+ * one axis.
+ */
+ private static class FixedMarginStrategy extends DisplayPortStrategy {
+ // The length of each axis of the display port will be the corresponding view length
+ // multiplied by this factor.
+ private final float SIZE_MULTIPLIER;
+
+ // If the visible rect is within the danger zone (measured as a fraction of the view size
+ // from the edge of the displayport) we start redrawing to minimize checkerboarding.
+ private final float DANGER_ZONE_X_MULTIPLIER;
+ private final float DANGER_ZONE_Y_MULTIPLIER;
+
+ FixedMarginStrategy(Map<String, Integer> prefs) {
+ SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_MULTIPLIER, 2000);
+ DANGER_ZONE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_X, 100);
+ DANGER_ZONE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_Y, 200);
+ }
+
+ @Override
+ public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+ float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
+ float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
+
+ // we need to avoid having a display port that is larger than the page, or we will end up
+ // painting things outside the page bounds (bug 729169). we simultaneously need to make
+ // the display port as large as possible so that we redraw less. reshape the display
+ // port dimensions to accomplish this.
+ FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
+ float horizontalBuffer = usableSize.width - metrics.getWidth();
+ float verticalBuffer = usableSize.height - metrics.getHeight();
+
+ // and now calculate the display port margins based on how much buffer we've decided to use and
+ // the page bounds, ensuring we use all of the available buffer amounts on one side or the other
+ // on any given axis. (i.e. if we're scrolled to the top of the page, the vertical buffer is
+ // entirely below the visible viewport, but if we're halfway down the page, the vertical buffer
+ // is split).
+ RectF margins = new RectF();
+ margins.left = horizontalBuffer / 2.0f;
+ margins.right = horizontalBuffer - margins.left;
+ margins.top = verticalBuffer / 2.0f;
+ margins.bottom = verticalBuffer - margins.top;
+ margins = shiftMarginsForPageBounds(margins, metrics);
+
+ return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+ }
+
+ @Override
+ public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+ // Increase the size of the viewport based on the danger zone multiplier (and clamp to page
+ // boundaries), and intersect it with the current displayport to determine whether we're
+ // close to checkerboarding.
+ RectF adjustedViewport = expandByDangerZone(metrics.getViewport(), DANGER_ZONE_X_MULTIPLIER, DANGER_ZONE_Y_MULTIPLIER, metrics);
+ return !displayPort.contains(adjustedViewport);
+ }
+
+ @Override
+ public String toString() {
+ return "FixedMarginStrategy mult=" + SIZE_MULTIPLIER + ", dangerX=" + DANGER_ZONE_X_MULTIPLIER + ", dangerY=" + DANGER_ZONE_Y_MULTIPLIER;
+ }
+ }
+
+ /**
+ * This class implements the variation with a small fixed-size margin with velocity bias.
+ * In this variation, the default margins are pretty small relative to the view size, but
+ * they are affected by the panning velocity. Specifically, if we are panning on one axis,
+ * we remove the margins on the other axis because we are likely axis-locked. Also once
+ * we are panning in one direction above a certain threshold velocity, we shift the buffer
+ * so that it is almost entirely in the direction of the pan, with a little bit in the
+ * reverse direction.
+ */
+ private static class VelocityBiasStrategy extends DisplayPortStrategy {
+ // The length of each axis of the display port will be the corresponding view length
+ // multiplied by this factor.
+ private final float SIZE_MULTIPLIER;
+ // The velocity above which we apply the velocity bias
+ private final float VELOCITY_THRESHOLD;
+ // How much of the buffer to keep in the reverse direction of the velocity
+ private final float REVERSE_BUFFER;
+ // If the visible rect is within the danger zone we start redrawing to minimize
+ // checkerboarding. the danger zone amount is a linear function of the form:
+ // viewportsize * (base + velocity * incr)
+ // where base and incr are configurable values.
+ private final float DANGER_ZONE_BASE_X_MULTIPLIER;
+ private final float DANGER_ZONE_BASE_Y_MULTIPLIER;
+ private final float DANGER_ZONE_INCR_X_MULTIPLIER;
+ private final float DANGER_ZONE_INCR_Y_MULTIPLIER;
+
+ VelocityBiasStrategy(Map<String, Integer> prefs) {
+ SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_MULTIPLIER, 2000);
+ VELOCITY_THRESHOLD = /*GeckoAppShell.getDpi()*/ LOKitShell.getDpi() * getFloatPref(prefs, PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD, 32);
+ REVERSE_BUFFER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_REVERSE_BUFFER, 200);
+ DANGER_ZONE_BASE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_BASE, 1000);
+ DANGER_ZONE_BASE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_BASE, 1000);
+ DANGER_ZONE_INCR_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_INCR, 0);
+ DANGER_ZONE_INCR_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_INCR, 0);
+ }
+
+ /**
+ * Split the given amounts into margins based on the VELOCITY_THRESHOLD and REVERSE_BUFFER values.
+ * If the velocity is above the VELOCITY_THRESHOLD on an axis, split the amount into REVERSE_BUFFER
+ * and 1.0 - REVERSE_BUFFER fractions. The REVERSE_BUFFER fraction is set as the margin in the
+ * direction opposite to the velocity, and the remaining fraction is set as the margin in the direction
+ * of the velocity. If the velocity is lower than VELOCITY_THRESHOLD, split the amount evenly into the
+ * two margins on that axis.
+ */
+ private RectF velocityBiasedMargins(float xAmount, float yAmount, PointF velocity) {
+ RectF margins = new RectF();
+
+ if (velocity.x > VELOCITY_THRESHOLD) {
+ margins.left = xAmount * REVERSE_BUFFER;
+ } else if (velocity.x < -VELOCITY_THRESHOLD) {
+ margins.left = xAmount * (1.0f - REVERSE_BUFFER);
+ } else {
+ margins.left = xAmount / 2.0f;
+ }
+ margins.right = xAmount - margins.left;
+
+ if (velocity.y > VELOCITY_THRESHOLD) {
+ margins.top = yAmount * REVERSE_BUFFER;
+ } else if (velocity.y < -VELOCITY_THRESHOLD) {
+ margins.top = yAmount * (1.0f - REVERSE_BUFFER);
+ } else {
+ margins.top = yAmount / 2.0f;
+ }
+ margins.bottom = yAmount - margins.top;
+
+ return margins;
+ }
+
+ @Override
+ public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+ float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
+ float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
+
+ // but if we're panning on one axis, set the margins for the other axis to zero since we are likely
+ // axis locked and won't be displaying that extra area.
+ if (Math.abs(velocity.x) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.y, 0)) {
+ displayPortHeight = metrics.getHeight();
+ } else if (Math.abs(velocity.y) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.x, 0)) {
+ displayPortWidth = metrics.getWidth();
+ }
+
+ // we need to avoid having a display port that is larger than the page, or we will end up
+ // painting things outside the page bounds (bug 729169).
+ displayPortWidth = Math.min(displayPortWidth, metrics.getPageWidth());
+ displayPortHeight = Math.min(displayPortHeight, metrics.getPageHeight());
+ float horizontalBuffer = displayPortWidth - metrics.getWidth();
+ float verticalBuffer = displayPortHeight - metrics.getHeight();
+
+ // split the buffer amounts into margins based on velocity, and shift it to
+ // take into account the page bounds
+ RectF margins = velocityBiasedMargins(horizontalBuffer, verticalBuffer, velocity);
+ margins = shiftMarginsForPageBounds(margins, metrics);
+
+ return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+ }
+
+ @Override
+ public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+ // calculate the danger zone amounts based on the prefs
+ float dangerZoneX = metrics.getWidth() * (DANGER_ZONE_BASE_X_MULTIPLIER + (velocity.x * DANGER_ZONE_INCR_X_MULTIPLIER));
+ float dangerZoneY = metrics.getHeight() * (DANGER_ZONE_BASE_Y_MULTIPLIER + (velocity.y * DANGER_ZONE_INCR_Y_MULTIPLIER));
+ // clamp it such that when added to the viewport, they don't exceed page size.
+ // this is a prerequisite to calling shiftMarginsForPageBounds as we do below.
+ dangerZoneX = Math.min(dangerZoneX, metrics.getPageWidth() - metrics.getWidth());
+ dangerZoneY = Math.min(dangerZoneY, metrics.getPageHeight() - metrics.getHeight());
+
+ // split the danger zone into margins based on velocity, and ensure it doesn't exceed
+ // page bounds.
+ RectF dangerMargins = velocityBiasedMargins(dangerZoneX, dangerZoneY, velocity);
+ dangerMargins = shiftMarginsForPageBounds(dangerMargins, metrics);
+
+ // we're about to checkerboard if the current viewport area + the danger zone margins
+ // fall out of the current displayport anywhere.
+ RectF adjustedViewport = new RectF(
+ metrics.viewportRectLeft - dangerMargins.left,
+ metrics.viewportRectTop - dangerMargins.top,
+ metrics.viewportRectRight + dangerMargins.right,
+ metrics.viewportRectBottom + dangerMargins.bottom);
+ return !displayPort.contains(adjustedViewport);
+ }
+
+ @Override
+ public String toString() {
+ return "VelocityBiasStrategy mult=" + SIZE_MULTIPLIER + ", threshold=" + VELOCITY_THRESHOLD + ", reverse=" + REVERSE_BUFFER
+ + ", dangerBaseX=" + DANGER_ZONE_BASE_X_MULTIPLIER + ", dangerBaseY=" + DANGER_ZONE_BASE_Y_MULTIPLIER
+ + ", dangerIncrX=" + DANGER_ZONE_INCR_Y_MULTIPLIER + ", dangerIncrY=" + DANGER_ZONE_INCR_Y_MULTIPLIER;
+ }
+ }
+
+ /**
+ * This class implements the variation where we draw more of the page at low resolution while panning.
+ * In this variation, as we pan faster, we increase the page area we are drawing, but reduce the draw
+ * resolution to compensate. This results in the same device-pixel area drawn; the compositor then
+ * scales this up to the viewport zoom level. This results in a large area of the page drawn but it
+ * looks blurry. The assumption is that drawing extra that we never display is better than checkerboarding,
+ * where we draw less but never even show it on the screen.
+ */
+ private static class DynamicResolutionStrategy extends DisplayPortStrategy {
+ // The length of each axis of the display port will be the corresponding view length
+ // multiplied by this factor.
+ private static final float SIZE_MULTIPLIER = 1.5f;
+
+ // The velocity above which we start zooming out the display port to keep up
+ // with the panning.
+ private static final float VELOCITY_EXPANSION_THRESHOLD = /*GeckoAppShell.getDpi()*/ LOKitShell.getDpi() / 16f;
+
+ // How much we increase the display port based on velocity. Assuming no friction and
+ // splitting (see below), this should be be the number of frames (@60fps) between us
+ // calculating the display port and the draw of the *next* display port getting composited
+ // and displayed on the screen. This is because the timeline looks like this:
+ // Java: pan pan pan pan pan pan ! pan pan pan pan pan pan !
+ // Gecko: \-> draw -> composite / \-> draw -> composite /
+ // The display port calculated on the first "pan" gets composited to the screen at the
+ // first exclamation mark, and remains on the screen until the second exclamation mark.
+ // In order to avoid checkerboarding, that display port must be able to contain all of
+ // the panning until the second exclamation mark, which encompasses two entire draw/composite
+ // cycles.
+ // If we take into account friction, our velocity multiplier should be reduced as the
+ // amount of pan will decrease each time. If we take into account display port splitting,
+ // it should be increased as the splitting means some of the display port will be used to
+ // draw in the opposite direction of the velocity. For now I'm assuming these two cancel
+ // each other out.
+ private static final float VELOCITY_MULTIPLIER = 60.0f;
+
+ // The following constants adjust how biased the display port is in the direction of panning.
+ // When panning fast (above the FAST_THRESHOLD) we use the fast split factor to split the
+ // display port "buffer" area, otherwise we use the slow split factor. This is based on the
+ // assumption that if the user is panning fast, they are less likely to reverse directions
+ // and go backwards, so we should spend more of our display port buffer in the direction of
+ // panning.
+ private static final float VELOCITY_FAST_THRESHOLD = VELOCITY_EXPANSION_THRESHOLD * 2.0f;
+ private static final float FAST_SPLIT_FACTOR = 0.95f;
+ private static final float SLOW_SPLIT_FACTOR = 0.8f;
+
+ // The following constants are used for viewport prediction; we use them to estimate where
+ // the viewport will be soon and whether or not we should trigger a draw right now. "soon"
+ // in the previous sentence really refers to the amount of time it would take to draw and
+ // composite from the point at which we do the calculation, and that is not really a known
+ // quantity. The velocity multiplier is how much we multiply the velocity by; it has the
+ // same caveats as the VELOCITY_MULTIPLIER above except that it only needs to take into account
+ // one draw/composite cycle instead of two. The danger zone multiplier is a multiplier of the
+ // viewport size that we use as an extra "danger zone" around the viewport; if this danger
+ // zone falls outside the display port then we are approaching the point at which we will
+ // checkerboard, and hence should start drawing. Note that if DANGER_ZONE_MULTIPLIER is
+ // greater than (SIZE_MULTIPLIER - 1.0f), then at zero velocity we will always be in the
+ // danger zone, and thus will be constantly drawing.
+ private static final float PREDICTION_VELOCITY_MULTIPLIER = 30.0f;
+ private static final float DANGER_ZONE_MULTIPLIER = 0.20f; // must be less than (SIZE_MULTIPLIER - 1.0f)
+
+ DynamicResolutionStrategy(Map<String, Integer> prefs) {
+ // ignore prefs for now
+ }
+
+ @Override
+ public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+ float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
+ float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
+
+ // for resolution calculation purposes, we need to know what the adjusted display port dimensions
+ // would be if we had zero velocity, so calculate that here before we increase the display port
+ // based on velocity.
+ FloatSize reshapedSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
+
+ // increase displayPortWidth and displayPortHeight based on the velocity, but maintaining their
+ // relative aspect ratio.
+ if (velocity.length() > VELOCITY_EXPANSION_THRESHOLD) {
+ float velocityFactor = Math.max(Math.abs(velocity.x) / displayPortWidth,
+ Math.abs(velocity.y) / displayPortHeight);
+ velocityFactor *= VELOCITY_MULTIPLIER;
+
+ displayPortWidth += (displayPortWidth * velocityFactor);
+ displayPortHeight += (displayPortHeight * velocityFactor);
+ }
+
+ // at this point, displayPortWidth and displayPortHeight are how much of the page (in device pixels)
+ // we want to be rendered by Gecko. Note here "device pixels" is equivalent to CSS pixels multiplied
+ // by metrics.zoomFactor
+
+ // we need to avoid having a display port that is larger than the page, or we will end up
+ // painting things outside the page bounds (bug 729169). we simultaneously need to make
+ // the display port as large as possible so that we redraw less. reshape the display
+ // port dimensions to accomplish this. this may change the aspect ratio of the display port,
+ // but we are assuming that this is desirable because the advantages from pre-drawing will
+ // outweigh the disadvantages from any buffer reallocations that might occur.
+ FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
+ float horizontalBuffer = usableSize.width - metrics.getWidth();
+ float verticalBuffer = usableSize.height - metrics.getHeight();
+
+ // at this point, horizontalBuffer and verticalBuffer are the dimensions of the buffer area we have.
+ // the buffer area is the off-screen area that is part of the display port and will be pre-drawn in case
+ // the user scrolls there. we now need to split the buffer area on each axis so that we know
+ // what the exact margins on each side will be. first we split the buffer amount based on the direction
+ // we're moving, so that we have a larger buffer in the direction of travel.
+ RectF margins = new RectF();
+ margins.left = splitBufferByVelocity(horizontalBuffer, velocity.x);
+ margins.right = horizontalBuffer - margins.left;
+ margins.top = splitBufferByVelocity(verticalBuffer, velocity.y);
+ margins.bottom = verticalBuffer - margins.top;
+
+ // then, we account for running into the page bounds - so that if we hit the top of the page, we need
+ // to drop the top margin and move that amount to the bottom margin.
+ margins = shiftMarginsForPageBounds(margins, metrics);
+
+ // finally, we calculate the resolution we want to render the display port area at. We do this
+ // so that as we expand the display port area (because of velocity), we reduce the resolution of
+ // the painted area so as to maintain the size of the buffer Gecko is painting into. we calculate
+ // the reduction in resolution by comparing the display port size with and without the velocity
+ // changes applied.
+ // this effectively means that as we pan faster and faster, the display port grows, but we paint
+ // at lower resolutions. this paints more area to reduce checkerboard at the cost of increasing
+ // compositor-scaling and blurriness. Once we stop panning, the blurriness must be entirely gone.
+ // Note that usable* could be less than base* if we are pinch-zoomed out into overscroll, so we
+ // clamp it to make sure this doesn't increase our display resolution past metrics.zoomFactor.
+ float scaleFactor = Math.min(reshapedSize.width / usableSize.width, reshapedSize.height / usableSize.height);
+ float displayResolution = metrics.zoomFactor * Math.min(1.0f, scaleFactor);
+
+ DisplayPortMetrics dpMetrics = new DisplayPortMetrics(
+ metrics.viewportRectLeft - margins.left,
+ metrics.viewportRectTop - margins.top,
+ metrics.viewportRectRight + margins.right,
+ metrics.viewportRectBottom + margins.bottom,
+ displayResolution);
+ return dpMetrics;
+ }
+
+ /**
+ * Split the given buffer amount into two based on the velocity.
+ * Given an amount of total usable buffer on an axis, this will
+ * return the amount that should be used on the left/top side of
+ * the axis (the side which a negative velocity vector corresponds
+ * to).
+ */
+ private float splitBufferByVelocity(float amount, float velocity) {
+ // if no velocity, so split evenly
+ if (FloatUtils.fuzzyEquals(velocity, 0)) {
+ return amount / 2.0f;
+ }
+ // if we're moving quickly, assign more of the amount in that direction
+ // since is is less likely that we will reverse direction immediately
+ if (velocity < -VELOCITY_FAST_THRESHOLD) {
+ return amount * FAST_SPLIT_FACTOR;
+ }
+ if (velocity > VELOCITY_FAST_THRESHOLD) {
+ return amount * (1.0f - FAST_SPLIT_FACTOR);
+ }
+ // if we're moving slowly, then assign less of the amount in that direction
+ if (velocity < 0) {
+ return amount * SLOW_SPLIT_FACTOR;
+ } else {
+ return amount * (1.0f - SLOW_SPLIT_FACTOR);
+ }
+ }
+
+ @Override
+ public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+ // Expand the viewport based on our velocity (and clamp it to page boundaries).
+ // Then intersect it with the last-requested displayport to determine whether we're
+ // close to checkerboarding.
+
+ RectF predictedViewport = metrics.getViewport();
+
+ // first we expand the viewport in the direction we're moving based on some
+ // multiple of the current velocity.
+ if (velocity.length() > 0) {
+ if (velocity.x < 0) {
+ predictedViewport.left += velocity.x * PREDICTION_VELOCITY_MULTIPLIER;
+ } else if (velocity.x > 0) {
+ predictedViewport.right += velocity.x * PREDICTION_VELOCITY_MULTIPLIER;
+ }
+
+ if (velocity.y < 0) {
+ predictedViewport.top += velocity.y * PREDICTION_VELOCITY_MULTIPLIER;
+ } else if (velocity.y > 0) {
+ predictedViewport.bottom += velocity.y * PREDICTION_VELOCITY_MULTIPLIER;
+ }
+ }
+
+ // then we expand the viewport evenly in all directions just to have an extra
+ // safety zone. this also clamps it to page bounds.
+ predictedViewport = expandByDangerZone(predictedViewport, DANGER_ZONE_MULTIPLIER, DANGER_ZONE_MULTIPLIER, metrics);
+ return !displayPort.contains(predictedViewport);
+ }
+
+ @Override
+ public String toString() {
+ return "DynamicResolutionStrategy";
+ }
+ }
+
+ /**
+ * This class implements the variation where we use the draw time to predict where we will be when
+ * a draw completes, and draw that instead of where we are now. In this variation, when our panning
+ * speed drops below a certain threshold, we draw 9 viewports' worth of content so that the user can
+ * pan in any direction without encountering checkerboarding.
+ * Once the user is panning, we modify the displayport to encompass an area range of where we think
+ * the user will be when the draw completes. This heuristic relies on both the estimated draw time
+ * the panning velocity; unexpected changes in either of these values will cause the heuristic to
+ * fail and show checkerboard.
+ */
+ private static class PredictionBiasStrategy extends DisplayPortStrategy {
+ private static float VELOCITY_THRESHOLD;
+
+ private int mPixelArea; // area of the viewport, used in draw time calculations
+ private int mMinFramesToDraw; // minimum number of frames we take to draw
+ private int mMaxFramesToDraw; // maximum number of frames we take to draw
+
+ PredictionBiasStrategy(Map<String, Integer> prefs) {
+ VELOCITY_THRESHOLD = /*GeckoAppShell.getDpi()*/ LOKitShell.getDpi() * getFloatPref(prefs, PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD, 16);
+ resetPageState();
+ }
+
+ @Override
+ public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+ float width = metrics.getWidth();
+ float height = metrics.getHeight();
+ mPixelArea = (int)(width * height);
+
+ if (velocity.length() < VELOCITY_THRESHOLD) {
+ // if we're going slow, expand the displayport to 9x viewport size
+ RectF margins = new RectF(width, height, width, height);
+ return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+ }
+
+ // figure out how far we expect to be
+ float minDx = velocity.x * mMinFramesToDraw;
+ float minDy = velocity.y * mMinFramesToDraw;
+ float maxDx = velocity.x * mMaxFramesToDraw;
+ float maxDy = velocity.y * mMaxFramesToDraw;
+
+ // figure out how many pixels we will be drawing when we draw the above-calculated range.
+ // this will be larger than the viewport area.
+ float pixelsToDraw = (width + Math.abs(maxDx - minDx)) * (height + Math.abs(maxDy - minDy));
+ // adjust how far we will get because of the time spent drawing all these extra pixels. this
+ // will again increase the number of pixels drawn so really we could keep iterating this over
+ // and over, but once seems enough for now.
+ maxDx = maxDx * pixelsToDraw / mPixelArea;
+ maxDy = maxDy * pixelsToDraw / mPixelArea;
+
+ // and finally generate the displayport. the min/max stuff takes care of
+ // negative velocities as well as positive.
+ RectF margins = new RectF(
+ -Math.min(minDx, maxDx),
+ -Math.min(minDy, maxDy),
+ Math.max(minDx, maxDx),
+ Math.max(minDy, maxDy));
+ return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+ }
+
+ @Override
+ public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+ // the code below is the same as in calculate() but is awkward to refactor since it has multiple outputs.
+ // refer to the comments in calculate() to understand what this is doing.
+ float minDx = velocity.x * mMinFramesToDraw;
+ float minDy = velocity.y * mMinFramesToDraw;
+ float maxDx = velocity.x * mMaxFramesToDraw;
+ float maxDy = velocity.y * mMaxFramesToDraw;
+ float pixelsToDraw = (metrics.getWidth() + Math.abs(maxDx - minDx)) * (metrics.getHeight() + Math.abs(maxDy - minDy));
+ maxDx = maxDx * pixelsToDraw / mPixelArea;
+ maxDy = maxDy * pixelsToDraw / mPixelArea;
+
+ // now that we have an idea of how far we will be when the draw completes, take the farthest
+ // end of that range and see if it falls outside the displayport bounds. if it does, allow
+ // the draw to go through
+ RectF predictedViewport = metrics.getViewport();
+ predictedViewport.left += maxDx;
+ predictedViewport.top += maxDy;
+ predictedViewport.right += maxDx;
+ predictedViewport.bottom += maxDy;
+
+ predictedViewport = clampToPageBounds(predictedViewport, metrics);
+ return !displayPort.contains(predictedViewport);
+ }
+
+ @Override
+ public boolean drawTimeUpdate(long millis, int pixels) {
+ // calculate the number of frames it took to draw a viewport-sized area
+ float normalizedTime = (float)mPixelArea * (float)millis / (float)pixels;
+ int normalizedFrames = (int)FloatMath.ceil(normalizedTime * 60f / 1000f);
+ // broaden our range on how long it takes to draw if the draw falls outside
+ // the range. this allows it to grow gradually. this heuristic may need to
+ // be tweaked into more of a floating window average or something.
+ if (normalizedFrames <= mMinFramesToDraw) {
+ mMinFramesToDraw--;
+ } else if (normalizedFrames > mMaxFramesToDraw) {
+ mMaxFramesToDraw++;
+ } else {
+ return true;
+ }
+ Log.d(LOGTAG, "Widened draw range to [" + mMinFramesToDraw + ", " + mMaxFramesToDraw + "]");
+ return true;
+ }
+
+ @Override
+ public void resetPageState() {
+ mMinFramesToDraw = 0;
+ mMaxFramesToDraw = 2;
+ }
+
+ @Override
+ public String toString() {
+ return "PredictionBiasStrategy threshold=" + VELOCITY_THRESHOLD;
+ }
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/DisplayPortMetrics.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/DisplayPortMetrics.java
new file mode 100644
index 000000000000..741136f90a53
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/DisplayPortMetrics.java
@@ -0,0 +1,78 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+//import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI;
+import org.mozilla.gecko.util.FloatUtils;
+
+import android.graphics.RectF;
+
+/*
+ * This class keeps track of the area we request Gecko to paint, as well
+ * as the resolution of the paint. The area may be different from the visible
+ * area of the page, and the resolution may be different from the resolution
+ * used in the compositor to render the page. This is so that we can ask Gecko
+ * to paint a much larger area without using extra memory, and then render some
+ * subsection of that with compositor scaling.
+ */
+public final class DisplayPortMetrics {
+ //@WrapElementForJNI
+ public final float resolution;
+ //@WrapElementForJNI
+ private final RectF mPosition;
+
+ public DisplayPortMetrics() {
+ this(0, 0, 0, 0, 1);
+ }
+
+ //@WrapElementForJNI
+ public DisplayPortMetrics(float left, float top, float right, float bottom, float resolution) {
+ this.resolution = resolution;
+ mPosition = new RectF(left, top, right, bottom);
+ }
+
+ public float getLeft() {
+ return mPosition.left;
+ }
+
+ public float getTop() {
+ return mPosition.top;
+ }
+
+ public float getRight() {
+ return mPosition.right;
+ }
+
+ public float getBottom() {
+ return mPosition.bottom;
+ }
+
+ public boolean contains(RectF rect) {
+ return mPosition.contains(rect);
+ }
+
+ public boolean fuzzyEquals(DisplayPortMetrics metrics) {
+ return RectUtils.fuzzyEquals(mPosition, metrics.mPosition)
+ && FloatUtils.fuzzyEquals(resolution, metrics.resolution);
+ }
+
+ public String toJSON() {
+ StringBuilder sb = new StringBuilder(256);
+ sb.append("{ \"left\": ").append(mPosition.left)
+ .append(", \"top\": ").append(mPosition.top)
+ .append(", \"right\": ").append(mPosition.right)
+ .append(", \"bottom\": ").append(mPosition.bottom)
+ .append(", \"resolution\": ").append(resolution)
+ .append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public String toString() {
+ return "DisplayPortMetrics v=(" + mPosition.left + "," + mPosition.top + "," + mPosition.right + ","
+ + mPosition.bottom + ") z=" + resolution;
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/DrawTimingQueue.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/DrawTimingQueue.java
new file mode 100644
index 000000000000..ce868f18ce2f
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/DrawTimingQueue.java
@@ -0,0 +1,95 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import android.os.SystemClock;
+
+/**
+ * A custom-built data structure to assist with measuring draw times.
+ *
+ * This class maintains a fixed-size circular buffer of DisplayPortMetrics
+ * objects and associated timestamps. It provides only three operations, which
+ * is all we require for our purposes of measuring draw times. Note
+ * in particular that the class is designed so that even though it is
+ * accessed from multiple threads, it does not require synchronization;
+ * any concurrency errors that result from this are handled gracefully.
+ *
+ * Assuming an unrolled buffer so that mTail is greater than mHead, the data
+ * stored in the buffer at entries [mHead, mTail) will never be modified, and
+ * so are "safe" to read. If this reading is done on the same thread that
+ * owns mHead, then reading the range [mHead, mTail) is guaranteed to be safe
+ * since the range itself will not shrink.
+ */
+final class DrawTimingQueue {
+ private static final String LOGTAG = "GeckoDrawTimingQueue";
+ private static final int BUFFER_SIZE = 16;
+
+ private final DisplayPortMetrics[] mMetrics;
+ private final long[] mTimestamps;
+
+ private int mHead;
+ private int mTail;
+
+ DrawTimingQueue() {
+ mMetrics = new DisplayPortMetrics[BUFFER_SIZE];
+ mTimestamps = new long[BUFFER_SIZE];
+ mHead = BUFFER_SIZE - 1;
+ mTail = 0;
+ }
+
+ /**
+ * Add a new entry to the tail of the queue. If the buffer is full,
+ * do nothing. This must only be called from the Java UI thread.
+ */
+ boolean add(DisplayPortMetrics metrics) {
+ if (mHead == mTail) {
+ return false;
+ }
+ mMetrics[mTail] = metrics;
+ mTimestamps[mTail] = SystemClock.uptimeMillis();
+ mTail = (mTail + 1) % BUFFER_SIZE;
+ return true;
+ }
+
+ /**
+ * Find the timestamp associated with the given metrics, AND remove
+ * all metrics objects from the start of the queue up to and including
+ * the one provided. Note that because of draw coalescing, the metrics
+ * object passed in here may not be the one at the head of the queue,
+ * and so we must iterate our way through the list to find it.
+ * This must only be called from the compositor thread.
+ */
+ long findTimeFor(DisplayPortMetrics metrics) {
+ // keep a copy of the tail pointer so that we ignore new items
+ // added to the queue while we are searching. this is fine because
+ // the one we are looking for will either have been added already
+ // or will not be in the queue at all.
+ int tail = mTail;
+ // walk through the "safe" range from mHead to tail; these entries
+ // will not be modified unless we change mHead.
+ int i = (mHead + 1) % BUFFER_SIZE;
+ while (i != tail) {
+ if (mMetrics[i].fuzzyEquals(metrics)) {
+ // found it, copy out the timestamp to a local var BEFORE
+ // changing mHead or add could clobber the timestamp.
+ long timestamp = mTimestamps[i];
+ mHead = i;
+ return timestamp;
+ }
+ i = (i + 1) % BUFFER_SIZE;
+ }
+ return -1;
+ }
+
+ /**
+ * Reset the buffer to empty.
+ * This must only be called from the compositor thread.
+ */
+ void reset() {
+ // we can only modify mHead on this thread.
+ mHead = (mTail + BUFFER_SIZE - 1) % BUFFER_SIZE;
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/FloatSize.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/FloatSize.java
new file mode 100644
index 000000000000..4b495ab77ecc
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/FloatSize.java
@@ -0,0 +1,54 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import org.mozilla.gecko.util.FloatUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class FloatSize {
+ public final float width, height;
+
+ public FloatSize(FloatSize size) { width = size.width; height = size.height; }
+ public FloatSize(IntSize size) { width = size.width; height = size.height; }
+ public FloatSize(float aWidth, float aHeight) { width = aWidth; height = aHeight; }
+
+ public FloatSize(JSONObject json) {
+ try {
+ width = (float)json.getDouble("width");
+ height = (float)json.getDouble("height");
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public String toString() { return "(" + width + "," + height + ")"; }
+
+ public boolean isPositive() {
+ return (width > 0 && height > 0);
+ }
+
+ public boolean fuzzyEquals(FloatSize size) {
+ return (FloatUtils.fuzzyEquals(size.width, width) &&
+ FloatUtils.fuzzyEquals(size.height, height));
+ }
+
+ public FloatSize scale(float factor) {
+ return new FloatSize(width * factor, height * factor);
+ }
+
+ /*
+ * Returns the size that represents a linear transition between this size and `to` at time `t`,
+ * which is on the scale [0, 1).
+ */
+ public FloatSize interpolate(FloatSize to, float t) {
+ return new FloatSize(FloatUtils.interpolate(width, to.width, t),
+ FloatUtils.interpolate(height, to.height, t));
+ }
+}
+
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/GLController.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/GLController.java
new file mode 100644
index 000000000000..f7f6b1e3ea1a
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/GLController.java
@@ -0,0 +1,354 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+//import org.mozilla.gecko.GeckoAppShell;
+//import org.mozilla.gecko.GeckoEvent;
+//import org.mozilla.gecko.GeckoThread;
+//import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI;
+import org.libreoffice.LOKitShell;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.util.Log;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+import javax.microedition.khronos.egl.EGLSurface;
+
+/**
+ * EGLPreloadingThread is purely a preloading optimization, not something
+ * we rely on for anything else than performance. We will be initializing
+ * EGL in GLController::initEGL() when we need it, but having EGL initialization
+ * already previously done by EGLPreloadingThread::run() will make it much
+ * faster for GLController to do again.
+ *
+ * For example, here are some timings recorded on two devices:
+ *
+ * Device | EGLPreloadingThread::run() | GLController::initEGL()
+ * -----------------------+----------------------------+------------------------
+ * Nexus S (Android 2.3) | ~ 80 ms | < 0.1 ms
+ * Nexus 10 (Android 4.3) | ~ 35 ms | < 0.1 ms
+ */
+class EGLPreloadingThread extends Thread
+{
+ private static final String LOGTAG = "EGLPreloadingThread";
+ private EGL10 mEGL;
+ private EGLDisplay mEGLDisplay;
+
+ public EGLPreloadingThread()
+ {
+ }
+
+ @Override
+ public void run()
+ {
+ mEGL = (EGL10)EGLContext.getEGL();
+ mEGLDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+ if (mEGLDisplay == EGL10.EGL_NO_DISPLAY) {
+ Log.w(LOGTAG, "Can't get EGL display!");
+ return;
+ }
+
+ int[] returnedVersion = new int[2];
+ if (!mEGL.eglInitialize(mEGLDisplay, returnedVersion)) {
+ Log.w(LOGTAG, "eglInitialize failed");
+ return;
+ }
+ }
+}
+
+/**
+ * This class is a singleton that tracks EGL and compositor things over
+ * the lifetime of Fennec running.
+ * We only ever create one C++ compositor over Fennec's lifetime, but
+ * most of the Java-side objects (e.g. LayerView, GeckoLayerClient,
+ * LayerRenderer) can all get destroyed and re-created if the GeckoApp
+ * activity is destroyed. This GLController is never destroyed, so that
+ * the mCompositorCreated field and other state variables are always
+ * accurate.
+ */
+public class GLController {
+ private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
+ private static final String LOGTAG = "GeckoGLController";
+
+ private static GLController sInstance;
+
+ private LayerView mView;
+ private boolean mServerSurfaceValid;
+ private int mWidth, mHeight;
+
+ /* This is written by the compositor thread (while the UI thread
+ * is blocked on it) and read by the UI thread. */
+ private volatile boolean mCompositorCreated;
+
+ private EGL10 mEGL;
+ private EGLDisplay mEGLDisplay;
+ private EGLConfig mEGLConfig;
+ private EGLPreloadingThread mEGLPreloadingThread;
+ private EGLSurface mEGLSurfaceForCompositor;
+
+ private static final int LOCAL_EGL_OPENGL_ES2_BIT = 4;
+
+ private static final int[] CONFIG_SPEC_16BPP = {
+ EGL10.EGL_RED_SIZE, 5,
+ EGL10.EGL_GREEN_SIZE, 6,
+ EGL10.EGL_BLUE_SIZE, 5,
+ EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT,
+ EGL10.EGL_RENDERABLE_TYPE, LOCAL_EGL_OPENGL_ES2_BIT,
+ EGL10.EGL_NONE
+ };
+
+ private static final int[] CONFIG_SPEC_24BPP = {
+ EGL10.EGL_RED_SIZE, 8,
+ EGL10.EGL_GREEN_SIZE, 8,
+ EGL10.EGL_BLUE_SIZE, 8,
+ EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT,
+ EGL10.EGL_RENDERABLE_TYPE, LOCAL_EGL_OPENGL_ES2_BIT,
+ EGL10.EGL_NONE
+ };
+
+ private GLController() {
+ mEGLPreloadingThread = new EGLPreloadingThread();
+ mEGLPreloadingThread.start();
+ }
+
+ static GLController getInstance(LayerView view) {
+ if (sInstance == null) {
+ sInstance = new GLController();
+ }
+ sInstance.mView = view;
+ return sInstance;
+ }
+
+ synchronized void serverSurfaceDestroyed() {
+ ThreadUtils.assertOnUiThread();
+ Log.w(LOGTAG, "GLController::serverSurfaceDestroyed() with mCompositorCreated=" + mCompositorCreated);
+
+ mServerSurfaceValid = false;
+
+ if (mEGLSurfaceForCompositor != null) {
+ mEGL.eglDestroySurface(mEGLDisplay, mEGLSurfaceForCompositor);
+ mEGLSurfaceForCompositor = null;
+ }
+
+ // We need to coordinate with Gecko when pausing composition, to ensure
+ // that Gecko never executes a draw event while the compositor is paused.
+ // This is sent synchronously to make sure that we don't attempt to use
+ // any outstanding Surfaces after we call this (such as from a
+ // serverSurfaceDestroyed notification), and to make sure that any in-flight
+ // Gecko draw events have been processed. When this returns, composition is
+ // definitely paused -- it'll synchronize with the Gecko event loop, which
+ // in turn will synchronize with the compositor thread.
+ if (mCompositorCreated) {
+ //GeckoAppShell.sendEventToGeckoSync(GeckoEvent.createCompositorPauseEvent());
+ }
+ Log.w(LOGTAG, "done GLController::serverSurfaceDestroyed()");
+ }
+
+ synchronized void serverSurfaceChanged(int newWidth, int newHeight) {
+ ThreadUtils.assertOnUiThread();
+ Log.w(LOGTAG, "GLController::serverSurfaceChanged(" + newWidth + ", " + newHeight + ")");
+
+ mWidth = newWidth;
+ mHeight = newHeight;
+ mServerSurfaceValid = true;
+
+ // we defer to a runnable the task of updating the compositor, because this is going to
+ // call back into createEGLSurfaceForCompositor, which will try to create an EGLSurface
+ // against mView, which we suspect might fail if called too early. By posting this to
+ // mView, we hope to ensure that it is deferred until mView is actually "ready" for some
+ // sense of "ready".
+ mView.post(new Runnable() {
+ @Override
+ public void run() {
+ updateCompositor();
+ }
+ });
+ }
+
+ void updateCompositor() {
+ ThreadUtils.assertOnUiThread();
+ Log.w(LOGTAG, "GLController::updateCompositor with mCompositorCreated=" + mCompositorCreated);
+
+ if (mCompositorCreated) {
+ // If the compositor has already been created, just resume it instead. We don't need
+ // to block here because if the surface is destroyed before the compositor grabs it,
+ // we can handle that gracefully (i.e. the compositor will remain paused).
+ resumeCompositor(mWidth, mHeight);
+ Log.w(LOGTAG, "done GLController::updateCompositor with compositor resume");
+ return;
+ }
+
+ if (!AttemptPreallocateEGLSurfaceForCompositor()) {
+ return;
+ }
+
+ // Only try to create the compositor if we have a valid surface and gecko is up. When these
+ // two conditions are satisfied, we can be relatively sure that the compositor creation will
+ // happen without needing to block anyhwere. Do it with a sync gecko event so that the
+ // android doesn't have a chance to destroy our surface in between.
+ /*if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
+ GeckoAppShell.sendEventToGeckoSync(GeckoEvent.createCompositorCreateEvent(mWidth, mHeight));
+ }*/
+ Log.w(LOGTAG, "done GLController::updateCompositor");
+ }
+
+ void compositorCreated() {
+ Log.w(LOGTAG, "GLController::compositorCreated");
+ // This is invoked on the compositor thread, while the java UI thread
+ // is blocked on the gecko sync event in updateCompositor() above
+ mCompositorCreated = true;
+ }
+
+ public boolean isServerSurfaceValid() {
+ return mServerSurfaceValid;
+ }
+
+ public boolean isCompositorCreated() {
+ return mCompositorCreated;
+ }
+
+ private void initEGL() {
+ if (mEGL != null) {
+ return;
+ }
+
+ // This join() should not be necessary, but makes this code a bit easier to think about.
+ // The EGLPreloadingThread should long be done by now, and even if it's not,
+ // it shouldn't be a problem to be initalizing EGL from two different threads.
+ // Still, having this join() here means that we don't have to wonder about what
+ // kind of caveats might exist with EGL initialization reentrancy on various drivers.
+ try {
+ mEGLPreloadingThread.join();
+ } catch (InterruptedException e) {
+ Log.w(LOGTAG, "EGLPreloadingThread interrupted", e);
+ }
+
+ mEGL = (EGL10)EGLContext.getEGL();
+
+ mEGLDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+ if (mEGLDisplay == EGL10.EGL_NO_DISPLAY) {
+ Log.w(LOGTAG, "Can't get EGL display!");
+ return;
+ }
+
+ // while calling eglInitialize here should not be necessary as it was already called
+ // by the EGLPreloadingThread, it really doesn't cost much to call it again here,
+ // and makes this code easier to think about: EGLPreloadingThread is only a
+ // preloading optimization, not something we rely on for anything else.
+ //
+ // Also note that while calling eglInitialize isn't necessary on Android 4.x
+ // (at least Android's HardwareRenderer does it for us already), it is necessary
+ // on Android 2.x.
+ int[] returnedVersion = new int[2];
+ if (!mEGL.eglInitialize(mEGLDisplay, returnedVersion)) {
+ Log.w(LOGTAG, "eglInitialize failed");
+ return;
+ }
+
+ mEGLConfig = chooseConfig();
+ }
+
+ private EGLConfig chooseConfig() {
+ int[] desiredConfig;
+ int rSize, gSize, bSize;
+ int[] numConfigs = new int[1];
+
+ switch (/*GeckoAppShell*/LOKitShell.getScreenDepth()) {
+ case 24:
+ desiredConfig = CONFIG_SPEC_24BPP;
+ rSize = gSize = bSize = 8;
+ break;
+ case 16:
+ default:
+ desiredConfig = CONFIG_SPEC_16BPP;
+ rSize = 5; gSize = 6; bSize = 5;
+ break;
+ }
+
+ if (!mEGL.eglChooseConfig(mEGLDisplay, desiredConfig, null, 0, numConfigs) ||
+ numConfigs[0] <= 0) {
+ throw new GLControllerException("No available EGL configurations " +
+ getEGLError());
+ }
+
+ EGLConfig[] configs = new EGLConfig[numConfigs[0]];
+ if (!mEGL.eglChooseConfig(mEGLDisplay, desiredConfig, configs, numConfigs[0], numConfigs)) {
+ throw new GLControllerException("No EGL configuration for that specification " +
+ getEGLError());
+ }
+
+ // Select the first configuration that matches the screen depth.
+ int[] red = new int[1], green = new int[1], blue = new int[1];
+ for (EGLConfig config : configs) {
+ mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_RED_SIZE, red);
+ mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_GREEN_SIZE, green);
+ mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_BLUE_SIZE, blue);
+ if (red[0] == rSize && green[0] == gSize && blue[0] == bSize) {
+ return config;
+ }
+ }
+
+ throw new GLControllerException("No suitable EGL configuration found");
+ }
+
+ private synchronized boolean AttemptPreallocateEGLSurfaceForCompositor() {
+ if (mEGLSurfaceForCompositor == null) {
+ initEGL();
+ try {
+ mEGLSurfaceForCompositor = mEGL.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, mView.getNativeWindow(), null);
+ // In failure cases, eglCreateWindowSurface should return EGL_NO_SURFACE.
+ // We currently normalize this to null, and compare to null in all our checks.
+ if (mEGLSurfaceForCompositor == EGL10.EGL_NO_SURFACE) {
+ mEGLSurfaceForCompositor = null;
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "eglCreateWindowSurface threw", e);
+ }
+ }
+ if (mEGLSurfaceForCompositor == null) {
+ Log.w(LOGTAG, "eglCreateWindowSurface returned no surface!");
+ }
+ return mEGLSurfaceForCompositor != null;
+ }
+
+ // @WrapElementForJNI(allowMultithread = true, stubName = "CreateEGLSurfaceForCompositorWrapper")
+ private synchronized EGLSurface createEGLSurfaceForCompositor() {
+ AttemptPreallocateEGLSurfaceForCompositor();
+ EGLSurface result = mEGLSurfaceForCompositor;
+ mEGLSurfaceForCompositor = null;
+ return result;
+ }
+
+ private String getEGLError() {
+ return "Error " + (mEGL == null ? "(no mEGL)" : mEGL.eglGetError());
+ }
+
+ void resumeCompositor(int width, int height) {
+ Log.w(LOGTAG, "GLController::resumeCompositor(" + width + ", " + height + ") and mCompositorCreated=" + mCompositorCreated);
+ // Asking Gecko to resume the compositor takes too long (see
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=735230#c23), so we
+ // resume the compositor directly. We still need to inform Gecko about
+ // the compositor resuming, so that Gecko knows that it can now draw.
+ // It is important to not notify Gecko until after the compositor has
+ // been resumed, otherwise Gecko may send updates that get dropped.
+ if (mCompositorCreated) {
+ //GeckoAppShell.scheduleResumeComposition(width, height);
+ //GeckoAppShell.sendEventToGecko(GeckoEvent.createCompositorResumeEvent());
+ }
+ Log.w(LOGTAG, "done GLController::resumeCompositor");
+ }
+
+ public static class GLControllerException extends RuntimeException {
+ public static final long serialVersionUID = 1L;
+
+ GLControllerException(String e) {
+ super(e);
+ }
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
new file mode 100644
index 000000000000..b0d8859394b1
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
@@ -0,0 +1,1000 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+//import org.mozilla.gecko.GeckoAppShell;
+//import org.mozilla.gecko.GeckoEvent;
+//import org.mozilla.gecko.Tab;
+//import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.ZoomConstraints;
+//import org.mozilla.gecko.mozglue.RobocopTarget;
+//import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI;
+import org.mozilla.gecko.util.EventDispatcher;
+import org.mozilla.gecko.util.FloatUtils;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.SystemClock;
+import android.util.DisplayMetrics;
+import android.util.Log;
+
+public class GeckoLayerClient implements LayerView.Listener, PanZoomTarget
+{
+ private static final String LOGTAG = "GeckoLayerClient";
+
+ private LayerRenderer mLayerRenderer;
+ private boolean mLayerRendererInitialized;
+
+ private Context mContext;
+ private IntSize mScreenSize;
+ private IntSize mWindowSize;
+ private DisplayPortMetrics mDisplayPort;
+
+ private boolean mRecordDrawTimes;
+ private final DrawTimingQueue mDrawTimingQueue;
+
+ private VirtualLayer mRootLayer;
+
+ /* The Gecko viewport as per the UI thread. Must be touched only on the UI thread.
+ * If any events being sent to Gecko that are relative to the Gecko viewport position,
+ * they must (a) be relative to this viewport, and (b) be sent on the UI thread to
+ * avoid races. As long as these two conditions are satisfied, and the events being
+ * sent to Gecko are processed in FIFO order, the events will properly be relative
+ * to the Gecko viewport position. Note that if Gecko updates its viewport independently,
+ * we get notified synchronously and also update this on the UI thread.
+ */
+ private ImmutableViewportMetrics mGeckoViewport;
+
+ /*
+ * The viewport metrics being used to draw the current frame. This is only
+ * accessed by the compositor thread, and so needs no synchronisation.
+ */
+ private ImmutableViewportMetrics mFrameMetrics;
+
+ private DrawListener mDrawListener;
+
+ /* Used as temporaries by syncViewportInfo */
+ private final ViewTransform mCurrentViewTransform;
+ private final RectF mCurrentViewTransformMargins;
+
+ /* Used as the return value of progressiveUpdateCallback */
+ private final ProgressiveUpdateData mProgressiveUpdateData;
+ private DisplayPortMetrics mProgressiveUpdateDisplayPort;
+ private boolean mLastProgressiveUpdateWasLowPrecision;
+ private boolean mProgressiveUpdateWasInDanger;
+
+ private boolean mForceRedraw;
+
+ /* The current viewport metrics.
+ * This is volatile so that we can read and write to it from different threads.
+ * We avoid synchronization to make getting the viewport metrics from
+ * the compositor as cheap as possible. The viewport is immutable so
+ * we don't need to worry about anyone mutating it while we're reading from it.
+ * Specifically:
+ * 1) reading mViewportMetrics from any thread is fine without synchronization
+ * 2) writing to mViewportMetrics requires synchronizing on the layer controller object
+ * 3) whenver reading multiple fields from mViewportMetrics without synchronization (i.e. in
+ * case 1 above) you should always frist grab a local copy of the reference, and then use
+ * that because mViewportMetrics might get reassigned in between reading the different
+ * fields. */
+ private volatile ImmutableViewportMetrics mViewportMetrics;
+ private OnMetricsChangedListener mViewportChangeListener;
+
+ private ZoomConstraints mZoomConstraints;
+
+ private boolean mGeckoIsReady;
+
+ private final PanZoomController mPanZoomController;
+ private final LayerMarginsAnimator mMarginsAnimator;
+ private LayerView mView;
+
+ /* This flag is true from the time that browser.js detects a first-paint is about to start,
+ * to the time that we receive the first-paint composite notification from the compositor.
+ * Note that there is a small race condition with this; if there are two paints that both
+ * have the first-paint flag set, and the second paint happens concurrently with the
+ * composite for the first paint, then this flag may be set to true prematurely. Fixing this
+ * is possible but risky; see https://bugzilla.mozilla.org/show_bug.cgi?id=797615#c751
+ */
+ private volatile boolean mContentDocumentIsDisplayed;
+
+ public GeckoLayerClient(Context context, LayerView view, EventDispatcher eventDispatcher) {
+ // we can fill these in with dummy values because they are always written
+ // to before being read
+ mContext = context;
+ mScreenSize = new IntSize(0, 0);
+ mWindowSize = new IntSize(0, 0);
+ mDisplayPort = new DisplayPortMetrics();
+ mRecordDrawTimes = true;
+ mDrawTimingQueue = new DrawTimingQueue();
+ mCurrentViewTransform = new ViewTransform(0, 0, 1);
+ mCurrentViewTransformMargins = new RectF();
+ mProgressiveUpdateData = new ProgressiveUpdateData();
+ mProgressiveUpdateDisplayPort = new DisplayPortMetrics();
+ mLastProgressiveUpdateWasLowPrecision = false;
+ mProgressiveUpdateWasInDanger = false;
+
+ mForceRedraw = true;
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ mViewportMetrics = new ImmutableViewportMetrics(displayMetrics)
+ .setViewportSize(view.getWidth(), view.getHeight());
+ mZoomConstraints = new ZoomConstraints(false);
+
+ /*Tab tab = Tabs.getInstance().getSelectedTab();
+ if (tab != null) {
+ mZoomConstraints = tab.getZoomConstraints();
+ mViewportMetrics = mViewportMetrics.setIsRTL(tab.getIsRTL());
+ }*/
+
+ mFrameMetrics = mViewportMetrics;
+
+ mPanZoomController = PanZoomController.Factory.create(this, view, eventDispatcher);
+ mMarginsAnimator = new LayerMarginsAnimator(this, view);
+ mView = view;
+ mView.setListener(this);
+ mContentDocumentIsDisplayed = true;
+ }
+
+ public void setOverscrollHandler(final Overscroll listener) {
+ mPanZoomController.setOverscrollHandler(listener);
+ }
+
+ /** Attaches to root layer so that Gecko appears. */
+ public void notifyGeckoReady() {
+ mGeckoIsReady = true;
+
+ mRootLayer = new VirtualLayer(new IntSize(mView.getWidth(), mView.getHeight()));
+ mLayerRenderer = mView.getRenderer();
+
+ sendResizeEventIfNecessary(true);
+
+ DisplayPortCalculator.initPrefs();
+
+ // Gecko being ready is one of the two conditions (along with having an available
+ // surface) that cause us to create the compositor. So here, now that we know gecko
+ // is ready, call updateCompositor() to see if we can actually do the creation.
+ // This needs to run on the UI thread so that the surface validity can't change on
+ // us while we're in the middle of creating the compositor.
+ mView.post(new Runnable() {
+ @Override
+ public void run() {
+ mView.getGLController().updateCompositor();
+ }
+ });
+ }
+
+ public void destroy() {
+ mPanZoomController.destroy();
+ mMarginsAnimator.destroy();
+ }
+
+ /**
+ * Returns true if this client is fine with performing a redraw operation or false if it
+ * would prefer that the action didn't take place.
+ */
+ private boolean getRedrawHint() {
+ if (mForceRedraw) {
+ mForceRedraw = false;
+ return true;
+ }
+
+ if (!mPanZoomController.getRedrawHint()) {
+ return false;
+ }
+
+ return DisplayPortCalculator.aboutToCheckerboard(mViewportMetrics,
+ mPanZoomController.getVelocityVector(), mDisplayPort);
+ }
+
+ Layer getRoot() {
+ return mGeckoIsReady ? mRootLayer : null;
+ }
+
+ public LayerView getView() {
+ return mView;
+ }
+
+ public FloatSize getViewportSize() {
+ return mViewportMetrics.getSize();
+ }
+
+ /**
+ * The view calls this function to indicate that the viewport changed size. It must hold the
+ * monitor while calling it.
+ *
+ * TODO: Refactor this to use an interface. Expose that interface only to the view and not
+ * to the layer client. That way, the layer client won't be tempted to call this, which might
+ * result in an infinite loop.
+ */
+ void setViewportSize(int width, int height) {
+ mViewportMetrics = mViewportMetrics.setViewportSize(width, height);
+
+ if (mGeckoIsReady) {
+ // here we send gecko a resize message. The code in browser.js is responsible for
+ // picking up on that resize event, modifying the viewport as necessary, and informing
+ // us of the new viewport.
+ sendResizeEventIfNecessary(true);
+ // the following call also sends gecko a message, which will be processed after the resize
+ // message above has updated the viewport. this message ensures that if we have just put
+ // focus in a text field, we scroll the content so that the text field is in view.
+
+ //GeckoAppShell.viewSizeChanged();
+ }
+ }
+
+ PanZoomController getPanZoomController() {
+ return mPanZoomController;
+ }
+
+ LayerMarginsAnimator getLayerMarginsAnimator() {
+ return mMarginsAnimator;
+ }
+
+ /* Informs Gecko that the screen size has changed. */
+ private void sendResizeEventIfNecessary(boolean force) {
+ DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
+
+ IntSize newScreenSize = new IntSize(metrics.widthPixels, metrics.heightPixels);
+ IntSize newWindowSize = new IntSize(mView.getWidth(), mView.getHeight());
+
+ boolean screenSizeChanged = !mScreenSize.equals(newScreenSize);
+ boolean windowSizeChanged = !mWindowSize.equals(newWindowSize);
+
+ if (!force && !screenSizeChanged && !windowSizeChanged) {
+ return;
+ }
+
+ mScreenSize = newScreenSize;
+ mWindowSize = newWindowSize;
+
+ if (screenSizeChanged) {
+ Log.d(LOGTAG, "Screen-size changed to " + mScreenSize);
+ }
+
+ if (windowSizeChanged) {
+ Log.d(LOGTAG, "Window-size changed to " + mWindowSize);
+ }
+
+ /*GeckoEvent event = GeckoEvent.createSizeChangedEvent(mWindowSize.width, mWindowSize.height,
+ mScreenSize.width, mScreenSize.height);
+ GeckoAppShell.sendEventToGecko(event);
+ GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Window:Resize", ""));*/
+ }
+
+ /** Sets the current page rect. You must hold the monitor while calling this. */
+ private void setPageRect(RectF rect, RectF cssRect) {
+ // Since the "rect" is always just a multiple of "cssRect" we don't need to
+ // check both; this function assumes that both "rect" and "cssRect" are relative
+ // the zoom factor in mViewportMetrics.
+ if (mViewportMetrics.getCssPageRect().equals(cssRect))
+ return;
+
+ mViewportMetrics = mViewportMetrics.setPageRect(rect, cssRect);
+
+ // Page size is owned by the layer client, so no need to notify it of
+ // this change.
+
+ post(new Runnable() {
+ @Override
+ public void run() {
+ mPanZoomController.pageRectUpdated();
+ mView.requestRender();
+ }
+ });
+ }
+
+ /**
+ * Derives content document fixed position margins/fixed layer margins from
+ * the view margins in the given metrics object.
+ */
+ private void getFixedMargins(ImmutableViewportMetrics metrics, RectF fixedMargins) {
+ fixedMargins.left = 0;
+ fixedMargins.top = 0;
+ fixedMargins.right = 0;
+ fixedMargins.bottom = 0;
+
+ // The maximum margins are determined by the scrollable area of the page.
+ float maxMarginWidth = Math.max(0, metrics.getPageWidth() - metrics.getWidthWithoutMargins());
+ float maxMarginHeight = Math.max(0, metrics.getPageHeight() - metrics.getHeightWithoutMargins());
+
+ // If the margins can't fully hide, they're pinned on - in which case,
+ // fixed margins should always be zero.
+ if (maxMarginWidth < metrics.marginLeft + metrics.marginRight) {
+ maxMarginWidth = 0;
+ }
+ if (maxMarginHeight < metrics.marginTop + metrics.marginBottom) {
+ maxMarginHeight = 0;
+ }
+
+ PointF offset = metrics.getMarginOffset();
+ RectF overscroll = metrics.getOverscroll();
+ if (offset.x >= 0) {
+ fixedMargins.right = Math.max(0, Math.min(offset.x - overscroll.right, maxMarginWidth));
+ } else {
+ fixedMargins.left = Math.max(0, Math.min(-offset.x - overscroll.left, maxMarginWidth));
+ }
+ if (offset.y >= 0) {
+ fixedMargins.bottom = Math.max(0, Math.min(offset.y - overscroll.bottom, maxMarginHeight));
+ } else {
+ fixedMargins.top = Math.max(0, Math.min(-offset.y - overscroll.top, maxMarginHeight));
+ }
+
+ // Adjust for overscroll. If we're overscrolled on one side, add that
+ // distance to the margins of the other side (limiting to the maximum
+ // margin size calculated above).
+ if (overscroll.left > 0) {
+ fixedMargins.right = Math.min(maxMarginWidth - fixedMargins.left,
+ fixedMargins.right + overscroll.left);
+ } else if (overscroll.right > 0) {
+ fixedMargins.left = Math.min(maxMarginWidth - fixedMargins.right,
+ fixedMargins.left + overscroll.right);
+ }
+ if (overscroll.top > 0) {
+ fixedMargins.bottom = Math.min(maxMarginHeight - fixedMargins.top,
+ fixedMargins.bottom + overscroll.top);
+ } else if (overscroll.bottom > 0) {
+ fixedMargins.top = Math.min(maxMarginHeight - fixedMargins.bottom,
+ fixedMargins.top + overscroll.bottom);
+ }
+ }
+
+ private void adjustViewport(DisplayPortMetrics displayPort) {
+ ImmutableViewportMetrics metrics = getViewportMetrics();
+ ImmutableViewportMetrics clampedMetrics = metrics.clamp();
+
+ RectF margins = new RectF();
+ getFixedMargins(metrics, margins);
+ clampedMetrics = clampedMetrics.setMargins(
+ margins.left, margins.top, margins.right, margins.bottom);
+
+ if (displayPort == null) {
+ displayPort = DisplayPortCalculator.calculate(metrics, mPanZoomController.getVelocityVector());
+ }
+
+ mDisplayPort = displayPort;
+ mGeckoViewport = clampedMetrics;
+
+ if (mRecordDrawTimes) {
+ mDrawTimingQueue.add(displayPort);
+ }
+
+ //GeckoAppShell.sendEventToGecko(GeckoEvent.createViewportEvent(clampedMetrics, displayPort));
+ }
+
+ /** Aborts any pan/zoom animation that is currently in progress. */
+ private void abortPanZoomAnimation() {
+ if (mPanZoomController != null) {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ mPanZoomController.abortAnimation();
+ }
+ });
+ }
+ }
+
+ /**
+ * The different types of Viewport messages handled. All viewport events
+ * expect a display-port to be returned, but can handle one not being
+ * returned.
+ */
+ private enum ViewportMessageType {
+ UPDATE, // The viewport has changed and should be entirely updated
+ PAGE_SIZE // The viewport's page-size has changed
+ }
+
+ /** Viewport message handler. */
+ private DisplayPortMetrics handleViewportMessage(ImmutableViewportMetrics messageMetrics, ViewportMessageType type) {
+ synchronized (getLock()) {
+ ImmutableViewportMetrics newMetrics;
+ ImmutableViewportMetrics oldMetrics = getViewportMetrics();
+
+ switch (type) {
+ default:
+ case UPDATE:
+ // Keep the old viewport size
+ newMetrics = messageMetrics.setViewportSize(oldMetrics.getWidth(), oldMetrics.getHeight());
+ if (!oldMetrics.fuzzyEquals(newMetrics)) {
+ abortPanZoomAnimation();
+ }
+ break;
+ case PAGE_SIZE:
+ // adjust the page dimensions to account for differences in zoom
+ // between the rendered content (which is what Gecko tells us)
+ // and our zoom level (which may have diverged).
+ float scaleFactor = oldMetrics.zoomFactor / messageMetrics.zoomFactor;
+ newMetrics = oldMetrics.setPageRect(RectUtils.scale(messageMetrics.getPageRect(), scaleFactor), messageMetrics.getCssPageRect());
+ break;
+ }
+
+ // Update the Gecko-side viewport metrics. Make sure to do this
+ // before modifying the metrics below.
+ final ImmutableViewportMetrics geckoMetrics = newMetrics.clamp();
+ post(new Runnable() {
+ @Override
+ public void run() {
+ mGeckoViewport = geckoMetrics;
+ }
+ });
+
+ setViewportMetrics(newMetrics, type == ViewportMessageType.UPDATE);
+ mDisplayPort = DisplayPortCalculator.calculate(getViewportMetrics(), null);
+ }
+ return mDisplayPort;
+ }
+
+ //@WrapElementForJNI
+ DisplayPortMetrics getDisplayPort(boolean pageSizeUpdate, boolean isBrowserContentDisplayed, int tabId, ImmutableViewportMetrics metrics) {
+ /**Tabs tabs = Tabs.getInstance();
+ if (isBrowserContentDisplayed && tabs.isSelectedTabId(tabId)) {
+ // for foreground tabs, send the viewport update unless the document
+ // displayed is different from the content document. In that case, just
+ // calculate the display port.
+ return handleViewportMessage(metrics, pageSizeUpdate ? ViewportMessageType.PAGE_SIZE : ViewportMessageType.UPDATE);
+ } else*/ {
+ // for background tabs, request a new display port calculation, so that
+ // when we do switch to that tab, we have the correct display port and
+ // don't need to draw twice (once to allow the first-paint viewport to
+ // get to java, and again once java figures out the display port).
+ return DisplayPortCalculator.calculate(metrics, null);
+ }
+ }
+
+ //@WrapElementForJNI
+ void contentDocumentChanged() {
+ mContentDocumentIsDisplayed = false;
+ }
+
+ //@WrapElementForJNI
+ boolean isContentDocumentDisplayed() {
+ return mContentDocumentIsDisplayed;
+ }
+
+ // This is called on the Gecko thread to determine if we're still interested
+ // in the update of this display-port to continue. We can return true here
+ // to abort the current update and continue with any subsequent ones. This
+ // is useful for slow-to-render pages when the display-port starts lagging
+ // behind enough that continuing to draw it is wasted effort.
+ //@WrapElementForJNI(allowMultithread = true)
+ public ProgressiveUpdateData progressiveUpdateCallback(boolean aHasPendingNewThebesContent,
+ float x, float y, float width, float height,
+ float resolution, boolean lowPrecision) {
+ // Reset the checkerboard risk flag when switching to low precision
+ // rendering.
+ if (lowPrecision && !mLastProgressiveUpdateWasLowPrecision) {
+ // Skip low precision rendering until we're at risk of checkerboarding.
+ if (!mProgressiveUpdateWasInDanger) {
+ mProgressiveUpdateData.abort = true;
+ return mProgressiveUpdateData;
+ }
+ mProgressiveUpdateWasInDanger = false;
+ }
+ mLastProgressiveUpdateWasLowPrecision = lowPrecision;
+
+ // Grab a local copy of the last display-port sent to Gecko and the
+ // current viewport metrics to avoid races when accessing them.
+ DisplayPortMetrics displayPort = mDisplayPort;
+ ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
+ mProgressiveUpdateData.setViewport(viewportMetrics);
+ mProgressiveUpdateData.abort = false;
+
+ // Always abort updates if the resolution has changed. There's no use
+ // in drawing at the incorrect resolution.
+ if (!FloatUtils.fuzzyEquals(resolution, viewportMetrics.zoomFactor)) {
+ Log.d(LOGTAG, "Aborting draw due to resolution change: " + resolution + " != " + viewportMetrics.zoomFactor);
+ mProgressiveUpdateData.abort = true;
+ return mProgressiveUpdateData;
+ }
+
+ // Store the high precision displayport for comparison when doing low
+ // precision updates.
+ if (!lowPrecision) {
+ if (!FloatUtils.fuzzyEquals(resolution, mProgressiveUpdateDisplayPort.resolution) ||
+ !FloatUtils.fuzzyEquals(x, mProgressiveUpdateDisplayPort.getLeft()) ||
+ !FloatUtils.fuzzyEquals(y, mProgressiveUpdateDisplayPort.getTop()) ||
+ !FloatUtils.fuzzyEquals(x + width, mProgressiveUpdateDisplayPort.getRight()) ||
+ !FloatUtils.fuzzyEquals(y + height, mProgressiveUpdateDisplayPort.getBottom())) {
+ mProgressiveUpdateDisplayPort =
+ new DisplayPortMetrics(x, y, x+width, y+height, resolution);
+ }
+ }
+
+ // If we're not doing low precision draws and we're about to
+ // checkerboard, enable low precision drawing.
+ if (!lowPrecision && !mProgressiveUpdateWasInDanger) {
+ if (DisplayPortCalculator.aboutToCheckerboard(viewportMetrics,
+ mPanZoomController.getVelocityVector(), mProgressiveUpdateDisplayPort)) {
+ mProgressiveUpdateWasInDanger = true;
+ }
+ }
+
+ // XXX All sorts of rounding happens inside Gecko that becomes hard to
+ // account exactly for. Given we align the display-port to tile
+ // boundaries (and so they rarely vary by sub-pixel amounts), just
+ // check that values are within a couple of pixels of the
+ // display-port bounds.
+
+ // Never abort drawing if we can't be sure we've sent a more recent
+ // display-port. If we abort updating when we shouldn't, we can end up
+ // with blank regions on the screen and we open up the risk of entering
+ // an endless updating cycle.
+ if (Math.abs(displayPort.getLeft() - mProgressiveUpdateDisplayPort.getLeft()) <= 2 &&
+ Math.abs(displayPort.getTop() - mProgressiveUpdateDisplayPort.getTop()) <= 2 &&
+ Math.abs(displayPort.getBottom() - mProgressiveUpdateDisplayPort.getBottom()) <= 2 &&
+ Math.abs(displayPort.getRight() - mProgressiveUpdateDisplayPort.getRight()) <= 2) {
+ return mProgressiveUpdateData;
+ }
+
+ // Abort updates when the display-port no longer contains the visible
+ // area of the page (that is, the viewport cropped by the page
+ // boundaries).
+ // XXX This makes the assumption that we never let the visible area of
+ // the page fall outside of the display-port.
+ if (Math.max(viewportMetrics.viewportRectLeft, viewportMetrics.pageRectLeft) + 1 < x ||
+ Math.max(viewportMetrics.viewportRectTop, viewportMetrics.pageRectTop) + 1 < y ||
+ Math.min(viewportMetrics.viewportRectRight, viewportMetrics.pageRectRight) - 1 > x + width ||
+ Math.min(viewportMetrics.viewportRectBottom, viewportMetrics.pageRectBottom) - 1 > y + height) {
+ Log.d(LOGTAG, "Aborting update due to viewport not in display-port");
+ mProgressiveUpdateData.abort = true;
+
+ // Enable low-precision drawing, as we're likely to be in danger if
+ // this situation has been encountered.
+ mProgressiveUpdateWasInDanger = true;
+
+ return mProgressiveUpdateData;
+ }
+
+ // Abort drawing stale low-precision content if there's a more recent
+ // display-port in the pipeline.
+ if (lowPrecision && !aHasPendingNewThebesContent) {
+ mProgressiveUpdateData.abort = true;
+ }
+ return mProgressiveUpdateData;
+ }
+
+ void setZoomConstraints(ZoomConstraints constraints) {
+ mZoomConstraints = constraints;
+ }
+
+ void setIsRTL(boolean aIsRTL) {
+ synchronized (getLock()) {
+ ImmutableViewportMetrics newMetrics = getViewportMetrics().setIsRTL(aIsRTL);
+ setViewportMetrics(newMetrics, false);
+ }
+ }
+
+ /** The compositor invokes this function just before compositing a frame where the document
+ * is different from the document composited on the last frame. In these cases, the viewport
+ * information we have in Java is no longer valid and needs to be replaced with the new
+ * viewport information provided. setPageRect will never be invoked on the same frame that
+ * this function is invoked on; and this function will always be called prior to syncViewportInfo.
+ */
+ //@WrapElementForJNI(allowMultithread = true)
+ public void setFirstPaintViewport(float offsetX, float offsetY, float zoom,
+ float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom) {
+ synchronized (getLock()) {
+ ImmutableViewportMetrics currentMetrics = getViewportMetrics();
+
+ //Tab tab = Tabs.getInstance().getSelectedTab();
+
+ RectF cssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom);
+ RectF pageRect = RectUtils.scaleAndRound(cssPageRect, zoom);
+
+ final ImmutableViewportMetrics newMetrics = currentMetrics
+ .setViewportOrigin(offsetX, offsetY)
+ .setZoomFactor(zoom)
+ .setPageRect(pageRect, cssPageRect)
+ /*.setIsRTL(tab.getIsRTL())*/;
+ // Since we have switched to displaying a different document, we need to update any
+ // viewport-related state we have lying around. This includes mGeckoViewport and
+ // mViewportMetrics. Usually this information is updated via handleViewportMessage
+ // while we remain on the same document.
+ post(new Runnable() {
+ @Override
+ public void run() {
+ mGeckoViewport = newMetrics;
+ }
+ });
+
+ setViewportMetrics(newMetrics);
+
+ //mView.setBackgroundColor(tab.getBackgroundColor());
+ //setZoomConstraints(tab.getZoomConstraints());
+
+ // At this point, we have just switched to displaying a different document than we
+ // we previously displaying. This means we need to abort any panning/zooming animations
+ // that are in progress and send an updated display port request to browser.js as soon
+ // as possible. The call to PanZoomController.abortAnimation accomplishes this by calling the
+ // forceRedraw function, which sends the viewport to gecko. The display port request is
+ // actually a full viewport update, which is fine because if browser.js has somehow moved to
+ // be out of sync with this first-paint viewport, then we force them back in sync.
+ abortPanZoomAnimation();
+
+ // Indicate that the document is about to be composited so the
+ // LayerView background can be removed.
+ if (mView.getPaintState() == LayerView.PAINT_START) {
+ mView.setPaintState(LayerView.PAINT_BEFORE_FIRST);
+ }
+ }
+ DisplayPortCalculator.resetPageState();
+ mDrawTimingQueue.reset();
+
+ mContentDocumentIsDisplayed = true;
+ }
+
+ /** The compositor invokes this function whenever it determines that the page rect
+ * has changed (based on the information it gets from layout). If setFirstPaintViewport
+ * is invoked on a frame, then this function will not be. For any given frame, this
+ * function will be invoked before syncViewportInfo.
+ */
+ //@WrapElementForJNI(allowMultithread = true)
+ public void setPageRect(float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom) {
+ synchronized (getLock()) {
+ RectF cssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom);
+ float ourZoom = getViewportMetrics().zoomFactor;
+ setPageRect(RectUtils.scale(cssPageRect, ourZoom), cssPageRect);
+ // Here the page size of the document has changed, but the document being displayed
+ // is still the same. Therefore, we don't need to send anything to browser.js; any
+ // changes we need to make to the display port will get sent the next time we call
+ // adjustViewport().
+ }
+ }
+
+ /** The compositor invokes this function on every frame to figure out what part of the
+ * page to display, and to inform Java of the current display port. Since it is called
+ * on every frame, it needs to be ultra-fast.
+ * It avoids taking any locks or allocating any objects. We keep around a
+ * mCurrentViewTransform so we don't need to allocate a new ViewTransform
+ * everytime we're called. NOTE: we might be able to return a ImmutableViewportMetrics
+ * which would avoid the copy into mCurrentViewTransform.
+ */
+ //@WrapElementForJNI(allowMultithread = true)
+ public ViewTransform syncViewportInfo(int x, int y, int width, int height, float resolution, boolean layersUpdated) {
+ // getViewportMetrics is thread safe so we don't need to synchronize.
+ // We save the viewport metrics here, so we later use it later in
+ // createFrame (which will be called by nsWindow::DrawWindowUnderlay on
+ // the native side, by the compositor). The viewport
+ // metrics can change between here and there, as it's accessed outside
+ // of the compositor thread.
+ mFrameMetrics = getViewportMetrics();
+
+ mCurrentViewTransform.x = mFrameMetrics.viewportRectLeft;
+ mCurrentViewTransform.y = mFrameMetrics.viewportRectTop;
+ mCurrentViewTransform.scale = mFrameMetrics.zoomFactor;
+
+ // Adjust the fixed layer margins so that overscroll subtracts from them.
+ getFixedMargins(mFrameMetrics, mCurrentViewTransformMargins);
+ mCurrentViewTransform.fixedLayerMarginLeft = mCurrentViewTransformMargins.left;
+ mCurrentViewTransform.fixedLayerMarginTop = mCurrentViewTransformMargins.top;
+ mCurrentViewTransform.fixedLayerMarginRight = mCurrentViewTransformMargins.right;
+ mCurrentViewTransform.fixedLayerMarginBottom = mCurrentViewTransformMargins.bottom;
+
+ // Offset the view transform so that it renders in the correct place.
+ PointF offset = mFrameMetrics.getMarginOffset();
+ mCurrentViewTransform.offsetX = offset.x;
+ mCurrentViewTransform.offsetY = offset.y;
+
+ mRootLayer.setPositionAndResolution(
+ Math.round(x + mCurrentViewTransform.offsetX),
+ Math.round(y + mCurrentViewTransform.offsetY),
+ Math.round(x + width + mCurrentViewTransform.offsetX),
+ Math.round(y + height + mCurrentViewTransform.offsetY),
+ resolution);
+
+ if (layersUpdated && mRecordDrawTimes) {
+ // If we got a layers update, that means a draw finished. Check to see if the area drawn matches
+ // one of our requested displayports; if it does calculate the draw time and notify the
+ // DisplayPortCalculator
+ DisplayPortMetrics drawn = new DisplayPortMetrics(x, y, x + width, y + height, resolution);
+ long time = mDrawTimingQueue.findTimeFor(drawn);
+ if (time >= 0) {
+ long now = SystemClock.uptimeMillis();
+ time = now - time;
+ mRecordDrawTimes = DisplayPortCalculator.drawTimeUpdate(time, width * height);
+ }
+ }
+
+ if (layersUpdated && mDrawListener != null) {
+ /* Used by robocop for testing purposes */
+ mDrawListener.drawFinished();
+ }
+
+ return mCurrentViewTransform;
+ }
+
+ //@WrapElementForJNI(allowMultithread = true)
+ public ViewTransform syncFrameMetrics(float offsetX, float offsetY, float zoom,
+ float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom,
+ boolean layersUpdated, int x, int y, int width, int height, float resolution,
+ boolean isFirstPaint)
+ {
+ if (isFirstPaint) {
+ setFirstPaintViewport(offsetX, offsetY, zoom,
+ cssPageLeft, cssPageTop, cssPageRight, cssPageBottom);
+ }
+
+ return syncViewportInfo(x, y, width, height, resolution, layersUpdated);
+ }
+
+ //@WrapElementForJNI(allowMultithread = true)
+ public LayerRenderer.Frame createFrame() {
+ // Create the shaders and textures if necessary.
+ if (!mLayerRendererInitialized) {
+ mLayerRenderer.checkMonitoringEnabled();
+ mLayerRenderer.createDefaultProgram();
+ mLayerRendererInitialized = true;
+ }
+
+ return mLayerRenderer.createFrame(mFrameMetrics);
+ }
+
+ //@WrapElementForJNI(allowMultithread = true)
+ public void activateProgram() {
+ mLayerRenderer.activateDefaultProgram();
+ }
+
+ //@WrapElementForJNI(allowMultithread = true)
+ public void deactivateProgram() {
+ mLayerRenderer.deactivateDefaultProgram();
+ }
+
+ private void geometryChanged(DisplayPortMetrics displayPort) {
+ /* Let Gecko know if the screensize has changed */
+ sendResizeEventIfNecessary(false);
+ if (getRedrawHint()) {
+ adjustViewport(displayPort);
+ }
+ }
+
+ /** Implementation of LayerView.Listener */
+ @Override
+ public void renderRequested() {
+ try {
+ //GeckoAppShell.scheduleComposite();
+ } catch (UnsupportedOperationException uoe) {
+ // In some very rare cases this gets called before libxul is loaded,
+ // so catch and ignore the exception that will throw. See bug 837821
+ Log.d(LOGTAG, "Dropping renderRequested call before libxul load.");
+ }
+ }
+
+ /** Implementation of LayerView.Listener */
+ @Override
+ public void sizeChanged(int width, int height) {
+ // We need to make sure a draw happens synchronously at this point,
+ // but resizing the surface before the SurfaceView has resized will
+ // cause a visible jump.
+ mView.getGLController().resumeCompositor(mWindowSize.width, mWindowSize.height);
+ }
+
+ /** Implementation of LayerView.Listener */
+ @Override
+ public void surfaceChanged(int width, int height) {
+ setViewportSize(width, height);
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public ImmutableViewportMetrics getViewportMetrics() {
+ return mViewportMetrics;
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public ZoomConstraints getZoomConstraints() {
+ return mZoomConstraints;
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public boolean isFullScreen() {
+ return mView.isFullScreen();
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public RectF getMaxMargins() {
+ return mMarginsAnimator.getMaxMargins();
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public void setAnimationTarget(ImmutableViewportMetrics metrics) {
+ if (mGeckoIsReady) {
+ // We know what the final viewport of the animation is going to be, so
+ // immediately request a draw of that area by setting the display port
+ // accordingly. This way we should have the content pre-rendered by the
+ // time the animation is done.
+ DisplayPortMetrics displayPort = DisplayPortCalculator.calculate(metrics, null);
+ adjustViewport(displayPort);
+ }
+ }
+
+ /** Implementation of PanZoomTarget
+ * You must hold the monitor while calling this.
+ */
+ @Override
+ public void setViewportMetrics(ImmutableViewportMetrics metrics) {
+ setViewportMetrics(metrics, true);
+ }
+
+ /*
+ * You must hold the monitor while calling this.
+ */
+ private void setViewportMetrics(ImmutableViewportMetrics metrics, boolean notifyGecko) {
+ // This class owns the viewport size and the fixed layer margins; don't let other pieces
+ // of code clobber either of them. The only place the viewport size should ever be
+ // updated is in GeckoLayerClient.setViewportSize, and the only place the margins should
+ // ever be updated is in GeckoLayerClient.setFixedLayerMargins; both of these assign to
+ // mViewportMetrics directly.
+ metrics = metrics.setViewportSize(mViewportMetrics.getWidth(), mViewportMetrics.getHeight());
+ metrics = metrics.setMarginsFrom(mViewportMetrics);
+ mViewportMetrics = metrics;
+
+ viewportMetricsChanged(notifyGecko);
+ }
+
+ /*
+ * You must hold the monitor while calling this.
+ */
+ private void viewportMetricsChanged(boolean notifyGecko) {
+ if (mViewportChangeListener != null) {
+ mViewportChangeListener.onMetricsChanged(mViewportMetrics);
+ }
+
+ mView.requestRender();
+ if (notifyGecko && mGeckoIsReady) {
+ geometryChanged(null);
+ }
+ }
+
+ /*
+ * Updates the viewport metrics, overriding the viewport size and margins
+ * which are normally retained when calling setViewportMetrics.
+ * You must hold the monitor while calling this.
+ */
+ void forceViewportMetrics(ImmutableViewportMetrics metrics, boolean notifyGecko, boolean forceRedraw) {
+ if (forceRedraw) {
+ mForceRedraw = true;
+ }
+ mViewportMetrics = metrics;
+ viewportMetricsChanged(notifyGecko);
+ }
+
+ /** Implementation of PanZoomTarget
+ * Scroll the viewport by a certain amount. This will take viewport margins
+ * and margin animation into account. If margins are currently animating,
+ * this will just go ahead and modify the viewport origin, otherwise the
+ * delta will be applied to the margins and the remainder will be applied to
+ * the viewport origin.
+ *
+ * You must hold the monitor while calling this.
+ */
+ @Override
+ public void scrollBy(float dx, float dy) {
+ // Set mViewportMetrics manually so the margin changes take.
+ mViewportMetrics = mMarginsAnimator.scrollBy(mViewportMetrics, dx, dy);
+ viewportMetricsChanged(true);
+ }
+
+ /** Implementation of PanZoomTarget
+ * Notification that a subdocument has been scrolled by a certain amount.
+ * This is used here to make sure that the margins are still accessible
+ * during subdocument scrolling.
+ *
+ * You must hold the monitor while calling this.
+ */
+ @Override
+ public void scrollMarginsBy(float dx, float dy) {
+ ImmutableViewportMetrics newMarginsMetrics =
+ mMarginsAnimator.scrollBy(mViewportMetrics, dx, dy);
+ mViewportMetrics = mViewportMetrics.setMarginsFrom(newMarginsMetrics);
+ viewportMetricsChanged(true);
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public void panZoomStopped() {
+ if (mViewportChangeListener != null) {
+ mViewportChangeListener.onPanZoomStopped();
+ }
+ }
+
+ public interface OnMetricsChangedListener {
+ public void onMetricsChanged(ImmutableViewportMetrics viewport);
+ public void onPanZoomStopped();
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public void forceRedraw(DisplayPortMetrics displayPort) {
+ mForceRedraw = true;
+ if (mGeckoIsReady) {
+ geometryChanged(displayPort);
+ }
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public boolean post(Runnable action) {
+ return mView.post(action);
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public void postRenderTask(RenderTask task) {
+ mView.postRenderTask(task);
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public void removeRenderTask(RenderTask task) {
+ mView.removeRenderTask(task);
+ }
+
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public boolean postDelayed(Runnable action, long delayMillis) {
+ return mView.postDelayed(action, delayMillis);
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public Object getLock() {
+ return this;
+ }
+
+ /** Implementation of PanZoomTarget
+ * Converts a point from layer view coordinates to layer coordinates. In other words, given a
+ * point measured in pixels from the top left corner of the layer view, returns the point in
+ * pixels measured from the last scroll position we sent to Gecko, in CSS pixels. Assuming the
+ * events being sent to Gecko are processed in FIFO order, this calculation should always be
+ * correct.
+ */
+ @Override
+ public PointF convertViewPointToLayerPoint(PointF viewPoint) {
+ if (!mGeckoIsReady) {
+ return null;
+ }
+
+ ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
+ PointF origin = viewportMetrics.getOrigin();
+ PointF offset = viewportMetrics.getMarginOffset();
+ origin.offset(-offset.x, -offset.y);
+ float zoom = viewportMetrics.zoomFactor;
+ ImmutableViewportMetrics geckoViewport = mGeckoViewport;
+ PointF geckoOrigin = geckoViewport.getOrigin();
+ float geckoZoom = geckoViewport.zoomFactor;
+
+ // viewPoint + origin - offset gives the coordinate in device pixels from the top-left corner of the page.
+ // Divided by zoom, this gives us the coordinate in CSS pixels from the top-left corner of the page.
+ // geckoOrigin / geckoZoom is where Gecko thinks it is (scrollTo position) in CSS pixels from
+ // the top-left corner of the page. Subtracting the two gives us the offset of the viewPoint from
+ // the current Gecko coordinate in CSS pixels.
+ PointF layerPoint = new PointF(
+ ((viewPoint.x + origin.x) / zoom) - (geckoOrigin.x / geckoZoom),
+ ((viewPoint.y + origin.y) / zoom) - (geckoOrigin.y / geckoZoom));
+
+ return layerPoint;
+ }
+
+ public void setOnMetricsChangedListener(OnMetricsChangedListener listener) {
+ mViewportChangeListener = listener;
+ }
+
+ /** Used by robocop for testing purposes. Not for production use! */
+ //@RobocopTarget
+ public void setDrawListener(DrawListener listener) {
+ mDrawListener = listener;
+ }
+
+ /** Used by robocop for testing purposes. Not for production use! */
+ //@RobocopTarget
+ public static interface DrawListener {
+ public void drawFinished();
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java
new file mode 100644
index 000000000000..463bc3c4a4a5
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java
@@ -0,0 +1,374 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+//import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI;
+import org.mozilla.gecko.util.FloatUtils;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.DisplayMetrics;
+
+/**
+ * ImmutableViewportMetrics are used to store the viewport metrics
+ * in way that we can access a version of them from multiple threads
+ * without having to take a lock
+ */
+public class ImmutableViewportMetrics {
+
+ // We need to flatten the RectF and FloatSize structures
+ // because Java doesn't have the concept of const classes
+ public final float pageRectLeft;
+ public final float pageRectTop;
+ public final float pageRectRight;
+ public final float pageRectBottom;
+ public final float cssPageRectLeft;
+ public final float cssPageRectTop;
+ public final float cssPageRectRight;
+ public final float cssPageRectBottom;
+ public final float viewportRectLeft;
+ public final float viewportRectTop;
+ public final float viewportRectRight;
+ public final float viewportRectBottom;
+ public final float marginLeft;
+ public final float marginTop;
+ public final float marginRight;
+ public final float marginBottom;
+ public final float zoomFactor;
+ public final boolean isRTL;
+
+ public ImmutableViewportMetrics(DisplayMetrics metrics) {
+ viewportRectLeft = pageRectLeft = cssPageRectLeft = 0;
+ viewportRectTop = pageRectTop = cssPageRectTop = 0;
+ viewportRectRight = pageRectRight = cssPageRectRight = metrics.widthPixels;
+ viewportRectBottom = pageRectBottom = cssPageRectBottom = metrics.heightPixels;
+ marginLeft = marginTop = marginRight = marginBottom = 0;
+ zoomFactor = 1.0f;
+ isRTL = false;
+ }
+
+ /** This constructor is used by native code in AndroidJavaWrappers.cpp, be
+ * careful when modifying the signature.
+ */
+ //@WrapElementForJNI(allowMultithread = true)
+ public ImmutableViewportMetrics(float aPageRectLeft, float aPageRectTop,
+ float aPageRectRight, float aPageRectBottom, float aCssPageRectLeft,
+ float aCssPageRectTop, float aCssPageRectRight, float aCssPageRectBottom,
+ float aViewportRectLeft, float aViewportRectTop, float aViewportRectRight,
+ float aViewportRectBottom, float aZoomFactor)
+ {
+ this(aPageRectLeft, aPageRectTop,
+ aPageRectRight, aPageRectBottom, aCssPageRectLeft,
+ aCssPageRectTop, aCssPageRectRight, aCssPageRectBottom,
+ aViewportRectLeft, aViewportRectTop, aViewportRectRight,
+ aViewportRectBottom, 0.0f, 0.0f, 0.0f, 0.0f, aZoomFactor, false);
+ }
+
+ private ImmutableViewportMetrics(float aPageRectLeft, float aPageRectTop,
+ float aPageRectRight, float aPageRectBottom, float aCssPageRectLeft,
+ float aCssPageRectTop, float aCssPageRectRight, float aCssPageRectBottom,
+ float aViewportRectLeft, float aViewportRectTop, float aViewportRectRight,
+ float aViewportRectBottom, float aMarginLeft,
+ float aMarginTop, float aMarginRight,
+ float aMarginBottom, float aZoomFactor, boolean aIsRTL)
+ {
+ pageRectLeft = aPageRectLeft;
+ pageRectTop = aPageRectTop;
+ pageRectRight = aPageRectRight;
+ pageRectBottom = aPageRectBottom;
+ cssPageRectLeft = aCssPageRectLeft;
+ cssPageRectTop = aCssPageRectTop;
+ cssPageRectRight = aCssPageRectRight;
+ cssPageRectBottom = aCssPageRectBottom;
+ viewportRectLeft = aViewportRectLeft;
+ viewportRectTop = aViewportRectTop;
+ viewportRectRight = aViewportRectRight;
+ viewportRectBottom = aViewportRectBottom;
+ marginLeft = aMarginLeft;
+ marginTop = aMarginTop;
+ marginRight = aMarginRight;
+ marginBottom = aMarginBottom;
+ zoomFactor = aZoomFactor;
+ isRTL = aIsRTL;
+ }
+
+ public float getWidth() {
+ return viewportRectRight - viewportRectLeft;
+ }
+
+ public float getHeight() {
+ return viewportRectBottom - viewportRectTop;
+ }
+
+ public float getWidthWithoutMargins() {
+ return viewportRectRight - viewportRectLeft - marginLeft - marginRight;
+ }
+
+ public float getHeightWithoutMargins() {
+ return viewportRectBottom - viewportRectTop - marginTop - marginBottom;
+ }
+
+ public PointF getOrigin() {
+ return new PointF(viewportRectLeft, viewportRectTop);
+ }
+
+ public PointF getMarginOffset() {
+ if (isRTL) {
+ return new PointF(marginLeft - marginRight, marginTop);
+ }
+ return new PointF(marginLeft, marginTop);
+ }
+
+ public FloatSize getSize() {
+ return new FloatSize(viewportRectRight - viewportRectLeft, viewportRectBottom - viewportRectTop);
+ }
+
+ public RectF getViewport() {
+ return new RectF(viewportRectLeft,
+ viewportRectTop,
+ viewportRectRight,
+ viewportRectBottom);
+ }
+
+ public RectF getCssViewport() {
+ return RectUtils.scale(getViewport(), 1/zoomFactor);
+ }
+
+ public RectF getPageRect() {
+ return new RectF(pageRectLeft, pageRectTop, pageRectRight, pageRectBottom);
+ }
+
+ public float getPageWidth() {
+ return pageRectRight - pageRectLeft;
+ }
+
+ public float getPageWidthWithMargins() {
+ return (pageRectRight - pageRectLeft) + marginLeft + marginRight;
+ }
+
+ public float getPageHeight() {
+ return pageRectBottom - pageRectTop;
+ }
+
+ public float getPageHeightWithMargins() {
+ return (pageRectBottom - pageRectTop) + marginTop + marginBottom;
+ }
+
+ public RectF getCssPageRect() {
+ return new RectF(cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom);
+ }
+
+ public RectF getOverscroll() {
+ return new RectF(Math.max(0, pageRectLeft - viewportRectLeft),
+ Math.max(0, pageRectTop - viewportRectTop),
+ Math.max(0, viewportRectRight - pageRectRight),
+ Math.max(0, viewportRectBottom - pageRectBottom));
+ }
+
+ /*
+ * Returns the viewport metrics that represent a linear transition between "this" and "to" at
+ * time "t", which is on the scale [0, 1). This function interpolates all values stored in
+ * the viewport metrics.
+ */
+ public ImmutableViewportMetrics interpolate(ImmutableViewportMetrics to, float t) {
+ return new ImmutableViewportMetrics(
+ FloatUtils.interpolate(pageRectLeft, to.pageRectLeft, t),
+ FloatUtils.interpolate(pageRectTop, to.pageRectTop, t),
+ FloatUtils.interpolate(pageRectRight, to.pageRectRight, t),
+ FloatUtils.interpolate(pageRectBottom, to.pageRectBottom, t),
+ FloatUtils.interpolate(cssPageRectLeft, to.cssPageRectLeft, t),
+ FloatUtils.interpolate(cssPageRectTop, to.cssPageRectTop, t),
+ FloatUtils.interpolate(cssPageRectRight, to.cssPageRectRight, t),
+ FloatUtils.interpolate(cssPageRectBottom, to.cssPageRectBottom, t),
+ FloatUtils.interpolate(viewportRectLeft, to.viewportRectLeft, t),
+ FloatUtils.interpolate(viewportRectTop, to.viewportRectTop, t),
+ FloatUtils.interpolate(viewportRectRight, to.viewportRectRight, t),
+ FloatUtils.interpolate(viewportRectBottom, to.viewportRectBottom, t),
+ FloatUtils.interpolate(marginLeft, to.marginLeft, t),
+ FloatUtils.interpolate(marginTop, to.marginTop, t),
+ FloatUtils.interpolate(marginRight, to.marginRight, t),
+ FloatUtils.interpolate(marginBottom, to.marginBottom, t),
+ FloatUtils.interpolate(zoomFactor, to.zoomFactor, t),
+ t >= 0.5 ? to.isRTL : isRTL);
+ }
+
+ public ImmutableViewportMetrics setViewportSize(float width, float height) {
+ if (FloatUtils.fuzzyEquals(width, getWidth()) && FloatUtils.fuzzyEquals(height, getHeight())) {
+ return this;
+ }
+
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ viewportRectLeft, viewportRectTop, viewportRectLeft + width, viewportRectTop + height,
+ marginLeft, marginTop, marginRight, marginBottom,
+ zoomFactor, isRTL);
+ }
+
+ public ImmutableViewportMetrics setViewportOrigin(float newOriginX, float newOriginY) {
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ newOriginX, newOriginY, newOriginX + getWidth(), newOriginY + getHeight(),
+ marginLeft, marginTop, marginRight, marginBottom,
+ zoomFactor, isRTL);
+ }
+
+ public ImmutableViewportMetrics setZoomFactor(float newZoomFactor) {
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ viewportRectLeft, viewportRectTop, viewportRectRight, viewportRectBottom,
+ marginLeft, marginTop, marginRight, marginBottom,
+ newZoomFactor, isRTL);
+ }
+
+ public ImmutableViewportMetrics offsetViewportBy(float dx, float dy) {
+ return setViewportOrigin(viewportRectLeft + dx, viewportRectTop + dy);
+ }
+
+ public ImmutableViewportMetrics offsetViewportByAndClamp(float dx, float dy) {
+ if (isRTL) {
+ return setViewportOrigin(
+ Math.min(pageRectRight - getWidthWithoutMargins(), Math.max(viewportRectLeft + dx, pageRectLeft)),
+ Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeightWithoutMargins())));
+ }
+ return setViewportOrigin(
+ Math.max(pageRectLeft, Math.min(viewportRectLeft + dx, pageRectRight - getWidthWithoutMargins())),
+ Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeightWithoutMargins())));
+ }
+
+ public ImmutableViewportMetrics setPageRect(RectF pageRect, RectF cssPageRect) {
+ return new ImmutableViewportMetrics(
+ pageRect.left, pageRect.top, pageRect.right, pageRect.bottom,
+ cssPageRect.left, cssPageRect.top, cssPageRect.right, cssPageRect.bottom,
+ viewportRectLeft, viewportRectTop, viewportRectRight, viewportRectBottom,
+ marginLeft, marginTop, marginRight, marginBottom,
+ zoomFactor, isRTL);
+ }
+
+ public ImmutableViewportMetrics setMargins(float left, float top, float right, float bottom) {
+ if (FloatUtils.fuzzyEquals(left, marginLeft)
+ && FloatUtils.fuzzyEquals(top, marginTop)
+ && FloatUtils.fuzzyEquals(right, marginRight)
+ && FloatUtils.fuzzyEquals(bottom, marginBottom)) {
+ return this;
+ }
+
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ viewportRectLeft, viewportRectTop, viewportRectRight, viewportRectBottom,
+ left, top, right, bottom, zoomFactor, isRTL);
+ }
+
+ public ImmutableViewportMetrics setMarginsFrom(ImmutableViewportMetrics fromMetrics) {
+ return setMargins(fromMetrics.marginLeft,
+ fromMetrics.marginTop,
+ fromMetrics.marginRight,
+ fromMetrics.marginBottom);
+ }
+
+ public ImmutableViewportMetrics setIsRTL(boolean aIsRTL) {
+ if (isRTL == aIsRTL) {
+ return this;
+ }
+
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ viewportRectLeft, viewportRectTop, viewportRectRight, viewportRectBottom,
+ marginLeft, marginTop, marginRight, marginBottom, zoomFactor, aIsRTL);
+ }
+
+ /* This will set the zoom factor and re-scale page-size and viewport offset
+ * accordingly. The given focus will remain at the same point on the screen
+ * after scaling.
+ */
+ public ImmutableViewportMetrics scaleTo(float newZoomFactor, PointF focus) {
+ // cssPageRect* is invariant, since we're setting the scale factor
+ // here. The page rect is based on the CSS page rect.
+ float newPageRectLeft = cssPageRectLeft * newZoomFactor;
+ float newPageRectTop = cssPageRectTop * newZoomFactor;
+ float newPageRectRight = cssPageRectLeft + ((cssPageRectRight - cssPageRectLeft) * newZoomFactor);
+ float newPageRectBottom = cssPageRectTop + ((cssPageRectBottom - cssPageRectTop) * newZoomFactor);
+
+ PointF origin = getOrigin();
+ origin.offset(focus.x, focus.y);
+ origin = PointUtils.scale(origin, newZoomFactor / zoomFactor);
+ origin.offset(-focus.x, -focus.y);
+
+ return new ImmutableViewportMetrics(
+ newPageRectLeft, newPageRectTop, newPageRectRight, newPageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ origin.x, origin.y, origin.x + getWidth(), origin.y + getHeight(),
+ marginLeft, marginTop, marginRight, marginBottom,
+ newZoomFactor, isRTL);
+ }
+
+ /** Clamps the viewport to remain within the page rect. */
+ private ImmutableViewportMetrics clamp(float marginLeft, float marginTop,
+ float marginRight, float marginBottom) {
+ RectF newViewport = getViewport();
+ PointF offset = getMarginOffset();
+
+ // The viewport bounds ought to never exceed the page bounds.
+ if (newViewport.right > pageRectRight + marginLeft + marginRight)
+ newViewport.offset((pageRectRight + marginLeft + marginRight) - newViewport.right, 0);
+ if (newViewport.left < pageRectLeft)
+ newViewport.offset(pageRectLeft - newViewport.left, 0);
+
+ if (newViewport.bottom > pageRectBottom + marginTop + marginBottom)
+ newViewport.offset(0, (pageRectBottom + marginTop + marginBottom) - newViewport.bottom);
+ if (newViewport.top < pageRectTop)
+ newViewport.offset(0, pageRectTop - newViewport.top);
+
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ newViewport.left, newViewport.top, newViewport.right, newViewport.bottom,
+ marginLeft, marginTop, marginRight, marginBottom,
+ zoomFactor, isRTL);
+ }
+
+ public ImmutableViewportMetrics clamp() {
+ return clamp(0, 0, 0, 0);
+ }
+
+ public ImmutableViewportMetrics clampWithMargins() {
+ return clamp(marginLeft, marginTop,
+ marginRight, marginBottom);
+ }
+
+ public boolean fuzzyEquals(ImmutableViewportMetrics other) {
+ // Don't bother checking the pageRectXXX values because they are a product
+ // of the cssPageRectXXX values and the zoomFactor, except with more rounding
+ // error. Checking those is both inefficient and can lead to false negatives.
+ //
+ // This doesn't return false if the margins differ as none of the users
+ // of this function are interested in the margins in that way.
+ return FloatUtils.fuzzyEquals(cssPageRectLeft, other.cssPageRectLeft)
+ && FloatUtils.fuzzyEquals(cssPageRectTop, other.cssPageRectTop)
+ && FloatUtils.fuzzyEquals(cssPageRectRight, other.cssPageRectRight)
+ && FloatUtils.fuzzyEquals(cssPageRectBottom, other.cssPageRectBottom)
+ && FloatUtils.fuzzyEquals(viewportRectLeft, other.viewportRectLeft)
+ && FloatUtils.fuzzyEquals(viewportRectTop, other.viewportRectTop)
+ && FloatUtils.fuzzyEquals(viewportRectRight, other.viewportRectRight)
+ && FloatUtils.fuzzyEquals(viewportRectBottom, other.viewportRectBottom)
+ && FloatUtils.fuzzyEquals(zoomFactor, other.zoomFactor);
+ }
+
+ @Override
+ public String toString() {
+ return "ImmutableViewportMetrics v=(" + viewportRectLeft + "," + viewportRectTop + ","
+ + viewportRectRight + "," + viewportRectBottom + ") p=(" + pageRectLeft + ","
+ + pageRectTop + "," + pageRectRight + "," + pageRectBottom + ") c=("
+ + cssPageRectLeft + "," + cssPageRectTop + "," + cssPageRectRight + ","
+ + cssPageRectBottom + ") m=(" + marginLeft + ","
+ + marginTop + "," + marginRight + ","
+ + marginBottom + ") z=" + zoomFactor + ", rtl=" + isRTL;
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/InputConnectionHandler.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/InputConnectionHandler.java
new file mode 100644
index 000000000000..9b3ca381b5a4
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/InputConnectionHandler.java
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import android.os.Handler;
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+public interface InputConnectionHandler
+{
+ Handler getHandler(Handler defHandler);
+ InputConnection onCreateInputConnection(EditorInfo outAttrs);
+ boolean onKeyPreIme(int keyCode, KeyEvent event);
+ boolean onKeyDown(int keyCode, KeyEvent event);
+ boolean onKeyLongPress(int keyCode, KeyEvent event);
+ boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event);
+ boolean onKeyUp(int keyCode, KeyEvent event);
+ boolean isIMEEnabled();
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/IntSize.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/IntSize.java
new file mode 100644
index 000000000000..b758d732c2ed
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/IntSize.java
@@ -0,0 +1,91 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.util.FloatMath;
+
+public class IntSize {
+ public final int width, height;
+
+ public IntSize(IntSize size) { width = size.width; height = size.height; }
+ public IntSize(int inWidth, int inHeight) { width = inWidth; height = inHeight; }
+
+ public IntSize(FloatSize size) {
+ width = Math.round(size.width);
+ height = Math.round(size.height);
+ }
+
+ public IntSize(JSONObject json) {
+ try {
+ width = json.getInt("width");
+ height = json.getInt("height");
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public int getArea() {
+ return width * height;
+ }
+
+ public boolean equals(IntSize size) {
+ return ((size.width == width) && (size.height == height));
+ }
+
+ public boolean isPositive() {
+ return (width > 0 && height > 0);
+ }
+
+ @Override
+ public String toString() { return "(" + width + "," + height + ")"; }
+
+ public IntSize scale(float factor) {
+ return new IntSize(Math.round(width * factor),
+ Math.round(height * factor));
+ }
+
+ /* Returns the power of two that is greater than or equal to value */
+ public static int nextPowerOfTwo(int value) {
+ // code taken from http://acius2.blogspot.com/2007/11/calculating-next-power-of-2.html
+ if (0 == value--) {
+ return 1;
+ }
+ value = (value >> 1) | value;
+ value = (value >> 2) | value;
+ value = (value >> 4) | value;
+ value = (value >> 8) | value;
+ value = (value >> 16) | value;
+ return value + 1;
+ }
+
+ public IntSize nextPowerOfTwo() {
+ return new IntSize(nextPowerOfTwo(width), nextPowerOfTwo(height));
+ }
+
+ public static boolean isPowerOfTwo(int value) {
+ if (value == 0)
+ return false;
+ return (value & (value - 1)) == 0;
+ }
+
+ public static int largestPowerOfTwoLessThan(float value) {
+ int val = (int)FloatMath.floor(value);
+ if (val <= 0) {
+ throw new IllegalArgumentException("Error: value must be > 0");
+ }
+ // keep dropping the least-significant set bits until only one is left
+ int bestVal = val;
+ while (val != 0) {
+ bestVal = val;
+ val &= (val - 1);
+ }
+ return bestVal;
+ }
+}
+
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/JavaPanZoomController.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/JavaPanZoomController.java
new file mode 100644
index 000000000000..ac1bf0d3d451
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/JavaPanZoomController.java
@@ -0,0 +1,1461 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import org.libreoffice.LOKitShell;
+//import org.mozilla.gecko.GeckoAppShell;
+//import org.mozilla.gecko.GeckoEvent;
+//import org.mozilla.gecko.PrefsHelper;
+//import org.mozilla.gecko.Tab;
+//import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.ZoomConstraints;
+import org.mozilla.gecko.util.EventDispatcher;
+import org.mozilla.gecko.util.FloatUtils;
+//import org.mozilla.gecko.util.GamepadUtils;
+import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import org.json.JSONObject;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.Build;
+import android.util.FloatMath;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+/*
+ * Handles the kinetic scrolling and zooming physics for a layer controller.
+ *
+ * Many ideas are from Joe Hewitt's Scrollability:
+ * https://github.com/joehewitt/scrollability/
+ */
+class JavaPanZoomController
+ extends GestureDetector.SimpleOnGestureListener
+ implements PanZoomController, SimpleScaleGestureDetector.SimpleScaleGestureListener, GeckoEventListener
+{
+ private static final String LOGTAG = "GeckoPanZoomController";
+
+ private static String MESSAGE_ZOOM_RECT = "Browser:ZoomToRect";
+ private static String MESSAGE_ZOOM_PAGE = "Browser:ZoomToPageWidth";
+ private static String MESSAGE_TOUCH_LISTENER = "Tab:HasTouchListener";
+
+ // Animation stops if the velocity is below this value when overscrolled or panning.
+ private static final float STOPPED_THRESHOLD = 4.0f;
+
+ // Animation stops is the velocity is below this threshold when flinging.
+ private static final float FLING_STOPPED_THRESHOLD = 0.1f;
+
+ // The distance the user has to pan before we recognize it as such (e.g. to avoid 1-pixel pans
+ // between the touch-down and touch-up of a click). In units of density-independent pixels.
+ public static final float PAN_THRESHOLD = 1/16f * LOKitShell.getDpi(); //GeckoAppShell.getDpi();
+
+ // Angle from axis within which we stay axis-locked
+ private static final double AXIS_LOCK_ANGLE = Math.PI / 6.0; // 30 degrees
+
+ // Axis-lock breakout angle
+ private static final double AXIS_BREAKOUT_ANGLE = Math.PI / 8.0;
+
+ // The distance the user has to pan before we consider breaking out of a locked axis
+ public static final float AXIS_BREAKOUT_THRESHOLD = 1/32f * LOKitShell.getDpi(); //GeckoAppShell.getDpi();
+
+ // The maximum amount we allow you to zoom into a page
+ private static final float MAX_ZOOM = 4.0f;
+
+ // The maximum amount we would like to scroll with the mouse
+ private static final float MAX_SCROLL = 0.075f * LOKitShell.getDpi();
+
+ // The maximum zoom factor adjustment per frame of the AUTONAV animation
+ private static final float MAX_ZOOM_DELTA = 0.125f;
+
+ // The duration of the bounce animation in ns
+ private static final int BOUNCE_ANIMATION_DURATION = 250000000;
+
+ private enum PanZoomState {
+ NOTHING, /* no touch-start events received */
+ FLING, /* all touches removed, but we're still scrolling page */
+ TOUCHING, /* one touch-start event received */
+ PANNING_LOCKED_X, /* touch-start followed by move (i.e. panning with axis lock) X axis */
+ PANNING_LOCKED_Y, /* as above for Y axis */
+ PANNING, /* panning without axis lock */
+ PANNING_HOLD, /* in panning, but not moving.
+ * similar to TOUCHING but after starting a pan */
+ PANNING_HOLD_LOCKED_X, /* like PANNING_HOLD, but axis lock still in effect for X axis */
+ PANNING_HOLD_LOCKED_Y, /* as above but for Y axis */
+ PINCHING, /* nth touch-start, where n > 1. this mode allows pan and zoom */
+ ANIMATED_ZOOM, /* animated zoom to a new rect */
+ BOUNCE, /* in a bounce animation */
+ WAITING_LISTENERS, /* a state halfway between NOTHING and TOUCHING - the user has
+ put a finger down, but we don't yet know if a touch listener has
+ prevented the default actions yet. we still need to abort animations. */
+ AUTONAV, /* We are scrolling using an AutonavRunnable animation. This is similar
+ to the FLING state except that it must be stopped manually by the code that
+ started it, and it's velocity can be updated while it's running. */
+ }
+
+ private enum AxisLockMode {
+ STANDARD, /* Default axis locking mode that doesn't break out until finger release */
+ FREE, /* No locking at all */
+ STICKY /* Break out with hysteresis so that it feels as free as possible whilst locking */
+ }
+
+ private final PanZoomTarget mTarget;
+ private final SubdocumentScrollHelper mSubscroller;
+ private final Axis mX;
+ private final Axis mY;
+ private final TouchEventHandler mTouchEventHandler;
+ private final EventDispatcher mEventDispatcher;
+
+ /* The task that handles flings, autonav or bounces. */
+ private PanZoomRenderTask mAnimationRenderTask;
+ /* The zoom focus at the first zoom event (in page coordinates). */
+ private PointF mLastZoomFocus;
+ /* The time the last motion event took place. */
+ private long mLastEventTime;
+ /* Current state the pan/zoom UI is in. */
+ private PanZoomState mState;
+ /* The per-frame zoom delta for the currently-running AUTONAV animation. */
+ private float mAutonavZoomDelta;
+ /* The user selected panning mode */
+ private AxisLockMode mMode;
+ /* A medium-length tap/press is happening */
+ private boolean mMediumPress;
+ /* Used to change the scrollY direction */
+ private boolean mNegateWheelScrollY;
+ /* Whether the current event has been default-prevented. */
+ private boolean mDefaultPrevented;
+
+ // Handler to be notified when overscroll occurs
+ private Overscroll mOverscroll;
+
+ public JavaPanZoomController(PanZoomTarget target, View view, EventDispatcher eventDispatcher) {
+ mTarget = target;
+ mSubscroller = new SubdocumentScrollHelper(eventDispatcher);
+ mX = new AxisX(mSubscroller);
+ mY = new AxisY(mSubscroller);
+ mTouchEventHandler = new TouchEventHandler(view.getContext(), view, this);
+
+ checkMainThread();
+
+ setState(PanZoomState.NOTHING);
+
+ mEventDispatcher = eventDispatcher;
+ registerEventListener(MESSAGE_ZOOM_RECT);
+ registerEventListener(MESSAGE_ZOOM_PAGE);
+ registerEventListener(MESSAGE_TOUCH_LISTENER);
+
+ mMode = AxisLockMode.STANDARD;
+
+ String[] prefs = { "ui.scrolling.axis_lock_mode",
+ "ui.scrolling.negate_wheel_scrollY",
+ "ui.scrolling.gamepad_dead_zone" };
+ mNegateWheelScrollY = false;
+
+ /*PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() {
+ @Override public void prefValue(String pref, String value) {
+ if (pref.equals("ui.scrolling.axis_lock_mode")) {
+ if (value.equals("standard")) {
+ mMode = AxisLockMode.STANDARD;
+ } else if (value.equals("free")) {
+ mMode = AxisLockMode.FREE;
+ } else {
+ mMode = AxisLockMode.STICKY;
+ }
+ }
+ }
+
+ @Override public void prefValue(String pref, int value) {
+ if (pref.equals("ui.scrolling.gamepad_dead_zone")) {
+ GamepadUtils.overrideDeadZoneThreshold((float)value / 1000f);
+ }
+ }
+
+ @Override public void prefValue(String pref, boolean value) {
+ if (pref.equals("ui.scrolling.negate_wheel_scrollY")) {
+ mNegateWheelScrollY = value;
+ }
+ }
+
+ @Override
+ public boolean isObserver() {
+ return true;
+ }
+ });*/
+
+ Axis.initPrefs();
+ }
+
+ @Override
+ public void destroy() {
+ unregisterEventListener(MESSAGE_ZOOM_RECT);
+ unregisterEventListener(MESSAGE_ZOOM_PAGE);
+ unregisterEventListener(MESSAGE_TOUCH_LISTENER);
+ mSubscroller.destroy();
+ mTouchEventHandler.destroy();
+ }
+
+ private final static float easeOut(float t) {
+ // ease-out approx.
+ // -(t-1)^2+1
+ t = t-1;
+ return -t*t+1;
+ }
+
+ private void registerEventListener(String event) {
+ mEventDispatcher.registerEventListener(event, this);
+ }
+
+ private void unregisterEventListener(String event) {
+ mEventDispatcher.unregisterEventListener(event, this);
+ }
+
+ private void setState(PanZoomState state) {
+ if (state != mState) {
+ //GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PanZoom:StateChange", state.toString()));
+ mState = state;
+
+ // Let the target know we've finished with it (for now)
+ if (state == PanZoomState.NOTHING) {
+ mTarget.panZoomStopped();
+ }
+ }
+ }
+
+ private ImmutableViewportMetrics getMetrics() {
+ return mTarget.getViewportMetrics();
+ }
+
+ private void checkMainThread() {
+ if (!ThreadUtils.isOnUiThread()) {
+ // log with full stack trace
+ Log.e(LOGTAG, "Uh-oh, we're running on the wrong thread!", new Exception());
+ }
+ }
+
+ @Override
+ public void handleMessage(String event, JSONObject message) {
+ try {
+ if (MESSAGE_ZOOM_RECT.equals(event)) {
+ float x = (float)message.getDouble("x");
+ float y = (float)message.getDouble("y");
+ final RectF zoomRect = new RectF(x, y,
+ x + (float)message.getDouble("w"),
+ y + (float)message.getDouble("h"));
+ if (message.optBoolean("animate", true)) {
+ mTarget.post(new Runnable() {
+ @Override
+ public void run() {
+ animatedZoomTo(zoomRect);
+ }
+ });
+ } else {
+ mTarget.setViewportMetrics(getMetricsToZoomTo(zoomRect));
+ }
+ } else if (MESSAGE_ZOOM_PAGE.equals(event)) {
+ ImmutableViewportMetrics metrics = getMetrics();
+ RectF cssPageRect = metrics.getCssPageRect();
+
+ RectF viewableRect = metrics.getCssViewport();
+ float y = viewableRect.top;
+ // attempt to keep zoom keep focused on the center of the viewport
+ float newHeight = viewableRect.height() * cssPageRect.width() / viewableRect.width();
+ float dh = viewableRect.height() - newHeight; // increase in the height
+ final RectF r = new RectF(0.0f,
+ y + dh/2,
+ cssPageRect.width(),
+ y + dh/2 + newHeight);
+ if (message.optBoolean("animate", true)) {
+ mTarget.post(new Runnable() {
+ @Override
+ public void run() {
+ animatedZoomTo(r);
+ }
+ });
+ } else {
+ mTarget.setViewportMetrics(getMetricsToZoomTo(r));
+ }
+ } else if (MESSAGE_TOUCH_LISTENER.equals(event)) {
+ /*int tabId = message.getInt("tabID");
+ final Tab tab = Tabs.getInstance().getTab(tabId);
+ tab.setHasTouchListeners(true);
+ mTarget.post(new Runnable() {
+ @Override
+ public void run() {
+ if (Tabs.getInstance().isSelectedTab(tab))
+ mTouchEventHandler.setWaitForTouchListeners(true);
+ }
+ });*/
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
+ }
+ }
+
+ /** This function MUST be called on the UI thread */
+ @Override
+ public boolean onKeyEvent(KeyEvent event) {
+ if (Build.VERSION.SDK_INT <= 11) {
+ return false;
+ }
+
+ if ((event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD
+ && event.getAction() == KeyEvent.ACTION_DOWN) {
+
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_ZOOM_IN:
+ return animatedScale(0.2f);
+ case KeyEvent.KEYCODE_ZOOM_OUT:
+ return animatedScale(-0.2f);
+ }
+ }
+ return false;
+ }
+
+ /** This function MUST be called on the UI thread */
+ @Override
+ public boolean onMotionEvent(MotionEvent event) {
+ if (Build.VERSION.SDK_INT <= 11) {
+ return false;
+ }
+
+ switch (event.getSource() & InputDevice.SOURCE_CLASS_MASK) {
+ case InputDevice.SOURCE_CLASS_POINTER:
+ switch (event.getAction() & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_SCROLL: return handlePointerScroll(event);
+ }
+ break;
+ case InputDevice.SOURCE_CLASS_JOYSTICK:
+ switch (event.getAction() & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_MOVE: return handleJoystickNav(event);
+ }
+ break;
+ }
+ return false;
+ }
+
+ /** This function MUST be called on the UI thread */
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ return mTouchEventHandler.handleEvent(event);
+ }
+
+ boolean handleEvent(MotionEvent event, boolean defaultPrevented) {
+ mDefaultPrevented = defaultPrevented;
+
+ switch (event.getAction() & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN: return handleTouchStart(event);
+ case MotionEvent.ACTION_MOVE: return handleTouchMove(event);
+ case MotionEvent.ACTION_UP: return handleTouchEnd(event);
+ case MotionEvent.ACTION_CANCEL: return handleTouchCancel(event);
+ }
+ return false;
+ }
+
+ /** This function MUST be called on the UI thread */
+ @Override
+ public void notifyDefaultActionPrevented(boolean prevented) {
+ mTouchEventHandler.handleEventListenerAction(!prevented);
+ }
+
+ /** This function must be called from the UI thread. */
+ @Override
+ public void abortAnimation() {
+ checkMainThread();
+ // this happens when gecko changes the viewport on us or if the device is rotated.
+ // if that's the case, abort any animation in progress and re-zoom so that the page
+ // snaps to edges. for other cases (where the user's finger(s) are down) don't do
+ // anything special.
+ switch (mState) {
+ case FLING:
+ mX.stopFling();
+ mY.stopFling();
+ // fall through
+ case BOUNCE:
+ case ANIMATED_ZOOM:
+ // the zoom that's in progress likely makes no sense any more (such as if
+ // the screen orientation changed) so abort it
+ setState(PanZoomState.NOTHING);
+ // fall through
+ case NOTHING:
+ // Don't do animations here; they're distracting and can cause flashes on page
+ // transitions.
+ synchronized (mTarget.getLock()) {
+ mTarget.setViewportMetrics(getValidViewportMetrics());
+ mTarget.forceRedraw(null);
+ }
+ break;
+ }
+ }
+
+ /** This function must be called on the UI thread. */
+ public void startingNewEventBlock(MotionEvent event, boolean waitingForTouchListeners) {
+ checkMainThread();
+ mSubscroller.cancel();
+ if (waitingForTouchListeners && (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
+ // this is the first touch point going down, so we enter the pending state
+ // seting the state will kill any animations in progress, possibly leaving
+ // the page in overscroll
+ setState(PanZoomState.WAITING_LISTENERS);
+ }
+ }
+
+ /** This must be called on the UI thread. */
+ @Override
+ public void pageRectUpdated() {
+ if (mState == PanZoomState.NOTHING) {
+ synchronized (mTarget.getLock()) {
+ ImmutableViewportMetrics validated = getValidViewportMetrics();
+ if (!getMetrics().fuzzyEquals(validated)) {
+ // page size changed such that we are now in overscroll. snap to the
+ // the nearest valid viewport
+ mTarget.setViewportMetrics(validated);
+ }
+ }
+ }
+ }
+
+ /*
+ * Panning/scrolling
+ */
+
+ private boolean handleTouchStart(MotionEvent event) {
+ // user is taking control of movement, so stop
+ // any auto-movement we have going
+ stopAnimationTask();
+
+ switch (mState) {
+ case ANIMATED_ZOOM:
+ // We just interrupted a double-tap animation, so force a redraw in
+ // case this touchstart is just a tap that doesn't end up triggering
+ // a redraw
+ mTarget.forceRedraw(null);
+ // fall through
+ case FLING:
+ case AUTONAV:
+ case BOUNCE:
+ case NOTHING:
+ case WAITING_LISTENERS:
+ startTouch(event.getX(0), event.getY(0), event.getEventTime());
+ return false;
+ case TOUCHING:
+ case PANNING:
+ case PANNING_LOCKED_X:
+ case PANNING_LOCKED_Y:
+ case PANNING_HOLD:
+ case PANNING_HOLD_LOCKED_X:
+ case PANNING_HOLD_LOCKED_Y:
+ case PINCHING:
+ Log.e(LOGTAG, "Received impossible touch down while in " + mState);
+ return false;
+ }
+ Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchStart");
+ return false;
+ }
+
+ private boolean handleTouchMove(MotionEvent event) {
+
+ switch (mState) {
+ case FLING:
+ case AUTONAV:
+ case BOUNCE:
+ case WAITING_LISTENERS:
+ // should never happen
+ Log.e(LOGTAG, "Received impossible touch move while in " + mState);
+ // fall through
+ case ANIMATED_ZOOM:
+ case NOTHING:
+ // may happen if user double-taps and drags without lifting after the
+ // second tap. ignore the move if this happens.
+ return false;
+
+ case TOUCHING:
+ // Don't allow panning if there is an element in full-screen mode. See bug 775511.
+ if ((mTarget.isFullScreen() && !mSubscroller.scrolling()) || panDistance(event) < PAN_THRESHOLD) {
+ return false;
+ }
+ cancelTouch();
+ startPanning(event.getX(0), event.getY(0), event.getEventTime());
+ track(event);
+ return true;
+
+ case PANNING_HOLD_LOCKED_X:
+ setState(PanZoomState.PANNING_LOCKED_X);
+ track(event);
+ return true;
+ case PANNING_HOLD_LOCKED_Y:
+ setState(PanZoomState.PANNING_LOCKED_Y);
+ // fall through
+ case PANNING_LOCKED_X:
+ case PANNING_LOCKED_Y:
+ track(event);
+ return true;
+
+ case PANNING_HOLD:
+ setState(PanZoomState.PANNING);
+ // fall through
+ case PANNING:
+ track(event);
+ return true;
+
+ case PINCHING:
+ // scale gesture listener will handle this
+ return false;
+ }
+ Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchMove");
+ return false;
+ }
+
+ private boolean handleTouchEnd(MotionEvent event) {
+
+ switch (mState) {
+ case FLING:
+ case AUTONAV:
+ case BOUNCE:
+ case ANIMATED_ZOOM:
+ case NOTHING:
+ // may happen if user double-taps and drags without lifting after the
+ // second tap. ignore if this happens.
+ return false;
+
+ case WAITING_LISTENERS:
+ if (!mDefaultPrevented) {
+ // should never happen
+ Log.e(LOGTAG, "Received impossible touch end while in " + mState);
+ }
+ // fall through
+ case TOUCHING:
+ // the switch into TOUCHING might have happened while the page was
+ // snapping back after overscroll. we need to finish the snap if that
+ // was the case
+ bounce();
+ return false;
+
+ case PANNING:
+ case PANNING_LOCKED_X:
+ case PANNING_LOCKED_Y:
+ case PANNING_HOLD:
+ case PANNING_HOLD_LOCKED_X:
+ case PANNING_HOLD_LOCKED_Y:
+ setState(PanZoomState.FLING);
+ fling();
+ return true;
+
+ case PINCHING:
+ setState(PanZoomState.NOTHING);
+ return true;
+ }
+ Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchEnd");
+ return false;
+ }
+
+ private boolean handleTouchCancel(MotionEvent event) {
+ cancelTouch();
+
+ // ensure we snap back if we're overscrolled
+ bounce();
+ return false;
+ }
+
+ private boolean handlePointerScroll(MotionEvent event) {
+ if (mState == PanZoomState.NOTHING || mState == PanZoomState.FLING) {
+ float scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+ float scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+ if (mNegateWheelScrollY) {
+ scrollY *= -1.0;
+ }
+ scrollBy(scrollX * MAX_SCROLL, scrollY * MAX_SCROLL);
+ bounce();
+ return true;
+ }
+ return false;
+ }
+
+ private float filterDeadZone(MotionEvent event, int axis) {
+ return 0; //(GamepadUtils.isValueInDeadZone(event, axis) ? 0 : event.getAxisValue(axis));
+ }
+
+ private float normalizeJoystickScroll(MotionEvent event, int axis) {
+ return filterDeadZone(event, axis) * MAX_SCROLL;
+ }
+
+ private float normalizeJoystickZoom(MotionEvent event, int axis) {
+ // negate MAX_ZOOM_DELTA so that pushing up on the stick zooms in
+ return filterDeadZone(event, axis) * -MAX_ZOOM_DELTA;
+ }
+
+ // Since this event is a position-based event rather than a motion-based event, we need to
+ // set up an AUTONAV animation to keep scrolling even while we don't get events.
+ private boolean handleJoystickNav(MotionEvent event) {
+ float velocityX = normalizeJoystickScroll(event, MotionEvent.AXIS_X);
+ float velocityY = normalizeJoystickScroll(event, MotionEvent.AXIS_Y);
+ float zoomDelta = normalizeJoystickZoom(event, MotionEvent.AXIS_RZ);
+
+ if (velocityX == 0 && velocityY == 0 && zoomDelta == 0) {
+ if (mState == PanZoomState.AUTONAV) {
+ bounce(); // if not needed, this will automatically go to state NOTHING
+ return true;
+ }
+ return false;
+ }
+
+ if (mState == PanZoomState.NOTHING) {
+ setState(PanZoomState.AUTONAV);
+ startAnimationRenderTask(new AutonavRenderTask());
+ }
+ if (mState == PanZoomState.AUTONAV) {
+ mX.setAutoscrollVelocity(velocityX);
+ mY.setAutoscrollVelocity(velocityY);
+ mAutonavZoomDelta = zoomDelta;
+ return true;
+ }
+ return false;
+ }
+
+ private void startTouch(float x, float y, long time) {
+ mX.startTouch(x);
+ mY.startTouch(y);
+ setState(PanZoomState.TOUCHING);
+ mLastEventTime = time;
+ }
+
+ private void startPanning(float x, float y, long time) {
+ float dx = mX.panDistance(x);
+ float dy = mY.panDistance(y);
+ double angle = Math.atan2(dy, dx); // range [-pi, pi]
+ angle = Math.abs(angle); // range [0, pi]
+
+ // When the touch move breaks through the pan threshold, reposition the touch down origin
+ // so the page won't jump when we start panning.
+ mX.startTouch(x);
+ mY.startTouch(y);
+ mLastEventTime = time;
+
+ if (mMode == AxisLockMode.STANDARD || mMode == AxisLockMode.STICKY) {
+ if (!mX.scrollable() || !mY.scrollable()) {
+ setState(PanZoomState.PANNING);
+ } else if (angle < AXIS_LOCK_ANGLE || angle > (Math.PI - AXIS_LOCK_ANGLE)) {
+ mY.setScrollingDisabled(true);
+ setState(PanZoomState.PANNING_LOCKED_X);
+ } else if (Math.abs(angle - (Math.PI / 2)) < AXIS_LOCK_ANGLE) {
+ mX.setScrollingDisabled(true);
+ setState(PanZoomState.PANNING_LOCKED_Y);
+ } else {
+ setState(PanZoomState.PANNING);
+ }
+ } else if (mMode == AxisLockMode.FREE) {
+ setState(PanZoomState.PANNING);
+ }
+ }
+
+ private float panDistance(MotionEvent move) {
+ float dx = mX.panDistance(move.getX(0));
+ float dy = mY.panDistance(move.getY(0));
+ return FloatMath.sqrt(dx * dx + dy * dy);
+ }
+
+ private void track(float x, float y, long time) {
+ float timeDelta = (float)(time - mLastEventTime);
+ if (FloatUtils.fuzzyEquals(timeDelta, 0)) {
+ // probably a duplicate event, ignore it. using a zero timeDelta will mess
+ // up our velocity
+ return;
+ }
+ mLastEventTime = time;
+
+
+ // if we're axis-locked check if the user is trying to scroll away from the lock
+ if (mMode == AxisLockMode.STICKY) {
+ float dx = mX.panDistance(x);
+ float dy = mY.panDistance(y);
+ double angle = Math.atan2(dy, dx); // range [-pi, pi]
+ angle = Math.abs(angle); // range [0, pi]
+
+ if (Math.abs(dx) > AXIS_BREAKOUT_THRESHOLD || Math.abs(dy) > AXIS_BREAKOUT_THRESHOLD) {
+ if (mState == PanZoomState.PANNING_LOCKED_X) {
+ if (angle > AXIS_BREAKOUT_ANGLE && angle < (Math.PI - AXIS_BREAKOUT_ANGLE)) {
+ mY.setScrollingDisabled(false);
+ setState(PanZoomState.PANNING);
+ }
+ } else if (mState == PanZoomState.PANNING_LOCKED_Y) {
+ if (Math.abs(angle - (Math.PI / 2)) > AXIS_BREAKOUT_ANGLE) {
+ mX.setScrollingDisabled(false);
+ setState(PanZoomState.PANNING);
+ }
+ }
+ }
+ }
+
+ mX.updateWithTouchAt(x, timeDelta);
+ mY.updateWithTouchAt(y, timeDelta);
+ }
+
+ private void track(MotionEvent event) {
+ mX.saveTouchPos();
+ mY.saveTouchPos();
+
+ for (int i = 0; i < event.getHistorySize(); i++) {
+ track(event.getHistoricalX(0, i),
+ event.getHistoricalY(0, i),
+ event.getHistoricalEventTime(i));
+ }
+ track(event.getX(0), event.getY(0), event.getEventTime());
+
+ if (stopped()) {
+ if (mState == PanZoomState.PANNING) {
+ setState(PanZoomState.PANNING_HOLD);
+ } else if (mState == PanZoomState.PANNING_LOCKED_X) {
+ setState(PanZoomState.PANNING_HOLD_LOCKED_X);
+ } else if (mState == PanZoomState.PANNING_LOCKED_Y) {
+ setState(PanZoomState.PANNING_HOLD_LOCKED_Y);
+ } else {
+ // should never happen, but handle anyway for robustness
+ Log.e(LOGTAG, "Impossible case " + mState + " when stopped in track");
+ setState(PanZoomState.PANNING_HOLD);
+ }
+ }
+
+ mX.startPan();
+ mY.startPan();
+ updatePosition();
+ }
+
+ private void scrollBy(float dx, float dy) {
+ mTarget.scrollBy(dx, dy);
+ }
+
+ private void fling() {
+ updatePosition();
+
+ stopAnimationTask();
+
+ boolean stopped = stopped();
+ mX.startFling(stopped);
+ mY.startFling(stopped);
+
+ startAnimationRenderTask(new FlingRenderTask());
+ }
+
+ /* Performs a bounce-back animation to the given viewport metrics. */
+ private void bounce(ImmutableViewportMetrics metrics, PanZoomState state) {
+ stopAnimationTask();
+
+ ImmutableViewportMetrics bounceStartMetrics = getMetrics();
+ if (bounceStartMetrics.fuzzyEquals(metrics)) {
+ setState(PanZoomState.NOTHING);
+ return;
+ }
+
+ setState(state);
+
+ // At this point we have already set mState to BOUNCE or ANIMATED_ZOOM, so
+ // getRedrawHint() is returning false. This means we can safely call
+ // setAnimationTarget to set the new final display port and not have it get
+ // clobbered by display ports from intermediate animation frames.
+ mTarget.setAnimationTarget(metrics);
+ startAnimationRenderTask(new BounceRenderTask(bounceStartMetrics, metrics));
+ }
+
+ /* Performs a bounce-back animation to the nearest valid viewport metrics. */
+ private void bounce() {
+ bounce(getValidViewportMetrics(), PanZoomState.BOUNCE);
+ }
+
+ /* Starts the fling or bounce animation. */
+ private void startAnimationRenderTask(final PanZoomRenderTask task) {
+ if (mAnimationRenderTask != null) {
+ Log.e(LOGTAG, "Attempted to start a new task without canceling the old one!");
+ stopAnimationTask();
+ }
+
+ mAnimationRenderTask = task;
+ mTarget.postRenderTask(mAnimationRenderTask);
+ }
+
+ /* Stops the fling or bounce animation. */
+ private void stopAnimationTask() {
+ if (mAnimationRenderTask != null) {
+ mAnimationRenderTask.terminate();
+ mTarget.removeRenderTask(mAnimationRenderTask);
+ mAnimationRenderTask = null;
+ }
+ }
+
+ private float getVelocity() {
+ float xvel = mX.getRealVelocity();
+ float yvel = mY.getRealVelocity();
+ return FloatMath.sqrt(xvel * xvel + yvel * yvel);
+ }
+
+ @Override
+ public PointF getVelocityVector() {
+ return new PointF(mX.getRealVelocity(), mY.getRealVelocity());
+ }
+
+ private boolean stopped() {
+ return getVelocity() < STOPPED_THRESHOLD;
+ }
+
+ PointF resetDisplacement() {
+ return new PointF(mX.resetDisplacement(), mY.resetDisplacement());
+ }
+
+ private void updatePosition() {
+ mX.displace();
+ mY.displace();
+ PointF displacement = resetDisplacement();
+ if (FloatUtils.fuzzyEquals(displacement.x, 0.0f) && FloatUtils.fuzzyEquals(displacement.y, 0.0f)) {
+ return;
+ }
+ if (mDefaultPrevented || mSubscroller.scrollBy(displacement)) {
+ synchronized (mTarget.getLock()) {
+ mTarget.scrollMarginsBy(displacement.x, displacement.y);
+ }
+ } else {
+ synchronized (mTarget.getLock()) {
+ scrollBy(displacement.x, displacement.y);
+ }
+ }
+ }
+
+ /**
+ * This class is an implementation of RenderTask which enforces its implementor to run in the UI thread.
+ *
+ */
+ private abstract class PanZoomRenderTask extends RenderTask {
+
+ /**
+ * the time when the current frame was started in ns.
+ */
+ protected long mCurrentFrameStartTime;
+ /**
+ * The current frame duration in ns.
+ */
+ protected long mLastFrameTimeDelta;
+
+ private final Runnable mRunnable = new Runnable() {
+ @Override
+ public final void run() {
+ if (mContinueAnimation) {
+ animateFrame();
+ }
+ }
+ };
+
+ private boolean mContinueAnimation = true;
+
+ public PanZoomRenderTask() {
+ super(false);
+ }
+
+ @Override
+ protected final boolean internalRun(long timeDelta, long currentFrameStartTime) {
+
+ mCurrentFrameStartTime = currentFrameStartTime;
+ mLastFrameTimeDelta = timeDelta;
+
+ mTarget.post(mRunnable);
+ return mContinueAnimation;
+ }
+
+ /**
+ * The method subclasses must override. This method is run on the UI thread thanks to internalRun
+ */
+ protected abstract void animateFrame();
+
+ /**
+ * Terminate the animation.
+ */
+ public void terminate() {
+ mContinueAnimation = false;
+ }
+ }
+
+ private class AutonavRenderTask extends PanZoomRenderTask {
+ public AutonavRenderTask() {
+ super();
+ }
+
+ @Override
+ protected void animateFrame() {
+ if (mState != PanZoomState.AUTONAV) {
+ finishAnimation();
+ return;
+ }
+
+ updatePosition();
+ synchronized (mTarget.getLock()) {
+ mTarget.setViewportMetrics(applyZoomDelta(getMetrics(), mAutonavZoomDelta));
+ }
+ }
+ }
+
+ /* The task that performs the bounce animation. */
+ private class BounceRenderTask extends PanZoomRenderTask {
+
+ /*
+ * The viewport metrics that represent the start and end of the bounce-back animation,
+ * respectively.
+ */
+ private ImmutableViewportMetrics mBounceStartMetrics;
+ private ImmutableViewportMetrics mBounceEndMetrics;
+ // How long ago this bounce was started in ns.
+ private long mBounceDuration;
+
+ BounceRenderTask(ImmutableViewportMetrics startMetrics, ImmutableViewportMetrics endMetrics) {
+ super();
+ mBounceStartMetrics = startMetrics;
+ mBounceEndMetrics = endMetrics;
+ }
+
+ @Override
+ protected void animateFrame() {
+ /*
+ * The pan/zoom controller might have signaled to us that it wants to abort the
+ * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail
+ * out.
+ */
+ if (!(mState == PanZoomState.BOUNCE || mState == PanZoomState.ANIMATED_ZOOM)) {
+ finishAnimation();
+ return;
+ }
+
+ /* Perform the next frame of the bounce-back animation. */
+ mBounceDuration = mCurrentFrameStartTime - getStartTime();
+ if (mBounceDuration < BOUNCE_ANIMATION_DURATION) {
+ advanceBounce();
+ return;
+ }
+
+ /* Finally, if there's nothing else to do, complete the animation and go to sleep. */
+ finishBounce();
+ finishAnimation();
+ setState(PanZoomState.NOTHING);
+ }
+
+ /* Performs one frame of a bounce animation. */
+ private void advanceBounce() {
+ synchronized (mTarget.getLock()) {
+ float t = easeOut((float)mBounceDuration / BOUNCE_ANIMATION_DURATION);
+ ImmutableViewportMetrics newMetrics = mBounceStartMetrics.interpolate(mBounceEndMetrics, t);
+ mTarget.setViewportMetrics(newMetrics);
+ }
+ }
+
+ /* Concludes a bounce animation and snaps the viewport into place. */
+ private void finishBounce() {
+ synchronized (mTarget.getLock()) {
+ mTarget.setViewportMetrics(mBounceEndMetrics);
+ }
+ }
+ }
+
+ // The callback that performs the fling animation.
+ private class FlingRenderTask extends PanZoomRenderTask {
+
+ public FlingRenderTask() {
+ super();
+ }
+
+ @Override
+ protected void animateFrame() {
+ /*
+ * The pan/zoom controller might have signaled to us that it wants to abort the
+ * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail
+ * out.
+ */
+ if (mState != PanZoomState.FLING) {
+ finishAnimation();
+ return;
+ }
+
+ /* Advance flings, if necessary. */
+ boolean flingingX = mX.advanceFling(mLastFrameTimeDelta);
+ boolean flingingY = mY.advanceFling(mLastFrameTimeDelta);
+
+ boolean overscrolled = (mX.overscrolled() || mY.overscrolled());
+
+ /* If we're still flinging in any direction, update the origin. */
+ if (flingingX || flingingY) {
+ updatePosition();
+
+ /*
+ * Check to see if we're still flinging with an appreciable velocity. The threshold is
+ * higher in the case of overscroll, so we bounce back eagerly when overscrolling but
+ * coast smoothly to a stop when not. In other words, require a greater velocity to
+ * maintain the fling once we enter overscroll.
+ */
+ float threshold = (overscrolled && !mSubscroller.scrolling() ? STOPPED_THRESHOLD : FLING_STOPPED_THRESHOLD);
+ if (getVelocity() >= threshold) {
+ // we're still flinging
+ return;
+ }
+
+ mX.stopFling();
+ mY.stopFling();
+ }
+
+ /* Perform a bounce-back animation if overscrolled. */
+ if (overscrolled) {
+ bounce();
+ } else {
+ finishAnimation();
+ setState(PanZoomState.NOTHING);
+ }
+ }
+ }
+
+ private void finishAnimation() {
+ checkMainThread();
+
+ stopAnimationTask();
+
+ // Force a viewport synchronisation
+ mTarget.forceRedraw(null);
+ }
+
+ /* Returns the nearest viewport metrics with no overscroll visible. */
+ private ImmutableViewportMetrics getValidViewportMetrics() {
+ return getValidViewportMetrics(getMetrics());
+ }
+
+ private ImmutableViewportMetrics getValidViewportMetrics(ImmutableViewportMetrics viewportMetrics) {
+ /* First, we adjust the zoom factor so that we can make no overscrolled area visible. */
+ float zoomFactor = viewportMetrics.zoomFactor;
+ RectF pageRect = viewportMetrics.getPageRect();
+ RectF viewport = viewportMetrics.getViewport();
+
+ float focusX = viewport.width() / 2.0f;
+ float focusY = viewport.height() / 2.0f;
+
+ float minZoomFactor = 0.0f;
+ float maxZoomFactor = MAX_ZOOM;
+
+ ZoomConstraints constraints = mTarget.getZoomConstraints();
+
+ if (constraints.getMinZoom() > 0)
+ minZoomFactor = constraints.getMinZoom();
+ if (constraints.getMaxZoom() > 0)
+ maxZoomFactor = constraints.getMaxZoom();
+
+ if (!constraints.getAllowZoom()) {
+ // If allowZoom is false, clamp to the default zoom level.
+ maxZoomFactor = minZoomFactor = constraints.getDefaultZoom();
+ }
+
+ // Ensure minZoomFactor keeps the page at least as big as the viewport.
+ if (pageRect.width() > 0) {
+ float pageWidth = pageRect.width() +
+ viewportMetrics.marginLeft +
+ viewportMetrics.marginRight;
+ float scaleFactor = viewport.width() / pageWidth;
+ minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
+ if (viewport.width() > pageWidth)
+ focusX = 0.0f;
+ }
+ if (pageRect.height() > 0) {
+ float pageHeight = pageRect.height() +
+ viewportMetrics.marginTop +
+ viewportMetrics.marginBottom;
+ float scaleFactor = viewport.height() / pageHeight;
+ minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
+ if (viewport.height() > pageHeight)
+ focusY = 0.0f;
+ }
+
+ maxZoomFactor = Math.max(maxZoomFactor, minZoomFactor);
+
+ if (zoomFactor < minZoomFactor) {
+ // if one (or both) of the page dimensions is smaller than the viewport,
+ // zoom using the top/left as the focus on that axis. this prevents the
+ // scenario where, if both dimensions are smaller than the viewport, but
+ // by different scale factors, we end up scrolled to the end on one axis
+ // after applying the scale
+ PointF center = new PointF(focusX, focusY);
+ viewportMetrics = viewportMetrics.scaleTo(minZoomFactor, center);
+ } else if (zoomFactor > maxZoomFactor) {
+ PointF center = new PointF(viewport.width() / 2.0f, viewport.height() / 2.0f);
+ viewportMetrics = viewportMetrics.scaleTo(maxZoomFactor, center);
+ }
+
+ /* Now we pan to the right origin. */
+ viewportMetrics = viewportMetrics.clampWithMargins();
+
+ return viewportMetrics;
+ }
+
+ private class AxisX extends Axis {
+ AxisX(SubdocumentScrollHelper subscroller) { super(subscroller); }
+ @Override
+ public float getOrigin() { return getMetrics().viewportRectLeft; }
+ @Override
+ protected float getViewportLength() { return getMetrics().getWidth(); }
+ @Override
+ protected float getPageStart() { return getMetrics().pageRectLeft; }
+ @Override
+ protected float getMarginStart() { return mTarget.getMaxMargins().left - getMetrics().marginLeft; }
+ @Override
+ protected float getMarginEnd() { return mTarget.getMaxMargins().right - getMetrics().marginRight; }
+ @Override
+ protected float getPageLength() { return getMetrics().getPageWidthWithMargins(); }
+ @Override
+ protected boolean marginsHidden() {
+ ImmutableViewportMetrics metrics = getMetrics();
+ RectF maxMargins = mTarget.getMaxMargins();
+ return (metrics.marginLeft < maxMargins.left || metrics.marginRight < maxMargins.right);
+ }
+ @Override
+ protected void overscrollFling(final float velocity) {
+ if (mOverscroll != null) {
+ mOverscroll.setVelocity(velocity, Overscroll.Axis.X);
+ }
+ }
+ @Override
+ protected void overscrollPan(final float distance) {
+ if (mOverscroll != null) {
+ mOverscroll.setDistance(distance, Overscroll.Axis.X);
+ }
+ }
+ }
+
+ private class AxisY extends Axis {
+ AxisY(SubdocumentScrollHelper subscroller) { super(subscroller); }
+ @Override
+ public float getOrigin() { return getMetrics().viewportRectTop; }
+ @Override
+ protected float getViewportLength() { return getMetrics().getHeight(); }
+ @Override
+ protected float getPageStart() { return getMetrics().pageRectTop; }
+ @Override
+ protected float getPageLength() { return getMetrics().getPageHeightWithMargins(); }
+ @Override
+ protected float getMarginStart() { return mTarget.getMaxMargins().top - getMetrics().marginTop; }
+ @Override
+ protected float getMarginEnd() { return mTarget.getMaxMargins().bottom - getMetrics().marginBottom; }
+ @Override
+ protected boolean marginsHidden() {
+ ImmutableViewportMetrics metrics = getMetrics();
+ RectF maxMargins = mTarget.getMaxMargins();
+ return (metrics.marginTop < maxMargins.top || metrics.marginBottom < maxMargins.bottom);
+ }
+ @Override
+ protected void overscrollFling(final float velocity) {
+ if (mOverscroll != null) {
+ mOverscroll.setVelocity(velocity, Overscroll.Axis.Y);
+ }
+ }
+ @Override
+ protected void overscrollPan(final float distance) {
+ if (mOverscroll != null) {
+ mOverscroll.setDistance(distance, Overscroll.Axis.Y);
+ }
+ }
+ }
+
+ /*
+ * Zooming
+ */
+ @Override
+ public boolean onScaleBegin(SimpleScaleGestureDetector detector) {
+ if (mState == PanZoomState.ANIMATED_ZOOM)
+ return false;
+
+ if (!mTarget.getZoomConstraints().getAllowZoom())
+ return false;
+
+ setState(PanZoomState.PINCHING);
+ mLastZoomFocus = new PointF(detector.getFocusX(), detector.getFocusY());
+ cancelTouch();
+
+ //GeckoAppShell.sendEventToGecko(GeckoEvent.createNativeGestureEvent(GeckoEvent.ACTION_MAGNIFY_START, mLastZoomFocus, getMetrics().zoomFactor));
+
+ return true;
+ }
+
+ @Override
+ public boolean onScale(SimpleScaleGestureDetector detector) {
+ if (mTarget.isFullScreen())
+ return false;
+
+ if (mState != PanZoomState.PINCHING)
+ return false;
+
+ float prevSpan = detector.getPreviousSpan();
+ if (FloatUtils.fuzzyEquals(prevSpan, 0.0f)) {
+ // let's eat this one to avoid setting the new zoom to infinity (bug 711453)
+ return true;
+ }
+
+ synchronized (mTarget.getLock()) {
+ float zoomFactor = getAdjustedZoomFactor(detector.getCurrentSpan() / prevSpan);
+ scrollBy(mLastZoomFocus.x - detector.getFocusX(),
+ mLastZoomFocus.y - detector.getFocusY());
+ mLastZoomFocus.set(detector.getFocusX(), detector.getFocusY());
+ ImmutableViewportMetrics target = getMetrics().scaleTo(zoomFactor, mLastZoomFocus);
+
+ // If overscroll is diabled, prevent zooming outside the normal document pans.
+ if (mX.getOverScrollMode() == View.OVER_SCROLL_NEVER || mY.getOverScrollMode() == View.OVER_SCROLL_NEVER) {
+ target = getValidViewportMetrics(target);
+ }
+ mTarget.setViewportMetrics(target);
+ }
+
+ //GeckoEvent event = GeckoEvent.createNativeGestureEvent(GeckoEvent.ACTION_MAGNIFY, mLastZoomFocus, getMetrics().zoomFactor);
+ //GeckoAppShell.sendEventToGecko(event);
+
+ return true;
+ }
+
+ private ImmutableViewportMetrics applyZoomDelta(ImmutableViewportMetrics metrics, float zoomDelta) {
+ float oldZoom = metrics.zoomFactor;
+ float newZoom = oldZoom + zoomDelta;
+ float adjustedZoom = getAdjustedZoomFactor(newZoom / oldZoom);
+ // since we don't have a particular focus to zoom to, just use the center
+ PointF center = new PointF(metrics.getWidth() / 2.0f, metrics.getHeight() / 2.0f);
+ metrics = metrics.scaleTo(adjustedZoom, center);
+ return metrics;
+ }
+
+ private boolean animatedScale(float zoomDelta) {
+ if (mState != PanZoomState.NOTHING && mState != PanZoomState.BOUNCE) {
+ return false;
+ }
+ synchronized (mTarget.getLock()) {
+ ImmutableViewportMetrics metrics = applyZoomDelta(getMetrics(), zoomDelta);
+ bounce(getValidViewportMetrics(metrics), PanZoomState.BOUNCE);
+ }
+ return true;
+ }
+
+ private float getAdjustedZoomFactor(float zoomRatio) {
+ /*
+ * Apply edge resistance if we're zoomed out smaller than the page size by scaling the zoom
+ * factor toward 1.0.
+ */
+ float resistance = Math.min(mX.getEdgeResistance(true), mY.getEdgeResistance(true));
+ if (zoomRatio > 1.0f)
+ zoomRatio = 1.0f + (zoomRatio - 1.0f) * resistance;
+ else
+ zoomRatio = 1.0f - (1.0f - zoomRatio) * resistance;
+
+ float newZoomFactor = getMetrics().zoomFactor * zoomRatio;
+ float minZoomFactor = 0.0f;
+ float maxZoomFactor = MAX_ZOOM;
+
+ ZoomConstraints constraints = mTarget.getZoomConstraints();
+
+ if (constraints.getMinZoom() > 0)
+ minZoomFactor = constraints.getMinZoom();
+ if (constraints.getMaxZoom() > 0)
+ maxZoomFactor = constraints.getMaxZoom();
+
+ if (newZoomFactor < minZoomFactor) {
+ // apply resistance when zooming past minZoomFactor,
+ // such that it asymptotically reaches minZoomFactor / 2.0
+ // but never exceeds that
+ final float rate = 0.5f; // controls how quickly we approach the limit
+ float excessZoom = minZoomFactor - newZoomFactor;
+ excessZoom = 1.0f - (float)Math.exp(-excessZoom * rate);
+ newZoomFactor = minZoomFactor * (1.0f - excessZoom / 2.0f);
+ }
+
+ if (newZoomFactor > maxZoomFactor) {
+ // apply resistance when zooming past maxZoomFactor,
+ // such that it asymptotically reaches maxZoomFactor + 1.0
+ // but never exceeds that
+ float excessZoom = newZoomFactor - maxZoomFactor;
+ excessZoom = 1.0f - (float)Math.exp(-excessZoom);
+ newZoomFactor = maxZoomFactor + excessZoom;
+ }
+
+ return newZoomFactor;
+ }
+
+ @Override
+ public void onScaleEnd(SimpleScaleGestureDetector detector) {
+ if (mState == PanZoomState.ANIMATED_ZOOM)
+ return;
+
+ // switch back to the touching state
+ startTouch(detector.getFocusX(), detector.getFocusY(), detector.getEventTime());
+
+ // Force a viewport synchronisation
+ mTarget.forceRedraw(null);
+
+ PointF point = new PointF(detector.getFocusX(), detector.getFocusY());
+ //GeckoEvent event = GeckoEvent.createNativeGestureEvent(GeckoEvent.ACTION_MAGNIFY_END, point, getMetrics().zoomFactor);
+
+ //if (event == null) {
+ // return;
+ //}
+
+ //GeckoAppShell.sendEventToGecko(event);
+ }
+
+ @Override
+ public boolean getRedrawHint() {
+ switch (mState) {
+ case PINCHING:
+ case ANIMATED_ZOOM:
+ case BOUNCE:
+ // don't redraw during these because the zoom is (or might be, in the case
+ // of BOUNCE) be changing rapidly and gecko will have to redraw the entire
+ // display port area. we trigger a force-redraw upon exiting these states.
+ return false;
+ default:
+ // allow redrawing in other states
+ return true;
+ }
+ }
+
+ private void sendPointToGecko(String event, MotionEvent motionEvent) {
+ String json;
+ try {
+ PointF point = new PointF(motionEvent.getX(), motionEvent.getY());
+ point = mTarget.convertViewPointToLayerPoint(point);
+ if (point == null) {
+ return;
+ }
+ json = PointUtils.toJSON(point).toString();
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Unable to convert point to JSON for " + event, e);
+ return;
+ }
+
+ //GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(event, json));
+ }
+
+ @Override
+ public boolean onDown(MotionEvent motionEvent) {
+ mMediumPress = false;
+ return false;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent motionEvent) {
+ // If we get this, it will be followed either by a call to
+ // onSingleTapUp (if the user lifts their finger before the
+ // long-press timeout) or a call to onLongPress (if the user
+ // does not). In the former case, we want to make sure it is
+ // treated as a click. (Note that if this is called, we will
+ // not get a call to onDoubleTap).
+ mMediumPress = true;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent motionEvent) {
+ sendPointToGecko("Gesture:LongPress", motionEvent);
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent motionEvent) {
+ // When zooming is enabled, we wait to see if there's a double-tap.
+ // However, if mMediumPress is true then we know there will be no
+ // double-tap so we treat this as a click.
+ if (mMediumPress || !mTarget.getZoomConstraints().getAllowZoom()) {
+ sendPointToGecko("Gesture:SingleTap", motionEvent);
+ }
+ // return false because we still want to get the ACTION_UP event that triggers this
+ return false;
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent motionEvent) {
+ // When zooming is disabled, we handle this in onSingleTapUp.
+ if (mTarget.getZoomConstraints().getAllowZoom()) {
+ sendPointToGecko("Gesture:SingleTap", motionEvent);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent motionEvent) {
+ if (mTarget.getZoomConstraints().getAllowZoom()) {
+ sendPointToGecko("Gesture:DoubleTap", motionEvent);
+ }
+ return true;
+ }
+
+ private void cancelTouch() {
+ //GeckoEvent e = GeckoEvent.createBroadcastEvent("Gesture:CancelTouch", "");
+ //GeckoAppShell.sendEventToGecko(e);
+ }
+
+ /**
+ * Zoom to a specified rect IN CSS PIXELS.
+ *
+ * While we usually use device pixels, @zoomToRect must be specified in CSS
+ * pixels.
+ */
+ private ImmutableViewportMetrics getMetricsToZoomTo(RectF zoomToRect) {
+ final float startZoom = getMetrics().zoomFactor;
+
+ RectF viewport = getMetrics().getViewport();
+ // 1. adjust the aspect ratio of zoomToRect to match that of the current viewport,
+ // enlarging as necessary (if it gets too big, it will get shrunk in the next step).
+ // while enlarging make sure we enlarge equally on both sides to keep the target rect
+ // centered.
+ float targetRatio = viewport.width() / viewport.height();
+ float rectRatio = zoomToRect.width() / zoomToRect.height();
+ if (FloatUtils.fuzzyEquals(targetRatio, rectRatio)) {
+ // all good, do nothing
+ } else if (targetRatio < rectRatio) {
+ // need to increase zoomToRect height
+ float newHeight = zoomToRect.width() / targetRatio;
+ zoomToRect.top -= (newHeight - zoomToRect.height()) / 2;
+ zoomToRect.bottom = zoomToRect.top + newHeight;
+ } else { // targetRatio > rectRatio) {
+ // need to increase zoomToRect width
+ float newWidth = targetRatio * zoomToRect.height();
+ zoomToRect.left -= (newWidth - zoomToRect.width()) / 2;
+ zoomToRect.right = zoomToRect.left + newWidth;
+ }
+
+ float finalZoom = viewport.width() / zoomToRect.width();
+
+ ImmutableViewportMetrics finalMetrics = getMetrics();
+ finalMetrics = finalMetrics.setViewportOrigin(
+ zoomToRect.left * finalMetrics.zoomFactor,
+ zoomToRect.top * finalMetrics.zoomFactor);
+ finalMetrics = finalMetrics.scaleTo(finalZoom, new PointF(0.0f, 0.0f));
+
+ // 2. now run getValidViewportMetrics on it, so that the target viewport is
+ // clamped down to prevent overscroll, over-zoom, and other bad conditions.
+ finalMetrics = getValidViewportMetrics(finalMetrics);
+ return finalMetrics;
+ }
+
+ private boolean animatedZoomTo(RectF zoomToRect) {
+ bounce(getMetricsToZoomTo(zoomToRect), PanZoomState.ANIMATED_ZOOM);
+ return true;
+ }
+
+ /** This function must be called from the UI thread. */
+ @Override
+ public void abortPanning() {
+ checkMainThread();
+ bounce();
+ }
+
+ @Override
+ public void setOverScrollMode(int overscrollMode) {
+ mX.setOverScrollMode(overscrollMode);
+ mY.setOverScrollMode(overscrollMode);
+ }
+
+ @Override
+ public int getOverScrollMode() {
+ return mX.getOverScrollMode();
+ }
+
+ @Override
+ public void setOverscrollHandler(final Overscroll handler) {
+ mOverscroll = handler;
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/Layer.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/Layer.java
new file mode 100644
index 000000000000..cae7377d7a29
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/Layer.java
@@ -0,0 +1,207 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import org.mozilla.gecko.util.FloatUtils;
+
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import java.nio.FloatBuffer;
+import java.util.concurrent.locks.ReentrantLock;
+
+public abstract class Layer {
+ private final ReentrantLock mTransactionLock;
+ private boolean mInTransaction;
+ private Rect mNewPosition;
+ private float mNewResolution;
+
+ protected Rect mPosition;
+ protected float mResolution;
+
+ public Layer() {
+ this(null);
+ }
+
+ public Layer(IntSize size) {
+ mTransactionLock = new ReentrantLock();
+ if (size == null) {
+ mPosition = new Rect();
+ } else {
+ mPosition = new Rect(0, 0, size.width, size.height);
+ }
+ mResolution = 1.0f;
+ }
+
+ /**
+ * Updates the layer. This returns false if there is still work to be done
+ * after this update.
+ */
+ public final boolean update(RenderContext context) {
+ if (mTransactionLock.isHeldByCurrentThread()) {
+ throw new RuntimeException("draw() called while transaction lock held by this " +
+ "thread?!");
+ }
+
+ if (mTransactionLock.tryLock()) {
+ try {
+ performUpdates(context);
+ return true;
+ } finally {
+ mTransactionLock.unlock();
+ }
+ }
+
+ return false;
+ }
+
+ /** Subclasses override this function to draw the layer. */
+ public abstract void draw(RenderContext context);
+
+ /** Given the intrinsic size of the layer, returns the pixel boundaries of the layer rect. */
+ protected RectF getBounds(RenderContext context) {
+ return RectUtils.scale(new RectF(mPosition), context.zoomFactor / mResolution);
+ }
+
+ /**
+ * Call this before modifying the layer. Note that, for TileLayers, "modifying the layer"
+ * includes altering the underlying CairoImage in any way. Thus you must call this function
+ * before modifying the byte buffer associated with this layer.
+ *
+ * This function may block, so you should never call this on the main UI thread.
+ */
+ public void beginTransaction() {
+ if (mTransactionLock.isHeldByCurrentThread())
+ throw new RuntimeException("Nested transactions are not supported");
+ mTransactionLock.lock();
+ mInTransaction = true;
+ mNewResolution = mResolution;
+ }
+
+ /** Call this when you're done modifying the layer. */
+ public void endTransaction() {
+ if (!mInTransaction)
+ throw new RuntimeException("endTransaction() called outside a transaction");
+ mInTransaction = false;
+ mTransactionLock.unlock();
+ }
+
+ /** Returns true if the layer is currently in a transaction and false otherwise. */
+ protected boolean inTransaction() {
+ return mInTransaction;
+ }
+
+ /** Returns the current layer position. */
+ public Rect getPosition() {
+ return mPosition;
+ }
+
+ /** Sets the position. Only valid inside a transaction. */
+ public void setPosition(Rect newPosition) {
+ if (!mInTransaction)
+ throw new RuntimeException("setPosition() is only valid inside a transaction");
+ mNewPosition = newPosition;
+ }
+
+ /** Returns the current layer's resolution. */
+ public float getResolution() {
+ return mResolution;
+ }
+
+ /**
+ * Sets the layer resolution. This value is used to determine how many pixels per
+ * device pixel this layer was rendered at. This will be reflected by scaling by
+ * the reciprocal of the resolution in the layer's transform() function.
+ * Only valid inside a transaction. */
+ public void setResolution(float newResolution) {
+ if (!mInTransaction)
+ throw new RuntimeException("setResolution() is only valid inside a transaction");
+ mNewResolution = newResolution;
+ }
+
+ /**
+ * Subclasses may override this method to perform custom layer updates. This will be called
+ * with the transaction lock held. Subclass implementations of this method must call the
+ * superclass implementation. Returns false if there is still work to be done after this
+ * update is complete.
+ */
+ protected void performUpdates(RenderContext context) {
+ if (mNewPosition != null) {
+ mPosition = mNewPosition;
+ mNewPosition = null;
+ }
+ if (mNewResolution != 0.0f) {
+ mResolution = mNewResolution;
+ mNewResolution = 0.0f;
+ }
+ }
+
+ /**
+ * This function fills in the provided <tt>dest</tt> array with values to render a texture.
+ * The array is filled with 4 sets of {x, y, z, texture_x, texture_y} values (so 20 values
+ * in total) corresponding to the corners of the rect.
+ */
+ protected final void fillRectCoordBuffer(float[] dest, RectF rect, float viewWidth, float viewHeight,
+ Rect cropRect, float texWidth, float texHeight) {
+ //x, y, z, texture_x, texture_y
+ dest[0] = rect.left / viewWidth;
+ dest[1] = rect.bottom / viewHeight;
+ dest[2] = 0;
+ dest[3] = cropRect.left / texWidth;
+ dest[4] = cropRect.top / texHeight;
+
+ dest[5] = rect.left / viewWidth;
+ dest[6] = rect.top / viewHeight;
+ dest[7] = 0;
+ dest[8] = cropRect.left / texWidth;
+ dest[9] = cropRect.bottom / texHeight;
+
+ dest[10] = rect.right / viewWidth;
+ dest[11] = rect.bottom / viewHeight;
+ dest[12] = 0;
+ dest[13] = cropRect.right / texWidth;
+ dest[14] = cropRect.top / texHeight;
+
+ dest[15] = rect.right / viewWidth;
+ dest[16] = rect.top / viewHeight;
+ dest[17] = 0;
+ dest[18] = cropRect.right / texWidth;
+ dest[19] = cropRect.bottom / texHeight;
+ }
+
+ public static class RenderContext {
+ public final RectF viewport;
+ public final RectF pageRect;
+ public final float zoomFactor;
+ public final PointF offset;
+ public final int positionHandle;
+ public final int textureHandle;
+ public final FloatBuffer coordBuffer;
+
+ public RenderContext(RectF aViewport, RectF aPageRect, float aZoomFactor, PointF aOffset,
+ int aPositionHandle, int aTextureHandle, FloatBuffer aCoordBuffer) {
+ viewport = aViewport;
+ pageRect = aPageRect;
+ zoomFactor = aZoomFactor;
+ offset = aOffset;
+ positionHandle = aPositionHandle;
+ textureHandle = aTextureHandle;
+ coordBuffer = aCoordBuffer;
+ }
+
+ public boolean fuzzyEquals(RenderContext other) {
+ if (other == null) {
+ return false;
+ }
+ return RectUtils.fuzzyEquals(viewport, other.viewport)
+ && RectUtils.fuzzyEquals(pageRect, other.pageRect)
+ && FloatUtils.fuzzyEquals(zoomFactor, other.zoomFactor)
+ && FloatUtils.fuzzyEquals(offset, other.offset);
+ }
+ }
+}
+
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/LayerMarginsAnimator.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/LayerMarginsAnimator.java
new file mode 100644
index 000000000000..c2b719b914dc
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/LayerMarginsAnimator.java
@@ -0,0 +1,324 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+//import org.mozilla.gecko.GeckoAppShell;
+//import org.mozilla.gecko.GeckoEvent;
+//import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.TouchEventInterceptor;
+import org.mozilla.gecko.util.FloatUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.animation.DecelerateInterpolator;
+import android.view.MotionEvent;
+import android.view.View;
+
+public class LayerMarginsAnimator implements TouchEventInterceptor {
+ private static final String LOGTAG = "GeckoLayerMarginsAnimator";
+ // The duration of the animation in ns
+ private static final long MARGIN_ANIMATION_DURATION = 250000000;
+ private static final String PREF_SHOW_MARGINS_THRESHOLD = "browser.ui.show-margins-threshold";
+
+ /* This is the proportion of the viewport rect, minus maximum margins,
+ * that needs to be travelled before margins will be exposed.
+ */
+ private float SHOW_MARGINS_THRESHOLD = 0.20f;
+
+ /* This rect stores the maximum value margins can grow to when scrolling. When writing
+ * to this member variable, or when reading from this member variable on a non-UI thread,
+ * you must synchronize on the LayerMarginsAnimator instance. */
+ private final RectF mMaxMargins;
+ /* If this boolean is true, scroll changes will not affect margins */
+ private boolean mMarginsPinned;
+ /* The task that handles showing/hiding margins */
+ private LayerMarginsAnimationTask mAnimationTask;
+ /* This interpolator is used for the above mentioned animation */
+ private final DecelerateInterpolator mInterpolator;
+ /* The GeckoLayerClient whose margins will be animated */
+ private final GeckoLayerClient mTarget;
+ /* The distance that has been scrolled since either the first touch event,
+ * or since the margins were last fully hidden */
+ private final PointF mTouchTravelDistance;
+ /* The ID of the prefs listener for the show-marginss threshold */
+ private Integer mPrefObserverId;
+
+ public LayerMarginsAnimator(GeckoLayerClient aTarget, LayerView aView) {
+ // Assign member variables from parameters
+ mTarget = aTarget;
+
+ // Create other member variables
+ mMaxMargins = new RectF();
+ mInterpolator = new DecelerateInterpolator();
+ mTouchTravelDistance = new PointF();
+
+ // Listen to the dynamic toolbar pref
+ /*mPrefObserverId = PrefsHelper.getPref(PREF_SHOW_MARGINS_THRESHOLD, new PrefsHelper.PrefHandlerBase() {
+ @Override
+ public void prefValue(String pref, int value) {
+ SHOW_MARGINS_THRESHOLD = (float)value / 100.0f;
+ }
+
+ @Override
+ public boolean isObserver() {
+ return true;
+ }
+ });*/
+
+ // Listen to touch events, for auto-pinning
+ aView.addTouchInterceptor(this);
+ }
+
+ public void destroy() {
+ if (mPrefObserverId != null) {
+ //PrefsHelper.removeObserver(mPrefObserverId);
+ mPrefObserverId = null;
+ }
+ }
+
+ /**
+ * Sets the maximum values for margins to grow to, in pixels.
+ */
+ public synchronized void setMaxMargins(float left, float top, float right, float bottom) {
+ ThreadUtils.assertOnUiThread();
+
+ mMaxMargins.set(left, top, right, bottom);
+
+ // Update the Gecko-side global for fixed viewport margins.
+ /*GeckoAppShell.sendEventToGecko(
+ GeckoEvent.createBroadcastEvent("Viewport:FixedMarginsChanged",
+ "{ \"top\" : " + top + ", \"right\" : " + right
+ + ", \"bottom\" : " + bottom + ", \"left\" : " + left + " }"));*/
+ }
+
+ RectF getMaxMargins() {
+ return mMaxMargins;
+ }
+
+ private void animateMargins(final float left, final float top, final float right, final float bottom, boolean immediately) {
+ if (mAnimationTask != null) {
+ mTarget.getView().removeRenderTask(mAnimationTask);
+ mAnimationTask = null;
+ }
+
+ if (immediately) {
+ ImmutableViewportMetrics newMetrics = mTarget.getViewportMetrics().setMargins(left, top, right, bottom);
+ mTarget.forceViewportMetrics(newMetrics, true, true);
+ return;
+ }
+
+ ImmutableViewportMetrics metrics = mTarget.getViewportMetrics();
+
+ mAnimationTask = new LayerMarginsAnimationTask(false, metrics, left, top, right, bottom);
+ mTarget.getView().postRenderTask(mAnimationTask);
+ }
+
+ /**
+ * Exposes the margin area by growing the margin components of the current
+ * metrics to the values set in setMaxMargins.
+ */
+ public synchronized void showMargins(boolean immediately) {
+ animateMargins(mMaxMargins.left, mMaxMargins.top, mMaxMargins.right, mMaxMargins.bottom, immediately);
+ }
+
+ public synchronized void hideMargins(boolean immediately) {
+ animateMargins(0, 0, 0, 0, immediately);
+ }
+
+ public void setMarginsPinned(boolean pin) {
+ if (pin == mMarginsPinned) {
+ return;
+ }
+
+ mMarginsPinned = pin;
+ }
+
+ public boolean areMarginsShown() {
+ final ImmutableViewportMetrics metrics = mTarget.getViewportMetrics();
+ return metrics.marginLeft != 0 ||
+ metrics.marginRight != 0 ||
+ metrics.marginTop != 0 ||
+ metrics.marginBottom != 0;
+ }
+
+ /**
+ * This function will scroll a margin down to zero, or up to the maximum
+ * specified margin size and return the left-over delta.
+ * aMargins are in/out parameters. In specifies the current margin size,
+ * and out specifies the modified margin size. They are specified in the
+ * order of start-margin, then end-margin.
+ * This function will also take into account how far the touch point has
+ * moved and react accordingly. If a touch point hasn't moved beyond a
+ * certain threshold, margins can only be hidden and not shown.
+ * aNegativeOffset can be used if the remaining delta should be determined
+ * by the end-margin instead of the start-margin (for example, in rtl
+ * pages).
+ */
+ private float scrollMargin(float[] aMargins, float aDelta,
+ float aOverscrollStart, float aOverscrollEnd,
+ float aTouchTravelDistance,
+ float aViewportStart, float aViewportEnd,
+ float aPageStart, float aPageEnd,
+ float aMaxMarginStart, float aMaxMarginEnd,
+ boolean aNegativeOffset) {
+ float marginStart = aMargins[0];
+ float marginEnd = aMargins[1];
+ float viewportSize = aViewportEnd - aViewportStart;
+ float exposeThreshold = viewportSize * SHOW_MARGINS_THRESHOLD;
+
+ if (aDelta >= 0) {
+ float marginDelta = Math.max(0, aDelta - aOverscrollStart);
+ aMargins[0] = marginStart - Math.min(marginDelta, marginStart);
+ if (aTouchTravelDistance < exposeThreshold && marginEnd == 0) {
+ // We only want the margin to be newly exposed after the touch
+ // has moved a certain distance.
+ marginDelta = Math.max(0, marginDelta - (aPageEnd - aViewportEnd));
+ }
+ aMargins[1] = marginEnd + Math.min(marginDelta, aMaxMarginEnd - marginEnd);
+ } else {
+ float marginDelta = Math.max(0, -aDelta - aOverscrollEnd);
+ aMargins[1] = marginEnd - Math.min(marginDelta, marginEnd);
+ if (-aTouchTravelDistance < exposeThreshold && marginStart == 0) {
+ marginDelta = Math.max(0, marginDelta - (aViewportStart - aPageStart));
+ }
+ aMargins[0] = marginStart + Math.min(marginDelta, aMaxMarginStart - marginStart);
+ }
+
+ if (aNegativeOffset) {
+ return aDelta - (marginEnd - aMargins[1]);
+ }
+ return aDelta - (marginStart - aMargins[0]);
+ }
+
+ /*
+ * Taking maximum margins into account, offsets the margins and then the
+ * viewport origin and returns the modified metrics.
+ */
+ ImmutableViewportMetrics scrollBy(ImmutableViewportMetrics aMetrics, float aDx, float aDy) {
+ float[] newMarginsX = { aMetrics.marginLeft, aMetrics.marginRight };
+ float[] newMarginsY = { aMetrics.marginTop, aMetrics.marginBottom };
+
+ // Only alter margins if the toolbar isn't pinned
+ if (!mMarginsPinned) {
+ // Make sure to cancel any margin animations when margin-scrolling begins
+ if (mAnimationTask != null) {
+ mTarget.getView().removeRenderTask(mAnimationTask);
+ mAnimationTask = null;
+ }
+
+ // Reset the touch travel when changing direction
+ if ((aDx >= 0) != (mTouchTravelDistance.x >= 0)) {
+ mTouchTravelDistance.x = 0;
+ }
+ if ((aDy >= 0) != (mTouchTravelDistance.y >= 0)) {
+ mTouchTravelDistance.y = 0;
+ }
+
+ mTouchTravelDistance.offset(aDx, aDy);
+ RectF overscroll = aMetrics.getOverscroll();
+
+ // Only allow margins to scroll if the page can fill the viewport.
+ if (aMetrics.getPageWidth() >= aMetrics.getWidth()) {
+ aDx = scrollMargin(newMarginsX, aDx,
+ overscroll.left, overscroll.right,
+ mTouchTravelDistance.x,
+ aMetrics.viewportRectLeft, aMetrics.viewportRectRight,
+ aMetrics.pageRectLeft, aMetrics.pageRectRight,
+ mMaxMargins.left, mMaxMargins.right,
+ aMetrics.isRTL);
+ }
+ if (aMetrics.getPageHeight() >= aMetrics.getHeight()) {
+ aDy = scrollMargin(newMarginsY, aDy,
+ overscroll.top, overscroll.bottom,
+ mTouchTravelDistance.y,
+ aMetrics.viewportRectTop, aMetrics.viewportRectBottom,
+ aMetrics.pageRectTop, aMetrics.pageRectBottom,
+ mMaxMargins.top, mMaxMargins.bottom,
+ false);
+ }
+ }
+
+ return aMetrics.setMargins(newMarginsX[0], newMarginsY[0], newMarginsX[1], newMarginsY[1]).offsetViewportBy(aDx, aDy);
+ }
+
+ /** Implementation of TouchEventInterceptor */
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ return false;
+ }
+
+ /** Implementation of TouchEventInterceptor */
+ @Override
+ public boolean onInterceptTouchEvent(View view, MotionEvent event) {
+ int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_DOWN && event.getPointerCount() == 1) {
+ mTouchTravelDistance.set(0.0f, 0.0f);
+ }
+
+ return false;
+ }
+
+ class LayerMarginsAnimationTask extends RenderTask {
+ private float mStartLeft, mStartTop, mStartRight, mStartBottom;
+ private float mTop, mBottom, mLeft, mRight;
+ private boolean mContinueAnimation;
+
+ public LayerMarginsAnimationTask(boolean runAfter, ImmutableViewportMetrics metrics,
+ float left, float top, float right, float bottom) {
+ super(runAfter);
+ mContinueAnimation = true;
+ this.mStartLeft = metrics.marginLeft;
+ this.mStartTop = metrics.marginTop;
+ this.mStartRight = metrics.marginRight;
+ this.mStartBottom = metrics.marginBottom;
+ this.mLeft = left;
+ this.mRight = right;
+ this.mTop = top;
+ this.mBottom = bottom;
+ }
+
+ @Override
+ public boolean internalRun(long timeDelta, long currentFrameStartTime) {
+ if (!mContinueAnimation) {
+ return false;
+ }
+
+ // Calculate the progress (between 0 and 1)
+ float progress = mInterpolator.getInterpolation(
+ Math.min(1.0f, (System.nanoTime() - getStartTime())
+ / (float)MARGIN_ANIMATION_DURATION));
+
+ // Calculate the new metrics accordingly
+ synchronized (mTarget.getLock()) {
+ ImmutableViewportMetrics oldMetrics = mTarget.getViewportMetrics();
+ ImmutableViewportMetrics newMetrics = oldMetrics.setMargins(
+ FloatUtils.interpolate(mStartLeft, mLeft, progress),
+ FloatUtils.interpolate(mStartTop, mTop, progress),
+ FloatUtils.interpolate(mStartRight, mRight, progress),
+ FloatUtils.interpolate(mStartBottom, mBottom, progress));
+ PointF oldOffset = oldMetrics.getMarginOffset();
+ PointF newOffset = newMetrics.getMarginOffset();
+ newMetrics =
+ newMetrics.offsetViewportByAndClamp(newOffset.x - oldOffset.x,
+ newOffset.y - oldOffset.y);
+
+ if (progress >= 1.0f) {
+ mContinueAnimation = false;
+
+ // Force a redraw and update Gecko
+ mTarget.forceViewportMetrics(newMetrics, true, true);
+ } else {
+ mTarget.forceViewportMetrics(newMetrics, false, false);
+ }
+ }
+ return mContinueAnimation;
+ }
+ }
+
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/LayerRenderer.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/LayerRenderer.java
new file mode 100644
index 000000000000..9e7a379095b2
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/LayerRenderer.java
@@ -0,0 +1,722 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import org.libreoffice.LOKitShell;
+import org.libreoffice.R;
+
+//import org.mozilla.gecko.GeckoAppShell;
+//import org.mozilla.gecko.R;
+//import org.mozilla.gecko.Tab;
+//import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.gfx.Layer.RenderContext;
+import org.mozilla.gecko.gfx.RenderTask;
+import org.mozilla.gecko.mozglue.DirectBufferAllocator;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.opengl.GLES20;
+import android.os.SystemClock;
+import android.util.Log;
+//import org.mozilla.gecko.mozglue.JNITarget;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.microedition.khronos.egl.EGLConfig;
+
+/**
+ * The layer renderer implements the rendering logic for a layer view.
+ */
+public class LayerRenderer /*implements Tabs.OnTabsChangedListener*/ {
+ private static final String LOGTAG = "GeckoLayerRenderer";
+ private static final String PROFTAG = "GeckoLayerRendererProf";
+
+ /*
+ * The amount of time a frame is allowed to take to render before we declare it a dropped
+ * frame.
+ */
+ private static final int MAX_FRAME_TIME = 16; /* 1000 ms / 60 FPS */
+
+ private static final int FRAME_RATE_METER_WIDTH = 128;
+ private static final int FRAME_RATE_METER_HEIGHT = 32;
+
+ private static final long NANOS_PER_MS = 1000000;
+ private static final int NANOS_PER_SECOND = 1000000000;
+
+ private final LayerView mView;
+ private TextLayer mFrameRateLayer;
+ private final ScrollbarLayer mHorizScrollLayer;
+ private final ScrollbarLayer mVertScrollLayer;
+ private final FadeRunnable mFadeRunnable;
+ private ByteBuffer mCoordByteBuffer;
+ private FloatBuffer mCoordBuffer;
+ private RenderContext mLastPageContext;
+ private int mMaxTextureSize;
+ private int mBackgroundColor;
+ private int mOverscrollColor;
+
+ private long mLastFrameTime;
+ private final CopyOnWriteArrayList<RenderTask> mTasks;
+
+ private CopyOnWriteArrayList<Layer> mExtraLayers = new CopyOnWriteArrayList<Layer>();
+
+ // Dropped frames display
+ private int[] mFrameTimings;
+ private int mCurrentFrame, mFrameTimingsSum, mDroppedFrames;
+
+ // Render profiling output
+ private int mFramesRendered;
+ private float mCompleteFramesRendered;
+ private boolean mProfileRender;
+ private long mProfileOutputTime;
+
+ private IntBuffer mPixelBuffer;
+
+ // Used by GLES 2.0
+ private int mProgram;
+ private int mPositionHandle;
+ private int mTextureHandle;
+ private int mSampleHandle;
+ private int mTMatrixHandle;
+
+ // column-major matrix applied to each vertex to shift the viewport from
+ // one ranging from (-1, -1),(1,1) to (0,0),(1,1) and to scale all sizes by
+ // a factor of 2 to fill up the screen
+ public static final float[] DEFAULT_TEXTURE_MATRIX = {
+ 2.0f, 0.0f, 0.0f, 0.0f,
+ 0.0f, 2.0f, 0.0f, 0.0f,
+ 0.0f, 0.0f, 2.0f, 0.0f,
+ -1.0f, -1.0f, 0.0f, 1.0f
+ };
+
+ private static final int COORD_BUFFER_SIZE = 20;
+
+ // The shaders run on the GPU directly, the vertex shader is only applying the
+ // matrix transform detailed above
+
+ // Note we flip the y-coordinate in the vertex shader from a
+ // coordinate system with (0,0) in the top left to one with (0,0) in
+ // the bottom left.
+
+ public static final String DEFAULT_VERTEX_SHADER =
+ "uniform mat4 uTMatrix;\n" +
+ "attribute vec4 vPosition;\n" +
+ "attribute vec2 aTexCoord;\n" +
+ "varying vec2 vTexCoord;\n" +
+ "void main() {\n" +
+ " gl_Position = uTMatrix * vPosition;\n" +
+ " vTexCoord.x = aTexCoord.x;\n" +
+ " vTexCoord.y = 1.0 - aTexCoord.y;\n" +
+ "}\n";
+
+ // We use highp because the screenshot textures
+ // we use are large and we stretch them alot
+ // so we need all the precision we can get.
+ // Unfortunately, highp is not required by ES 2.0
+ // so on GPU's like Mali we end up getting mediump
+ public static final String DEFAULT_FRAGMENT_SHADER =
+ "precision highp float;\n" +
+ "varying vec2 vTexCoord;\n" +
+ "uniform sampler2D sTexture;\n" +
+ "void main() {\n" +
+ " gl_FragColor = texture2D(sTexture, vTexCoord);\n" +
+ "}\n";
+
+ public LayerRenderer(LayerView view) {
+ mView = view;
+ mOverscrollColor = view.getContext().getResources().getColor(R.color.background_normal);
+
+ Bitmap scrollbarImage = view.getScrollbarImage();
+ IntSize size = new IntSize(scrollbarImage.getWidth(), scrollbarImage.getHeight());
+ scrollbarImage = expandCanvasToPowerOfTwo(scrollbarImage, size);
+
+ mTasks = new CopyOnWriteArrayList<RenderTask>();
+ mLastFrameTime = System.nanoTime();
+
+ mVertScrollLayer = new ScrollbarLayer(this, scrollbarImage, size, true);
+ mHorizScrollLayer = new ScrollbarLayer(this, diagonalFlip(scrollbarImage), new IntSize(size.height, size.width), false);
+ mFadeRunnable = new FadeRunnable();
+
+ mFrameTimings = new int[60];
+ mCurrentFrame = mFrameTimingsSum = mDroppedFrames = 0;
+
+ // Initialize the FloatBuffer that will be used to store all vertices and texture
+ // coordinates in draw() commands.
+ mCoordByteBuffer = DirectBufferAllocator.allocate(COORD_BUFFER_SIZE * 4);
+ mCoordByteBuffer.order(ByteOrder.nativeOrder());
+ mCoordBuffer = mCoordByteBuffer.asFloatBuffer();
+
+ //Tabs.registerOnTabsChangedListener(this);
+ }
+
+ private Bitmap expandCanvasToPowerOfTwo(Bitmap image, IntSize size) {
+ IntSize potSize = size.nextPowerOfTwo();
+ if (size.equals(potSize)) {
+ return image;
+ }
+ // make the bitmap size a power-of-two in both dimensions if it's not already.
+ Bitmap potImage = Bitmap.createBitmap(potSize.width, potSize.height, image.getConfig());
+ new Canvas(potImage).drawBitmap(image, new Matrix(), null);
+ return potImage;
+ }
+
+ private Bitmap diagonalFlip(Bitmap image) {
+ Matrix rotation = new Matrix();
+ rotation.setValues(new float[] { 0, 1, 0, 1, 0, 0, 0, 0, 1 }); // transform (x,y) into (y,x)
+ Bitmap rotated = Bitmap.createBitmap(image, 0, 0, image.getWidth(), image.getHeight(), rotation, true);
+ return rotated;
+ }
+
+ public void destroy() {
+ DirectBufferAllocator.free(mCoordByteBuffer);
+ mCoordByteBuffer = null;
+ mCoordBuffer = null;
+ mHorizScrollLayer.destroy();
+ mVertScrollLayer.destroy();
+ if (mFrameRateLayer != null) {
+ mFrameRateLayer.destroy();
+ }
+ //Tabs.unregisterOnTabsChangedListener(this);
+ }
+
+ void onSurfaceCreated(EGLConfig config) {
+ checkMonitoringEnabled();
+ createDefaultProgram();
+ activateDefaultProgram();
+ }
+
+ public void createDefaultProgram() {
+ int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DEFAULT_VERTEX_SHADER);
+ int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DEFAULT_FRAGMENT_SHADER);
+
+ mProgram = GLES20.glCreateProgram();
+ GLES20.glAttachShader(mProgram, vertexShader); // add the vertex shader to program
+ GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
+ GLES20.glLinkProgram(mProgram); // creates OpenGL program executables
+
+ // Get handles to the vertex shader's vPosition, aTexCoord, sTexture, and uTMatrix members.
+ mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
+ mTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTexCoord");
+ mSampleHandle = GLES20.glGetUniformLocation(mProgram, "sTexture");
+ mTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uTMatrix");
+
+ int maxTextureSizeResult[] = new int[1];
+ GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxTextureSizeResult, 0);
+ mMaxTextureSize = maxTextureSizeResult[0];
+ }
+
+ // Activates the shader program.
+ public void activateDefaultProgram() {
+ // Add the program to the OpenGL environment
+ GLES20.glUseProgram(mProgram);
+
+ // Set the transformation matrix
+ GLES20.glUniformMatrix4fv(mTMatrixHandle, 1, false, DEFAULT_TEXTURE_MATRIX, 0);
+
+ // Enable the arrays from which we get the vertex and texture coordinates
+ GLES20.glEnableVertexAttribArray(mPositionHandle);
+ GLES20.glEnableVertexAttribArray(mTextureHandle);
+
+ GLES20.glUniform1i(mSampleHandle, 0);
+
+ // TODO: Move these calls into a separate deactivate() call that is called after the
+ // underlay and overlay are rendered.
+ }
+
+ // Deactivates the shader program. This must be done to avoid crashes after returning to the
+ // Gecko C++ compositor from Java.
+ public void deactivateDefaultProgram() {
+ GLES20.glDisableVertexAttribArray(mTextureHandle);
+ GLES20.glDisableVertexAttribArray(mPositionHandle);
+ GLES20.glUseProgram(0);
+ }
+
+ public int getMaxTextureSize() {
+ return mMaxTextureSize;
+ }
+
+ public void postRenderTask(RenderTask aTask) {
+ mTasks.add(aTask);
+ mView.requestRender();
+ }
+
+ public void removeRenderTask(RenderTask aTask) {
+ mTasks.remove(aTask);
+ }
+
+ private void runRenderTasks(CopyOnWriteArrayList<RenderTask> tasks, boolean after, long frameStartTime) {
+ for (RenderTask task : tasks) {
+ if (task.runAfter != after) {
+ continue;
+ }
+
+ boolean stillRunning = task.run(frameStartTime - mLastFrameTime, frameStartTime);
+
+ // Remove the task from the list if its finished
+ if (!stillRunning) {
+ tasks.remove(task);
+ }
+ }
+ }
+
+ public void addLayer(Layer layer) {
+ synchronized (mExtraLayers) {
+ if (mExtraLayers.contains(layer)) {
+ mExtraLayers.remove(layer);
+ }
+
+ mExtraLayers.add(layer);
+ }
+ }
+
+ public void removeLayer(Layer layer) {
+ synchronized (mExtraLayers) {
+ mExtraLayers.remove(layer);
+ }
+ }
+
+ private void printCheckerboardStats() {
+ Log.d(PROFTAG, "Frames rendered over last 1000ms: " + mCompleteFramesRendered + "/" + mFramesRendered);
+ mFramesRendered = 0;
+ mCompleteFramesRendered = 0;
+ }
+
+ /** Used by robocop for testing purposes. Not for production use! */
+ IntBuffer getPixels() {
+ IntBuffer pixelBuffer = IntBuffer.allocate(mView.getWidth() * mView.getHeight());
+ synchronized (pixelBuffer) {
+ mPixelBuffer = pixelBuffer;
+ mView.requestRender();
+ try {
+ pixelBuffer.wait();
+ } catch (InterruptedException ie) {
+ }
+ mPixelBuffer = null;
+ }
+ return pixelBuffer;
+ }
+
+ private RenderContext createScreenContext(ImmutableViewportMetrics metrics, PointF offset) {
+ RectF viewport = new RectF(0.0f, 0.0f, metrics.getWidth(), metrics.getHeight());
+ RectF pageRect = metrics.getPageRect();
+
+ return createContext(viewport, pageRect, 1.0f, offset);
+ }
+
+ private RenderContext createPageContext(ImmutableViewportMetrics metrics, PointF offset) {
+ RectF viewport = metrics.getViewport();
+ RectF pageRect = metrics.getPageRect();
+ float zoomFactor = metrics.zoomFactor;
+
+ return createContext(new RectF(RectUtils.round(viewport)), pageRect, zoomFactor, offset);
+ }
+
+ private RenderContext createContext(RectF viewport, RectF pageRect, float zoomFactor, PointF offset) {
+ return new RenderContext(viewport, pageRect, zoomFactor, offset, mPositionHandle, mTextureHandle,
+ mCoordBuffer);
+ }
+
+ private void updateDroppedFrames(long frameStartTime) {
+ int frameElapsedTime = (int)((System.nanoTime() - frameStartTime) / NANOS_PER_MS);
+
+ /* Update the running statistics. */
+ mFrameTimingsSum -= mFrameTimings[mCurrentFrame];
+ mFrameTimingsSum += frameElapsedTime;
+ mDroppedFrames -= (mFrameTimings[mCurrentFrame] + 1) / MAX_FRAME_TIME;
+ mDroppedFrames += (frameElapsedTime + 1) / MAX_FRAME_TIME;
+
+ mFrameTimings[mCurrentFrame] = frameElapsedTime;
+ mCurrentFrame = (mCurrentFrame + 1) % mFrameTimings.length;
+
+ int averageTime = mFrameTimingsSum / mFrameTimings.length;
+ mFrameRateLayer.beginTransaction(); // called on compositor thread
+ try {
+ mFrameRateLayer.setText(averageTime + " ms/" + mDroppedFrames);
+ } finally {
+ mFrameRateLayer.endTransaction();
+ }
+ }
+
+ /* Given the new dimensions for the surface, moves the frame rate layer appropriately. */
+ private void moveFrameRateLayer(int width, int height) {
+ mFrameRateLayer.beginTransaction(); // called on compositor thread
+ try {
+ Rect position = new Rect(width - FRAME_RATE_METER_WIDTH - 8,
+ height - FRAME_RATE_METER_HEIGHT + 8,
+ width - 8,
+ height + 8);
+ mFrameRateLayer.setPosition(position);
+ } finally {
+ mFrameRateLayer.endTransaction();
+ }
+ }
+
+ void checkMonitoringEnabled() {
+ /* Do this I/O off the main thread to minimize its impact on startup time. */
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Context context = mView.getContext();
+ SharedPreferences preferences = context.getSharedPreferences("GeckoApp", 0);
+ if (preferences.getBoolean("showFrameRate", false)) {
+ IntSize frameRateLayerSize = new IntSize(FRAME_RATE_METER_WIDTH, FRAME_RATE_METER_HEIGHT);
+ mFrameRateLayer = TextLayer.create(frameRateLayerSize, "-- ms/--");
+ moveFrameRateLayer(mView.getWidth(), mView.getHeight());
+ }
+ mProfileRender = Log.isLoggable(PROFTAG, Log.DEBUG);
+ }
+ }).start();
+ }
+
+ /*
+ * create a vertex shader type (GLES20.GL_VERTEX_SHADER)
+ * or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
+ */
+ public static int loadShader(int type, String shaderCode) {
+ int shader = GLES20.glCreateShader(type);
+ GLES20.glShaderSource(shader, shaderCode);
+ GLES20.glCompileShader(shader);
+ return shader;
+ }
+
+ public Frame createFrame(ImmutableViewportMetrics metrics) {
+ return new Frame(metrics);
+ }
+
+ class FadeRunnable implements Runnable {
+ private boolean mStarted;
+ private long mRunAt;
+
+ void scheduleStartFade(long delay) {
+ mRunAt = SystemClock.elapsedRealtime() + delay;
+ if (!mStarted) {
+ mView.postDelayed(this, delay);
+ mStarted = true;
+ }
+ }
+
+ void scheduleNextFadeFrame() {
+ if (mStarted) {
+ Log.e(LOGTAG, "scheduleNextFadeFrame() called while scheduled for starting fade");
+ }
+ mView.postDelayed(this, 1000L / 60L); // request another frame at 60fps
+ }
+
+ boolean timeToFade() {
+ return !mStarted;
+ }
+
+ @Override
+ public void run() {
+ long timeDelta = mRunAt - SystemClock.elapsedRealtime();
+ if (timeDelta > 0) {
+ // the run-at time was pushed back, so reschedule
+ mView.postDelayed(this, timeDelta);
+ } else {
+ // reached the run-at time, execute
+ mStarted = false;
+ mView.requestRender();
+ }
+ }
+ }
+
+ public class Frame {
+ // The timestamp recording the start of this frame.
+ private long mFrameStartTime;
+ // A fixed snapshot of the viewport metrics that this frame is using to render content.
+ private ImmutableViewportMetrics mFrameMetrics;
+ // A rendering context for page-positioned layers, and one for screen-positioned layers.
+ private RenderContext mPageContext, mScreenContext;
+ // Whether a layer was updated.
+ private boolean mUpdated;
+ private final Rect mPageRect;
+ private final Rect mAbsolutePageRect;
+ private final PointF mRenderOffset;
+
+ public Frame(ImmutableViewportMetrics metrics) {
+ mFrameMetrics = metrics;
+
+ // Work out the offset due to margins
+ Layer rootLayer = mView.getLayerClient().getRoot();
+ mRenderOffset = mFrameMetrics.getMarginOffset();
+ mPageContext = createPageContext(metrics, mRenderOffset);
+ mScreenContext = createScreenContext(metrics, mRenderOffset);
+
+ RectF pageRect = mFrameMetrics.getPageRect();
+ mAbsolutePageRect = RectUtils.round(pageRect);
+
+ PointF origin = mFrameMetrics.getOrigin();
+ pageRect.offset(-origin.x, -origin.y);
+ mPageRect = RectUtils.round(pageRect);
+ }
+
+ private void setScissorRect() {
+ Rect scissorRect = transformToScissorRect(mPageRect);
+ GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
+ GLES20.glScissor(scissorRect.left, scissorRect.top,
+ scissorRect.width(), scissorRect.height());
+ }
+
+ private Rect transformToScissorRect(Rect rect) {
+ IntSize screenSize = new IntSize(mFrameMetrics.getSize());
+
+ int left = Math.max(0, rect.left);
+ int top = Math.max(0, rect.top);
+ int right = Math.min(screenSize.width, rect.right);
+ int bottom = Math.min(screenSize.height, rect.bottom);
+
+ Rect scissorRect = new Rect(left, screenSize.height - bottom, right,
+ (screenSize.height - bottom) + (bottom - top));
+ scissorRect.offset(Math.round(-mRenderOffset.x), Math.round(-mRenderOffset.y));
+
+ return scissorRect;
+ }
+
+ /** This function is invoked via JNI; be careful when modifying signature. */
+ //@JNITarget
+ public void beginDrawing() {
+ mFrameStartTime = System.nanoTime();
+
+ TextureReaper.get().reap();
+ TextureGenerator.get().fill();
+
+ mUpdated = true;
+
+ Layer rootLayer = mView.getLayerClient().getRoot();
+
+ // Run through pre-render tasks
+ runRenderTasks(mTasks, false, mFrameStartTime);
+
+ if (!mPageContext.fuzzyEquals(mLastPageContext) && !mView.isFullScreen()) {
+ // The viewport or page changed, so show the scrollbars again
+ // as per UX decision. Don't do this if we're in full-screen mode though.
+ mVertScrollLayer.unfade();
+ mHorizScrollLayer.unfade();
+ mFadeRunnable.scheduleStartFade(ScrollbarLayer.FADE_DELAY);
+ } else if (mFadeRunnable.timeToFade()) {
+ boolean stillFading = mVertScrollLayer.fade() | mHorizScrollLayer.fade();
+ if (stillFading) {
+ mFadeRunnable.scheduleNextFadeFrame();
+ }
+ }
+ mLastPageContext = mPageContext;
+
+ /* Update layers. */
+ if (rootLayer != null) {
+ // Called on compositor thread.
+ mUpdated &= rootLayer.update(mPageContext);
+ }
+
+ if (mFrameRateLayer != null) {
+ // Called on compositor thread.
+ mUpdated &= mFrameRateLayer.update(mScreenContext);
+ }
+
+ mUpdated &= mVertScrollLayer.update(mPageContext); // called on compositor thread
+ mUpdated &= mHorizScrollLayer.update(mPageContext); // called on compositor thread
+
+ for (Layer layer : mExtraLayers) {
+ mUpdated &= layer.update(mPageContext); // called on compositor thread
+ }
+ }
+
+ /** Retrieves the bounds for the layer, rounded in such a way that it
+ * can be used as a mask for something that will render underneath it.
+ * This will round the bounds inwards, but stretch the mask towards any
+ * near page edge, where near is considered to be 'within 2 pixels'.
+ * Returns null if the given layer is null.
+ */
+ private Rect getMaskForLayer(Layer layer) {
+ if (layer == null) {
+ return null;
+ }
+
+ RectF bounds = RectUtils.contract(layer.getBounds(mPageContext), 1.0f, 1.0f);
+ Rect mask = RectUtils.roundIn(bounds);
+
+ // If the mask is within two pixels of any page edge, stretch it over
+ // that edge. This is to avoid drawing thin slivers when masking
+ // layers.
+ if (mask.top <= 2) {
+ mask.top = -1;
+ }
+ if (mask.left <= 2) {
+ mask.left = -1;
+ }
+
+ // Because we're drawing relative to the page-rect, we only need to
+ // take into account its width and height (and not its origin)
+ int pageRight = mPageRect.width();
+ int pageBottom = mPageRect.height();
+
+ if (mask.right >= pageRight - 2) {
+ mask.right = pageRight + 1;
+ }
+ if (mask.bottom >= pageBottom - 2) {
+ mask.bottom = pageBottom + 1;
+ }
+
+ return mask;
+ }
+
+ private void clear(int color) {
+ GLES20.glClearColor(((color >> 16) & 0xFF) / 255.0f,
+ ((color >> 8) & 0xFF) / 255.0f,
+ (color & 0xFF) / 255.0f,
+ 0.0f);
+ // The bits set here need to match up with those used
+ // in gfx/layers/opengl/LayerManagerOGL.cpp.
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT |
+ GLES20.GL_DEPTH_BUFFER_BIT);
+ }
+
+ /** This function is invoked via JNI; be careful when modifying signature. */
+ //@JNITarget
+ public void drawBackground() {
+ // Any GL state which is changed here must be restored in
+ // CompositorOGL::RestoreState
+
+ GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
+
+ // Draw the overscroll background area as a solid color
+ clear(mOverscrollColor);
+
+ // Update background color.
+ mBackgroundColor = mView.getBackgroundColor();
+
+ // Clear the page area to the page background colour.
+ setScissorRect();
+ clear(mBackgroundColor);
+ GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
+ }
+
+ // Draws the layer the client added to us.
+ void drawRootLayer() {
+ Layer rootLayer = mView.getLayerClient().getRoot();
+ if (rootLayer == null) {
+ return;
+ }
+
+ rootLayer.draw(mPageContext);
+ }
+
+ //@JNITarget
+ public void drawForeground() {
+ // Any GL state which is changed here must be restored in
+ // CompositorOGL::RestoreState
+
+ /* Draw any extra layers that were added (likely plugins) */
+ if (mExtraLayers.size() > 0) {
+ for (Layer layer : mExtraLayers) {
+ layer.draw(mPageContext);
+ }
+ }
+
+ /* Draw the vertical scrollbar. */
+ if (mPageRect.height() > mFrameMetrics.getHeight())
+ mVertScrollLayer.draw(mPageContext);
+
+ /* Draw the horizontal scrollbar. */
+ if (mPageRect.width() > mFrameMetrics.getWidth())
+ mHorizScrollLayer.draw(mPageContext);
+
+ /* Measure how much of the screen is checkerboarding */
+ Layer rootLayer = mView.getLayerClient().getRoot();
+ if ((rootLayer != null) &&
+ (mProfileRender || PanningPerfAPI.isRecordingCheckerboard())) {
+ // Calculate the incompletely rendered area of the page
+ float checkerboard = 1.0f - /*GeckoAppShell*/LOKitShell.computeRenderIntegrity();
+
+ PanningPerfAPI.recordCheckerboard(checkerboard);
+ if (checkerboard < 0.0f || checkerboard > 1.0f) {
+ Log.e(LOGTAG, "Checkerboard value out of bounds: " + checkerboard);
+ }
+
+ mCompleteFramesRendered += 1.0f - checkerboard;
+ mFramesRendered ++;
+
+ if (mFrameStartTime - mProfileOutputTime > NANOS_PER_SECOND) {
+ mProfileOutputTime = mFrameStartTime;
+ printCheckerboardStats();
+ }
+ }
+
+ runRenderTasks(mTasks, true, mFrameStartTime);
+
+ /* Draw the FPS. */
+ if (mFrameRateLayer != null) {
+ updateDroppedFrames(mFrameStartTime);
+
+ GLES20.glEnable(GLES20.GL_BLEND);
+ GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
+ mFrameRateLayer.draw(mScreenContext);
+ }
+ }
+
+ /** This function is invoked via JNI; be careful when modifying signature. */
+ //@JNITarget
+ public void endDrawing() {
+ // If a layer update requires further work, schedule another redraw
+ if (!mUpdated)
+ mView.requestRender();
+
+ PanningPerfAPI.recordFrameTime();
+
+ /* Used by robocop for testing purposes */
+ IntBuffer pixelBuffer = mPixelBuffer;
+ if (mUpdated && pixelBuffer != null) {
+ synchronized (pixelBuffer) {
+ pixelBuffer.position(0);
+ GLES20.glReadPixels(0, 0, (int)mScreenContext.viewport.width(),
+ (int)mScreenContext.viewport.height(), GLES20.GL_RGBA,
+ GLES20.GL_UNSIGNED_BYTE, pixelBuffer);
+ pixelBuffer.notify();
+ }
+ }
+
+ // Remove background color once we've painted. GeckoLayerClient is
+ // responsible for setting this flag before current document is
+ // composited.
+ if (mView.getPaintState() == LayerView.PAINT_BEFORE_FIRST) {
+ mView.post(new Runnable() {
+ @Override
+ public void run() {
+ mView.getChildAt(0).setBackgroundColor(Color.TRANSPARENT);
+ }
+ });
+ mView.setPaintState(LayerView.PAINT_AFTER_FIRST);
+ }
+ mLastFrameTime = mFrameStartTime;
+ }
+ }
+
+ /*@Override
+ public void onTabChanged(final Tab tab, Tabs.TabEvents msg, Object data) {
+ // Sets the background of the newly selected tab. This background color
+ // gets cleared in endDrawing(). This function runs on the UI thread,
+ // but other code that touches the paint state is run on the compositor
+ // thread, so this may need to be changed if any problems appear.
+ if (msg == Tabs.TabEvents.SELECTED) {
+ if (mView != null) {
+ if (mView.getChildAt(0) != null) {
+ mView.getChildAt(0).setBackgroundColor(tab.getBackgroundColor());
+ }
+ mView.setPaintState(LayerView.PAINT_START);
+ }
+ }
+ }*/
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/LayerView.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/LayerView.java
new file mode 100644
index 000000000000..95c6e65660bf
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/LayerView.java
@@ -0,0 +1,692 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+//import org.mozilla.gecko.GeckoAccessibility;
+//import org.mozilla.gecko.GeckoAppShell;
+//import org.mozilla.gecko.GeckoEvent;
+//import org.mozilla.gecko.PrefsHelper;
+//import org.mozilla.gecko.R;
+//import org.mozilla.gecko.Tab;
+//import org.mozilla.gecko.Tabs;
+import org.libreoffice.LOKitShell;
+import org.mozilla.gecko.TouchEventInterceptor;
+import org.mozilla.gecko.ZoomConstraints;
+//import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI;
+//import org.mozilla.gecko.mozglue.RobocopTarget;
+import org.mozilla.gecko.util.EventDispatcher;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.os.Build;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.FrameLayout;
+
+import java.nio.IntBuffer;
+import java.util.ArrayList;
+
+/**
+ * A view rendered by the layer compositor.
+ *
+ * Note that LayerView is accessed by Robocop via reflection.
+ */
+public class LayerView extends FrameLayout /*implements Tabs.OnTabsChangedListener */ {
+ private static String LOGTAG = "GeckoLayerView";
+
+ private GeckoLayerClient mLayerClient;
+ private PanZoomController mPanZoomController;
+ private LayerMarginsAnimator mMarginsAnimator;
+ private GLController mGLController;
+ private InputConnectionHandler mInputConnectionHandler;
+ private LayerRenderer mRenderer;
+ /* Must be a PAINT_xxx constant */
+ private int mPaintState;
+ private int mBackgroundColor;
+ private boolean mFullScreen;
+
+ private SurfaceView mSurfaceView;
+ private TextureView mTextureView;
+
+ private Listener mListener;
+
+ /* This should only be modified on the Java UI thread. */
+ private final ArrayList<TouchEventInterceptor> mTouchInterceptors;
+ private final Overscroll mOverscroll;
+
+ /* Flags used to determine when to show the painted surface. */
+ public static final int PAINT_START = 0;
+ public static final int PAINT_BEFORE_FIRST = 1;
+ public static final int PAINT_AFTER_FIRST = 2;
+
+ public boolean shouldUseTextureView() {
+ // Disable TextureView support for now as it causes panning/zooming
+ // performance regressions (see bug 792259). Uncomment the code below
+ // once this bug is fixed.
+ return false;
+
+ /*
+ // we can only use TextureView on ICS or higher
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ Log.i(LOGTAG, "Not using TextureView: not on ICS+");
+ return false;
+ }
+
+ try {
+ // and then we can only use it if we have a hardware accelerated window
+ Method m = View.class.getMethod("isHardwareAccelerated", (Class[]) null);
+ return (Boolean) m.invoke(this);
+ } catch (Exception e) {
+ Log.i(LOGTAG, "Not using TextureView: caught exception checking for hw accel: " + e.toString());
+ return false;
+ } */
+ }
+
+ public LayerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mGLController = GLController.getInstance(this);
+ mPaintState = PAINT_START;
+ mBackgroundColor = Color.WHITE;
+
+ mTouchInterceptors = new ArrayList<TouchEventInterceptor>();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ mOverscroll = new OverscrollEdgeEffect(this);
+ } else {
+ mOverscroll = null;
+ }
+ //Tabs.registerOnTabsChangedListener(this);
+ }
+
+ public LayerView(Context context) {
+ this(context, null);
+ }
+
+ public void initializeView(EventDispatcher eventDispatcher) {
+ mLayerClient = new GeckoLayerClient(getContext(), this, eventDispatcher);
+ if (mOverscroll != null) {
+ mLayerClient.setOverscrollHandler(mOverscroll);
+ }
+
+ mPanZoomController = mLayerClient.getPanZoomController();
+ mMarginsAnimator = mLayerClient.getLayerMarginsAnimator();
+
+ mRenderer = new LayerRenderer(this);
+ mInputConnectionHandler = null;
+
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+
+ //GeckoAccessibility.setDelegate(this);
+ }
+
+ private Point getEventRadius(MotionEvent event) {
+ if (Build.VERSION.SDK_INT >= 9) {
+ return new Point((int)event.getToolMajor()/2,
+ (int)event.getToolMinor()/2);
+ }
+
+ float size = event.getSize();
+ DisplayMetrics displaymetrics = getContext().getResources().getDisplayMetrics();
+ size = size * Math.min(displaymetrics.heightPixels, displaymetrics.widthPixels);
+ return new Point((int)size, (int)size);
+ }
+
+ public void geckoConnected() {
+ // See if we want to force 16-bit colour before doing anything
+ /*PrefsHelper.getPref("gfx.android.rgb16.force", new PrefsHelper.PrefHandlerBase() {
+ @Override public void prefValue(String pref, boolean force16bit) {
+ if (force16bit) {
+ GeckoAppShell.setScreenDepthOverride(16);
+ }
+ }
+ });*/
+
+ mLayerClient.notifyGeckoReady();
+ addTouchInterceptor(new TouchEventInterceptor() {
+ private PointF mInitialTouchPoint = null;
+
+ @Override
+ public boolean onInterceptTouchEvent(View view, MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ if (event == null) {
+ return true;
+ }
+
+ int action = event.getActionMasked();
+ PointF point = new PointF(event.getX(), event.getY());
+ if (action == MotionEvent.ACTION_DOWN) {
+ mInitialTouchPoint = point;
+ }
+
+ if (mInitialTouchPoint != null && action == MotionEvent.ACTION_MOVE) {
+ Point p = getEventRadius(event);
+
+ if (PointUtils.subtract(point, mInitialTouchPoint).length() <
+ Math.max(PanZoomController.CLICK_THRESHOLD, Math.min(Math.min(p.x, p.y), PanZoomController.PAN_THRESHOLD))) {
+ // Don't send the touchmove event if if the users finger hasn't moved far.
+ // Necessary for Google Maps to work correctly. See bug 771099.
+ return true;
+ } else {
+ mInitialTouchPoint = null;
+ }
+ }
+
+ //GeckoAppShell.sendEventToGecko(GeckoEvent.createMotionEvent(event, false));
+ return true;
+ }
+ });
+ }
+
+ public void showSurface() {
+ // Fix this if TextureView support is turned back on above
+ mSurfaceView.setVisibility(View.VISIBLE);
+ }
+
+ public void hideSurface() {
+ // Fix this if TextureView support is turned back on above
+ mSurfaceView.setVisibility(View.INVISIBLE);
+ }
+
+ public void destroy() {
+ if (mLayerClient != null) {
+ mLayerClient.destroy();
+ }
+ if (mRenderer != null) {
+ mRenderer.destroy();
+ }
+ //Tabs.unregisterOnTabsChangedListener(this);
+ }
+
+ public void addTouchInterceptor(final TouchEventInterceptor aTouchInterceptor) {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ mTouchInterceptors.add(aTouchInterceptor);
+ }
+ });
+ }
+
+ public void removeTouchInterceptor(final TouchEventInterceptor aTouchInterceptor) {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ mTouchInterceptors.remove(aTouchInterceptor);
+ }
+ });
+ }
+
+ private boolean runTouchInterceptors(MotionEvent event, boolean aOnTouch) {
+ boolean result = false;
+ for (TouchEventInterceptor i : mTouchInterceptors) {
+ if (aOnTouch) {
+ result |= i.onTouch(this, event);
+ } else {
+ result |= i.onInterceptTouchEvent(this, event);
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ public void dispatchDraw(final Canvas canvas) {
+ super.dispatchDraw(canvas);
+
+ // We must have a layer client to get valid viewport metrics
+ if (mLayerClient != null && mOverscroll != null) {
+ mOverscroll.draw(canvas, getViewportMetrics());
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ requestFocus();
+ }
+
+ if (runTouchInterceptors(event, false)) {
+ return true;
+ }
+ if (mPanZoomController != null && mPanZoomController.onTouchEvent(event)) {
+ return true;
+ }
+ if (runTouchInterceptors(event, true)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ if (runTouchInterceptors(event, true)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ if (mPanZoomController != null && mPanZoomController.onMotionEvent(event)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ // This check should not be done before the view is attached to a window
+ // as hardware acceleration will not be enabled at that point.
+ // We must create and add the SurfaceView instance before the view tree
+ // is fully created to avoid flickering (see bug 801477).
+ if (shouldUseTextureView()) {
+ mTextureView = new TextureView(getContext());
+ mTextureView.setSurfaceTextureListener(new SurfaceTextureListener());
+
+ // The background is set to this color when the LayerView is
+ // created, and it will be shown immediately at startup. Shortly
+ // after, the tab's background color will be used before any content
+ // is shown.
+ mTextureView.setBackgroundColor(Color.WHITE);
+ addView(mTextureView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+ } else {
+ // This will stop PropertyAnimator from creating a drawing cache (i.e. a bitmap)
+ // from a SurfaceView, which is just not possible (the bitmap will be transparent).
+ setWillNotCacheDrawing(false);
+
+ mSurfaceView = new LayerSurfaceView(getContext(), this);
+ mSurfaceView.setBackgroundColor(Color.WHITE);
+ addView(mSurfaceView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+
+ SurfaceHolder holder = mSurfaceView.getHolder();
+ holder.addCallback(new SurfaceListener());
+ holder.setFormat(PixelFormat.RGB_565);
+ }
+ }
+
+ //@RobocopTarget
+ public GeckoLayerClient getLayerClient() { return mLayerClient; }
+ public PanZoomController getPanZoomController() { return mPanZoomController; }
+ public LayerMarginsAnimator getLayerMarginsAnimator() { return mMarginsAnimator; }
+
+ public ImmutableViewportMetrics getViewportMetrics() {
+ return mLayerClient.getViewportMetrics();
+ }
+
+ public void abortPanning() {
+ if (mPanZoomController != null) {
+ mPanZoomController.abortPanning();
+ }
+ }
+
+ public PointF convertViewPointToLayerPoint(PointF viewPoint) {
+ return mLayerClient.convertViewPointToLayerPoint(viewPoint);
+ }
+
+ int getBackgroundColor() {
+ return mBackgroundColor;
+ }
+
+ @Override
+ public void setBackgroundColor(int newColor) {
+ mBackgroundColor = newColor;
+ requestRender();
+ }
+
+ public void setZoomConstraints(ZoomConstraints constraints) {
+ mLayerClient.setZoomConstraints(constraints);
+ }
+
+ public void setIsRTL(boolean aIsRTL) {
+ mLayerClient.setIsRTL(aIsRTL);
+ }
+
+ public void setInputConnectionHandler(InputConnectionHandler inputConnectionHandler) {
+ mInputConnectionHandler = inputConnectionHandler;
+ mLayerClient.forceRedraw(null);
+ }
+
+ @Override
+ public Handler getHandler() {
+ if (mInputConnectionHandler != null)
+ return mInputConnectionHandler.getHandler(super.getHandler());
+ return super.getHandler();
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ if (mInputConnectionHandler != null)
+ return mInputConnectionHandler.onCreateInputConnection(outAttrs);
+ return null;
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (mInputConnectionHandler != null && mInputConnectionHandler.onKeyPreIme(keyCode, event)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (mPanZoomController != null && mPanZoomController.onKeyEvent(event)) {
+ return true;
+ }
+ if (mInputConnectionHandler != null && mInputConnectionHandler.onKeyDown(keyCode, event)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ if (mInputConnectionHandler != null && mInputConnectionHandler.onKeyLongPress(keyCode, event)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ if (mInputConnectionHandler != null && mInputConnectionHandler.onKeyMultiple(keyCode, repeatCount, event)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (mInputConnectionHandler != null && mInputConnectionHandler.onKeyUp(keyCode, event)) {
+ return true;
+ }
+ return false;
+ }
+
+ public boolean isIMEEnabled() {
+ if (mInputConnectionHandler != null) {
+ return mInputConnectionHandler.isIMEEnabled();
+ }
+ return false;
+ }
+
+ public void requestRender() {
+ if (mListener != null) {
+ mListener.renderRequested();
+ }
+ }
+
+ public void addLayer(Layer layer) {
+ mRenderer.addLayer(layer);
+ }
+
+ public void removeLayer(Layer layer) {
+ mRenderer.removeLayer(layer);
+ }
+
+ public void postRenderTask(RenderTask task) {
+ mRenderer.postRenderTask(task);
+ }
+
+ public void removeRenderTask(RenderTask task) {
+ mRenderer.removeRenderTask(task);
+ }
+
+ public int getMaxTextureSize() {
+ return mRenderer.getMaxTextureSize();
+ }
+
+ /** Used by robocop for testing purposes. Not for production use! */
+ //@RobocopTarget
+ public IntBuffer getPixels() {
+ return mRenderer.getPixels();
+ }
+
+ /* paintState must be a PAINT_xxx constant. */
+ public void setPaintState(int paintState) {
+ mPaintState = paintState;
+ }
+
+ public int getPaintState() {
+ return mPaintState;
+ }
+
+ public LayerRenderer getRenderer() {
+ return mRenderer;
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ Listener getListener() {
+ return mListener;
+ }
+
+ public GLController getGLController() {
+ return mGLController;
+ }
+
+ private Bitmap getDrawable(String name) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inScaled = false;
+ Context context = getContext();
+ int resId = context.getResources().getIdentifier(name, "drawable", context.getPackageName());
+ return BitmapUtils.decodeResource(context, resId, options);
+ }
+
+ Bitmap getScrollbarImage() {
+ return getDrawable("scrollbar");
+ }
+
+ /* When using a SurfaceView (mSurfaceView != null), resizing happens in two
+ * phases. First, the LayerView changes size, then, often some frames later,
+ * the SurfaceView changes size. Because of this, we need to split the
+ * resize into two phases to avoid jittering.
+ *
+ * The first phase is the LayerView size change. mListener is notified so
+ * that a synchronous draw can be performed (otherwise a blank frame will
+ * appear).
+ *
+ * The second phase is the SurfaceView size change. At this point, the
+ * backing GL surface is resized and another synchronous draw is performed.
+ * Gecko is also sent the new window size, and this will likely cause an
+ * extra draw a few frames later, after it's re-rendered and caught up.
+ *
+ * In the case that there is no valid GL surface (for example, when
+ * resuming, or when coming back from the awesomescreen), or we're using a
+ * TextureView instead of a SurfaceView, the first phase is skipped.
+ */
+ private void onSizeChanged(int width, int height) {
+ if (!mGLController.isCompositorCreated()) {
+ return;
+ }
+
+ surfaceChanged(width, height);
+
+ if (mSurfaceView == null) {
+ return;
+ }
+
+ if (mListener != null) {
+ mListener.sizeChanged(width, height);
+ }
+
+ if (mOverscroll != null) {
+ mOverscroll.setSize(width, height);
+ }
+ }
+
+ private void surfaceChanged(int width, int height) {
+ mGLController.serverSurfaceChanged(width, height);
+
+ if (mListener != null) {
+ mListener.surfaceChanged(width, height);
+ }
+
+ if (mOverscroll != null) {
+ mOverscroll.setSize(width, height);
+ }
+ }
+
+ private void onDestroyed() {
+ mGLController.serverSurfaceDestroyed();
+ }
+
+ public Object getNativeWindow() {
+ if (mSurfaceView != null)
+ return mSurfaceView.getHolder();
+
+ return mTextureView.getSurfaceTexture();
+ }
+
+ //@WrapElementForJNI(allowMultithread = true, stubName = "RegisterCompositorWrapper")
+ public static GLController registerCxxCompositor() {
+ try {
+ LayerView layerView = /*GeckoAppShell*/LOKitShell.getLayerView();
+ GLController controller = layerView.getGLController();
+ controller.compositorCreated();
+ return controller;
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Error registering compositor!", e);
+ return null;
+ }
+ }
+
+ public interface Listener {
+ void renderRequested();
+ void sizeChanged(int width, int height);
+ void surfaceChanged(int width, int height);
+ }
+
+ private class SurfaceListener implements SurfaceHolder.Callback {
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width,
+ int height) {
+ onSizeChanged(width, height);
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ onDestroyed();
+ }
+ }
+
+ /* A subclass of SurfaceView to listen to layout changes, as
+ * View.OnLayoutChangeListener requires API level 11.
+ */
+ private class LayerSurfaceView extends SurfaceView {
+ LayerView mParent;
+
+ public LayerSurfaceView(Context aContext, LayerView aParent) {
+ super(aContext);
+ mParent = aParent;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ if (changed) {
+ mParent.surfaceChanged(right - left, bottom - top);
+ }
+ }
+ }
+
+ private class SurfaceTextureListener implements TextureView.SurfaceTextureListener {
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+ // We don't do this for surfaceCreated above because it is always followed by a surfaceChanged,
+ // but that is not the case here.
+ onSizeChanged(width, height);
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+ onDestroyed();
+ return true; // allow Android to call release() on the SurfaceTexture, we are done drawing to it
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+ onSizeChanged(width, height);
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+
+ }
+ }
+
+ @Override
+ public void setOverScrollMode(int overscrollMode) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
+ super.setOverScrollMode(overscrollMode);
+ }
+ if (mPanZoomController != null) {
+ mPanZoomController.setOverScrollMode(overscrollMode);
+ }
+ }
+
+ @Override
+ public int getOverScrollMode() {
+ if (mPanZoomController != null) {
+ return mPanZoomController.getOverScrollMode();
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
+ return super.getOverScrollMode();
+ }
+ return View.OVER_SCROLL_ALWAYS;
+ }
+
+ @Override
+ public void onFocusChanged (boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+ //GeckoAccessibility.onLayerViewFocusChanged(this, gainFocus);
+ }
+
+ public void setFullScreen(boolean fullScreen) {
+ mFullScreen = fullScreen;
+ }
+
+ public boolean isFullScreen() {
+ return mFullScreen;
+ }
+
+ /*@Override
+ public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
+ if (msg == Tabs.TabEvents.VIEWPORT_CHANGE && Tabs.getInstance().isSelectedTab(tab) && mLayerClient != null) {
+ setZoomConstraints(tab.getZoomConstraints());
+ setIsRTL(tab.getIsRTL());
+ }
+ }*/
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/Overscroll.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/Overscroll.java
new file mode 100644
index 000000000000..e442444d5a7b
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/Overscroll.java
@@ -0,0 +1,21 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import android.graphics.Canvas;
+
+public interface Overscroll {
+ // The axis to show overscroll on.
+ public enum Axis {
+ X,
+ Y,
+ };
+
+ public void draw(final Canvas canvas, final ImmutableViewportMetrics metrics);
+ public void setSize(final int width, final int height);
+ public void setVelocity(final float velocity, final Axis axis);
+ public void setDistance(final float distance, final Axis axis);
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java
new file mode 100644
index 000000000000..9ab64d5f3e51
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java
@@ -0,0 +1,130 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.os.Build;
+import android.widget.EdgeEffect;
+import android.view.View;
+
+
+public class OverscrollEdgeEffect implements Overscroll {
+ // Used to index particular edges in the edges array
+ private static final int TOP = 0;
+ private static final int BOTTOM = 1;
+ private static final int LEFT = 2;
+ private static final int RIGHT = 3;
+
+ // All four edges of the screen
+ private final EdgeEffect[] mEdges = new EdgeEffect[4];
+
+ // The view we're showing this overscroll on.
+ private final View mView;
+
+ public OverscrollEdgeEffect(final View v) {
+ mView = v;
+ Context context = v.getContext();
+ for (int i = 0; i < 4; i++) {
+ mEdges[i] = new EdgeEffect(context);
+ }
+ }
+
+ public void setSize(final int width, final int height) {
+ mEdges[LEFT].setSize(height, width);
+ mEdges[RIGHT].setSize(height, width);
+ mEdges[TOP].setSize(width, height);
+ mEdges[BOTTOM].setSize(width, height);
+ }
+
+ private EdgeEffect getEdgeForAxisAndSide(final Axis axis, final float side) {
+ if (axis == Axis.Y) {
+ if (side < 0) {
+ return mEdges[TOP];
+ } else {
+ return mEdges[BOTTOM];
+ }
+ } else {
+ if (side < 0) {
+ return mEdges[LEFT];
+ } else {
+ return mEdges[RIGHT];
+ }
+ }
+ }
+
+ private void invalidate() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ mView.postInvalidateOnAnimation();
+ } else {
+ mView.postInvalidateDelayed(10);
+ }
+ }
+
+ public void setVelocity(final float velocity, final Axis axis) {
+ final EdgeEffect edge = getEdgeForAxisAndSide(axis, velocity);
+
+ // If we're showing overscroll already, start fading it out.
+ if (!edge.isFinished()) {
+ edge.onRelease();
+ } else {
+ // Otherwise, show an absorb effect
+ edge.onAbsorb((int)velocity);
+ }
+
+ invalidate();
+ }
+
+ public void setDistance(final float distance, final Axis axis) {
+ // The first overscroll event often has zero distance. Throw it out
+ if (distance == 0.0f) {
+ return;
+ }
+
+ final EdgeEffect edge = getEdgeForAxisAndSide(axis, (int)distance);
+ edge.onPull(distance / (axis == Axis.X ? mView.getWidth() : mView.getHeight()));
+ invalidate();
+ }
+
+ public void draw(final Canvas canvas, final ImmutableViewportMetrics metrics) {
+ if (metrics == null) {
+ return;
+ }
+
+ // If we're pulling an edge, or fading it out, draw!
+ boolean invalidate = false;
+ if (!mEdges[TOP].isFinished()) {
+ invalidate |= draw(mEdges[TOP], canvas, metrics.marginLeft, metrics.marginTop, 0);
+ }
+
+ if (!mEdges[BOTTOM].isFinished()) {
+ invalidate |= draw(mEdges[BOTTOM], canvas, mView.getWidth(), mView.getHeight(), 180);
+ }
+
+ if (!mEdges[LEFT].isFinished()) {
+ invalidate |= draw(mEdges[LEFT], canvas, metrics.marginLeft, mView.getHeight(), 270);
+ }
+
+ if (!mEdges[RIGHT].isFinished()) {
+ invalidate |= draw(mEdges[RIGHT], canvas, mView.getWidth(), metrics.marginTop, 90);
+ }
+
+ // If the edge effect is animating off screen, invalidate.
+ if (invalidate) {
+ invalidate();
+ }
+ }
+
+ public boolean draw(final EdgeEffect edge, final Canvas canvas, final float translateX, final float translateY, final float rotation) {
+ final int state = canvas.save();
+ canvas.translate(translateX, translateY);
+ canvas.rotate(rotation);
+ boolean invalidate = edge.draw(canvas);
+ canvas.restoreToCount(state);
+
+ return invalidate;
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java
new file mode 100644
index 000000000000..5ef25a64628c
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java
@@ -0,0 +1,49 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+//import org.mozilla.gecko.GeckoAppShell;
+import org.libreoffice.LOKitShell;
+import org.mozilla.gecko.util.EventDispatcher;
+
+import android.graphics.PointF;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+public interface PanZoomController {
+ // The distance the user has to pan before we recognize it as such (e.g. to avoid 1-pixel pans
+ // between the touch-down and touch-up of a click). In units of density-independent pixels.
+ public static final float PAN_THRESHOLD = 1/16f * /*GeckoAppShell*/LOKitShell.getDpi();
+
+ // Threshold for sending touch move events to content
+ public static final float CLICK_THRESHOLD = 1/50f * /*GeckoAppShell*/LOKitShell.getDpi();
+
+ static class Factory {
+ static PanZoomController create(PanZoomTarget target, View view, EventDispatcher dispatcher) {
+ return new JavaPanZoomController(target, view, dispatcher);
+ }
+ }
+
+ public void destroy();
+
+ public boolean onTouchEvent(MotionEvent event);
+ public boolean onMotionEvent(MotionEvent event);
+ public boolean onKeyEvent(KeyEvent event);
+ public void notifyDefaultActionPrevented(boolean prevented);
+
+ public boolean getRedrawHint();
+ public PointF getVelocityVector();
+
+ public void pageRectUpdated();
+ public void abortPanning();
+ public void abortAnimation();
+
+ public void setOverScrollMode(int overscrollMode);
+ public int getOverScrollMode();
+
+ public void setOverscrollHandler(final Overscroll controller);
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/PanZoomTarget.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/PanZoomTarget.java
new file mode 100644
index 000000000000..c32f213937f2
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/PanZoomTarget.java
@@ -0,0 +1,33 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import org.mozilla.gecko.ZoomConstraints;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+
+public interface PanZoomTarget {
+ public ImmutableViewportMetrics getViewportMetrics();
+ public ZoomConstraints getZoomConstraints();
+ public boolean isFullScreen();
+ public RectF getMaxMargins();
+
+ public void setAnimationTarget(ImmutableViewportMetrics viewport);
+ public void setViewportMetrics(ImmutableViewportMetrics viewport);
+ public void scrollBy(float dx, float dy);
+ public void scrollMarginsBy(float dx, float dy);
+ public void panZoomStopped();
+ /** This triggers an (asynchronous) viewport update/redraw. */
+ public void forceRedraw(DisplayPortMetrics displayPort);
+
+ public boolean post(Runnable action);
+ public boolean postDelayed(Runnable action, long delayMillis);
+ public void postRenderTask(RenderTask task);
+ public void removeRenderTask(RenderTask task);
+ public Object getLock();
+ public PointF convertViewPointToLayerPoint(PointF viewPoint);
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java
new file mode 100644
index 000000000000..7c2ca2b9d030
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java
@@ -0,0 +1,123 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+//import org.mozilla.gecko.mozglue.RobocopTarget;
+
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PanningPerfAPI {
+ private static final String LOGTAG = "GeckoPanningPerfAPI";
+
+ // make this large enough to avoid having to resize the frame time
+ // list, as that may be expensive and impact the thing we're trying
+ // to measure.
+ private static final int EXPECTED_FRAME_COUNT = 2048;
+
+ private static boolean mRecordingFrames = false;
+ private static List<Long> mFrameTimes;
+ private static long mFrameStartTime;
+
+ private static boolean mRecordingCheckerboard = false;
+ private static List<Float> mCheckerboardAmounts;
+ private static long mCheckerboardStartTime;
+
+ private static void initialiseRecordingArrays() {
+ if (mFrameTimes == null) {
+ mFrameTimes = new ArrayList<Long>(EXPECTED_FRAME_COUNT);
+ } else {
+ mFrameTimes.clear();
+ }
+ if (mCheckerboardAmounts == null) {
+ mCheckerboardAmounts = new ArrayList<Float>(EXPECTED_FRAME_COUNT);
+ } else {
+ mCheckerboardAmounts.clear();
+ }
+ }
+
+ //@RobocopTarget
+ public static void startFrameTimeRecording() {
+ if (mRecordingFrames || mRecordingCheckerboard) {
+ Log.e(LOGTAG, "Error: startFrameTimeRecording() called while already recording!");
+ return;
+ }
+ mRecordingFrames = true;
+ initialiseRecordingArrays();
+ mFrameStartTime = SystemClock.uptimeMillis();
+ }
+
+ //@RobocopTarget
+ public static List<Long> stopFrameTimeRecording() {
+ if (!mRecordingFrames) {
+ Log.e(LOGTAG, "Error: stopFrameTimeRecording() called when not recording!");
+ return null;
+ }
+ mRecordingFrames = false;
+ return mFrameTimes;
+ }
+
+ public static void recordFrameTime() {
+ // this will be called often, so try to make it as quick as possible
+ if (mRecordingFrames) {
+ mFrameTimes.add(SystemClock.uptimeMillis() - mFrameStartTime);
+ }
+ }
+
+ public static boolean isRecordingCheckerboard() {
+ return mRecordingCheckerboard;
+ }
+
+ //@RobocopTarget
+ public static void startCheckerboardRecording() {
+ if (mRecordingCheckerboard || mRecordingFrames) {
+ Log.e(LOGTAG, "Error: startCheckerboardRecording() called while already recording!");
+ return;
+ }
+ mRecordingCheckerboard = true;
+ initialiseRecordingArrays();
+ mCheckerboardStartTime = SystemClock.uptimeMillis();
+ }
+
+ //@RobocopTarget
+ public static List<Float> stopCheckerboardRecording() {
+ if (!mRecordingCheckerboard) {
+ Log.e(LOGTAG, "Error: stopCheckerboardRecording() called when not recording!");
+ return null;
+ }
+ mRecordingCheckerboard = false;
+
+ // We take the number of values in mCheckerboardAmounts here, as there's
+ // the possibility that this function is called while recordCheckerboard
+ // is still executing. As values are added to this list last, we use
+ // this number as the canonical number of recordings.
+ int values = mCheckerboardAmounts.size();
+
+ // The score will be the sum of all the values in mCheckerboardAmounts,
+ // so weight the checkerboard values by time so that frame-rate and
+ // run-length don't affect score.
+ long lastTime = 0;
+ float totalTime = mFrameTimes.get(values - 1);
+ for (int i = 0; i < values; i++) {
+ long elapsedTime = mFrameTimes.get(i) - lastTime;
+ mCheckerboardAmounts.set(i, mCheckerboardAmounts.get(i) * elapsedTime / totalTime);
+ lastTime += elapsedTime;
+ }
+
+ return mCheckerboardAmounts;
+ }
+
+ public static void recordCheckerboard(float amount) {
+ // this will be called often, so try to make it as quick as possible
+ if (mRecordingCheckerboard) {
+ mFrameTimes.add(SystemClock.uptimeMillis() - mCheckerboardStartTime);
+ mCheckerboardAmounts.add(amount);
+ }
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/PointUtils.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/PointUtils.java
new file mode 100644
index 000000000000..8db329c9fe8d
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/PointUtils.java
@@ -0,0 +1,51 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.graphics.Point;
+import android.graphics.PointF;
+
+public final class PointUtils {
+ public static PointF add(PointF one, PointF two) {
+ return new PointF(one.x + two.x, one.y + two.y);
+ }
+
+ public static PointF subtract(PointF one, PointF two) {
+ return new PointF(one.x - two.x, one.y - two.y);
+ }
+
+ public static PointF scale(PointF point, float factor) {
+ return new PointF(point.x * factor, point.y * factor);
+ }
+
+ public static Point round(PointF point) {
+ return new Point(Math.round(point.x), Math.round(point.y));
+ }
+
+ /* Computes the magnitude of the given vector. */
+ public static float distance(PointF point) {
+ return (float)Math.sqrt(point.x * point.x + point.y * point.y);
+ }
+
+ /** Computes the scalar distance between two points. */
+ public static float distance(PointF one, PointF two) {
+ return PointF.length(one.x - two.x, one.y - two.y);
+ }
+
+ public static JSONObject toJSON(PointF point) throws JSONException {
+ // Ensure we put ints, not longs, because Gecko message handlers call getInt().
+ int x = Math.round(point.x);
+ int y = Math.round(point.y);
+ JSONObject json = new JSONObject();
+ json.put("x", x);
+ json.put("y", y);
+ return json;
+ }
+}
+
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/ProgressiveUpdateData.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/ProgressiveUpdateData.java
new file mode 100644
index 000000000000..b7c381c688b0
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/ProgressiveUpdateData.java
@@ -0,0 +1,33 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+//import org.mozilla.gecko.mozglue.generatorannotations.WrapEntireClassForJNI;
+
+/**
+ * This is the data structure that's returned by the progressive tile update
+ * callback function. It encompasses the current viewport and a boolean value
+ * representing whether the front-end is interested in the current progressive
+ * update continuing.
+ */
+//@WrapEntireClassForJNI
+public class ProgressiveUpdateData {
+ public float x;
+ public float y;
+ public float width;
+ public float height;
+ public float scale;
+ public boolean abort;
+
+ public void setViewport(ImmutableViewportMetrics viewport) {
+ this.x = viewport.viewportRectLeft;
+ this.y = viewport.viewportRectTop;
+ this.width = viewport.viewportRectRight - this.x;
+ this.height = viewport.viewportRectBottom - this.y;
+ this.scale = viewport.zoomFactor;
+ }
+}
+
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/RectUtils.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/RectUtils.java
new file mode 100644
index 000000000000..22151db76921
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/RectUtils.java
@@ -0,0 +1,126 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import org.mozilla.gecko.util.FloatUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+public final class RectUtils {
+ private RectUtils() {}
+
+ public static Rect create(JSONObject json) {
+ try {
+ int x = json.getInt("x");
+ int y = json.getInt("y");
+ int width = json.getInt("width");
+ int height = json.getInt("height");
+ return new Rect(x, y, x + width, y + height);
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static String toJSON(RectF rect) {
+ StringBuilder sb = new StringBuilder(256);
+ sb.append("{ \"left\": ").append(rect.left)
+ .append(", \"top\": ").append(rect.top)
+ .append(", \"right\": ").append(rect.right)
+ .append(", \"bottom\": ").append(rect.bottom)
+ .append('}');
+ return sb.toString();
+ }
+
+ public static RectF expand(RectF rect, float moreWidth, float moreHeight) {
+ float halfMoreWidth = moreWidth / 2;
+ float halfMoreHeight = moreHeight / 2;
+ return new RectF(rect.left - halfMoreWidth,
+ rect.top - halfMoreHeight,
+ rect.right + halfMoreWidth,
+ rect.bottom + halfMoreHeight);
+ }
+
+ public static RectF contract(RectF rect, float lessWidth, float lessHeight) {
+ float halfLessWidth = lessWidth / 2.0f;
+ float halfLessHeight = lessHeight / 2.0f;
+ return new RectF(rect.left + halfLessWidth,
+ rect.top + halfLessHeight,
+ rect.right - halfLessWidth,
+ rect.bottom - halfLessHeight);
+ }
+
+ public static RectF intersect(RectF one, RectF two) {
+ float left = Math.max(one.left, two.left);
+ float top = Math.max(one.top, two.top);
+ float right = Math.min(one.right, two.right);
+ float bottom = Math.min(one.bottom, two.bottom);
+ return new RectF(left, top, Math.max(right, left), Math.max(bottom, top));
+ }
+
+ public static RectF scale(RectF rect, float scale) {
+ float x = rect.left * scale;
+ float y = rect.top * scale;
+ return new RectF(x, y,
+ x + (rect.width() * scale),
+ y + (rect.height() * scale));
+ }
+
+ public static RectF scaleAndRound(RectF rect, float scale) {
+ float left = rect.left * scale;
+ float top = rect.top * scale;
+ return new RectF(Math.round(left),
+ Math.round(top),
+ Math.round(left + (rect.width() * scale)),
+ Math.round(top + (rect.height() * scale)));
+ }
+
+ /** Returns the nearest integer rect of the given rect. */
+ public static Rect round(RectF rect) {
+ Rect r = new Rect();
+ round(rect, r);
+ return r;
+ }
+
+ public static void round(RectF rect, Rect dest) {
+ dest.set(Math.round(rect.left), Math.round(rect.top),
+ Math.round(rect.right), Math.round(rect.bottom));
+ }
+
+ public static Rect roundIn(RectF rect) {
+ return new Rect((int)Math.ceil(rect.left), (int)Math.ceil(rect.top),
+ (int)Math.floor(rect.right), (int)Math.floor(rect.bottom));
+ }
+
+ public static IntSize getSize(Rect rect) {
+ return new IntSize(rect.width(), rect.height());
+ }
+
+ public static Point getOrigin(Rect rect) {
+ return new Point(rect.left, rect.top);
+ }
+
+ public static PointF getOrigin(RectF rect) {
+ return new PointF(rect.left, rect.top);
+ }
+
+ public static boolean fuzzyEquals(RectF a, RectF b) {
+ if (a == null && b == null)
+ return true;
+ else if ((a == null && b != null) || (a != null && b == null))
+ return false;
+ else
+ return FloatUtils.fuzzyEquals(a.top, b.top)
+ && FloatUtils.fuzzyEquals(a.left, b.left)
+ && FloatUtils.fuzzyEquals(a.right, b.right)
+ && FloatUtils.fuzzyEquals(a.bottom, b.bottom);
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/RenderTask.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/RenderTask.java
new file mode 100644
index 000000000000..39c6eacb59f8
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/RenderTask.java
@@ -0,0 +1,80 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+/**
+ * A class used to schedule a callback to occur when the next frame is drawn.
+ * Subclasses must redefine the internalRun method, not the run method.
+ */
+public abstract class RenderTask {
+ /**
+ * Whether to run the task after the render, or before.
+ */
+ public final boolean runAfter;
+
+ /**
+ * Time when this task has first run, in ns. Useful for tasks which run for a specific duration.
+ */
+ private long mStartTime;
+
+ /**
+ * Whether we should initialise mStartTime on the next frame run.
+ */
+ private boolean mResetStartTime = true;
+
+ /**
+ * The callback to run on each frame. timeDelta is the time elapsed since
+ * the last call, in nanoseconds. Returns true if it should continue
+ * running, or false if it should be removed from the task queue. Returning
+ * true implicitly schedules a redraw.
+ *
+ * This method first initializes the start time if resetStartTime has been invoked,
+ * then calls internalRun.
+ *
+ * Note : subclasses should override internalRun.
+ *
+ * @param timeDelta the time between the beginning of last frame and the beginning of this frame, in ns.
+ * @param currentFrameStartTime the startTime of the current frame, in ns.
+ * @return true if animation should be run at the next frame, false otherwise
+ * @see org.mozilla.gecko.gfx.RenderTask#internalRun(long, long)
+ */
+ public final boolean run(long timeDelta, long currentFrameStartTime) {
+ if (mResetStartTime) {
+ mStartTime = currentFrameStartTime;
+ mResetStartTime = false;
+ }
+ return internalRun(timeDelta, currentFrameStartTime);
+ }
+
+ /**
+ * Abstract method to be overridden by subclasses.
+ * @param timeDelta the time between the beginning of last frame and the beginning of this frame, in ns
+ * @param currentFrameStartTime the startTime of the current frame, in ns.
+ * @return true if animation should be run at the next frame, false otherwise
+ */
+ protected abstract boolean internalRun(long timeDelta, long currentFrameStartTime);
+
+ public RenderTask(boolean aRunAfter) {
+ runAfter = aRunAfter;
+ }
+
+ /**
+ * Get the start time of this task.
+ * It is the start time of the first frame this task was run on.
+ * @return the start time in ns
+ */
+ public long getStartTime() {
+ return mStartTime;
+ }
+
+ /**
+ * Schedule a reset of the recorded start time next time {@link org.mozilla.gecko.gfx.RenderTask#run(long, long)} is run.
+ * @see org.mozilla.gecko.gfx.RenderTask#getStartTime()
+ */
+ public void resetStartTime() {
+ mResetStartTime = true;
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/ScrollbarLayer.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/ScrollbarLayer.java
new file mode 100644
index 000000000000..043c82775467
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/ScrollbarLayer.java
@@ -0,0 +1,297 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import org.mozilla.gecko.util.FloatUtils;
+
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.opengl.GLES20;
+
+import java.nio.FloatBuffer;
+
+public class ScrollbarLayer extends TileLayer {
+ public static final long FADE_DELAY = 500; // milliseconds before fade-out starts
+ private static final float FADE_AMOUNT = 0.03f; // how much (as a percent) the scrollbar should fade per frame
+
+ private final boolean mVertical;
+ private float mOpacity;
+
+ // To avoid excessive GC, declare some objects here that would otherwise
+ // be created and destroyed frequently during draw().
+ private final RectF mBarRectF;
+ private final Rect mBarRect;
+ private final float[] mCoords;
+ private final RectF mCapRectF;
+
+ private LayerRenderer mRenderer;
+ private int mProgram;
+ private int mPositionHandle;
+ private int mTextureHandle;
+ private int mSampleHandle;
+ private int mTMatrixHandle;
+ private int mOpacityHandle;
+
+ // Fragment shader used to draw the scroll-bar with opacity
+ private static final String FRAGMENT_SHADER =
+ "precision mediump float;\n" +
+ "varying vec2 vTexCoord;\n" +
+ "uniform sampler2D sTexture;\n" +
+ "uniform float uOpacity;\n" +
+ "void main() {\n" +
+ " gl_FragColor = texture2D(sTexture, vTexCoord);\n" +
+ " gl_FragColor.a *= uOpacity;\n" +
+ "}\n";
+
+ // Dimensions of the texture bitmap (will always be power-of-two)
+ private final int mTexWidth;
+ private final int mTexHeight;
+ // Some useful dimensions of the actual content in the bitmap
+ private final int mBarWidth;
+ private final int mCapLength;
+
+ private final Rect mStartCapTexCoords; // top/left endcap coordinates
+ private final Rect mBodyTexCoords; // 1-pixel slice of the texture to be stretched
+ private final Rect mEndCapTexCoords; // bottom/right endcap coordinates
+
+ ScrollbarLayer(LayerRenderer renderer, Bitmap scrollbarImage, IntSize imageSize, boolean vertical) {
+ super(new BufferedCairoImage(scrollbarImage), TileLayer.PaintMode.NORMAL);
+ mRenderer = renderer;
+ mVertical = vertical;
+
+ mBarRectF = new RectF();
+ mBarRect = new Rect();
+ mCoords = new float[20];
+ mCapRectF = new RectF();
+
+ mTexHeight = scrollbarImage.getHeight();
+ mTexWidth = scrollbarImage.getWidth();
+
+ if (mVertical) {
+ mBarWidth = imageSize.width;
+ mCapLength = imageSize.height / 2;
+ mStartCapTexCoords = new Rect(0, mTexHeight - mCapLength, imageSize.width, mTexHeight);
+ mBodyTexCoords = new Rect(0, mTexHeight - (mCapLength + 1), imageSize.width, mTexHeight - mCapLength);
+ mEndCapTexCoords = new Rect(0, mTexHeight - imageSize.height, imageSize.width, mTexHeight - (mCapLength + 1));
+ } else {
+ mBarWidth = imageSize.height;
+ mCapLength = imageSize.width / 2;
+ mStartCapTexCoords = new Rect(0, mTexHeight - imageSize.height, mCapLength, mTexHeight);
+ mBodyTexCoords = new Rect(mCapLength, mTexHeight - imageSize.height, mCapLength + 1, mTexHeight);
+ mEndCapTexCoords = new Rect(mCapLength + 1, mTexHeight - imageSize.height, imageSize.width, mTexHeight);
+ }
+ }
+
+ private void createProgram() {
+ int vertexShader = LayerRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
+ LayerRenderer.DEFAULT_VERTEX_SHADER);
+ int fragmentShader = LayerRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
+ FRAGMENT_SHADER);
+
+ mProgram = GLES20.glCreateProgram();
+ GLES20.glAttachShader(mProgram, vertexShader); // add the vertex shader to program
+ GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
+ GLES20.glLinkProgram(mProgram); // creates OpenGL program executables
+
+ // Get handles to the shaders' vPosition, aTexCoord, sTexture, and uTMatrix members.
+ mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
+ mTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTexCoord");
+ mSampleHandle = GLES20.glGetUniformLocation(mProgram, "sTexture");
+ mTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uTMatrix");
+ mOpacityHandle = GLES20.glGetUniformLocation(mProgram, "uOpacity");
+ }
+
+ private void activateProgram() {
+ // Add the program to the OpenGL environment
+ GLES20.glUseProgram(mProgram);
+
+ // Set the transformation matrix
+ GLES20.glUniformMatrix4fv(mTMatrixHandle, 1, false,
+ LayerRenderer.DEFAULT_TEXTURE_MATRIX, 0);
+
+ // Enable the arrays from which we get the vertex and texture coordinates
+ GLES20.glEnableVertexAttribArray(mPositionHandle);
+ GLES20.glEnableVertexAttribArray(mTextureHandle);
+
+ GLES20.glUniform1i(mSampleHandle, 0);
+ GLES20.glUniform1f(mOpacityHandle, mOpacity);
+ }
+
+ private void deactivateProgram() {
+ GLES20.glDisableVertexAttribArray(mTextureHandle);
+ GLES20.glDisableVertexAttribArray(mPositionHandle);
+ GLES20.glUseProgram(0);
+ }
+
+ /**
+ * Decrease the opacity of the scrollbar by one frame's worth.
+ * Return true if the opacity was decreased, or false if the scrollbars
+ * are already fully faded out.
+ */
+ public boolean fade() {
+ if (FloatUtils.fuzzyEquals(mOpacity, 0.0f)) {
+ return false;
+ }
+ beginTransaction(); // called on compositor thread
+ mOpacity = Math.max(mOpacity - FADE_AMOUNT, 0.0f);
+ endTransaction();
+ return true;
+ }
+
+ /**
+ * Restore the opacity of the scrollbar to fully opaque.
+ * Return true if the opacity was changed, or false if the scrollbars
+ * are already fully opaque.
+ */
+ public boolean unfade() {
+ if (FloatUtils.fuzzyEquals(mOpacity, 1.0f)) {
+ return false;
+ }
+ beginTransaction(); // called on compositor thread
+ mOpacity = 1.0f;
+ endTransaction();
+ return true;
+ }
+
+ @Override
+ public void draw(RenderContext context) {
+ if (!initialized())
+ return;
+
+ // Create the shader program, if necessary
+ if (mProgram == 0) {
+ createProgram();
+ }
+
+ // Enable the shader program
+ mRenderer.deactivateDefaultProgram();
+ activateProgram();
+
+ GLES20.glEnable(GLES20.GL_BLEND);
+ GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
+
+ if (mVertical) {
+ getVerticalRect(context, mBarRectF);
+ } else {
+ getHorizontalRect(context, mBarRectF);
+ }
+ RectUtils.round(mBarRectF, mBarRect);
+
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getTextureID());
+
+ float viewWidth = context.viewport.width();
+ float viewHeight = context.viewport.height();
+
+ mBarRectF.set(mBarRect.left, viewHeight - mBarRect.top, mBarRect.right, viewHeight - mBarRect.bottom);
+ mBarRectF.offset(context.offset.x, -context.offset.y);
+
+ // We take a 1-pixel slice from the center of the image and scale it to become the bar
+ fillRectCoordBuffer(mCoords, mBarRectF, viewWidth, viewHeight, mBodyTexCoords, mTexWidth, mTexHeight);
+
+ // Get the buffer and handles from the context
+ FloatBuffer coordBuffer = context.coordBuffer;
+ int positionHandle = mPositionHandle;
+ int textureHandle = mTextureHandle;
+
+ // Make sure we are at position zero in the buffer in case other draw methods did not
+ // clean up after themselves
+ coordBuffer.position(0);
+ coordBuffer.put(mCoords);
+
+ // Unbind any the current array buffer so we can use client side buffers
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+
+ // Vertex coordinates are x,y,z starting at position 0 into the buffer.
+ coordBuffer.position(0);
+ GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, coordBuffer);
+
+ // Texture coordinates are texture_x, texture_y starting at position 3 into the buffer.
+ coordBuffer.position(3);
+ GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, coordBuffer);
+
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+
+ // Reset the position in the buffer for the next set of vertex and texture coordinates.
+ coordBuffer.position(0);
+ if (mVertical) {
+ // top endcap
+ mCapRectF.set(mBarRectF.left, mBarRectF.top + mCapLength, mBarRectF.right, mBarRectF.top);
+ } else {
+ // left endcap
+ mCapRectF.set(mBarRectF.left - mCapLength, mBarRectF.bottom + mBarWidth, mBarRectF.left, mBarRectF.bottom);
+ }
+
+ fillRectCoordBuffer(mCoords, mCapRectF, viewWidth, viewHeight, mStartCapTexCoords, mTexWidth, mTexHeight);
+ coordBuffer.put(mCoords);
+
+ // Vertex coordinates are x,y,z starting at position 0 into the buffer.
+ coordBuffer.position(0);
+ GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, coordBuffer);
+
+ // Texture coordinates are texture_x, texture_y starting at position 3 into the buffer.
+ coordBuffer.position(3);
+ GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, coordBuffer);
+
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+
+ // Reset the position in the buffer for the next set of vertex and texture coordinates.
+ coordBuffer.position(0);
+ if (mVertical) {
+ // bottom endcap
+ mCapRectF.set(mBarRectF.left, mBarRectF.bottom, mBarRectF.right, mBarRectF.bottom - mCapLength);
+ } else {
+ // right endcap
+ mCapRectF.set(mBarRectF.right, mBarRectF.bottom + mBarWidth, mBarRectF.right + mCapLength, mBarRectF.bottom);
+ }
+ fillRectCoordBuffer(mCoords, mCapRectF, viewWidth, viewHeight, mEndCapTexCoords, mTexWidth, mTexHeight);
+ coordBuffer.put(mCoords);
+
+ // Vertex coordinates are x,y,z starting at position 0 into the buffer.
+ coordBuffer.position(0);
+ GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, coordBuffer);
+
+ // Texture coordinates are texture_x, texture_y starting at position 3 into the buffer.
+ coordBuffer.position(3);
+ GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, coordBuffer);
+
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+
+ // Reset the position in the buffer for the next set of vertex and texture coordinates.
+ coordBuffer.position(0);
+
+ // Enable the default shader program again
+ deactivateProgram();
+ mRenderer.activateDefaultProgram();
+ }
+
+ private void getVerticalRect(RenderContext context, RectF dest) {
+ RectF viewport = context.viewport;
+ RectF pageRect = context.pageRect;
+ float viewportHeight = viewport.height() - context.offset.y;
+ float barStart = ((viewport.top - context.offset.y - pageRect.top) * (viewportHeight / pageRect.height())) + mCapLength;
+ float barEnd = ((viewport.bottom - context.offset.y - pageRect.top) * (viewportHeight / pageRect.height())) - mCapLength;
+ if (barStart > barEnd) {
+ float middle = (barStart + barEnd) / 2.0f;
+ barStart = barEnd = middle;
+ }
+ dest.set(viewport.width() - mBarWidth, barStart, viewport.width(), barEnd);
+ }
+
+ private void getHorizontalRect(RenderContext context, RectF dest) {
+ RectF viewport = context.viewport;
+ RectF pageRect = context.pageRect;
+ float viewportWidth = viewport.width() - context.offset.x;
+ float barStart = ((viewport.left - context.offset.x - pageRect.left) * (viewport.width() / pageRect.width())) + mCapLength;
+ float barEnd = ((viewport.right - context.offset.x - pageRect.left) * (viewport.width() / pageRect.width())) - mCapLength;
+ if (barStart > barEnd) {
+ float middle = (barStart + barEnd) / 2.0f;
+ barStart = barEnd = middle;
+ }
+ dest.set(barStart, viewport.height() - mBarWidth, barEnd, viewport.height());
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/SimpleScaleGestureDetector.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/SimpleScaleGestureDetector.java
new file mode 100644
index 000000000000..b3f6fcbc55e9
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/SimpleScaleGestureDetector.java
@@ -0,0 +1,322 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import org.json.JSONException;
+
+import android.graphics.PointF;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import java.util.LinkedList;
+import java.util.ListIterator;
+import java.util.Stack;
+
+/**
+ * A less buggy, and smoother, replacement for the built-in Android ScaleGestureDetector.
+ *
+ * This gesture detector is more reliable than the built-in ScaleGestureDetector because:
+ *
+ * - It doesn't assume that pointer IDs are numbered 0 and 1.
+ *
+ * - It doesn't attempt to correct for "slop" when resting one's hand on the device. On some
+ * devices (e.g. the Droid X) this can cause the ScaleGestureDetector to lose track of how many
+ * pointers are down, with disastrous results (bug 706684).
+ *
+ * - Cancelling a zoom into a pan is handled correctly.
+ *
+ * - Starting with three or more fingers down, releasing fingers so that only two are down, and
+ * then performing a scale gesture is handled correctly.
+ *
+ * - It doesn't take pressure into account, which results in smoother scaling.
+ */
+class SimpleScaleGestureDetector {
+ private static final String LOGTAG = "GeckoSimpleScaleGestureDetector";
+
+ private SimpleScaleGestureListener mListener;
+ private long mLastEventTime;
+ private boolean mScaleResult;
+
+ /* Information about all pointers that are down. */
+ private LinkedList<PointerInfo> mPointerInfo;
+
+ /** Creates a new gesture detector with the given listener. */
+ SimpleScaleGestureDetector(SimpleScaleGestureListener listener) {
+ mListener = listener;
+ mPointerInfo = new LinkedList<PointerInfo>();
+ }
+
+ /** Forward touch events to this function. */
+ public void onTouchEvent(MotionEvent event) {
+ switch (event.getAction() & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN:
+ // If we get ACTION_DOWN while still tracking any pointers,
+ // something is wrong. Cancel the current gesture and start over.
+ if (getPointersDown() > 0)
+ onTouchEnd(event);
+ onTouchStart(event);
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ onTouchStart(event);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ onTouchMove(event);
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ onTouchEnd(event);
+ break;
+ }
+ }
+
+ private int getPointersDown() {
+ return mPointerInfo.size();
+ }
+
+ private int getActionIndex(MotionEvent event) {
+ return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
+ >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+ }
+
+ private void onTouchStart(MotionEvent event) {
+ mLastEventTime = event.getEventTime();
+ mPointerInfo.addFirst(PointerInfo.create(event, getActionIndex(event)));
+ if (getPointersDown() == 2) {
+ sendScaleGesture(EventType.BEGIN);
+ }
+ }
+
+ private void onTouchMove(MotionEvent event) {
+ mLastEventTime = event.getEventTime();
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ PointerInfo pointerInfo = pointerInfoForEventIndex(event, i);
+ if (pointerInfo != null) {
+ pointerInfo.populate(event, i);
+ }
+ }
+
+ if (getPointersDown() == 2) {
+ sendScaleGesture(EventType.CONTINUE);
+ }
+ }
+
+ private void onTouchEnd(MotionEvent event) {
+ mLastEventTime = event.getEventTime();
+
+ int action = event.getAction() & MotionEvent.ACTION_MASK;
+ boolean isCancel = (action == MotionEvent.ACTION_CANCEL ||
+ action == MotionEvent.ACTION_DOWN);
+
+ int id = event.getPointerId(getActionIndex(event));
+ ListIterator<PointerInfo> iterator = mPointerInfo.listIterator();
+ while (iterator.hasNext()) {
+ PointerInfo pointerInfo = iterator.next();
+ if (!(isCancel || pointerInfo.getId() == id)) {
+ continue;
+ }
+
+ // One of the pointers we were tracking was lifted. Remove its info object from the
+ // list, recycle it to avoid GC pauses, and send an onScaleEnd() notification if this
+ // ended the gesture.
+ iterator.remove();
+ pointerInfo.recycle();
+ if (getPointersDown() == 1) {
+ sendScaleGesture(EventType.END);
+ }
+ }
+ }
+
+ /**
+ * Returns the X coordinate of the focus location (the midpoint of the two fingers). If only
+ * one finger is down, returns the location of that finger.
+ */
+ public float getFocusX() {
+ switch (getPointersDown()) {
+ case 1:
+ return mPointerInfo.getFirst().getCurrent().x;
+ case 2:
+ PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast();
+ return (pointerA.getCurrent().x + pointerB.getCurrent().x) / 2.0f;
+ }
+
+ Log.e(LOGTAG, "No gesture taking place in getFocusX()!");
+ return 0.0f;
+ }
+
+ /**
+ * Returns the Y coordinate of the focus location (the midpoint of the two fingers). If only
+ * one finger is down, returns the location of that finger.
+ */
+ public float getFocusY() {
+ switch (getPointersDown()) {
+ case 1:
+ return mPointerInfo.getFirst().getCurrent().y;
+ case 2:
+ PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast();
+ return (pointerA.getCurrent().y + pointerB.getCurrent().y) / 2.0f;
+ }
+
+ Log.e(LOGTAG, "No gesture taking place in getFocusY()!");
+ return 0.0f;
+ }
+
+ /** Returns the most recent distance between the two pointers. */
+ public float getCurrentSpan() {
+ if (getPointersDown() != 2) {
+ Log.e(LOGTAG, "No gesture taking place in getCurrentSpan()!");
+ return 0.0f;
+ }
+
+ PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast();
+ return PointUtils.distance(pointerA.getCurrent(), pointerB.getCurrent());
+ }
+
+ /** Returns the second most recent distance between the two pointers. */
+ public float getPreviousSpan() {
+ if (getPointersDown() != 2) {
+ Log.e(LOGTAG, "No gesture taking place in getPreviousSpan()!");
+ return 0.0f;
+ }
+
+ PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast();
+ PointF a = pointerA.getPrevious(), b = pointerB.getPrevious();
+ if (a == null || b == null) {
+ a = pointerA.getCurrent();
+ b = pointerB.getCurrent();
+ }
+
+ return PointUtils.distance(a, b);
+ }
+
+ /** Returns the time of the last event related to the gesture. */
+ public long getEventTime() {
+ return mLastEventTime;
+ }
+
+ /** Returns true if the scale gesture is in progress and false otherwise. */
+ public boolean isInProgress() {
+ return getPointersDown() == 2;
+ }
+
+ /* Sends the requested scale gesture notification to the listener. */
+ private void sendScaleGesture(EventType eventType) {
+ switch (eventType) {
+ case BEGIN:
+ mScaleResult = mListener.onScaleBegin(this);
+ break;
+ case CONTINUE:
+ if (mScaleResult) {
+ mListener.onScale(this);
+ }
+ break;
+ case END:
+ if (mScaleResult) {
+ mListener.onScaleEnd(this);
+ }
+ break;
+ }
+ }
+
+ /*
+ * Returns the pointer info corresponding to the given pointer index, or null if the pointer
+ * isn't one that's being tracked.
+ */
+ private PointerInfo pointerInfoForEventIndex(MotionEvent event, int index) {
+ int id = event.getPointerId(index);
+ for (PointerInfo pointerInfo : mPointerInfo) {
+ if (pointerInfo.getId() == id) {
+ return pointerInfo;
+ }
+ }
+ return null;
+ }
+
+ private enum EventType {
+ BEGIN,
+ CONTINUE,
+ END,
+ }
+
+ /* Encapsulates information about one of the two fingers involved in the gesture. */
+ private static class PointerInfo {
+ /* A free list that recycles pointer info objects, to reduce GC pauses. */
+ private static Stack<PointerInfo> sPointerInfoFreeList;
+
+ private int mId;
+ private PointF mCurrent, mPrevious;
+
+ private PointerInfo() {
+ // External users should use create() instead.
+ }
+
+ /* Creates or recycles a new PointerInfo instance from an event and a pointer index. */
+ public static PointerInfo create(MotionEvent event, int index) {
+ if (sPointerInfoFreeList == null) {
+ sPointerInfoFreeList = new Stack<PointerInfo>();
+ }
+
+ PointerInfo pointerInfo;
+ if (sPointerInfoFreeList.empty()) {
+ pointerInfo = new PointerInfo();
+ } else {
+ pointerInfo = sPointerInfoFreeList.pop();
+ }
+
+ pointerInfo.populate(event, index);
+ return pointerInfo;
+ }
+
+ /*
+ * Fills in the fields of this instance from the given motion event and pointer index
+ * within that event.
+ */
+ public void populate(MotionEvent event, int index) {
+ mId = event.getPointerId(index);
+ mPrevious = mCurrent;
+ mCurrent = new PointF(event.getX(index), event.getY(index));
+ }
+
+ public void recycle() {
+ mId = -1;
+ mPrevious = mCurrent = null;
+ sPointerInfoFreeList.push(this);
+ }
+
+ public int getId() { return mId; }
+ public PointF getCurrent() { return mCurrent; }
+ public PointF getPrevious() { return mPrevious; }
+
+ @Override
+ public String toString() {
+ if (mId == -1) {
+ return "(up)";
+ }
+
+ try {
+ String prevString;
+ if (mPrevious == null) {
+ prevString = "n/a";
+ } else {
+ prevString = PointUtils.toJSON(mPrevious).toString();
+ }
+
+ // The current position should always be non-null.
+ String currentString = PointUtils.toJSON(mCurrent).toString();
+ return "id=" + mId + " cur=" + currentString + " prev=" + prevString;
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ public static interface SimpleScaleGestureListener {
+ public boolean onScale(SimpleScaleGestureDetector detector);
+ public boolean onScaleBegin(SimpleScaleGestureDetector detector);
+ public void onScaleEnd(SimpleScaleGestureDetector detector);
+ }
+}
+
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/SingleTileLayer.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/SingleTileLayer.java
new file mode 100644
index 000000000000..4b29c515271a
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/SingleTileLayer.java
@@ -0,0 +1,153 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.graphics.RegionIterator;
+import android.opengl.GLES20;
+
+import java.nio.FloatBuffer;
+
+/**
+ * Encapsulates the logic needed to draw a single textured tile.
+ *
+ * TODO: Repeating textures really should be their own type of layer.
+ */
+public class SingleTileLayer extends TileLayer {
+ private static final String LOGTAG = "GeckoSingleTileLayer";
+
+ private Rect mMask;
+
+ // To avoid excessive GC, declare some objects here that would otherwise
+ // be created and destroyed frequently during draw().
+ private final RectF mBounds;
+ private final RectF mTextureBounds;
+ private final RectF mViewport;
+ private final Rect mIntBounds;
+ private final Rect mSubRect;
+ private final RectF mSubRectF;
+ private final Region mMaskedBounds;
+ private final Rect mCropRect;
+ private final RectF mObjRectF;
+ private final float[] mCoords;
+
+ public SingleTileLayer(CairoImage image) {
+ this(false, image);
+ }
+
+ public SingleTileLayer(boolean repeat, CairoImage image) {
+ this(image, repeat ? PaintMode.REPEAT : PaintMode.NORMAL);
+ }
+
+ public SingleTileLayer(CairoImage image, PaintMode paintMode) {
+ super(image, paintMode);
+
+ mBounds = new RectF();
+ mTextureBounds = new RectF();
+ mViewport = new RectF();
+ mIntBounds = new Rect();
+ mSubRect = new Rect();
+ mSubRectF = new RectF();
+ mMaskedBounds = new Region();
+ mCropRect = new Rect();
+ mObjRectF = new RectF();
+ mCoords = new float[20];
+ }
+
+ /**
+ * Set an area to mask out when rendering.
+ */
+ public void setMask(Rect aMaskRect) {
+ mMask = aMaskRect;
+ }
+
+ @Override
+ public void draw(RenderContext context) {
+ // mTextureIDs may be null here during startup if Layer.java's draw method
+ // failed to acquire the transaction lock and call performUpdates.
+ if (!initialized())
+ return;
+
+ mViewport.set(context.viewport);
+
+ if (repeats()) {
+ // If we're repeating, we want to adjust the texture bounds so that
+ // the texture repeats the correct number of times when drawn at
+ // the size of the viewport.
+ mBounds.set(getBounds(context));
+ mTextureBounds.set(0.0f, 0.0f, mBounds.width(), mBounds.height());
+ mBounds.set(0.0f, 0.0f, mViewport.width(), mViewport.height());
+ } else if (stretches()) {
+ // If we're stretching, we just want the bounds and texture bounds
+ // to fit to the page.
+ mBounds.set(context.pageRect);
+ mTextureBounds.set(mBounds);
+ } else {
+ mBounds.set(getBounds(context));
+ mTextureBounds.set(mBounds);
+ }
+
+ mBounds.roundOut(mIntBounds);
+ mMaskedBounds.set(mIntBounds);
+ if (mMask != null) {
+ mMaskedBounds.op(mMask, Region.Op.DIFFERENCE);
+ if (mMaskedBounds.isEmpty())
+ return;
+ }
+
+ // XXX Possible optimisation here, form this array so we can draw it in
+ // a single call.
+ RegionIterator i = new RegionIterator(mMaskedBounds);
+ while (i.next(mSubRect)) {
+ // Compensate for rounding errors at the edge of the tile caused by
+ // the roundOut above
+ mSubRectF.set(Math.max(mBounds.left, (float)mSubRect.left),
+ Math.max(mBounds.top, (float)mSubRect.top),
+ Math.min(mBounds.right, (float)mSubRect.right),
+ Math.min(mBounds.bottom, (float)mSubRect.bottom));
+
+ // This is the left/top/right/bottom of the rect, relative to the
+ // bottom-left of the layer, to use for texture coordinates.
+ mCropRect.set(Math.round(mSubRectF.left - mBounds.left),
+ Math.round(mBounds.bottom - mSubRectF.top),
+ Math.round(mSubRectF.right - mBounds.left),
+ Math.round(mBounds.bottom - mSubRectF.bottom));
+
+ mObjRectF.set(mSubRectF.left - mViewport.left,
+ mViewport.bottom - mSubRectF.bottom,
+ mSubRectF.right - mViewport.left,
+ mViewport.bottom - mSubRectF.top);
+
+ fillRectCoordBuffer(mCoords, mObjRectF, mViewport.width(), mViewport.height(),
+ mCropRect, mTextureBounds.width(), mTextureBounds.height());
+
+ FloatBuffer coordBuffer = context.coordBuffer;
+ int positionHandle = context.positionHandle;
+ int textureHandle = context.textureHandle;
+
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getTextureID());
+
+ // Make sure we are at position zero in the buffer
+ coordBuffer.position(0);
+ coordBuffer.put(mCoords);
+
+ // Unbind any the current array buffer so we can use client side buffers
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+
+ // Vertex coordinates are x,y,z starting at position 0 into the buffer.
+ coordBuffer.position(0);
+ GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, coordBuffer);
+
+ // Texture coordinates are texture_x, texture_y starting at position 3 into the buffer.
+ coordBuffer.position(3);
+ GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, coordBuffer);
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+ }
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java
new file mode 100644
index 000000000000..b581d3147ec1
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java
@@ -0,0 +1,148 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+//import org.mozilla.gecko.GeckoAppShell;
+//import org.mozilla.gecko.GeckoEvent;
+import org.mozilla.gecko.util.EventDispatcher;
+import org.mozilla.gecko.util.GeckoEventListener;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.graphics.PointF;
+import android.os.Handler;
+import android.util.Log;
+
+class SubdocumentScrollHelper implements GeckoEventListener {
+ private static final String LOGTAG = "GeckoSubdocScroll";
+
+ private static String MESSAGE_PANNING_OVERRIDE = "Panning:Override";
+ private static String MESSAGE_CANCEL_OVERRIDE = "Panning:CancelOverride";
+ private static String MESSAGE_SCROLL = "Gesture:Scroll";
+ private static String MESSAGE_SCROLL_ACK = "Gesture:ScrollAck";
+
+ private final Handler mUiHandler;
+ private final EventDispatcher mEventDispatcher;
+
+ /* This is the amount of displacement we have accepted but not yet sent to JS; this is
+ * only valid when mOverrideScrollPending is true. */
+ private final PointF mPendingDisplacement;
+
+ /* When this is true, we're sending scroll events to JS to scroll the active subdocument. */
+ private boolean mOverridePanning;
+
+ /* When this is true, we have received an ack for the last scroll event we sent to JS, and
+ * are ready to send the next scroll event. Note we only ever have one scroll event inflight
+ * at a time. */
+ private boolean mOverrideScrollAck;
+
+ /* When this is true, we have a pending scroll that we need to send to JS; we were unable
+ * to send it when it was initially requested because mOverrideScrollAck was not true. */
+ private boolean mOverrideScrollPending;
+
+ /* When this is true, the last scroll event we sent actually did some amount of scrolling on
+ * the subdocument; we use this to decide when we have reached the end of the subdocument. */
+ private boolean mScrollSucceeded;
+
+ SubdocumentScrollHelper(EventDispatcher eventDispatcher) {
+ // mUiHandler will be bound to the UI thread since that's where this constructor runs
+ mUiHandler = new Handler();
+ mPendingDisplacement = new PointF();
+
+ mEventDispatcher = eventDispatcher;
+ registerEventListener(MESSAGE_PANNING_OVERRIDE);
+ registerEventListener(MESSAGE_CANCEL_OVERRIDE);
+ registerEventListener(MESSAGE_SCROLL_ACK);
+ }
+
+ void destroy() {
+ unregisterEventListener(MESSAGE_PANNING_OVERRIDE);
+ unregisterEventListener(MESSAGE_CANCEL_OVERRIDE);
+ unregisterEventListener(MESSAGE_SCROLL_ACK);
+ }
+
+ private void registerEventListener(String event) {
+ mEventDispatcher.registerEventListener(event, this);
+ }
+
+ private void unregisterEventListener(String event) {
+ mEventDispatcher.unregisterEventListener(event, this);
+ }
+
+ boolean scrollBy(PointF displacement) {
+ if (! mOverridePanning) {
+ return false;
+ }
+
+ if (! mOverrideScrollAck) {
+ mOverrideScrollPending = true;
+ mPendingDisplacement.x += displacement.x;
+ mPendingDisplacement.y += displacement.y;
+ return true;
+ }
+
+ JSONObject json = new JSONObject();
+ try {
+ json.put("x", displacement.x);
+ json.put("y", displacement.y);
+ } catch (JSONException e) {
+ Log.e(LOGTAG, "Error forming subwindow scroll message: ", e);
+ }
+ //GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(MESSAGE_SCROLL, json.toString()));
+
+ mOverrideScrollAck = false;
+ mOverrideScrollPending = false;
+ // clear the |mPendingDisplacement| after serializing |displacement| to
+ // JSON because they might be the same object
+ mPendingDisplacement.x = 0;
+ mPendingDisplacement.y = 0;
+
+ return true;
+ }
+
+ void cancel() {
+ mOverridePanning = false;
+ }
+
+ boolean scrolling() {
+ return mOverridePanning;
+ }
+
+ boolean lastScrollSucceeded() {
+ return mScrollSucceeded;
+ }
+
+ // GeckoEventListener implementation
+
+ @Override
+ public void handleMessage(final String event, final JSONObject message) {
+ // This comes in on the Gecko thread; hand off the handling to the UI thread.
+ mUiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (MESSAGE_PANNING_OVERRIDE.equals(event)) {
+ mOverridePanning = true;
+ mOverrideScrollAck = true;
+ mOverrideScrollPending = false;
+ mScrollSucceeded = true;
+ } else if (MESSAGE_CANCEL_OVERRIDE.equals(event)) {
+ mOverridePanning = false;
+ } else if (MESSAGE_SCROLL_ACK.equals(event)) {
+ mOverrideScrollAck = true;
+ mScrollSucceeded = message.getBoolean("scrolled");
+ if (mOverridePanning && mOverrideScrollPending) {
+ scrollBy(mPendingDisplacement);
+ }
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Exception handling message", e);
+ }
+ }
+ });
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TextLayer.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TextLayer.java
new file mode 100644
index 000000000000..c8eb99cb4a88
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TextLayer.java
@@ -0,0 +1,69 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import org.mozilla.gecko.mozglue.DirectBufferAllocator;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Draws text on a layer. This is used for the frame rate meter.
+ */
+public class TextLayer extends SingleTileLayer {
+ private final ByteBuffer mBuffer; // this buffer is owned by the BufferedCairoImage
+ private final IntSize mSize;
+
+ /*
+ * This awkward pattern is necessary due to Java's restrictions on when one can call superclass
+ * constructors.
+ */
+ private TextLayer(ByteBuffer buffer, BufferedCairoImage image, IntSize size, String text) {
+ super(false, image);
+ mBuffer = buffer;
+ mSize = size;
+ renderText(text);
+ }
+
+ public static TextLayer create(IntSize size, String text) {
+ ByteBuffer buffer = DirectBufferAllocator.allocate(size.width * size.height * 4);
+ BufferedCairoImage image = new BufferedCairoImage(buffer, size.width, size.height,
+ CairoImage.FORMAT_ARGB32);
+ return new TextLayer(buffer, image, size, text);
+ }
+
+ public void setText(String text) {
+ renderText(text);
+ invalidate();
+ }
+
+ private void renderText(String text) {
+ Bitmap bitmap = Bitmap.createBitmap(mSize.width, mSize.height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+
+ Paint textPaint = new Paint();
+ textPaint.setAntiAlias(true);
+ textPaint.setColor(Color.WHITE);
+ textPaint.setFakeBoldText(true);
+ textPaint.setTextSize(18.0f);
+ textPaint.setTypeface(Typeface.DEFAULT_BOLD);
+ float width = textPaint.measureText(text) + 18.0f;
+
+ Paint backgroundPaint = new Paint();
+ backgroundPaint.setColor(Color.argb(127, 0, 0, 0));
+ canvas.drawRect(0.0f, 0.0f, width, 18.0f + 6.0f, backgroundPaint);
+
+ canvas.drawText(text, 6.0f, 18.0f, textPaint);
+
+ bitmap.copyPixelsToBuffer(mBuffer.asIntBuffer());
+ }
+}
+
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TextureGenerator.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TextureGenerator.java
new file mode 100644
index 000000000000..239ba7bf7694
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TextureGenerator.java
@@ -0,0 +1,75 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import android.opengl.GLES20;
+import android.util.Log;
+
+import java.util.concurrent.ArrayBlockingQueue;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLContext;
+
+public class TextureGenerator {
+ private static final String LOGTAG = "TextureGenerator";
+ private static final int POOL_SIZE = 5;
+
+ private static TextureGenerator sSharedInstance;
+
+ private ArrayBlockingQueue<Integer> mTextureIds;
+ private EGLContext mContext;
+
+ private TextureGenerator() { mTextureIds = new ArrayBlockingQueue<Integer>(POOL_SIZE); }
+
+ public static TextureGenerator get() {
+ if (sSharedInstance == null)
+ sSharedInstance = new TextureGenerator();
+ return sSharedInstance;
+ }
+
+ public synchronized int take() {
+ try {
+ // Will block until one becomes available
+ return (int)mTextureIds.take();
+ } catch (InterruptedException e) {
+ return 0;
+ }
+ }
+
+ public synchronized void fill() {
+ EGL10 egl = (EGL10)EGLContext.getEGL();
+ EGLContext context = egl.eglGetCurrentContext();
+
+ if (mContext != null && mContext != context) {
+ mTextureIds.clear();
+ }
+
+ mContext = context;
+
+ int numNeeded = mTextureIds.remainingCapacity();
+ if (numNeeded == 0)
+ return;
+
+ // Clear existing GL errors
+ int error;
+ while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
+ Log.w(LOGTAG, String.format("Clearing GL error: %#x", error));
+ }
+
+ int[] textures = new int[numNeeded];
+ GLES20.glGenTextures(numNeeded, textures, 0);
+
+ error = GLES20.glGetError();
+ if (error != GLES20.GL_NO_ERROR) {
+ Log.e(LOGTAG, String.format("Failed to generate textures: %#x", error), new Exception());
+ return;
+ }
+
+ for (int i = 0; i < numNeeded; i++) {
+ mTextureIds.offer(textures[i]);
+ }
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TextureReaper.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TextureReaper.java
new file mode 100644
index 000000000000..71b4690eb9f1
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TextureReaper.java
@@ -0,0 +1,51 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import android.opengl.GLES20;
+
+import java.util.ArrayList;
+
+/** Manages a list of dead tiles, so we don't leak resources. */
+public class TextureReaper {
+ private static TextureReaper sSharedInstance;
+ private ArrayList<Integer> mDeadTextureIDs;
+
+ private TextureReaper() { mDeadTextureIDs = new ArrayList<Integer>(); }
+
+ public static TextureReaper get() {
+ if (sSharedInstance == null)
+ sSharedInstance = new TextureReaper();
+ return sSharedInstance;
+ }
+
+ public void add(int[] textureIDs) {
+ for (int textureID : textureIDs)
+ add(textureID);
+ }
+
+ public void add(int textureID) {
+ mDeadTextureIDs.add(textureID);
+ }
+
+ public void reap() {
+ int numTextures = mDeadTextureIDs.size();
+ // Adreno 200 will generate INVALID_VALUE if len == 0 is passed to glDeleteTextures,
+ // even though it's not supposed to.
+ if (numTextures == 0)
+ return;
+
+ int[] deadTextureIDs = new int[numTextures];
+ for (int i = 0; i < numTextures; i++) {
+ deadTextureIDs[i] = mDeadTextureIDs.get(i);
+ }
+ mDeadTextureIDs.clear();
+
+ GLES20.glDeleteTextures(deadTextureIDs.length, deadTextureIDs, 0);
+ }
+}
+
+
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TileLayer.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TileLayer.java
new file mode 100644
index 000000000000..e860ff91b8f5
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TileLayer.java
@@ -0,0 +1,177 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import android.graphics.Rect;
+import android.opengl.GLES20;
+import android.util.Log;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Base class for tile layers, which encapsulate the logic needed to draw textured tiles in OpenGL
+ * ES.
+ */
+public abstract class TileLayer extends Layer {
+ private static final String LOGTAG = "GeckoTileLayer";
+
+ private final Rect mDirtyRect;
+ private IntSize mSize;
+ private int[] mTextureIDs;
+
+ protected final CairoImage mImage;
+
+ public enum PaintMode { NORMAL, REPEAT, STRETCH };
+ private PaintMode mPaintMode;
+
+ public TileLayer(CairoImage image, PaintMode paintMode) {
+ super(image.getSize());
+
+ mPaintMode = paintMode;
+ mImage = image;
+ mSize = new IntSize(0, 0);
+ mDirtyRect = new Rect();
+ }
+
+ protected boolean repeats() { return mPaintMode == PaintMode.REPEAT; }
+ protected boolean stretches() { return mPaintMode == PaintMode.STRETCH; }
+ protected int getTextureID() { return mTextureIDs[0]; }
+ protected boolean initialized() { return mImage != null && mTextureIDs != null; }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mTextureIDs != null)
+ TextureReaper.get().add(mTextureIDs);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ public void destroy() {
+ try {
+ if (mImage != null) {
+ mImage.destroy();
+ }
+ } catch (Exception ex) {
+ Log.e(LOGTAG, "error clearing buffers: ", ex);
+ }
+ }
+
+ public void setPaintMode(PaintMode mode) {
+ mPaintMode = mode;
+ }
+
+ /**
+ * Invalidates the entire buffer so that it will be uploaded again. Only valid inside a
+ * transaction.
+ */
+
+ public void invalidate() {
+ if (!inTransaction())
+ throw new RuntimeException("invalidate() is only valid inside a transaction");
+ IntSize bufferSize = mImage.getSize();
+ mDirtyRect.set(0, 0, bufferSize.width, bufferSize.height);
+ }
+
+ private void validateTexture() {
+ /* Calculate the ideal texture size. This must be a power of two if
+ * the texture is repeated or OpenGL ES 2.0 isn't supported, as
+ * OpenGL ES 2.0 is required for NPOT texture support (without
+ * extensions), but doesn't support repeating NPOT textures.
+ *
+ * XXX Currently, we don't pick a GLES 2.0 context, so always round.
+ */
+ IntSize textureSize = mImage.getSize().nextPowerOfTwo();
+
+ if (!textureSize.equals(mSize)) {
+ mSize = textureSize;
+
+ // Delete the old texture
+ if (mTextureIDs != null) {
+ TextureReaper.get().add(mTextureIDs);
+ mTextureIDs = null;
+
+ // Free the texture immediately, so we don't incur a
+ // temporarily increased memory usage.
+ TextureReaper.get().reap();
+ }
+ }
+ }
+
+ @Override
+ protected void performUpdates(RenderContext context) {
+ super.performUpdates(context);
+
+ // Reallocate the texture if the size has changed
+ validateTexture();
+
+ // Don't do any work if the image has an invalid size.
+ if (!mImage.getSize().isPositive())
+ return;
+
+ // If we haven't allocated a texture, assume the whole region is dirty
+ if (mTextureIDs == null) {
+ uploadFullTexture();
+ } else {
+ uploadDirtyRect(mDirtyRect);
+ }
+
+ mDirtyRect.setEmpty();
+ }
+
+ private void uploadFullTexture() {
+ IntSize bufferSize = mImage.getSize();
+ uploadDirtyRect(new Rect(0, 0, bufferSize.width, bufferSize.height));
+ }
+
+ private void uploadDirtyRect(Rect dirtyRect) {
+ // If we have nothing to upload, just return for now
+ if (dirtyRect.isEmpty())
+ return;
+
+ // It's possible that the buffer will be null, check for that and return
+ ByteBuffer imageBuffer = mImage.getBuffer();
+ if (imageBuffer == null)
+ return;
+
+ if (mTextureIDs == null) {
+ mTextureIDs = new int[1];
+ GLES20.glGenTextures(mTextureIDs.length, mTextureIDs, 0);
+ }
+
+ int cairoFormat = mImage.getFormat();
+ CairoGLInfo glInfo = new CairoGLInfo(cairoFormat);
+
+ bindAndSetGLParameters();
+
+ // XXX TexSubImage2D is too broken to rely on on Adreno, and very slow
+ // on other chipsets, so we always upload the entire buffer.
+ IntSize bufferSize = mImage.getSize();
+ if (mSize.equals(bufferSize)) {
+ GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, glInfo.internalFormat, mSize.width,
+ mSize.height, 0, glInfo.format, glInfo.type, imageBuffer);
+ } else {
+ // Our texture has been expanded to the next power of two.
+ // XXX We probably never want to take this path, so throw an exception.
+ throw new RuntimeException("Buffer/image size mismatch in TileLayer!");
+ }
+ }
+
+ private void bindAndSetGLParameters() {
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureIDs[0]);
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
+ GLES20.GL_LINEAR);
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
+ GLES20.GL_LINEAR);
+
+ int repeatMode = repeats() ? GLES20.GL_REPEAT : GLES20.GL_CLAMP_TO_EDGE;
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, repeatMode);
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, repeatMode);
+ }
+}
+
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TouchEventHandler.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TouchEventHandler.java
new file mode 100644
index 000000000000..9710bd41df5f
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/TouchEventHandler.java
@@ -0,0 +1,306 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+//import org.mozilla.gecko.Tab;
+//import org.mozilla.gecko.Tabs;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+/**
+ * This class handles incoming touch events from the user and sends them to
+ * listeners in Gecko and/or performs the "default action" (asynchronous pan/zoom
+ * behaviour. EVERYTHING IN THIS CLASS MUST RUN ON THE UI THREAD.
+ *
+ * In the following code/comments, a "block" of events refers to a contiguous
+ * sequence of events that starts with a DOWN or POINTER_DOWN and goes up to
+ * but not including the next DOWN or POINTER_DOWN event.
+ *
+ * "Dispatching" an event refers to performing the default actions for the event,
+ * which at our level of abstraction just means sending it off to the gesture
+ * detectors and the pan/zoom controller.
+ *
+ * If an event is "default-prevented" that means one or more listeners in Gecko
+ * has called preventDefault() on the event, which means that the default action
+ * for that event should not occur. Usually we care about a "block" of events being
+ * default-prevented, which means that the DOWN/POINTER_DOWN event that started
+ * the block, or the first MOVE event following that, were prevent-defaulted.
+ *
+ * A "default-prevented notification" is when we here in Java-land receive a notification
+ * from gecko as to whether or not a block of events was default-prevented. This happens
+ * at some point after the first or second event in the block is processed in Gecko.
+ * This code assumes we get EXACTLY ONE default-prevented notification for each block
+ * of events.
+ *
+ * Note that even if all events are default-prevented, we still send specific types
+ * of notifications to the pan/zoom controller. The notifications are needed
+ * to respond to user actions a timely manner regardless of default-prevention,
+ * and fix issues like bug 749384.
+ */
+final class TouchEventHandler /*implements Tabs.OnTabsChangedListener*/ {
+ private static final String LOGTAG = "GeckoTouchEventHandler";
+
+ // The time limit for listeners to respond with preventDefault on touchevents
+ // before we begin panning the page
+ private final int EVENT_LISTENER_TIMEOUT = 200;
+
+ private final View mView;
+ private final GestureDetector mGestureDetector;
+ private final SimpleScaleGestureDetector mScaleGestureDetector;
+ private final JavaPanZoomController mPanZoomController;
+
+ // the queue of events that we are holding on to while waiting for a preventDefault
+ // notification
+ private final Queue<MotionEvent> mEventQueue;
+ private final ListenerTimeoutProcessor mListenerTimeoutProcessor;
+
+ // whether or not we should wait for touch listeners to respond (this state is
+ // per-tab and is updated when we switch tabs).
+ private boolean mWaitForTouchListeners;
+
+ // true if we should hold incoming events in our queue. this is re-set for every
+ // block of events, this is cleared once we find out if the block has been
+ // default-prevented or not (or we time out waiting for that).
+ private boolean mHoldInQueue;
+
+ // false if the current event block has been default-prevented. In this case,
+ // we still pass the event to both Gecko and the pan/zoom controller, but the
+ // latter will not use it to scroll content. It may still use the events for
+ // other things, such as making the dynamic toolbar visible.
+ private boolean mAllowDefaultAction;
+
+ // this next variable requires some explanation. strap yourself in.
+ //
+ // for each block of events, we do two things: (1) send the events to gecko and expect
+ // exactly one default-prevented notification in return, and (2) kick off a delayed
+ // ListenerTimeoutProcessor that triggers in case we don't hear from the listener in
+ // a timely fashion.
+ // since events are constantly coming in, we need to be able to handle more than one
+ // block of events in the queue.
+ //
+ // this means that there are ordering restrictions on these that we can take advantage of,
+ // and need to abide by. blocks of events in the queue will always be in the order that
+ // the user generated them. default-prevented notifications we get from gecko will be in
+ // the same order as the blocks of events in the queue. the ListenerTimeoutProcessors that
+ // have been posted will also fire in the same order as the blocks of events in the queue.
+ // HOWEVER, we may get multiple default-prevented notifications interleaved with multiple
+ // ListenerTimeoutProcessor firings, and that interleaving is not predictable.
+ //
+ // therefore, we need to make sure that for each block of events, we process the queued
+ // events exactly once, either when we get the default-prevented notification, or when the
+ // timeout expires (whichever happens first). there is no way to associate the
+ // default-prevented notification with a particular block of events other than via ordering,
+ //
+ // so what we do to accomplish this is to track a "processing balance", which is the number
+ // of default-prevented notifications that we have received, minus the number of ListenerTimeoutProcessors
+ // that have fired. (think "balance" as in teeter-totter balance). this value is:
+ // - zero when we are in a state where the next default-prevented notification we expect
+ // to receive and the next ListenerTimeoutProcessor we expect to fire both correspond to
+ // the next block of events in the queue.
+ // - positive when we are in a state where we have received more default-prevented notifications
+ // than ListenerTimeoutProcessors. This means that the next default-prevented notification
+ // does correspond to the block at the head of the queue, but the next n ListenerTimeoutProcessors
+ // need to be ignored as they are for blocks we have already processed. (n is the absolute value
+ // of the balance.)
+ // - negative when we are in a state where we have received more ListenerTimeoutProcessors than
+ // default-prevented notifications. This means that the next ListenerTimeoutProcessor that
+ // we receive does correspond to the block at the head of the queue, but the next n
+ // default-prevented notifications need to be ignored as they are for blocks we have already
+ // processed. (n is the absolute value of the balance.)
+ private int mProcessingBalance;
+
+ TouchEventHandler(Context context, View view, JavaPanZoomController panZoomController) {
+ mView = view;
+
+ mEventQueue = new LinkedList<MotionEvent>();
+ mPanZoomController = panZoomController;
+ mGestureDetector = new GestureDetector(context, mPanZoomController);
+ mScaleGestureDetector = new SimpleScaleGestureDetector(mPanZoomController);
+ mListenerTimeoutProcessor = new ListenerTimeoutProcessor();
+ mAllowDefaultAction = true;
+
+ mGestureDetector.setOnDoubleTapListener(mPanZoomController);
+
+ //Tabs.registerOnTabsChangedListener(this);
+ }
+
+ public void destroy() {
+ //Tabs.unregisterOnTabsChangedListener(this);
+ }
+
+ /* This function MUST be called on the UI thread */
+ public boolean handleEvent(MotionEvent event) {
+ if (isDownEvent(event)) {
+ // this is the start of a new block of events! whee!
+ mHoldInQueue = mWaitForTouchListeners;
+
+ // Set mAllowDefaultAction to true so that in the event we dispatch events, the
+ // PanZoomController doesn't treat them as if they've been prevent-defaulted
+ // when they haven't.
+ mAllowDefaultAction = true;
+ if (mHoldInQueue) {
+ // if the new block we are starting is the current block (i.e. there are no
+ // other blocks waiting in the queue, then we should let the pan/zoom controller
+ // know we are waiting for the touch listeners to run
+ if (mEventQueue.isEmpty()) {
+ mPanZoomController.startingNewEventBlock(event, true);
+ }
+ } else {
+ // we're not going to be holding this block of events in the queue, but we need
+ // a marker of some sort so that the processEventBlock loop deals with the blocks
+ // in the right order as notifications come in. we use a single null event in
+ // the queue as a placeholder for a block of events that has already been dispatched.
+ mEventQueue.add(null);
+ mPanZoomController.startingNewEventBlock(event, false);
+ }
+
+ // set the timeout so that we dispatch these events and update mProcessingBalance
+ // if we don't get a default-prevented notification
+ mView.postDelayed(mListenerTimeoutProcessor, EVENT_LISTENER_TIMEOUT);
+ }
+
+ // if we need to hold the events, add it to the queue, otherwise dispatch
+ // it directly.
+ if (mHoldInQueue) {
+ mEventQueue.add(MotionEvent.obtain(event));
+ } else {
+ dispatchEvent(event, mAllowDefaultAction);
+ }
+
+ return false;
+ }
+
+ /**
+ * This function is how gecko sends us a default-prevented notification. It is called
+ * once gecko knows definitively whether the block of events has had preventDefault
+ * called on it (either on the initial down event that starts the block, or on
+ * the first event following that down event).
+ *
+ * This function MUST be called on the UI thread.
+ */
+ public void handleEventListenerAction(boolean allowDefaultAction) {
+ if (mProcessingBalance > 0) {
+ // this event listener that triggered this took too long, and the corresponding
+ // ListenerTimeoutProcessor runnable already ran for the event in question. the
+ // block of events this is for has already been processed, so we don't need to
+ // do anything here.
+ } else {
+ processEventBlock(allowDefaultAction);
+ }
+ mProcessingBalance--;
+ }
+
+ /* This function MUST be called on the UI thread. */
+ public void setWaitForTouchListeners(boolean aValue) {
+ mWaitForTouchListeners = aValue;
+ }
+
+ private boolean isDownEvent(MotionEvent event) {
+ int action = (event.getAction() & MotionEvent.ACTION_MASK);
+ return (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN);
+ }
+
+ private boolean touchFinished(MotionEvent event) {
+ int action = (event.getAction() & MotionEvent.ACTION_MASK);
+ return (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL);
+ }
+
+ /**
+ * Dispatch the event to the gesture detectors and the pan/zoom controller.
+ */
+ private void dispatchEvent(MotionEvent event, boolean allowDefaultAction) {
+ if (allowDefaultAction) {
+ if (mGestureDetector.onTouchEvent(event)) {
+ return;
+ }
+ mScaleGestureDetector.onTouchEvent(event);
+ if (mScaleGestureDetector.isInProgress()) {
+ return;
+ }
+ }
+ mPanZoomController.handleEvent(event, !allowDefaultAction);
+ }
+
+ /**
+ * Process the block of events at the head of the queue now that we know
+ * whether it has been default-prevented or not.
+ */
+ private void processEventBlock(boolean allowDefaultAction) {
+ if (mEventQueue.isEmpty()) {
+ Log.e(LOGTAG, "Unexpected empty event queue in processEventBlock!", new Exception());
+ return;
+ }
+
+ // the odd loop condition is because the first event in the queue will
+ // always be a DOWN or POINTER_DOWN event, and we want to process all
+ // the events in the queue starting at that one, up to but not including
+ // the next DOWN or POINTER_DOWN event.
+
+ MotionEvent event = mEventQueue.poll();
+ while (true) {
+ // event being null here is valid and represents a block of events
+ // that has already been dispatched.
+
+ if (event != null) {
+ dispatchEvent(event, allowDefaultAction);
+ }
+ if (mEventQueue.isEmpty()) {
+ // we have processed the backlog of events, and are all caught up.
+ // now we can set clear the hold flag and set the dispatch flag so
+ // that the handleEvent() function can do the right thing for all
+ // remaining events in this block (which is still ongoing) without
+ // having to put them in the queue.
+ mHoldInQueue = false;
+ mAllowDefaultAction = allowDefaultAction;
+ break;
+ }
+ event = mEventQueue.peek();
+ if (event == null || isDownEvent(event)) {
+ // we have finished processing the block we were interested in.
+ // now we wait for the next call to processEventBlock
+ if (event != null) {
+ mPanZoomController.startingNewEventBlock(event, true);
+ }
+ break;
+ }
+ // pop the event we peeked above, as it is still part of the block and
+ // we want to keep processing
+ mEventQueue.remove();
+ }
+ }
+
+ private class ListenerTimeoutProcessor implements Runnable {
+ /* This MUST be run on the UI thread */
+ @Override
+ public void run() {
+ if (mProcessingBalance < 0) {
+ // gecko already responded with default-prevented notification, and so
+ // the block of events this ListenerTimeoutProcessor corresponds to have
+ // already been removed from the queue.
+ } else {
+ processEventBlock(true);
+ }
+ mProcessingBalance++;
+ }
+ }
+
+ // Tabs.OnTabsChangedListener implementation
+
+ /*@Override
+ public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
+ if ((Tabs.getInstance().isSelectedTab(tab) && msg == Tabs.TabEvents.STOP) || msg == Tabs.TabEvents.SELECTED) {
+ mWaitForTouchListeners = tab.getHasTouchListeners();
+ }
+ }*/
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/ViewTransform.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/ViewTransform.java
new file mode 100644
index 000000000000..97ca109f118d
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/ViewTransform.java
@@ -0,0 +1,34 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+//import org.mozilla.gecko.mozglue.generatorannotations.WrapEntireClassForJNI;
+
+//@WrapEntireClassForJNI
+public class ViewTransform {
+ public float x;
+ public float y;
+ public float scale;
+ public float fixedLayerMarginLeft;
+ public float fixedLayerMarginTop;
+ public float fixedLayerMarginRight;
+ public float fixedLayerMarginBottom;
+ public float offsetX;
+ public float offsetY;
+
+ public ViewTransform(float inX, float inY, float inScale) {
+ x = inX;
+ y = inY;
+ scale = inScale;
+ fixedLayerMarginLeft = 0;
+ fixedLayerMarginTop = 0;
+ fixedLayerMarginRight = 0;
+ fixedLayerMarginBottom = 0;
+ offsetX = 0;
+ offsetY = 0;
+ }
+}
+
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/VirtualLayer.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/VirtualLayer.java
new file mode 100644
index 000000000000..83d012876176
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/VirtualLayer.java
@@ -0,0 +1,36 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+public class VirtualLayer extends Layer {
+ public VirtualLayer(IntSize size) {
+ super(size);
+ }
+
+ @Override
+ public void draw(RenderContext context) {
+ // No-op.
+ }
+
+ void setPositionAndResolution(int left, int top, int right, int bottom, float newResolution) {
+ // This is an optimized version of the following code:
+ // beginTransaction();
+ // try {
+ // setPosition(new Rect(left, top, right, bottom));
+ // setResolution(newResolution);
+ // performUpdates(null);
+ // } finally {
+ // endTransaction();
+ // }
+
+ // it is safe to drop the transaction lock in this instance (i.e. for the
+ // VirtualLayer that is just a shadow of what gecko is painting) because
+ // the position and resolution of this layer are always touched on the compositor
+ // thread, and therefore do not require synchronization.
+ mPosition.set(left, top, right, bottom);
+ mResolution = newResolution;
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/mozglue/DirectBufferAllocator.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/mozglue/DirectBufferAllocator.java
new file mode 100644
index 000000000000..61444e6cfc47
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/mozglue/DirectBufferAllocator.java
@@ -0,0 +1,51 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.mozglue;
+
+import java.nio.ByteBuffer;
+
+//
+// We must manually allocate direct buffers in JNI to work around a bug where Honeycomb's
+// ByteBuffer.allocateDirect() grossly overallocates the direct buffer size.
+// https://code.google.com/p/android/issues/detail?id=16941
+//
+
+public final class DirectBufferAllocator {
+ private DirectBufferAllocator() {}
+
+ public static ByteBuffer allocate(int size) {
+ if (size <= 0) {
+ throw new IllegalArgumentException("Invalid size " + size);
+ }
+
+ ByteBuffer directBuffer = ByteBuffer.allocateDirect(size);
+ //ByteBuffer directBuffer = nativeAllocateDirectBuffer(size);
+ if (directBuffer == null) {
+ throw new OutOfMemoryError("allocateDirectBuffer() returned null");
+ } else if (!directBuffer.isDirect()) {
+ throw new AssertionError("allocateDirectBuffer() did not return a direct buffer");
+ }
+
+ return directBuffer;
+ }
+
+ public static ByteBuffer free(ByteBuffer buffer) {
+ if (buffer == null) {
+ return null;
+ }
+
+ if (!buffer.isDirect()) {
+ throw new IllegalArgumentException("buffer must be direct");
+ }
+
+ //nativeFreeDirectBuffer(buffer);
+ return null;
+ }
+
+ // These JNI methods are implemented in mozglue/android/nsGeckoUtils.cpp.
+ //private static native ByteBuffer nativeAllocateDirectBuffer(long size);
+ //private static native void nativeFreeDirectBuffer(ByteBuffer buf);
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/EventDispatcher.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/EventDispatcher.java
new file mode 100644
index 000000000000..5b6d50883b42
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/EventDispatcher.java
@@ -0,0 +1,115 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import org.json.JSONObject;
+
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public final class EventDispatcher {
+ private static final String LOGTAG = "GeckoEventDispatcher";
+
+ private final Map<String, CopyOnWriteArrayList<GeckoEventListener>> mEventListeners
+ = new HashMap<String, CopyOnWriteArrayList<GeckoEventListener>>();
+
+ public void registerEventListener(String event, GeckoEventListener listener) {
+ synchronized (mEventListeners) {
+ CopyOnWriteArrayList<GeckoEventListener> listeners = mEventListeners.get(event);
+ if (listeners == null) {
+ // create a CopyOnWriteArrayList so that we can modify it
+ // concurrently with iterating through it in handleGeckoMessage.
+ // Otherwise we could end up throwing a ConcurrentModificationException.
+ listeners = new CopyOnWriteArrayList<GeckoEventListener>();
+ } else if (listeners.contains(listener)) {
+ Log.w(LOGTAG, "EventListener already registered for event '" + event + "'",
+ new IllegalArgumentException());
+ }
+ listeners.add(listener);
+ mEventListeners.put(event, listeners);
+ }
+ }
+
+ public void unregisterEventListener(String event, GeckoEventListener listener) {
+ synchronized (mEventListeners) {
+ CopyOnWriteArrayList<GeckoEventListener> listeners = mEventListeners.get(event);
+ if (listeners == null) {
+ Log.w(LOGTAG, "unregisterEventListener: event '" + event + "' has no listeners");
+ return;
+ }
+ if (!listeners.remove(listener)) {
+ Log.w(LOGTAG, "unregisterEventListener: tried to remove an unregistered listener " +
+ "for event '" + event + "'");
+ }
+ if (listeners.size() == 0) {
+ mEventListeners.remove(event);
+ }
+ }
+ }
+
+ public String dispatchEvent(String message) {
+ try {
+ JSONObject json = new JSONObject(message);
+ return dispatchEvent(json);
+ } catch (Exception e) {
+ Log.e(LOGTAG, "dispatchEvent: malformed JSON.", e);
+ }
+
+ return "";
+ }
+
+ public String dispatchEvent(JSONObject json) {
+ // {
+ // "type": "value",
+ // "event_specific": "value",
+ // ...
+ try {
+ JSONObject gecko = json.has("gecko") ? json.getJSONObject("gecko") : null;
+ if (gecko != null) {
+ json = gecko;
+ }
+
+ String type = json.getString("type");
+
+ if (gecko != null) {
+ Log.w(LOGTAG, "Message '" + type + "' has deprecated 'gecko' property!");
+ }
+
+ CopyOnWriteArrayList<GeckoEventListener> listeners;
+ synchronized (mEventListeners) {
+ listeners = mEventListeners.get(type);
+ }
+
+ if (listeners == null || listeners.size() == 0) {
+ Log.d(LOGTAG, "dispatchEvent: no listeners registered for event '" + type + "'");
+ return "";
+ }
+
+ String response = null;
+
+ for (GeckoEventListener listener : listeners) {
+ listener.handleMessage(type, json);
+ if (listener instanceof GeckoEventResponder) {
+ String newResponse = ((GeckoEventResponder)listener).getResponse(json);
+ if (response != null && newResponse != null) {
+ Log.e(LOGTAG, "Received two responses for message of type " + type);
+ }
+ response = newResponse;
+ }
+ }
+
+ if (response != null)
+ return response;
+
+ } catch (Exception e) {
+ Log.e(LOGTAG, "handleGeckoMessage throws " + e, e);
+ }
+
+ return "";
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/FloatUtils.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/FloatUtils.java
new file mode 100644
index 000000000000..fbcd7254f62b
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/FloatUtils.java
@@ -0,0 +1,43 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import android.graphics.PointF;
+
+import java.lang.IllegalArgumentException;
+
+public final class FloatUtils {
+ private FloatUtils() {}
+
+ public static boolean fuzzyEquals(float a, float b) {
+ return (Math.abs(a - b) < 1e-6);
+ }
+
+ public static boolean fuzzyEquals(PointF a, PointF b) {
+ return fuzzyEquals(a.x, b.x) && fuzzyEquals(a.y, b.y);
+ }
+
+ /*
+ * Returns the value that represents a linear transition between `from` and `to` at time `t`,
+ * which is on the scale [0, 1). Thus with t = 0.0f, this returns `from`; with t = 1.0f, this
+ * returns `to`; with t = 0.5f, this returns the value halfway from `from` to `to`.
+ */
+ public static float interpolate(float from, float to, float t) {
+ return from + (to - from) * t;
+ }
+
+ /**
+ * Returns 'value', clamped so that it isn't any lower than 'low', and it
+ * isn't any higher than 'high'.
+ */
+ public static float clamp(float value, float low, float high) {
+ if (high < low) {
+ throw new IllegalArgumentException(
+ "clamp called with invalid parameters (" + high + " < " + low + ")" );
+ }
+ return Math.max(low, Math.min(high, value));
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java
new file mode 100644
index 000000000000..f7873fe733d3
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.concurrent.SynchronousQueue;
+
+final class GeckoBackgroundThread extends Thread {
+ private static final String LOOPER_NAME = "GeckoBackgroundThread";
+
+ // Guarded by 'this'.
+ private static Handler sHandler = null;
+ private SynchronousQueue<Handler> mHandlerQueue = new SynchronousQueue<Handler>();
+
+ // Singleton, so private constructor.
+ private GeckoBackgroundThread() {
+ super();
+ }
+
+ @Override
+ public void run() {
+ setName(LOOPER_NAME);
+ Looper.prepare();
+ try {
+ mHandlerQueue.put(new Handler());
+ } catch (InterruptedException ie) {}
+
+ Looper.loop();
+ }
+
+ // Get a Handler for a looper thread, or create one if it doesn't yet exist.
+ /*package*/ static synchronized Handler getHandler() {
+ if (sHandler == null) {
+ GeckoBackgroundThread lt = new GeckoBackgroundThread();
+ ThreadUtils.setBackgroundThread(lt);
+ lt.start();
+ try {
+ sHandler = lt.mHandlerQueue.take();
+ } catch (InterruptedException ie) {}
+ }
+ return sHandler;
+ }
+
+ /*package*/ static void post(Runnable runnable) {
+ Handler handler = getHandler();
+ if (handler == null) {
+ throw new IllegalStateException("No handler! Must have been interrupted. Not posting.");
+ }
+ handler.post(runnable);
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/GeckoEventListener.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/GeckoEventListener.java
new file mode 100644
index 000000000000..4d0c313b0c6a
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/GeckoEventListener.java
@@ -0,0 +1,14 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import org.json.JSONObject;
+//import org.mozilla.gecko.mozglue.RobocopTarget;
+
+//@RobocopTarget
+public interface GeckoEventListener {
+ void handleMessage(String event, JSONObject message);
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/GeckoEventResponder.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/GeckoEventResponder.java
new file mode 100644
index 000000000000..dc4561561c8a
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/GeckoEventResponder.java
@@ -0,0 +1,16 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+package org.mozilla.gecko.util;
+
+import org.json.JSONObject;
+
+public interface GeckoEventResponder extends GeckoEventListener {
+ String getResponse(JSONObject response);
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/ThreadUtils.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/ThreadUtils.java
new file mode 100644
index 000000000000..a646f1ae4e9d
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/ThreadUtils.java
@@ -0,0 +1,169 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import android.os.Handler;
+import android.os.MessageQueue;
+import android.util.Log;
+
+import java.util.Map;
+
+public final class ThreadUtils {
+ private static final String LOGTAG = "ThreadUtils";
+
+ private static Thread sUiThread;
+ private static Thread sBackgroundThread;
+
+ private static Handler sUiHandler;
+
+ // Referenced directly from GeckoAppShell in highly performance-sensitive code (The extra
+ // function call of the getter was harming performance. (Bug 897123))
+ // Once Bug 709230 is resolved we should reconsider this as ProGuard should be able to optimise
+ // this out at compile time.
+ public static Handler sGeckoHandler;
+ public static MessageQueue sGeckoQueue;
+ public static Thread sGeckoThread;
+
+ // Delayed Runnable that resets the Gecko thread priority.
+ private static final Runnable sPriorityResetRunnable = new Runnable() {
+ @Override
+ public void run() {
+ resetGeckoPriority();
+ }
+ };
+
+ private static boolean sIsGeckoPriorityReduced;
+
+ @SuppressWarnings("serial")
+ public static class UiThreadBlockedException extends RuntimeException {
+ public UiThreadBlockedException() {
+ super();
+ }
+
+ public UiThreadBlockedException(String msg) {
+ super(msg);
+ }
+
+ public UiThreadBlockedException(String msg, Throwable e) {
+ super(msg, e);
+ }
+
+ public UiThreadBlockedException(Throwable e) {
+ super(e);
+ }
+ }
+
+ public static void dumpAllStackTraces() {
+ Log.w(LOGTAG, "Dumping ALL the threads!");
+ Map<Thread, StackTraceElement[]> allStacks = Thread.getAllStackTraces();
+ for (Thread t : allStacks.keySet()) {
+ Log.w(LOGTAG, t.toString());
+ for (StackTraceElement ste : allStacks.get(t)) {
+ Log.w(LOGTAG, ste.toString());
+ }
+ Log.w(LOGTAG, "----");
+ }
+ }
+
+ public static void setUiThread(Thread thread, Handler handler) {
+ sUiThread = thread;
+ sUiHandler = handler;
+ }
+
+ public static void setBackgroundThread(Thread thread) {
+ sBackgroundThread = thread;
+ }
+
+ public static Thread getUiThread() {
+ return sUiThread;
+ }
+
+ public static Handler getUiHandler() {
+ return sUiHandler;
+ }
+
+ public static void postToUiThread(Runnable runnable) {
+ sUiHandler.post(runnable);
+ }
+
+ public static Thread getBackgroundThread() {
+ return sBackgroundThread;
+ }
+
+ public static Handler getBackgroundHandler() {
+ return GeckoBackgroundThread.getHandler();
+ }
+
+ public static void postToBackgroundThread(Runnable runnable) {
+ GeckoBackgroundThread.post(runnable);
+ }
+
+ public static void assertOnUiThread() {
+ assertOnThread(getUiThread());
+ }
+
+ public static void assertOnGeckoThread() {
+ assertOnThread(sGeckoThread);
+ }
+
+ public static void assertOnBackgroundThread() {
+ assertOnThread(getBackgroundThread());
+ }
+
+ public static void assertOnThread(Thread expectedThread) {
+ Thread currentThread = Thread.currentThread();
+ long currentThreadId = currentThread.getId();
+ long expectedThreadId = expectedThread.getId();
+
+ if (currentThreadId != expectedThreadId) {
+ throw new IllegalThreadStateException("Expected thread " + expectedThreadId + " (\""
+ + expectedThread.getName()
+ + "\"), but running on thread " + currentThreadId
+ + " (\"" + currentThread.getName() + ")");
+ }
+ }
+
+ public static boolean isOnUiThread() {
+ return isOnThread(getUiThread());
+ }
+
+ public static boolean isOnBackgroundThread() {
+ return isOnThread(sBackgroundThread);
+ }
+
+ public static boolean isOnThread(Thread thread) {
+ return (Thread.currentThread().getId() == thread.getId());
+ }
+
+ /**
+ * Reduces the priority of the Gecko thread, allowing other operations
+ * (such as those related to the UI and database) to take precedence.
+ *
+ * Note that there are no guards in place to prevent multiple calls
+ * to this method from conflicting with each other.
+ *
+ * @param timeout Timeout in ms after which the priority will be reset
+ */
+ public static void reduceGeckoPriority(long timeout) {
+ if (!sIsGeckoPriorityReduced) {
+ sIsGeckoPriorityReduced = true;
+ sGeckoThread.setPriority(Thread.MIN_PRIORITY);
+ getUiHandler().postDelayed(sPriorityResetRunnable, timeout);
+ }
+ }
+
+ /**
+ * Resets the priority of a thread whose priority has been reduced
+ * by reduceGeckoPriority.
+ */
+ public static void resetGeckoPriority() {
+ if (sIsGeckoPriorityReduced) {
+ sIsGeckoPriorityReduced = false;
+ sGeckoThread.setPriority(Thread.NORM_PRIORITY);
+ getUiHandler().removeCallbacks(sPriorityResetRunnable);
+ }
+ }
+}
diff --git a/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/UiAsyncTask.java b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/UiAsyncTask.java
new file mode 100644
index 000000000000..aee875c33dc9
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/util/UiAsyncTask.java
@@ -0,0 +1,86 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import android.os.Handler;
+import android.os.Looper;
+
+/**
+ * Executes a background task and publishes the result on the UI thread.
+ *
+ * The standard {@link android.os.AsyncTask} only runs onPostExecute on the
+ * thread it is constructed on, so this is a convenience class for creating
+ * tasks off the UI thread.
+ */
+public abstract class UiAsyncTask<Params, Progress, Result> {
+ private volatile boolean mCancelled = false;
+ private final Handler mBackgroundThreadHandler;
+ private static Handler sHandler;
+
+ /**
+ * Creates a new asynchronous task.
+ *
+ * @param backgroundThreadHandler the handler to execute the background task on
+ */
+ public UiAsyncTask(Handler backgroundThreadHandler) {
+ mBackgroundThreadHandler = backgroundThreadHandler;
+ }
+
+ private static synchronized Handler getUiHandler() {
+ if (sHandler == null) {
+ sHandler = new Handler(Looper.getMainLooper());
+ }
+ return sHandler;
+ }
+
+ private final class BackgroundTaskRunnable implements Runnable {
+ private Params[] mParams;
+
+ public BackgroundTaskRunnable(Params... params) {
+ mParams = params;
+ }
+
+ @Override
+ public void run() {
+ final Result result = doInBackground(mParams);
+
+ getUiHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ if (mCancelled)
+ onCancelled();
+ else
+ onPostExecute(result);
+ }
+ });
+ }
+ }
+
+ public final void execute(final Params... params) {
+ getUiHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ onPreExecute();
+ mBackgroundThreadHandler.post(new BackgroundTaskRunnable(params));
+ }
+ });
+ }
+
+ @SuppressWarnings({"UnusedParameters"})
+ public final boolean cancel(boolean mayInterruptIfRunning) {
+ mCancelled = true;
+ return mCancelled;
+ }
+
+ public final boolean isCancelled() {
+ return mCancelled;
+ }
+
+ protected void onPreExecute() { }
+ protected void onPostExecute(Result result) { }
+ protected void onCancelled() { }
+ protected abstract Result doInBackground(Params... params);
+}
diff --git a/android/experimental/LOAndroid/app/src/main/res/drawable-hdpi/ic_launcher.png b/android/experimental/LOAndroid/app/src/main/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 000000000000..96a442e5b8e9
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/android/experimental/LOAndroid/app/src/main/res/drawable-hdpi/ic_status_logo.png b/android/experimental/LOAndroid/app/src/main/res/drawable-hdpi/ic_status_logo.png
new file mode 100644
index 000000000000..d5f16694f342
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/res/drawable-hdpi/ic_status_logo.png
Binary files differ
diff --git a/android/experimental/LOAndroid/app/src/main/res/drawable-mdpi/ic_launcher.png b/android/experimental/LOAndroid/app/src/main/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 000000000000..359047dfa4ed
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/android/experimental/LOAndroid/app/src/main/res/drawable-mdpi/ic_status_logo.png b/android/experimental/LOAndroid/app/src/main/res/drawable-mdpi/ic_status_logo.png
new file mode 100644
index 000000000000..835fc9290727
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/res/drawable-mdpi/ic_status_logo.png
Binary files differ
diff --git a/android/experimental/LOAndroid/app/src/main/res/drawable-xhdpi/ic_launcher.png b/android/experimental/LOAndroid/app/src/main/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 000000000000..71c6d760f051
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/android/experimental/LOAndroid/app/src/main/res/drawable-xhdpi/ic_status_logo.png b/android/experimental/LOAndroid/app/src/main/res/drawable-xhdpi/ic_status_logo.png
new file mode 100644
index 000000000000..c8005425416a
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/res/drawable-xhdpi/ic_status_logo.png
Binary files differ
diff --git a/android/experimental/LOAndroid/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/android/experimental/LOAndroid/app/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000000..4df18946442e
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/android/experimental/LOAndroid/app/src/main/res/layout/activity_main.xml b/android/experimental/LOAndroid/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 000000000000..600acdd4b40b
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,15 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ tools:context="org.libreoffice.MainActivity">
+
+ <org.libreoffice.MainLayerView android:id="@+id/layer_view"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"/>
+
+</RelativeLayout>
diff --git a/android/experimental/LOAndroid/app/src/main/res/menu/main.xml b/android/experimental/LOAndroid/app/src/main/res/menu/main.xml
new file mode 100644
index 000000000000..6768fd32a890
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/res/menu/main.xml
@@ -0,0 +1,9 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:context="org.libreoffice.MainActivity" >
+ <item android:id="@+id/action_settings"
+ android:title="@string/action_settings"
+ android:orderInCategory="100"
+ app:showAsAction="never" />
+</menu>
diff --git a/android/experimental/LOAndroid/app/src/main/res/values-w820dp/dimens.xml b/android/experimental/LOAndroid/app/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 000000000000..63fc81644461
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+<resources>
+ <!-- Example customization of dimensions originally defined in res/values/dimens.xml
+ (such as screen margins) for screens with more than 820dp of available width. This
+ would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
+ <dimen name="activity_horizontal_margin">64dp</dimen>
+</resources>
diff --git a/android/experimental/LOAndroid/app/src/main/res/values/colors.xml b/android/experimental/LOAndroid/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000000..f8e207d4e00e
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/res/values/colors.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+ <color name="background_light">#FFECF0F3</color>
+ <color name="background_normal">#FFCED7DE</color>
+ <color name="background_private">#FF292C29</color>
+ <color name="background_tabs">#FF363B40</color>
+ <color name="highlight">#33000000</color>
+ <color name="highlight_focused">#1A000000</color>
+ <color name="highlight_dark">#33FFFFFF</color>
+ <color name="highlight_dark_focused">#1AFFFFFF</color>
+
+ <!-- highlight on shaped button: 20% white over background_tabs -->
+ <color name="highlight_shaped">#FF696D71</color>
+
+ <!-- highlight-focused on shaped button: 10% white over background_tabs -->
+ <color name="highlight_shaped_focused">#FF565B60</color>
+
+ <!-- highlight on nav button: 20% black over background_normal -->
+ <color name="highlight_nav">#FFA5ACB2</color>
+
+ <!-- highlight-focused on nav button: 10% black over background_normal -->
+ <color name="highlight_nav_focused">#FFB9C1C7</color>
+
+ <!-- highlight on private nav button: 20% white over background_private -->
+ <color name="highlight_nav_pb">#FF545654</color>
+
+ <!-- highlight-focused on private nav button: 10% white over background_private -->
+ <color name="highlight_nav_focused_pb">#FF3F423F</color>
+
+ <!--
+ Application theme colors
+ -->
+ <!-- Default colors -->
+ <color name="text_color_primary">#222222</color>
+ <color name="text_color_secondary">#777777</color>
+ <color name="text_color_tertiary">#9198A1</color>
+
+ <!-- Default inverse colors -->
+ <color name="text_color_primary_inverse">#FFFFFF</color>
+ <color name="text_color_secondary_inverse">#DDDDDD</color>
+ <color name="text_color_tertiary_inverse">#A4A7A9</color>
+
+ <!-- Disabled colors -->
+ <color name="text_color_primary_disable_only">#999999</color>
+
+ <!-- Hint colors -->
+ <color name="text_color_hint">#666666</color>
+ <color name="text_color_hint_inverse">#7F828A</color>
+
+ <!-- Highlight colors -->
+ <color name="text_color_highlight">#FF9500</color>
+ <color name="text_color_highlight_inverse">#D06BFF</color>
+
+ <!-- Link colors -->
+ <color name="text_color_link">#22629E</color>
+
+ <color name="splash_background">#000000</color>
+ <color name="splash_msgfont">#ffffff</color>
+ <color name="splash_urlfont">#000000</color>
+ <color name="splash_content">#ffffff</color>
+
+ <color name="doorhanger_text">#FF222222</color>
+ <color name="doorhanger_link">#FF2AA1FE</color>
+ <color name="doorhanger_divider_light">#FFD1D5DA</color>
+ <color name="doorhanger_divider_dark">#FFB3C2CE</color>
+ <color name="doorhanger_background_dark">#FFDDE4EA</color>
+
+ <color name="validation_message_text">#ffffff</color>
+ <color name="url_bar_text_highlight">#FFFF9500</color>
+ <color name="url_bar_text_highlight_pb">#FFD06BFF</color>
+ <color name="suggestion_primary">#dddddd</color>
+ <color name="suggestion_pressed">#bbbbbb</color>
+ <color name="tab_row_pressed">#4D000000</color>
+ <color name="dialogtitle_textcolor">#ffffff</color>
+
+ <color name="textbox_background">#FFF</color>
+ <color name="textbox_background_disabled">#DDD</color>
+ <color name="textbox_stroke">#000</color>
+ <color name="textbox_stroke_disabled">#666</color>
+
+ <color name="url_bar_urltext">#A6A6A6</color>
+ <color name="url_bar_domaintext">#000</color>
+ <color name="url_bar_domaintext_private">#FFF</color>
+ <color name="url_bar_blockedtext">#b14646</color>
+ <color name="url_bar_shadow">#12000000</color>
+
+ <color name="home_last_tab_bar_bg">#FFF5F7F9</color>
+
+ <color name="panel_grid_item_image_background">#D1D9E1</color>
+</resources>
+
diff --git a/android/experimental/LOAndroid/app/src/main/res/values/dimens.xml b/android/experimental/LOAndroid/app/src/main/res/values/dimens.xml
new file mode 100644
index 000000000000..47c82246738c
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/res/values/dimens.xml
@@ -0,0 +1,5 @@
+<resources>
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+</resources>
diff --git a/android/experimental/LOAndroid/app/src/main/res/values/strings.xml b/android/experimental/LOAndroid/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000000..8864167560f8
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">LOAndroid</string>
+ <string name="hello_world">Hello world!</string>
+ <string name="action_settings">Settings</string>
+
+</resources>
diff --git a/android/experimental/LOAndroid/app/src/main/res/values/styles.xml b/android/experimental/LOAndroid/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000000..ff6c9d2c0fb9
--- /dev/null
+++ b/android/experimental/LOAndroid/app/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
+ <!-- Customize your theme here. -->
+ </style>
+
+</resources>
diff --git a/android/experimental/LOAndroid/build.gradle b/android/experimental/LOAndroid/build.gradle
new file mode 100644
index 000000000000..80eec1a79307
--- /dev/null
+++ b/android/experimental/LOAndroid/build.gradle
@@ -0,0 +1,16 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:0.9.+'
+ }
+}
+
+allprojects {
+ repositories {
+ mavenCentral()
+ }
+}
diff --git a/android/experimental/LOAndroid/gradle.properties b/android/experimental/LOAndroid/gradle.properties
new file mode 100644
index 000000000000..5d08ba75bb97
--- /dev/null
+++ b/android/experimental/LOAndroid/gradle.properties
@@ -0,0 +1,18 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Settings specified in this file will override any Gradle settings
+# configured through the IDE.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true \ No newline at end of file
diff --git a/android/experimental/LOAndroid/gradle/wrapper/gradle-wrapper.jar b/android/experimental/LOAndroid/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000000..8c0fb64a8698
--- /dev/null
+++ b/android/experimental/LOAndroid/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/android/experimental/LOAndroid/gradle/wrapper/gradle-wrapper.properties b/android/experimental/LOAndroid/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000000..5de946b072f1
--- /dev/null
+++ b/android/experimental/LOAndroid/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Apr 10 15:27:10 PDT 2013
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=http\://services.gradle.org/distributions/gradle-1.10-all.zip
diff --git a/android/experimental/LOAndroid/gradlew b/android/experimental/LOAndroid/gradlew
new file mode 100644
index 000000000000..91a7e269e19d
--- /dev/null
+++ b/android/experimental/LOAndroid/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/android/experimental/LOAndroid/gradlew.bat b/android/experimental/LOAndroid/gradlew.bat
new file mode 100644
index 000000000000..aec99730b4e8
--- /dev/null
+++ b/android/experimental/LOAndroid/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/android/experimental/LOAndroid/settings.gradle b/android/experimental/LOAndroid/settings.gradle
new file mode 100644
index 000000000000..e7b4def49cb5
--- /dev/null
+++ b/android/experimental/LOAndroid/settings.gradle
@@ -0,0 +1 @@
+include ':app'