diff options
author | Tomaž Vajngerl <tomaz.vajngerl@collabora.com> | 2014-06-03 15:23:39 +0200 |
---|---|---|
committer | Jan Holesovsky <kendy@collabora.com> | 2014-06-30 14:48:01 +0200 |
commit | 8339f8abc5470e5b9bad611e5577fccf34fef240 (patch) | |
tree | bb6bc5ff9213184990255f44cfc04476fab9f202 /android | |
parent | 71e176c0205f916f04a3d8fa3908da4e6dd20f50 (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')
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="<template>" 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="<template>" 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 Binary files differnew file mode 100644 index 000000000000..96a442e5b8e9 --- /dev/null +++ b/android/experimental/LOAndroid/app/src/main/res/drawable-hdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 000000000000..d5f16694f342 --- /dev/null +++ b/android/experimental/LOAndroid/app/src/main/res/drawable-hdpi/ic_status_logo.png 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 Binary files differnew file mode 100644 index 000000000000..359047dfa4ed --- /dev/null +++ b/android/experimental/LOAndroid/app/src/main/res/drawable-mdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 000000000000..835fc9290727 --- /dev/null +++ b/android/experimental/LOAndroid/app/src/main/res/drawable-mdpi/ic_status_logo.png 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 Binary files differnew file mode 100644 index 000000000000..71c6d760f051 --- /dev/null +++ b/android/experimental/LOAndroid/app/src/main/res/drawable-xhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 000000000000..c8005425416a --- /dev/null +++ b/android/experimental/LOAndroid/app/src/main/res/drawable-xhdpi/ic_status_logo.png 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 Binary files differnew file mode 100644 index 000000000000..4df18946442e --- /dev/null +++ b/android/experimental/LOAndroid/app/src/main/res/drawable-xxhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 000000000000..8c0fb64a8698 --- /dev/null +++ b/android/experimental/LOAndroid/gradle/wrapper/gradle-wrapper.jar 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' |