summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /mobile/android/geckoview
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'mobile/android/geckoview')
-rw-r--r--mobile/android/geckoview/build.gradle176
-rw-r--r--mobile/android/geckoview/proguard-rules.txt175
-rw-r--r--mobile/android/geckoview/src/main/AndroidManifest.xml39
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/AlarmReceiver.java42
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java425
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/BaseGeckoInterface.java169
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/ContextGetter.java15
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/CrashHandler.java478
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java503
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAccessibility.java410
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java2239
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoBatteryManager.java202
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java1589
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableClient.java33
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableListener.java43
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoHalDefines.java27
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java1060
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java491
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfile.java1002
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfileDirectories.java230
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenOrientation.java423
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSharedPrefs.java318
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java677
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoView.java736
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewChrome.java81
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewContent.java56
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewFragment.java52
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputConnectionListener.java25
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputMethods.java76
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/NSSBridge.java55
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/NotificationListener.java17
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/PrefsHelper.java308
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/SysInfo.java237
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/TouchEventInterceptor.java14
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/JNITarget.java14
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/ReflectionTarget.java18
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/RobocopTarget.java15
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WebRTCJNITarget.java14
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WrapForJNI.java51
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BitmapUtils.java290
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BufferedImage.java94
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BufferedImageGLInfo.java35
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DynamicToolbarAnimator.java605
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FloatSize.java54
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FullScreenState.java12
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java694
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java282
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/IntSize.java89
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerRenderer.java275
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java711
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java300
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/Overscroll.java21
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java162
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java38
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomTarget.java15
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java73
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PointUtils.java51
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ProgressiveUpdateData.java29
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RectUtils.java126
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RenderTask.java80
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/StackScroller.java695
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceTextureListener.java38
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ViewTransform.java28
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/ByteBufferInputStream.java64
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/DirectBufferAllocator.java52
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java549
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/JNIObject.java11
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeReference.java13
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeZip.java84
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/SafeIntent.java134
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionBlock.java133
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/Permissions.java210
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionsHelper.java32
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/ByteBufferInputStream.java38
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/MatrixBlobCursor.java366
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridge.java387
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridgeException.java18
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandler.java11
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandlerMap.java24
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityUtils.java72
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java25
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/Clipboard.java117
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ContextUtils.java51
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DateUtil.java55
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java29
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FileUtils.java259
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FloatUtils.java43
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GamepadUtils.java140
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java76
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoEventListener.java14
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoJarReader.java261
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoRequest.java94
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java169
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java117
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INIParser.java176
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INISection.java123
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IOUtils.java129
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputOptionsUtils.java45
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java109
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/JSONUtils.java69
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/MenuUtils.java33
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeEventListener.java23
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSContainer.java37
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSObject.java533
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.java177
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NonEvictingLruCache.java44
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/PrefUtils.java70
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java155
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/RawResource.java52
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StringUtils.java293
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java247
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UIAsyncTask.java121
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UUIDUtil.java19
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WeakReferenceHandler.java27
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WindowUtils.java59
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/publicsuffix/PublicSuffix.java121
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/publicsuffix/PublicSuffixPatterns.java117
-rw-r--r--mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/ISelfBrailleService.java147
-rw-r--r--mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/SelfBrailleClient.java267
-rw-r--r--mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/WriteData.java192
120 files changed, 24365 insertions, 0 deletions
diff --git a/mobile/android/geckoview/build.gradle b/mobile/android/geckoview/build.gradle
new file mode 100644
index 000000000..cc46530cb
--- /dev/null
+++ b/mobile/android/geckoview/build.gradle
@@ -0,0 +1,176 @@
+buildDir "${topobjdir}/gradle/build/mobile/android/geckoview"
+
+apply plugin: 'android-sdk-manager' // Must come before 'com.android.*'.
+apply plugin: 'com.android.library'
+
+def VERSION_NAME = '0.0.1'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
+
+ defaultConfig {
+ // TODO: version GeckoView explicitly. We'd like to avoid
+ // mozconfig.substs.ANDROID_VERSION_CODE, which won't be intuitive to
+ // consumer (and advances very quickly on pre-release channels).
+ versionCode 1
+ versionName VERSION_NAME
+ targetSdkVersion 23
+ minSdkVersion 15
+ consumerProguardFiles 'proguard-rules.txt'
+ }
+
+ buildTypes {
+ withGeckoBinaries {
+ initWith release
+ }
+ withoutGeckoBinaries { // For clarity and consistency throughout the tree.
+ initWith release
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+
+ dexOptions {
+ javaMaxHeapSize "2g"
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+
+ sourceSets {
+ main {
+ java {
+ srcDir "${topsrcdir}/mobile/android/geckoview/src/thirdparty/java"
+
+ // TODO: support WebRTC.
+ // if (mozconfig.substs.MOZ_WEBRTC) {
+ // srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/audio_device/android/java/src"
+ // srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_capture/android/java/src"
+ // srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_render/android/java/src"
+ // }
+
+ // TODO: don't use AppConstants.
+ srcDir "${project.buildDir}/generated/source/preprocessed_code" // See syncPreprocessedCode.
+ }
+
+ assets {
+ }
+ }
+ }
+}
+
+dependencies {
+ compile "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+}
+
+task syncPreprocessedCode(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
+ into("${project.buildDir}/generated/source/preprocessed_code")
+ from("${topobjdir}/mobile/android/base/generated/preprocessed") {
+ // AdjustConstants is included in the main app project.
+ exclude '**/AdjustConstants.java'
+ }
+}
+
+apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"
+
+android.libraryVariants.all { variant ->
+ variant.preBuild.dependsOn syncPreprocessedCode
+
+ // Like 'debug', 'release', or 'withGeckoBinaries'.
+ def buildType = variant.buildType.name
+
+ // It would be most natural for :geckoview to always include the Gecko
+ // binaries, but that's difficult; see the notes in
+ // mobile/android/gradle/with_gecko_binaries.gradle. Instead :app uses
+ // :geckoview:release and handles it's own Gecko binary inclusion.
+ if (buildType.equals('withGeckoBinaries')) {
+ configureVariantWithGeckoBinaries(variant)
+ }
+
+ // Javadoc and Sources JAR configuration cribbed from
+ // https://github.com/mapbox/mapbox-gl-native/blob/d169ea55c1cfa85cd8bf19f94c5f023569f71810/platform/android/MapboxGLAndroidSDK/build.gradle#L85
+ // informed by
+ // https://code.tutsplus.com/tutorials/creating-and-publishing-an-android-library--cms-24582,
+ // and amended from numerous Stackoverflow posts.
+ def name = variant.name
+ def javadoc = task "javadoc${name.capitalize()}"(type: Javadoc) {
+ description = "Generate Javadoc for build variant $name"
+ failOnError = false
+ destinationDir = new File(destinationDir, variant.baseName)
+ source = files(variant.javaCompile.source)
+ classpath = files(variant.javaCompile.classpath.files) + files(android.bootClasspath)
+ options.windowTitle("Mozilla GeckoView Android API $VERSION_NAME Reference")
+ options.docTitle("Mozilla GeckoView Android API $VERSION_NAME")
+ options.header("Mozilla GeckoView Android API $VERSION_NAME Reference")
+ options.bottom("&copy; 2016 Mozilla. All rights reserved.")
+ options.links("http://docs.oracle.com/javase/7/docs/api/")
+ options.linksOffline("http://d.android.com/reference/", "$System.env.ANDROID_HOME/docs/reference")
+ // TODO: options.overview("src/main/java/overview.html")
+ options.group("Mozilla GeckoView", "org.mozilla.gecko*") // TODO: narrow this down.
+ exclude '**/R.java', '**/BuildConfig.java', 'com/googlecode/**'
+ }
+
+ task "javadocJar${name.capitalize()}"(type: Jar, dependsOn: javadoc) {
+ classifier = 'javadoc'
+ from javadoc.destinationDir
+ }
+
+ task "sourcesJar${name.capitalize()}"(type: Jar) {
+ classifier 'sources'
+ description = "Generate Javadoc for build variant $name"
+ destinationDir = new File(destinationDir, variant.baseName)
+ from files(variant.javaCompile.source)
+ }
+}
+
+apply plugin: 'maven'
+
+uploadArchives {
+ repositories.mavenDeployer {
+ pom.groupId = 'org.mozilla'
+ pom.artifactId = 'geckoview'
+ pom.version = VERSION_NAME
+ pom.project {
+ licenses {
+ license {
+ name 'The Mozilla Public License, v. 2.0'
+ url 'http://mozilla.org/MPL/2.0/'
+ distribution 'repo'
+ }
+ }
+ }
+ repository(url: "file://${project.buildDir}/maven")
+ }
+}
+
+// This is all related to the withGeckoBinaries approach; see
+// mobile/android/gradle/with_gecko_binaries.gradle.
+afterEvaluate {
+ // The bundle tasks are only present when the particular configuration is
+ // being built, so this task might not exist. (This is due to the way the
+ // Android Gradle plugin defines things during configuration.)
+ def bundleWithGeckoBinaries = tasks.findByName('bundleWithGeckoBinaries')
+ if (!bundleWithGeckoBinaries) {
+ return
+ }
+
+ // Remove default configuration, which is the release configuration, when
+ // we're actually building withGeckoBinaries. This makes `gradle install`
+ // install the withGeckoBinaries artifacts, not the release artifacts (which
+ // are withoutGeckoBinaries and not suitable for distribution.)
+ def Configuration archivesConfig = project.getConfigurations().getByName('archives')
+ archivesConfig.artifacts.removeAll { it.extension.equals('aar') }
+
+ artifacts {
+ // Instead of default (release) configuration, publish one with Gecko binaries.
+ archives bundleWithGeckoBinaries
+ // Javadoc and sources for developer ergononomics.
+ archives javadocJarWithGeckoBinaries
+ archives sourcesJarWithGeckoBinaries
+ }
+}
diff --git a/mobile/android/geckoview/proguard-rules.txt b/mobile/android/geckoview/proguard-rules.txt
new file mode 100644
index 000000000..6dbc4260b
--- /dev/null
+++ b/mobile/android/geckoview/proguard-rules.txt
@@ -0,0 +1,175 @@
+# Modified from https://robotsandpencils.com/blog/use-proguard-android-library/.
+
+# Preserve all annotations.
+
+-keepattributes *Annotation*
+
+# Preserve all public classes, and their public and protected fields and
+# methods.
+
+-keep public class * {
+ public protected *;
+}
+
+# Preserve all .class method names.
+
+-keepclassmembernames class * {
+ java.lang.Class class$(java.lang.String);
+ java.lang.Class class$(java.lang.String, boolean);
+}
+
+# Preserve all native method names and the names of their classes.
+
+-keepclasseswithmembernames class * {
+ native <methods>;
+}
+
+# Preserve the special static methods that are required in all enumeration
+# classes.
+
+-keepclassmembers class * extends java.lang.Enum {
+ public static **[] values();
+ public static ** valueOf(java.lang.String);
+}
+
+# Explicitly preserve all serialization members. The Serializable interface
+# is only a marker interface, so it wouldn't save them.
+# You can comment this out if your library doesn't use serialization.
+# If your code contains serializable classes that have to be backward
+# compatible, please refer to the manual.
+
+-keepclassmembers class * implements java.io.Serializable {
+ static final long serialVersionUID;
+ static final java.io.ObjectStreamField[] serialPersistentFields;
+ private void writeObject(java.io.ObjectOutputStream);
+ private void readObject(java.io.ObjectInputStream);
+ java.lang.Object writeReplace();
+ java.lang.Object readResolve();
+}
+
+# Preserve all View implementations and their special context constructors.
+
+-keep public class * extends android.view.View {
+ public <init>(android.content.Context);
+ public <init>(android.content.Context, android.util.AttributeSet);
+ public <init>(android.content.Context, android.util.AttributeSet, int);
+ public void set*(...);
+}
+
+# Keep setters in Views so that animations can still work.
+# See http://proguard.sourceforge.net/manual/examples.html#beans
+# From tools/proguard/proguard-android.txt.
+-keepclassmembers public class * extends android.view.View {
+ void set*(***);
+ *** get*();
+}
+
+# Preserve all classes that have special context constructors, and the
+# constructors themselves.
+
+-keepclasseswithmembers class * {
+ public <init>(android.content.Context, android.util.AttributeSet);
+}
+
+# Preserve the special fields of all Parcelable implementations.
+
+-keepclassmembers class * implements android.os.Parcelable {
+ static android.os.Parcelable$Creator CREATOR;
+}
+
+# Preserve static fields of inner classes of R classes that might be accessed
+# through introspection.
+
+-keepclassmembers class **.R$* {
+ public static <fields>;
+}
+
+# GeckoView specific rules.
+
+# Keep classes, and all their contents, compiled before annotation.*.
+-keep class org.mozilla.gecko.AppConstants {
+ *;
+}
+-keep class org.mozilla.gecko.AppConstants$Versions {
+ *;
+}
+-keep class org.mozilla.gecko.SysInfo {
+ *;
+}
+
+# Keep the annotation.
+-keep @interface org.mozilla.gecko.annotation.JNITarget
+
+# Keep classes tagged with the annotation.
+-keep @org.mozilla.gecko.annotation.JNITarget class *
+
+# Keep all members of an annotated class.
+-keepclassmembers @org.mozilla.gecko.annotation.JNITarget class * {
+ *;
+}
+
+# Keep annotated members of any class.
+-keepclassmembers class * {
+ @org.mozilla.gecko.annotation.JNITarget *;
+}
+
+# Keep classes which contain at least one annotated element. Split over two directives
+# because, according to the developer of ProGuard, "the option -keepclasseswithmembers
+# doesn't combine well with the '*' wildcard" (And, indeed, using it causes things to
+# be deleted that we want to keep.)
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.annotation.JNITarget <methods>;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.annotation.JNITarget <fields>;
+}
+
+# Keep WebRTC targets.
+-keep @interface org.mozilla.gecko.annotation.WebRTCJNITarget
+-keep @org.mozilla.gecko.annotation.WebRTCJNITarget class *
+-keepclassmembers class * {
+ @org.mozilla.gecko.annotation.WebRTCJNITarget *;
+}
+-keepclassmembers @org.mozilla.gecko.annotation.WebRTCJNITarget class * {
+ *;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.annotation.WebRTCJNITarget <methods>;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.annotation.WebRTCJNITarget <fields>;
+}
+
+# Keep generator-targeted entry points.
+-keep @interface org.mozilla.gecko.annotation.WrapForJNI
+-keep @org.mozilla.gecko.annotation.WrapForJNI class *
+-keepclassmembers class * {
+ @org.mozilla.gecko.annotation.WrapForJNI *;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.annotation.WrapForJNI <methods>;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.annotation.WrapForJNI <fields>;
+}
+
+# Keep all members of an annotated class.
+-keepclassmembers @org.mozilla.gecko.annotation.WrapForJNI class * {
+ *;
+}
+
+# Keep Reflection targets.
+-keep @interface org.mozilla.gecko.annotation.ReflectionTarget
+-keep @org.mozilla.gecko.annotation.ReflectionTarget class *
+-keepclassmembers class * {
+ @org.mozilla.gecko.annotation.ReflectionTarget *;
+}
+-keepclassmembers @org.mozilla.gecko.annotation.ReflectionTarget class * {
+ *;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.annotation.ReflectionTarget <methods>;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.annotation.ReflectionTarget <fields>;
+}
diff --git a/mobile/android/geckoview/src/main/AndroidManifest.xml b/mobile/android/geckoview/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..4e2aaf447
--- /dev/null
+++ b/mobile/android/geckoview/src/main/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.mozilla.geckoview">
+
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+ <!-- READ_EXTERNAL_STORAGE was added in API 16, and is only enforced in API
+ 19+. We declare it so that the bouncer APK and the main APK have the
+ same set of permissions. -->
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
+ <uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT"/>
+
+ <uses-permission android:name="android.permission.WAKE_LOCK"/>
+ <uses-permission android:name="android.permission.VIBRATE"/>
+
+ <uses-feature android:name="android.hardware.location" android:required="false"/>
+ <uses-feature android:name="android.hardware.location.gps" android:required="false"/>
+ <uses-feature android:name="android.hardware.touchscreen"/>
+
+ <!--#ifdef MOZ_WEBRTC-->
+ <!--<uses-permission android:name="android.permission.RECORD_AUDIO"/>-->
+ <!--<uses-feature android:name="android.hardware.audio.low_latency" android:required="false"/>-->
+ <!--<uses-feature android:name="android.hardware.camera.any" android:required="false"/>-->
+ <!--<uses-feature android:name="android.hardware.microphone" android:required="false"/>-->
+ <!--#endif-->
+
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-feature android:name="android.hardware.camera" android:required="false"/>
+ <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
+
+ <!-- App requires OpenGL ES 2.0 -->
+ <uses-feature android:glEsVersion="0x00020000" android:required="true" />
+
+</manifest>
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AlarmReceiver.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AlarmReceiver.java
new file mode 100644
index 000000000..a098113fa
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AlarmReceiver.java
@@ -0,0 +1,42 @@
+/* -*- 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;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.content.BroadcastReceiver;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class AlarmReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
+ final WakeLock wakeLock = powerManager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, "GeckoAlarm");
+ wakeLock.acquire();
+
+ AlarmReceiver.notifyAlarmFired();
+ TimerTask releaseLockTask = new TimerTask() {
+ @Override
+ public void run() {
+ wakeLock.release();
+ }
+ };
+ Timer timer = new Timer();
+ // 5 seconds ought to be enough for anybody
+ timer.schedule(releaseLockTask, 5 * 1000);
+ }
+
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ private static native void notifyAlarmFired();
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java
new file mode 100644
index 000000000..54e1b0931
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java
@@ -0,0 +1,425 @@
+/* -*- 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;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.GamepadUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.content.Context;
+import android.hardware.input.InputManager;
+import android.util.SparseArray;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+
+public class AndroidGamepadManager {
+ // This is completely arbitrary.
+ private static final float TRIGGER_PRESSED_THRESHOLD = 0.25f;
+ private static final long POLL_TIMER_PERIOD = 1000; // milliseconds
+
+ private static enum Axis {
+ X(MotionEvent.AXIS_X),
+ Y(MotionEvent.AXIS_Y),
+ Z(MotionEvent.AXIS_Z),
+ RZ(MotionEvent.AXIS_RZ);
+
+ public final int axis;
+
+ private Axis(int axis) {
+ this.axis = axis;
+ }
+ }
+
+ // A list of gamepad button mappings. Axes are determined at
+ // runtime, as they vary by Android version.
+ private static enum Trigger {
+ Left(6),
+ Right(7);
+
+ public final int button;
+
+ private Trigger(int button) {
+ this.button = button;
+ }
+ }
+
+ private static final int FIRST_DPAD_BUTTON = 12;
+ // A list of axis number, gamepad button mappings for negative, positive.
+ // Button mappings are added to FIRST_DPAD_BUTTON.
+ private static enum DpadAxis {
+ UpDown(MotionEvent.AXIS_HAT_Y, 0, 1),
+ LeftRight(MotionEvent.AXIS_HAT_X, 2, 3);
+
+ public final int axis;
+ public final int negativeButton;
+ public final int positiveButton;
+
+ private DpadAxis(int axis, int negativeButton, int positiveButton) {
+ this.axis = axis;
+ this.negativeButton = negativeButton;
+ this.positiveButton = positiveButton;
+ }
+ }
+
+ private static enum Button {
+ A(KeyEvent.KEYCODE_BUTTON_A),
+ B(KeyEvent.KEYCODE_BUTTON_B),
+ X(KeyEvent.KEYCODE_BUTTON_X),
+ Y(KeyEvent.KEYCODE_BUTTON_Y),
+ L1(KeyEvent.KEYCODE_BUTTON_L1),
+ R1(KeyEvent.KEYCODE_BUTTON_R1),
+ L2(KeyEvent.KEYCODE_BUTTON_L2),
+ R2(KeyEvent.KEYCODE_BUTTON_R2),
+ SELECT(KeyEvent.KEYCODE_BUTTON_SELECT),
+ START(KeyEvent.KEYCODE_BUTTON_START),
+ THUMBL(KeyEvent.KEYCODE_BUTTON_THUMBL),
+ THUMBR(KeyEvent.KEYCODE_BUTTON_THUMBR),
+ DPAD_UP(KeyEvent.KEYCODE_DPAD_UP),
+ DPAD_DOWN(KeyEvent.KEYCODE_DPAD_DOWN),
+ DPAD_LEFT(KeyEvent.KEYCODE_DPAD_LEFT),
+ DPAD_RIGHT(KeyEvent.KEYCODE_DPAD_RIGHT);
+
+ public final int button;
+
+ private Button(int button) {
+ this.button = button;
+ }
+ }
+
+ private static class Gamepad {
+ // ID from GamepadService
+ public int id;
+ // Retain axis state so we can determine changes.
+ public float axes[];
+ public boolean dpad[];
+ public int triggerAxes[];
+ public float triggers[];
+
+ public Gamepad(int serviceId, int deviceId) {
+ id = serviceId;
+ axes = new float[Axis.values().length];
+ dpad = new boolean[4];
+ triggers = new float[2];
+
+ InputDevice device = InputDevice.getDevice(deviceId);
+ if (device != null) {
+ // LTRIGGER/RTRIGGER don't seem to be exposed on older
+ // versions of Android.
+ if (device.getMotionRange(MotionEvent.AXIS_LTRIGGER) != null && device.getMotionRange(MotionEvent.AXIS_RTRIGGER) != null) {
+ triggerAxes = new int[]{MotionEvent.AXIS_LTRIGGER,
+ MotionEvent.AXIS_RTRIGGER};
+ } else if (device.getMotionRange(MotionEvent.AXIS_BRAKE) != null && device.getMotionRange(MotionEvent.AXIS_GAS) != null) {
+ triggerAxes = new int[]{MotionEvent.AXIS_BRAKE,
+ MotionEvent.AXIS_GAS};
+ } else {
+ triggerAxes = null;
+ }
+ }
+ }
+ }
+
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ private static native void onGamepadChange(int id, boolean added);
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ private static native void onButtonChange(int id, int button, boolean pressed, float value);
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ private static native void onAxisChange(int id, boolean[] valid, float[] values);
+
+ private static boolean sStarted;
+ private static final SparseArray<Gamepad> sGamepads = new SparseArray<>();
+ private static final SparseArray<List<KeyEvent>> sPendingGamepads = new SparseArray<>();
+ private static InputManager.InputDeviceListener sListener;
+ private static Timer sPollTimer;
+
+ private AndroidGamepadManager() {
+ }
+
+ @WrapForJNI
+ private static void start() {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doStart();
+ }
+ });
+ }
+
+ /* package */ static void doStart() {
+ ThreadUtils.assertOnUiThread();
+ if (!sStarted) {
+ scanForGamepads();
+ addDeviceListener();
+ sStarted = true;
+ }
+ }
+
+ @WrapForJNI
+ private static void stop() {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doStop();
+ }
+ });
+ }
+
+ /* package */ static void doStop() {
+ ThreadUtils.assertOnUiThread();
+ if (sStarted) {
+ removeDeviceListener();
+ sPendingGamepads.clear();
+ sGamepads.clear();
+ sStarted = false;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void onGamepadAdded(final int device_id, final int service_id) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ handleGamepadAdded(device_id, service_id);
+ }
+ });
+ }
+
+ /* package */ static void handleGamepadAdded(int deviceId, int serviceId) {
+ ThreadUtils.assertOnUiThread();
+ if (!sStarted) {
+ return;
+ }
+
+ final List<KeyEvent> pending = sPendingGamepads.get(deviceId);
+ if (pending == null) {
+ removeGamepad(deviceId);
+ return;
+ }
+
+ sPendingGamepads.remove(deviceId);
+ sGamepads.put(deviceId, new Gamepad(serviceId, deviceId));
+ // Handle queued KeyEvents
+ for (KeyEvent ev : pending) {
+ handleKeyEvent(ev);
+ }
+ }
+
+ private static float deadZone(MotionEvent ev, int axis) {
+ if (GamepadUtils.isValueInDeadZone(ev, axis)) {
+ return 0.0f;
+ }
+ return ev.getAxisValue(axis);
+ }
+
+ private static void mapDpadAxis(Gamepad gamepad,
+ boolean pressed,
+ float value,
+ int which) {
+ if (pressed != gamepad.dpad[which]) {
+ gamepad.dpad[which] = pressed;
+ onButtonChange(gamepad.id, FIRST_DPAD_BUTTON + which, pressed, Math.abs(value));
+ }
+ }
+
+ public static boolean handleMotionEvent(MotionEvent ev) {
+ ThreadUtils.assertOnUiThread();
+ if (!sStarted) {
+ return false;
+ }
+
+ final Gamepad gamepad = sGamepads.get(ev.getDeviceId());
+ if (gamepad == null) {
+ // Not a device we care about.
+ return false;
+ }
+
+ // First check the analog stick axes
+ boolean[] valid = new boolean[Axis.values().length];
+ float[] axes = new float[Axis.values().length];
+ boolean anyValidAxes = false;
+ for (Axis axis : Axis.values()) {
+ float value = deadZone(ev, axis.axis);
+ int i = axis.ordinal();
+ if (value != gamepad.axes[i]) {
+ axes[i] = value;
+ gamepad.axes[i] = value;
+ valid[i] = true;
+ anyValidAxes = true;
+ }
+ }
+ if (anyValidAxes) {
+ // Send an axismove event.
+ onAxisChange(gamepad.id, valid, axes);
+ }
+
+ // Map triggers to buttons.
+ if (gamepad.triggerAxes != null) {
+ for (Trigger trigger : Trigger.values()) {
+ int i = trigger.ordinal();
+ int axis = gamepad.triggerAxes[i];
+ float value = deadZone(ev, axis);
+ if (value != gamepad.triggers[i]) {
+ gamepad.triggers[i] = value;
+ boolean pressed = value > TRIGGER_PRESSED_THRESHOLD;
+ onButtonChange(gamepad.id, trigger.button, pressed, value);
+ }
+ }
+ }
+ // Map d-pad to buttons.
+ for (DpadAxis dpadaxis : DpadAxis.values()) {
+ float value = deadZone(ev, dpadaxis.axis);
+ mapDpadAxis(gamepad, value < 0.0f, value, dpadaxis.negativeButton);
+ mapDpadAxis(gamepad, value > 0.0f, value, dpadaxis.positiveButton);
+ }
+ return true;
+ }
+
+ public static boolean handleKeyEvent(KeyEvent ev) {
+ ThreadUtils.assertOnUiThread();
+ if (!sStarted) {
+ return false;
+ }
+
+ int deviceId = ev.getDeviceId();
+ final List<KeyEvent> pendingGamepad = sPendingGamepads.get(deviceId);
+ if (pendingGamepad != null) {
+ // Queue up key events for pending devices.
+ pendingGamepad.add(ev);
+ return true;
+ }
+
+ if (sGamepads.get(deviceId) == null) {
+ InputDevice device = ev.getDevice();
+ if (device != null &&
+ (device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
+ // This is a gamepad we haven't seen yet.
+ addGamepad(device);
+ sPendingGamepads.get(deviceId).add(ev);
+ return true;
+ }
+ // Not a device we care about.
+ return false;
+ }
+
+ int key = -1;
+ for (Button button : Button.values()) {
+ if (button.button == ev.getKeyCode()) {
+ key = button.ordinal();
+ break;
+ }
+ }
+ if (key == -1) {
+ // Not a key we know how to handle.
+ return false;
+ }
+ if (ev.getRepeatCount() > 0) {
+ // We would handle this key, but we're not interested in
+ // repeats. Eat it.
+ return true;
+ }
+
+ Gamepad gamepad = sGamepads.get(deviceId);
+ boolean pressed = ev.getAction() == KeyEvent.ACTION_DOWN;
+ onButtonChange(gamepad.id, key, pressed, pressed ? 1.0f : 0.0f);
+ return true;
+ }
+
+ private static void scanForGamepads() {
+ int[] deviceIds = InputDevice.getDeviceIds();
+ if (deviceIds == null) {
+ return;
+ }
+ for (int i = 0; i < deviceIds.length; i++) {
+ InputDevice device = InputDevice.getDevice(deviceIds[i]);
+ if (device == null) {
+ continue;
+ }
+ if ((device.getSources() & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD) {
+ continue;
+ }
+ addGamepad(device);
+ }
+ }
+
+ private static void addGamepad(InputDevice device) {
+ sPendingGamepads.put(device.getId(), new ArrayList<KeyEvent>());
+ onGamepadChange(device.getId(), true);
+ }
+
+ private static void removeGamepad(int deviceId) {
+ Gamepad gamepad = sGamepads.get(deviceId);
+ onGamepadChange(gamepad.id, false);
+ sGamepads.remove(deviceId);
+ }
+
+ private static void addDeviceListener() {
+ if (Versions.preJB) {
+ // Poll known gamepads to see if they've disappeared.
+ sPollTimer = new Timer();
+ sPollTimer.scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() {
+ for (int i = 0; i < sGamepads.size(); ++i) {
+ final int deviceId = sGamepads.keyAt(i);
+ if (InputDevice.getDevice(deviceId) == null) {
+ removeGamepad(deviceId);
+ }
+ }
+ }
+ }, POLL_TIMER_PERIOD, POLL_TIMER_PERIOD);
+ return;
+ }
+ sListener = new InputManager.InputDeviceListener() {
+ @Override
+ public void onInputDeviceAdded(int deviceId) {
+ InputDevice device = InputDevice.getDevice(deviceId);
+ if (device == null) {
+ return;
+ }
+ if ((device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
+ addGamepad(device);
+ }
+ }
+
+ @Override
+ public void onInputDeviceRemoved(int deviceId) {
+ if (sPendingGamepads.get(deviceId) != null) {
+ // Got removed before Gecko's ack reached us.
+ // gamepadAdded will deal with it.
+ sPendingGamepads.remove(deviceId);
+ return;
+ }
+ if (sGamepads.get(deviceId) != null) {
+ removeGamepad(deviceId);
+ }
+ }
+
+ @Override
+ public void onInputDeviceChanged(int deviceId) {
+ }
+ };
+ ((InputManager)GeckoAppShell.getContext().getSystemService(Context.INPUT_SERVICE)).registerInputDeviceListener(sListener, ThreadUtils.getUiHandler());
+ }
+
+ private static void removeDeviceListener() {
+ if (Versions.preJB) {
+ if (sPollTimer != null) {
+ sPollTimer.cancel();
+ sPollTimer = null;
+ }
+ return;
+ }
+ ((InputManager)GeckoAppShell.getContext().getSystemService(Context.INPUT_SERVICE)).unregisterInputDeviceListener(sListener);
+ sListener = null;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/BaseGeckoInterface.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/BaseGeckoInterface.java
new file mode 100644
index 000000000..c4f64fd3d
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/BaseGeckoInterface.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;
+
+import org.mozilla.gecko.util.ActivityUtils;
+import org.mozilla.gecko.util.HardwareUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.RectF;
+import android.hardware.SensorEventListener;
+import android.location.LocationListener;
+import android.view.View;
+import android.widget.AbsoluteLayout;
+
+public class BaseGeckoInterface implements GeckoAppShell.GeckoInterface {
+ // Bug 908744: Implement GeckoEventListener
+ // Bug 908752: Implement SensorEventListener
+ // Bug 908755: Implement LocationListener
+ // Bug 908756: Implement Tabs.OnTabsChangedListener
+ // Bug 908760: Implement GeckoEventResponder
+
+ private final Context mContext;
+ private GeckoProfile mProfile;
+ private final EventDispatcher eventDispatcher;
+
+ public BaseGeckoInterface(Context context) {
+ mContext = context;
+ eventDispatcher = new EventDispatcher();
+ }
+
+ @Override
+ public EventDispatcher getAppEventDispatcher() {
+ return eventDispatcher;
+ }
+
+ @Override
+ public GeckoProfile getProfile() {
+ // Fall back to default profile if we didn't load a specific one
+ if (mProfile == null) {
+ mProfile = GeckoProfile.get(mContext);
+ }
+ return mProfile;
+ }
+
+ @Override
+ public Activity getActivity() {
+ return (Activity)mContext;
+ }
+
+ @Override
+ public String getDefaultUAString() {
+ return HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET :
+ AppConstants.USER_AGENT_FENNEC_MOBILE;
+ }
+
+ // Bug 908775: Implement this
+ @Override
+ public void doRestart() {}
+
+ @Override
+ public void setFullScreen(final boolean fullscreen) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ActivityUtils.setFullScreen(getActivity(), fullscreen);
+ }
+ });
+ }
+
+ // Bug 908779: Implement this
+ @Override
+ public void addPluginView(final View view) {}
+
+ // Bug 908781: Implement this
+ @Override
+ public void removePluginView(final View view) {}
+
+ @Override
+ public void enableOrientationListener() {}
+
+ @Override
+ public void disableOrientationListener() {}
+
+ // Bug 908786: Implement this
+ @Override
+ public void addAppStateListener(GeckoAppShell.AppStateListener listener) {}
+
+ // Bug 908787: Implement this
+ @Override
+ public void removeAppStateListener(GeckoAppShell.AppStateListener listener) {}
+
+ // Bug 908789: Implement this
+ @Override
+ public void notifyWakeLockChanged(String topic, String state) {}
+
+ @Override
+ public boolean areTabsShown() {
+ return false;
+ }
+
+ // Bug 908791: Implement this
+ @Override
+ public AbsoluteLayout getPluginContainer() {
+ return null;
+ }
+
+ @Override
+ public void notifyCheckUpdateResult(String result) {
+ GeckoAppShell.notifyObservers("Update:CheckResult", result);
+ }
+
+ // Bug 908792: Implement this
+ @Override
+ public void invalidateOptionsMenu() {}
+
+ @Override
+ public void createShortcut(String title, String URI) {
+ // By default, do nothing.
+ }
+
+ @Override
+ public void checkUriVisited(String uri) {
+ // By default, no URIs are considered visited.
+ }
+
+ @Override
+ public void markUriVisited(final String uri) {
+ // By default, no URIs are marked as visited.
+ }
+
+ @Override
+ public void setUriTitle(final String uri, final String title) {
+ // By default, no titles are associated with URIs.
+ }
+
+ @Override
+ public void setAccessibilityEnabled(boolean enabled) {
+ // By default, take no action when accessibility is toggled on or off.
+ }
+
+ @Override
+ public boolean openUriExternal(String targetURI, String mimeType, String packageName, String className, String action, String title) {
+ // By default, never open external URIs.
+ return false;
+ }
+
+ @Override
+ public String[] getHandlersForMimeType(String mimeType, String action) {
+ // By default, offer no handlers for any MIME type.
+ return new String[] {};
+ }
+
+ @Override
+ public String[] getHandlersForURL(String url, String action) {
+ // By default, offer no handlers for any URL.
+ return new String[] {};
+ }
+
+ @Override
+ public String getDefaultChromeURI() {
+ // By default, use the GeckoView-specific chrome URI.
+ return "chrome://browser/content/geckoview.xul";
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/ContextGetter.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/ContextGetter.java
new file mode 100644
index 000000000..315854633
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/ContextGetter.java
@@ -0,0 +1,15 @@
+/* -*- 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;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+public interface ContextGetter {
+ Context getContext();
+ SharedPreferences getSharedPreferences();
+}
+
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/CrashHandler.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/CrashHandler.java
new file mode 100644
index 000000000..15df27336
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/CrashHandler.java
@@ -0,0 +1,478 @@
+/* -*- 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;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.UUID;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Process;
+import android.util.Log;
+
+public class CrashHandler implements Thread.UncaughtExceptionHandler {
+
+ private static final String LOGTAG = "GeckoCrashHandler";
+ private static final Thread MAIN_THREAD = Thread.currentThread();
+ private static final String DEFAULT_SERVER_URL =
+ "https://crash-reports.mozilla.com/submit?id=%1$s&version=%2$s&buildid=%3$s";
+
+ // Context for getting device information
+ protected final Context appContext;
+ // Thread that this handler applies to, or null for a global handler
+ protected final Thread handlerThread;
+ protected final Thread.UncaughtExceptionHandler systemUncaughtHandler;
+
+ protected boolean crashing;
+ protected boolean unregistered;
+
+ /**
+ * Get the root exception from the 'cause' chain of an exception.
+ *
+ * @param exc An exception
+ * @return The root exception
+ */
+ public static Throwable getRootException(Throwable exc) {
+ for (Throwable cause = exc; cause != null; cause = cause.getCause()) {
+ exc = cause;
+ }
+ return exc;
+ }
+
+ /**
+ * Get the standard stack trace string of an exception.
+ *
+ * @param exc An exception
+ * @return The exception stack trace.
+ */
+ public static String getExceptionStackTrace(final Throwable exc) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ exc.printStackTrace(pw);
+ pw.flush();
+ return sw.toString();
+ }
+
+ /**
+ * Terminate the current process.
+ */
+ public static void terminateProcess() {
+ Process.killProcess(Process.myPid());
+ }
+
+ /**
+ * Create and register a CrashHandler for all threads and thread groups.
+ */
+ public CrashHandler() {
+ this((Context) null);
+ }
+
+ /**
+ * Create and register a CrashHandler for all threads and thread groups.
+ *
+ * @param appContext A Context for retrieving application information.
+ */
+ public CrashHandler(final Context appContext) {
+ this.appContext = appContext;
+ this.handlerThread = null;
+ this.systemUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();
+ Thread.setDefaultUncaughtExceptionHandler(this);
+ }
+
+ /**
+ * Create and register a CrashHandler for a particular thread.
+ *
+ * @param thread A thread to register the CrashHandler
+ */
+ public CrashHandler(final Thread thread) {
+ this(thread, null);
+ }
+
+ /**
+ * Create and register a CrashHandler for a particular thread.
+ *
+ * @param thread A thread to register the CrashHandler
+ * @param appContext A Context for retrieving application information.
+ */
+ public CrashHandler(final Thread thread, final Context appContext) {
+ this.appContext = appContext;
+ this.handlerThread = thread;
+ this.systemUncaughtHandler = thread.getUncaughtExceptionHandler();
+ thread.setUncaughtExceptionHandler(this);
+ }
+
+ /**
+ * Unregister this CrashHandler for exception handling.
+ */
+ public void unregister() {
+ unregistered = true;
+
+ // Restore the previous handler if we are still the topmost handler.
+ // If not, we are part of a chain of handlers, and we cannot just restore the previous
+ // handler, because that would replace whatever handler that's above us in the chain.
+
+ if (handlerThread != null) {
+ if (handlerThread.getUncaughtExceptionHandler() == this) {
+ handlerThread.setUncaughtExceptionHandler(systemUncaughtHandler);
+ }
+ } else {
+ if (Thread.getDefaultUncaughtExceptionHandler() == this) {
+ Thread.setDefaultUncaughtExceptionHandler(systemUncaughtHandler);
+ }
+ }
+ }
+
+ /**
+ * Record an exception stack in logs.
+ *
+ * @param thread The exception thread
+ * @param exc An exception
+ */
+ public static void logException(final Thread thread, final Throwable exc) {
+ try {
+ Log.e(LOGTAG, ">>> REPORTING UNCAUGHT EXCEPTION FROM THREAD "
+ + thread.getId() + " (\"" + thread.getName() + "\")", exc);
+
+ if (MAIN_THREAD != thread) {
+ Log.e(LOGTAG, "Main thread (" + MAIN_THREAD.getId() + ") stack:");
+ for (StackTraceElement ste : MAIN_THREAD.getStackTrace()) {
+ Log.e(LOGTAG, " " + ste.toString());
+ }
+ }
+ } catch (final Throwable e) {
+ // If something throws here, we want to continue to report the exception,
+ // so we catch all exceptions and ignore them.
+ }
+ }
+
+ private static long getCrashTime() {
+ return System.currentTimeMillis() / 1000;
+ }
+
+ private static long getStartupTime() {
+ // Process start time is also the proc file modified time.
+ final long uptimeMins = (new File("/proc/self/cmdline")).lastModified();
+ if (uptimeMins == 0L) {
+ return getCrashTime();
+ }
+ return uptimeMins / 1000;
+ }
+
+ private static String getJavaPackageName() {
+ return CrashHandler.class.getPackage().getName();
+ }
+
+ private static String getProcessName() {
+ try {
+ final FileReader reader = new FileReader("/proc/self/cmdline");
+ final char[] buffer = new char[64];
+ try {
+ if (reader.read(buffer) > 0) {
+ // cmdline is delimited by '\0', and we want the first token.
+ final int nul = Arrays.asList(buffer).indexOf('\0');
+ return (new String(buffer, 0, nul < 0 ? buffer.length : nul)).trim();
+ }
+ } finally {
+ reader.close();
+ }
+ } catch (final IOException e) {
+ }
+
+ return null;
+ }
+
+ protected String getAppPackageName() {
+ final Context context = getAppContext();
+
+ if (context != null) {
+ return context.getPackageName();
+ }
+
+ // Package name is also the process name in most cases.
+ String processName = getProcessName();
+ if (processName != null) {
+ return processName;
+ }
+
+ // Fallback to using CrashHandler's package name.
+ return getJavaPackageName();
+ }
+
+ protected Context getAppContext() {
+ return appContext;
+ }
+
+ /**
+ * Get the crash "extras" to be reported.
+ *
+ * @param thread The exception thread
+ * @param exc An exception
+ * @return "Extras" in the from of a Bundle
+ */
+ protected Bundle getCrashExtras(final Thread thread, final Throwable exc) {
+ final Context context = getAppContext();
+ final Bundle extras = new Bundle();
+ final String pkgName = getAppPackageName();
+ final String processName = getProcessName();
+
+ extras.putString("ProductName", pkgName);
+ extras.putLong("CrashTime", getCrashTime());
+ extras.putLong("StartupTime", getStartupTime());
+ extras.putString("AndroidProcessName", getProcessName());
+
+ if (context != null) {
+ final PackageManager pkgMgr = context.getPackageManager();
+ try {
+ final PackageInfo pkgInfo = pkgMgr.getPackageInfo(pkgName, 0);
+ extras.putString("Version", pkgInfo.versionName);
+ extras.putInt("BuildID", pkgInfo.versionCode);
+ extras.putLong("InstallTime", pkgInfo.lastUpdateTime / 1000);
+ } catch (final PackageManager.NameNotFoundException e) {
+ Log.i(LOGTAG, "Error getting package info", e);
+ }
+ }
+
+ extras.putString("JavaStackTrace", getExceptionStackTrace(exc));
+ return extras;
+ }
+
+ /**
+ * Get the crash minidump content to be reported.
+ *
+ * @param thread The exception thread
+ * @param exc An exception
+ * @return Minidump content
+ */
+ protected byte[] getCrashDump(final Thread thread, final Throwable exc) {
+ return new byte[0]; // No minidump.
+ }
+
+ protected static String normalizeUrlString(final String str) {
+ if (str == null) {
+ return "";
+ }
+ return Uri.encode(str);
+ }
+
+ /**
+ * Get the server URL to send the crash report to.
+ *
+ * @param extras The crash extras Bundle
+ */
+ protected String getServerUrl(final Bundle extras) {
+ return String.format(DEFAULT_SERVER_URL,
+ normalizeUrlString(extras.getString("ProductID")),
+ normalizeUrlString(extras.getString("Version")),
+ normalizeUrlString(extras.getString("BuildID")));
+ }
+
+ /**
+ * Launch the crash reporter activity that sends the crash report to the server.
+ *
+ * @param dumpFile Path for the minidump file
+ * @param extraFile Path for the crash extra file
+ * @return Whether the crash reporter was successfully launched
+ */
+ protected boolean launchCrashReporter(final String dumpFile, final String extraFile) {
+ try {
+ final Context context = getAppContext();
+ final String javaPkg = getJavaPackageName();
+ final String pkg = getAppPackageName();
+ final String component = javaPkg + ".CrashReporter";
+ final String action = javaPkg + ".reportCrash";
+ final ProcessBuilder pb;
+
+ if (context != null) {
+ final Intent intent = new Intent(action);
+ intent.setComponent(new ComponentName(pkg, component));
+ intent.putExtra("minidumpPath", dumpFile);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ return true;
+ }
+
+ // Avoid AppConstants dependency for SDK version constants,
+ // because CrashHandler could be used outside of Fennec code.
+ if (Build.VERSION.SDK_INT < 17) {
+ pb = new ProcessBuilder(
+ "/system/bin/am", "start",
+ "-a", action,
+ "-n", pkg + '/' + component,
+ "--es", "minidumpPath", dumpFile);
+ } else {
+ pb = new ProcessBuilder(
+ "/system/bin/am", "start",
+ "--user", /* USER_CURRENT_OR_SELF */ "-3",
+ "-a", action,
+ "-n", pkg + '/' + component,
+ "--es", "minidumpPath", dumpFile);
+ }
+
+ pb.start().waitFor();
+
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Error launching crash reporter", e);
+ return false;
+
+ } catch (final InterruptedException e) {
+ Log.i(LOGTAG, "Interrupted while waiting to launch crash reporter", e);
+ // Fall-through
+ }
+ return true;
+ }
+
+ /**
+ * Report an exception to Socorro.
+ *
+ * @param thread The exception thread
+ * @param exc An exception
+ * @return Whether the exception was successfully reported
+ */
+ protected boolean reportException(final Thread thread, final Throwable exc) {
+ final Context context = getAppContext();
+ final String id = UUID.randomUUID().toString();
+
+ // Use the cache directory under the app directory to store crash files.
+ final File dir;
+ if (context != null) {
+ dir = context.getCacheDir();
+ } else {
+ dir = new File("/data/data/" + getAppPackageName() + "/cache");
+ }
+
+ dir.mkdirs();
+ if (!dir.exists()) {
+ return false;
+ }
+
+ final File dmpFile = new File(dir, id + ".dmp");
+ final File extraFile = new File(dir, id + ".extra");
+
+ try {
+ // Write out minidump file as binary.
+
+ final byte[] minidump = getCrashDump(thread, exc);
+ final FileOutputStream dmpStream = new FileOutputStream(dmpFile);
+ try {
+ dmpStream.write(minidump);
+ } finally {
+ dmpStream.close();
+ }
+
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Error writing minidump file", e);
+ return false;
+ }
+
+ try {
+ // Write out crash extra file as text.
+
+ final Bundle extras = getCrashExtras(thread, exc);
+ final String url = getServerUrl(extras);
+ extras.putString("ServerURL", url);
+
+ final BufferedWriter extraWriter = new BufferedWriter(new FileWriter(extraFile));
+ try {
+ for (String key : extras.keySet()) {
+ // Each extra line is in the format, key=value, with newlines escaped.
+ extraWriter.write(key);
+ extraWriter.write('=');
+ extraWriter.write(String.valueOf(extras.get(key)).replace("\n", "\\n"));
+ extraWriter.write('\n');
+ }
+ } finally {
+ extraWriter.close();
+ }
+
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Error writing extra file", e);
+ return false;
+ }
+
+ return launchCrashReporter(dmpFile.getAbsolutePath(), extraFile.getAbsolutePath());
+ }
+
+ /**
+ * Implements the default behavior for handling uncaught exceptions.
+ *
+ * @param thread The exception thread
+ * @param exc An uncaught exception
+ */
+ @Override
+ public void uncaughtException(Thread thread, Throwable exc) {
+ if (this.crashing) {
+ // Prevent possible infinite recusions.
+ return;
+ }
+
+ if (thread == null) {
+ // Gecko may pass in null for thread to denote the current thread.
+ thread = Thread.currentThread();
+ }
+
+ try {
+ if (!this.unregistered) {
+ // Only process crash ourselves if we have not been unregistered.
+
+ this.crashing = true;
+ exc = getRootException(exc);
+ logException(thread, exc);
+
+ if (reportException(thread, exc)) {
+ // Reporting succeeded; we can terminate our process now.
+ return;
+ }
+ }
+
+ if (systemUncaughtHandler != null) {
+ // Follow the chain of uncaught handlers.
+ systemUncaughtHandler.uncaughtException(thread, exc);
+ }
+ } finally {
+ terminateProcess();
+ }
+ }
+
+ public static CrashHandler createDefaultCrashHandler(final Context context) {
+ return new CrashHandler(context) {
+ @Override
+ protected Bundle getCrashExtras(final Thread thread, final Throwable exc) {
+ final Bundle extras = super.getCrashExtras(thread, exc);
+
+ extras.putString("ProductName", AppConstants.MOZ_APP_BASENAME);
+ extras.putString("ProductID", AppConstants.MOZ_APP_ID);
+ extras.putString("Version", AppConstants.MOZ_APP_VERSION);
+ extras.putString("BuildID", AppConstants.MOZ_APP_BUILDID);
+ extras.putString("Vendor", AppConstants.MOZ_APP_VENDOR);
+ extras.putString("ReleaseChannel", AppConstants.MOZ_UPDATE_CHANNEL);
+ return extras;
+ }
+
+ @Override
+ public boolean reportException(final Thread thread, final Throwable exc) {
+ if (AppConstants.MOZ_CRASHREPORTER && AppConstants.MOZILLA_OFFICIAL) {
+ // Only use Java crash reporter if enabled on official build.
+ return super.reportException(thread, exc);
+ }
+ return false;
+ }
+ };
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java
new file mode 100644
index 000000000..6c4e67b43
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java
@@ -0,0 +1,503 @@
+/* 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.mozilla.gecko.annotation.ReflectionTarget;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSContainer;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+@RobocopTarget
+public final class EventDispatcher {
+ private static final String LOGTAG = "GeckoEventDispatcher";
+ /* package */ static final String GUID = "__guid__";
+ private static final String STATUS_ERROR = "error";
+ private static final String STATUS_SUCCESS = "success";
+
+ private static final EventDispatcher INSTANCE = new EventDispatcher();
+
+ /**
+ * The capacity of a HashMap is rounded up to the next power-of-2. Every time the size
+ * of the map goes beyond 75% of the capacity, the map is rehashed. Therefore, to
+ * empirically determine the initial capacity that avoids rehashing, we need to
+ * determine the initial size, divide it by 75%, and round up to the next power-of-2.
+ */
+ private static final int DEFAULT_GECKO_NATIVE_EVENTS_COUNT = 0; // Default for HashMap
+ private static final int DEFAULT_GECKO_JSON_EVENTS_COUNT = 256; // Empirically measured
+ private static final int DEFAULT_UI_EVENTS_COUNT = 0; // Default for HashMap
+ private static final int DEFAULT_BACKGROUND_EVENTS_COUNT = 0; // Default for HashMap
+
+ private final Map<String, List<NativeEventListener>> mGeckoThreadNativeListeners =
+ new HashMap<String, List<NativeEventListener>>(DEFAULT_GECKO_NATIVE_EVENTS_COUNT);
+ private final Map<String, List<GeckoEventListener>> mGeckoThreadJSONListeners =
+ new HashMap<String, List<GeckoEventListener>>(DEFAULT_GECKO_JSON_EVENTS_COUNT);
+ private final Map<String, List<BundleEventListener>> mUiThreadListeners =
+ new HashMap<String, List<BundleEventListener>>(DEFAULT_UI_EVENTS_COUNT);
+ private final Map<String, List<BundleEventListener>> mBackgroundThreadListeners =
+ new HashMap<String, List<BundleEventListener>>(DEFAULT_BACKGROUND_EVENTS_COUNT);
+
+ @ReflectionTarget
+ public static EventDispatcher getInstance() {
+ return INSTANCE;
+ }
+
+ public EventDispatcher() {
+ }
+
+ private <T> void registerListener(final Class<?> listType,
+ final Map<String, List<T>> listenersMap,
+ final T listener,
+ final String[] events) {
+ try {
+ synchronized (listenersMap) {
+ for (final String event : events) {
+ List<T> listeners = listenersMap.get(event);
+ if (listeners == null) {
+ // Java doesn't let us put Class<? extends List<T>> as the type for listType.
+ @SuppressWarnings("unchecked")
+ final Class<? extends List<T>> type = (Class) listType;
+ listeners = type.newInstance();
+ listenersMap.put(event, listeners);
+ }
+ if (!AppConstants.RELEASE_OR_BETA && listeners.contains(listener)) {
+ throw new IllegalStateException("Already registered " + event);
+ }
+ listeners.add(listener);
+ }
+ }
+ } catch (final IllegalAccessException | InstantiationException e) {
+ throw new IllegalArgumentException("Invalid new list type", e);
+ }
+ }
+
+ private void checkNotRegisteredElsewhere(final Map<String, ?> allowedMap,
+ final String[] events) {
+ if (AppConstants.RELEASE_OR_BETA) {
+ // for performance reasons, we only check for
+ // already-registered listeners in non-release builds.
+ return;
+ }
+ for (final Map<String, ?> listenersMap : Arrays.asList(mGeckoThreadNativeListeners,
+ mGeckoThreadJSONListeners,
+ mUiThreadListeners,
+ mBackgroundThreadListeners)) {
+ if (listenersMap == allowedMap) {
+ continue;
+ }
+ synchronized (listenersMap) {
+ for (final String event : events) {
+ if (listenersMap.get(event) != null) {
+ throw new IllegalStateException(
+ "Already registered " + event + " under a different type");
+ }
+ }
+ }
+ }
+ }
+
+ private <T> void unregisterListener(final Map<String, List<T>> listenersMap,
+ final T listener,
+ final String[] events) {
+ synchronized (listenersMap) {
+ for (final String event : events) {
+ List<T> listeners = listenersMap.get(event);
+ if ((listeners == null ||
+ !listeners.remove(listener)) && !AppConstants.RELEASE_OR_BETA) {
+ throw new IllegalArgumentException(event + " was not registered");
+ }
+ }
+ }
+ }
+
+ public void registerGeckoThreadListener(final NativeEventListener listener,
+ final String... events) {
+ checkNotRegisteredElsewhere(mGeckoThreadNativeListeners, events);
+
+ // For listeners running on the Gecko thread, we want to notify the listeners
+ // outside of our synchronized block, because the listeners may take an
+ // indeterminate amount of time to run. Therefore, to ensure concurrency when
+ // iterating the list outside of the synchronized block, we use a
+ // CopyOnWriteArrayList.
+ registerListener(CopyOnWriteArrayList.class,
+ mGeckoThreadNativeListeners, listener, events);
+ }
+
+ @Deprecated // Use NativeEventListener instead
+ public void registerGeckoThreadListener(final GeckoEventListener listener,
+ final String... events) {
+ checkNotRegisteredElsewhere(mGeckoThreadJSONListeners, events);
+
+ registerListener(CopyOnWriteArrayList.class,
+ mGeckoThreadJSONListeners, listener, events);
+ }
+
+ public void registerUiThreadListener(final BundleEventListener listener,
+ final String... events) {
+ checkNotRegisteredElsewhere(mUiThreadListeners, events);
+
+ registerListener(ArrayList.class,
+ mUiThreadListeners, listener, events);
+ }
+
+ @ReflectionTarget
+ public void registerBackgroundThreadListener(final BundleEventListener listener,
+ final String... events) {
+ checkNotRegisteredElsewhere(mBackgroundThreadListeners, events);
+
+ registerListener(ArrayList.class,
+ mBackgroundThreadListeners, listener, events);
+ }
+
+ public void unregisterGeckoThreadListener(final NativeEventListener listener,
+ final String... events) {
+ unregisterListener(mGeckoThreadNativeListeners, listener, events);
+ }
+
+ @Deprecated // Use NativeEventListener instead
+ public void unregisterGeckoThreadListener(final GeckoEventListener listener,
+ final String... events) {
+ unregisterListener(mGeckoThreadJSONListeners, listener, events);
+ }
+
+ public void unregisterUiThreadListener(final BundleEventListener listener,
+ final String... events) {
+ unregisterListener(mUiThreadListeners, listener, events);
+ }
+
+ public void unregisterBackgroundThreadListener(final BundleEventListener listener,
+ final String... events) {
+ unregisterListener(mBackgroundThreadListeners, listener, events);
+ }
+
+ private List<NativeEventListener> getNativeListeners(final String type) {
+ final List<NativeEventListener> listeners;
+ synchronized (mGeckoThreadNativeListeners) {
+ listeners = mGeckoThreadNativeListeners.get(type);
+ }
+ return listeners;
+ }
+
+ private List<GeckoEventListener> getGeckoListeners(final String type) {
+ final List<GeckoEventListener> listeners;
+ synchronized (mGeckoThreadJSONListeners) {
+ listeners = mGeckoThreadJSONListeners.get(type);
+ }
+ return listeners;
+ }
+
+ public boolean dispatchEvent(final NativeJSContainer message) {
+ // First try native listeners.
+ final String type = message.optString("type", null);
+ if (type == null) {
+ Log.e(LOGTAG, "JSON message must have a type property");
+ return true; // It may seem odd to return true here, but it's necessary to preserve the correct behavior.
+ }
+
+ final List<NativeEventListener> listeners = getNativeListeners(type);
+
+ final String guid = message.optString(GUID, null);
+ EventCallback callback = null;
+ if (guid != null) {
+ callback = new GeckoEventCallback(guid, type);
+ }
+
+ if (listeners != null) {
+ if (listeners.isEmpty()) {
+ Log.w(LOGTAG, "No listeners for " + type);
+
+ // There were native listeners, and they're gone. Return a failure rather than
+ // looking for JSON listeners. This is an optimization, as we can safely assume
+ // that an event which previously had native listeners will never have JSON
+ // listeners.
+ return false;
+ }
+ try {
+ for (final NativeEventListener listener : listeners) {
+ listener.handleMessage(type, message, callback);
+ }
+ } catch (final NativeJSObject.InvalidPropertyException e) {
+ Log.e(LOGTAG, "Exception occurred while handling " + type, e);
+ }
+ // If we found native listeners, we assume we don't have any other types of listeners
+ // and return early. This assumption is checked when registering listeners.
+ return true;
+ }
+
+ // Check for thread event listeners before checking for JSON event listeners,
+ // because checking for thread listeners is very fast and doesn't require us to
+ // serialize into JSON and construct a JSONObject.
+ if (dispatchToThreads(type, message, /* bundle */ null, callback)) {
+ // If we found thread listeners, we assume we don't have any other types of listeners
+ // and return early. This assumption is checked when registering listeners.
+ return true;
+ }
+
+ try {
+ // If we didn't find native listeners, try JSON listeners.
+ return dispatchEvent(new JSONObject(message.toString()), callback);
+ } catch (final JSONException e) {
+ Log.e(LOGTAG, "Cannot parse JSON", e);
+ } catch (final UnsupportedOperationException e) {
+ Log.e(LOGTAG, "Cannot convert message to JSON", e);
+ }
+
+ return true;
+ }
+
+ /**
+ * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * @param message Bundle message with "type" value specifying the event type.
+ */
+ public void dispatch(final Bundle message) {
+ dispatch(message, /* callback */ null);
+ }
+
+ /**
+ * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * @param message Bundle message with "type" value specifying the event type.
+ * @param callback Optional object for callbacks from events.
+ */
+ public void dispatch(final Bundle message, final EventCallback callback) {
+ if (message == null) {
+ throw new IllegalArgumentException("Null message");
+ }
+
+ final String type = message.getCharSequence("type").toString();
+ if (type == null) {
+ Log.e(LOGTAG, "Bundle message must have a type property");
+ return;
+ }
+ dispatchToThreads(type, /* js */ null, message, /* callback */ callback);
+ }
+
+ /**
+ * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * @param type Event type
+ * @param message Bundle message
+ */
+ public void dispatch(final String type, final Bundle message) {
+ dispatch(type, message, /* callback */ null);
+ }
+
+ /**
+ * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * @param type Event type
+ * @param message Bundle message
+ * @param callback Optional object for callbacks from events.
+ */
+ public void dispatch(final String type, final Bundle message, final EventCallback callback) {
+ dispatchToThreads(type, /* js */ null, message, /* callback */ callback);
+ }
+
+ private boolean dispatchToThreads(final String type,
+ final NativeJSObject jsMessage,
+ final Bundle bundleMessage,
+ final EventCallback callback) {
+ if (dispatchToThread(type, jsMessage, bundleMessage, callback,
+ mUiThreadListeners, ThreadUtils.getUiHandler())) {
+ return true;
+ }
+
+ if (dispatchToThread(type, jsMessage, bundleMessage, callback,
+ mBackgroundThreadListeners, ThreadUtils.getBackgroundHandler())) {
+ return true;
+ }
+
+ if (jsMessage == null) {
+ Log.w(LOGTAG, "No listeners for " + type + " in dispatchToThreads");
+ }
+
+ if (!AppConstants.RELEASE_OR_BETA && jsMessage == null) {
+ // We're dispatching a Bundle message. Because Gecko thread listeners are not
+ // supported for Bundle messages, do a sanity check to make sure we don't have
+ // matching Gecko thread listeners.
+ boolean hasGeckoListener = false;
+ synchronized (mGeckoThreadNativeListeners) {
+ hasGeckoListener |= mGeckoThreadNativeListeners.containsKey(type);
+ }
+ synchronized (mGeckoThreadJSONListeners) {
+ hasGeckoListener |= mGeckoThreadJSONListeners.containsKey(type);
+ }
+ if (hasGeckoListener) {
+ throw new IllegalStateException(
+ "Dispatching Bundle message to Gecko listener " + type);
+ }
+ }
+
+ return false;
+ }
+
+ private boolean dispatchToThread(final String type,
+ final NativeJSObject jsMessage,
+ final Bundle bundleMessage,
+ final EventCallback callback,
+ final Map<String, List<BundleEventListener>> listenersMap,
+ final Handler thread) {
+ // We need to hold the lock throughout dispatching, to ensure the listeners list
+ // is consistent, while we iterate over it. We don't have to worry about listeners
+ // running for a long time while we have the lock, because the listeners will run
+ // on a separate thread.
+ synchronized (listenersMap) {
+ final List<BundleEventListener> listeners = listenersMap.get(type);
+ if (listeners == null) {
+ return false;
+ }
+
+ if (listeners.isEmpty()) {
+ Log.w(LOGTAG, "No listeners for " + type + " in dispatchToThread");
+
+ // There were native listeners, and they're gone.
+ return false;
+ }
+
+ final Bundle messageAsBundle;
+ try {
+ messageAsBundle = jsMessage != null ? jsMessage.toBundle() : bundleMessage;
+ } catch (final NativeJSObject.InvalidPropertyException e) {
+ Log.e(LOGTAG, "Exception occurred while handling " + type, e);
+ return true;
+ }
+
+ // Event listeners will call | callback.sendError | if applicable.
+ for (final BundleEventListener listener : listeners) {
+ thread.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.handleMessage(type, messageAsBundle, callback);
+ }
+ });
+ }
+ return true;
+ }
+ }
+
+ public boolean dispatchEvent(final JSONObject message, final EventCallback callback) {
+ // {
+ // "type": "value",
+ // "event_specific": "value",
+ // ...
+ try {
+ final String type = message.getString("type");
+
+ final List<GeckoEventListener> listeners = getGeckoListeners(type);
+
+ if (listeners == null || listeners.isEmpty()) {
+ Log.w(LOGTAG, "No listeners for " + type + " in dispatchEvent");
+
+ return false;
+ }
+
+ for (final GeckoEventListener listener : listeners) {
+ listener.handleMessage(type, message);
+ }
+ } catch (final JSONException e) {
+ Log.e(LOGTAG, "handleGeckoMessage throws " + e, e);
+ }
+
+ return true;
+ }
+
+ @RobocopTarget
+ @Deprecated
+ public static void sendResponse(JSONObject message, Object response) {
+ sendResponseHelper(STATUS_SUCCESS, message, response);
+ }
+
+ @Deprecated
+ public static void sendError(JSONObject message, Object response) {
+ sendResponseHelper(STATUS_ERROR, message, response);
+ }
+
+ @Deprecated
+ private static void sendResponseHelper(String status, JSONObject message, Object response) {
+ try {
+ final String topic = message.getString("type") + ":Response";
+ final JSONObject wrapper = new JSONObject();
+ wrapper.put(GUID, message.getString(GUID));
+ wrapper.put("status", status);
+ wrapper.put("response", response);
+
+ if (ThreadUtils.isOnGeckoThread()) {
+ GeckoAppShell.syncNotifyObservers(topic, wrapper.toString());
+ } else {
+ GeckoAppShell.notifyObservers(topic, wrapper.toString(),
+ GeckoThread.State.PROFILE_READY);
+ }
+ } catch (final JSONException e) {
+ Log.e(LOGTAG, "Unable to send response", e);
+ }
+ }
+
+ /* package */ static class GeckoEventCallback implements EventCallback {
+ private final String guid;
+ private final String type;
+ private boolean sent;
+
+ public GeckoEventCallback(final String guid, final String type) {
+ this.guid = guid;
+ this.type = type;
+ }
+
+ @Override
+ public void sendSuccess(final Object response) {
+ sendResponse(STATUS_SUCCESS, response);
+ }
+
+ @Override
+ public void sendError(final Object response) {
+ sendResponse(STATUS_ERROR, response);
+ }
+
+ private void sendResponse(final String status, final Object response) {
+ if (sent) {
+ throw new IllegalStateException("Callback has already been executed for type=" +
+ type + ", guid=" + guid);
+ }
+
+ sent = true;
+
+ try {
+ final String topic = type + ":Response";
+ final JSONObject wrapper = new JSONObject();
+ wrapper.put(GUID, guid);
+ wrapper.put("status", status);
+ wrapper.put("response", response);
+
+ if (ThreadUtils.isOnGeckoThread()) {
+ GeckoAppShell.syncNotifyObservers(topic, wrapper.toString());
+ } else {
+ GeckoAppShell.notifyObservers(topic, wrapper.toString(),
+ GeckoThread.State.PROFILE_READY);
+ }
+ } catch (final JSONException e) {
+ Log.e(LOGTAG, "Unable to send response for: " + type, e);
+ }
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAccessibility.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAccessibility.java
new file mode 100644
index 000000000..8d4c0fb2a
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAccessibility.java
@@ -0,0 +1,410 @@
+/* -*- 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.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.UIAsyncTask;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+
+import com.googlecode.eyesfree.braille.selfbraille.SelfBrailleClient;
+import com.googlecode.eyesfree.braille.selfbraille.WriteData;
+
+public class GeckoAccessibility {
+ private static final String LOGTAG = "GeckoAccessibility";
+ private static final int VIRTUAL_ENTRY_POINT_BEFORE = 1;
+ private static final int VIRTUAL_CURSOR_POSITION = 2;
+ private static final int VIRTUAL_ENTRY_POINT_AFTER = 3;
+
+ private static boolean sEnabled;
+ // Used to store the JSON message and populate the event later in the code path.
+ private static JSONObject sHoverEnter;
+ private static AccessibilityNodeInfo sVirtualCursorNode;
+ private static int sCurrentNode;
+
+ // This is the number Brailleback uses to start indexing routing keys.
+ private static final int BRAILLE_CLICK_BASE_INDEX = -275000000;
+ private static SelfBrailleClient sSelfBrailleClient;
+
+ public static void updateAccessibilitySettings (final Context context) {
+ new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
+ @Override
+ public Void doInBackground() {
+ JSONObject ret = new JSONObject();
+ sEnabled = false;
+ AccessibilityManager accessibilityManager =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ sEnabled = accessibilityManager.isEnabled() && accessibilityManager.isTouchExplorationEnabled();
+ if (Versions.feature16Plus && sEnabled && sSelfBrailleClient == null) {
+ sSelfBrailleClient = new SelfBrailleClient(context, false);
+ }
+
+ try {
+ ret.put("enabled", sEnabled);
+ } catch (Exception ex) {
+ Log.e(LOGTAG, "Error building JSON arguments for Accessibility:Settings:", ex);
+ }
+
+ GeckoAppShell.notifyObservers("Accessibility:Settings", ret.toString());
+ return null;
+ }
+
+ @Override
+ public void onPostExecute(Void args) {
+ final GeckoAppShell.GeckoInterface geckoInterface = GeckoAppShell.getGeckoInterface();
+ if (geckoInterface == null) {
+ return;
+ }
+ geckoInterface.setAccessibilityEnabled(sEnabled);
+ }
+ }.execute();
+ }
+
+ private static void populateEventFromJSON (AccessibilityEvent event, JSONObject message) {
+ final JSONArray textArray = message.optJSONArray("text");
+ if (textArray != null) {
+ for (int i = 0; i < textArray.length(); i++)
+ event.getText().add(textArray.optString(i));
+ }
+
+ event.setContentDescription(message.optString("description"));
+ event.setEnabled(message.optBoolean("enabled", true));
+ event.setChecked(message.optBoolean("checked"));
+ event.setPassword(message.optBoolean("password"));
+ event.setAddedCount(message.optInt("addedCount", -1));
+ event.setRemovedCount(message.optInt("removedCount", -1));
+ event.setFromIndex(message.optInt("fromIndex", -1));
+ event.setItemCount(message.optInt("itemCount", -1));
+ event.setCurrentItemIndex(message.optInt("currentItemIndex", -1));
+ event.setBeforeText(message.optString("beforeText"));
+ event.setToIndex(message.optInt("toIndex", -1));
+ event.setScrollable(message.optBoolean("scrollable"));
+ event.setScrollX(message.optInt("scrollX", -1));
+ event.setScrollY(message.optInt("scrollY", -1));
+ event.setMaxScrollX(message.optInt("maxScrollX", -1));
+ event.setMaxScrollY(message.optInt("maxScrollY", -1));
+ }
+
+ private static void sendDirectAccessibilityEvent(int eventType, JSONObject message) {
+ final Context context = GeckoAppShell.getApplicationContext();
+ final AccessibilityEvent accEvent = AccessibilityEvent.obtain(eventType);
+ accEvent.setClassName(GeckoAccessibility.class.getName());
+ accEvent.setPackageName(context.getPackageName());
+ populateEventFromJSON(accEvent, message);
+ AccessibilityManager accessibilityManager =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ try {
+ accessibilityManager.sendAccessibilityEvent(accEvent);
+ } catch (IllegalStateException e) {
+ // Accessibility is off.
+ }
+ }
+
+ public static boolean isEnabled() {
+ return sEnabled;
+ }
+
+ public static void sendAccessibilityEvent (final JSONObject message) {
+ if (!sEnabled)
+ return;
+
+ final int eventType = message.optInt("eventType", -1);
+ if (eventType < 0) {
+ Log.e(LOGTAG, "No accessibility event type provided");
+ return;
+ }
+
+ sendAccessibilityEvent(message, eventType);
+ }
+
+ public static void sendAccessibilityEvent (final JSONObject message, final int eventType) {
+ if (!sEnabled)
+ return;
+
+ final String exitView = message.optString("exitView");
+ if (exitView.equals("moveNext")) {
+ sCurrentNode = VIRTUAL_ENTRY_POINT_AFTER;
+ } else if (exitView.equals("movePrevious")) {
+ sCurrentNode = VIRTUAL_ENTRY_POINT_BEFORE;
+ } else {
+ sCurrentNode = VIRTUAL_CURSOR_POSITION;
+ }
+
+ if (Versions.preJB) {
+ // Before Jelly Bean we send events directly from here while spoofing the source by setting
+ // the package and class name manually.
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ sendDirectAccessibilityEvent(eventType, message);
+ }
+ });
+ } else {
+ // In Jelly Bean we populate an AccessibilityNodeInfo with the minimal amount of data to have
+ // it work with TalkBack.
+ final View view = GeckoAppShell.getLayerView();
+ if (view == null)
+ return;
+
+ if (sVirtualCursorNode == null)
+ sVirtualCursorNode = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
+ sVirtualCursorNode.setEnabled(message.optBoolean("enabled", true));
+ sVirtualCursorNode.setClickable(message.optBoolean("clickable"));
+ sVirtualCursorNode.setCheckable(message.optBoolean("checkable"));
+ sVirtualCursorNode.setChecked(message.optBoolean("checked"));
+ sVirtualCursorNode.setPassword(message.optBoolean("password"));
+
+ final JSONArray textArray = message.optJSONArray("text");
+ StringBuilder sb = new StringBuilder();
+ if (textArray != null && textArray.length() > 0) {
+ sb.append(textArray.optString(0));
+ for (int i = 1; i < textArray.length(); i++) {
+ sb.append(" ").append(textArray.optString(i));
+ }
+ sVirtualCursorNode.setText(sb.toString());
+ }
+ sVirtualCursorNode.setContentDescription(message.optString("description"));
+
+ JSONObject bounds = message.optJSONObject("bounds");
+ if (bounds != null) {
+ Rect relativeBounds = new Rect(bounds.optInt("left"), bounds.optInt("top"),
+ bounds.optInt("right"), bounds.optInt("bottom"));
+ sVirtualCursorNode.setBoundsInParent(relativeBounds);
+ int[] locationOnScreen = new int[2];
+ view.getLocationOnScreen(locationOnScreen);
+ Rect screenBounds = new Rect(relativeBounds);
+ screenBounds.offset(locationOnScreen[0], locationOnScreen[1]);
+ sVirtualCursorNode.setBoundsInScreen(screenBounds);
+ }
+
+ final JSONObject braille = message.optJSONObject("brailleOutput");
+ if (braille != null) {
+ sendBrailleText(view, braille.optString("text"),
+ braille.optInt("selectionStart"), braille.optInt("selectionEnd"));
+ }
+
+ if (eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER) {
+ sHoverEnter = message;
+ }
+
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+ event.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
+ event.setClassName(GeckoAccessibility.class.getName());
+ if (eventType == AccessibilityEvent.TYPE_ANNOUNCEMENT ||
+ eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
+ event.setSource(view, View.NO_ID);
+ } else {
+ event.setSource(view, VIRTUAL_CURSOR_POSITION);
+ }
+ populateEventFromJSON(event, message);
+ ((ViewParent) view).requestSendAccessibilityEvent(view, event);
+ }
+ });
+
+ }
+ }
+
+ private static void sendBrailleText(final View view, final String text, final int selectionStart, final int selectionEnd) {
+ AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
+ WriteData data = WriteData.forInfo(info);
+ data.setText(text);
+ // Set either the focus blink or the current caret position/selection
+ data.setSelectionStart(selectionStart);
+ data.setSelectionEnd(selectionEnd);
+ sSelfBrailleClient.write(data);
+ }
+
+ public static void setDelegate(View view) {
+ // Only use this delegate in Jelly Bean.
+ if (Versions.feature16Plus) {
+ view.setAccessibilityDelegate(new GeckoAccessibilityDelegate());
+ view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+ }
+
+ public static void setAccessibilityManagerListeners(final Context context) {
+ AccessibilityManager accessibilityManager =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+
+ accessibilityManager.addAccessibilityStateChangeListener(new AccessibilityManager.AccessibilityStateChangeListener() {
+ @Override
+ public void onAccessibilityStateChanged(boolean enabled) {
+ updateAccessibilitySettings(context);
+ }
+ });
+
+ if (Versions.feature19Plus) {
+ accessibilityManager.addTouchExplorationStateChangeListener(new AccessibilityManager.TouchExplorationStateChangeListener() {
+ @Override
+ public void onTouchExplorationStateChanged(boolean enabled) {
+ updateAccessibilitySettings(context);
+ }
+ });
+ }
+ }
+
+ public static void onLayerViewFocusChanged(boolean gainFocus) {
+ if (sEnabled)
+ GeckoAppShell.notifyObservers("Accessibility:Focus", gainFocus ? "true" : "false");
+ }
+
+ public static class GeckoAccessibilityDelegate extends View.AccessibilityDelegate {
+ AccessibilityNodeProvider mAccessibilityNodeProvider;
+
+ @Override
+ public AccessibilityNodeProvider getAccessibilityNodeProvider(final View host) {
+ if (mAccessibilityNodeProvider == null)
+ // The accessibility node structure for web content consists of 3 LayerView child nodes:
+ // 1. VIRTUAL_ENTRY_POINT_BEFORE: Represents the entry point before the LayerView.
+ // 2. VIRTUAL_CURSOR_POSITION: Represents the current position of the virtual cursor.
+ // 3. VIRTUAL_ENTRY_POINT_AFTER: Represents the entry point after the LayerView.
+ mAccessibilityNodeProvider = new AccessibilityNodeProvider() {
+ @Override
+ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualDescendantId) {
+ AccessibilityNodeInfo info = (virtualDescendantId == VIRTUAL_CURSOR_POSITION && sVirtualCursorNode != null) ?
+ AccessibilityNodeInfo.obtain(sVirtualCursorNode) :
+ AccessibilityNodeInfo.obtain(host, virtualDescendantId);
+
+ switch (virtualDescendantId) {
+ case View.NO_ID:
+ // This is the parent LayerView node, populate it with children.
+ onInitializeAccessibilityNodeInfo(host, info);
+ info.addChild(host, VIRTUAL_ENTRY_POINT_BEFORE);
+ info.addChild(host, VIRTUAL_CURSOR_POSITION);
+ info.addChild(host, VIRTUAL_ENTRY_POINT_AFTER);
+ break;
+ default:
+ info.setParent(host);
+ info.setSource(host, virtualDescendantId);
+ info.setVisibleToUser(host.isShown());
+ info.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
+ info.setClassName(host.getClass().getName());
+ info.setEnabled(true);
+ info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+ info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+ info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
+ info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
+ info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
+ info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+ info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
+ info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
+ info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER |
+ AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD |
+ AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE |
+ AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH);
+ break;
+ }
+ return info;
+ }
+
+ @Override
+ public boolean performAction (int virtualViewId, int action, Bundle arguments) {
+ if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) {
+ // The accessibility focus is permanently on the middle node, VIRTUAL_CURSOR_POSITION.
+ // When we enter the view forward or backward we just ask Gecko to get focus, keeping the current position.
+ if (virtualViewId == VIRTUAL_CURSOR_POSITION && sHoverEnter != null) {
+ GeckoAccessibility.sendAccessibilityEvent(sHoverEnter, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+ } else {
+ GeckoAppShell.notifyObservers("Accessibility:Focus", "true");
+ }
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_CLICK && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ GeckoAppShell.notifyObservers("Accessibility:ActivateObject", null);
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_LONG_CLICK && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ GeckoAppShell.notifyObservers("Accessibility:LongPress", null);
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ GeckoAppShell.notifyObservers("Accessibility:ScrollForward", null);
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ GeckoAppShell.notifyObservers("Accessibility:ScrollBackward", null);
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ String traversalRule = "";
+ if (arguments != null) {
+ traversalRule = arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
+ }
+ GeckoAppShell.notifyObservers("Accessibility:NextObject", traversalRule);
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ String traversalRule = "";
+ if (arguments != null) {
+ traversalRule = arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
+ }
+ GeckoAppShell.notifyObservers("Accessibility:PreviousObject", traversalRule);
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY &&
+ virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ // XXX: Self brailling gives this action with a bogus argument instead of an actual click action;
+ // the argument value is the BRAILLE_CLICK_BASE_INDEX - the index of the routing key that was hit.
+ // Other negative values are used by ChromeVox, but we don't support them.
+ // FAKE_GRANULARITY_READ_CURRENT = -1
+ // FAKE_GRANULARITY_READ_TITLE = -2
+ // FAKE_GRANULARITY_STOP_SPEECH = -3
+ // FAKE_GRANULARITY_CHANGE_SHIFTER = -4
+ int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
+ if (granularity <= BRAILLE_CLICK_BASE_INDEX) {
+ int keyIndex = BRAILLE_CLICK_BASE_INDEX - granularity;
+ JSONObject activationData = new JSONObject();
+ try {
+ activationData.put("keyIndex", keyIndex);
+ } catch (JSONException e) {
+ return true;
+ }
+ GeckoAppShell.notifyObservers("Accessibility:ActivateObject", activationData.toString());
+ } else if (granularity > 0) {
+ JSONObject movementData = new JSONObject();
+ try {
+ movementData.put("direction", "Next");
+ movementData.put("granularity", granularity);
+ } catch (JSONException e) {
+ return true;
+ }
+ GeckoAppShell.notifyObservers("Accessibility:MoveByGranularity", movementData.toString());
+ }
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY &&
+ virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ JSONObject movementData = new JSONObject();
+ int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
+ try {
+ movementData.put("direction", "Previous");
+ movementData.put("granularity", granularity);
+ } catch (JSONException e) {
+ return true;
+ }
+ if (granularity > 0) {
+ GeckoAppShell.notifyObservers("Accessibility:MoveByGranularity", movementData.toString());
+ }
+ return true;
+ }
+ return host.performAccessibilityAction(action, arguments);
+ }
+ };
+
+ return mAccessibilityNodeProvider;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java
new file mode 100644
index 000000000..a80212639
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java
@@ -0,0 +1,2239 @@
+/* -*- 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;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.net.MalformedURLException;
+import java.net.Proxy;
+import java.net.URLConnection;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
+
+import android.annotation.SuppressLint;
+import org.mozilla.gecko.annotation.JNITarget;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.gfx.BitmapUtils;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.gfx.PanZoomController;
+import org.mozilla.gecko.permissions.Permissions;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoRequest;
+import org.mozilla.gecko.util.HardwareCodecCapabilityUtils;
+import org.mozilla.gecko.util.HardwareUtils;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSContainer;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.ProxySelector;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.pm.Signature;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.SurfaceTexture;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.hardware.Camera;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.location.Criteria;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.Display;
+import android.view.HapticFeedbackConstants;
+import android.view.Surface;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import android.webkit.MimeTypeMap;
+import android.widget.AbsoluteLayout;
+
+public class GeckoAppShell
+{
+ private static final String LOGTAG = "GeckoAppShell";
+
+ // We have static members only.
+ private GeckoAppShell() { }
+
+ private static final CrashHandler CRASH_HANDLER = new CrashHandler() {
+ @Override
+ protected String getAppPackageName() {
+ return AppConstants.ANDROID_PACKAGE_NAME;
+ }
+
+ @Override
+ protected Context getAppContext() {
+ return sContextGetter != null ? getApplicationContext() : null;
+ }
+
+ @Override
+ protected Bundle getCrashExtras(final Thread thread, final Throwable exc) {
+ final Bundle extras = super.getCrashExtras(thread, exc);
+
+ extras.putString("ProductName", AppConstants.MOZ_APP_BASENAME);
+ extras.putString("ProductID", AppConstants.MOZ_APP_ID);
+ extras.putString("Version", AppConstants.MOZ_APP_VERSION);
+ extras.putString("BuildID", AppConstants.MOZ_APP_BUILDID);
+ extras.putString("Vendor", AppConstants.MOZ_APP_VENDOR);
+ extras.putString("ReleaseChannel", AppConstants.MOZ_UPDATE_CHANNEL);
+ return extras;
+ }
+
+ @Override
+ public void uncaughtException(final Thread thread, final Throwable exc) {
+ if (GeckoThread.isState(GeckoThread.State.EXITING) ||
+ GeckoThread.isState(GeckoThread.State.EXITED)) {
+ // We've called System.exit. All exceptions after this point are Android
+ // berating us for being nasty to it.
+ return;
+ }
+
+ super.uncaughtException(thread, exc);
+ }
+
+ @Override
+ public boolean reportException(final Thread thread, final Throwable exc) {
+ try {
+ if (exc instanceof OutOfMemoryError) {
+ SharedPreferences prefs = getSharedPreferences();
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(PREFS_OOM_EXCEPTION, true);
+
+ // Synchronously write to disk so we know it's done before we
+ // shutdown
+ editor.commit();
+ }
+
+ reportJavaCrash(exc, getExceptionStackTrace(exc));
+
+ } catch (final Throwable e) {
+ }
+
+ // reportJavaCrash should have caused us to hard crash. If we're still here,
+ // it probably means Gecko is not loaded, and we should do something else.
+ if (AppConstants.MOZ_CRASHREPORTER && AppConstants.MOZILLA_OFFICIAL) {
+ // Only use Java crash reporter if enabled on official build.
+ return super.reportException(thread, exc);
+ }
+ return false;
+ }
+ };
+
+ public static CrashHandler ensureCrashHandling() {
+ // Crash handling is automatically enabled when GeckoAppShell is loaded.
+ return CRASH_HANDLER;
+ }
+
+ private static volatile boolean locationHighAccuracyEnabled;
+
+ // See also HardwareUtils.LOW_MEMORY_THRESHOLD_MB.
+ private static final int HIGH_MEMORY_DEVICE_THRESHOLD_MB = 768;
+
+ static private int sDensityDpi;
+ static private int sScreenDepth;
+
+ /* Is the value in sVibrationEndTime valid? */
+ private static boolean sVibrationMaybePlaying;
+
+ /* Time (in System.nanoTime() units) when the currently-playing vibration
+ * is scheduled to end. This value is valid only when
+ * sVibrationMaybePlaying is true. */
+ private static long sVibrationEndTime;
+
+ private static Sensor gAccelerometerSensor;
+ private static Sensor gLinearAccelerometerSensor;
+ private static Sensor gGyroscopeSensor;
+ private static Sensor gOrientationSensor;
+ private static Sensor gProximitySensor;
+ private static Sensor gLightSensor;
+ private static Sensor gRotationVectorSensor;
+ private static Sensor gGameRotationVectorSensor;
+
+ private static final String GECKOREQUEST_RESPONSE_KEY = "response";
+ private static final String GECKOREQUEST_ERROR_KEY = "error";
+
+ /*
+ * Keep in sync with constants found here:
+ * http://dxr.mozilla.org/mozilla-central/source/uriloader/base/nsIWebProgressListener.idl
+ */
+ static public final int WPL_STATE_START = 0x00000001;
+ static public final int WPL_STATE_STOP = 0x00000010;
+ static public final int WPL_STATE_IS_DOCUMENT = 0x00020000;
+ static public final int WPL_STATE_IS_NETWORK = 0x00040000;
+
+ /* Keep in sync with constants found here:
+ http://dxr.mozilla.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
+ */
+ static public final int LINK_TYPE_UNKNOWN = 0;
+ static public final int LINK_TYPE_ETHERNET = 1;
+ static public final int LINK_TYPE_USB = 2;
+ static public final int LINK_TYPE_WIFI = 3;
+ static public final int LINK_TYPE_WIMAX = 4;
+ static public final int LINK_TYPE_2G = 5;
+ static public final int LINK_TYPE_3G = 6;
+ static public final int LINK_TYPE_4G = 7;
+
+ public static final String PREFS_OOM_EXCEPTION = "OOMException";
+
+ /* The Android-side API: API methods that Android calls */
+
+ // helper methods
+ @WrapForJNI
+ /* package */ static native void reportJavaCrash(Throwable exc, String stackTrace);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ public static native void notifyUriVisited(String uri);
+
+ private static LayerView sLayerView;
+ private static Rect sScreenSize;
+
+ public static void setLayerView(LayerView lv) {
+ if (sLayerView == lv) {
+ return;
+ }
+ sLayerView = lv;
+ }
+
+ @RobocopTarget
+ public static LayerView getLayerView() {
+ return sLayerView;
+ }
+
+ /**
+ * Sends an asynchronous request to Gecko.
+ *
+ * The response data will be passed to {@link GeckoRequest#onResponse(NativeJSObject)} if the
+ * request succeeds; otherwise, {@link GeckoRequest#onError()} will fire.
+ *
+ * It can be called from any thread. The GeckoRequest callbacks will be executed on the Gecko thread.
+ *
+ * @param request The request to dispatch. Cannot be null.
+ */
+ @RobocopTarget
+ public static void sendRequestToGecko(final GeckoRequest request) {
+ final String responseMessage = "Gecko:Request" + request.getId();
+
+ EventDispatcher.getInstance().registerGeckoThreadListener(new NativeEventListener() {
+ @Override
+ public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(this, event);
+ if (!message.has(GECKOREQUEST_RESPONSE_KEY)) {
+ request.onError(message.getObject(GECKOREQUEST_ERROR_KEY));
+ return;
+ }
+ request.onResponse(message.getObject(GECKOREQUEST_RESPONSE_KEY));
+ }
+ }, responseMessage);
+
+ notifyObservers(request.getName(), request.getData());
+ }
+
+ // Synchronously notify a Gecko observer; must be called from Gecko thread.
+ @WrapForJNI(calledFrom = "gecko")
+ public static native void syncNotifyObservers(String topic, String data);
+
+ @WrapForJNI(stubName = "NotifyObservers", dispatchTo = "gecko")
+ private static native void nativeNotifyObservers(String topic, String data);
+
+ @RobocopTarget
+ public static void notifyObservers(final String topic, final String data) {
+ notifyObservers(topic, data, GeckoThread.State.RUNNING);
+ }
+
+ public static void notifyObservers(final String topic, final String data, final GeckoThread.State state) {
+ if (GeckoThread.isStateAtLeast(state)) {
+ nativeNotifyObservers(topic, data);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ state, GeckoAppShell.class, "nativeNotifyObservers",
+ String.class, topic, String.class, data);
+ }
+ }
+
+ /*
+ * The Gecko-side API: API methods that Gecko calls
+ */
+
+ @WrapForJNI(exceptionMode = "ignore")
+ private static String getExceptionStackTrace(Throwable e) {
+ return CrashHandler.getExceptionStackTrace(CrashHandler.getRootException(e));
+ }
+
+ @WrapForJNI(exceptionMode = "ignore")
+ private static void handleUncaughtException(Throwable e) {
+ CRASH_HANDLER.uncaughtException(null, e);
+ }
+
+ @WrapForJNI
+ public static void openWindowForNotification() {
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
+
+ getApplicationContext().startActivity(intent);
+ }
+
+ private static float getLocationAccuracy(Location location) {
+ float radius = location.getAccuracy();
+ return (location.hasAccuracy() && radius > 0) ? radius : 1001;
+ }
+
+ @SuppressLint("MissingPermission") // Permissions are explicitly checked for in enableLocation()
+ private static Location getLastKnownLocation(LocationManager lm) {
+ Location lastKnownLocation = null;
+ List<String> providers = lm.getAllProviders();
+
+ for (String provider : providers) {
+ Location location = lm.getLastKnownLocation(provider);
+ if (location == null) {
+ continue;
+ }
+
+ if (lastKnownLocation == null) {
+ lastKnownLocation = location;
+ continue;
+ }
+
+ long timeDiff = location.getTime() - lastKnownLocation.getTime();
+ if (timeDiff > 0 ||
+ (timeDiff == 0 &&
+ getLocationAccuracy(location) < getLocationAccuracy(lastKnownLocation))) {
+ lastKnownLocation = location;
+ }
+ }
+
+ return lastKnownLocation;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ @SuppressLint("MissingPermission") // Permissions are explicitly checked for within this method
+ private static void enableLocation(final boolean enable) {
+ final Runnable requestLocation = new Runnable() {
+ @Override
+ public void run() {
+ LocationManager lm = getLocationManager(getApplicationContext());
+ if (lm == null) {
+ return;
+ }
+
+ if (!enable) {
+ lm.removeUpdates(getLocationListener());
+ return;
+ }
+
+ Location lastKnownLocation = getLastKnownLocation(lm);
+ if (lastKnownLocation != null) {
+ getLocationListener().onLocationChanged(lastKnownLocation);
+ }
+
+ Criteria criteria = new Criteria();
+ criteria.setSpeedRequired(false);
+ criteria.setBearingRequired(false);
+ criteria.setAltitudeRequired(false);
+ if (locationHighAccuracyEnabled) {
+ criteria.setAccuracy(Criteria.ACCURACY_FINE);
+ criteria.setCostAllowed(true);
+ criteria.setPowerRequirement(Criteria.POWER_HIGH);
+ } else {
+ criteria.setAccuracy(Criteria.ACCURACY_COARSE);
+ criteria.setCostAllowed(false);
+ criteria.setPowerRequirement(Criteria.POWER_LOW);
+ }
+
+ String provider = lm.getBestProvider(criteria, true);
+ if (provider == null)
+ return;
+
+ Looper l = Looper.getMainLooper();
+ lm.requestLocationUpdates(provider, 100, 0.5f, getLocationListener(), l);
+ }
+ };
+
+ Permissions
+ .from((Activity) getContext())
+ .withPermissions(Manifest.permission.ACCESS_FINE_LOCATION)
+ .onUIThread()
+ .doNotPromptIf(!enable)
+ .run(requestLocation);
+ }
+
+ private static LocationManager getLocationManager(Context context) {
+ try {
+ return (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
+ } catch (NoSuchFieldError e) {
+ // Some Tegras throw exceptions about missing the CONTROL_LOCATION_UPDATES permission,
+ // which allows enabling/disabling location update notifications from the cell radio.
+ // CONTROL_LOCATION_UPDATES is not for use by normal applications, but we might be
+ // hitting this problem if the Tegras are confused about missing cell radios.
+ Log.e(LOGTAG, "LOCATION_SERVICE not found?!", e);
+ return null;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void enableLocationHighAccuracy(final boolean enable) {
+ locationHighAccuracyEnabled = enable;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean setAlarm(int aSeconds, int aNanoSeconds) {
+ AlarmManager am = (AlarmManager)
+ getApplicationContext().getSystemService(Context.ALARM_SERVICE);
+
+ Intent intent = new Intent(getApplicationContext(), AlarmReceiver.class);
+ PendingIntent pi = PendingIntent.getBroadcast(
+ getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ // AlarmManager only supports millisecond precision
+ long time = ((long) aSeconds * 1000) + ((long) aNanoSeconds / 1_000_000L);
+ am.setExact(AlarmManager.RTC_WAKEUP, time, pi);
+
+ return true;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void disableAlarm() {
+ AlarmManager am = (AlarmManager)
+ getApplicationContext().getSystemService(Context.ALARM_SERVICE);
+
+ Intent intent = new Intent(getApplicationContext(), AlarmReceiver.class);
+ PendingIntent pi = PendingIntent.getBroadcast(
+ getApplicationContext(), 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ am.cancel(pi);
+ }
+
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ /* package */ static native void onSensorChanged(int hal_type, float x, float y, float z,
+ float w, int accuracy, long time);
+
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ /* package */ static native void onLocationChanged(double latitude, double longitude,
+ double altitude, float accuracy,
+ float bearing, float speed, long time);
+
+ private static class DefaultListeners
+ implements SensorEventListener, LocationListener, NotificationListener {
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ }
+
+ private static int HalSensorAccuracyFor(int androidAccuracy) {
+ switch (androidAccuracy) {
+ case SensorManager.SENSOR_STATUS_UNRELIABLE:
+ return GeckoHalDefines.SENSOR_ACCURACY_UNRELIABLE;
+ case SensorManager.SENSOR_STATUS_ACCURACY_LOW:
+ return GeckoHalDefines.SENSOR_ACCURACY_LOW;
+ case SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM:
+ return GeckoHalDefines.SENSOR_ACCURACY_MED;
+ case SensorManager.SENSOR_STATUS_ACCURACY_HIGH:
+ return GeckoHalDefines.SENSOR_ACCURACY_HIGH;
+ }
+ return GeckoHalDefines.SENSOR_ACCURACY_UNKNOWN;
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent s) {
+ int sensor_type = s.sensor.getType();
+ int hal_type = 0;
+ float x = 0.0f, y = 0.0f, z = 0.0f, w = 0.0f;
+ final int accuracy = HalSensorAccuracyFor(s.accuracy);
+ // SensorEvent timestamp is in nanoseconds, Gecko expects microseconds.
+ final long time = s.timestamp / 1000;
+
+ switch (sensor_type) {
+ case Sensor.TYPE_ACCELEROMETER:
+ case Sensor.TYPE_LINEAR_ACCELERATION:
+ case Sensor.TYPE_ORIENTATION:
+ if (sensor_type == Sensor.TYPE_ACCELEROMETER) {
+ hal_type = GeckoHalDefines.SENSOR_ACCELERATION;
+ } else if (sensor_type == Sensor.TYPE_LINEAR_ACCELERATION) {
+ hal_type = GeckoHalDefines.SENSOR_LINEAR_ACCELERATION;
+ } else {
+ hal_type = GeckoHalDefines.SENSOR_ORIENTATION;
+ }
+ x = s.values[0];
+ y = s.values[1];
+ z = s.values[2];
+ break;
+
+ case Sensor.TYPE_GYROSCOPE:
+ hal_type = GeckoHalDefines.SENSOR_GYROSCOPE;
+ x = (float) Math.toDegrees(s.values[0]);
+ y = (float) Math.toDegrees(s.values[1]);
+ z = (float) Math.toDegrees(s.values[2]);
+ break;
+
+ case Sensor.TYPE_PROXIMITY:
+ hal_type = GeckoHalDefines.SENSOR_PROXIMITY;
+ x = s.values[0];
+ z = s.sensor.getMaximumRange();
+ break;
+
+ case Sensor.TYPE_LIGHT:
+ hal_type = GeckoHalDefines.SENSOR_LIGHT;
+ x = s.values[0];
+ break;
+
+ case Sensor.TYPE_ROTATION_VECTOR:
+ case Sensor.TYPE_GAME_ROTATION_VECTOR: // API >= 18
+ hal_type = (sensor_type == Sensor.TYPE_ROTATION_VECTOR ?
+ GeckoHalDefines.SENSOR_ROTATION_VECTOR :
+ GeckoHalDefines.SENSOR_GAME_ROTATION_VECTOR);
+ x = s.values[0];
+ y = s.values[1];
+ z = s.values[2];
+ if (s.values.length >= 4) {
+ w = s.values[3];
+ } else {
+ // s.values[3] was optional in API <= 18, so we need to compute it
+ // The values form a unit quaternion, so we can compute the angle of
+ // rotation purely based on the given 3 values.
+ w = 1.0f - s.values[0] * s.values[0] -
+ s.values[1] * s.values[1] - s.values[2] * s.values[2];
+ w = (w > 0.0f) ? (float) Math.sqrt(w) : 0.0f;
+ }
+ break;
+ }
+
+ GeckoAppShell.onSensorChanged(hal_type, x, y, z, w, accuracy, time);
+ }
+
+ // Geolocation.
+ @Override
+ public void onLocationChanged(Location location) {
+ // No logging here: user-identifying information.
+ GeckoAppShell.onLocationChanged(location.getLatitude(), location.getLongitude(),
+ location.getAltitude(), location.getAccuracy(),
+ location.getBearing(), location.getSpeed(),
+ location.getTime());
+ }
+
+ @Override
+ public void onProviderDisabled(String provider)
+ {
+ }
+
+ @Override
+ public void onProviderEnabled(String provider)
+ {
+ }
+
+ @Override
+ public void onStatusChanged(String provider, int status, Bundle extras)
+ {
+ }
+
+ @Override // NotificationListener
+ public void showNotification(String name, String cookie, String host,
+ String title, String text, String imageUrl) {
+ // Default is to not show the notification, and immediate send close message.
+ GeckoAppShell.onNotificationClose(name, cookie);
+ }
+
+ @Override // NotificationListener
+ public void showPersistentNotification(String name, String cookie, String host,
+ String title, String text, String imageUrl,
+ String data) {
+ // Default is to not show the notification, and immediate send close message.
+ GeckoAppShell.onNotificationClose(name, cookie);
+ }
+
+ @Override // NotificationListener
+ public void closeNotification(String name) {
+ // Do nothing.
+ }
+ }
+
+ private static final DefaultListeners DEFAULT_LISTENERS = new DefaultListeners();
+ private static SensorEventListener sSensorListener = DEFAULT_LISTENERS;
+ private static LocationListener sLocationListener = DEFAULT_LISTENERS;
+ private static NotificationListener sNotificationListener = DEFAULT_LISTENERS;
+
+ public static SensorEventListener getSensorListener() {
+ return sSensorListener;
+ }
+
+ public static void setSensorListener(final SensorEventListener listener) {
+ sSensorListener = (listener != null) ? listener : DEFAULT_LISTENERS;
+ }
+
+ public static LocationListener getLocationListener() {
+ return sLocationListener;
+ }
+
+ public static void setLocationListener(final LocationListener listener) {
+ sLocationListener = (listener != null) ? listener : DEFAULT_LISTENERS;
+ }
+
+ public static NotificationListener getNotificationListener() {
+ return sNotificationListener;
+ }
+
+ public static void setNotificationListener(final NotificationListener listener) {
+ sNotificationListener = (listener != null) ? listener : DEFAULT_LISTENERS;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void enableSensor(int aSensortype) {
+ GeckoInterface gi = getGeckoInterface();
+ if (gi == null) {
+ return;
+ }
+ SensorManager sm = (SensorManager)
+ getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
+
+ switch (aSensortype) {
+ case GeckoHalDefines.SENSOR_GAME_ROTATION_VECTOR:
+ if (gGameRotationVectorSensor == null) {
+ gGameRotationVectorSensor = sm.getDefaultSensor(15);
+ // sm.getDefaultSensor(
+ // Sensor.TYPE_GAME_ROTATION_VECTOR); // API >= 18
+ }
+ if (gGameRotationVectorSensor != null) {
+ sm.registerListener(getSensorListener(),
+ gGameRotationVectorSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ if (gGameRotationVectorSensor != null) {
+ break;
+ }
+ // Fallthrough
+
+ case GeckoHalDefines.SENSOR_ROTATION_VECTOR:
+ if (gRotationVectorSensor == null) {
+ gRotationVectorSensor = sm.getDefaultSensor(
+ Sensor.TYPE_ROTATION_VECTOR);
+ }
+ if (gRotationVectorSensor != null) {
+ sm.registerListener(getSensorListener(),
+ gRotationVectorSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ if (gRotationVectorSensor != null) {
+ break;
+ }
+ // Fallthrough
+
+ case GeckoHalDefines.SENSOR_ORIENTATION:
+ if (gOrientationSensor == null) {
+ gOrientationSensor = sm.getDefaultSensor(
+ Sensor.TYPE_ORIENTATION);
+ }
+ if (gOrientationSensor != null) {
+ sm.registerListener(getSensorListener(),
+ gOrientationSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_ACCELERATION:
+ if (gAccelerometerSensor == null) {
+ gAccelerometerSensor = sm.getDefaultSensor(
+ Sensor.TYPE_ACCELEROMETER);
+ }
+ if (gAccelerometerSensor != null) {
+ sm.registerListener(getSensorListener(),
+ gAccelerometerSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_PROXIMITY:
+ if (gProximitySensor == null) {
+ gProximitySensor = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY);
+ }
+ if (gProximitySensor != null) {
+ sm.registerListener(getSensorListener(),
+ gProximitySensor,
+ SensorManager.SENSOR_DELAY_NORMAL);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_LIGHT:
+ if (gLightSensor == null) {
+ gLightSensor = sm.getDefaultSensor(Sensor.TYPE_LIGHT);
+ }
+ if (gLightSensor != null) {
+ sm.registerListener(getSensorListener(),
+ gLightSensor,
+ SensorManager.SENSOR_DELAY_NORMAL);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_LINEAR_ACCELERATION:
+ if (gLinearAccelerometerSensor == null) {
+ gLinearAccelerometerSensor = sm.getDefaultSensor(
+ Sensor.TYPE_LINEAR_ACCELERATION);
+ }
+ if (gLinearAccelerometerSensor != null) {
+ sm.registerListener(getSensorListener(),
+ gLinearAccelerometerSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_GYROSCOPE:
+ if (gGyroscopeSensor == null) {
+ gGyroscopeSensor = sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
+ }
+ if (gGyroscopeSensor != null) {
+ sm.registerListener(getSensorListener(),
+ gGyroscopeSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ break;
+
+ default:
+ Log.w(LOGTAG, "Error! Can't enable unknown SENSOR type " +
+ aSensortype);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void disableSensor(int aSensortype) {
+ GeckoInterface gi = getGeckoInterface();
+ if (gi == null)
+ return;
+
+ SensorManager sm = (SensorManager)
+ getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
+
+ switch (aSensortype) {
+ case GeckoHalDefines.SENSOR_GAME_ROTATION_VECTOR:
+ if (gGameRotationVectorSensor != null) {
+ sm.unregisterListener(getSensorListener(), gGameRotationVectorSensor);
+ break;
+ }
+ // Fallthrough
+
+ case GeckoHalDefines.SENSOR_ROTATION_VECTOR:
+ if (gRotationVectorSensor != null) {
+ sm.unregisterListener(getSensorListener(), gRotationVectorSensor);
+ break;
+ }
+ // Fallthrough
+
+ case GeckoHalDefines.SENSOR_ORIENTATION:
+ if (gOrientationSensor != null) {
+ sm.unregisterListener(getSensorListener(), gOrientationSensor);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_ACCELERATION:
+ if (gAccelerometerSensor != null) {
+ sm.unregisterListener(getSensorListener(), gAccelerometerSensor);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_PROXIMITY:
+ if (gProximitySensor != null) {
+ sm.unregisterListener(getSensorListener(), gProximitySensor);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_LIGHT:
+ if (gLightSensor != null) {
+ sm.unregisterListener(getSensorListener(), gLightSensor);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_LINEAR_ACCELERATION:
+ if (gLinearAccelerometerSensor != null) {
+ sm.unregisterListener(getSensorListener(), gLinearAccelerometerSensor);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_GYROSCOPE:
+ if (gGyroscopeSensor != null) {
+ sm.unregisterListener(getSensorListener(), gGyroscopeSensor);
+ }
+ break;
+ default:
+ Log.w(LOGTAG, "Error! Can't disable unknown SENSOR type " + aSensortype);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void moveTaskToBack() {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().getActivity().moveTaskToBack(true);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static void scheduleRestart() {
+ getGeckoInterface().doRestart();
+ }
+
+ // Creates a homescreen shortcut for a web page.
+ // This is the entry point from nsIShellService.
+ @WrapForJNI(calledFrom = "gecko")
+ public static void createShortcut(final String aTitle, final String aURI) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return;
+ }
+ geckoInterface.createShortcut(aTitle, aURI);
+ }
+
+ @JNITarget
+ static public int getPreferredIconSize() {
+ ActivityManager am = (ActivityManager)
+ getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
+ return am.getLauncherLargeIconSize();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static String[] getHandlersForMimeType(String aMimeType, String aAction) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return new String[] {};
+ }
+ return geckoInterface.getHandlersForMimeType(aMimeType, aAction);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static String[] getHandlersForURL(String aURL, String aAction) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return new String[] {};
+ }
+ return geckoInterface.getHandlersForURL(aURL, aAction);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean getHWEncoderCapability() {
+ return HardwareCodecCapabilityUtils.getHWEncoderCapability();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean getHWDecoderCapability() {
+ return HardwareCodecCapabilityUtils.getHWDecoderCapability();
+ }
+
+ static List<ResolveInfo> queryIntentActivities(Intent intent) {
+ final PackageManager pm = getApplicationContext().getPackageManager();
+
+ // Exclude any non-exported activities: we can't open them even if we want to!
+ // Bug 1031569 has some details.
+ final ArrayList<ResolveInfo> list = new ArrayList<>();
+ for (ResolveInfo ri: pm.queryIntentActivities(intent, 0)) {
+ if (ri.activityInfo.exported) {
+ list.add(ri);
+ }
+ }
+
+ return list;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static String getExtensionFromMimeType(String aMimeType) {
+ return MimeTypeMap.getSingleton().getExtensionFromMimeType(aMimeType);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static String getMimeTypeFromExtensions(String aFileExt) {
+ StringTokenizer st = new StringTokenizer(aFileExt, ".,; ");
+ String type = null;
+ String subType = null;
+ while (st.hasMoreElements()) {
+ String ext = st.nextToken();
+ String mt = getMimeTypeFromExtension(ext);
+ if (mt == null)
+ continue;
+ int slash = mt.indexOf('/');
+ String tmpType = mt.substring(0, slash);
+ if (!tmpType.equalsIgnoreCase(type))
+ type = type == null ? tmpType : "*";
+ String tmpSubType = mt.substring(slash + 1);
+ if (!tmpSubType.equalsIgnoreCase(subType))
+ subType = subType == null ? tmpSubType : "*";
+ }
+ if (type == null)
+ type = "*";
+ if (subType == null)
+ subType = "*";
+ return type + "/" + subType;
+ }
+
+ static boolean isUriSafeForScheme(Uri aUri) {
+ // Bug 794034 - We don't want to pass MWI or USSD codes to the
+ // dialer, and ensure the Uri class doesn't parse a URI
+ // containing a fragment ('#')
+ final String scheme = aUri.getScheme();
+ if ("tel".equals(scheme) || "sms".equals(scheme)) {
+ final String number = aUri.getSchemeSpecificPart();
+ if (number.contains("#") || number.contains("*") || aUri.getFragment() != null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean openUriExternal(String targetURI,
+ String mimeType,
+ String packageName,
+ String className,
+ String action,
+ String title) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return false;
+ }
+ return geckoInterface.openUriExternal(targetURI, mimeType, packageName, className, action, title);
+ }
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void notifyAlertListener(String name, String topic, String cookie);
+
+ /**
+ * Called by the NotificationListener to notify Gecko that a notification has been
+ * shown.
+ */
+ public static void onNotificationShow(final String name, final String cookie) {
+ if (GeckoThread.isRunning()) {
+ notifyAlertListener(name, "alertshow", cookie);
+ }
+ }
+
+ /**
+ * Called by the NotificationListener to notify Gecko that a previously shown
+ * notification has been closed.
+ */
+ public static void onNotificationClose(final String name, final String cookie) {
+ if (GeckoThread.isRunning()) {
+ notifyAlertListener(name, "alertfinished", cookie);
+ }
+ }
+
+ /**
+ * Called by the NotificationListener to notify Gecko that a previously shown
+ * notification has been clicked on.
+ */
+ public static void onNotificationClick(final String name, final String cookie) {
+ if (GeckoThread.isRunning()) {
+ notifyAlertListener(name, "alertclickcallback", cookie);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void showNotification(String name, String cookie, String title,
+ String text, String host, String imageUrl,
+ String persistentData) {
+ if (persistentData == null) {
+ getNotificationListener().showNotification(name, cookie, title, text, host, imageUrl);
+ return;
+ }
+
+ getNotificationListener().showPersistentNotification(
+ name, cookie, title, text, host, imageUrl, persistentData);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void closeNotification(String name) {
+ getNotificationListener().closeNotification(name);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static int getDpi() {
+ if (sDensityDpi == 0) {
+ sDensityDpi = getApplicationContext().getResources().getDisplayMetrics().densityDpi;
+ }
+
+ return sDensityDpi;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static float getDensity() {
+ return getApplicationContext().getResources().getDisplayMetrics().density;
+ }
+
+ private static boolean isHighMemoryDevice() {
+ return HardwareUtils.getMemSize() > HIGH_MEMORY_DEVICE_THRESHOLD_MB;
+ }
+
+ /**
+ * Returns the colour depth of the default screen. This will either be
+ * 24 or 16.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ public static synchronized int getScreenDepth() {
+ if (sScreenDepth == 0) {
+ sScreenDepth = 16;
+ PixelFormat info = new PixelFormat();
+ final WindowManager wm = (WindowManager)
+ getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
+ PixelFormat.getPixelFormatInfo(wm.getDefaultDisplay().getPixelFormat(), info);
+ if (info.bitsPerPixel >= 24 && isHighMemoryDevice()) {
+ sScreenDepth = 24;
+ }
+ }
+
+ return sScreenDepth;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static synchronized void setScreenDepthOverride(int aScreenDepth) {
+ if (sScreenDepth != 0) {
+ Log.e(LOGTAG, "Tried to override screen depth after it's already been set");
+ return;
+ }
+
+ sScreenDepth = aScreenDepth;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void setFullScreen(boolean fullscreen) {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().setFullScreen(fullscreen);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void performHapticFeedback(boolean aIsLongPress) {
+ // Don't perform haptic feedback if a vibration is currently playing,
+ // because the haptic feedback will nuke the vibration.
+ if (!sVibrationMaybePlaying || System.nanoTime() >= sVibrationEndTime) {
+ LayerView layerView = getLayerView();
+ layerView.performHapticFeedback(aIsLongPress ?
+ HapticFeedbackConstants.LONG_PRESS :
+ HapticFeedbackConstants.VIRTUAL_KEY);
+ }
+ }
+
+ private static Vibrator vibrator() {
+ return (Vibrator) getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE);
+ }
+
+ // Helper method to convert integer array to long array.
+ private static long[] convertIntToLongArray(int[] input) {
+ long[] output = new long[input.length];
+ for (int i = 0; i < input.length; i++) {
+ output[i] = input[i];
+ }
+ return output;
+ }
+
+ // Vibrate only if haptic feedback is enabled.
+ public static void vibrateOnHapticFeedbackEnabled(int[] milliseconds) {
+ if (Settings.System.getInt(getApplicationContext().getContentResolver(),
+ Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) > 0) {
+ vibrate(convertIntToLongArray(milliseconds), -1);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void vibrate(long milliseconds) {
+ sVibrationEndTime = System.nanoTime() + milliseconds * 1000000;
+ sVibrationMaybePlaying = true;
+ vibrator().vibrate(milliseconds);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void vibrate(long[] pattern, int repeat) {
+ // If pattern.length is even, the last element in the pattern is a
+ // meaningless delay, so don't include it in vibrationDuration.
+ long vibrationDuration = 0;
+ int iterLen = pattern.length - (pattern.length % 2 == 0 ? 1 : 0);
+ for (int i = 0; i < iterLen; i++) {
+ vibrationDuration += pattern[i];
+ }
+
+ sVibrationEndTime = System.nanoTime() + vibrationDuration * 1000000;
+ sVibrationMaybePlaying = true;
+ vibrator().vibrate(pattern, repeat);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void cancelVibrate() {
+ sVibrationMaybePlaying = false;
+ sVibrationEndTime = 0;
+ vibrator().cancel();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void setKeepScreenOn(final boolean on) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // TODO
+ }
+ });
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean isNetworkLinkUp() {
+ ConnectivityManager cm = (ConnectivityManager)
+ getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+ try {
+ NetworkInfo info = cm.getActiveNetworkInfo();
+ if (info == null || !info.isConnected())
+ return false;
+ } catch (SecurityException se) {
+ return false;
+ }
+ return true;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean isNetworkLinkKnown() {
+ ConnectivityManager cm = (ConnectivityManager)
+ getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+ try {
+ if (cm.getActiveNetworkInfo() == null)
+ return false;
+ } catch (SecurityException se) {
+ return false;
+ }
+ return true;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static int getNetworkLinkType() {
+ ConnectivityManager cm = (ConnectivityManager)
+ getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo info = cm.getActiveNetworkInfo();
+ if (info == null) {
+ return LINK_TYPE_UNKNOWN;
+ }
+
+ switch (info.getType()) {
+ case ConnectivityManager.TYPE_ETHERNET:
+ return LINK_TYPE_ETHERNET;
+ case ConnectivityManager.TYPE_WIFI:
+ return LINK_TYPE_WIFI;
+ case ConnectivityManager.TYPE_WIMAX:
+ return LINK_TYPE_WIMAX;
+ case ConnectivityManager.TYPE_MOBILE:
+ break; // We will handle sub-types after the switch.
+ default:
+ Log.w(LOGTAG, "Ignoring the current network type.");
+ return LINK_TYPE_UNKNOWN;
+ }
+
+ TelephonyManager tm = (TelephonyManager)
+ getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);
+ if (tm == null) {
+ Log.e(LOGTAG, "Telephony service does not exist");
+ return LINK_TYPE_UNKNOWN;
+ }
+
+ switch (tm.getNetworkType()) {
+ case TelephonyManager.NETWORK_TYPE_IDEN:
+ case TelephonyManager.NETWORK_TYPE_CDMA:
+ case TelephonyManager.NETWORK_TYPE_GPRS:
+ return LINK_TYPE_2G;
+ case TelephonyManager.NETWORK_TYPE_1xRTT:
+ case TelephonyManager.NETWORK_TYPE_EDGE:
+ return LINK_TYPE_2G; // 2.5G
+ case TelephonyManager.NETWORK_TYPE_UMTS:
+ case TelephonyManager.NETWORK_TYPE_EVDO_0:
+ return LINK_TYPE_3G;
+ case TelephonyManager.NETWORK_TYPE_HSPA:
+ case TelephonyManager.NETWORK_TYPE_HSDPA:
+ case TelephonyManager.NETWORK_TYPE_HSUPA:
+ case TelephonyManager.NETWORK_TYPE_EVDO_A:
+ case TelephonyManager.NETWORK_TYPE_EVDO_B:
+ case TelephonyManager.NETWORK_TYPE_EHRPD:
+ return LINK_TYPE_3G; // 3.5G
+ case TelephonyManager.NETWORK_TYPE_HSPAP:
+ return LINK_TYPE_3G; // 3.75G
+ case TelephonyManager.NETWORK_TYPE_LTE:
+ return LINK_TYPE_4G; // 3.9G
+ case TelephonyManager.NETWORK_TYPE_UNKNOWN:
+ default:
+ Log.w(LOGTAG, "Connected to an unknown mobile network!");
+ return LINK_TYPE_UNKNOWN;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static int[] getSystemColors() {
+ // attrsAppearance[] must correspond to AndroidSystemColors structure in android/AndroidBridge.h
+ final int[] attrsAppearance = {
+ android.R.attr.textColor,
+ android.R.attr.textColorPrimary,
+ android.R.attr.textColorPrimaryInverse,
+ android.R.attr.textColorSecondary,
+ android.R.attr.textColorSecondaryInverse,
+ android.R.attr.textColorTertiary,
+ android.R.attr.textColorTertiaryInverse,
+ android.R.attr.textColorHighlight,
+ android.R.attr.colorForeground,
+ android.R.attr.colorBackground,
+ android.R.attr.panelColorForeground,
+ android.R.attr.panelColorBackground
+ };
+
+ int[] result = new int[attrsAppearance.length];
+
+ final ContextThemeWrapper contextThemeWrapper =
+ new ContextThemeWrapper(getApplicationContext(), android.R.style.TextAppearance);
+
+ final TypedArray appearance = contextThemeWrapper.getTheme().obtainStyledAttributes(attrsAppearance);
+
+ if (appearance != null) {
+ for (int i = 0; i < appearance.getIndexCount(); i++) {
+ int idx = appearance.getIndex(i);
+ int color = appearance.getColor(idx, 0);
+ result[idx] = color;
+ }
+ appearance.recycle();
+ }
+
+ return result;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static void killAnyZombies() {
+ GeckoProcessesVisitor visitor = new GeckoProcessesVisitor() {
+ @Override
+ public boolean callback(int pid) {
+ if (pid != android.os.Process.myPid())
+ android.os.Process.killProcess(pid);
+ return true;
+ }
+ };
+
+ EnumerateGeckoProcesses(visitor);
+ }
+
+ interface GeckoProcessesVisitor {
+ boolean callback(int pid);
+ }
+
+ private static void EnumerateGeckoProcesses(GeckoProcessesVisitor visiter) {
+ int pidColumn = -1;
+ int userColumn = -1;
+
+ try {
+ // run ps and parse its output
+ java.lang.Process ps = Runtime.getRuntime().exec("ps");
+ BufferedReader in = new BufferedReader(new InputStreamReader(ps.getInputStream()),
+ 2048);
+
+ String headerOutput = in.readLine();
+
+ // figure out the column offsets. We only care about the pid and user fields
+ StringTokenizer st = new StringTokenizer(headerOutput);
+
+ int tokenSoFar = 0;
+ while (st.hasMoreTokens()) {
+ String next = st.nextToken();
+ if (next.equalsIgnoreCase("PID"))
+ pidColumn = tokenSoFar;
+ else if (next.equalsIgnoreCase("USER"))
+ userColumn = tokenSoFar;
+ tokenSoFar++;
+ }
+
+ // alright, the rest are process entries.
+ String psOutput = null;
+ while ((psOutput = in.readLine()) != null) {
+ String[] split = psOutput.split("\\s+");
+ if (split.length <= pidColumn || split.length <= userColumn)
+ continue;
+ int uid = android.os.Process.getUidForName(split[userColumn]);
+ if (uid == android.os.Process.myUid() &&
+ !split[split.length - 1].equalsIgnoreCase("ps")) {
+ int pid = Integer.parseInt(split[pidColumn]);
+ boolean keepGoing = visiter.callback(pid);
+ if (keepGoing == false)
+ break;
+ }
+ }
+ in.close();
+ }
+ catch (Exception e) {
+ Log.w(LOGTAG, "Failed to enumerate Gecko processes.", e);
+ }
+ }
+
+ public static String getAppNameByPID(int pid) {
+ BufferedReader cmdlineReader = null;
+ String path = "/proc/" + pid + "/cmdline";
+ try {
+ File cmdlineFile = new File(path);
+ if (!cmdlineFile.exists())
+ return "";
+ cmdlineReader = new BufferedReader(new FileReader(cmdlineFile));
+ return cmdlineReader.readLine().trim();
+ } catch (Exception ex) {
+ return "";
+ } finally {
+ if (null != cmdlineReader) {
+ try {
+ cmdlineReader.close();
+ } catch (Exception e) { }
+ }
+ }
+ }
+
+ public static void listOfOpenFiles() {
+ int pidColumn = -1;
+ int nameColumn = -1;
+
+ try {
+ String filter = GeckoProfile.get(getApplicationContext()).getDir().toString();
+ Log.d(LOGTAG, "[OPENFILE] Filter: " + filter);
+
+ // run lsof and parse its output
+ java.lang.Process lsof = Runtime.getRuntime().exec("lsof");
+ BufferedReader in = new BufferedReader(new InputStreamReader(lsof.getInputStream()), 2048);
+
+ String headerOutput = in.readLine();
+ StringTokenizer st = new StringTokenizer(headerOutput);
+ int token = 0;
+ while (st.hasMoreTokens()) {
+ String next = st.nextToken();
+ if (next.equalsIgnoreCase("PID"))
+ pidColumn = token;
+ else if (next.equalsIgnoreCase("NAME"))
+ nameColumn = token;
+ token++;
+ }
+
+ // alright, the rest are open file entries.
+ Map<Integer, String> pidNameMap = new TreeMap<Integer, String>();
+ String output = null;
+ while ((output = in.readLine()) != null) {
+ String[] split = output.split("\\s+");
+ if (split.length <= pidColumn || split.length <= nameColumn)
+ continue;
+ final Integer pid = Integer.valueOf(split[pidColumn]);
+ String name = pidNameMap.get(pid);
+ if (name == null) {
+ name = getAppNameByPID(pid.intValue());
+ pidNameMap.put(pid, name);
+ }
+ String file = split[nameColumn];
+ if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(file) && file.startsWith(filter))
+ Log.d(LOGTAG, "[OPENFILE] " + name + "(" + split[pidColumn] + ") : " + file);
+ }
+ in.close();
+ } catch (Exception e) { }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static byte[] getIconForExtension(String aExt, int iconSize) {
+ try {
+ if (iconSize <= 0)
+ iconSize = 16;
+
+ if (aExt != null && aExt.length() > 1 && aExt.charAt(0) == '.')
+ aExt = aExt.substring(1);
+
+ PackageManager pm = getApplicationContext().getPackageManager();
+ Drawable icon = getDrawableForExtension(pm, aExt);
+ if (icon == null) {
+ // Use a generic icon
+ icon = pm.getDefaultActivityIcon();
+ }
+
+ Bitmap bitmap = ((BitmapDrawable)icon).getBitmap();
+ if (bitmap.getWidth() != iconSize || bitmap.getHeight() != iconSize)
+ bitmap = Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, true);
+
+ ByteBuffer buf = ByteBuffer.allocate(iconSize * iconSize * 4);
+ bitmap.copyPixelsToBuffer(buf);
+
+ return buf.array();
+ }
+ catch (Exception e) {
+ Log.w(LOGTAG, "getIconForExtension failed.", e);
+ return null;
+ }
+ }
+
+ public static String getMimeTypeFromExtension(String ext) {
+ final MimeTypeMap mtm = MimeTypeMap.getSingleton();
+ return mtm.getMimeTypeFromExtension(ext);
+ }
+
+ private static Drawable getDrawableForExtension(PackageManager pm, String aExt) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ final String mimeType = getMimeTypeFromExtension(aExt);
+ if (mimeType != null && mimeType.length() > 0)
+ intent.setType(mimeType);
+ else
+ return null;
+
+ List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
+ if (list.size() == 0)
+ return null;
+
+ ResolveInfo resolveInfo = list.get(0);
+
+ if (resolveInfo == null)
+ return null;
+
+ ActivityInfo activityInfo = resolveInfo.activityInfo;
+
+ return activityInfo.loadIcon(pm);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean getShowPasswordSetting() {
+ try {
+ int showPassword =
+ Settings.System.getInt(getApplicationContext().getContentResolver(),
+ Settings.System.TEXT_SHOW_PASSWORD, 1);
+ return (showPassword > 0);
+ }
+ catch (Exception e) {
+ return true;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ public static native void onFullScreenPluginHidden(View view);
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void addFullScreenPluginView(View view) {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().addPluginView(view);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void removeFullScreenPluginView(View view) {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().removePluginView(view);
+ }
+
+ /**
+ * A plugin that wish to be loaded in the WebView must provide this permission
+ * in their AndroidManifest.xml.
+ */
+ public static final String PLUGIN_ACTION = "android.webkit.PLUGIN";
+ public static final String PLUGIN_PERMISSION = "android.webkit.permission.PLUGIN";
+
+ private static final String PLUGIN_SYSTEM_LIB = "/system/lib/plugins/";
+
+ private static final String PLUGIN_TYPE = "type";
+ private static final String TYPE_NATIVE = "native";
+ public static final ArrayList<PackageInfo> mPackageInfoCache = new ArrayList<>();
+
+ // Returns null if plugins are blocked on the device.
+ static String[] getPluginDirectories() {
+
+ // Block on Pixel C.
+ if ((new File("/system/lib/hw/power.dragon.so")).exists()) {
+ Log.w(LOGTAG, "Blocking plugins because of Pixel C device (bug 1255122)");
+ return null;
+ }
+ // An awful hack to detect Tegra devices. Easiest way to do it without spinning up a EGL context.
+ boolean isTegra = (new File("/system/lib/hw/gralloc.tegra.so")).exists() ||
+ (new File("/system/lib/hw/gralloc.tegra3.so")).exists() ||
+ (new File("/sys/class/nvidia-gpu")).exists();
+ if (isTegra) {
+ // disable on KitKat (bug 957694)
+ if (Versions.feature19Plus) {
+ Log.w(LOGTAG, "Blocking plugins because of Tegra (bug 957694)");
+ return null;
+ }
+
+ // disable Flash on Tegra ICS with CM9 and other custom firmware (bug 736421)
+ final File vfile = new File("/proc/version");
+ try {
+ if (vfile.canRead()) {
+ final BufferedReader reader = new BufferedReader(new FileReader(vfile));
+ try {
+ final String version = reader.readLine();
+ if (version.indexOf("CM9") != -1 ||
+ version.indexOf("cyanogen") != -1 ||
+ version.indexOf("Nova") != -1) {
+ Log.w(LOGTAG, "Blocking plugins because of Tegra 2 + unofficial ICS bug (bug 736421)");
+ return null;
+ }
+ } finally {
+ reader.close();
+ }
+ }
+ } catch (IOException ex) {
+ // Do nothing.
+ }
+ }
+
+ ArrayList<String> directories = new ArrayList<String>();
+ PackageManager pm = getApplicationContext().getPackageManager();
+ List<ResolveInfo> plugins = pm.queryIntentServices(new Intent(PLUGIN_ACTION),
+ PackageManager.GET_META_DATA);
+
+ synchronized (mPackageInfoCache) {
+
+ // clear the list of existing packageInfo objects
+ mPackageInfoCache.clear();
+
+
+ for (ResolveInfo info : plugins) {
+
+ // retrieve the plugin's service information
+ ServiceInfo serviceInfo = info.serviceInfo;
+ if (serviceInfo == null) {
+ Log.w(LOGTAG, "Ignoring bad plugin.");
+ continue;
+ }
+
+ // Blacklist HTC's flash lite.
+ // See bug #704516 - We're not quite sure what Flash Lite does,
+ // but loading it causes Flash to give errors and fail to draw.
+ if (serviceInfo.packageName.equals("com.htc.flashliteplugin")) {
+ Log.w(LOGTAG, "Skipping HTC's flash lite plugin");
+ continue;
+ }
+
+
+ // Retrieve information from the plugin's manifest.
+ PackageInfo pkgInfo;
+ try {
+ pkgInfo = pm.getPackageInfo(serviceInfo.packageName,
+ PackageManager.GET_PERMISSIONS
+ | PackageManager.GET_SIGNATURES);
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Can't find plugin: " + serviceInfo.packageName);
+ continue;
+ }
+
+ if (pkgInfo == null) {
+ Log.w(LOGTAG, "Not loading plugin: " + serviceInfo.packageName + ". Could not load package information.");
+ continue;
+ }
+
+ /*
+ * find the location of the plugin's shared library. The default
+ * is to assume the app is either a user installed app or an
+ * updated system app. In both of these cases the library is
+ * stored in the app's data directory.
+ */
+ String directory = pkgInfo.applicationInfo.dataDir + "/lib";
+ final int appFlags = pkgInfo.applicationInfo.flags;
+ final int updatedSystemFlags = ApplicationInfo.FLAG_SYSTEM |
+ ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
+
+ // preloaded system app with no user updates
+ if ((appFlags & updatedSystemFlags) == ApplicationInfo.FLAG_SYSTEM) {
+ directory = PLUGIN_SYSTEM_LIB + pkgInfo.packageName;
+ }
+
+ // check if the plugin has the required permissions
+ String permissions[] = pkgInfo.requestedPermissions;
+ if (permissions == null) {
+ Log.w(LOGTAG, "Not loading plugin: " + serviceInfo.packageName + ". Does not have required permission.");
+ continue;
+ }
+ boolean permissionOk = false;
+ for (String permit : permissions) {
+ if (PLUGIN_PERMISSION.equals(permit)) {
+ permissionOk = true;
+ break;
+ }
+ }
+ if (!permissionOk) {
+ Log.w(LOGTAG, "Not loading plugin: " + serviceInfo.packageName + ". Does not have required permission (2).");
+ continue;
+ }
+
+ // check to ensure the plugin is properly signed
+ Signature signatures[] = pkgInfo.signatures;
+ if (signatures == null) {
+ Log.w(LOGTAG, "Not loading plugin: " + serviceInfo.packageName + ". Not signed.");
+ continue;
+ }
+
+ // determine the type of plugin from the manifest
+ if (serviceInfo.metaData == null) {
+ Log.e(LOGTAG, "The plugin '" + serviceInfo.name + "' has no defined type.");
+ continue;
+ }
+
+ String pluginType = serviceInfo.metaData.getString(PLUGIN_TYPE);
+ if (!TYPE_NATIVE.equals(pluginType)) {
+ Log.e(LOGTAG, "Unrecognized plugin type: " + pluginType);
+ continue;
+ }
+
+ try {
+ Class<?> cls = getPluginClass(serviceInfo.packageName, serviceInfo.name);
+
+ //TODO implement any requirements of the plugin class here!
+ boolean classFound = true;
+
+ if (!classFound) {
+ Log.e(LOGTAG, "The plugin's class' " + serviceInfo.name + "' does not extend the appropriate class.");
+ continue;
+ }
+
+ } catch (NameNotFoundException e) {
+ Log.e(LOGTAG, "Can't find plugin: " + serviceInfo.packageName);
+ continue;
+ } catch (ClassNotFoundException e) {
+ Log.e(LOGTAG, "Can't find plugin's class: " + serviceInfo.name);
+ continue;
+ }
+
+ // if all checks have passed then make the plugin available
+ mPackageInfoCache.add(pkgInfo);
+ directories.add(directory);
+ }
+ }
+
+ return directories.toArray(new String[directories.size()]);
+ }
+
+ static String getPluginPackage(String pluginLib) {
+
+ if (pluginLib == null || pluginLib.length() == 0) {
+ return null;
+ }
+
+ synchronized (mPackageInfoCache) {
+ for (PackageInfo pkgInfo : mPackageInfoCache) {
+ if (pluginLib.contains(pkgInfo.packageName)) {
+ return pkgInfo.packageName;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ static Class<?> getPluginClass(String packageName, String className)
+ throws NameNotFoundException, ClassNotFoundException {
+ Context pluginContext = getApplicationContext().createPackageContext(packageName,
+ Context.CONTEXT_INCLUDE_CODE |
+ Context.CONTEXT_IGNORE_SECURITY);
+ ClassLoader pluginCL = pluginContext.getClassLoader();
+ return pluginCL.loadClass(className);
+ }
+
+ @WrapForJNI
+ private static Class<?> loadPluginClass(String className, String libName) {
+ if (getGeckoInterface() == null)
+ return null;
+ try {
+ final String packageName = getPluginPackage(libName);
+ final int contextFlags = Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY;
+ final Context pluginContext = getApplicationContext().createPackageContext(
+ packageName, contextFlags);
+ return pluginContext.getClassLoader().loadClass(className);
+ } catch (java.lang.ClassNotFoundException cnfe) {
+ Log.w(LOGTAG, "Couldn't find plugin class " + className, cnfe);
+ return null;
+ } catch (android.content.pm.PackageManager.NameNotFoundException nnfe) {
+ Log.w(LOGTAG, "Couldn't find package.", nnfe);
+ return null;
+ }
+ }
+
+ private static Context sApplicationContext;
+ private static ContextGetter sContextGetter;
+
+ @Deprecated
+ @WrapForJNI
+ public static Context getContext() {
+ return sContextGetter.getContext();
+ }
+
+ public static void setContextGetter(ContextGetter cg) {
+ sContextGetter = cg;
+ }
+
+ @WrapForJNI
+ public static Context getApplicationContext() {
+ return sApplicationContext;
+ }
+
+ public static void setApplicationContext(final Context context) {
+ sApplicationContext = context;
+ }
+
+ public static SharedPreferences getSharedPreferences() {
+ if (sContextGetter == null) {
+ throw new IllegalStateException("No ContextGetter; cannot fetch prefs.");
+ }
+ return sContextGetter.getSharedPreferences();
+ }
+
+ public interface AppStateListener {
+ public void onPause();
+ public void onResume();
+ public void onOrientationChanged();
+ }
+
+ public interface GeckoInterface {
+ public EventDispatcher getAppEventDispatcher();
+ public GeckoProfile getProfile();
+ public Activity getActivity();
+ public String getDefaultUAString();
+ public void doRestart();
+ public void setFullScreen(boolean fullscreen);
+ public void addPluginView(View view);
+ public void removePluginView(final View view);
+ public void enableOrientationListener();
+ public void disableOrientationListener();
+ public void addAppStateListener(AppStateListener listener);
+ public void removeAppStateListener(AppStateListener listener);
+ public void notifyWakeLockChanged(String topic, String state);
+ public boolean areTabsShown();
+ public AbsoluteLayout getPluginContainer();
+ public void notifyCheckUpdateResult(String result);
+ public void invalidateOptionsMenu();
+
+ /**
+ * Create a shortcut -- generally a home-screen icon -- linking the given title to the given URI.
+ * <p>
+ * This method is always invoked on the Gecko thread.
+ *
+ * @param title of URI to link to.
+ * @param URI to link to.
+ */
+ public void createShortcut(String title, String URI);
+
+ /**
+ * Check if the given URI is visited.
+ * <p/>
+ * If it has been visited, call {@link GeckoAppShell#notifyUriVisited(String)}. (If it
+ * has not been visited, do nothing.)
+ * <p/>
+ * This method is always invoked on the Gecko thread.
+ *
+ * @param uri to check.
+ */
+ public void checkUriVisited(String uri);
+
+ /**
+ * Mark the given URI as visited in Gecko.
+ * <p/>
+ * Implementors may maintain some local store of visited URIs in order to be able to
+ * answer {@link #checkUriVisited(String)} requests affirmatively.
+ * <p/>
+ * This method is always invoked on the Gecko thread.
+ *
+ * @param uri to mark.
+ */
+ public void markUriVisited(final String uri);
+
+ /**
+ * Set the title of the given URI, as determined by Gecko.
+ * <p/>
+ * This method is always invoked on the Gecko thread.
+ *
+ * @param uri given.
+ * @param title to associate with the given URI.
+ */
+ public void setUriTitle(final String uri, final String title);
+
+ public void setAccessibilityEnabled(boolean enabled);
+
+ public boolean openUriExternal(String targetURI, String mimeType, String packageName, String className, String action, String title);
+
+ public String[] getHandlersForMimeType(String mimeType, String action);
+ public String[] getHandlersForURL(String url, String action);
+
+ /**
+ * URI of the underlying chrome window to be opened, or null to use the default GeckoView
+ * XUL container <tt>chrome://browser/content/geckoview.xul</tt>. See
+ * <a href="https://developer.mozilla.org/en/docs/toolkit.defaultChromeURI">https://developer.mozilla.org/en/docs/toolkit.defaultChromeURI</a>
+ *
+ * @return URI or null.
+ */
+ String getDefaultChromeURI();
+ };
+
+ private static GeckoInterface sGeckoInterface;
+
+ public static GeckoInterface getGeckoInterface() {
+ return sGeckoInterface;
+ }
+
+ public static void setGeckoInterface(GeckoInterface aGeckoInterface) {
+ sGeckoInterface = aGeckoInterface;
+ }
+
+ /* package */ static Camera sCamera;
+
+ private static final int kPreferredFPS = 25;
+ private static byte[] sCameraBuffer;
+
+ private static class CameraCallback implements Camera.PreviewCallback {
+ @WrapForJNI(calledFrom = "gecko")
+ private static native void onFrameData(int camera, byte[] data);
+
+ private final int mCamera;
+
+ public CameraCallback(int camera) {
+ mCamera = camera;
+ }
+
+ @Override
+ public void onPreviewFrame(byte[] data, Camera camera) {
+ onFrameData(mCamera, data);
+
+ if (sCamera != null) {
+ sCamera.addCallbackBuffer(sCameraBuffer);
+ }
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static int[] initCamera(String aContentType, int aCamera, int aWidth, int aHeight) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().enableOrientationListener();
+ } catch (Exception e) { }
+ }
+ });
+
+ // [0] = 0|1 (failure/success)
+ // [1] = width
+ // [2] = height
+ // [3] = fps
+ int[] result = new int[4];
+ result[0] = 0;
+
+ if (Camera.getNumberOfCameras() == 0) {
+ return result;
+ }
+
+ try {
+ sCamera = Camera.open(aCamera);
+
+ Camera.Parameters params = sCamera.getParameters();
+ params.setPreviewFormat(ImageFormat.NV21);
+
+ // use the preview fps closest to 25 fps.
+ int fpsDelta = 1000;
+ try {
+ Iterator<Integer> it = params.getSupportedPreviewFrameRates().iterator();
+ while (it.hasNext()) {
+ int nFps = it.next();
+ if (Math.abs(nFps - kPreferredFPS) < fpsDelta) {
+ fpsDelta = Math.abs(nFps - kPreferredFPS);
+ params.setPreviewFrameRate(nFps);
+ }
+ }
+ } catch (Exception e) {
+ params.setPreviewFrameRate(kPreferredFPS);
+ }
+
+ // set up the closest preview size available
+ Iterator<Camera.Size> sit = params.getSupportedPreviewSizes().iterator();
+ int sizeDelta = 10000000;
+ int bufferSize = 0;
+ while (sit.hasNext()) {
+ Camera.Size size = sit.next();
+ if (Math.abs(size.width * size.height - aWidth * aHeight) < sizeDelta) {
+ sizeDelta = Math.abs(size.width * size.height - aWidth * aHeight);
+ params.setPreviewSize(size.width, size.height);
+ bufferSize = size.width * size.height;
+ }
+ }
+
+ sCamera.setParameters(params);
+ sCameraBuffer = new byte[(bufferSize * 12) / 8];
+ sCamera.addCallbackBuffer(sCameraBuffer);
+ sCamera.setPreviewCallbackWithBuffer(new CameraCallback(aCamera));
+ sCamera.startPreview();
+ params = sCamera.getParameters();
+ result[0] = 1;
+ result[1] = params.getPreviewSize().width;
+ result[2] = params.getPreviewSize().height;
+ result[3] = params.getPreviewFrameRate();
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "initCamera RuntimeException.", e);
+ result[0] = result[1] = result[2] = result[3] = 0;
+ }
+ return result;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static synchronized void closeCamera() {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().disableOrientationListener();
+ } catch (Exception e) { }
+ }
+ });
+ if (sCamera != null) {
+ sCamera.stopPreview();
+ sCamera.release();
+ sCamera = null;
+ sCameraBuffer = null;
+ }
+ }
+
+ /*
+ * Battery API related methods.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ private static void enableBatteryNotifications() {
+ GeckoBatteryManager.enableNotifications();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void handleGeckoMessage(final NativeJSContainer message) {
+ boolean success = EventDispatcher.getInstance().dispatchEvent(message);
+ if (getGeckoInterface() != null && getGeckoInterface().getAppEventDispatcher() != null) {
+ success |= getGeckoInterface().getAppEventDispatcher().dispatchEvent(message);
+ }
+
+ if (!success) {
+ final String type = message.optString("type", null);
+ final String guid = message.optString(EventDispatcher.GUID, null);
+ if (type != null && guid != null) {
+ (new EventDispatcher.GeckoEventCallback(guid, type)).sendError("No listeners for request");
+ }
+ }
+ message.disposeNative();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void disableBatteryNotifications() {
+ GeckoBatteryManager.disableNotifications();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static double[] getCurrentBatteryInformation() {
+ return GeckoBatteryManager.getCurrentInformation();
+ }
+
+ @WrapForJNI(stubName = "CheckURIVisited", calledFrom = "gecko")
+ private static void checkUriVisited(String uri) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return;
+ }
+ geckoInterface.checkUriVisited(uri);
+ }
+
+ @WrapForJNI(stubName = "MarkURIVisited", calledFrom = "gecko")
+ private static void markUriVisited(final String uri) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return;
+ }
+ geckoInterface.markUriVisited(uri);
+ }
+
+ @WrapForJNI(stubName = "SetURITitle", calledFrom = "gecko")
+ private static void setUriTitle(final String uri, final String title) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return;
+ }
+ geckoInterface.setUriTitle(uri, title);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void hideProgressDialog() {
+ // unused stub
+ }
+
+ /* Called by JNI from AndroidBridge, and by reflection from tests/BaseTest.java.in */
+ @WrapForJNI(calledFrom = "gecko")
+ @RobocopTarget
+ public static boolean isTablet() {
+ return HardwareUtils.isTablet();
+ }
+
+ private static boolean sImeWasEnabledOnLastResize = false;
+ public static void viewSizeChanged() {
+ GeckoView v = (GeckoView) getLayerView();
+ if (v == null) {
+ return;
+ }
+ boolean imeIsEnabled = v.isIMEEnabled();
+ if (imeIsEnabled && !sImeWasEnabledOnLastResize) {
+ // The IME just came up after not being up, so let's scroll
+ // to the focused input.
+ notifyObservers("ScrollTo:FocusedInput", "");
+ }
+ sImeWasEnabledOnLastResize = imeIsEnabled;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static double[] getCurrentNetworkInformation() {
+ return GeckoNetworkManager.getInstance().getCurrentInformation();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void enableNetworkNotifications() {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ GeckoNetworkManager.getInstance().enableNotifications();
+ }
+ });
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void disableNetworkNotifications() {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ GeckoNetworkManager.getInstance().disableNotifications();
+ }
+ });
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static short getScreenOrientation() {
+ return GeckoScreenOrientation.getInstance().getScreenOrientation().value;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static int getScreenAngle() {
+ return GeckoScreenOrientation.getInstance().getAngle();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void enableScreenOrientationNotifications() {
+ GeckoScreenOrientation.getInstance().enableNotifications();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void disableScreenOrientationNotifications() {
+ GeckoScreenOrientation.getInstance().disableNotifications();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void lockScreenOrientation(int aOrientation) {
+ GeckoScreenOrientation.getInstance().lock(aOrientation);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void unlockScreenOrientation() {
+ GeckoScreenOrientation.getInstance().unlock();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void notifyWakeLockChanged(String topic, String state) {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().notifyWakeLockChanged(topic, state);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean unlockProfile() {
+ // Try to kill any zombie Fennec's that might be running
+ GeckoAppShell.killAnyZombies();
+
+ // Then force unlock this profile
+ if (getGeckoInterface() != null) {
+ GeckoProfile profile = getGeckoInterface().getProfile();
+ File lock = profile.getFile(".parentlock");
+ return lock.exists() && lock.delete();
+ }
+ return false;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static String getProxyForURI(String spec, String scheme, String host, int port) {
+ final ProxySelector ps = new ProxySelector();
+
+ Proxy proxy = ps.select(scheme, host);
+ if (Proxy.NO_PROXY.equals(proxy)) {
+ return "DIRECT";
+ }
+
+ switch (proxy.type()) {
+ case HTTP:
+ return "PROXY " + proxy.address().toString();
+ case SOCKS:
+ return "SOCKS " + proxy.address().toString();
+ }
+
+ return "DIRECT";
+ }
+
+ @WrapForJNI
+ private static InputStream createInputStream(URLConnection connection) throws IOException {
+ return connection.getInputStream();
+ }
+
+ private static class BitmapConnection extends URLConnection {
+ private Bitmap bitmap;
+
+ BitmapConnection(Bitmap b) throws MalformedURLException, IOException {
+ super(null);
+ bitmap = b;
+ }
+
+ @Override
+ public void connect() {}
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return new BitmapInputStream();
+ }
+
+ @Override
+ public String getContentType() {
+ return "image/png";
+ }
+
+ private final class BitmapInputStream extends PipedInputStream {
+ private boolean mHaveConnected = false;
+
+ @Override
+ public synchronized int read(byte[] buffer, int byteOffset, int byteCount)
+ throws IOException {
+ if (mHaveConnected) {
+ return super.read(buffer, byteOffset, byteCount);
+ }
+
+ final PipedOutputStream output = new PipedOutputStream();
+ connect(output);
+ ThreadUtils.postToBackgroundThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ try {
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, output);
+ output.close();
+ } catch (IOException ioe) { }
+ }
+ });
+ mHaveConnected = true;
+ return super.read(buffer, byteOffset, byteCount);
+ }
+ }
+ }
+
+ @WrapForJNI
+ private static URLConnection getConnection(String url) {
+ try {
+ String spec;
+ if (url.startsWith("android://")) {
+ spec = url.substring(10);
+ } else {
+ spec = url.substring(8);
+ }
+
+ // Check if we are loading a package icon.
+ try {
+ if (spec.startsWith("icon/")) {
+ String[] splits = spec.split("/");
+ if (splits.length != 2) {
+ return null;
+ }
+ final String pkg = splits[1];
+ final PackageManager pm = getApplicationContext().getPackageManager();
+ final Drawable d = pm.getApplicationIcon(pkg);
+ final Bitmap bitmap = BitmapUtils.getBitmapFromDrawable(d);
+ return new BitmapConnection(bitmap);
+ }
+ } catch (Exception ex) {
+ Log.e(LOGTAG, "error", ex);
+ }
+
+ // if the colon got stripped, put it back
+ int colon = spec.indexOf(':');
+ if (colon == -1 || colon > spec.indexOf('/')) {
+ spec = spec.replaceFirst("/", ":/");
+ }
+ } catch (Exception ex) {
+ return null;
+ }
+ return null;
+ }
+
+ @WrapForJNI
+ private static String connectionGetMimeType(URLConnection connection) {
+ return connection.getContentType();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static int getMaxTouchPoints() {
+ PackageManager pm = getApplicationContext().getPackageManager();
+ if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_JAZZHAND)) {
+ // at least, 5+ fingers.
+ return 5;
+ } else if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
+ // at least, 2+ fingers.
+ return 2;
+ } else if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)) {
+ // 2 fingers
+ return 2;
+ } else if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) {
+ // 1 finger
+ return 1;
+ }
+ return 0;
+ }
+
+ public static synchronized void resetScreenSize() {
+ sScreenSize = null;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static synchronized Rect getScreenSize() {
+ if (sScreenSize == null) {
+ final WindowManager wm = (WindowManager)
+ getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
+ final Display disp = wm.getDefaultDisplay();
+ sScreenSize = new Rect(0, 0, disp.getWidth(), disp.getHeight());
+ }
+ return sScreenSize;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoBatteryManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoBatteryManager.java
new file mode 100644
index 000000000..1a41c390a
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoBatteryManager.java
@@ -0,0 +1,202 @@
+/* -*- 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;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+import android.os.Build;
+import android.os.SystemClock;
+import android.util.Log;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+public class GeckoBatteryManager extends BroadcastReceiver {
+ private static final String LOGTAG = "GeckoBatteryManager";
+
+ // Those constants should be keep in sync with the ones in:
+ // dom/battery/Constants.h
+ private final static double kDefaultLevel = 1.0;
+ private final static boolean kDefaultCharging = true;
+ private final static double kDefaultRemainingTime = 0.0;
+ private final static double kUnknownRemainingTime = -1.0;
+
+ private static long sLastLevelChange;
+ private static boolean sNotificationsEnabled;
+ private static double sLevel = kDefaultLevel;
+ private static boolean sCharging = kDefaultCharging;
+ private static double sRemainingTime = kDefaultRemainingTime;
+
+ private static final GeckoBatteryManager sInstance = new GeckoBatteryManager();
+
+ private final IntentFilter mFilter;
+ private Context mApplicationContext;
+ private boolean mIsEnabled;
+
+ public static GeckoBatteryManager getInstance() {
+ return sInstance;
+ }
+
+ private GeckoBatteryManager() {
+ mFilter = new IntentFilter();
+ mFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
+ }
+
+ public synchronized void start(final Context context) {
+ if (mIsEnabled) {
+ Log.w(LOGTAG, "Already started!");
+ return;
+ }
+
+ mApplicationContext = context.getApplicationContext();
+ // registerReceiver will return null if registering fails.
+ if (mApplicationContext.registerReceiver(this, mFilter) == null) {
+ Log.e(LOGTAG, "Registering receiver failed");
+ } else {
+ mIsEnabled = true;
+ }
+ }
+
+ public synchronized void stop() {
+ if (!mIsEnabled) {
+ Log.w(LOGTAG, "Already stopped!");
+ return;
+ }
+
+ mApplicationContext.unregisterReceiver(this);
+ mApplicationContext = null;
+ mIsEnabled = false;
+ }
+
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ private static native void onBatteryChange(double level, boolean charging,
+ double remainingTime);
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) {
+ Log.e(LOGTAG, "Got an unexpected intent!");
+ return;
+ }
+
+ boolean previousCharging = isCharging();
+ double previousLevel = getLevel();
+
+ // NOTE: it might not be common (in 2012) but technically, Android can run
+ // on a device that has no battery so we want to make sure it's not the case
+ // before bothering checking for battery state.
+ // However, the Galaxy Nexus phone advertises itself as battery-less which
+ // force us to special-case the logic.
+ // See the Google bug: https://code.google.com/p/android/issues/detail?id=22035
+ if (intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false) ||
+ Build.MODEL.equals("Galaxy Nexus")) {
+ int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
+ if (plugged == -1) {
+ sCharging = kDefaultCharging;
+ Log.e(LOGTAG, "Failed to get the plugged status!");
+ } else {
+ // Likely, if plugged > 0, it's likely plugged and charging but the doc
+ // isn't clear about that.
+ sCharging = plugged != 0;
+ }
+
+ if (sCharging != previousCharging) {
+ sRemainingTime = kUnknownRemainingTime;
+ // The new remaining time is going to take some time to show up but
+ // it's the best way to show a not too wrong value.
+ sLastLevelChange = 0;
+ }
+
+ // We need two doubles because sLevel is a double.
+ double current = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
+ double max = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
+ if (current == -1 || max == -1) {
+ Log.e(LOGTAG, "Failed to get battery level!");
+ sLevel = kDefaultLevel;
+ } else {
+ sLevel = current / max;
+ }
+
+ if (sLevel == 1.0 && sCharging) {
+ sRemainingTime = kDefaultRemainingTime;
+ } else if (sLevel != previousLevel) {
+ // Estimate remaining time.
+ if (sLastLevelChange != 0) {
+ // Use elapsedRealtime() because we want to track time across device sleeps.
+ long currentTime = SystemClock.elapsedRealtime();
+ long dt = (currentTime - sLastLevelChange) / 1000;
+ double dLevel = sLevel - previousLevel;
+
+ if (sCharging) {
+ if (dLevel < 0) {
+ sRemainingTime = kUnknownRemainingTime;
+ } else {
+ sRemainingTime = Math.round(dt / dLevel * (1.0 - sLevel));
+ }
+ } else {
+ if (dLevel > 0) {
+ Log.w(LOGTAG, "When discharging, level should decrease!");
+ sRemainingTime = kUnknownRemainingTime;
+ } else {
+ sRemainingTime = Math.round(dt / -dLevel * sLevel);
+ }
+ }
+
+ sLastLevelChange = currentTime;
+ } else {
+ // That's the first time we got an update, we can't do anything.
+ sLastLevelChange = SystemClock.elapsedRealtime();
+ }
+ }
+ } else {
+ sLevel = kDefaultLevel;
+ sCharging = kDefaultCharging;
+ sRemainingTime = kDefaultRemainingTime;
+ }
+
+ /*
+ * We want to inform listeners if the following conditions are fulfilled:
+ * - we have at least one observer;
+ * - the charging state or the level has changed.
+ *
+ * Note: no need to check for a remaining time change given that it's only
+ * updated if there is a level change or a charging change.
+ *
+ * The idea is to prevent doing all the way to the DOM code in the child
+ * process to finally not send an event.
+ */
+ if (sNotificationsEnabled &&
+ (previousCharging != isCharging() || previousLevel != getLevel())) {
+ onBatteryChange(getLevel(), isCharging(), getRemainingTime());
+ }
+ }
+
+ public static boolean isCharging() {
+ return sCharging;
+ }
+
+ public static double getLevel() {
+ return sLevel;
+ }
+
+ public static double getRemainingTime() {
+ return sRemainingTime;
+ }
+
+ public static void enableNotifications() {
+ sNotificationsEnabled = true;
+ }
+
+ public static void disableNotifications() {
+ sNotificationsEnabled = false;
+ }
+
+ public static double[] getCurrentInformation() {
+ return new double[] { getLevel(), isCharging() ? 1.0 : 0.0, getRemainingTime() };
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java
new file mode 100644
index 000000000..695cff443
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java
@@ -0,0 +1,1589 @@
+/* -*- 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;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import org.json.JSONObject;
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
+
+import android.graphics.RectF;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.NoCopySpan;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.style.CharacterStyle;
+import android.util.Log;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+
+/*
+ GeckoEditable implements only some functions of Editable
+ The field mText contains the actual underlying
+ SpannableStringBuilder/Editable that contains our text.
+*/
+final class GeckoEditable extends JNIObject
+ implements InvocationHandler, Editable, GeckoEditableClient {
+
+ private static final boolean DEBUG = false;
+ private static final String LOGTAG = "GeckoEditable";
+
+ // Filters to implement Editable's filtering functionality
+ private InputFilter[] mFilters;
+
+ private final AsyncText mText;
+ private final Editable mProxy;
+ private final ConcurrentLinkedQueue<Action> mActions;
+ private KeyCharacterMap mKeyMap;
+
+ // mIcRunHandler is the Handler that currently runs Gecko-to-IC Runnables
+ // mIcPostHandler is the Handler to post Gecko-to-IC Runnables to
+ // The two can be different when switching from one handler to another
+ private Handler mIcRunHandler;
+ private Handler mIcPostHandler;
+
+ /* package */ GeckoEditableListener mListener;
+ /* package */ GeckoView mView;
+
+ /* package */ boolean mInBatchMode; // Used by IC thread
+ /* package */ boolean mNeedSync; // Used by IC thread
+ // Gecko side needs an updated composition from Java;
+ private boolean mNeedUpdateComposition; // Used by IC thread
+ private boolean mSuppressKeyUp; // Used by IC thread
+
+ private boolean mGeckoFocused; // Used by Gecko thread
+ private boolean mIgnoreSelectionChange; // Used by Gecko thread
+
+ private static final int IME_RANGE_CARETPOSITION = 1;
+ private static final int IME_RANGE_RAWINPUT = 2;
+ private static final int IME_RANGE_SELECTEDRAWTEXT = 3;
+ private static final int IME_RANGE_CONVERTEDTEXT = 4;
+ private static final int IME_RANGE_SELECTEDCONVERTEDTEXT = 5;
+
+ private static final int IME_RANGE_LINE_NONE = 0;
+ private static final int IME_RANGE_LINE_DOTTED = 1;
+ private static final int IME_RANGE_LINE_DASHED = 2;
+ private static final int IME_RANGE_LINE_SOLID = 3;
+ private static final int IME_RANGE_LINE_DOUBLE = 4;
+ private static final int IME_RANGE_LINE_WAVY = 5;
+
+ private static final int IME_RANGE_UNDERLINE = 1;
+ private static final int IME_RANGE_FORECOLOR = 2;
+ private static final int IME_RANGE_BACKCOLOR = 4;
+ private static final int IME_RANGE_LINECOLOR = 8;
+
+ @WrapForJNI(dispatchTo = "proxy")
+ private native void onKeyEvent(int action, int keyCode, int scanCode, int metaState,
+ long time, int unicodeChar, int baseUnicodeChar,
+ int domPrintableKeyValue, int repeatCount, int flags,
+ boolean isSynthesizedImeKey, KeyEvent event);
+
+ private void onKeyEvent(KeyEvent event, int action, int savedMetaState,
+ boolean isSynthesizedImeKey) {
+ // Use a separate action argument so we can override the key's original action,
+ // e.g. change ACTION_MULTIPLE to ACTION_DOWN. That way we don't have to allocate
+ // a new key event just to change its action field.
+ //
+ // Normally we expect event.getMetaState() to reflect the current meta-state; however,
+ // some software-generated key events may not have event.getMetaState() set, e.g. key
+ // events from Swype. Therefore, it's necessary to combine the key's meta-states
+ // with the meta-states that we keep separately in KeyListener
+ final int metaState = event.getMetaState() | savedMetaState;
+ final int unmodifiedMetaState = metaState &
+ ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK | KeyEvent.META_META_MASK);
+ final int unicodeChar = event.getUnicodeChar(metaState);
+ final int domPrintableKeyValue =
+ unicodeChar >= ' ' ? unicodeChar :
+ unmodifiedMetaState != metaState ? event.getUnicodeChar(unmodifiedMetaState) :
+ 0;
+ onKeyEvent(action, event.getKeyCode(), event.getScanCode(),
+ metaState, event.getEventTime(), unicodeChar,
+ // e.g. for Ctrl+A, Android returns 0 for unicodeChar,
+ // but Gecko expects 'a', so we return that in baseUnicodeChar.
+ event.getUnicodeChar(0), domPrintableKeyValue, event.getRepeatCount(),
+ event.getFlags(), isSynthesizedImeKey, event);
+ }
+
+ @WrapForJNI(dispatchTo = "proxy")
+ private native void onImeSynchronize();
+
+ @WrapForJNI(dispatchTo = "proxy")
+ private native void onImeReplaceText(int start, int end, String text);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ private native void onImeAddCompositionRange(int start, int end, int rangeType,
+ int rangeStyles, int rangeLineStyle,
+ boolean rangeBoldLine, int rangeForeColor,
+ int rangeBackColor, int rangeLineColor);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ private native void onImeUpdateComposition(int start, int end);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ private native void onImeRequestCursorUpdates(int requestMode);
+
+ /**
+ * Class that encapsulates asynchronous text editing. There are two copies of the
+ * text, a current copy and a shadow copy. Both can be modified independently through
+ * the current*** and shadow*** methods, respectively. The current copy can only be
+ * modified on the Gecko side and reflects the authoritative version of the text. The
+ * shadow copy can only be modified on the IC side and reflects what we think the
+ * current text is. Periodically, the shadow copy can be synced to the current copy
+ * through syncShadowText, so the shadow copy once again refers to the same text as
+ * the current copy.
+ */
+ private final class AsyncText {
+ // The current text is the update-to-date version of the text, and is only updated
+ // on the Gecko side.
+ private final SpannableStringBuilder mCurrentText = new SpannableStringBuilder();
+ // Track changes on the current side for syncing purposes.
+ // Start of the changed range in current text since last sync.
+ private int mCurrentStart = Integer.MAX_VALUE;
+ // End of the changed range (before the change) in current text since last sync.
+ private int mCurrentOldEnd;
+ // End of the changed range (after the change) in current text since last sync.
+ private int mCurrentNewEnd;
+ // Track selection changes separately.
+ private boolean mCurrentSelectionChanged;
+
+ // The shadow text is what we think the current text is on the Java side, and is
+ // periodically synced with the current text.
+ private final SpannableStringBuilder mShadowText = new SpannableStringBuilder();
+ // Track changes on the shadow side for syncing purposes.
+ // Start of the changed range in shadow text since last sync.
+ private int mShadowStart = Integer.MAX_VALUE;
+ // End of the changed range (before the change) in shadow text since last sync.
+ private int mShadowOldEnd;
+ // End of the changed range (after the change) in shadow text since last sync.
+ private int mShadowNewEnd;
+
+ private void addCurrentChangeLocked(final int start, final int oldEnd, final int newEnd) {
+ // Merge the new change into any existing change.
+ mCurrentStart = Math.min(mCurrentStart, start);
+ mCurrentOldEnd += Math.max(0, oldEnd - mCurrentNewEnd);
+ mCurrentNewEnd = newEnd + Math.max(0, mCurrentNewEnd - oldEnd);
+ }
+
+ public synchronized void currentReplace(final int start, final int end,
+ final CharSequence newText) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ mCurrentText.replace(start, end, newText);
+ addCurrentChangeLocked(start, end, start + newText.length());
+ }
+
+ public synchronized void currentSetSelection(final int start, final int end) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ Selection.setSelection(mCurrentText, start, end);
+ mCurrentSelectionChanged = true;
+ }
+
+ public synchronized void currentSetSpan(final Object obj, final int start,
+ final int end, final int flags) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ mCurrentText.setSpan(obj, start, end, flags);
+ addCurrentChangeLocked(start, end, end);
+ }
+
+ public synchronized void currentRemoveSpan(final Object obj) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ if (obj == null) {
+ mCurrentText.clearSpans();
+ addCurrentChangeLocked(0, mCurrentText.length(), mCurrentText.length());
+ return;
+ }
+ final int start = mCurrentText.getSpanStart(obj);
+ final int end = mCurrentText.getSpanEnd(obj);
+ if (start < 0 || end < 0) {
+ return;
+ }
+ mCurrentText.removeSpan(obj);
+ addCurrentChangeLocked(start, end, end);
+ }
+
+ // Return Spanned instead of Editable because the returned object is supposed to
+ // be read-only. Editing should be done through one of the current*** methods.
+ public Spanned getCurrentText() {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ return mCurrentText;
+ }
+
+ private void addShadowChange(final int start, final int oldEnd, final int newEnd) {
+ // Merge the new change into any existing change.
+ mShadowStart = Math.min(mShadowStart, start);
+ mShadowOldEnd += Math.max(0, oldEnd - mShadowNewEnd);
+ mShadowNewEnd = newEnd + Math.max(0, mShadowNewEnd - oldEnd);
+ }
+
+ public void shadowReplace(final int start, final int end,
+ final CharSequence newText)
+ {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ mShadowText.replace(start, end, newText);
+ addShadowChange(start, end, start + newText.length());
+ }
+
+ public void shadowSetSpan(final Object obj, final int start,
+ final int end, final int flags) {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ mShadowText.setSpan(obj, start, end, flags);
+ addShadowChange(start, end, end);
+ }
+
+ public void shadowRemoveSpan(final Object obj) {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ if (obj == null) {
+ mShadowText.clearSpans();
+ addShadowChange(0, mShadowText.length(), mShadowText.length());
+ return;
+ }
+ final int start = mShadowText.getSpanStart(obj);
+ final int end = mShadowText.getSpanEnd(obj);
+ if (start < 0 || end < 0) {
+ return;
+ }
+ mShadowText.removeSpan(obj);
+ addShadowChange(start, end, end);
+ }
+
+ // Return Spanned instead of Editable because the returned object is supposed to
+ // be read-only. Editing should be done through one of the shadow*** methods.
+ public Spanned getShadowText() {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ return mShadowText;
+ }
+
+ public synchronized void syncShadowText(final GeckoEditableListener listener) {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+
+ if (mCurrentStart > mCurrentOldEnd && mShadowStart > mShadowOldEnd) {
+ // Still check selection changes.
+ if (!mCurrentSelectionChanged) {
+ return;
+ }
+ final int start = Selection.getSelectionStart(mCurrentText);
+ final int end = Selection.getSelectionEnd(mCurrentText);
+ Selection.setSelection(mShadowText, start, end);
+ mCurrentSelectionChanged = false;
+
+ if (listener != null) {
+ listener.onSelectionChange();
+ }
+ return;
+ }
+
+ // Copy the portion of the current text that has changed over to the shadow
+ // text, with consideration for any concurrent changes in the shadow text.
+ final int start = Math.min(mShadowStart, mCurrentStart);
+ final int shadowEnd = mShadowNewEnd + Math.max(0, mCurrentOldEnd - mShadowOldEnd);
+ final int currentEnd = mCurrentNewEnd + Math.max(0, mShadowOldEnd - mCurrentOldEnd);
+
+ // Perform replacement in two steps (delete and insert) so that old spans are
+ // properly deleted before identical new spans are inserted. Otherwise the new
+ // spans won't be inserted due to the text already having the old spans.
+ mShadowText.delete(start, shadowEnd);
+ mShadowText.insert(start, mCurrentText, start, currentEnd);
+
+ // SpannableStringBuilder has some internal logic to fix up selections, but we
+ // don't want that, so we always fix up the selection a second time.
+ final int selStart = Selection.getSelectionStart(mCurrentText);
+ final int selEnd = Selection.getSelectionEnd(mCurrentText);
+ Selection.setSelection(mShadowText, selStart, selEnd);
+
+ if (DEBUG && !mShadowText.equals(mCurrentText)) {
+ // Sanity check.
+ throw new IllegalStateException("Failed to sync: " +
+ mShadowStart + '-' + mShadowOldEnd + '-' + mShadowNewEnd + '/' +
+ mCurrentStart + '-' + mCurrentOldEnd + '-' + mCurrentNewEnd);
+ }
+
+ if (listener != null) {
+ // Call onTextChange after selection fix-up but before we call
+ // onSelectionChange.
+ listener.onTextChange();
+
+ if (mCurrentSelectionChanged || (mCurrentOldEnd != mCurrentNewEnd &&
+ (selStart >= mCurrentStart || selEnd >= mCurrentStart))) {
+ listener.onSelectionChange();
+ }
+ }
+
+ // These values ensure the first change is properly added.
+ mCurrentStart = mShadowStart = Integer.MAX_VALUE;
+ mCurrentOldEnd = mShadowOldEnd = 0;
+ mCurrentNewEnd = mShadowNewEnd = 0;
+ mCurrentSelectionChanged = false;
+ }
+ }
+
+ /* An action that alters the Editable
+
+ Each action corresponds to a Gecko event. While the Gecko event is being sent to the Gecko
+ thread, the action stays on top of mActions queue. After the Gecko event is processed and
+ replied, the action is removed from the queue
+ */
+ private static final class Action {
+ // For input events (keypress, etc.); use with onImeSynchronize
+ static final int TYPE_EVENT = 0;
+ // For Editable.replace() call; use with onImeReplaceText
+ static final int TYPE_REPLACE_TEXT = 1;
+ // For Editable.setSpan() call; use with onImeSynchronize
+ static final int TYPE_SET_SPAN = 2;
+ // For Editable.removeSpan() call; use with onImeSynchronize
+ static final int TYPE_REMOVE_SPAN = 3;
+ // For switching handler; use with onImeSynchronize
+ static final int TYPE_SET_HANDLER = 4;
+
+ final int mType;
+ int mStart;
+ int mEnd;
+ CharSequence mSequence;
+ Object mSpanObject;
+ int mSpanFlags;
+ Handler mHandler;
+
+ Action(int type) {
+ mType = type;
+ }
+
+ static Action newReplaceText(CharSequence text, int start, int end) {
+ if (start < 0 || start > end) {
+ Log.e(LOGTAG, "invalid replace text offsets: " + start + " to " + end);
+ throw new IllegalArgumentException("invalid replace text offsets");
+ }
+
+ final Action action = new Action(TYPE_REPLACE_TEXT);
+ action.mSequence = text;
+ action.mStart = start;
+ action.mEnd = end;
+ return action;
+ }
+
+ static Action newSetSpan(Object object, int start, int end, int flags) {
+ if (start < 0 || start > end) {
+ Log.e(LOGTAG, "invalid span offsets: " + start + " to " + end);
+ throw new IllegalArgumentException("invalid span offsets");
+ }
+ final Action action = new Action(TYPE_SET_SPAN);
+ action.mSpanObject = object;
+ action.mStart = start;
+ action.mEnd = end;
+ action.mSpanFlags = flags;
+ return action;
+ }
+
+ static Action newRemoveSpan(Object object) {
+ final Action action = new Action(TYPE_REMOVE_SPAN);
+ action.mSpanObject = object;
+ return action;
+ }
+
+ static Action newSetHandler(Handler handler) {
+ final Action action = new Action(TYPE_SET_HANDLER);
+ action.mHandler = handler;
+ return action;
+ }
+ }
+
+ private void icOfferAction(final Action action) {
+ if (DEBUG) {
+ assertOnIcThread();
+ Log.d(LOGTAG, "offer: Action(" +
+ getConstantName(Action.class, "TYPE_", action.mType) + ")");
+ }
+
+ if (mListener == null) {
+ // We haven't initialized or we've been destroyed.
+ return;
+ }
+
+ mActions.offer(action);
+
+ switch (action.mType) {
+ case Action.TYPE_EVENT:
+ case Action.TYPE_SET_HANDLER:
+ onImeSynchronize();
+ break;
+
+ case Action.TYPE_SET_SPAN:
+ mText.shadowSetSpan(action.mSpanObject, action.mStart,
+ action.mEnd, action.mSpanFlags);
+ action.mSequence = TextUtils.substring(
+ mText.getShadowText(), action.mStart, action.mEnd);
+
+ mNeedUpdateComposition |= (action.mSpanFlags & Spanned.SPAN_INTERMEDIATE) == 0 &&
+ ((action.mSpanFlags & Spanned.SPAN_COMPOSING) != 0 ||
+ action.mSpanObject == Selection.SELECTION_START ||
+ action.mSpanObject == Selection.SELECTION_END);
+
+ onImeSynchronize();
+ break;
+
+ case Action.TYPE_REMOVE_SPAN:
+ final int flags = mText.getShadowText().getSpanFlags(action.mSpanObject);
+ mText.shadowRemoveSpan(action.mSpanObject);
+
+ mNeedUpdateComposition |= (flags & Spanned.SPAN_INTERMEDIATE) == 0 &&
+ (flags & Spanned.SPAN_COMPOSING) != 0;
+
+ onImeSynchronize();
+ break;
+
+ case Action.TYPE_REPLACE_TEXT:
+ // Always sync text after a replace action, so that if the Gecko
+ // text is not changed, we will revert the shadow text to before.
+ mNeedSync = true;
+
+ // Because we get composition styling here essentially for free,
+ // we don't need to check if we're in batch mode.
+ if (!icMaybeSendComposition(
+ action.mSequence, /* useEntireText */ true, /* notifyGecko */ false)) {
+ // Since we don't have a composition, we can try sending key events.
+ sendCharKeyEvents(action);
+ }
+ mText.shadowReplace(action.mStart, action.mEnd, action.mSequence);
+ onImeReplaceText(action.mStart, action.mEnd, action.mSequence.toString());
+ break;
+
+ default:
+ throw new IllegalStateException("Action not processed");
+ }
+ }
+
+ private KeyEvent [] synthesizeKeyEvents(CharSequence cs) {
+ try {
+ if (mKeyMap == null) {
+ mKeyMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+ }
+ } catch (Exception e) {
+ // KeyCharacterMap.UnavailableException is not found on Gingerbread;
+ // besides, it seems like HC and ICS will throw something other than
+ // KeyCharacterMap.UnavailableException; so use a generic Exception here
+ return null;
+ }
+ KeyEvent [] keyEvents = mKeyMap.getEvents(cs.toString().toCharArray());
+ if (keyEvents == null || keyEvents.length == 0) {
+ return null;
+ }
+ return keyEvents;
+ }
+
+ private void sendCharKeyEvents(Action action) {
+ if (action.mSequence.length() != 1 ||
+ (action.mSequence instanceof Spannable &&
+ ((Spannable)action.mSequence).nextSpanTransition(
+ -1, Integer.MAX_VALUE, null) < Integer.MAX_VALUE)) {
+ // Spans are not preserved when we use key events,
+ // so we need the sequence to not have any spans
+ return;
+ }
+ KeyEvent [] keyEvents = synthesizeKeyEvents(action.mSequence);
+ if (keyEvents == null) {
+ return;
+ }
+ for (KeyEvent event : keyEvents) {
+ if (KeyEvent.isModifierKey(event.getKeyCode())) {
+ continue;
+ }
+ if (event.getAction() == KeyEvent.ACTION_UP && mSuppressKeyUp) {
+ continue;
+ }
+ if (DEBUG) {
+ Log.d(LOGTAG, "sending: " + event);
+ }
+ onKeyEvent(event, event.getAction(),
+ /* metaState */ 0, /* isSynthesizedImeKey */ true);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ GeckoEditable(final GeckoView v) {
+ if (DEBUG) {
+ // Called by nsWindow.
+ ThreadUtils.assertOnGeckoThread();
+ }
+
+ mText = new AsyncText();
+ mActions = new ConcurrentLinkedQueue<Action>();
+
+ final Class<?>[] PROXY_INTERFACES = { Editable.class };
+ mProxy = (Editable)Proxy.newProxyInstance(
+ Editable.class.getClassLoader(),
+ PROXY_INTERFACES, this);
+
+ mIcRunHandler = mIcPostHandler = ThreadUtils.getUiHandler();
+
+ onViewChange(v);
+ }
+
+ @WrapForJNI(dispatchTo = "proxy") @Override
+ protected native void disposeNative();
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void onViewChange(final GeckoView v) {
+ if (DEBUG) {
+ // Called by nsWindow.
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "onViewChange(" + v + ")");
+ }
+
+ final GeckoEditableListener newListener =
+ v != null ? GeckoInputConnection.create(v, this) : null;
+
+ final Runnable setListenerRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) {
+ Log.d(LOGTAG, "onViewChange (set listener)");
+ }
+
+ mListener = newListener;
+
+ if (newListener == null) {
+ // We're being destroyed. By this point, we should have cleared all
+ // pending Runnables on the IC thread, so it's safe to call
+ // disposeNative here.
+ GeckoEditable.this.disposeNative();
+ }
+ }
+ };
+
+ // Post to UI thread first to make sure any code that is using the old input
+ // connection has finished running, before we switch to a new input connection or
+ // before we clear the input connection on destruction.
+ final Handler icHandler = mIcPostHandler;
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) {
+ Log.d(LOGTAG, "onViewChange (set IC)");
+ }
+
+ if (mView != null) {
+ // Detach the previous view.
+ mView.setInputConnectionListener(null);
+ }
+ if (v != null) {
+ // And attach the new view.
+ v.setInputConnectionListener((InputConnectionListener) newListener);
+ }
+
+ mView = v;
+ icHandler.post(setListenerRunnable);
+ }
+ });
+ }
+
+ private boolean onIcThread() {
+ return mIcRunHandler.getLooper() == Looper.myLooper();
+ }
+
+ private void assertOnIcThread() {
+ ThreadUtils.assertOnThread(mIcRunHandler.getLooper().getThread(), AssertBehavior.THROW);
+ }
+
+ private void geckoPostToIc(Runnable runnable) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ mIcPostHandler.post(runnable);
+ }
+
+ private Object getField(Object obj, String field, Object def) {
+ try {
+ return obj.getClass().getField(field).get(obj);
+ } catch (Exception e) {
+ return def;
+ }
+ }
+
+ /**
+ * Send composition ranges to Gecko for the entire shadow text.
+ */
+ private void icMaybeSendComposition() {
+ if (!mNeedUpdateComposition) {
+ return;
+ }
+
+ icMaybeSendComposition(mText.getShadowText(),
+ /* useEntireText */ false, /* notifyGecko */ true);
+ }
+
+ /**
+ * Send composition ranges to Gecko if the text has composing spans.
+ *
+ * @param sequence Text with possible composing spans
+ * @param useEntireText If text has composing spans, treat the entire text as
+ * a Gecko composition, instead of just the spanned part.
+ * @param notifyGecko Notify Gecko of the new composition ranges;
+ * otherwise, the caller is responsible for notifying Gecko.
+ * @return Whether there was a composition
+ */
+ private boolean icMaybeSendComposition(final CharSequence sequence,
+ final boolean useEntireText,
+ final boolean notifyGecko) {
+ mNeedUpdateComposition = false;
+
+ int selStart = Selection.getSelectionStart(sequence);
+ int selEnd = Selection.getSelectionEnd(sequence);
+
+ if (sequence instanceof Spanned) {
+ final Spanned text = (Spanned) sequence;
+ final Object[] spans = text.getSpans(0, text.length(), Object.class);
+ boolean found = false;
+ int composingStart = useEntireText ? 0 : Integer.MAX_VALUE;
+ int composingEnd = useEntireText ? text.length() : 0;
+
+ // Find existence and range of any composing spans (spans with the
+ // SPAN_COMPOSING flag set).
+ for (Object span : spans) {
+ if ((text.getSpanFlags(span) & Spanned.SPAN_COMPOSING) == 0) {
+ continue;
+ }
+ found = true;
+ if (useEntireText) {
+ break;
+ }
+ composingStart = Math.min(composingStart, text.getSpanStart(span));
+ composingEnd = Math.max(composingEnd, text.getSpanEnd(span));
+ }
+
+ if (useEntireText && (selStart < 0 || selEnd < 0)) {
+ selStart = composingEnd;
+ selEnd = composingEnd;
+ }
+
+ if (found) {
+ icSendComposition(text, selStart, selEnd, composingStart, composingEnd);
+ if (notifyGecko) {
+ onImeUpdateComposition(composingStart, composingEnd);
+ }
+ return true;
+ }
+ }
+
+ if (notifyGecko) {
+ // Set the selection by using a composition without ranges
+ onImeUpdateComposition(selStart, selEnd);
+ }
+
+ if (DEBUG) {
+ Log.d(LOGTAG, "icSendComposition(): no composition");
+ }
+ return false;
+ }
+
+ private void icSendComposition(final Spanned text,
+ final int selStart, final int selEnd,
+ final int composingStart, final int composingEnd) {
+ if (DEBUG) {
+ assertOnIcThread();
+ Log.d(LOGTAG, "icSendComposition(\"" + text + "\", " +
+ composingStart + ", " + composingEnd + ")");
+ }
+ if (DEBUG) {
+ Log.d(LOGTAG, " range = " + composingStart + "-" + composingEnd);
+ Log.d(LOGTAG, " selection = " + selStart + "-" + selEnd);
+ }
+
+ if (selEnd >= composingStart && selEnd <= composingEnd) {
+ onImeAddCompositionRange(
+ selEnd - composingStart, selEnd - composingStart,
+ IME_RANGE_CARETPOSITION, 0, 0, false, 0, 0, 0);
+ }
+
+ int rangeStart = composingStart;
+ TextPaint tp = new TextPaint();
+ TextPaint emptyTp = new TextPaint();
+ // set initial foreground color to 0, because we check for tp.getColor() == 0
+ // below to decide whether to pass a foreground color to Gecko
+ emptyTp.setColor(0);
+ do {
+ int rangeType, rangeStyles = 0, rangeLineStyle = IME_RANGE_LINE_NONE;
+ boolean rangeBoldLine = false;
+ int rangeForeColor = 0, rangeBackColor = 0, rangeLineColor = 0;
+ int rangeEnd = text.nextSpanTransition(rangeStart, composingEnd, Object.class);
+
+ if (selStart > rangeStart && selStart < rangeEnd) {
+ rangeEnd = selStart;
+ } else if (selEnd > rangeStart && selEnd < rangeEnd) {
+ rangeEnd = selEnd;
+ }
+ CharacterStyle[] styleSpans =
+ text.getSpans(rangeStart, rangeEnd, CharacterStyle.class);
+
+ if (DEBUG) {
+ Log.d(LOGTAG, " found " + styleSpans.length + " spans @ " +
+ rangeStart + "-" + rangeEnd);
+ }
+
+ if (styleSpans.length == 0) {
+ rangeType = (selStart == rangeStart && selEnd == rangeEnd)
+ ? IME_RANGE_SELECTEDRAWTEXT
+ : IME_RANGE_RAWINPUT;
+ } else {
+ rangeType = (selStart == rangeStart && selEnd == rangeEnd)
+ ? IME_RANGE_SELECTEDCONVERTEDTEXT
+ : IME_RANGE_CONVERTEDTEXT;
+ tp.set(emptyTp);
+ for (CharacterStyle span : styleSpans) {
+ span.updateDrawState(tp);
+ }
+ int tpUnderlineColor = 0;
+ float tpUnderlineThickness = 0.0f;
+
+ // These TextPaint fields only exist on Android ICS+ and are not in the SDK.
+ tpUnderlineColor = (Integer)getField(tp, "underlineColor", 0);
+ tpUnderlineThickness = (Float)getField(tp, "underlineThickness", 0.0f);
+ if (tpUnderlineColor != 0) {
+ rangeStyles |= IME_RANGE_UNDERLINE | IME_RANGE_LINECOLOR;
+ rangeLineColor = tpUnderlineColor;
+ // Approximately translate underline thickness to what Gecko understands
+ if (tpUnderlineThickness <= 0.5f) {
+ rangeLineStyle = IME_RANGE_LINE_DOTTED;
+ } else {
+ rangeLineStyle = IME_RANGE_LINE_SOLID;
+ if (tpUnderlineThickness >= 2.0f) {
+ rangeBoldLine = true;
+ }
+ }
+ } else if (tp.isUnderlineText()) {
+ rangeStyles |= IME_RANGE_UNDERLINE;
+ rangeLineStyle = IME_RANGE_LINE_SOLID;
+ }
+ if (tp.getColor() != 0) {
+ rangeStyles |= IME_RANGE_FORECOLOR;
+ rangeForeColor = tp.getColor();
+ }
+ if (tp.bgColor != 0) {
+ rangeStyles |= IME_RANGE_BACKCOLOR;
+ rangeBackColor = tp.bgColor;
+ }
+ }
+ onImeAddCompositionRange(
+ rangeStart - composingStart, rangeEnd - composingStart,
+ rangeType, rangeStyles, rangeLineStyle, rangeBoldLine,
+ rangeForeColor, rangeBackColor, rangeLineColor);
+ rangeStart = rangeEnd;
+
+ if (DEBUG) {
+ Log.d(LOGTAG, " added " + rangeType +
+ " : " + Integer.toHexString(rangeStyles) +
+ " : " + Integer.toHexString(rangeForeColor) +
+ " : " + Integer.toHexString(rangeBackColor));
+ }
+ } while (rangeStart < composingEnd);
+ }
+
+ // GeckoEditableClient interface
+
+ @Override
+ public void sendKeyEvent(final KeyEvent event, int action, int metaState) {
+ if (DEBUG) {
+ assertOnIcThread();
+ Log.d(LOGTAG, "sendKeyEvent(" + event + ", " + action + ", " + metaState + ")");
+ }
+ /*
+ We are actually sending two events to Gecko here,
+ 1. Event from the event parameter (key event)
+ 2. Sync event from the icOfferAction call
+ The first event is a normal event that does not reply back to us,
+ the second sync event will have a reply, during which we see that there is a pending
+ event-type action, and update the shadow text accordingly.
+ */
+ icMaybeSendComposition();
+ onKeyEvent(event, action, metaState, /* isSynthesizedImeKey */ false);
+ icOfferAction(new Action(Action.TYPE_EVENT));
+ }
+
+ @Override
+ public Editable getEditable() {
+ if (!onIcThread()) {
+ // Android may be holding an old InputConnection; ignore
+ if (DEBUG) {
+ Log.i(LOGTAG, "getEditable() called on non-IC thread");
+ }
+ return null;
+ }
+ if (mListener == null) {
+ // We haven't initialized or we've been destroyed.
+ return null;
+ }
+ return mProxy;
+ }
+
+ @Override
+ public void setBatchMode(boolean inBatchMode) {
+ if (!onIcThread()) {
+ // Android may be holding an old InputConnection; ignore
+ if (DEBUG) {
+ Log.i(LOGTAG, "setBatchMode() called on non-IC thread");
+ }
+ return;
+ }
+
+ mInBatchMode = inBatchMode;
+
+ if (!inBatchMode && mNeedSync) {
+ icSyncShadowText();
+ }
+ }
+
+ /* package */ void icSyncShadowText() {
+ if (mListener == null) {
+ // Not yet attached or already destroyed.
+ return;
+ }
+
+ if (mInBatchMode || !mActions.isEmpty()) {
+ mNeedSync = true;
+ return;
+ }
+
+ mNeedSync = false;
+ mText.syncShadowText(mListener);
+ }
+
+ private void geckoScheduleSyncShadowText() {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ icSyncShadowText();
+ }
+ });
+ }
+
+ @Override
+ public void setSuppressKeyUp(boolean suppress) {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ // Suppress key up event generated as a result of
+ // translating characters to key events
+ mSuppressKeyUp = suppress;
+ }
+
+ @Override // GeckoEditableClient
+ public Handler setInputConnectionHandler(final Handler handler) {
+ if (handler == mIcRunHandler) {
+ return mIcRunHandler;
+ }
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+
+ // There are three threads at this point: Gecko thread, old IC thread, and new IC
+ // thread, and we want to safely switch from old IC thread to new IC thread.
+ // We first send a TYPE_SET_HANDLER action to the Gecko thread; this ensures that
+ // the Gecko thread is stopped at a known point. At the same time, the old IC
+ // thread blocks on the action; this ensures that the old IC thread is stopped at
+ // a known point. Finally, inside the Gecko thread, we post a Runnable to the old
+ // IC thread; this Runnable switches from old IC thread to new IC thread. We
+ // switch IC thread on the old IC thread to ensure any pending Runnables on the
+ // old IC thread are processed before we switch over. Inside the Gecko thread, we
+ // also post a Runnable to the new IC thread; this Runnable blocks until the
+ // switch is complete; this ensures that the new IC thread won't accept
+ // InputConnection calls until after the switch.
+
+ handler.post(new Runnable() { // Make the new IC thread wait.
+ @Override
+ public void run() {
+ synchronized (handler) {
+ while (mIcRunHandler != handler) {
+ try {
+ handler.wait();
+ } catch (final InterruptedException e) {
+ }
+ }
+ }
+ }
+ });
+
+ icOfferAction(Action.newSetHandler(handler));
+ return handler;
+ }
+
+ @Override // GeckoEditableClient
+ public void postToInputConnection(final Runnable runnable) {
+ mIcPostHandler.post(runnable);
+ }
+
+ @Override // GeckoEditableClient
+ public void requestCursorUpdates(int requestMode) {
+ onImeRequestCursorUpdates(requestMode);
+ }
+
+ private void geckoSetIcHandler(final Handler newHandler) {
+ geckoPostToIc(new Runnable() { // posting to old IC thread
+ @Override
+ public void run() {
+ synchronized (newHandler) {
+ mIcRunHandler = newHandler;
+ newHandler.notify();
+ }
+ }
+ });
+
+ // At this point, all future Runnables should be posted to the new IC thread, but
+ // we don't switch mIcRunHandler yet because there may be pending Runnables on the
+ // old IC thread still waiting to run.
+ mIcPostHandler = newHandler;
+ }
+
+ private void geckoActionReply(final Action action) {
+ if (!mGeckoFocused) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "discarding stale reply");
+ }
+ return;
+ }
+
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "reply: Action(" +
+ getConstantName(Action.class, "TYPE_", action.mType) + ")");
+ }
+ switch (action.mType) {
+ case Action.TYPE_SET_SPAN:
+ final int len = mText.getCurrentText().length();
+ if (action.mStart > len || action.mEnd > len ||
+ !TextUtils.substring(mText.getCurrentText(), action.mStart,
+ action.mEnd).equals(action.mSequence)) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "discarding stale set span call");
+ }
+ break;
+ }
+ mText.currentSetSpan(action.mSpanObject, action.mStart, action.mEnd, action.mSpanFlags);
+ break;
+
+ case Action.TYPE_REMOVE_SPAN:
+ mText.currentRemoveSpan(action.mSpanObject);
+ break;
+
+ case Action.TYPE_SET_HANDLER:
+ geckoSetIcHandler(action.mHandler);
+ break;
+ }
+ }
+
+ private void notifyCommitComposition() {
+ // Gecko already committed its composition. However, Android keyboards
+ // have trouble dealing with us removing the composition manually on
+ // the Java side. Therefore, we keep the composition intact on the Java
+ // side. The text content should still be in-sync on both sides.
+ }
+
+ private void notifyCancelComposition() {
+ // Composition should have been canceled on our side
+ // through text update notifications; verify that here.
+ if (DEBUG) {
+ final Spanned text = mText.getCurrentText();
+ final Object[] spans = text.getSpans(0, text.length(), Object.class);
+ for (Object span : spans) {
+ if ((text.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) {
+ throw new IllegalStateException("composition not cancelled");
+ }
+ }
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void notifyIME(final int type) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ // NOTIFY_IME_REPLY_EVENT is logged separately, inside geckoActionReply()
+ if (type != GeckoEditableListener.NOTIFY_IME_REPLY_EVENT) {
+ Log.d(LOGTAG, "notifyIME(" +
+ getConstantName(GeckoEditableListener.class, "NOTIFY_IME_", type) +
+ ")");
+ }
+ }
+
+ if (type == GeckoEditableListener.NOTIFY_IME_REPLY_EVENT) {
+ geckoActionReply(mActions.poll());
+ if (!mGeckoFocused || !mActions.isEmpty()) {
+ // Only post to IC thread below when the queue is empty.
+ return;
+ }
+ } else if (type == GeckoEditableListener.NOTIFY_IME_TO_COMMIT_COMPOSITION) {
+ notifyCommitComposition();
+ return;
+ } else if (type == GeckoEditableListener.NOTIFY_IME_TO_CANCEL_COMPOSITION) {
+ notifyCancelComposition();
+ return;
+ }
+
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ if (type == GeckoEditableListener.NOTIFY_IME_REPLY_EVENT) {
+ if (mNeedSync) {
+ icSyncShadowText();
+ }
+ return;
+ }
+
+ if (type == GeckoEditableListener.NOTIFY_IME_OF_FOCUS && mListener != null) {
+ mNeedSync = false;
+ mText.syncShadowText(/* listener */ null);
+ }
+
+ if (mListener != null) {
+ mListener.notifyIME(type);
+ }
+ }
+ });
+
+ // Update the mGeckoFocused flag.
+ if (type == GeckoEditableListener.NOTIFY_IME_OF_BLUR) {
+ mGeckoFocused = false;
+ } else if (type == GeckoEditableListener.NOTIFY_IME_OF_FOCUS) {
+ mGeckoFocused = true;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void notifyIMEContext(final int state, final String typeHint,
+ final String modeHint, final String actionHint) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "notifyIMEContext(" +
+ getConstantName(GeckoEditableListener.class, "IME_STATE_", state) +
+ ", \"" + typeHint + "\", \"" + modeHint + "\", \"" + actionHint + "\")");
+ }
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ if (mListener == null) {
+ return;
+ }
+ mListener.notifyIMEContext(state, typeHint, modeHint, actionHint);
+ }
+ });
+ }
+
+ @WrapForJNI(calledFrom = "gecko", exceptionMode = "ignore")
+ private void onSelectionChange(final int start, final int end) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "onSelectionChange(" + start + ", " + end + ")");
+ }
+
+ final int currentLength = mText.getCurrentText().length();
+ if (start < 0 || start > currentLength || end < 0 || end > currentLength) {
+ Log.e(LOGTAG, "invalid selection notification range: " +
+ start + " to " + end + ", length: " + currentLength);
+ throw new IllegalArgumentException("invalid selection notification range");
+ }
+
+ if (mIgnoreSelectionChange) {
+ mIgnoreSelectionChange = false;
+ } else {
+ mText.currentSetSelection(start, end);
+ }
+
+ geckoScheduleSyncShadowText();
+ }
+
+ private boolean geckoIsSameText(int start, int oldEnd, CharSequence newText) {
+ return oldEnd - start == newText.length() &&
+ TextUtils.regionMatches(mText.getCurrentText(), start, newText, 0, oldEnd - start);
+ }
+
+ @WrapForJNI(calledFrom = "gecko", exceptionMode = "ignore")
+ private void onTextChange(final CharSequence text, final int start,
+ final int unboundedOldEnd, final int unboundedNewEnd) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ StringBuilder sb = new StringBuilder("onTextChange(");
+ debugAppend(sb, text);
+ sb.append(", ").append(start).append(", ")
+ .append(unboundedOldEnd).append(", ")
+ .append(unboundedNewEnd).append(")");
+ Log.d(LOGTAG, sb.toString());
+ }
+ if (start < 0 || start > unboundedOldEnd) {
+ Log.e(LOGTAG, "invalid text notification range: " +
+ start + " to " + unboundedOldEnd);
+ throw new IllegalArgumentException("invalid text notification range");
+ }
+
+ final int currentLength = mText.getCurrentText().length();
+
+ /* For the "end" parameters, Gecko can pass in a large
+ number to denote "end of the text". Fix that here */
+ final int oldEnd = unboundedOldEnd > currentLength ? currentLength : unboundedOldEnd;
+ // new end should always match text
+ if (unboundedOldEnd <= currentLength && unboundedNewEnd != (start + text.length())) {
+ Log.e(LOGTAG, "newEnd does not match text: " + unboundedNewEnd + " vs " +
+ (start + text.length()));
+ throw new IllegalArgumentException("newEnd does not match text");
+ }
+
+ final int newEnd = start + text.length();
+ final Action action = mActions.peek();
+
+ if (start == 0 && unboundedOldEnd > currentLength) {
+ // Simply replace the text for newly-focused editors. Replace in two steps to
+ // properly clear composing spans that span the whole range.
+ mText.currentReplace(0, currentLength, "");
+ mText.currentReplace(0, 0, text);
+
+ // Don't ignore the next selection change because we are re-syncing with Gecko
+ mIgnoreSelectionChange = false;
+
+ } else if (action != null &&
+ action.mType == Action.TYPE_REPLACE_TEXT &&
+ start <= action.mStart &&
+ oldEnd >= action.mEnd &&
+ newEnd >= action.mStart + action.mSequence.length()) {
+
+ // Try to preserve both old spans and new spans in action.mSequence.
+ // indexInText is where we can find waction.mSequence within the passed in text.
+ final int startWithinText = action.mStart - start;
+ int indexInText = TextUtils.indexOf(text, action.mSequence, startWithinText);
+ if (indexInText < 0 && startWithinText >= action.mSequence.length()) {
+ indexInText = text.toString().lastIndexOf(action.mSequence.toString(),
+ startWithinText);
+ }
+
+ if (indexInText < 0) {
+ // Text was changed from under us. We are forced to discard any new spans.
+ mText.currentReplace(start, oldEnd, text);
+
+ // Don't ignore the next selection change because we are forced to re-sync
+ // with Gecko here.
+ mIgnoreSelectionChange = false;
+
+ } else if (indexInText == 0 && text.length() == action.mSequence.length() &&
+ oldEnd - start == action.mEnd - action.mStart) {
+ // The new change exactly matches our saved change, so do a direct replace.
+ mText.currentReplace(start, oldEnd, action.mSequence);
+
+ // Ignore the next selection change because the selection change is a
+ // side-effect of the replace-text event we sent.
+ mIgnoreSelectionChange = true;
+
+ } else {
+ // The sequence is embedded within the changed text, so we have to perform
+ // replacement in parts. First replace part of text before the sequence.
+ mText.currentReplace(start, action.mStart, text.subSequence(0, indexInText));
+
+ // Then replace part of the text after the sequence.
+ final int actionStart = indexInText + start;
+ final int delta = actionStart - action.mStart;
+ final int actionEnd = delta + action.mEnd;
+
+ final Spanned currentText = mText.getCurrentText();
+ final boolean resetSelStart = Selection.getSelectionStart(currentText) == actionEnd;
+ final boolean resetSelEnd = Selection.getSelectionEnd(currentText) == actionEnd;
+
+ mText.currentReplace(actionEnd, delta + oldEnd, text.subSequence(
+ indexInText + action.mSequence.length(), text.length()));
+
+ // The replacement above may have shifted our selection, if the selection
+ // was at the start of the replacement range. If so, we need to reset
+ // our selection to the previous position.
+ if (resetSelStart || resetSelEnd) {
+ mText.currentSetSelection(
+ resetSelStart ? actionEnd : Selection.getSelectionStart(currentText),
+ resetSelEnd ? actionEnd : Selection.getSelectionEnd(currentText));
+ }
+
+ // Finally replace the sequence itself to preserve new spans.
+ mText.currentReplace(actionStart, actionEnd, action.mSequence);
+
+ // Ignore the next selection change because the selection change is a
+ // side-effect of the replace-text event we sent.
+ mIgnoreSelectionChange = true;
+ }
+
+ } else if (geckoIsSameText(start, oldEnd, text)) {
+ // Nothing to do because the text is the same. This could happen when
+ // the composition is updated for example, in which case we want to keep the
+ // Java selection.
+ mIgnoreSelectionChange = mIgnoreSelectionChange ||
+ (action != null && action.mType == Action.TYPE_REPLACE_TEXT);
+ return;
+
+ } else {
+ // Gecko side initiated the text change. Replace in two steps to properly
+ // clear composing spans that span the whole range.
+ mText.currentReplace(start, oldEnd, "");
+ mText.currentReplace(start, start, text);
+
+ // Don't ignore the next selection change because we are forced to re-sync
+ // with Gecko here.
+ mIgnoreSelectionChange = false;
+ }
+
+ // onTextChange is always followed by onSelectionChange, so we let
+ // onSelectionChange schedule a shadow text sync.
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void onDefaultKeyEvent(final KeyEvent event) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ StringBuilder sb = new StringBuilder("onDefaultKeyEvent(");
+ sb.append("action=").append(event.getAction()).append(", ")
+ .append("keyCode=").append(event.getKeyCode()).append(", ")
+ .append("metaState=").append(event.getMetaState()).append(", ")
+ .append("time=").append(event.getEventTime()).append(", ")
+ .append("repeatCount=").append(event.getRepeatCount()).append(")");
+ Log.d(LOGTAG, sb.toString());
+ }
+
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ if (mListener == null) {
+ return;
+ }
+ mListener.onDefaultKeyEvent(event);
+ }
+ });
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void updateCompositionRects(final RectF[] aRects) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "updateCompositionRects(aRects.length = " + aRects.length + ")");
+ }
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ if (mListener == null) {
+ return;
+ }
+ mListener.updateCompositionRects(aRects);
+ }
+ });
+ }
+
+ // InvocationHandler interface
+
+ static String getConstantName(Class<?> cls, String prefix, Object value) {
+ for (Field fld : cls.getDeclaredFields()) {
+ try {
+ if (fld.getName().startsWith(prefix) &&
+ fld.get(null).equals(value)) {
+ return fld.getName();
+ }
+ } catch (IllegalAccessException e) {
+ }
+ }
+ return String.valueOf(value);
+ }
+
+ static StringBuilder debugAppend(StringBuilder sb, Object obj) {
+ if (obj == null) {
+ sb.append("null");
+ } else if (obj instanceof GeckoEditable) {
+ sb.append("GeckoEditable");
+ } else if (Proxy.isProxyClass(obj.getClass())) {
+ debugAppend(sb, Proxy.getInvocationHandler(obj));
+ } else if (obj instanceof CharSequence) {
+ sb.append('"').append(obj.toString().replace('\n', '\u21b2')).append('"');
+ } else if (obj.getClass().isArray()) {
+ sb.append(obj.getClass().getComponentType().getSimpleName()).append('[')
+ .append(Array.getLength(obj)).append(']');
+ } else {
+ sb.append(obj);
+ }
+ return sb;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args)
+ throws Throwable {
+ Object target;
+ final Class<?> methodInterface = method.getDeclaringClass();
+ if (DEBUG) {
+ // Editable methods should all be called from the IC thread
+ assertOnIcThread();
+ }
+ if (methodInterface == Editable.class ||
+ methodInterface == Appendable.class ||
+ methodInterface == Spannable.class) {
+ // Method alters the Editable; route calls to our implementation
+ target = this;
+ } else {
+ target = mText.getShadowText();
+ }
+ Object ret;
+ try {
+ ret = method.invoke(target, args);
+ } catch (InvocationTargetException e) {
+ // Bug 817386
+ // Most likely Gecko has changed the text while GeckoInputConnection is
+ // trying to access the text. If we pass through the exception here, Fennec
+ // will crash due to a lack of exception handler. Log the exception and
+ // return an empty value instead.
+ if (!(e.getCause() instanceof IndexOutOfBoundsException)) {
+ // Only handle IndexOutOfBoundsException for now,
+ // as other exceptions might signal other bugs
+ throw e;
+ }
+ Log.w(LOGTAG, "Exception in GeckoEditable." + method.getName(), e.getCause());
+ Class<?> retClass = method.getReturnType();
+ if (retClass == Character.TYPE) {
+ ret = '\0';
+ } else if (retClass == Integer.TYPE) {
+ ret = 0;
+ } else if (retClass == String.class) {
+ ret = "";
+ } else {
+ ret = null;
+ }
+ }
+ if (DEBUG) {
+ StringBuilder log = new StringBuilder(method.getName());
+ log.append("(");
+ if (args != null) {
+ for (Object arg : args) {
+ debugAppend(log, arg).append(", ");
+ }
+ if (args.length > 0) {
+ log.setLength(log.length() - 2);
+ }
+ }
+ if (method.getReturnType().equals(Void.TYPE)) {
+ log.append(")");
+ } else {
+ debugAppend(log.append(") = "), ret);
+ }
+ Log.d(LOGTAG, log.toString());
+ }
+ return ret;
+ }
+
+ // Spannable interface
+
+ @Override
+ public void removeSpan(Object what) {
+ if (what == null) {
+ return;
+ }
+
+ if (what == Selection.SELECTION_START ||
+ what == Selection.SELECTION_END) {
+ Log.w(LOGTAG, "selection removed with removeSpan()");
+ }
+
+ icOfferAction(Action.newRemoveSpan(what));
+ }
+
+ @Override
+ public void setSpan(Object what, int start, int end, int flags) {
+ icOfferAction(Action.newSetSpan(what, start, end, flags));
+ }
+
+ // Appendable interface
+
+ @Override
+ public Editable append(CharSequence text) {
+ return replace(mProxy.length(), mProxy.length(), text, 0, text.length());
+ }
+
+ @Override
+ public Editable append(CharSequence text, int start, int end) {
+ return replace(mProxy.length(), mProxy.length(), text, start, end);
+ }
+
+ @Override
+ public Editable append(char text) {
+ return replace(mProxy.length(), mProxy.length(), String.valueOf(text), 0, 1);
+ }
+
+ // Editable interface
+
+ @Override
+ public InputFilter[] getFilters() {
+ return mFilters;
+ }
+
+ @Override
+ public void setFilters(InputFilter[] filters) {
+ mFilters = filters;
+ }
+
+ @Override
+ public void clearSpans() {
+ /* XXX this clears the selection spans too,
+ but there is no way to clear the corresponding selection in Gecko */
+ Log.w(LOGTAG, "selection cleared with clearSpans()");
+ icOfferAction(Action.newRemoveSpan(/* what */ null));
+ }
+
+ @Override
+ public Editable replace(int st, int en,
+ CharSequence source, int start, int end) {
+
+ CharSequence text = source;
+ if (start < 0 || start > end || end > text.length()) {
+ Log.e(LOGTAG, "invalid replace offsets: " +
+ start + " to " + end + ", length: " + text.length());
+ throw new IllegalArgumentException("invalid replace offsets");
+ }
+ if (start != 0 || end != text.length()) {
+ text = text.subSequence(start, end);
+ }
+ if (mFilters != null) {
+ // Filter text before sending the request to Gecko
+ for (int i = 0; i < mFilters.length; ++i) {
+ final CharSequence cs = mFilters[i].filter(
+ text, 0, text.length(), mProxy, st, en);
+ if (cs != null) {
+ text = cs;
+ }
+ }
+ }
+ if (text == source) {
+ // Always create a copy
+ text = new SpannableString(source);
+ }
+ icOfferAction(Action.newReplaceText(text, Math.min(st, en), Math.max(st, en)));
+ return mProxy;
+ }
+
+ @Override
+ public void clear() {
+ replace(0, mProxy.length(), "", 0, 0);
+ }
+
+ @Override
+ public Editable delete(int st, int en) {
+ return replace(st, en, "", 0, 0);
+ }
+
+ @Override
+ public Editable insert(int where, CharSequence text,
+ int start, int end) {
+ return replace(where, where, text, start, end);
+ }
+
+ @Override
+ public Editable insert(int where, CharSequence text) {
+ return replace(where, where, text, 0, text.length());
+ }
+
+ @Override
+ public Editable replace(int st, int en, CharSequence text) {
+ return replace(st, en, text, 0, text.length());
+ }
+
+ /* GetChars interface */
+
+ @Override
+ public void getChars(int start, int end, char[] dest, int destoff) {
+ /* overridden Editable interface methods in GeckoEditable must not be called directly
+ outside of GeckoEditable. Instead, the call must go through mProxy, which ensures
+ that Java is properly synchronized with Gecko */
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ /* Spanned interface */
+
+ @Override
+ public int getSpanEnd(Object tag) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public int getSpanFlags(Object tag) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public int getSpanStart(Object tag) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public <T> T[] getSpans(int start, int end, Class<T> type) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ @SuppressWarnings("rawtypes") // nextSpanTransition uses raw Class in its Android declaration
+ public int nextSpanTransition(int start, int limit, Class type) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ /* CharSequence interface */
+
+ @Override
+ public char charAt(int index) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public int length() {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public String toString() {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+}
+
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableClient.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableClient.java
new file mode 100644
index 000000000..5e721b3af
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableClient.java
@@ -0,0 +1,33 @@
+/* -*- 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;
+
+import android.os.Handler;
+import android.text.Editable;
+import android.view.KeyEvent;
+
+/**
+ * Interface for the IC thread.
+ */
+interface GeckoEditableClient {
+ void sendKeyEvent(KeyEvent event, int action, int metaState);
+ Editable getEditable();
+ void setBatchMode(boolean isBatchMode);
+ void setSuppressKeyUp(boolean suppress);
+ Handler setInputConnectionHandler(Handler handler);
+ void postToInputConnection(Runnable runnable);
+
+ // The following value is used by requestCursorUpdates
+
+ // ONE_SHOT calls updateCompositionRects() after getting current composing character rects.
+ public static final int ONE_SHOT = 1;
+ // START_MONITOR start the monitor for composing character rects. If is is updaed, call updateCompositionRects()
+ public static final int START_MONITOR = 2;
+ // ENDT_MONITOR stops the monitor for composing character rects.
+ public static final int END_MONITOR = 3;
+
+ void requestCursorUpdates(int requestMode);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableListener.java
new file mode 100644
index 000000000..db594aaf7
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableListener.java
@@ -0,0 +1,43 @@
+/* -*- 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;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+import android.graphics.RectF;
+import android.view.KeyEvent;
+
+/**
+ * Interface for the Editable to listen on the Gecko thread, as well as for the IC thread to listen
+ * to the Editable.
+ */
+interface GeckoEditableListener {
+ // IME notification type for notifyIME(), corresponding to NotificationToIME enum in Gecko
+ @WrapForJNI
+ int NOTIFY_IME_OPEN_VKB = -2;
+ @WrapForJNI
+ int NOTIFY_IME_REPLY_EVENT = -1;
+ @WrapForJNI
+ int NOTIFY_IME_OF_FOCUS = 1;
+ @WrapForJNI
+ int NOTIFY_IME_OF_BLUR = 2;
+ @WrapForJNI
+ int NOTIFY_IME_TO_COMMIT_COMPOSITION = 8;
+ @WrapForJNI
+ int NOTIFY_IME_TO_CANCEL_COMPOSITION = 9;
+ // IME enabled state for notifyIMEContext()
+ int IME_STATE_DISABLED = 0;
+ int IME_STATE_ENABLED = 1;
+ int IME_STATE_PASSWORD = 2;
+ int IME_STATE_PLUGIN = 3;
+
+ void notifyIME(int type);
+ void notifyIMEContext(int state, String typeHint, String modeHint, String actionHint);
+ void onSelectionChange();
+ void onTextChange();
+ void onDefaultKeyEvent(KeyEvent event);
+ void updateCompositionRects(final RectF[] aRects);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoHalDefines.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoHalDefines.java
new file mode 100644
index 000000000..3d9b97427
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoHalDefines.java
@@ -0,0 +1,27 @@
+/* -*- 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;
+
+public class GeckoHalDefines
+{
+ /*
+ * Keep these values consistent with |SensorType| in HalSensor.h
+ */
+ public static final int SENSOR_ORIENTATION = 0;
+ public static final int SENSOR_ACCELERATION = 1;
+ public static final int SENSOR_PROXIMITY = 2;
+ public static final int SENSOR_LINEAR_ACCELERATION = 3;
+ public static final int SENSOR_GYROSCOPE = 4;
+ public static final int SENSOR_LIGHT = 5;
+ public static final int SENSOR_ROTATION_VECTOR = 6;
+ public static final int SENSOR_GAME_ROTATION_VECTOR = 7;
+
+ public static final int SENSOR_ACCURACY_UNKNOWN = -1;
+ public static final int SENSOR_ACCURACY_UNRELIABLE = 0;
+ public static final int SENSOR_ACCURACY_LOW = 1;
+ public static final int SENSOR_ACCURACY_MED = 2;
+ public static final int SENSOR_ACCURACY_HIGH = 3;
+};
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
new file mode 100644
index 000000000..a80be0bce
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
@@ -0,0 +1,1060 @@
+/* -*- 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 java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.concurrent.SynchronousQueue;
+
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
+import org.mozilla.gecko.util.Clipboard;
+import org.mozilla.gecko.util.GamepadUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.media.AudioManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.Selection;
+import android.text.SpannableString;
+import android.text.method.KeyListener;
+import android.text.method.TextKeyListener;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.CursorAnchorInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+
+class GeckoInputConnection
+ extends BaseInputConnection
+ implements InputConnectionListener, GeckoEditableListener {
+
+ private static final boolean DEBUG = false;
+ protected static final String LOGTAG = "GeckoInputConnection";
+
+ private static final String CUSTOM_HANDLER_TEST_METHOD = "testInputConnection";
+ private static final String CUSTOM_HANDLER_TEST_CLASS =
+ "org.mozilla.gecko.tests.components.GeckoViewComponent$TextInput";
+
+ private static final int INLINE_IME_MIN_DISPLAY_SIZE = 480;
+
+ private static Handler sBackgroundHandler;
+
+ // Managed only by notifyIMEContext; see comments in notifyIMEContext
+ private int mIMEState;
+ private String mIMETypeHint = "";
+ private String mIMEModeHint = "";
+ private String mIMEActionHint = "";
+ private boolean mFocused;
+
+ private String mCurrentInputMethod = "";
+
+ private final View mView;
+ private final GeckoEditableClient mEditableClient;
+ protected int mBatchEditCount;
+ private ExtractedTextRequest mUpdateRequest;
+ private final ExtractedText mUpdateExtract = new ExtractedText();
+ private final InputConnection mKeyInputConnection;
+ private CursorAnchorInfo.Builder mCursorAnchorInfoBuilder;
+
+ // Prevent showSoftInput and hideSoftInput from causing reentrant calls on some devices.
+ private volatile boolean mSoftInputReentrancyGuard;
+
+ public static GeckoEditableListener create(View targetView,
+ GeckoEditableClient editable) {
+ if (DEBUG)
+ return DebugGeckoInputConnection.create(targetView, editable);
+ else
+ return new GeckoInputConnection(targetView, editable);
+ }
+
+ protected GeckoInputConnection(View targetView,
+ GeckoEditableClient editable) {
+ super(targetView, true);
+ mView = targetView;
+ mEditableClient = editable;
+ mIMEState = IME_STATE_DISABLED;
+ // InputConnection that sends keys for plugins, which don't have full editors
+ mKeyInputConnection = new BaseInputConnection(targetView, false);
+ }
+
+ @Override
+ public synchronized boolean beginBatchEdit() {
+ mBatchEditCount++;
+ if (mBatchEditCount == 1) {
+ mEditableClient.setBatchMode(true);
+ }
+ return true;
+ }
+
+ @Override
+ public synchronized boolean endBatchEdit() {
+ if (mBatchEditCount <= 0) {
+ Log.w(LOGTAG, "endBatchEdit() called, but mBatchEditCount <= 0?!");
+ return true;
+ }
+
+ mBatchEditCount--;
+ if (mBatchEditCount != 0) {
+ return true;
+ }
+
+ // setBatchMode will call onTextChange and/or onSelectionChange for us.
+ mEditableClient.setBatchMode(false);
+ return true;
+ }
+
+ @Override
+ public Editable getEditable() {
+ return mEditableClient.getEditable();
+ }
+
+ @Override
+ public boolean performContextMenuAction(int id) {
+ Editable editable = getEditable();
+ if (editable == null) {
+ return false;
+ }
+ int selStart = Selection.getSelectionStart(editable);
+ int selEnd = Selection.getSelectionEnd(editable);
+
+ switch (id) {
+ case android.R.id.selectAll:
+ setSelection(0, editable.length());
+ break;
+ case android.R.id.cut:
+ // If selection is empty, we'll select everything
+ if (selStart == selEnd) {
+ // Fill the clipboard
+ Clipboard.setText(editable);
+ editable.clear();
+ } else {
+ Clipboard.setText(
+ editable.toString().substring(
+ Math.min(selStart, selEnd),
+ Math.max(selStart, selEnd)));
+ editable.delete(selStart, selEnd);
+ }
+ break;
+ case android.R.id.paste:
+ commitText(Clipboard.getText(), 1);
+ break;
+ case android.R.id.copy:
+ // Copy the current selection or the empty string if nothing is selected.
+ String copiedText = selStart == selEnd ? "" :
+ editable.toString().substring(
+ Math.min(selStart, selEnd),
+ Math.max(selStart, selEnd));
+ Clipboard.setText(copiedText);
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ public ExtractedText getExtractedText(ExtractedTextRequest req, int flags) {
+ if (req == null)
+ return null;
+
+ if ((flags & GET_EXTRACTED_TEXT_MONITOR) != 0)
+ mUpdateRequest = req;
+
+ Editable editable = getEditable();
+ if (editable == null) {
+ return null;
+ }
+ int selStart = Selection.getSelectionStart(editable);
+ int selEnd = Selection.getSelectionEnd(editable);
+
+ ExtractedText extract = new ExtractedText();
+ extract.flags = 0;
+ extract.partialStartOffset = -1;
+ extract.partialEndOffset = -1;
+ extract.selectionStart = selStart;
+ extract.selectionEnd = selEnd;
+ extract.startOffset = 0;
+ if ((req.flags & GET_TEXT_WITH_STYLES) != 0) {
+ extract.text = new SpannableString(editable);
+ } else {
+ extract.text = editable.toString();
+ }
+ return extract;
+ }
+
+ private View getView() {
+ return mView;
+ }
+
+ private InputMethodManager getInputMethodManager() {
+ View view = getView();
+ if (view == null) {
+ return null;
+ }
+ Context context = view.getContext();
+ return InputMethods.getInputMethodManager(context);
+ }
+
+ private void showSoftInput() {
+ if (mSoftInputReentrancyGuard) {
+ return;
+ }
+ final View v = getView();
+ final InputMethodManager imm = getInputMethodManager();
+ if (v == null || imm == null) {
+ return;
+ }
+
+ v.post(new Runnable() {
+ @Override
+ public void run() {
+ if (v.hasFocus() && !imm.isActive(v)) {
+ // Marshmallow workaround: The view has focus but it is not the active
+ // view for the input method. (Bug 1211848)
+ v.clearFocus();
+ v.requestFocus();
+ }
+ GeckoAppShell.getLayerView().getDynamicToolbarAnimator().showToolbar(/*immediately*/true);
+ mSoftInputReentrancyGuard = true;
+ imm.showSoftInput(v, 0);
+ mSoftInputReentrancyGuard = false;
+ }
+ });
+ }
+
+ private void hideSoftInput() {
+ if (mSoftInputReentrancyGuard) {
+ return;
+ }
+ final InputMethodManager imm = getInputMethodManager();
+ if (imm != null) {
+ final View v = getView();
+ mSoftInputReentrancyGuard = true;
+ imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
+ mSoftInputReentrancyGuard = false;
+ }
+ }
+
+ private void restartInput() {
+
+ final InputMethodManager imm = getInputMethodManager();
+ if (imm == null) {
+ return;
+ }
+ final View v = getView();
+ // InputMethodManager has internal logic to detect if we are restarting input
+ // in an already focused View, which is the case here because all content text
+ // fields are inside one LayerView. When this happens, InputMethodManager will
+ // tell the input method to soft reset instead of hard reset. Stock latin IME
+ // on Android 4.2+ has a quirk that when it soft resets, it does not clear the
+ // composition. The following workaround tricks the IME into clearing the
+ // composition when soft resetting.
+ if (InputMethods.needsSoftResetWorkaround(mCurrentInputMethod)) {
+ // Fake a selection change, because the IME clears the composition when
+ // the selection changes, even if soft-resetting. Offsets here must be
+ // different from the previous selection offsets, and -1 seems to be a
+ // reasonable, deterministic value
+ notifySelectionChange(-1, -1);
+ }
+ try {
+ imm.restartInput(v);
+ } catch (RuntimeException e) {
+ Log.e(LOGTAG, "Error restarting input", e);
+ }
+ }
+
+ private void resetInputConnection() {
+ if (mBatchEditCount != 0) {
+ Log.w(LOGTAG, "resetting with mBatchEditCount = " + mBatchEditCount);
+ mBatchEditCount = 0;
+ }
+
+ // Do not reset mIMEState here; see comments in notifyIMEContext
+
+ restartInput();
+ }
+
+ @Override // GeckoEditableListener
+ public void onTextChange() {
+
+ if (mUpdateRequest == null) {
+ return;
+ }
+
+ final InputMethodManager imm = getInputMethodManager();
+ final View v = getView();
+ final Editable editable = getEditable();
+ if (imm == null || v == null || editable == null) {
+ return;
+ }
+ mUpdateExtract.flags = 0;
+ // Update the entire Editable range
+ mUpdateExtract.partialStartOffset = -1;
+ mUpdateExtract.partialEndOffset = -1;
+ mUpdateExtract.selectionStart = Selection.getSelectionStart(editable);
+ mUpdateExtract.selectionEnd = Selection.getSelectionEnd(editable);
+ mUpdateExtract.startOffset = 0;
+ if ((mUpdateRequest.flags & GET_TEXT_WITH_STYLES) != 0) {
+ mUpdateExtract.text = new SpannableString(editable);
+ } else {
+ mUpdateExtract.text = editable.toString();
+ }
+ imm.updateExtractedText(v, mUpdateRequest.token, mUpdateExtract);
+ }
+
+ @Override // GeckoEditableListener
+ public void onSelectionChange() {
+
+ final Editable editable = getEditable();
+ if (editable != null) {
+ notifySelectionChange(Selection.getSelectionStart(editable),
+ Selection.getSelectionEnd(editable));
+ }
+ }
+
+ private void notifySelectionChange(int start, int end) {
+
+ final InputMethodManager imm = getInputMethodManager();
+ final View v = getView();
+ final Editable editable = getEditable();
+ if (imm == null || v == null || editable == null) {
+ return;
+ }
+ imm.updateSelection(v, start, end, getComposingSpanStart(editable),
+ getComposingSpanEnd(editable));
+ }
+
+ @Override
+ public void updateCompositionRects(final RectF[] aRects) {
+ if (!Versions.feature21Plus) {
+ return;
+ }
+
+ if (mCursorAnchorInfoBuilder == null) {
+ mCursorAnchorInfoBuilder = new CursorAnchorInfo.Builder();
+ }
+ mCursorAnchorInfoBuilder.reset();
+
+ // Calculate Gecko logical coords to screen coords
+ final View v = getView();
+ if (v == null) {
+ return;
+ }
+
+ int[] viewCoords = new int[2];
+ v.getLocationOnScreen(viewCoords);
+
+ DynamicToolbarAnimator animator = GeckoAppShell.getLayerView().getDynamicToolbarAnimator();
+ float toolbarHeight = animator.getMaxTranslation() - animator.getToolbarTranslation();
+
+ Matrix matrix = GeckoAppShell.getLayerView().getMatrixForLayerRectToViewRect();
+ if (matrix == null) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "Cannot get Matrix to convert from Gecko coords to layer view coords");
+ }
+ return;
+ }
+ matrix.postTranslate(viewCoords[0], viewCoords[1] + toolbarHeight);
+ mCursorAnchorInfoBuilder.setMatrix(matrix);
+
+ final Editable content = getEditable();
+ if (content == null) {
+ return;
+ }
+ int composingStart = getComposingSpanStart(content);
+ int composingEnd = getComposingSpanEnd(content);
+ if (composingStart < 0 || composingEnd < 0) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "No composition for updates");
+ }
+ return;
+ }
+
+ for (int i = 0; i < aRects.length; i++) {
+ mCursorAnchorInfoBuilder.addCharacterBounds(i,
+ aRects[i].left,
+ aRects[i].top,
+ aRects[i].right,
+ aRects[i].bottom,
+ CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION);
+ }
+
+ mCursorAnchorInfoBuilder.setComposingText(0, content.subSequence(composingStart, composingEnd));
+
+ updateCursor();
+ }
+
+ @TargetApi(21)
+ private void updateCursor() {
+ if (mCursorAnchorInfoBuilder == null) {
+ return;
+ }
+
+ final InputMethodManager imm = getInputMethodManager();
+ final View v = getView();
+ if (imm == null || v == null) {
+ return;
+ }
+
+ imm.updateCursorAnchorInfo(v, mCursorAnchorInfoBuilder.build());
+ }
+
+ @Override
+ public boolean requestCursorUpdates(int cursorUpdateMode) {
+
+ if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0) {
+ mEditableClient.requestCursorUpdates(GeckoEditableClient.ONE_SHOT);
+ }
+
+ if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_MONITOR) != 0) {
+ mEditableClient.requestCursorUpdates(GeckoEditableClient.START_MONITOR);
+ } else {
+ mEditableClient.requestCursorUpdates(GeckoEditableClient.END_MONITOR);
+ }
+ return true;
+ }
+
+ @Override
+ public void onDefaultKeyEvent(final KeyEvent event) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ GeckoInputConnection.this.performDefaultKeyAction(event);
+ }
+ });
+ }
+
+ private static synchronized Handler getBackgroundHandler() {
+ if (sBackgroundHandler != null) {
+ return sBackgroundHandler;
+ }
+ // Don't use GeckoBackgroundThread because Gecko thread may block waiting on
+ // GeckoBackgroundThread. If we were to use GeckoBackgroundThread, due to IME,
+ // GeckoBackgroundThread may end up also block waiting on Gecko thread and a
+ // deadlock occurs
+ Thread backgroundThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Looper.prepare();
+ synchronized (GeckoInputConnection.class) {
+ sBackgroundHandler = new Handler();
+ GeckoInputConnection.class.notify();
+ }
+ Looper.loop();
+ // We should never be exiting the thread loop.
+ throw new IllegalThreadStateException("unreachable code");
+ }
+ }, LOGTAG);
+ backgroundThread.setDaemon(true);
+ backgroundThread.start();
+ while (sBackgroundHandler == null) {
+ try {
+ // wait for new thread to set sBackgroundHandler
+ GeckoInputConnection.class.wait();
+ } catch (InterruptedException e) {
+ }
+ }
+ return sBackgroundHandler;
+ }
+
+ private boolean canReturnCustomHandler() {
+ if (mIMEState == IME_STATE_DISABLED) {
+ return false;
+ }
+ for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
+ // We only return our custom Handler to InputMethodManager's InputConnection
+ // proxy. For all other purposes, we return the regular Handler.
+ // InputMethodManager retrieves the Handler for its InputConnection proxy
+ // inside its method startInputInner(), so we check for that here. This is
+ // valid from Android 2.2 to at least Android 4.2. If this situation ever
+ // changes, we gracefully fall back to using the regular Handler.
+ if ("startInputInner".equals(frame.getMethodName()) &&
+ "android.view.inputmethod.InputMethodManager".equals(frame.getClassName())) {
+ // only return our own Handler to InputMethodManager
+ return true;
+ }
+ if (CUSTOM_HANDLER_TEST_METHOD.equals(frame.getMethodName()) &&
+ CUSTOM_HANDLER_TEST_CLASS.equals(frame.getClassName())) {
+ // InputConnection tests should also run on the custom handler
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isPhysicalKeyboardPresent() {
+ final View v = getView();
+ if (v == null) {
+ return false;
+ }
+ final Configuration config = v.getContext().getResources().getConfiguration();
+ return config.keyboard != Configuration.KEYBOARD_NOKEYS;
+ }
+
+ // Android N: @Override // InputConnection
+ // We need to suppress lint complaining about the lack override here in the meantime: it wants us to build
+ // against sdk 24, even though we're using 23, and therefore complains about the lack of override.
+ // Once we update to 24, we can use the actual override annotation and remove the lint suppression.
+ @SuppressLint("Override")
+ public Handler getHandler() {
+ if (isPhysicalKeyboardPresent()) {
+ return ThreadUtils.getUiHandler();
+ }
+
+ return getBackgroundHandler();
+ }
+
+ @Override // InputConnectionListener
+ public Handler getHandler(Handler defHandler) {
+ if (!canReturnCustomHandler()) {
+ return defHandler;
+ }
+
+ return mEditableClient.setInputConnectionHandler(getHandler());
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ // Some keyboards require us to fill out outAttrs even if we return null.
+ outAttrs.inputType = InputType.TYPE_CLASS_TEXT;
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE;
+ outAttrs.actionLabel = null;
+
+ if (mIMEState == IME_STATE_DISABLED) {
+ hideSoftInput();
+ return null;
+ }
+
+ if (mIMEState == IME_STATE_PASSWORD ||
+ "password".equalsIgnoreCase(mIMETypeHint))
+ outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_PASSWORD;
+ else if (mIMEState == IME_STATE_PLUGIN)
+ outAttrs.inputType = InputType.TYPE_NULL; // "send key events" mode
+ else if (mIMETypeHint.equalsIgnoreCase("url"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_URI;
+ else if (mIMETypeHint.equalsIgnoreCase("email"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+ else if (mIMETypeHint.equalsIgnoreCase("tel"))
+ outAttrs.inputType = InputType.TYPE_CLASS_PHONE;
+ else if (mIMETypeHint.equalsIgnoreCase("number") ||
+ mIMETypeHint.equalsIgnoreCase("range"))
+ outAttrs.inputType = InputType.TYPE_CLASS_NUMBER
+ | InputType.TYPE_NUMBER_FLAG_SIGNED
+ | InputType.TYPE_NUMBER_FLAG_DECIMAL;
+ else if (mIMETypeHint.equalsIgnoreCase("week") ||
+ mIMETypeHint.equalsIgnoreCase("month"))
+ outAttrs.inputType = InputType.TYPE_CLASS_DATETIME
+ | InputType.TYPE_DATETIME_VARIATION_DATE;
+ else if (mIMEModeHint.equalsIgnoreCase("numeric"))
+ outAttrs.inputType = InputType.TYPE_CLASS_NUMBER |
+ InputType.TYPE_NUMBER_FLAG_SIGNED |
+ InputType.TYPE_NUMBER_FLAG_DECIMAL;
+ else if (mIMEModeHint.equalsIgnoreCase("digit"))
+ outAttrs.inputType = InputType.TYPE_CLASS_NUMBER;
+ else {
+ // TYPE_TEXT_FLAG_IME_MULTI_LINE flag makes the fullscreen IME line wrap
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT |
+ InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE;
+ if (mIMETypeHint.equalsIgnoreCase("textarea") ||
+ mIMETypeHint.length() == 0) {
+ // empty mIMETypeHint indicates contentEditable/designMode documents
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
+ }
+ if (mIMEModeHint.equalsIgnoreCase("uppercase"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
+ else if (mIMEModeHint.equalsIgnoreCase("titlecase"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
+ else if (mIMETypeHint.equalsIgnoreCase("text") &&
+ !mIMEModeHint.equalsIgnoreCase("autocapitalized"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_NORMAL;
+ else if (!mIMEModeHint.equalsIgnoreCase("lowercase"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
+ // auto-capitalized mode is the default for types other than text
+ }
+
+ if (mIMEActionHint.equalsIgnoreCase("go"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_GO;
+ else if (mIMEActionHint.equalsIgnoreCase("done"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE;
+ else if (mIMEActionHint.equalsIgnoreCase("next"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT;
+ else if (mIMEActionHint.equalsIgnoreCase("search") ||
+ mIMETypeHint.equalsIgnoreCase("search"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH;
+ else if (mIMEActionHint.equalsIgnoreCase("send"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_SEND;
+ else if (mIMEActionHint.length() > 0) {
+ if (DEBUG)
+ Log.w(LOGTAG, "Unexpected mIMEActionHint=\"" + mIMEActionHint + "\"");
+ outAttrs.actionLabel = mIMEActionHint;
+ }
+
+ Context context = getView().getContext();
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ if (Math.min(metrics.widthPixels, metrics.heightPixels) > INLINE_IME_MIN_DISPLAY_SIZE) {
+ // prevent showing full-screen keyboard only when the screen is tall enough
+ // to show some reasonable amount of the page (see bug 752709)
+ outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI
+ | EditorInfo.IME_FLAG_NO_FULLSCREEN;
+ }
+
+ if (DEBUG) {
+ Log.d(LOGTAG, "mapped IME states to: inputType = " +
+ Integer.toHexString(outAttrs.inputType) + ", imeOptions = " +
+ Integer.toHexString(outAttrs.imeOptions));
+ }
+
+ String prevInputMethod = mCurrentInputMethod;
+ mCurrentInputMethod = InputMethods.getCurrentInputMethod(context);
+ if (DEBUG) {
+ Log.d(LOGTAG, "IME: CurrentInputMethod=" + mCurrentInputMethod);
+ }
+
+ if (mIMEState == IME_STATE_PLUGIN) {
+ // Since we are using a temporary string as the editable, the selection is at 0
+ outAttrs.initialSelStart = 0;
+ outAttrs.initialSelEnd = 0;
+ return mKeyInputConnection;
+ }
+ Editable editable = getEditable();
+ outAttrs.initialSelStart = Selection.getSelectionStart(editable);
+ outAttrs.initialSelEnd = Selection.getSelectionEnd(editable);
+
+ showSoftInput();
+ return this;
+ }
+
+ private boolean replaceComposingSpanWithSelection() {
+ final Editable content = getEditable();
+ if (content == null) {
+ return false;
+ }
+ int a = getComposingSpanStart(content),
+ b = getComposingSpanEnd(content);
+ if (a != -1 && b != -1) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "removing composition at " + a + "-" + b);
+ }
+ removeComposingSpans(content);
+ Selection.setSelection(content, a, b);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean commitText(CharSequence text, int newCursorPosition) {
+ if (InputMethods.shouldCommitCharAsKey(mCurrentInputMethod) &&
+ text.length() == 1 && newCursorPosition > 0) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "committing \"" + text + "\" as key");
+ }
+ // mKeyInputConnection is a BaseInputConnection that commits text as keys;
+ // but we first need to replace any composing span with a selection,
+ // so that the new key events will generate characters to replace
+ // text from the old composing span
+ return replaceComposingSpanWithSelection() &&
+ mKeyInputConnection.commitText(text, newCursorPosition);
+ }
+ return super.commitText(text, newCursorPosition);
+ }
+
+ @Override
+ public boolean setSelection(int start, int end) {
+ if (start < 0 || end < 0) {
+ // Some keyboards (e.g. Samsung) can call setSelection with
+ // negative offsets. In that case we ignore the call, similar to how
+ // BaseInputConnection.setSelection ignores offsets that go past the length.
+ return true;
+ }
+ return super.setSelection(start, end);
+ }
+
+ /* package */ void sendKeyEvent(final int action, KeyEvent event) {
+ final Editable editable = getEditable();
+ if (editable == null) {
+ return;
+ }
+
+ final KeyListener keyListener = TextKeyListener.getInstance();
+ event = translateKey(event.getKeyCode(), event);
+
+ // We only let TextKeyListener do UI things on the UI thread.
+ final View v = ThreadUtils.isOnUiThread() ? getView() : null;
+ final int keyCode = event.getKeyCode();
+ final boolean handled;
+
+ if (shouldSkipKeyListener(keyCode, event)) {
+ handled = false;
+ } else if (action == KeyEvent.ACTION_DOWN) {
+ mEditableClient.setSuppressKeyUp(true);
+ handled = keyListener.onKeyDown(v, editable, keyCode, event);
+ } else if (action == KeyEvent.ACTION_UP) {
+ handled = keyListener.onKeyUp(v, editable, keyCode, event);
+ } else {
+ handled = keyListener.onKeyOther(v, editable, event);
+ }
+
+ if (!handled) {
+ mEditableClient.sendKeyEvent(event, action, TextKeyListener.getMetaState(editable));
+ }
+
+ if (action == KeyEvent.ACTION_DOWN) {
+ if (!handled) {
+ // Usually, the down key listener call above adjusts meta states for us.
+ // However, if the call didn't handle the event, we have to manually
+ // adjust meta states so the meta states remain consistent.
+ TextKeyListener.adjustMetaAfterKeypress(editable);
+ }
+ mEditableClient.setSuppressKeyUp(false);
+ }
+ }
+
+ @Override
+ public boolean sendKeyEvent(KeyEvent event) {
+ sendKeyEvent(event.getAction(), event);
+ return false; // seems to always return false
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ private boolean shouldProcessKey(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_MENU:
+ case KeyEvent.KEYCODE_BACK:
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ case KeyEvent.KEYCODE_SEARCH:
+ // ignore HEADSETHOOK to allow hold-for-voice-search to work
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ return false;
+ }
+ return true;
+ }
+
+ private boolean shouldSkipKeyListener(int keyCode, KeyEvent event) {
+ if (mIMEState == IME_STATE_DISABLED ||
+ mIMEState == IME_STATE_PLUGIN) {
+ return true;
+ }
+ // Preserve enter and tab keys for the browser
+ if (keyCode == KeyEvent.KEYCODE_ENTER ||
+ keyCode == KeyEvent.KEYCODE_TAB) {
+ return true;
+ }
+ // BaseKeyListener returns false even if it handled these keys for us,
+ // so we skip the key listener entirely and handle these ourselves
+ if (keyCode == KeyEvent.KEYCODE_DEL ||
+ keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
+ return true;
+ }
+ return false;
+ }
+
+ private KeyEvent translateKey(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ENTER:
+ if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0 &&
+ mIMEActionHint.equalsIgnoreCase("next")) {
+ return new KeyEvent(event.getAction(), KeyEvent.KEYCODE_TAB);
+ }
+ break;
+ }
+
+ if (GamepadUtils.isSonyXperiaGamepadKeyEvent(event)) {
+ return GamepadUtils.translateSonyXperiaGamepadKeys(keyCode, event);
+ }
+
+ return event;
+ }
+
+ // Called by OnDefaultKeyEvent handler, up from Gecko
+ /* package */ void performDefaultKeyAction(KeyEvent event) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_MUTE:
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ case KeyEvent.KEYCODE_MEDIA_STOP:
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ case KeyEvent.KEYCODE_MEDIA_REWIND:
+ case KeyEvent.KEYCODE_MEDIA_RECORD:
+ case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
+ case KeyEvent.KEYCODE_MEDIA_CLOSE:
+ case KeyEvent.KEYCODE_MEDIA_EJECT:
+ case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK:
+ // Forward media keypresses to the registered handler so headset controls work
+ // Does the same thing as Chromium
+ // https://chromium.googlesource.com/chromium/src/+/49.0.2623.67/chrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsDelegateAndroid.java#445
+ // These are all the keys dispatchMediaKeyEvent supports.
+ if (AppConstants.Versions.feature19Plus) {
+ // dispatchMediaKeyEvent is only available on Android 4.4+
+ Context viewContext = getView().getContext();
+ AudioManager am = (AudioManager)viewContext.getSystemService(Context.AUDIO_SERVICE);
+ am.dispatchMediaKeyEvent(event);
+ }
+ break;
+ }
+ }
+
+ private boolean processKey(final int action, final int keyCode, final KeyEvent event) {
+
+ if (keyCode > KeyEvent.getMaxKeyCode() || !shouldProcessKey(keyCode, event)) {
+ return false;
+ }
+
+ mEditableClient.postToInputConnection(new Runnable() {
+ @Override
+ public void run() {
+ sendKeyEvent(action, event);
+ }
+ });
+ return true;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return processKey(KeyEvent.ACTION_DOWN, keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return processKey(KeyEvent.ACTION_UP, keyCode, event);
+ }
+
+ /**
+ * Get a key that represents a given character.
+ */
+ private KeyEvent getCharKeyEvent(final char c) {
+ final long time = SystemClock.uptimeMillis();
+ return new KeyEvent(time, time, KeyEvent.ACTION_MULTIPLE,
+ KeyEvent.KEYCODE_UNKNOWN, /* repeat */ 0) {
+ @Override
+ public int getUnicodeChar() {
+ return c;
+ }
+
+ @Override
+ public int getUnicodeChar(int metaState) {
+ return c;
+ }
+ };
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, final KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
+ // KEYCODE_UNKNOWN means the characters are in KeyEvent.getCharacters()
+ final String str = event.getCharacters();
+ for (int i = 0; i < str.length(); i++) {
+ final KeyEvent charEvent = getCharKeyEvent(str.charAt(i));
+ if (!processKey(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_UNKNOWN, charEvent) ||
+ !processKey(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_UNKNOWN, charEvent)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ while ((repeatCount--) != 0) {
+ if (!processKey(KeyEvent.ACTION_DOWN, keyCode, event) ||
+ !processKey(KeyEvent.ACTION_UP, keyCode, event)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ View v = getView();
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_MENU:
+ InputMethodManager imm = getInputMethodManager();
+ imm.toggleSoftInputFromWindow(v.getWindowToken(),
+ InputMethodManager.SHOW_FORCED, 0);
+ return true;
+ default:
+ break;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isIMEEnabled() {
+ // make sure this picks up PASSWORD and PLUGIN states as well
+ return mIMEState != IME_STATE_DISABLED;
+ }
+
+ @Override
+ public void notifyIME(int type) {
+ switch (type) {
+
+ case NOTIFY_IME_OF_FOCUS:
+ // Showing/hiding vkb is done in notifyIMEContext
+ mFocused = true;
+ resetInputConnection();
+ break;
+
+ case NOTIFY_IME_OF_BLUR:
+ // Showing/hiding vkb is done in notifyIMEContext
+ mFocused = false;
+ break;
+
+ case NOTIFY_IME_OPEN_VKB:
+ showSoftInput();
+ break;
+
+ default:
+ if (DEBUG) {
+ throw new IllegalArgumentException("Unexpected NOTIFY_IME=" + type);
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void notifyIMEContext(int state, String typeHint, String modeHint, String actionHint) {
+ // For some input type we will use a widget to display the ui, for those we must not
+ // display the ime. We can display a widget for date and time types and, if the sdk version
+ // is 11 or greater, for datetime/month/week as well.
+ if (typeHint != null &&
+ (typeHint.equalsIgnoreCase("date") ||
+ typeHint.equalsIgnoreCase("time") ||
+ typeHint.equalsIgnoreCase("datetime") ||
+ typeHint.equalsIgnoreCase("month") ||
+ typeHint.equalsIgnoreCase("week") ||
+ typeHint.equalsIgnoreCase("datetime-local"))) {
+ state = IME_STATE_DISABLED;
+ }
+
+ // mIMEState and the mIME*Hint fields should only be changed by notifyIMEContext,
+ // and not reset anywhere else. Usually, notifyIMEContext is called right after a
+ // focus or blur, so resetting mIMEState during the focus or blur seems harmless.
+ // However, this behavior is not guaranteed. Gecko may call notifyIMEContext
+ // independent of focus change; that is, a focus change may not be accompanied by
+ // a notifyIMEContext call. So if we reset mIMEState inside focus, there may not
+ // be another notifyIMEContext call to set mIMEState to a proper value (bug 829318)
+ /* When IME is 'disabled', IME processing is disabled.
+ In addition, the IME UI is hidden */
+ mIMEState = state;
+ mIMETypeHint = (typeHint == null) ? "" : typeHint;
+ mIMEModeHint = (modeHint == null) ? "" : modeHint;
+ mIMEActionHint = (actionHint == null) ? "" : actionHint;
+
+ // These fields are reset here and will be updated when restartInput is called below
+ mUpdateRequest = null;
+ mCurrentInputMethod = "";
+
+ View v = getView();
+ if (v == null || !v.hasFocus()) {
+ // When using Find In Page, we can still receive notifyIMEContext calls due to the
+ // selection changing when highlighting. However in this case we don't want to reset/
+ // show/hide the keyboard because the find box has the focus and is taking input from
+ // the keyboard.
+ return;
+ }
+
+ // On focus, the notifyIMEContext call comes *before* the
+ // notifyIME(NOTIFY_IME_OF_FOCUS) call, but we need to call restartInput during
+ // notifyIME, so we skip restartInput here. On blur, the notifyIMEContext call
+ // comes *after* the notifyIME(NOTIFY_IME_OF_BLUR) call, and we need to call
+ // restartInput here.
+ if (mIMEState == IME_STATE_DISABLED || mFocused) {
+ restartInput();
+ }
+ }
+}
+
+final class DebugGeckoInputConnection
+ extends GeckoInputConnection
+ implements InvocationHandler {
+
+ private InputConnection mProxy;
+ private final StringBuilder mCallLevel;
+
+ private DebugGeckoInputConnection(View targetView,
+ GeckoEditableClient editable) {
+ super(targetView, editable);
+ mCallLevel = new StringBuilder();
+ }
+
+ public static GeckoEditableListener create(View targetView,
+ GeckoEditableClient editable) {
+ final Class<?>[] PROXY_INTERFACES = { InputConnection.class,
+ InputConnectionListener.class,
+ GeckoEditableListener.class };
+ DebugGeckoInputConnection dgic =
+ new DebugGeckoInputConnection(targetView, editable);
+ dgic.mProxy = (InputConnection)Proxy.newProxyInstance(
+ GeckoInputConnection.class.getClassLoader(),
+ PROXY_INTERFACES, dgic);
+ return (GeckoEditableListener)dgic.mProxy;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args)
+ throws Throwable {
+
+ StringBuilder log = new StringBuilder(mCallLevel);
+ log.append("> ").append(method.getName()).append("(");
+ if (args != null) {
+ for (Object arg : args) {
+ // translate argument values to constant names
+ if ("notifyIME".equals(method.getName()) && arg == args[0]) {
+ log.append(GeckoEditable.getConstantName(
+ GeckoEditableListener.class, "NOTIFY_IME_", arg));
+ } else if ("notifyIMEContext".equals(method.getName()) && arg == args[0]) {
+ log.append(GeckoEditable.getConstantName(
+ GeckoEditableListener.class, "IME_STATE_", arg));
+ } else {
+ GeckoEditable.debugAppend(log, arg);
+ }
+ log.append(", ");
+ }
+ if (args.length > 0) {
+ log.setLength(log.length() - 2);
+ }
+ }
+ log.append(")");
+ Log.d(LOGTAG, log.toString());
+
+ mCallLevel.append(' ');
+ Object ret = method.invoke(this, args);
+ if (ret == this) {
+ ret = mProxy;
+ }
+ mCallLevel.setLength(Math.max(0, mCallLevel.length() - 1));
+
+ log.setLength(mCallLevel.length());
+ log.append("< ").append(method.getName());
+ if (!method.getReturnType().equals(Void.TYPE)) {
+ GeckoEditable.debugAppend(log.append(": "), ret);
+ }
+ Log.d(LOGTAG, log.toString());
+ return ret;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java
new file mode 100644
index 000000000..0cb56a7d2
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java
@@ -0,0 +1,491 @@
+/* -*- 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;
+
+import org.mozilla.gecko.annotation.JNITarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.NetworkUtils;
+import org.mozilla.gecko.util.NetworkUtils.ConnectionSubType;
+import org.mozilla.gecko.util.NetworkUtils.ConnectionType;
+import org.mozilla.gecko.util.NetworkUtils.NetworkStatus;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.DhcpInfo;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telephony.TelephonyManager;
+import android.text.format.Formatter;
+import android.util.Log;
+
+/**
+ * Provides connection type, subtype and general network status (up/down).
+ *
+ * According to spec of Network Information API version 3, connection types include:
+ * bluetooth, cellular, ethernet, none, wifi and other. The objective of providing such general
+ * connection is due to some security concerns. In short, we don't want to expose exact network type,
+ * especially the cellular network type.
+ *
+ * Specific mobile subtypes are mapped to general 2G, 3G and 4G buckets.
+ *
+ * Logic is implemented as a state machine, so see the transition matrix to figure out what happens when.
+ * This class depends on access to the context, so only use after GeckoAppShell has been initialized.
+ */
+public class GeckoNetworkManager extends BroadcastReceiver implements NativeEventListener {
+ private static final String LOGTAG = "GeckoNetworkManager";
+
+ private static final String LINK_DATA_CHANGED = "changed";
+
+ private static GeckoNetworkManager instance;
+
+ // We hackishly (yet harmlessly, in this case) keep a Context reference passed in via the start method.
+ // See context handling notes in handleManagerEvent, and Bug 1277333.
+ private Context context;
+
+ public static void destroy() {
+ if (instance != null) {
+ instance.onDestroy();
+ instance = null;
+ }
+ }
+
+ public enum ManagerState {
+ OffNoListeners,
+ OffWithListeners,
+ OnNoListeners,
+ OnWithListeners
+ }
+
+ public enum ManagerEvent {
+ start,
+ stop,
+ enableNotifications,
+ disableNotifications,
+ receivedUpdate
+ }
+
+ private ManagerState currentState = ManagerState.OffNoListeners;
+ private ConnectionType currentConnectionType = ConnectionType.NONE;
+ private ConnectionType previousConnectionType = ConnectionType.NONE;
+ private ConnectionSubType currentConnectionSubtype = ConnectionSubType.UNKNOWN;
+ private ConnectionSubType previousConnectionSubtype = ConnectionSubType.UNKNOWN;
+ private NetworkStatus currentNetworkStatus = NetworkStatus.UNKNOWN;
+ private NetworkStatus previousNetworkStatus = NetworkStatus.UNKNOWN;
+
+ private enum InfoType {
+ MCC,
+ MNC
+ }
+
+ private GeckoNetworkManager() {
+ EventDispatcher.getInstance().registerGeckoThreadListener(this,
+ "Wifi:Enable",
+ "Wifi:GetIPAddress");
+ }
+
+ private void onDestroy() {
+ handleManagerEvent(ManagerEvent.stop);
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
+ "Wifi:Enable",
+ "Wifi:GetIPAddress");
+ }
+
+ public static GeckoNetworkManager getInstance() {
+ if (instance == null) {
+ instance = new GeckoNetworkManager();
+ }
+
+ return instance;
+ }
+
+ public double[] getCurrentInformation() {
+ final Context applicationContext = GeckoAppShell.getApplicationContext();
+ final ConnectionType connectionType = currentConnectionType;
+ return new double[] {
+ connectionType.value,
+ connectionType == ConnectionType.WIFI ? 1.0 : 0.0,
+ connectionType == ConnectionType.WIFI ? wifiDhcpGatewayAddress(applicationContext) : 0.0
+ };
+ }
+
+ @Override
+ public void onReceive(Context aContext, Intent aIntent) {
+ handleManagerEvent(ManagerEvent.receivedUpdate);
+ }
+
+ public void start(final Context context) {
+ this.context = context;
+ handleManagerEvent(ManagerEvent.start);
+ }
+
+ public void stop() {
+ handleManagerEvent(ManagerEvent.stop);
+ }
+
+ public void enableNotifications() {
+ handleManagerEvent(ManagerEvent.enableNotifications);
+ }
+
+ public void disableNotifications() {
+ handleManagerEvent(ManagerEvent.disableNotifications);
+ }
+
+ /**
+ * For a given event, figure out the next state, run any transition by-product actions, and switch
+ * current state to the next state. If event is invalid for the current state, this is a no-op.
+ *
+ * @param event Incoming event
+ * @return Boolean indicating if transition was performed.
+ */
+ private synchronized boolean handleManagerEvent(ManagerEvent event) {
+ final ManagerState nextState = getNextState(currentState, event);
+
+ Log.d(LOGTAG, "Incoming event " + event + " for state " + currentState + " -> " + nextState);
+ if (nextState == null) {
+ Log.w(LOGTAG, "Invalid event " + event + " for state " + currentState);
+ return false;
+ }
+
+ // We're being deliberately careful about handling context here; it's possible that in some
+ // rare cases and possibly related to timing of when this is called (seems to be early in the startup phase),
+ // GeckoAppShell.getApplicationContext() will be null, and .start() wasn't called yet,
+ // so we don't have a local Context reference either. If both of these are true, we have to drop the event.
+ // NB: this is hacky (and these checks attempt to isolate the hackiness), and root cause
+ // seems to be how this class fits into the larger ecosystem and general flow of events.
+ // See Bug 1277333.
+ final Context contextForAction;
+ if (context != null) {
+ contextForAction = context;
+ } else {
+ contextForAction = GeckoAppShell.getApplicationContext();
+ }
+
+ if (contextForAction == null) {
+ Log.w(LOGTAG, "Context is not available while processing event " + event + " for state " + currentState);
+ return false;
+ }
+
+ performActionsForStateEvent(contextForAction, currentState, event);
+ currentState = nextState;
+
+ return true;
+ }
+
+ /**
+ * Defines a transition matrix for our state machine. For a given state/event pair, returns nextState.
+ *
+ * @param currentState Current state against which we have an incoming event
+ * @param event Incoming event for which we'd like to figure out the next state
+ * @return State into which we should transition as result of given event
+ */
+ @Nullable
+ public static ManagerState getNextState(@NonNull ManagerState currentState, @NonNull ManagerEvent event) {
+ switch (currentState) {
+ case OffNoListeners:
+ switch (event) {
+ case start:
+ return ManagerState.OnNoListeners;
+ case enableNotifications:
+ return ManagerState.OffWithListeners;
+ default:
+ return null;
+ }
+ case OnNoListeners:
+ switch (event) {
+ case stop:
+ return ManagerState.OffNoListeners;
+ case enableNotifications:
+ return ManagerState.OnWithListeners;
+ case receivedUpdate:
+ return ManagerState.OnNoListeners;
+ default:
+ return null;
+ }
+ case OnWithListeners:
+ switch (event) {
+ case stop:
+ return ManagerState.OffWithListeners;
+ case disableNotifications:
+ return ManagerState.OnNoListeners;
+ case receivedUpdate:
+ return ManagerState.OnWithListeners;
+ default:
+ return null;
+ }
+ case OffWithListeners:
+ switch (event) {
+ case start:
+ return ManagerState.OnWithListeners;
+ case disableNotifications:
+ return ManagerState.OffNoListeners;
+ default:
+ return null;
+ }
+ default:
+ throw new IllegalStateException("Unknown current state: " + currentState.name());
+ }
+ }
+
+ /**
+ * For a given state/event combination, run any actions which are by-products of leaving the state
+ * because of a given event. Since this is a deterministic state machine, we can easily do that
+ * without any additional information.
+ *
+ * @param currentState State which we are leaving
+ * @param event Event which is causing us to leave the state
+ */
+ private void performActionsForStateEvent(final Context context, final ManagerState currentState, final ManagerEvent event) {
+ // NB: network state might be queried via getCurrentInformation at any time; pre-rewrite behaviour was
+ // that network state was updated whenever enableNotifications was called. To avoid deviating
+ // from previous behaviour and causing weird side-effects, we call updateNetworkStateAndConnectionType
+ // whenever notifications are enabled.
+ switch (currentState) {
+ case OffNoListeners:
+ if (event == ManagerEvent.start) {
+ updateNetworkStateAndConnectionType(context);
+ registerBroadcastReceiver(context, this);
+ }
+ if (event == ManagerEvent.enableNotifications) {
+ updateNetworkStateAndConnectionType(context);
+ }
+ break;
+ case OnNoListeners:
+ if (event == ManagerEvent.receivedUpdate) {
+ updateNetworkStateAndConnectionType(context);
+ sendNetworkStateToListeners(context);
+ }
+ if (event == ManagerEvent.enableNotifications) {
+ updateNetworkStateAndConnectionType(context);
+ registerBroadcastReceiver(context, this);
+ }
+ if (event == ManagerEvent.stop) {
+ unregisterBroadcastReceiver(context, this);
+ }
+ break;
+ case OnWithListeners:
+ if (event == ManagerEvent.receivedUpdate) {
+ updateNetworkStateAndConnectionType(context);
+ sendNetworkStateToListeners(context);
+ }
+ if (event == ManagerEvent.stop) {
+ unregisterBroadcastReceiver(context, this);
+ }
+ /* no-op event: ManagerEvent.disableNotifications */
+ break;
+ case OffWithListeners:
+ if (event == ManagerEvent.start) {
+ registerBroadcastReceiver(context, this);
+ }
+ /* no-op event: ManagerEvent.disableNotifications */
+ break;
+ default:
+ throw new IllegalStateException("Unknown current state: " + currentState.name());
+ }
+ }
+
+ /**
+ * Update current network state and connection types.
+ */
+ private void updateNetworkStateAndConnectionType(final Context context) {
+ final ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ // Type/status getters below all have a defined behaviour for when connectivityManager == null
+ if (connectivityManager == null) {
+ Log.e(LOGTAG, "ConnectivityManager does not exist.");
+ }
+ currentConnectionType = NetworkUtils.getConnectionType(connectivityManager);
+ currentNetworkStatus = NetworkUtils.getNetworkStatus(connectivityManager);
+ currentConnectionSubtype = NetworkUtils.getConnectionSubType(connectivityManager);
+ Log.d(LOGTAG, "New network state: " + currentNetworkStatus + ", " + currentConnectionType + ", " + currentConnectionSubtype);
+ }
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void onConnectionChanged(int type, String subType,
+ boolean isWifi, int DHCPGateway);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void onStatusChanged(String status);
+
+ /**
+ * Send current network state and connection type to whomever is listening.
+ */
+ private void sendNetworkStateToListeners(final Context context) {
+ if (currentConnectionType != previousConnectionType ||
+ currentConnectionSubtype != previousConnectionSubtype) {
+ previousConnectionType = currentConnectionType;
+ previousConnectionSubtype = currentConnectionSubtype;
+
+ final boolean isWifi = currentConnectionType == ConnectionType.WIFI;
+ final int gateway = !isWifi ? 0 :
+ wifiDhcpGatewayAddress(context);
+
+ if (GeckoThread.isRunning()) {
+ onConnectionChanged(currentConnectionType.value,
+ currentConnectionSubtype.value, isWifi, gateway);
+ } else {
+ GeckoThread.queueNativeCall(GeckoNetworkManager.class, "onConnectionChanged",
+ currentConnectionType.value,
+ String.class, currentConnectionSubtype.value,
+ isWifi, gateway);
+ }
+ }
+
+ final String status;
+
+ if (currentNetworkStatus != previousNetworkStatus) {
+ previousNetworkStatus = currentNetworkStatus;
+ status = currentNetworkStatus.value;
+ } else {
+ status = LINK_DATA_CHANGED;
+ }
+
+ if (GeckoThread.isRunning()) {
+ onStatusChanged(status);
+ } else {
+ GeckoThread.queueNativeCall(GeckoNetworkManager.class, "onStatusChanged",
+ String.class, status);
+ }
+ }
+
+ /**
+ * Stop listening for network state updates.
+ */
+ private static void unregisterBroadcastReceiver(final Context context, final BroadcastReceiver receiver) {
+ context.unregisterReceiver(receiver);
+ }
+
+ /**
+ * Start listening for network state updates.
+ */
+ private static void registerBroadcastReceiver(final Context context, final BroadcastReceiver receiver) {
+ final IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
+ context.registerReceiver(receiver, filter);
+ }
+
+ private static int wifiDhcpGatewayAddress(final Context context) {
+ if (context == null) {
+ return 0;
+ }
+
+ try {
+ WifiManager mgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ DhcpInfo d = mgr.getDhcpInfo();
+ if (d == null) {
+ return 0;
+ }
+
+ return d.gateway;
+
+ } catch (Exception ex) {
+ // getDhcpInfo() is not documented to require any permissions, but on some devices
+ // requires android.permission.ACCESS_WIFI_STATE. Just catch the generic exception
+ // here and returning 0. Not logging because this could be noisy.
+ return 0;
+ }
+ }
+
+ @Override
+ /**
+ * Handles native messages, not part of the state machine flow.
+ */
+ public void handleMessage(final String event, final NativeJSObject message,
+ final EventCallback callback) {
+ final Context applicationContext = GeckoAppShell.getApplicationContext();
+ switch (event) {
+ case "Wifi:Enable":
+ final WifiManager mgr = (WifiManager) applicationContext.getSystemService(Context.WIFI_SERVICE);
+
+ if (!mgr.isWifiEnabled()) {
+ mgr.setWifiEnabled(true);
+ } else {
+ // If Wifi is enabled, maybe you need to select a network
+ Intent intent = new Intent(android.provider.Settings.ACTION_WIFI_SETTINGS);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ applicationContext.startActivity(intent);
+ }
+ break;
+ case "Wifi:GetIPAddress":
+ getWifiIPAddress(callback);
+ break;
+ }
+ }
+
+ // This function only works for IPv4; not part of the state machine flow.
+ private void getWifiIPAddress(final EventCallback callback) {
+ final WifiManager mgr = (WifiManager) GeckoAppShell.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+
+ if (mgr == null) {
+ callback.sendError("Cannot get WifiManager");
+ return;
+ }
+
+ final WifiInfo info = mgr.getConnectionInfo();
+ if (info == null) {
+ callback.sendError("Cannot get connection info");
+ return;
+ }
+
+ int ip = info.getIpAddress();
+ if (ip == 0) {
+ callback.sendError("Cannot get IPv4 address");
+ return;
+ }
+ callback.sendSuccess(Formatter.formatIpAddress(ip));
+ }
+
+ private static int getNetworkOperator(InfoType type, Context context) {
+ if (null == context) {
+ return -1;
+ }
+
+ TelephonyManager tel = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ if (tel == null) {
+ Log.e(LOGTAG, "Telephony service does not exist");
+ return -1;
+ }
+
+ String networkOperator = tel.getNetworkOperator();
+ if (networkOperator == null || networkOperator.length() <= 3) {
+ return -1;
+ }
+
+ if (type == InfoType.MNC) {
+ return Integer.parseInt(networkOperator.substring(3));
+ }
+
+ if (type == InfoType.MCC) {
+ return Integer.parseInt(networkOperator.substring(0, 3));
+ }
+
+ return -1;
+ }
+
+ /**
+ * These are called from JavaScript ctypes. Avoid letting ProGuard delete them.
+ *
+ * Note that these methods must only be called after GeckoAppShell has been
+ * initialized: they depend on access to the context.
+ *
+ * Not part of the state machine flow.
+ */
+ @JNITarget
+ public static int getMCC() {
+ return getNetworkOperator(InfoType.MCC, GeckoAppShell.getApplicationContext());
+ }
+
+ @JNITarget
+ public static int getMNC() {
+ return getNetworkOperator(InfoType.MNC, GeckoAppShell.getApplicationContext());
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfile.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfile.java
new file mode 100644
index 000000000..27ec4f1dd
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfile.java
@@ -0,0 +1,1002 @@
+/* -*- 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.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.support.annotation.WorkerThread;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
+import org.mozilla.gecko.GeckoProfileDirectories.NoSuchProfileException;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.util.FileUtils;
+import org.mozilla.gecko.util.INIParser;
+import org.mozilla.gecko.util.INISection;
+import org.mozilla.gecko.util.IntentUtils;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class GeckoProfile {
+ private static final String LOGTAG = "GeckoProfile";
+
+ // The path in the profile to the file containing the client ID.
+ private static final String CLIENT_ID_FILE_PATH = "datareporting/state.json";
+ private static final String FHR_CLIENT_ID_FILE_PATH = "healthreport/state.json";
+ // In the client ID file, the attribute title in the JSON object containing the client ID value.
+ private static final String CLIENT_ID_JSON_ATTR = "clientID";
+
+ private static final String TIMES_PATH = "times.json";
+ private static final String PROFILE_CREATION_DATE_JSON_ATTR = "created";
+
+ // Only tests should need to do this.
+ // We can default this to AppConstants.RELEASE_OR_BETA once we fix Bug 1069687.
+ private static volatile boolean sAcceptDirectoryChanges = true;
+
+ @RobocopTarget
+ public static void enableDirectoryChanges() {
+ Log.w(LOGTAG, "Directory changes should only be enabled for tests. And even then it's a bad idea.");
+ sAcceptDirectoryChanges = true;
+ }
+
+ public static final String DEFAULT_PROFILE = "default";
+ // Profile is using a custom directory outside of the Mozilla directory.
+ public static final String CUSTOM_PROFILE = "";
+
+ public static final String GUEST_PROFILE_DIR = "guest";
+ public static final String GUEST_MODE_PREF = "guestMode";
+
+ // Session store
+ private static final String SESSION_FILE = "sessionstore.js";
+ private static final String SESSION_FILE_BACKUP = "sessionstore.bak";
+ private static final String SESSION_FILE_PREVIOUS = "sessionstore.old";
+ private static final long MAX_PREVIOUS_FILE_AGE = 1000 * 3600 * 24; // 24 hours
+
+ private boolean mOldSessionDataProcessed = false;
+
+ private static final ConcurrentHashMap<String, GeckoProfile> sProfileCache =
+ new ConcurrentHashMap<String, GeckoProfile>(
+ /* capacity */ 4, /* load factor */ 0.75f, /* concurrency */ 2);
+ private static String sDefaultProfileName;
+
+ private final String mName;
+ private final File mMozillaDir;
+ private final Context mApplicationContext;
+
+ private Object mData;
+
+ /**
+ * Access to this member should be synchronized to avoid
+ * races during creation -- particularly between getDir and GeckoView#init.
+ *
+ * Not final because this is lazily computed.
+ */
+ private File mProfileDir;
+
+ private Boolean mInGuestMode;
+
+ public static boolean shouldUseGuestMode(final Context context) {
+ return GeckoSharedPrefs.forApp(context).getBoolean(GUEST_MODE_PREF, false);
+ }
+
+ public static void enterGuestMode(final Context context) {
+ GeckoSharedPrefs.forApp(context).edit().putBoolean(GUEST_MODE_PREF, true).commit();
+ }
+
+ public static void leaveGuestMode(final Context context) {
+ GeckoSharedPrefs.forApp(context).edit().putBoolean(GUEST_MODE_PREF, false).commit();
+ }
+
+ public static GeckoProfile initFromArgs(final Context context, final String args) {
+ if (shouldUseGuestMode(context)) {
+ final GeckoProfile guestProfile = getGuestProfile(context);
+ if (guestProfile != null) {
+ return guestProfile;
+ }
+ // Failed to create guest profile; leave guest mode.
+ leaveGuestMode(context);
+ }
+
+ // We never want to use the guest mode profile concurrently with a normal profile
+ // -- no syncing to it, no dual-profile usage, nothing. GeckoThread startup with
+ // a conventional GeckoProfile will cause the guest profile to be deleted and
+ // guest mode to reset.
+ if (getGuestDir(context).isDirectory()) {
+ final GeckoProfile guestProfile = getGuestProfile(context);
+ if (guestProfile != null) {
+ removeProfile(context, guestProfile);
+ }
+ }
+
+ String profileName = null;
+ String profilePath = null;
+
+ if (args != null && args.contains("-P")) {
+ final Pattern p = Pattern.compile("(?:-P\\s*)(\\w*)(\\s*)");
+ final Matcher m = p.matcher(args);
+ if (m.find()) {
+ profileName = m.group(1);
+ }
+ }
+
+ if (args != null && args.contains("-profile")) {
+ final Pattern p = Pattern.compile("(?:-profile\\s*)(\\S*)(\\s*)");
+ final Matcher m = p.matcher(args);
+ if (m.find()) {
+ profilePath = m.group(1);
+ }
+ }
+
+ if (profileName == null && profilePath == null) {
+ // Get the default profile for the Activity.
+ return getDefaultProfile(context);
+ }
+
+ return GeckoProfile.get(context, profileName, profilePath);
+ }
+
+ private static GeckoProfile getDefaultProfile(Context context) {
+ try {
+ return get(context, getDefaultProfileName(context));
+
+ } catch (final NoMozillaDirectoryException e) {
+ // If this failed, we're screwed.
+ Log.wtf(LOGTAG, "Unable to get default profile name.", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static GeckoProfile get(Context context) {
+ return get(context, null, (File) null);
+ }
+
+ public static GeckoProfile get(Context context, String profileName) {
+ if (profileName != null) {
+ GeckoProfile profile = sProfileCache.get(profileName);
+ if (profile != null)
+ return profile;
+ }
+ return get(context, profileName, (File)null);
+ }
+
+ @RobocopTarget
+ public static GeckoProfile get(Context context, String profileName, String profilePath) {
+ File dir = null;
+ if (!TextUtils.isEmpty(profilePath)) {
+ dir = new File(profilePath);
+ if (!dir.exists() || !dir.isDirectory()) {
+ Log.w(LOGTAG, "requested profile directory missing: " + profilePath);
+ }
+ }
+ return get(context, profileName, dir);
+ }
+
+ // Note that the profile cache respects only the profile name!
+ // If the directory changes, the returned GeckoProfile instance will be mutated.
+ @RobocopTarget
+ public static GeckoProfile get(Context context, String profileName, File profileDir) {
+ if (context == null) {
+ throw new IllegalArgumentException("context must be non-null");
+ }
+
+ // Null name? | Null dir? | Returned profile
+ // ------------------------------------------
+ // Yes | Yes | Active profile or default profile.
+ // No | Yes | Profile with specified name at default dir.
+ // Yes | No | Custom (anonymous) profile with specified dir.
+ // No | No | Profile with specified name at specified dir.
+
+ if (profileName == null && profileDir == null) {
+ // If no profile info was passed in, look for the active profile or a default profile.
+ final GeckoProfile profile = GeckoThread.getActiveProfile();
+ if (profile != null) {
+ return profile;
+ }
+
+ final String args;
+ if (context instanceof Activity) {
+ args = IntentUtils.getStringExtraSafe(((Activity) context).getIntent(), "args");
+ } else {
+ args = null;
+ }
+
+ return GeckoProfile.initFromArgs(context, args);
+
+ } else if (profileName == null) {
+ // If only profile dir was passed in, use custom (anonymous) profile.
+ profileName = CUSTOM_PROFILE;
+
+ } else if (AppConstants.DEBUG_BUILD) {
+ Log.v(LOGTAG, "Fetching profile: '" + profileName + "', '" + profileDir + "'");
+ }
+
+ // We require the profile dir to exist if specified, so create it here if needed.
+ final boolean init = profileDir != null && profileDir.mkdirs();
+
+ // Actually try to look up the profile.
+ GeckoProfile profile = sProfileCache.get(profileName);
+ GeckoProfile newProfile = null;
+
+ if (profile == null) {
+ try {
+ newProfile = new GeckoProfile(context, profileName, profileDir);
+ } catch (NoMozillaDirectoryException e) {
+ // We're unable to do anything sane here.
+ throw new RuntimeException(e);
+ }
+
+ profile = sProfileCache.putIfAbsent(profileName, newProfile);
+ }
+
+ if (profile == null) {
+ profile = newProfile;
+
+ } else if (profileDir != null) {
+ // We have an existing profile but was given an alternate directory.
+ boolean consistent = false;
+ try {
+ consistent = profile.mProfileDir != null &&
+ profile.mProfileDir.getCanonicalPath().equals(profileDir.getCanonicalPath());
+ } catch (final IOException e) {
+ }
+
+ if (!consistent) {
+ if (!sAcceptDirectoryChanges || !profileDir.isDirectory()) {
+ throw new IllegalStateException(
+ "Refusing to reuse profile with a different directory.");
+ }
+
+ if (AppConstants.RELEASE_OR_BETA) {
+ Log.e(LOGTAG, "Release build trying to switch out profile dir. " +
+ "This is an error, but let's do what we can.");
+ }
+ profile.setDir(profileDir);
+ }
+ }
+
+ if (init) {
+ // Initialize the profile directory if we had to create it.
+ profile.enqueueInitialization(profileDir);
+ }
+
+ return profile;
+ }
+
+ // Currently unused outside of testing.
+ @RobocopTarget
+ public static boolean removeProfile(final Context context, final GeckoProfile profile) {
+ final boolean success = profile.remove();
+
+ if (success) {
+ // Clear all shared prefs for the given profile.
+ GeckoSharedPrefs.forProfileName(context, profile.getName())
+ .edit().clear().apply();
+ }
+
+ return success;
+ }
+
+ private static File getGuestDir(final Context context) {
+ return context.getFileStreamPath(GUEST_PROFILE_DIR);
+ }
+
+ @RobocopTarget
+ public static GeckoProfile getGuestProfile(final Context context) {
+ return get(context, CUSTOM_PROFILE, getGuestDir(context));
+ }
+
+ public static boolean isGuestProfile(final Context context, final String profileName,
+ final File profileDir) {
+ // Guest profile is just a custom profile with a special path.
+ if (profileDir == null || !CUSTOM_PROFILE.equals(profileName)) {
+ return false;
+ }
+
+ try {
+ return profileDir.getCanonicalPath().equals(getGuestDir(context).getCanonicalPath());
+ } catch (final IOException e) {
+ return false;
+ }
+ }
+
+ private GeckoProfile(Context context, String profileName, File profileDir) throws NoMozillaDirectoryException {
+ if (profileName == null) {
+ throw new IllegalArgumentException("Unable to create GeckoProfile for empty profile name.");
+ } else if (CUSTOM_PROFILE.equals(profileName) && profileDir == null) {
+ throw new IllegalArgumentException("Custom profile must have a directory");
+ }
+
+ mApplicationContext = context.getApplicationContext();
+ mName = profileName;
+ mMozillaDir = GeckoProfileDirectories.getMozillaDirectory(context);
+
+ mProfileDir = profileDir;
+ if (profileDir != null && !profileDir.isDirectory()) {
+ throw new IllegalArgumentException("Profile directory must exist if specified.");
+ }
+ }
+
+ /**
+ * Return the custom data object associated with this profile, which was set by the
+ * previous {@link #setData(Object)} call. This association is valid for the duration
+ * of the process lifetime. The caller must ensure proper synchronization, typically
+ * by synchronizing on the object returned by {@link #getLock()}.
+ *
+ * The data object is usually a database object that stores per-profile data such as
+ * page history. However, it can be any other object that needs to maintain
+ * profile-specific state.
+ *
+ * @return Associated data object
+ */
+ public Object getData() {
+ return mData;
+ }
+
+ /**
+ * Associate this profile with a custom data object, which can be retrieved by
+ * subsequent {@link #getData()} calls. The caller must ensure proper
+ * synchronization, typically by synchronizing on the object returned by {@link
+ * #getLock()}.
+ *
+ * @param data Custom data object
+ */
+ public void setData(final Object data) {
+ mData = data;
+ }
+
+ private void setDir(File dir) {
+ if (dir != null && dir.exists() && dir.isDirectory()) {
+ synchronized (this) {
+ mProfileDir = dir;
+ mInGuestMode = null;
+ }
+ }
+ }
+
+ @RobocopTarget
+ public String getName() {
+ return mName;
+ }
+
+ public boolean isCustomProfile() {
+ return CUSTOM_PROFILE.equals(mName);
+ }
+
+ @RobocopTarget
+ public boolean inGuestMode() {
+ if (mInGuestMode == null) {
+ mInGuestMode = isGuestProfile(GeckoAppShell.getApplicationContext(),
+ mName, mProfileDir);
+ }
+ return mInGuestMode;
+ }
+
+ /**
+ * Return an Object that can be used with a synchronized statement to allow
+ * exclusive access to the profile.
+ */
+ public Object getLock() {
+ return this;
+ }
+
+ /**
+ * Retrieves the directory backing the profile. This method acts
+ * as a lazy initializer for the GeckoProfile instance.
+ */
+ @RobocopTarget
+ public synchronized File getDir() {
+ forceCreateLocked();
+ return mProfileDir;
+ }
+
+ /**
+ * Forces profile creation. Consider using {@link #getDir()} to initialize the profile instead - it is the
+ * lazy initializer and, for our code reasoning abilities, we should initialize the profile in one place.
+ */
+ private void forceCreateLocked() {
+ if (mProfileDir != null) {
+ return;
+ }
+
+ try {
+ // Check if a profile with this name already exists.
+ try {
+ mProfileDir = findProfileDir();
+ Log.d(LOGTAG, "Found profile dir.");
+ } catch (NoSuchProfileException noSuchProfile) {
+ // If it doesn't exist, create it.
+ mProfileDir = createProfileDir();
+ }
+ } catch (IOException ioe) {
+ Log.e(LOGTAG, "Error getting profile dir", ioe);
+ }
+ }
+
+ public File getFile(String aFile) {
+ File f = getDir();
+ if (f == null)
+ return null;
+
+ return new File(f, aFile);
+ }
+
+ /**
+ * Retrieves the Gecko client ID from the filesystem. If the client ID does not exist, we attempt to migrate and
+ * persist it from FHR and, if that fails, we attempt to create a new one ourselves.
+ *
+ * This method assumes the client ID is located in a file at a hard-coded path within the profile. The format of
+ * this file is a JSONObject which at the bottom level contains a String -> String mapping containing the client ID.
+ *
+ * WARNING: the platform provides a JSM to retrieve the client ID [1] and this would be a
+ * robust way to access it. However, we don't want to rely on Gecko running in order to get
+ * the client ID so instead we access the file this module accesses directly. However, it's
+ * possible the format of this file (and the access calls in the jsm) will change, leaving
+ * this code to fail. There are tests in TestGeckoProfile to verify the file format but be
+ * warned: THIS IS NOT FOOLPROOF.
+ *
+ * [1]: https://dxr.mozilla.org/mozilla-central/source/toolkit/modules/ClientID.jsm
+ *
+ * @throws IOException if the client ID could not be retrieved.
+ */
+ // Mimics ClientID.jsm – _doLoadClientID.
+ @WorkerThread
+ public String getClientId() throws IOException {
+ try {
+ return getValidClientIdFromDisk(CLIENT_ID_FILE_PATH);
+ } catch (final IOException e) {
+ // Avoid log spam: don't log the full Exception w/ the stack trace.
+ Log.d(LOGTAG, "Could not get client ID - attempting to migrate ID from FHR: " + e.getLocalizedMessage());
+ }
+
+ String clientIdToWrite;
+ try {
+ clientIdToWrite = getValidClientIdFromDisk(FHR_CLIENT_ID_FILE_PATH);
+ } catch (final IOException e) {
+ // Avoid log spam: don't log the full Exception w/ the stack trace.
+ Log.d(LOGTAG, "Could not migrate client ID from FHR – creating a new one: " + e.getLocalizedMessage());
+ clientIdToWrite = generateNewClientId();
+ }
+
+ // There is a possibility Gecko is running and the Gecko telemetry implementation decided it's time to generate
+ // the client ID, writing client ID underneath us. Since it's highly unlikely (e.g. we run in onStart before
+ // Gecko is started), we don't handle that possibility besides writing the ID and then reading from the file
+ // again (rather than just returning the value we generated before writing).
+ //
+ // In the event it does happen, any discrepancy will be resolved after a restart. In the mean time, both this
+ // implementation and the Gecko implementation could upload documents with inconsistent IDs.
+ //
+ // In any case, if we get an exception, intentionally throw - there's nothing more to do here.
+ persistClientId(clientIdToWrite);
+ return getValidClientIdFromDisk(CLIENT_ID_FILE_PATH);
+ }
+
+ protected static String generateNewClientId() {
+ return UUID.randomUUID().toString();
+ }
+
+ /**
+ * @return a valid client ID
+ * @throws IOException if a valid client ID could not be retrieved
+ */
+ @WorkerThread
+ private String getValidClientIdFromDisk(final String filePath) throws IOException {
+ final JSONObject obj = readJSONObjectFromFile(filePath);
+ final String clientId = obj.optString(CLIENT_ID_JSON_ATTR);
+ if (isClientIdValid(clientId)) {
+ return clientId;
+ }
+ throw new IOException("Received client ID is invalid: " + clientId);
+ }
+
+ /**
+ * Persists the given client ID to disk. This will overwrite any existing files.
+ */
+ @WorkerThread
+ private void persistClientId(final String clientId) throws IOException {
+ if (!ensureParentDirs(CLIENT_ID_FILE_PATH)) {
+ throw new IOException("Could not create client ID parent directories");
+ }
+
+ final JSONObject obj = new JSONObject();
+ try {
+ obj.put(CLIENT_ID_JSON_ATTR, clientId);
+ } catch (final JSONException e) {
+ throw new IOException("Could not create client ID JSON object", e);
+ }
+
+ // ClientID.jsm overwrites the file to store the client ID so it's okay if we do it too.
+ Log.d(LOGTAG, "Attempting to write new client ID");
+ writeFile(CLIENT_ID_FILE_PATH, obj.toString()); // Logs errors within function: ideally we'd throw.
+ }
+
+ // From ClientID.jsm - isValidClientID.
+ public static boolean isClientIdValid(final String clientId) {
+ // We could use UUID.fromString but, for consistency, we take the implementation from ClientID.jsm.
+ if (TextUtils.isEmpty(clientId)) {
+ return false;
+ }
+ return clientId.matches("(?i:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})");
+ }
+
+ /**
+ * Gets the profile creation date and persists it if it had to be generated.
+ *
+ * To get this value, we first look in times.json. If that could not be accessed, we
+ * return the package's first install date. This is not a perfect solution because a
+ * user may have large gap between install time and first use.
+ *
+ * A more correct algorithm could be the one performed by the JS code in ProfileAge.jsm
+ * getOldestProfileTimestamp: walk the tree and return the oldest timestamp on the files
+ * within the profile. However, since times.json will only not exist for the small
+ * number of really old profiles, we're okay with the package install date compromise for
+ * simplicity.
+ *
+ * @return the profile creation date in the format returned by {@link System#currentTimeMillis()}
+ * or -1 if the value could not be persisted.
+ */
+ @WorkerThread
+ public long getAndPersistProfileCreationDate(final Context context) {
+ try {
+ return getProfileCreationDateFromTimesFile();
+ } catch (final IOException e) {
+ Log.d(LOGTAG, "Unable to retrieve profile creation date from times.json. Getting from system...");
+ final long packageInstallMillis = org.mozilla.gecko.util.ContextUtils.getCurrentPackageInfo(context).firstInstallTime;
+ try {
+ persistProfileCreationDateToTimesFile(packageInstallMillis);
+ } catch (final IOException ioEx) {
+ // We return -1 to ensure the profileCreationDate
+ // will either be an error (-1) or a consistent value.
+ Log.w(LOGTAG, "Unable to persist profile creation date - returning -1");
+ return -1;
+ }
+
+ return packageInstallMillis;
+ }
+ }
+
+ @WorkerThread
+ private long getProfileCreationDateFromTimesFile() throws IOException {
+ final JSONObject obj = readJSONObjectFromFile(TIMES_PATH);
+ try {
+ return obj.getLong(PROFILE_CREATION_DATE_JSON_ATTR);
+ } catch (final JSONException e) {
+ // Don't log to avoid leaking data in JSONObject.
+ throw new IOException("Profile creation does not exist in JSONObject");
+ }
+ }
+
+ @WorkerThread
+ private void persistProfileCreationDateToTimesFile(final long profileCreationMillis) throws IOException {
+ final JSONObject obj = new JSONObject();
+ try {
+ obj.put(PROFILE_CREATION_DATE_JSON_ATTR, profileCreationMillis);
+ } catch (final JSONException e) {
+ // Don't log to avoid leaking data in JSONObject.
+ throw new IOException("Unable to persist profile creation date to times file");
+ }
+ Log.d(LOGTAG, "Attempting to write new profile creation date");
+ writeFile(TIMES_PATH, obj.toString()); // Ideally we'd throw here too.
+ }
+
+ /**
+ * Updates the state of the old session data file.
+ *
+ * sessionstore.js should hold the current session, and sessionstore.old should
+ * hold the previous session (where it is used to read the "tabs from last time").
+ * If we're not restoring tabs automatically, sessionstore.js needs to be moved to
+ * sessionstore.old, so we can display the correct "tabs from last time".
+ * If we *are* restoring tabs, we need to delete outdated copies of sessionstore.old,
+ * so we don't continue showing stale "tabs from last time" indefinitely.
+ *
+ * @param shouldRestore Pass true if we are automatically restoring last session's tabs.
+ */
+ public void updateSessionFile(boolean shouldRestore) {
+ File sessionFilePrevious = getFile(SESSION_FILE_PREVIOUS);
+ if (!shouldRestore) {
+ File sessionFile = getFile(SESSION_FILE);
+ if (sessionFile != null && sessionFile.exists()) {
+ sessionFile.renameTo(sessionFilePrevious);
+ }
+ } else {
+ if (sessionFilePrevious != null && sessionFilePrevious.exists() &&
+ System.currentTimeMillis() - sessionFilePrevious.lastModified() > MAX_PREVIOUS_FILE_AGE) {
+ sessionFilePrevious.delete();
+ }
+ }
+ synchronized (this) {
+ mOldSessionDataProcessed = true;
+ notifyAll();
+ }
+ }
+
+ public void waitForOldSessionDataProcessing() {
+ synchronized (this) {
+ while (!mOldSessionDataProcessed) {
+ try {
+ wait();
+ } catch (final InterruptedException e) {
+ // Ignore and wait again.
+ }
+ }
+ }
+ }
+
+ /**
+ * Get the string from a session file.
+ *
+ * The session can either be read from sessionstore.js or sessionstore.bak.
+ * In general, sessionstore.js holds the current session, and
+ * sessionstore.bak holds a backup copy in case of interrupted writes.
+ *
+ * @param readBackup if true, the session is read from sessionstore.bak;
+ * otherwise, the session is read from sessionstore.js
+ *
+ * @return the session string
+ */
+ public String readSessionFile(boolean readBackup) {
+ return readSessionFile(readBackup ? SESSION_FILE_BACKUP : SESSION_FILE);
+ }
+
+ /**
+ * Get the string from last session's session file.
+ *
+ * If we are not restoring tabs automatically, sessionstore.old will contain
+ * the previous session.
+ *
+ * @return the session string
+ */
+ public String readPreviousSessionFile() {
+ return readSessionFile(SESSION_FILE_PREVIOUS);
+ }
+
+ private String readSessionFile(String fileName) {
+ File sessionFile = getFile(fileName);
+
+ try {
+ if (sessionFile != null && sessionFile.exists()) {
+ return readFile(sessionFile);
+ }
+ } catch (IOException ioe) {
+ Log.e(LOGTAG, "Unable to read session file", ioe);
+ }
+ return null;
+ }
+
+ /**
+ * Checks whether the session store file exists.
+ */
+ public boolean sessionFileExists() {
+ File sessionFile = getFile(SESSION_FILE);
+
+ return sessionFile != null && sessionFile.exists();
+ }
+
+ /**
+ * Ensures the parent director(y|ies) of the given filename exist by making them
+ * if they don't already exist..
+ *
+ * @param filename The path to the file whose parents should be made directories
+ * @return true if the parent directory exists, false otherwise
+ */
+ @WorkerThread
+ protected boolean ensureParentDirs(final String filename) {
+ final File file = new File(getDir(), filename);
+ final File parentFile = file.getParentFile();
+ return parentFile.mkdirs() || parentFile.isDirectory();
+ }
+
+ public void writeFile(final String filename, final String data) {
+ File file = new File(getDir(), filename);
+ BufferedWriter bufferedWriter = null;
+ try {
+ bufferedWriter = new BufferedWriter(new FileWriter(file, false));
+ bufferedWriter.write(data);
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Unable to write to file", e);
+ } finally {
+ try {
+ if (bufferedWriter != null) {
+ bufferedWriter.close();
+ }
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Error closing writer while writing to file", e);
+ }
+ }
+ }
+
+ @WorkerThread
+ public JSONObject readJSONObjectFromFile(final String filename) throws IOException {
+ final String fileContents;
+ try {
+ fileContents = readFile(filename);
+ } catch (final IOException e) {
+ // Don't log exception to avoid leaking profile path.
+ throw new IOException("Could not access given file to retrieve JSONObject");
+ }
+
+ try {
+ return new JSONObject(fileContents);
+ } catch (final JSONException e) {
+ // Don't log exception to avoid leaking profile path.
+ throw new IOException("Could not parse JSON to retrieve JSONObject");
+ }
+ }
+
+ public JSONArray readJSONArrayFromFile(final String filename) {
+ String fileContent;
+ try {
+ fileContent = readFile(filename);
+ } catch (IOException expected) {
+ return new JSONArray();
+ }
+
+ JSONArray jsonArray;
+ try {
+ jsonArray = new JSONArray(fileContent);
+ } catch (JSONException e) {
+ jsonArray = new JSONArray();
+ }
+ return jsonArray;
+ }
+
+ public String readFile(String filename) throws IOException {
+ File dir = getDir();
+ if (dir == null) {
+ throw new IOException("No profile directory found");
+ }
+ File target = new File(dir, filename);
+ return readFile(target);
+ }
+
+ private String readFile(File target) throws IOException {
+ FileReader fr = new FileReader(target);
+ try {
+ StringBuilder sb = new StringBuilder();
+ char[] buf = new char[8192];
+ int read = fr.read(buf);
+ while (read >= 0) {
+ sb.append(buf, 0, read);
+ read = fr.read(buf);
+ }
+ return sb.toString();
+ } finally {
+ fr.close();
+ }
+ }
+
+ public boolean deleteFileFromProfileDir(String fileName) throws IllegalArgumentException {
+ if (TextUtils.isEmpty(fileName)) {
+ throw new IllegalArgumentException("Filename cannot be empty.");
+ }
+ File file = new File(getDir(), fileName);
+ return file.delete();
+ }
+
+ private boolean remove() {
+ try {
+ synchronized (this) {
+ if (mProfileDir != null && mProfileDir.exists()) {
+ FileUtils.delete(mProfileDir);
+ }
+
+ if (isCustomProfile()) {
+ // Custom profiles don't have profile.ini sections that we need to remove.
+ return true;
+ }
+
+ try {
+ // If findProfileDir() succeeds, it means the profile was created
+ // through forceCreate(), so we set mProfileDir to null to enable
+ // forceCreate() to create the profile again.
+ findProfileDir();
+ mProfileDir = null;
+
+ } catch (final NoSuchProfileException e) {
+ // If findProfileDir() throws, it means the profile was not created
+ // through forceCreate(), and we have to preserve mProfileDir because
+ // it was given to us. In that case, there's nothing left to do here.
+ return true;
+ }
+ }
+
+ final INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir);
+ final Hashtable<String, INISection> sections = parser.getSections();
+ for (Enumeration<INISection> e = sections.elements(); e.hasMoreElements();) {
+ final INISection section = e.nextElement();
+ String name = section.getStringProperty("Name");
+
+ if (name == null || !name.equals(mName)) {
+ continue;
+ }
+
+ if (section.getName().startsWith("Profile")) {
+ // ok, we have stupid Profile#-named things. Rename backwards.
+ try {
+ int sectionNumber = Integer.parseInt(section.getName().substring("Profile".length()));
+ String curSection = "Profile" + sectionNumber;
+ String nextSection = "Profile" + (sectionNumber + 1);
+
+ sections.remove(curSection);
+
+ while (sections.containsKey(nextSection)) {
+ parser.renameSection(nextSection, curSection);
+ sectionNumber++;
+
+ curSection = nextSection;
+ nextSection = "Profile" + (sectionNumber + 1);
+ }
+ } catch (NumberFormatException nex) {
+ // uhm, malformed Profile thing; we can't do much.
+ Log.e(LOGTAG, "Malformed section name in profiles.ini: " + section.getName());
+ return false;
+ }
+ } else {
+ // this really shouldn't be the case, but handle it anyway
+ parser.removeSection(mName);
+ }
+
+ break;
+ }
+
+ parser.write();
+ return true;
+ } catch (IOException ex) {
+ Log.w(LOGTAG, "Failed to remove profile.", ex);
+ return false;
+ }
+ }
+
+ /**
+ * @return the default profile name for this application, or
+ * {@link GeckoProfile#DEFAULT_PROFILE} if none could be found.
+ *
+ * @throws NoMozillaDirectoryException
+ * if the Mozilla directory did not exist and could not be
+ * created.
+ */
+ public static String getDefaultProfileName(final Context context) throws NoMozillaDirectoryException {
+ // Have we read the default profile from the INI already?
+ // Changing the default profile requires a restart, so we don't
+ // need to worry about runtime changes.
+ if (sDefaultProfileName != null) {
+ return sDefaultProfileName;
+ }
+
+ final String profileName = GeckoProfileDirectories.findDefaultProfileName(context);
+ if (profileName == null) {
+ // Note that we don't persist this back to profiles.ini.
+ sDefaultProfileName = DEFAULT_PROFILE;
+ return DEFAULT_PROFILE;
+ }
+
+ sDefaultProfileName = profileName;
+ return sDefaultProfileName;
+ }
+
+ private File findProfileDir() throws NoSuchProfileException {
+ if (isCustomProfile()) {
+ return mProfileDir;
+ }
+ return GeckoProfileDirectories.findProfileDir(mMozillaDir, mName);
+ }
+
+ @WorkerThread
+ private File createProfileDir() throws IOException {
+ if (isCustomProfile()) {
+ // Custom profiles must already exist.
+ return mProfileDir;
+ }
+
+ INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir);
+
+ // Salt the name of our requested profile
+ String saltedName;
+ File profileDir;
+ do {
+ saltedName = GeckoProfileDirectories.saltProfileName(mName);
+ profileDir = new File(mMozillaDir, saltedName);
+ } while (profileDir.exists());
+
+ // Attempt to create the salted profile dir
+ if (!profileDir.mkdirs()) {
+ throw new IOException("Unable to create profile.");
+ }
+ Log.d(LOGTAG, "Created new profile dir.");
+
+ // Now update profiles.ini
+ // If this is the first time its created, we also add a General section
+ // look for the first profile number that isn't taken yet
+ int profileNum = 0;
+ boolean isDefaultSet = false;
+ INISection profileSection;
+ while ((profileSection = parser.getSection("Profile" + profileNum)) != null) {
+ profileNum++;
+ if (profileSection.getProperty("Default") != null) {
+ isDefaultSet = true;
+ }
+ }
+
+ profileSection = new INISection("Profile" + profileNum);
+ profileSection.setProperty("Name", mName);
+ profileSection.setProperty("IsRelative", 1);
+ profileSection.setProperty("Path", saltedName);
+
+ if (parser.getSection("General") == null) {
+ INISection generalSection = new INISection("General");
+ generalSection.setProperty("StartWithLastProfile", 1);
+ parser.addSection(generalSection);
+ }
+
+ if (!isDefaultSet) {
+ // only set as default if this is the first profile we're creating
+ profileSection.setProperty("Default", 1);
+ }
+
+ parser.addSection(profileSection);
+ parser.write();
+
+ enqueueInitialization(profileDir);
+
+ // Write out profile creation time, mirroring the logic in nsToolkitProfileService.
+ try {
+ FileOutputStream stream = new FileOutputStream(profileDir.getAbsolutePath() + File.separator + TIMES_PATH);
+ OutputStreamWriter writer = new OutputStreamWriter(stream, Charset.forName("UTF-8"));
+ try {
+ writer.append("{\"created\": " + System.currentTimeMillis() + "}\n");
+ } finally {
+ writer.close();
+ }
+ } catch (Exception e) {
+ // Best-effort.
+ Log.w(LOGTAG, "Couldn't write " + TIMES_PATH, e);
+ }
+
+ // Create the client ID file before Gecko starts (we assume this method
+ // is called before Gecko starts). If we let Gecko start, the JS telemetry
+ // code may try to write to the file at the same time Java does.
+ persistClientId(generateNewClientId());
+
+ return profileDir;
+ }
+
+ /**
+ * This method is called once, immediately before creation of the profile
+ * directory completes.
+ *
+ * It queues up work to be done in the background to prepare the profile,
+ * such as adding default bookmarks.
+ *
+ * This is public for use *from tests only*!
+ */
+ @RobocopTarget
+ public void enqueueInitialization(final File profileDir) {
+ Log.i(LOGTAG, "Enqueuing profile init.");
+
+ final Bundle message = new Bundle(2);
+ message.putCharSequence("name", getName());
+ message.putCharSequence("path", profileDir.getAbsolutePath());
+ EventDispatcher.getInstance().dispatch("Profile:Create", message);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfileDirectories.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfileDirectories.java
new file mode 100644
index 000000000..2afb54bc4
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfileDirectories.java
@@ -0,0 +1,230 @@
+/* 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 java.io.File;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.util.INIParser;
+import org.mozilla.gecko.util.INISection;
+
+import android.content.Context;
+
+/**
+ * <code>GeckoProfileDirectories</code> manages access to mappings from profile
+ * names to salted profile directory paths, as well as the default profile name.
+ *
+ * This class will eventually come to encapsulate the remaining logic embedded
+ * in profiles.ini; for now it's a read-only wrapper.
+ */
+public class GeckoProfileDirectories {
+ @SuppressWarnings("serial")
+ public static class NoMozillaDirectoryException extends Exception {
+ public NoMozillaDirectoryException(Throwable cause) {
+ super(cause);
+ }
+
+ public NoMozillaDirectoryException(String reason) {
+ super(reason);
+ }
+
+ public NoMozillaDirectoryException(String reason, Throwable cause) {
+ super(reason, cause);
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public static class NoSuchProfileException extends Exception {
+ public NoSuchProfileException(String detailMessage, Throwable cause) {
+ super(detailMessage, cause);
+ }
+
+ public NoSuchProfileException(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ private interface INISectionPredicate {
+ public boolean matches(INISection section);
+ }
+
+ private static final String MOZILLA_DIR_NAME = "mozilla";
+
+ /**
+ * Returns true if the supplied profile entry represents the default profile.
+ */
+ private static final INISectionPredicate sectionIsDefault = new INISectionPredicate() {
+ @Override
+ public boolean matches(INISection section) {
+ return section.getIntProperty("Default") == 1;
+ }
+ };
+
+ /**
+ * Returns true if the supplied profile entry has a 'Name' field.
+ */
+ private static final INISectionPredicate sectionHasName = new INISectionPredicate() {
+ @Override
+ public boolean matches(INISection section) {
+ final String name = section.getStringProperty("Name");
+ return name != null;
+ }
+ };
+
+ @RobocopTarget
+ public static INIParser getProfilesINI(File mozillaDir) {
+ return new INIParser(new File(mozillaDir, "profiles.ini"));
+ }
+
+ /**
+ * Utility method to compute a salted profile name: eight random alphanumeric
+ * characters, followed by a period, followed by the profile name.
+ */
+ public static String saltProfileName(final String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("Cannot salt null profile name.");
+ }
+
+ final String allowedChars = "abcdefghijklmnopqrstuvwxyz0123456789";
+ final int scale = allowedChars.length();
+ final int saltSize = 8;
+
+ final StringBuilder saltBuilder = new StringBuilder(saltSize + 1 + name.length());
+ for (int i = 0; i < saltSize; i++) {
+ saltBuilder.append(allowedChars.charAt((int)(Math.random() * scale)));
+ }
+ saltBuilder.append('.');
+ saltBuilder.append(name);
+ return saltBuilder.toString();
+ }
+
+ /**
+ * Return the Mozilla directory within the files directory of the provided
+ * context. This should always be the same within a running application.
+ *
+ * This method is package-scoped so that new {@link GeckoProfile} instances can
+ * contextualize themselves.
+ *
+ * @return a new File object for the Mozilla directory.
+ * @throws NoMozillaDirectoryException
+ * if the directory did not exist and could not be created.
+ */
+ @RobocopTarget
+ public static File getMozillaDirectory(Context context) throws NoMozillaDirectoryException {
+ final File mozillaDir = new File(context.getFilesDir(), MOZILLA_DIR_NAME);
+ if (mozillaDir.mkdirs() || mozillaDir.isDirectory()) {
+ return mozillaDir;
+ }
+
+ // Although this leaks a path to the system log, the path is
+ // predictable (unlike a profile directory), so this is fine.
+ throw new NoMozillaDirectoryException("Unable to create mozilla directory at " + mozillaDir.getAbsolutePath());
+ }
+
+ /**
+ * Discover the default profile name by examining profiles.ini.
+ *
+ * Package-scoped because {@link GeckoProfile} needs access to it.
+ *
+ * @return null if there is no "Default" entry in profiles.ini, or the profile
+ * name if there is.
+ * @throws NoMozillaDirectoryException
+ * if the Mozilla directory did not exist and could not be created.
+ */
+ static String findDefaultProfileName(final Context context) throws NoMozillaDirectoryException {
+ final INIParser parser = GeckoProfileDirectories.getProfilesINI(getMozillaDirectory(context));
+ if (parser.getSections() != null) {
+ for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements(); ) {
+ final INISection section = e.nextElement();
+ if (section.getIntProperty("Default") == 1) {
+ return section.getStringProperty("Name");
+ }
+ }
+ }
+ return null;
+ }
+
+ static Map<String, String> getDefaultProfile(final File mozillaDir) {
+ return getMatchingProfiles(mozillaDir, sectionIsDefault, true);
+ }
+
+ static Map<String, String> getProfilesNamed(final File mozillaDir, final String name) {
+ final INISectionPredicate predicate = new INISectionPredicate() {
+ @Override
+ public boolean matches(final INISection section) {
+ return name.equals(section.getStringProperty("Name"));
+ }
+ };
+ return getMatchingProfiles(mozillaDir, predicate, true);
+ }
+
+ /**
+ * Calls {@link GeckoProfileDirectories#getMatchingProfiles(File, INISectionPredicate, boolean)}
+ * with a filter to ensure that all profiles are named.
+ */
+ static Map<String, String> getAllProfiles(final File mozillaDir) {
+ return getMatchingProfiles(mozillaDir, sectionHasName, false);
+ }
+
+ /**
+ * Return a mapping from the names of all matching profiles (that is,
+ * profiles appearing in profiles.ini that match the supplied predicate) to
+ * their absolute paths on disk.
+ *
+ * @param mozillaDir
+ * a directory containing profiles.ini.
+ * @param predicate
+ * a predicate to use when evaluating whether to include a
+ * particular INI section.
+ * @param stopOnSuccess
+ * if true, this method will return with the first result that
+ * matches the predicate; if false, all matching results are
+ * included.
+ * @return a {@link Map} from name to path.
+ */
+ public static Map<String, String> getMatchingProfiles(final File mozillaDir, INISectionPredicate predicate, boolean stopOnSuccess) {
+ final HashMap<String, String> result = new HashMap<String, String>();
+ final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozillaDir);
+
+ if (parser.getSections() != null) {
+ for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements(); ) {
+ final INISection section = e.nextElement();
+ if (predicate == null || predicate.matches(section)) {
+ final String name = section.getStringProperty("Name");
+ final String pathString = section.getStringProperty("Path");
+ final boolean isRelative = section.getIntProperty("IsRelative") == 1;
+ final File path = isRelative ? new File(mozillaDir, pathString) : new File(pathString);
+ result.put(name, path.getAbsolutePath());
+
+ if (stopOnSuccess) {
+ return result;
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ public static File findProfileDir(final File mozillaDir, final String profileName) throws NoSuchProfileException {
+ // Open profiles.ini to find the correct path.
+ final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozillaDir);
+ if (parser.getSections() != null) {
+ for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements(); ) {
+ final INISection section = e.nextElement();
+ final String name = section.getStringProperty("Name");
+ if (name != null && name.equals(profileName)) {
+ if (section.getIntProperty("IsRelative") == 1) {
+ return new File(mozillaDir, section.getStringProperty("Path"));
+ }
+ return new File(section.getStringProperty("Path"));
+ }
+ }
+ }
+ throw new NoSuchProfileException("No profile " + profileName);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenOrientation.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenOrientation.java
new file mode 100644
index 000000000..23f84f52a
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenOrientation.java
@@ -0,0 +1,423 @@
+/* -*- 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;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.util.Log;
+import android.view.Surface;
+import android.app.Activity;
+
+import java.util.Arrays;
+import java.util.List;
+
+/*
+ * Updates, locks and unlocks the screen orientation.
+ *
+ * Note: Replaces the OnOrientationChangeListener to avoid redundant rotation
+ * event handling.
+ */
+public class GeckoScreenOrientation {
+ private static final String LOGTAG = "GeckoScreenOrientation";
+
+ // Make sure that any change in dom/base/ScreenOrientation.h happens here too.
+ public enum ScreenOrientation {
+ NONE(0),
+ PORTRAIT_PRIMARY(1 << 0),
+ PORTRAIT_SECONDARY(1 << 1),
+ PORTRAIT(PORTRAIT_PRIMARY.value | PORTRAIT_SECONDARY.value),
+ LANDSCAPE_PRIMARY(1 << 2),
+ LANDSCAPE_SECONDARY(1 << 3),
+ LANDSCAPE(LANDSCAPE_PRIMARY.value | LANDSCAPE_SECONDARY.value),
+ DEFAULT(1 << 4);
+
+ public final short value;
+
+ private ScreenOrientation(int value) {
+ this.value = (short)value;
+ }
+
+ private final static ScreenOrientation[] sValues = ScreenOrientation.values();
+
+ public static ScreenOrientation get(int value) {
+ for (ScreenOrientation orient: sValues) {
+ if (orient.value == value) {
+ return orient;
+ }
+ }
+ return NONE;
+ }
+ }
+
+ // Singleton instance.
+ private static GeckoScreenOrientation sInstance;
+ // Default screen orientation, used for initialization and unlocking.
+ private static final ScreenOrientation DEFAULT_SCREEN_ORIENTATION = ScreenOrientation.DEFAULT;
+ // Default rotation, used when device rotation is unknown.
+ private static final int DEFAULT_ROTATION = Surface.ROTATION_0;
+ // Default orientation, used if screen orientation is unspecified.
+ private ScreenOrientation mDefaultScreenOrientation;
+ // Last updated screen orientation.
+ private ScreenOrientation mScreenOrientation;
+ // Whether the update should notify Gecko about screen orientation changes.
+ private boolean mShouldNotify = true;
+ // Configuration screen orientation preference path.
+ private static final String DEFAULT_SCREEN_ORIENTATION_PREF = "app.orientation.default";
+
+ public GeckoScreenOrientation() {
+ PrefsHelper.getPref(DEFAULT_SCREEN_ORIENTATION_PREF, new PrefsHelper.PrefHandlerBase() {
+ @Override public void prefValue(String pref, String value) {
+ // Read and update the configuration default preference.
+ mDefaultScreenOrientation = screenOrientationFromArrayString(value);
+ setRequestedOrientation(mDefaultScreenOrientation);
+ }
+ });
+
+ mDefaultScreenOrientation = DEFAULT_SCREEN_ORIENTATION;
+ update();
+ }
+
+ public static GeckoScreenOrientation getInstance() {
+ if (sInstance == null) {
+ sInstance = new GeckoScreenOrientation();
+ }
+ return sInstance;
+ }
+
+ /*
+ * Enable Gecko screen orientation events on update.
+ */
+ public void enableNotifications() {
+ update();
+ mShouldNotify = true;
+ }
+
+ /*
+ * Disable Gecko screen orientation events on update.
+ */
+ public void disableNotifications() {
+ mShouldNotify = false;
+ }
+
+ /*
+ * Update screen orientation.
+ * Retrieve orientation and rotation via GeckoAppShell.
+ *
+ * @return Whether the screen orientation has changed.
+ */
+ public boolean update() {
+ Activity activity = getActivity();
+ if (activity == null) {
+ return false;
+ }
+ Configuration config = activity.getResources().getConfiguration();
+ return update(config.orientation);
+ }
+
+ /*
+ * Update screen orientation given the android orientation.
+ * Retrieve rotation via GeckoAppShell.
+ *
+ * @param aAndroidOrientation
+ * Android screen orientation from Configuration.orientation.
+ *
+ * @return Whether the screen orientation has changed.
+ */
+ public boolean update(int aAndroidOrientation) {
+ return update(getScreenOrientation(aAndroidOrientation, getRotation()));
+ }
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void onOrientationChange(short screenOrientation, short angle);
+
+ /*
+ * Update screen orientation given the screen orientation.
+ *
+ * @param aScreenOrientation
+ * Gecko screen orientation based on android orientation and rotation.
+ *
+ * @return Whether the screen orientation has changed.
+ */
+ public boolean update(ScreenOrientation aScreenOrientation) {
+ if (mScreenOrientation == aScreenOrientation) {
+ return false;
+ }
+ mScreenOrientation = aScreenOrientation;
+ Log.d(LOGTAG, "updating to new orientation " + mScreenOrientation);
+ if (mShouldNotify) {
+ // Gecko expects a definite screen orientation, so we default to the
+ // primary orientations.
+ if (aScreenOrientation == ScreenOrientation.PORTRAIT) {
+ aScreenOrientation = ScreenOrientation.PORTRAIT_PRIMARY;
+ } else if (aScreenOrientation == ScreenOrientation.LANDSCAPE) {
+ aScreenOrientation = ScreenOrientation.LANDSCAPE_PRIMARY;
+ }
+
+ if (GeckoThread.isRunning()) {
+ onOrientationChange(aScreenOrientation.value, getAngle());
+ } else {
+ GeckoThread.queueNativeCall(GeckoScreenOrientation.class, "onOrientationChange",
+ aScreenOrientation.value, getAngle());
+ }
+ }
+ GeckoAppShell.resetScreenSize();
+ return true;
+ }
+
+ /*
+ * @return The Android orientation (Configuration.orientation).
+ */
+ public int getAndroidOrientation() {
+ return screenOrientationToAndroidOrientation(getScreenOrientation());
+ }
+
+ /*
+ * @return The Gecko screen orientation derived from Android orientation and
+ * rotation.
+ */
+ public ScreenOrientation getScreenOrientation() {
+ return mScreenOrientation;
+ }
+
+ /*
+ * Lock screen orientation given the Gecko screen orientation.
+ *
+ * @param aGeckoOrientation
+ * The Gecko orientation provided.
+ */
+ public void lock(int aGeckoOrientation) {
+ lock(ScreenOrientation.get(aGeckoOrientation));
+ }
+
+ /*
+ * Lock screen orientation given the Gecko screen orientation.
+ * Retrieve rotation via GeckoAppShell.
+ *
+ * @param aScreenOrientation
+ * Gecko screen orientation derived from Android orientation and
+ * rotation.
+ *
+ * @return Whether the locking was successful.
+ */
+ public boolean lock(ScreenOrientation aScreenOrientation) {
+ Log.d(LOGTAG, "locking to " + aScreenOrientation);
+ update(aScreenOrientation);
+ return setRequestedOrientation(aScreenOrientation);
+ }
+
+ /*
+ * Unlock and update screen orientation.
+ *
+ * @return Whether the unlocking was successful.
+ */
+ public boolean unlock() {
+ Log.d(LOGTAG, "unlocking");
+ setRequestedOrientation(mDefaultScreenOrientation);
+ return update();
+ }
+
+ private Activity getActivity() {
+ if (GeckoAppShell.getGeckoInterface() == null) {
+ return null;
+ }
+ return GeckoAppShell.getGeckoInterface().getActivity();
+ }
+
+ /*
+ * Set the given requested orientation for the current activity.
+ * This is essentially an unlock without an update.
+ *
+ * @param aScreenOrientation
+ * Gecko screen orientation.
+ *
+ * @return Whether the requested orientation was set. This can only fail if
+ * the current activity cannot be retrieved via GeckoAppShell.
+ *
+ */
+ private boolean setRequestedOrientation(ScreenOrientation aScreenOrientation) {
+ int activityOrientation = screenOrientationToActivityInfoOrientation(aScreenOrientation);
+ Activity activity = getActivity();
+ if (activity == null) {
+ Log.w(LOGTAG, "setRequestOrientation: failed to get activity");
+ return false;
+ }
+ if (activity.getRequestedOrientation() == activityOrientation) {
+ return false;
+ }
+ activity.setRequestedOrientation(activityOrientation);
+ return true;
+ }
+
+ /*
+ * Combine the Android orientation and rotation to the Gecko orientation.
+ *
+ * @param aAndroidOrientation
+ * Android orientation from Configuration.orientation.
+ * @param aRotation
+ * Device rotation from Display.getRotation().
+ *
+ * @return Gecko screen orientation.
+ */
+ private ScreenOrientation getScreenOrientation(int aAndroidOrientation, int aRotation) {
+ boolean isPrimary = aRotation == Surface.ROTATION_0 || aRotation == Surface.ROTATION_90;
+ if (aAndroidOrientation == Configuration.ORIENTATION_PORTRAIT) {
+ if (isPrimary) {
+ // Non-rotated portrait device or landscape device rotated
+ // to primary portrait mode counter-clockwise.
+ return ScreenOrientation.PORTRAIT_PRIMARY;
+ }
+ return ScreenOrientation.PORTRAIT_SECONDARY;
+ }
+ if (aAndroidOrientation == Configuration.ORIENTATION_LANDSCAPE) {
+ if (isPrimary) {
+ // Non-rotated landscape device or portrait device rotated
+ // to primary landscape mode counter-clockwise.
+ return ScreenOrientation.LANDSCAPE_PRIMARY;
+ }
+ return ScreenOrientation.LANDSCAPE_SECONDARY;
+ }
+ return ScreenOrientation.NONE;
+ }
+
+ /*
+ * @return Device rotation converted to an angle.
+ */
+ public short getAngle() {
+ switch (getRotation()) {
+ case Surface.ROTATION_0:
+ return 0;
+ case Surface.ROTATION_90:
+ return 90;
+ case Surface.ROTATION_180:
+ return 180;
+ case Surface.ROTATION_270:
+ return 270;
+ default:
+ Log.w(LOGTAG, "getAngle: unexpected rotation value");
+ return 0;
+ }
+ }
+
+ /*
+ * @return Device rotation from Display.getRotation().
+ */
+ private int getRotation() {
+ Activity activity = getActivity();
+ if (activity == null) {
+ Log.w(LOGTAG, "getRotation: failed to get activity");
+ return DEFAULT_ROTATION;
+ }
+ return activity.getWindowManager().getDefaultDisplay().getRotation();
+ }
+
+ /*
+ * Retrieve the screen orientation from an array string.
+ *
+ * @param aArray
+ * String containing comma-delimited strings.
+ *
+ * @return First parsed Gecko screen orientation.
+ */
+ public static ScreenOrientation screenOrientationFromArrayString(String aArray) {
+ List<String> orientations = Arrays.asList(aArray.split(","));
+ if (orientations.size() == 0) {
+ // If nothing is listed, return default.
+ Log.w(LOGTAG, "screenOrientationFromArrayString: no orientation in string");
+ return DEFAULT_SCREEN_ORIENTATION;
+ }
+
+ // We don't support multiple orientations yet. To avoid developer
+ // confusion, just take the first one listed.
+ return screenOrientationFromString(orientations.get(0));
+ }
+
+ /*
+ * Retrieve the screen orientation from a string.
+ *
+ * @param aStr
+ * String hopefully containing a screen orientation name.
+ * @return Gecko screen orientation if matched, DEFAULT_SCREEN_ORIENTATION
+ * otherwise.
+ */
+ public static ScreenOrientation screenOrientationFromString(String aStr) {
+ switch (aStr) {
+ case "portrait":
+ return ScreenOrientation.PORTRAIT;
+ case "landscape":
+ return ScreenOrientation.LANDSCAPE;
+ case "portrait-primary":
+ return ScreenOrientation.PORTRAIT_PRIMARY;
+ case "portrait-secondary":
+ return ScreenOrientation.PORTRAIT_SECONDARY;
+ case "landscape-primary":
+ return ScreenOrientation.LANDSCAPE_PRIMARY;
+ case "landscape-secondary":
+ return ScreenOrientation.LANDSCAPE_SECONDARY;
+ }
+
+ Log.w(LOGTAG, "screenOrientationFromString: unknown orientation string: " + aStr);
+ return DEFAULT_SCREEN_ORIENTATION;
+ }
+
+ /*
+ * Convert Gecko screen orientation to Android orientation.
+ *
+ * @param aScreenOrientation
+ * Gecko screen orientation.
+ * @return Android orientation. This conversion is lossy, the Android
+ * orientation does not differentiate between primary and secondary
+ * orientations.
+ */
+ public static int screenOrientationToAndroidOrientation(ScreenOrientation aScreenOrientation) {
+ switch (aScreenOrientation) {
+ case PORTRAIT:
+ case PORTRAIT_PRIMARY:
+ case PORTRAIT_SECONDARY:
+ return Configuration.ORIENTATION_PORTRAIT;
+ case LANDSCAPE:
+ case LANDSCAPE_PRIMARY:
+ case LANDSCAPE_SECONDARY:
+ return Configuration.ORIENTATION_LANDSCAPE;
+ case NONE:
+ case DEFAULT:
+ default:
+ return Configuration.ORIENTATION_UNDEFINED;
+ }
+ }
+
+
+ /*
+ * Convert Gecko screen orientation to Android ActivityInfo orientation.
+ * This is yet another orientation used by Android, but it's more detailed
+ * than the Android orientation.
+ * It is required for screen orientation locking and unlocking.
+ *
+ * @param aScreenOrientation
+ * Gecko screen orientation.
+ * @return Android ActivityInfo orientation.
+ */
+ public static int screenOrientationToActivityInfoOrientation(ScreenOrientation aScreenOrientation) {
+ switch (aScreenOrientation) {
+ case PORTRAIT:
+ case PORTRAIT_PRIMARY:
+ return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+ case PORTRAIT_SECONDARY:
+ return ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+ case LANDSCAPE:
+ case LANDSCAPE_PRIMARY:
+ return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+ case LANDSCAPE_SECONDARY:
+ return ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+ case DEFAULT:
+ case NONE:
+ return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+ default:
+ return ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSharedPrefs.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSharedPrefs.java
new file mode 100644
index 000000000..ec928dd86
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSharedPrefs.java
@@ -0,0 +1,318 @@
+/* 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 java.util.Arrays;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.os.StrictMode;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+/**
+ * {@code GeckoSharedPrefs} provides scoped SharedPreferences instances.
+ * You should use this API instead of using Context.getSharedPreferences()
+ * directly. There are four methods to get scoped SharedPreferences instances:
+ *
+ * forApp()
+ * Use it for app-wide, cross-profile pref keys.
+ * forCrashReporter()
+ * For the crash reporter, which runs in its own process.
+ * forProfile()
+ * Use it to fetch and store keys for the current profile.
+ * forProfileName()
+ * Use it to fetch and store keys from/for a specific profile.
+ *
+ * {@code GeckoSharedPrefs} has a notion of migrations. Migrations can used to
+ * migrate keys from one scope to another. You can trigger a new migration by
+ * incrementing PREFS_VERSION and updating migrateIfNecessary() accordingly.
+ *
+ * Migration history:
+ * 1: Move all PreferenceManager keys to app/profile scopes
+ * 2: Move the crash reporter's private preferences into their own scope
+ */
+@RobocopTarget
+public final class GeckoSharedPrefs {
+ private static final String LOGTAG = "GeckoSharedPrefs";
+
+ // Increment it to trigger a new migration
+ public static final int PREFS_VERSION = 2;
+
+ // Name for app-scoped prefs
+ public static final String APP_PREFS_NAME = "GeckoApp";
+
+ // Name for crash reporter prefs
+ public static final String CRASH_PREFS_NAME = "CrashReporter";
+
+ // Used when fetching profile-scoped prefs.
+ public static final String PROFILE_PREFS_NAME_PREFIX = "GeckoProfile-";
+
+ // The prefs key that holds the current migration
+ private static final String PREFS_VERSION_KEY = "gecko_shared_prefs_migration";
+
+ // For disabling migration when getting a SharedPreferences instance
+ private static final EnumSet<Flags> disableMigrations = EnumSet.of(Flags.DISABLE_MIGRATIONS);
+
+ // The keys that have to be moved from ProfileManager's default
+ // shared prefs to the profile from version 0 to 1.
+ private static final String[] PROFILE_MIGRATIONS_0_TO_1 = {
+ "home_panels",
+ "home_locale"
+ };
+
+ // The keys that have to be moved from the app prefs
+ // into the crash reporter's own prefs.
+ private static final String[] PROFILE_MIGRATIONS_1_TO_2 = {
+ "sendReport",
+ "includeUrl",
+ "allowContact",
+ "contactEmail"
+ };
+
+ // For optimizing the migration check in subsequent get() calls
+ private static volatile boolean migrationDone;
+
+ public enum Flags {
+ DISABLE_MIGRATIONS
+ }
+
+ public static SharedPreferences forApp(Context context) {
+ return forApp(context, EnumSet.noneOf(Flags.class));
+ }
+
+ /**
+ * Returns an app-scoped SharedPreferences instance. You can disable
+ * migrations by using the DISABLE_MIGRATIONS flag.
+ */
+ public static SharedPreferences forApp(Context context, EnumSet<Flags> flags) {
+ if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) {
+ migrateIfNecessary(context);
+ }
+
+ return context.getSharedPreferences(APP_PREFS_NAME, 0);
+ }
+
+ public static SharedPreferences forCrashReporter(Context context) {
+ return forCrashReporter(context, EnumSet.noneOf(Flags.class));
+ }
+
+ /**
+ * Returns a crash-reporter-scoped SharedPreferences instance. You can disable
+ * migrations by using the DISABLE_MIGRATIONS flag.
+ */
+ public static SharedPreferences forCrashReporter(Context context, EnumSet<Flags> flags) {
+ if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) {
+ migrateIfNecessary(context);
+ }
+
+ return context.getSharedPreferences(CRASH_PREFS_NAME, 0);
+ }
+
+ public static SharedPreferences forProfile(Context context) {
+ return forProfile(context, EnumSet.noneOf(Flags.class));
+ }
+
+ /**
+ * Returns a SharedPreferences instance scoped to the current profile
+ * in the app. You can disable migrations by using the DISABLE_MIGRATIONS
+ * flag.
+ */
+ public static SharedPreferences forProfile(Context context, EnumSet<Flags> flags) {
+ String profileName = GeckoProfile.get(context).getName();
+ if (profileName == null) {
+ throw new IllegalStateException("Could not get current profile name");
+ }
+
+ return forProfileName(context, profileName, flags);
+ }
+
+ public static SharedPreferences forProfileName(Context context, String profileName) {
+ return forProfileName(context, profileName, EnumSet.noneOf(Flags.class));
+ }
+
+ /**
+ * Returns an SharedPreferences instance scoped to the given profile name.
+ * You can disable migrations by using the DISABLE_MIGRATION flag.
+ */
+ public static SharedPreferences forProfileName(Context context, String profileName,
+ EnumSet<Flags> flags) {
+ if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) {
+ migrateIfNecessary(context);
+ }
+
+ final String prefsName = PROFILE_PREFS_NAME_PREFIX + profileName;
+ return context.getSharedPreferences(prefsName, 0);
+ }
+
+ /**
+ * Returns the current version of the prefs.
+ */
+ public static int getVersion(Context context) {
+ return forApp(context, disableMigrations).getInt(PREFS_VERSION_KEY, 0);
+ }
+
+ /**
+ * Resets migration flag. Should only be used in tests.
+ */
+ public static synchronized void reset() {
+ migrationDone = false;
+ }
+
+ /**
+ * Performs all prefs migrations in the background thread to avoid StrictMode
+ * exceptions from reading/writing in the UI thread. This method will block
+ * the current thread until the migration is finished.
+ */
+ private static synchronized void migrateIfNecessary(final Context context) {
+ if (migrationDone) {
+ return;
+ }
+
+ // We deliberately perform the migration in the current thread (which
+ // is likely the UI thread) as this is actually cheaper than enforcing a
+ // context switch to another thread (see bug 940575).
+ // Avoid strict mode warnings when doing so.
+ final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+ StrictMode.allowThreadDiskWrites();
+ try {
+ performMigration(context);
+ } finally {
+ StrictMode.setThreadPolicy(savedPolicy);
+ }
+
+ migrationDone = true;
+ }
+
+ private static void performMigration(Context context) {
+ final SharedPreferences appPrefs = forApp(context, disableMigrations);
+
+ final int currentVersion = appPrefs.getInt(PREFS_VERSION_KEY, 0);
+ Log.d(LOGTAG, "Current version = " + currentVersion + ", prefs version = " + PREFS_VERSION);
+
+ if (currentVersion == PREFS_VERSION) {
+ return;
+ }
+
+ Log.d(LOGTAG, "Performing migration");
+
+ final Editor appEditor = appPrefs.edit();
+
+ // The migration always moves prefs to the default profile, not
+ // the current one. We might have to revisit this if we ever support
+ // multiple profiles.
+ final String defaultProfileName;
+ try {
+ defaultProfileName = GeckoProfile.getDefaultProfileName(context);
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to get default profile name for migration");
+ }
+
+ final Editor profileEditor = forProfileName(context, defaultProfileName, disableMigrations).edit();
+ final Editor crashEditor = forCrashReporter(context, disableMigrations).edit();
+
+ List<String> profileKeys;
+ Editor pmEditor = null;
+
+ for (int v = currentVersion + 1; v <= PREFS_VERSION; v++) {
+ Log.d(LOGTAG, "Migrating to version = " + v);
+
+ switch (v) {
+ case 1:
+ profileKeys = Arrays.asList(PROFILE_MIGRATIONS_0_TO_1);
+ pmEditor = migrateFromPreferenceManager(context, appEditor, profileEditor, profileKeys);
+ break;
+ case 2:
+ profileKeys = Arrays.asList(PROFILE_MIGRATIONS_1_TO_2);
+ migrateCrashReporterSettings(appPrefs, appEditor, crashEditor, profileKeys);
+ break;
+ }
+ }
+
+ // Update prefs version accordingly.
+ appEditor.putInt(PREFS_VERSION_KEY, PREFS_VERSION);
+
+ appEditor.apply();
+ profileEditor.apply();
+ crashEditor.apply();
+ if (pmEditor != null) {
+ pmEditor.apply();
+ }
+
+ Log.d(LOGTAG, "All keys have been migrated");
+ }
+
+ /**
+ * Moves all preferences stored in PreferenceManager's default prefs
+ * to either app or profile scopes. The profile-scoped keys are defined
+ * in given profileKeys list, all other keys are moved to the app scope.
+ */
+ public static Editor migrateFromPreferenceManager(Context context, Editor appEditor,
+ Editor profileEditor, List<String> profileKeys) {
+ Log.d(LOGTAG, "Migrating from PreferenceManager");
+
+ final SharedPreferences pmPrefs =
+ PreferenceManager.getDefaultSharedPreferences(context);
+
+ for (Map.Entry<String, ?> entry : pmPrefs.getAll().entrySet()) {
+ final String key = entry.getKey();
+
+ final Editor to;
+ if (profileKeys.contains(key)) {
+ to = profileEditor;
+ } else {
+ to = appEditor;
+ }
+
+ putEntry(to, key, entry.getValue());
+ }
+
+ // Clear PreferenceManager's prefs once we're done
+ // and return the Editor to be committed.
+ return pmPrefs.edit().clear();
+ }
+
+ /**
+ * Moves the crash reporter's preferences from the app-wide prefs
+ * into its own shared prefs to avoid cross-process pref accesses.
+ */
+ public static void migrateCrashReporterSettings(SharedPreferences appPrefs, Editor appEditor,
+ Editor crashEditor, List<String> profileKeys) {
+ Log.d(LOGTAG, "Migrating crash reporter settings");
+
+ for (Map.Entry<String, ?> entry : appPrefs.getAll().entrySet()) {
+ final String key = entry.getKey();
+
+ if (profileKeys.contains(key)) {
+ putEntry(crashEditor, key, entry.getValue());
+ appEditor.remove(key);
+ }
+ }
+ }
+
+ private static void putEntry(Editor to, String key, Object value) {
+ Log.d(LOGTAG, "Migrating key = " + key + " with value = " + value);
+
+ if (value instanceof String) {
+ to.putString(key, (String) value);
+ } else if (value instanceof Boolean) {
+ to.putBoolean(key, (Boolean) value);
+ } else if (value instanceof Long) {
+ to.putLong(key, (Long) value);
+ } else if (value instanceof Float) {
+ to.putFloat(key, (Float) value);
+ } else if (value instanceof Integer) {
+ to.putInt(key, (Integer) value);
+ } else {
+ throw new IllegalStateException("Unrecognized value type for key: " + key);
+ }
+ }
+} \ No newline at end of file
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java
new file mode 100644
index 000000000..b57222a31
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java
@@ -0,0 +1,677 @@
+/* -*- 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;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.GeckoLoader;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Locale;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+public class GeckoThread extends Thread {
+ private static final String LOGTAG = "GeckoThread";
+
+ public enum State {
+ // After being loaded by class loader.
+ @WrapForJNI INITIAL(0),
+ // After launching Gecko thread
+ @WrapForJNI LAUNCHED(1),
+ // After loading the mozglue library.
+ @WrapForJNI MOZGLUE_READY(2),
+ // After loading the libxul library.
+ @WrapForJNI LIBS_READY(3),
+ // After initializing nsAppShell and JNI calls.
+ @WrapForJNI JNI_READY(4),
+ // After initializing profile and prefs.
+ @WrapForJNI PROFILE_READY(5),
+ // After initializing frontend JS
+ @WrapForJNI RUNNING(6),
+ // After leaving Gecko event loop
+ @WrapForJNI EXITING(3),
+ // After exiting GeckoThread (corresponding to "Gecko:Exited" event)
+ @WrapForJNI EXITED(0);
+
+ /* The rank is an arbitrary value reflecting the amount of components or features
+ * that are available for use. During startup and up to the RUNNING state, the
+ * rank value increases because more components are initialized and available for
+ * use. During shutdown and up to the EXITED state, the rank value decreases as
+ * components are shut down and become unavailable. EXITING has the same rank as
+ * LIBS_READY because both states have a similar amount of components available.
+ */
+ private final int rank;
+
+ private State(int rank) {
+ this.rank = rank;
+ }
+
+ public boolean is(final State other) {
+ return this == other;
+ }
+
+ public boolean isAtLeast(final State other) {
+ return this.rank >= other.rank;
+ }
+
+ public boolean isAtMost(final State other) {
+ return this.rank <= other.rank;
+ }
+
+ // Inclusive
+ public boolean isBetween(final State min, final State max) {
+ return this.rank >= min.rank && this.rank <= max.rank;
+ }
+ }
+
+ public static final State MIN_STATE = State.INITIAL;
+ public static final State MAX_STATE = State.EXITED;
+
+ private static volatile State sState = State.INITIAL;
+
+ private static class QueuedCall {
+ public Method method;
+ public Object target;
+ public Object[] args;
+ public State state;
+
+ public QueuedCall(final Method method, final Object target,
+ final Object[] args, final State state) {
+ this.method = method;
+ this.target = target;
+ this.args = args;
+ this.state = state;
+ }
+ }
+
+ private static final int QUEUED_CALLS_COUNT = 16;
+ private static final ArrayList<QueuedCall> QUEUED_CALLS = new ArrayList<>(QUEUED_CALLS_COUNT);
+
+ private static final Runnable UI_THREAD_CALLBACK = new Runnable() {
+ @Override
+ public void run() {
+ ThreadUtils.assertOnUiThread();
+ long nextDelay = runUiThreadCallback();
+ if (nextDelay >= 0) {
+ ThreadUtils.getUiHandler().postDelayed(this, nextDelay);
+ }
+ }
+ };
+
+ private static GeckoThread sGeckoThread;
+
+ @WrapForJNI
+ private static final ClassLoader clsLoader = GeckoThread.class.getClassLoader();
+ @WrapForJNI
+ private static MessageQueue msgQueue;
+
+ private GeckoProfile mProfile;
+
+ private final String mArgs;
+ private final String mAction;
+ private final boolean mDebugging;
+
+ GeckoThread(GeckoProfile profile, String args, String action, boolean debugging) {
+ mProfile = profile;
+ mArgs = args;
+ mAction = action;
+ mDebugging = debugging;
+
+ setName("Gecko");
+ }
+
+ public static boolean init(GeckoProfile profile, String args, String action, boolean debugging) {
+ ThreadUtils.assertOnUiThread();
+ if (isState(State.INITIAL) && sGeckoThread == null) {
+ sGeckoThread = new GeckoThread(profile, args, action, debugging);
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean canUseProfile(final Context context, final GeckoProfile profile,
+ final String profileName, final File profileDir) {
+ if (profileDir != null && !profileDir.isDirectory()) {
+ return false;
+ }
+
+ if (profile == null) {
+ // We haven't initialized; any profile is okay as long as we follow the guest mode setting.
+ return GeckoProfile.shouldUseGuestMode(context) ==
+ GeckoProfile.isGuestProfile(context, profileName, profileDir);
+ }
+
+ // We already initialized and have a profile; see if it matches ours.
+ try {
+ return profileDir == null ? profileName.equals(profile.getName()) :
+ profile.getDir().getCanonicalPath().equals(profileDir.getCanonicalPath());
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Cannot compare profile " + profileName);
+ return false;
+ }
+ }
+
+ public static boolean canUseProfile(final String profileName, final File profileDir) {
+ if (profileName == null) {
+ throw new IllegalArgumentException("Null profile name");
+ }
+ return canUseProfile(GeckoAppShell.getApplicationContext(), getActiveProfile(),
+ profileName, profileDir);
+ }
+
+ public static boolean initWithProfile(final String profileName, final File profileDir) {
+ if (profileName == null) {
+ throw new IllegalArgumentException("Null profile name");
+ }
+
+ final Context context = GeckoAppShell.getApplicationContext();
+ final GeckoProfile profile = getActiveProfile();
+
+ if (!canUseProfile(context, profile, profileName, profileDir)) {
+ // Profile is incompatible with current profile.
+ return false;
+ }
+
+ if (profile != null) {
+ // We already have a compatible profile.
+ return true;
+ }
+
+ // We haven't initialized yet; okay to initialize now.
+ return init(GeckoProfile.get(context, profileName, profileDir),
+ /* args */ null, /* action */ null, /* debugging */ false);
+ }
+
+ public static boolean launch() {
+ ThreadUtils.assertOnUiThread();
+ if (checkAndSetState(State.INITIAL, State.LAUNCHED)) {
+ sGeckoThread.start();
+ return true;
+ }
+ return false;
+ }
+
+ public static boolean isLaunched() {
+ return !isState(State.INITIAL);
+ }
+
+ @RobocopTarget
+ public static boolean isRunning() {
+ return isState(State.RUNNING);
+ }
+
+ // Invoke the given Method and handle checked Exceptions.
+ private static void invokeMethod(final Method method, final Object obj, final Object[] args) {
+ try {
+ method.setAccessible(true);
+ method.invoke(obj, args);
+ } catch (final IllegalAccessException e) {
+ throw new IllegalStateException("Unexpected exception", e);
+ } catch (final InvocationTargetException e) {
+ throw new UnsupportedOperationException("Cannot make call", e.getCause());
+ }
+ }
+
+ // Queue a call to the given method.
+ private static void queueNativeCallLocked(final Class<?> cls, final String methodName,
+ final Object obj, final Object[] args,
+ final State state) {
+ final ArrayList<Class<?>> argTypes = new ArrayList<>(args.length);
+ final ArrayList<Object> argValues = new ArrayList<>(args.length);
+
+ for (int i = 0; i < args.length; i++) {
+ if (args[i] instanceof Class) {
+ argTypes.add((Class<?>) args[i]);
+ argValues.add(args[++i]);
+ continue;
+ }
+ Class<?> argType = args[i].getClass();
+ if (argType == Boolean.class) argType = Boolean.TYPE;
+ else if (argType == Byte.class) argType = Byte.TYPE;
+ else if (argType == Character.class) argType = Character.TYPE;
+ else if (argType == Double.class) argType = Double.TYPE;
+ else if (argType == Float.class) argType = Float.TYPE;
+ else if (argType == Integer.class) argType = Integer.TYPE;
+ else if (argType == Long.class) argType = Long.TYPE;
+ else if (argType == Short.class) argType = Short.TYPE;
+ argTypes.add(argType);
+ argValues.add(args[i]);
+ }
+ final Method method;
+ try {
+ method = cls.getDeclaredMethod(
+ methodName, argTypes.toArray(new Class<?>[argTypes.size()]));
+ } catch (final NoSuchMethodException e) {
+ throw new IllegalArgumentException("Cannot find method", e);
+ }
+
+ if (!Modifier.isNative(method.getModifiers())) {
+ // As a precaution, we disallow queuing non-native methods. Queuing non-native
+ // methods is dangerous because the method could end up being called on either
+ // the original thread or the Gecko thread depending on timing. Native methods
+ // usually handle this by posting an event to the Gecko thread automatically,
+ // but there is no automatic mechanism for non-native methods.
+ throw new UnsupportedOperationException("Not allowed to queue non-native methods");
+ }
+
+ if (isStateAtLeast(state)) {
+ invokeMethod(method, obj, argValues.toArray());
+ return;
+ }
+
+ QUEUED_CALLS.add(new QueuedCall(
+ method, obj, argValues.toArray(), state));
+ }
+
+ /**
+ * Queue a call to the given static method until Gecko is in the given state.
+ *
+ * @param state The Gecko state in which the native call could be executed.
+ * Default is State.RUNNING, which means this queued call will
+ * run when Gecko is at or after RUNNING state.
+ * @param cls Class that declares the static method.
+ * @param methodName Name of the static method.
+ * @param args Args to call the static method with; to specify a parameter type,
+ * pass in a Class instance first, followed by the value.
+ */
+ public static void queueNativeCallUntil(final State state, final Class<?> cls,
+ final String methodName, final Object... args) {
+ synchronized (QUEUED_CALLS) {
+ queueNativeCallLocked(cls, methodName, null, args, state);
+ }
+ }
+
+ /**
+ * Queue a call to the given static method until Gecko is in the RUNNING state.
+ */
+ public static void queueNativeCall(final Class<?> cls, final String methodName,
+ final Object... args) {
+ synchronized (QUEUED_CALLS) {
+ queueNativeCallLocked(cls, methodName, null, args, State.RUNNING);
+ }
+ }
+
+ /**
+ * Queue a call to the given instance method until Gecko is in the given state.
+ *
+ * @param state The Gecko state in which the native call could be executed.
+ * @param obj Object that declares the instance method.
+ * @param methodName Name of the instance method.
+ * @param args Args to call the instance method with; to specify a parameter type,
+ * pass in a Class instance first, followed by the value.
+ */
+ public static void queueNativeCallUntil(final State state, final Object obj,
+ final String methodName, final Object... args) {
+ synchronized (QUEUED_CALLS) {
+ queueNativeCallLocked(obj.getClass(), methodName, obj, args, state);
+ }
+ }
+
+ /**
+ * Queue a call to the given instance method until Gecko is in the RUNNING state.
+ */
+ public static void queueNativeCall(final Object obj, final String methodName,
+ final Object... args) {
+ synchronized (QUEUED_CALLS) {
+ queueNativeCallLocked(obj.getClass(), methodName, obj, args, State.RUNNING);
+ }
+ }
+
+ // Run all queued methods
+ private static void flushQueuedNativeCallsLocked(final State state) {
+ int lastSkipped = -1;
+ for (int i = 0; i < QUEUED_CALLS.size(); i++) {
+ final QueuedCall call = QUEUED_CALLS.get(i);
+ if (call == null) {
+ // We already handled the call.
+ continue;
+ }
+ if (!state.isAtLeast(call.state)) {
+ // The call is not ready yet; skip it.
+ lastSkipped = i;
+ continue;
+ }
+ // Mark as handled.
+ QUEUED_CALLS.set(i, null);
+
+ invokeMethod(call.method, call.target, call.args);
+ }
+ if (lastSkipped < 0) {
+ // We're done here; release the memory
+ QUEUED_CALLS.clear();
+ QUEUED_CALLS.trimToSize();
+ } else if (lastSkipped < QUEUED_CALLS.size() - 1) {
+ // We skipped some; free up null entries at the end,
+ // but keep all the previous entries for later.
+ QUEUED_CALLS.subList(lastSkipped + 1, QUEUED_CALLS.size()).clear();
+ }
+ }
+
+ private static String initGeckoEnvironment() {
+ final Context context = GeckoAppShell.getApplicationContext();
+ GeckoLoader.loadMozGlue(context);
+ setState(State.MOZGLUE_READY);
+
+ final Locale locale = Locale.getDefault();
+ final Resources res = context.getResources();
+ if (locale.toString().equalsIgnoreCase("zh_hk")) {
+ final Locale mappedLocale = Locale.TRADITIONAL_CHINESE;
+ Locale.setDefault(mappedLocale);
+ Configuration config = res.getConfiguration();
+ config.locale = mappedLocale;
+ res.updateConfiguration(config, null);
+ }
+
+ String[] pluginDirs = null;
+ try {
+ pluginDirs = GeckoAppShell.getPluginDirectories();
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Caught exception getting plugin dirs.", e);
+ }
+
+ final String resourcePath = context.getPackageResourcePath();
+ GeckoLoader.setupGeckoEnvironment(context, pluginDirs, context.getFilesDir().getPath());
+
+ GeckoLoader.loadSQLiteLibs(context, resourcePath);
+ GeckoLoader.loadNSSLibs(context, resourcePath);
+ GeckoLoader.loadGeckoLibs(context, resourcePath);
+ setState(State.LIBS_READY);
+
+ return resourcePath;
+ }
+
+ private String addCustomProfileArg(String args) {
+ String profileArg = "";
+
+ // Make sure a profile exists.
+ final GeckoProfile profile = getProfile();
+ profile.getDir(); // call the lazy initializer
+
+ // If args don't include the profile, make sure it's included.
+ if (args == null || !args.matches(".*\\B-(P|profile)\\s+\\S+.*")) {
+ if (profile.isCustomProfile()) {
+ profileArg = " -profile " + profile.getDir().getAbsolutePath();
+ } else {
+ profileArg = " -P " + profile.getName();
+ }
+ }
+
+ return (args != null ? args : "") + profileArg;
+ }
+
+ private String getGeckoArgs(final String apkPath) {
+ // argv[0] is the program name, which for us is the package name.
+ final Context context = GeckoAppShell.getApplicationContext();
+ final StringBuilder args = new StringBuilder(context.getPackageName());
+ args.append(" -greomni ").append(apkPath);
+
+ final String userArgs = addCustomProfileArg(mArgs);
+ if (userArgs != null) {
+ args.append(' ').append(userArgs);
+ }
+
+ // In un-official builds, we want to load Javascript resources fresh
+ // with each build. In official builds, the startup cache is purged by
+ // the buildid mechanism, but most un-official builds don't bump the
+ // buildid, so we purge here instead.
+ if (!AppConstants.MOZILLA_OFFICIAL) {
+ Log.w(LOGTAG, "STARTUP PERFORMANCE WARNING: un-official build: purging the " +
+ "startup (JavaScript) caches.");
+ args.append(" -purgecaches");
+ }
+
+ return args.toString();
+ }
+
+ public static GeckoProfile getActiveProfile() {
+ if (sGeckoThread == null) {
+ return null;
+ }
+ final GeckoProfile profile = sGeckoThread.mProfile;
+ if (profile != null) {
+ return profile;
+ }
+ return sGeckoThread.getProfile();
+ }
+
+ public synchronized GeckoProfile getProfile() {
+ if (mProfile == null) {
+ final Context context = GeckoAppShell.getApplicationContext();
+ mProfile = GeckoProfile.initFromArgs(context, mArgs);
+ }
+ return mProfile;
+ }
+
+ @Override
+ public void run() {
+ Log.i(LOGTAG, "preparing to run Gecko");
+
+ Looper.prepare();
+ GeckoThread.msgQueue = Looper.myQueue();
+ ThreadUtils.sGeckoThread = this;
+ ThreadUtils.sGeckoHandler = new Handler();
+
+ // Preparation for pumpMessageLoop()
+ final MessageQueue.IdleHandler idleHandler = new MessageQueue.IdleHandler() {
+ @Override public boolean queueIdle() {
+ final Handler geckoHandler = ThreadUtils.sGeckoHandler;
+ Message idleMsg = Message.obtain(geckoHandler);
+ // Use |Message.obj == GeckoHandler| to identify our "queue is empty" message
+ idleMsg.obj = geckoHandler;
+ geckoHandler.sendMessageAtFrontOfQueue(idleMsg);
+ // Keep this IdleHandler
+ return true;
+ }
+ };
+ Looper.myQueue().addIdleHandler(idleHandler);
+
+ if (mDebugging) {
+ try {
+ Thread.sleep(5 * 1000 /* 5 seconds */);
+ } catch (final InterruptedException e) {
+ }
+ }
+
+ final String args = getGeckoArgs(initGeckoEnvironment());
+
+ // This can only happen after the call to initGeckoEnvironment
+ // above, because otherwise the JNI code hasn't been loaded yet.
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override public void run() {
+ registerUiThread();
+ }
+ });
+
+ Log.w(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - runGecko");
+
+ if (!AppConstants.MOZILLA_OFFICIAL) {
+ Log.i(LOGTAG, "RunGecko - args = " + args);
+ }
+
+ // And go.
+ GeckoLoader.nativeRun(args);
+
+ // And... we're done.
+ setState(State.EXITED);
+
+ try {
+ final JSONObject msg = new JSONObject();
+ msg.put("type", "Gecko:Exited");
+ GeckoAppShell.getGeckoInterface().getAppEventDispatcher().dispatchEvent(msg, null);
+ EventDispatcher.getInstance().dispatchEvent(msg, null);
+ } catch (final JSONException e) {
+ Log.e(LOGTAG, "unable to dispatch event", e);
+ }
+
+ // Remove pumpMessageLoop() idle handler
+ Looper.myQueue().removeIdleHandler(idleHandler);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean pumpMessageLoop(final Message msg) {
+ final Handler geckoHandler = ThreadUtils.sGeckoHandler;
+
+ if (msg.obj == geckoHandler && msg.getTarget() == geckoHandler) {
+ // Our "queue is empty" message; see runGecko()
+ return false;
+ }
+
+ if (msg.getTarget() == null) {
+ Looper.myLooper().quit();
+ } else {
+ msg.getTarget().dispatchMessage(msg);
+ }
+
+ return true;
+ }
+
+ /**
+ * Check that the current Gecko thread state matches the given state.
+ *
+ * @param state State to check
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isState(final State state) {
+ return sState.is(state);
+ }
+
+ /**
+ * Check that the current Gecko thread state is at the given state or further along,
+ * according to the order defined in the State enum.
+ *
+ * @param state State to check
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isStateAtLeast(final State state) {
+ return sState.isAtLeast(state);
+ }
+
+ /**
+ * Check that the current Gecko thread state is at the given state or prior,
+ * according to the order defined in the State enum.
+ *
+ * @param state State to check
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isStateAtMost(final State state) {
+ return sState.isAtMost(state);
+ }
+
+ /**
+ * Check that the current Gecko thread state falls into an inclusive range of states,
+ * according to the order defined in the State enum.
+ *
+ * @param minState Lower range of allowable states
+ * @param maxState Upper range of allowable states
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isStateBetween(final State minState, final State maxState) {
+ return sState.isBetween(minState, maxState);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void setState(final State newState) {
+ ThreadUtils.assertOnGeckoThread();
+ synchronized (QUEUED_CALLS) {
+ flushQueuedNativeCallsLocked(newState);
+ sState = newState;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean checkAndSetState(final State currentState, final State newState) {
+ synchronized (QUEUED_CALLS) {
+ if (sState == currentState) {
+ flushQueuedNativeCallsLocked(newState);
+ sState = newState;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @WrapForJNI(stubName = "SpeculativeConnect")
+ private static native void speculativeConnectNative(String uri);
+
+ public static void speculativeConnect(final String uri) {
+ // This is almost always called before Gecko loads, so we don't
+ // bother checking here if Gecko is actually loaded or not.
+ // Speculative connection depends on proxy settings,
+ // so the earliest it can happen is after profile is ready.
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class,
+ "speculativeConnectNative", uri);
+ }
+
+ @WrapForJNI @RobocopTarget
+ public static native void waitOnGecko();
+
+ @WrapForJNI(stubName = "OnPause", dispatchTo = "gecko")
+ private static native void nativeOnPause();
+
+ public static void onPause() {
+ if (isStateAtLeast(State.PROFILE_READY)) {
+ nativeOnPause();
+ } else {
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class,
+ "nativeOnPause");
+ }
+ }
+
+ @WrapForJNI(stubName = "OnResume", dispatchTo = "gecko")
+ private static native void nativeOnResume();
+
+ public static void onResume() {
+ if (isStateAtLeast(State.PROFILE_READY)) {
+ nativeOnResume();
+ } else {
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class,
+ "nativeOnResume");
+ }
+ }
+
+ @WrapForJNI(stubName = "CreateServices", dispatchTo = "gecko")
+ private static native void nativeCreateServices(String category, String data);
+
+ public static void createServices(final String category, final String data) {
+ if (isStateAtLeast(State.PROFILE_READY)) {
+ nativeCreateServices(category, data);
+ } else {
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class, "nativeCreateServices",
+ String.class, category, String.class, data);
+ }
+ }
+
+ // Implemented in mozglue/android/APKOpen.cpp.
+ /* package */ static native void registerUiThread();
+
+ @WrapForJNI(calledFrom = "ui")
+ /* package */ static native long runUiThreadCallback();
+
+ @WrapForJNI
+ private static void requestUiThreadCallback(long delay) {
+ ThreadUtils.getUiHandler().postDelayed(UI_THREAD_CALLBACK, delay);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoView.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoView.java
new file mode 100644
index 000000000..93d738361
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoView.java
@@ -0,0 +1,736 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * vim: ts=4 sw=4 expandtab:
+ * 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 java.util.Set;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.annotation.ReflectionTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+public class GeckoView extends LayerView
+ implements ContextGetter, GeckoEventListener, NativeEventListener {
+
+ private static final String DEFAULT_SHARED_PREFERENCES_FILE = "GeckoView";
+ private static final String LOGTAG = "GeckoView";
+
+ private ChromeDelegate mChromeDelegate;
+ private ContentDelegate mContentDelegate;
+
+ private InputConnectionListener mInputConnectionListener;
+
+ protected boolean onAttachedToWindowCalled;
+ protected String chromeURI = getGeckoInterface().getDefaultChromeURI();
+ protected int screenId = 0; // default to the primary screen
+
+ @Override
+ public void handleMessage(final String event, final JSONObject message) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (event.equals("Gecko:Ready")) {
+ handleReady(message);
+ } else if (event.equals("Content:StateChange")) {
+ handleStateChange(message);
+ } else if (event.equals("Content:LoadError")) {
+ handleLoadError(message);
+ } else if (event.equals("Content:PageShow")) {
+ handlePageShow(message);
+ } else if (event.equals("DOMTitleChanged")) {
+ handleTitleChanged(message);
+ } else if (event.equals("Link:Favicon")) {
+ handleLinkFavicon(message);
+ } else if (event.equals("Prompt:Show") || event.equals("Prompt:ShowTop")) {
+ handlePrompt(message);
+ } else if (event.equals("Accessibility:Event")) {
+ int mode = getImportantForAccessibility();
+ if (mode == View.IMPORTANT_FOR_ACCESSIBILITY_YES ||
+ mode == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ GeckoAccessibility.sendAccessibilityEvent(message);
+ }
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "handleMessage threw for " + event, e);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) {
+ try {
+ if ("Accessibility:Ready".equals(event)) {
+ GeckoAccessibility.updateAccessibilitySettings(getContext());
+ } else if ("GeckoView:Message".equals(event)) {
+ // We need to pull out the bundle while on the Gecko thread.
+ NativeJSObject json = message.optObject("data", null);
+ if (json == null) {
+ // Must have payload to call the message handler.
+ return;
+ }
+ final Bundle data = json.toBundle();
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ handleScriptMessage(data, callback);
+ }
+ });
+ }
+ } catch (Exception e) {
+ Log.w(LOGTAG, "handleMessage threw for " + event, e);
+ }
+ }
+
+ @WrapForJNI(dispatchTo = "proxy")
+ protected static final class Window extends JNIObject {
+ @WrapForJNI(skip = true)
+ /* package */ Window() {}
+
+ static native void open(Window instance, GeckoView view, Object compositor,
+ String chromeURI, int screenId);
+
+ @Override protected native void disposeNative();
+ native void close();
+ native void reattach(GeckoView view, Object compositor);
+ native void loadUri(String uri, int flags);
+ }
+
+ // Object to hold onto our nsWindow connection when GeckoView gets destroyed.
+ private static class StateBinder extends Binder implements Parcelable {
+ public final Parcelable superState;
+ public final Window window;
+
+ public StateBinder(Parcelable superState, Window window) {
+ this.superState = superState;
+ this.window = window;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ // Always write out the super-state, so that even if we lose this binder, we
+ // will still have something to pass into super.onRestoreInstanceState.
+ out.writeParcelable(superState, flags);
+ out.writeStrongBinder(this);
+ }
+
+ @ReflectionTarget
+ public static final Parcelable.Creator<StateBinder> CREATOR
+ = new Parcelable.Creator<StateBinder>() {
+ @Override
+ public StateBinder createFromParcel(Parcel in) {
+ final Parcelable superState = in.readParcelable(null);
+ final IBinder binder = in.readStrongBinder();
+ if (binder instanceof StateBinder) {
+ return (StateBinder) binder;
+ }
+ // Not the original object we saved; return null state.
+ return new StateBinder(superState, null);
+ }
+
+ @Override
+ public StateBinder[] newArray(int size) {
+ return new StateBinder[size];
+ }
+ };
+ }
+
+ protected Window window;
+ private boolean stateSaved;
+
+ public GeckoView(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public GeckoView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ private void init(Context context) {
+ if (GeckoAppShell.getApplicationContext() == null) {
+ GeckoAppShell.setApplicationContext(context.getApplicationContext());
+ }
+
+ // Set the GeckoInterface if the context is an activity and the GeckoInterface
+ // has not already been set
+ if (context instanceof Activity && getGeckoInterface() == null) {
+ setGeckoInterface(new BaseGeckoInterface(context));
+ GeckoAppShell.setContextGetter(this);
+ }
+
+ // Perform common initialization for Fennec/GeckoView.
+ GeckoAppShell.setLayerView(this);
+
+ initializeView(EventDispatcher.getInstance());
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState()
+ {
+ final Parcelable superState = super.onSaveInstanceState();
+ stateSaved = true;
+ return new StateBinder(superState, this.window);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(final Parcelable state)
+ {
+ final StateBinder stateBinder = (StateBinder) state;
+
+ if (stateBinder.window != null) {
+ this.window = stateBinder.window;
+ }
+ stateSaved = false;
+
+ if (onAttachedToWindowCalled) {
+ reattachWindow();
+ }
+
+ // We have to always call super.onRestoreInstanceState because View keeps
+ // track of these calls and throws an exception when we don't call it.
+ super.onRestoreInstanceState(stateBinder.superState);
+ }
+
+ protected void openWindow() {
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ Window.open(window, this, getCompositor(),
+ chromeURI, screenId);
+ } else {
+ GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY, Window.class,
+ "open", window, GeckoView.class, this, Object.class, getCompositor(),
+ String.class, chromeURI, screenId);
+ }
+ }
+
+ protected void reattachWindow() {
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ window.reattach(this, getCompositor());
+ } else {
+ GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
+ window, "reattach", GeckoView.class, this, Object.class, getCompositor());
+ }
+ }
+
+ @Override
+ public void onAttachedToWindow()
+ {
+ final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
+
+ if (window == null) {
+ // Open a new nsWindow if we didn't have one from before.
+ window = new Window();
+ openWindow();
+ } else {
+ reattachWindow();
+ }
+
+ super.onAttachedToWindow();
+
+ onAttachedToWindowCalled = true;
+ }
+
+ @Override
+ public void onDetachedFromWindow()
+ {
+ super.onDetachedFromWindow();
+ super.destroy();
+
+ if (stateSaved) {
+ // If we saved state earlier, we don't want to close the nsWindow.
+ return;
+ }
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ window.close();
+ window.disposeNative();
+ } else {
+ GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
+ window, "close");
+ GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
+ window, "disposeNative");
+ }
+
+ onAttachedToWindowCalled = false;
+ }
+
+ @WrapForJNI public static final int LOAD_DEFAULT = 0;
+ @WrapForJNI public static final int LOAD_NEW_TAB = 1;
+ @WrapForJNI public static final int LOAD_SWITCH_TAB = 2;
+
+ public void loadUri(String uri, int flags) {
+ if (window == null) {
+ throw new IllegalStateException("Not attached to window");
+ }
+
+ if (GeckoThread.isRunning()) {
+ window.loadUri(uri, flags);
+ } else {
+ GeckoThread.queueNativeCall(window, "loadUri", String.class, uri, flags);
+ }
+ }
+
+ /* package */ void setInputConnectionListener(final InputConnectionListener icl) {
+ mInputConnectionListener = icl;
+ }
+
+ @Override
+ public Handler getHandler() {
+ if (mInputConnectionListener != null) {
+ return mInputConnectionListener.getHandler(super.getHandler());
+ }
+ return super.getHandler();
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ if (mInputConnectionListener != null) {
+ return mInputConnectionListener.onCreateInputConnection(outAttrs);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (super.onKeyPreIme(keyCode, event)) {
+ return true;
+ }
+ return mInputConnectionListener != null &&
+ mInputConnectionListener.onKeyPreIme(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (super.onKeyUp(keyCode, event)) {
+ return true;
+ }
+ return mInputConnectionListener != null &&
+ mInputConnectionListener.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (super.onKeyDown(keyCode, event)) {
+ return true;
+ }
+ return mInputConnectionListener != null &&
+ mInputConnectionListener.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ if (super.onKeyLongPress(keyCode, event)) {
+ return true;
+ }
+ return mInputConnectionListener != null &&
+ mInputConnectionListener.onKeyLongPress(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ if (super.onKeyMultiple(keyCode, repeatCount, event)) {
+ return true;
+ }
+ return mInputConnectionListener != null &&
+ mInputConnectionListener.onKeyMultiple(keyCode, repeatCount, event);
+ }
+
+ /* package */ boolean isIMEEnabled() {
+ return mInputConnectionListener != null &&
+ mInputConnectionListener.isIMEEnabled();
+ }
+
+ public void importScript(final String url) {
+ if (url.startsWith("resource://android/assets/")) {
+ GeckoAppShell.notifyObservers("GeckoView:ImportScript", url);
+ return;
+ }
+
+ throw new IllegalArgumentException("Must import script from 'resources://android/assets/' location.");
+ }
+
+ private void handleReady(final JSONObject message) {
+ if (mChromeDelegate != null) {
+ mChromeDelegate.onReady(this);
+ }
+ }
+
+ private void handleStateChange(final JSONObject message) throws JSONException {
+ int state = message.getInt("state");
+ if ((state & GeckoAppShell.WPL_STATE_IS_NETWORK) != 0) {
+ if ((state & GeckoAppShell.WPL_STATE_START) != 0) {
+ if (mContentDelegate != null) {
+ int id = message.getInt("tabID");
+ mContentDelegate.onPageStart(this, new Browser(id), message.getString("uri"));
+ }
+ } else if ((state & GeckoAppShell.WPL_STATE_STOP) != 0) {
+ if (mContentDelegate != null) {
+ int id = message.getInt("tabID");
+ mContentDelegate.onPageStop(this, new Browser(id), message.getBoolean("success"));
+ }
+ }
+ }
+ }
+
+ private void handleLoadError(final JSONObject message) throws JSONException {
+ if (mContentDelegate != null) {
+ int id = message.getInt("tabID");
+ mContentDelegate.onPageStop(GeckoView.this, new Browser(id), false);
+ }
+ }
+
+ private void handlePageShow(final JSONObject message) throws JSONException {
+ if (mContentDelegate != null) {
+ int id = message.getInt("tabID");
+ mContentDelegate.onPageShow(GeckoView.this, new Browser(id));
+ }
+ }
+
+ private void handleTitleChanged(final JSONObject message) throws JSONException {
+ if (mContentDelegate != null) {
+ int id = message.getInt("tabID");
+ mContentDelegate.onReceivedTitle(GeckoView.this, new Browser(id), message.getString("title"));
+ }
+ }
+
+ private void handleLinkFavicon(final JSONObject message) throws JSONException {
+ if (mContentDelegate != null) {
+ int id = message.getInt("tabID");
+ mContentDelegate.onReceivedFavicon(GeckoView.this, new Browser(id), message.getString("href"), message.getInt("size"));
+ }
+ }
+
+ private void handlePrompt(final JSONObject message) throws JSONException {
+ if (mChromeDelegate != null) {
+ String hint = message.optString("hint");
+ if ("alert".equals(hint)) {
+ String text = message.optString("text");
+ mChromeDelegate.onAlert(GeckoView.this, null, text, new PromptResult(message));
+ } else if ("confirm".equals(hint)) {
+ String text = message.optString("text");
+ mChromeDelegate.onConfirm(GeckoView.this, null, text, new PromptResult(message));
+ } else if ("prompt".equals(hint)) {
+ String text = message.optString("text");
+ String defaultValue = message.optString("textbox0");
+ mChromeDelegate.onPrompt(GeckoView.this, null, text, defaultValue, new PromptResult(message));
+ } else if ("remotedebug".equals(hint)) {
+ mChromeDelegate.onDebugRequest(GeckoView.this, new PromptResult(message));
+ }
+ }
+ }
+
+ private void handleScriptMessage(final Bundle data, final EventCallback callback) {
+ if (mChromeDelegate != null) {
+ MessageResult result = null;
+ if (callback != null) {
+ result = new MessageResult(callback);
+ }
+ mChromeDelegate.onScriptMessage(GeckoView.this, data, result);
+ }
+ }
+
+ /**
+ * Set the chrome callback handler.
+ * This will replace the current handler.
+ * @param chrome An implementation of GeckoViewChrome.
+ */
+ public void setChromeDelegate(ChromeDelegate chrome) {
+ mChromeDelegate = chrome;
+ }
+
+ /**
+ * Set the content callback handler.
+ * This will replace the current handler.
+ * @param content An implementation of ContentDelegate.
+ */
+ public void setContentDelegate(ContentDelegate content) {
+ mContentDelegate = content;
+ }
+
+ public static void setGeckoInterface(final BaseGeckoInterface geckoInterface) {
+ GeckoAppShell.setGeckoInterface(geckoInterface);
+ }
+
+ public static GeckoAppShell.GeckoInterface getGeckoInterface() {
+ return GeckoAppShell.getGeckoInterface();
+ }
+
+ protected String getSharedPreferencesFile() {
+ return DEFAULT_SHARED_PREFERENCES_FILE;
+ }
+
+ @Override
+ public SharedPreferences getSharedPreferences() {
+ return getContext().getSharedPreferences(getSharedPreferencesFile(), 0);
+ }
+
+ /**
+ * Wrapper for a browser in the GeckoView container. Associated with a browser
+ * element in the Gecko system.
+ */
+ public class Browser {
+ private final int mId;
+ private Browser(int Id) {
+ mId = Id;
+ }
+
+ /**
+ * Get the ID of the Browser. This is the same ID used by Gecko for it's underlying
+ * browser element.
+ * @return The integer ID of the Browser.
+ */
+ private int getId() {
+ return mId;
+ }
+
+ /**
+ * Load a URL resource into the Browser.
+ * @param url The URL string.
+ */
+ public void loadUrl(String url) {
+ JSONObject args = new JSONObject();
+ try {
+ args.put("url", url);
+ args.put("parentId", -1);
+ args.put("newTab", false);
+ args.put("tabID", mId);
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Error building JSON arguments for loadUrl.", e);
+ }
+ GeckoAppShell.notifyObservers("Tab:Load", args.toString());
+ }
+ }
+
+ /* Provides a means for the client to indicate whether a JavaScript
+ * dialog request should proceed. An instance of this class is passed to
+ * various GeckoViewChrome callback actions.
+ */
+ public class PromptResult {
+ private final int RESULT_OK = 0;
+ private final int RESULT_CANCEL = 1;
+
+ private final JSONObject mMessage;
+
+ public PromptResult(JSONObject message) {
+ mMessage = message;
+ }
+
+ private JSONObject makeResult(int resultCode) {
+ JSONObject result = new JSONObject();
+ try {
+ result.put("button", resultCode);
+ } catch (JSONException ex) { }
+ return result;
+ }
+
+ /**
+ * Handle a confirmation response from the user.
+ */
+ public void confirm() {
+ JSONObject result = makeResult(RESULT_OK);
+ EventDispatcher.sendResponse(mMessage, result);
+ }
+
+ /**
+ * Handle a confirmation response from the user.
+ * @param value String value to return to the browser context.
+ */
+ public void confirmWithValue(String value) {
+ JSONObject result = makeResult(RESULT_OK);
+ try {
+ result.put("textbox0", value);
+ } catch (JSONException ex) { }
+ EventDispatcher.sendResponse(mMessage, result);
+ }
+
+ /**
+ * Handle a cancellation response from the user.
+ */
+ public void cancel() {
+ JSONObject result = makeResult(RESULT_CANCEL);
+ EventDispatcher.sendResponse(mMessage, result);
+ }
+ }
+
+ /* Provides a means for the client to respond to a script message with some data.
+ * An instance of this class is passed to GeckoViewChrome.onScriptMessage.
+ */
+ public class MessageResult {
+ private final EventCallback mCallback;
+
+ public MessageResult(EventCallback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("EventCallback should not be null.");
+ }
+ mCallback = callback;
+ }
+
+ private JSONObject bundleToJSON(Bundle data) {
+ JSONObject result = new JSONObject();
+ if (data == null) {
+ return result;
+ }
+
+ final Set<String> keys = data.keySet();
+ for (String key : keys) {
+ try {
+ result.put(key, data.get(key));
+ } catch (JSONException e) {
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Handle a successful response to a script message.
+ * @param value Bundle value to return to the script context.
+ */
+ public void success(Bundle data) {
+ mCallback.sendSuccess(bundleToJSON(data));
+ }
+
+ /**
+ * Handle a failure response to a script message.
+ */
+ public void failure(Bundle data) {
+ mCallback.sendError(bundleToJSON(data));
+ }
+ }
+
+ public interface ChromeDelegate {
+ /**
+ * Tell the host application that Gecko is ready to handle requests.
+ * @param view The GeckoView that initiated the callback.
+ */
+ public void onReady(GeckoView view);
+
+ /**
+ * Tell the host application to display an alert dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param message The string to display in the dialog.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ public void onAlert(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result);
+
+ /**
+ * Tell the host application to display a confirmation dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param message The string to display in the dialog.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ public void onConfirm(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result);
+
+ /**
+ * Tell the host application to display an input prompt dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param message The string to display in the dialog.
+ * @param defaultValue The string to use as default input.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ public void onPrompt(GeckoView view, GeckoView.Browser browser, String message, String defaultValue, GeckoView.PromptResult result);
+
+ /**
+ * Tell the host application to display a remote debugging request dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ public void onDebugRequest(GeckoView view, GeckoView.PromptResult result);
+
+ /**
+ * Receive a message from an imported script.
+ * @param view The GeckoView that initiated the callback.
+ * @param data Bundle of data sent with the message. Never null.
+ * @param result A MessageResult used to send back a response without blocking. Can be null.
+ * Defaults to do nothing.
+ */
+ public void onScriptMessage(GeckoView view, Bundle data, GeckoView.MessageResult result);
+ }
+
+ public interface ContentDelegate {
+ /**
+ * A Browser has started loading content from the network.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param url The resource being loaded.
+ */
+ public void onPageStart(GeckoView view, GeckoView.Browser browser, String url);
+
+ /**
+ * A Browser has finished loading content from the network.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that was loading the content.
+ * @param success Whether the page loaded successfully or an error occurred.
+ */
+ public void onPageStop(GeckoView view, GeckoView.Browser browser, boolean success);
+
+ /**
+ * A Browser is displaying content. This page could have been loaded via
+ * network or from the session history.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is showing the content.
+ */
+ public void onPageShow(GeckoView view, GeckoView.Browser browser);
+
+ /**
+ * A page title was discovered in the content or updated after the content
+ * loaded.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is showing the content.
+ * @param title The title sent from the content.
+ */
+ public void onReceivedTitle(GeckoView view, GeckoView.Browser browser, String title);
+
+ /**
+ * A link element was discovered in the content or updated after the content
+ * loaded that specifies a favicon.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is showing the content.
+ * @param url The href of the link element specifying the favicon.
+ * @param size The maximum size specified for the favicon, or -1 for any size.
+ */
+ public void onReceivedFavicon(GeckoView view, GeckoView.Browser browser, String url, int size);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewChrome.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewChrome.java
new file mode 100644
index 000000000..403c6dbca
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewChrome.java
@@ -0,0 +1,81 @@
+/* -*- 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.os.Bundle;
+
+public class GeckoViewChrome implements GeckoView.ChromeDelegate {
+ /**
+ * Tell the host application that Gecko is ready to handle requests.
+ * @param view The GeckoView that initiated the callback.
+ */
+ @Override
+ public void onReady(GeckoView view) {}
+
+ /**
+ * Tell the host application to display an alert dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param message The string to display in the dialog.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ @Override
+ public void onAlert(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result) {
+ result.cancel();
+ }
+
+ /**
+ * Tell the host application to display a confirmation dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param message The string to display in the dialog.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ @Override
+ public void onConfirm(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result) {
+ result.cancel();
+ }
+
+ /**
+ * Tell the host application to display an input prompt dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param message The string to display in the dialog.
+ * @param defaultValue The string to use as default input.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ @Override
+ public void onPrompt(GeckoView view, GeckoView.Browser browser, String message, String defaultValue, GeckoView.PromptResult result) {
+ result.cancel();
+ }
+
+ /**
+ * Tell the host application to display a remote debugging request dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ @Override
+ public void onDebugRequest(GeckoView view, GeckoView.PromptResult result) {
+ result.cancel();
+ }
+
+ /**
+ * Receive a message from an imported script.
+ * @param view The GeckoView that initiated the callback.
+ * @param data Bundle of data sent with the message. Never null.
+ * @param result A MessageResult used to send back a response without blocking. Can be null.
+ * Defaults to cancel requests with a failed response.
+ */
+ public void onScriptMessage(GeckoView view, Bundle data, GeckoView.MessageResult result) {
+ if (result != null) {
+ result.failure(null);
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewContent.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewContent.java
new file mode 100644
index 000000000..22d0ede75
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewContent.java
@@ -0,0 +1,56 @@
+/* -*- 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;
+
+public class GeckoViewContent implements GeckoView.ContentDelegate {
+ /**
+ * A Browser has started loading content from the network.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param url The resource being loaded.
+ */
+ @Override
+ public void onPageStart(GeckoView view, GeckoView.Browser browser, String url) {}
+
+ /**
+ * A Browser has finished loading content from the network.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that was loading the content.
+ * @param success Whether the page loaded successfully or an error occurred.
+ */
+ @Override
+ public void onPageStop(GeckoView view, GeckoView.Browser browser, boolean success) {}
+
+ /**
+ * A Browser is displaying content. This page could have been loaded via
+ * network or from the session history.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is showing the content.
+ */
+ @Override
+ public void onPageShow(GeckoView view, GeckoView.Browser browser) {}
+
+ /**
+ * A page title was discovered in the content or updated after the content
+ * loaded.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is showing the content.
+ * @param title The title sent from the content.
+ */
+ @Override
+ public void onReceivedTitle(GeckoView view, GeckoView.Browser browser, String title) {}
+
+ /**
+ * A link element was discovered in the content or updated after the content
+ * loaded that specifies a favicon.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is showing the content.
+ * @param url The href of the link element specifying the favicon.
+ * @param size The maximum size specified for the favicon, or -1 for any size.
+ */
+ @Override
+ public void onReceivedFavicon(GeckoView view, GeckoView.Browser browser, String url, int size) {}
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewFragment.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewFragment.java
new file mode 100644
index 000000000..51320636e
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewFragment.java
@@ -0,0 +1,52 @@
+/* -*- 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.support.v4.app.Fragment;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class GeckoViewFragment extends android.support.v4.app.Fragment {
+ private static final String LOGTAG = "GeckoViewFragment";
+
+ private static Parcelable state = null;
+ private static GeckoViewFragment lastUsed = null;
+ private GeckoView geckoView = null;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ setRetainInstance(true);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ geckoView = new GeckoView(getContext());
+ return geckoView;
+ }
+
+ @Override
+ public void onResume() {
+ if (state != null && lastUsed != this) {
+ // "Restore" the window from the previously used GeckoView to this GeckoView and attach it
+ geckoView.onRestoreInstanceState(state);
+ state = null;
+ }
+ super.onResume();
+ }
+
+ @Override
+ public void onPause() {
+ state = geckoView.onSaveInstanceState();
+ lastUsed = this;
+ super.onPause();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputConnectionListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputConnectionListener.java
new file mode 100644
index 000000000..baddc4ed2
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputConnectionListener.java
@@ -0,0 +1,25 @@
+/* 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.os.Handler;
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+/**
+ * Interface for interacting with GeckoInputConnection from GeckoView.
+ */
+interface InputConnectionListener
+{
+ 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/mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputMethods.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputMethods.java
new file mode 100644
index 000000000..57649b0da
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputMethods.java
@@ -0,0 +1,76 @@
+/* -*- 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 java.util.Collection;
+
+import org.mozilla.gecko.AppConstants.Versions;
+
+import android.content.Context;
+import android.provider.Settings.Secure;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
+
+final public class InputMethods {
+ public static final String METHOD_ANDROID_LATINIME = "com.android.inputmethod.latin/.LatinIME";
+ public static final String METHOD_ATOK = "com.justsystems.atokmobile.service/.AtokInputMethodService";
+ public static final String METHOD_GOOGLE_JAPANESE_INPUT = "com.google.android.inputmethod.japanese/.MozcService";
+ public static final String METHOD_GOOGLE_LATINIME = "com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME";
+ public static final String METHOD_HTC_TOUCH_INPUT = "com.htc.android.htcime/.HTCIMEService";
+ public static final String METHOD_IWNN = "jp.co.omronsoft.iwnnime.ml/.standardcommon.IWnnLanguageSwitcher";
+ public static final String METHOD_OPENWNN_PLUS = "com.owplus.ime.openwnnplus/.OpenWnnJAJP";
+ public static final String METHOD_SAMSUNG = "com.sec.android.inputmethod/.SamsungKeypad";
+ public static final String METHOD_SIMEJI = "com.adamrocker.android.input.simeji/.OpenWnnSimeji";
+ public static final String METHOD_SWIFTKEY = "com.touchtype.swiftkey/com.touchtype.KeyboardService";
+ public static final String METHOD_SWYPE = "com.swype.android.inputmethod/.SwypeInputMethod";
+ public static final String METHOD_SWYPE_BETA = "com.nuance.swype.input/.IME";
+ public static final String METHOD_TOUCHPAL_KEYBOARD = "com.cootek.smartinputv5/com.cootek.smartinput5.TouchPalIME";
+
+ private InputMethods() {}
+
+ public static String getCurrentInputMethod(Context context) {
+ String inputMethod = Secure.getString(context.getContentResolver(), Secure.DEFAULT_INPUT_METHOD);
+ return (inputMethod != null ? inputMethod : "");
+ }
+
+ public static InputMethodInfo getInputMethodInfo(Context context, String inputMethod) {
+ InputMethodManager imm = getInputMethodManager(context);
+ Collection<InputMethodInfo> infos = imm.getEnabledInputMethodList();
+ for (InputMethodInfo info : infos) {
+ if (info.getId().equals(inputMethod)) {
+ return info;
+ }
+ }
+ return null;
+ }
+
+ public static InputMethodManager getInputMethodManager(Context context) {
+ return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ }
+
+ public static boolean needsSoftResetWorkaround(String inputMethod) {
+ // Stock latin IME on Android 4.2 and above
+ return Versions.feature17Plus &&
+ (METHOD_ANDROID_LATINIME.equals(inputMethod) ||
+ METHOD_GOOGLE_LATINIME.equals(inputMethod));
+ }
+
+ public static boolean shouldCommitCharAsKey(String inputMethod) {
+ return METHOD_HTC_TOUCH_INPUT.equals(inputMethod);
+ }
+
+ public static boolean isGestureKeyboard(Context context) {
+ // SwiftKey is a gesture keyboard, but it doesn't seem to need any special-casing
+ // to do AwesomeBar auto-spacing.
+ String inputMethod = getCurrentInputMethod(context);
+ return (Versions.feature17Plus &&
+ (METHOD_ANDROID_LATINIME.equals(inputMethod) ||
+ METHOD_GOOGLE_LATINIME.equals(inputMethod))) ||
+ METHOD_SWYPE.equals(inputMethod) ||
+ METHOD_SWYPE_BETA.equals(inputMethod) ||
+ METHOD_TOUCHPAL_KEYBOARD.equals(inputMethod);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NSSBridge.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NSSBridge.java
new file mode 100644
index 000000000..8d525b0ba
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NSSBridge.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;
+
+import org.mozilla.gecko.mozglue.GeckoLoader;
+
+import android.content.Context;
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+public class NSSBridge {
+ private static final String LOGTAG = "NSSBridge";
+
+ private static native String nativeEncrypt(String aDb, String aValue);
+ private static native String nativeDecrypt(String aDb, String aValue);
+
+ @RobocopTarget
+ static public String encrypt(Context context, String aValue)
+ throws Exception {
+ String resourcePath = context.getPackageResourcePath();
+ GeckoLoader.loadNSSLibs(context, resourcePath);
+
+ String path = GeckoProfile.get(context).getDir().toString();
+ return nativeEncrypt(path, aValue);
+ }
+
+ @RobocopTarget
+ static public String encrypt(Context context, String profilePath, String aValue)
+ throws Exception {
+ String resourcePath = context.getPackageResourcePath();
+ GeckoLoader.loadNSSLibs(context, resourcePath);
+
+ return nativeEncrypt(profilePath, aValue);
+ }
+
+ @RobocopTarget
+ static public String decrypt(Context context, String aValue)
+ throws Exception {
+ String resourcePath = context.getPackageResourcePath();
+ GeckoLoader.loadNSSLibs(context, resourcePath);
+
+ String path = GeckoProfile.get(context).getDir().toString();
+ return nativeDecrypt(path, aValue);
+ }
+
+ @RobocopTarget
+ static public String decrypt(Context context, String profilePath, String aValue)
+ throws Exception {
+ String resourcePath = context.getPackageResourcePath();
+ GeckoLoader.loadNSSLibs(context, resourcePath);
+
+ return nativeDecrypt(profilePath, aValue);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NotificationListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NotificationListener.java
new file mode 100644
index 000000000..85a68768f
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NotificationListener.java
@@ -0,0 +1,17 @@
+/* -*- 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;
+
+public interface NotificationListener
+{
+ void showNotification(String name, String cookie, String title, String text,
+ String host, String imageUrl);
+
+ void showPersistentNotification(String name, String cookie, String title, String text,
+ String host, String imageUrl, String data);
+
+ void closeNotification(String name);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/PrefsHelper.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/PrefsHelper.java
new file mode 100644
index 000000000..b60f6fd88
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/PrefsHelper.java
@@ -0,0 +1,308 @@
+/* -*- 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.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+import android.support.v4.util.SimpleArrayMap;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Helper class to get/set gecko prefs.
+ */
+public final class PrefsHelper {
+ private static final String LOGTAG = "GeckoPrefsHelper";
+
+ // Map pref name to ArrayList for multiple observers or PrefHandler for single observer.
+ private static final SimpleArrayMap<String, Object> OBSERVERS = new SimpleArrayMap<>();
+ private static final HashSet<String> INT_TO_STRING_PREFS = new HashSet<>(8);
+ private static final HashSet<String> INT_TO_BOOL_PREFS = new HashSet<>(2);
+
+ static {
+ INT_TO_STRING_PREFS.add("browser.chrome.titlebarMode");
+ INT_TO_STRING_PREFS.add("network.cookie.cookieBehavior");
+ INT_TO_STRING_PREFS.add("font.size.inflation.minTwips");
+ INT_TO_STRING_PREFS.add("home.sync.updateMode");
+ INT_TO_STRING_PREFS.add("browser.image_blocking");
+ INT_TO_BOOL_PREFS.add("browser.display.use_document_fonts");
+ }
+
+ @WrapForJNI
+ private static final int PREF_INVALID = -1;
+ @WrapForJNI
+ private static final int PREF_FINISH = 0;
+ @WrapForJNI
+ private static final int PREF_BOOL = 1;
+ @WrapForJNI
+ private static final int PREF_INT = 2;
+ @WrapForJNI
+ private static final int PREF_STRING = 3;
+
+ @WrapForJNI(stubName = "GetPrefs", dispatchTo = "gecko")
+ private static native void nativeGetPrefs(String[] prefNames, PrefHandler handler);
+ @WrapForJNI(stubName = "SetPref", dispatchTo = "gecko")
+ private static native void nativeSetPref(String prefName, boolean flush, int type,
+ boolean boolVal, int intVal, String strVal);
+ @WrapForJNI(stubName = "AddObserver", dispatchTo = "gecko")
+ private static native void nativeAddObserver(String[] prefNames, PrefHandler handler,
+ String[] prefsToObserve);
+ @WrapForJNI(stubName = "RemoveObserver", dispatchTo = "gecko")
+ private static native void nativeRemoveObserver(String[] prefToUnobserve);
+
+ @RobocopTarget
+ public static void getPrefs(final String[] prefNames, final PrefHandler callback) {
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ nativeGetPrefs(prefNames, callback);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeGetPrefs",
+ String[].class, prefNames, PrefHandler.class, callback);
+ }
+ }
+
+ public static void getPref(final String prefName, final PrefHandler callback) {
+ getPrefs(new String[] { prefName }, callback);
+ }
+
+ public static void getPrefs(final ArrayList<String> prefNames, final PrefHandler callback) {
+ getPrefs(prefNames.toArray(new String[prefNames.size()]), callback);
+ }
+
+ @RobocopTarget
+ public static void setPref(final String pref, final Object value, final boolean flush) {
+ final int type;
+ boolean boolVal = false;
+ int intVal = 0;
+ String strVal = null;
+
+ if (INT_TO_STRING_PREFS.contains(pref)) {
+ // When sending to Java, we normalized special preferences that use integers
+ // and strings to represent booleans. Here, we convert them back to their
+ // actual types so we can store them.
+ type = PREF_INT;
+ intVal = Integer.parseInt(String.valueOf(value));
+ } else if (INT_TO_BOOL_PREFS.contains(pref)) {
+ type = PREF_INT;
+ intVal = (Boolean) value ? 1 : 0;
+ } else if (value instanceof Boolean) {
+ type = PREF_BOOL;
+ boolVal = (Boolean) value;
+ } else if (value instanceof Integer) {
+ type = PREF_INT;
+ intVal = (Integer) value;
+ } else {
+ type = PREF_STRING;
+ strVal = String.valueOf(value);
+ }
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ nativeSetPref(pref, flush, type, boolVal, intVal, strVal);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeSetPref",
+ String.class, pref, flush, type, boolVal, intVal, String.class, strVal);
+ }
+ }
+
+ public static void setPref(final String pref, final Object value) {
+ setPref(pref, value, /* flush */ false);
+ }
+
+ @RobocopTarget
+ public synchronized static void addObserver(final String[] prefNames,
+ final PrefHandler handler) {
+ List<String> prefsToObserve = null;
+
+ for (String pref : prefNames) {
+ final Object existing = OBSERVERS.get(pref);
+
+ if (existing == null) {
+ // Not observing yet, so add observer.
+ if (prefsToObserve == null) {
+ prefsToObserve = new ArrayList<>(prefNames.length);
+ }
+ prefsToObserve.add(pref);
+ OBSERVERS.put(pref, handler);
+
+ } else if (existing instanceof PrefHandler) {
+ // Already observing one, so turn it into an array.
+ final List<PrefHandler> handlerList = new ArrayList<>(2);
+ handlerList.add((PrefHandler) existing);
+ handlerList.add(handler);
+ OBSERVERS.put(pref, handlerList);
+
+ } else {
+ // Already observing multiple, so add to existing array.
+ @SuppressWarnings("unchecked")
+ final List<PrefHandler> handlerList = (List) existing;
+ handlerList.add(handler);
+ }
+ }
+
+ final String[] namesToObserve = prefsToObserve == null ? null :
+ prefsToObserve.toArray(new String[prefsToObserve.size()]);
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ nativeAddObserver(prefNames, handler, namesToObserve);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeAddObserver",
+ String[].class, prefNames, PrefHandler.class, handler,
+ String[].class, namesToObserve);
+ }
+ }
+
+ @RobocopTarget
+ public synchronized static void removeObserver(final PrefHandler handler) {
+ List<String> prefsToUnobserve = null;
+
+ for (int i = OBSERVERS.size() - 1; i >= 0; i--) {
+ final Object existing = OBSERVERS.valueAt(i);
+ boolean removeObserver = false;
+
+ if (existing == handler) {
+ removeObserver = true;
+
+ } else if (!(existing instanceof PrefHandler)) {
+ // Removing existing handler from list.
+ @SuppressWarnings("unchecked")
+ final List<PrefHandler> handlerList = (List) existing;
+ if (handlerList.remove(handler) && handlerList.isEmpty()) {
+ removeObserver = true;
+ }
+ }
+
+ if (removeObserver) {
+ // Removed last handler, so remove observer.
+ if (prefsToUnobserve == null) {
+ prefsToUnobserve = new ArrayList<>();
+ }
+ prefsToUnobserve.add(OBSERVERS.keyAt(i));
+ OBSERVERS.removeAt(i);
+ }
+ }
+
+ if (prefsToUnobserve == null) {
+ return;
+ }
+
+ final String[] namesToUnobserve =
+ prefsToUnobserve.toArray(new String[prefsToUnobserve.size()]);
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ nativeRemoveObserver(namesToUnobserve);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeRemoveObserver",
+ String[].class, namesToUnobserve);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void callPrefHandler(final PrefHandler handler, int type, final String pref,
+ boolean boolVal, int intVal, String strVal) {
+
+ // Some Gecko preferences use integers or strings to reference state instead of
+ // directly representing the value. Since the Java UI uses the type to determine
+ // which ui elements to show and how to handle them, we need to normalize these
+ // preferences to the correct type.
+ if (INT_TO_STRING_PREFS.contains(pref)) {
+ type = PREF_STRING;
+ strVal = String.valueOf(intVal);
+ } else if (INT_TO_BOOL_PREFS.contains(pref)) {
+ type = PREF_BOOL;
+ boolVal = intVal == 1;
+ }
+
+ switch (type) {
+ case PREF_FINISH:
+ handler.finish();
+ return;
+ case PREF_BOOL:
+ handler.prefValue(pref, boolVal);
+ return;
+ case PREF_INT:
+ handler.prefValue(pref, intVal);
+ return;
+ case PREF_STRING:
+ handler.prefValue(pref, strVal);
+ return;
+ }
+ throw new IllegalArgumentException();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private synchronized static void onPrefChange(final String pref, final int type,
+ final boolean boolVal, final int intVal,
+ final String strVal) {
+ final Object existing = OBSERVERS.get(pref);
+
+ if (existing == null) {
+ return;
+ }
+
+ final Iterator<PrefHandler> itor;
+ PrefHandler handler;
+
+ if (existing instanceof PrefHandler) {
+ itor = null;
+ handler = (PrefHandler) existing;
+ } else {
+ @SuppressWarnings("unchecked")
+ final List<PrefHandler> handlerList = (List) existing;
+ if (handlerList.isEmpty()) {
+ return;
+ }
+ itor = handlerList.iterator();
+ handler = itor.next();
+ }
+
+ do {
+ callPrefHandler(handler, type, pref, boolVal, intVal, strVal);
+ handler.finish();
+
+ handler = itor != null && itor.hasNext() ? itor.next() : null;
+ } while (handler != null);
+ }
+
+ public interface PrefHandler {
+ void prefValue(String pref, boolean value);
+ void prefValue(String pref, int value);
+ void prefValue(String pref, String value);
+ void finish();
+ }
+
+ public static abstract class PrefHandlerBase implements PrefHandler {
+ @Override
+ public void prefValue(String pref, boolean value) {
+ throw new UnsupportedOperationException(
+ "Unhandled boolean pref " + pref + "; wrong type?");
+ }
+
+ @Override
+ public void prefValue(String pref, int value) {
+ throw new UnsupportedOperationException(
+ "Unhandled int pref " + pref + "; wrong type?");
+ }
+
+ @Override
+ public void prefValue(String pref, String value) {
+ throw new UnsupportedOperationException(
+ "Unhandled String pref " + pref + "; wrong type?");
+ }
+
+ @Override
+ public void finish() {
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SysInfo.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SysInfo.java
new file mode 100644
index 000000000..5c53ef465
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SysInfo.java
@@ -0,0 +1,237 @@
+/* -*- 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.os.StrictMode;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import java.util.regex.Pattern;
+
+/**
+ * A collection of system info values, broadly mirroring a subset of
+ * nsSystemInfo. See also the constants in AppConstants, which reflect
+ * much of nsIXULAppInfo.
+ */
+// Normally, we'd annotate with @RobocopTarget. Since SysInfo is compiled
+// before RobocopTarget, we instead add o.m.g.SysInfo directly to the Proguard
+// configuration.
+public final class SysInfo {
+ private static final String LOG_TAG = "GeckoSysInfo";
+
+ // Number of bytes of /proc/meminfo to read in one go.
+ private static final int MEMINFO_BUFFER_SIZE_BYTES = 256;
+
+ // We don't mind an instant of possible duplicate work, we only wish to
+ // avoid inconsistency, so we don't bother with synchronization for
+ // these.
+ private static volatile int cpuCount = -1;
+
+ private static volatile int totalRAM = -1;
+
+ /**
+ * Get the number of cores on the device.
+ *
+ * We can't use a nice tidy API call, because they're all wrong:
+ *
+ * <http://stackoverflow.com/questions/7962155/how-can-you-detect-a-dual-core-
+ * cpu-on-an-android-device-from-code>
+ *
+ * This method is based on that code.
+ *
+ * @return the number of CPU cores, or 1 if the number could not be
+ * determined.
+ */
+ public static int getCPUCount() {
+ if (cpuCount > 0) {
+ return cpuCount;
+ }
+
+ // Avoid a strict mode warning.
+ StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+ try {
+ return readCPUCount();
+ } finally {
+ StrictMode.setThreadPolicy(savedPolicy);
+ }
+ }
+
+ private static int readCPUCount() {
+ class CpuFilter implements FileFilter {
+ @Override
+ public boolean accept(File pathname) {
+ return Pattern.matches("cpu[0-9]+", pathname.getName());
+ }
+ }
+ try {
+ final File dir = new File("/sys/devices/system/cpu/");
+ return cpuCount = dir.listFiles(new CpuFilter()).length;
+ } catch (Exception e) {
+ Log.w(LOG_TAG, "Assuming 1 CPU; got exception.", e);
+ return cpuCount = 1;
+ }
+ }
+
+ /**
+ * Helper functions used to extract key/value data from /proc/meminfo
+ * Pulled from:
+ * http://androidxref.com/4.2_r1/xref/frameworks/base/core/java/com/android/internal/util/MemInfoReader.java
+ */
+ private static boolean matchMemText(byte[] buffer, int index, int bufferLength, byte[] text) {
+ final int N = text.length;
+ if ((index + N) >= bufferLength) {
+ return false;
+ }
+ for (int i = 0; i < N; i++) {
+ if (buffer[index + i] != text[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Parses a line like:
+ *
+ * MemTotal: 1605324 kB
+ *
+ * into 1605324.
+ *
+ * @return the first uninterrupted sequence of digits following the
+ * specified index, parsed as an integer value in KB.
+ */
+ private static int extractMemValue(byte[] buffer, int offset, int length) {
+ if (offset >= length) {
+ return 0;
+ }
+
+ while (offset < length && buffer[offset] != '\n') {
+ if (buffer[offset] >= '0' && buffer[offset] <= '9') {
+ int start = offset++;
+ while (offset < length &&
+ buffer[offset] >= '0' &&
+ buffer[offset] <= '9') {
+ ++offset;
+ }
+ return Integer.parseInt(new String(buffer, start, offset - start), 10);
+ }
+ ++offset;
+ }
+ return 0;
+ }
+
+ /**
+ * Fetch the total memory of the device in MB by parsing /proc/meminfo.
+ *
+ * Of course, Android doesn't have a neat and tidy way to find total
+ * RAM, so we do it by parsing /proc/meminfo.
+ *
+ * @return 0 if a problem occurred, or memory size in MB.
+ */
+ public static int getMemSize() {
+ if (totalRAM >= 0) {
+ return totalRAM;
+ }
+
+ // This is the string "MemTotal" that we're searching for in the buffer.
+ final byte[] MEMTOTAL = {'M', 'e', 'm', 'T', 'o', 't', 'a', 'l'};
+
+ // `/proc/meminfo` is not a real file and thus safe to read on the main thread.
+ final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+ try {
+ final byte[] buffer = new byte[MEMINFO_BUFFER_SIZE_BYTES];
+ final FileInputStream is = new FileInputStream("/proc/meminfo");
+ try {
+ final int length = is.read(buffer);
+
+ for (int i = 0; i < length; i++) {
+ if (matchMemText(buffer, i, length, MEMTOTAL)) {
+ i += 8;
+ totalRAM = extractMemValue(buffer, i, length) / 1024;
+ Log.d(LOG_TAG, "System memory: " + totalRAM + "MB.");
+ return totalRAM;
+ }
+ }
+ } finally {
+ is.close();
+ }
+
+ Log.w(LOG_TAG, "Did not find MemTotal line in /proc/meminfo.");
+ return totalRAM = 0;
+ } catch (FileNotFoundException f) {
+ return totalRAM = 0;
+ } catch (IOException e) {
+ return totalRAM = 0;
+ } finally {
+ StrictMode.setThreadPolicy(savedPolicy);
+ }
+ }
+
+ /**
+ * @return the SDK version supported by this device, such as '16'.
+ */
+ public static int getVersion() {
+ return android.os.Build.VERSION.SDK_INT;
+ }
+
+ /**
+ * @return the release version string, such as "4.1.2".
+ */
+ public static String getReleaseVersion() {
+ return android.os.Build.VERSION.RELEASE;
+ }
+
+ /**
+ * @return the kernel version string, such as "3.4.10-geb45596".
+ */
+ public static String getKernelVersion() {
+ return System.getProperty("os.version", "");
+ }
+
+ /**
+ * @return the device manufacturer, such as "HTC".
+ */
+ public static String getManufacturer() {
+ return android.os.Build.MANUFACTURER;
+ }
+
+ /**
+ * @return the device name, such as "HTC One".
+ */
+ public static String getDevice() {
+ // No, not android.os.Build.DEVICE.
+ return android.os.Build.MODEL;
+ }
+
+ /**
+ * @return the Android "hardware" identifier, such as "m7".
+ */
+ public static String getHardware() {
+ return android.os.Build.HARDWARE;
+ }
+
+ /**
+ * @return the system OS name. Hardcoded to "Android".
+ */
+ public static String getName() {
+ // We deliberately differ from PR_SI_SYSNAME, which is "Linux".
+ return "Android";
+ }
+
+ /**
+ * @return the Android architecture string, including ABI.
+ */
+ public static String getArchABI() {
+ // Android likes to include the ABI, too ("armeabiv7"), so we
+ // differ to add value.
+ return android.os.Build.CPU_ABI;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/TouchEventInterceptor.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/TouchEventInterceptor.java
new file mode 100644
index 000000000..41a71dfa5
--- /dev/null
+++ b/mobile/android/geckoview/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/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/JNITarget.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/JNITarget.java
new file mode 100644
index 000000000..d6140a1ff
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/JNITarget.java
@@ -0,0 +1,14 @@
+/* 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.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
+@Retention(RetentionPolicy.CLASS)
+public @interface JNITarget {}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/ReflectionTarget.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/ReflectionTarget.java
new file mode 100644
index 000000000..e873ebeb9
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/ReflectionTarget.java
@@ -0,0 +1,18 @@
+/* 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.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/*
+ * Used to indicate to ProGuard that this definition is accessed
+ * via reflection and should not be stripped from the source.
+ */
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
+@Retention(RetentionPolicy.CLASS)
+public @interface ReflectionTarget {}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/RobocopTarget.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/RobocopTarget.java
new file mode 100644
index 000000000..e15130674
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/RobocopTarget.java
@@ -0,0 +1,15 @@
+/* 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.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
+@Retention(RetentionPolicy.CLASS)
+public @interface RobocopTarget {}
+
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WebRTCJNITarget.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WebRTCJNITarget.java
new file mode 100644
index 000000000..f58dea148
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WebRTCJNITarget.java
@@ -0,0 +1,14 @@
+/* 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.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
+@Retention(RetentionPolicy.CLASS)
+public @interface WebRTCJNITarget {}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WrapForJNI.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WrapForJNI.java
new file mode 100644
index 000000000..358ed5d56
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WrapForJNI.java
@@ -0,0 +1,51 @@
+/* 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.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation is used to tag methods that are to have wrapper methods generated.
+ * Such methods will be protected from destruction by ProGuard, and allow us to avoid
+ * writing by hand large amounts of boring boilerplate.
+ */
+@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface WrapForJNI {
+ /**
+ * Skip this member when generating wrappers for a whole class.
+ */
+ boolean skip() default false;
+
+ /**
+ * Optional parameter specifying the name of the generated method stub. If omitted,
+ * the capitalized name of the Java method will be used.
+ */
+ String stubName() default "";
+
+ /**
+ * Action to take if member access returns an exception.
+ * One of "abort", "ignore", or "nsresult". "nsresult" is not supported for native
+ * methods.
+ */
+ String exceptionMode() default "abort";
+
+ /**
+ * The thread that the method will be called from.
+ * One of "any", "gecko", or "ui". Not supported for fields.
+ */
+ String calledFrom() default "any";
+
+ /**
+ * The thread that the method call will be dispatched to.
+ * One of "current", "gecko", or "proxy". Not supported for non-native methods,
+ * fields, and constructors. Only void-return methods are supported for anything other
+ * than current thread.
+ */
+ String dispatchTo() default "current";
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BitmapUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BitmapUtils.java
new file mode 100644
index 000000000..a4b516519
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BitmapUtils.java
@@ -0,0 +1,290 @@
+/* -*- 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.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+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.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.util.Base64;
+import android.util.Log;
+
+public final class BitmapUtils {
+ private static final String LOGTAG = "GeckoBitmapUtils";
+
+ private BitmapUtils() {}
+
+ 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) {
+ if (dataURI == null) {
+ return null;
+ }
+
+ byte[] raw = getBytesFromDataURI(dataURI);
+ if (raw == null || raw.length == 0) {
+ return null;
+ }
+
+ return decodeByteArray(raw);
+ }
+
+ /**
+ * Return a byte[] containing the bytes in a given base64 string, or null if this is not a valid
+ * base64 string.
+ */
+ public static byte[] getBytesFromBase64(String base64) {
+ try {
+ return Base64.decode(base64, Base64.DEFAULT);
+ } catch (Exception e) {
+ Log.e(LOGTAG, "exception decoding bitmap from data URI: " + base64, e);
+ }
+
+ return null;
+ }
+
+ public static byte[] getBytesFromDataURI(String dataURI) {
+ final String base64 = dataURI.substring(dataURI.indexOf(',') + 1);
+ return getBytesFromBase64(base64);
+ }
+
+ 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(final Context context, final Uri resourceUrl) {
+ final String scheme = resourceUrl.getScheme();
+ if (!"drawable".equals(scheme)) {
+ // Return a "not found" default icon that's easy to spot.
+ return android.R.drawable.sym_def_app_icon;
+ }
+
+ String resource = resourceUrl.getSchemeSpecificPart();
+ if (resource.startsWith("//")) {
+ resource = resource.substring(2);
+ }
+
+ final Resources res = context.getResources();
+ int id = res.getIdentifier(resource, "drawable", context.getPackageName());
+ if (id != 0) {
+ return id;
+ }
+
+ // For backwards compatibility, we also search in system resources.
+ id = res.getIdentifier(resource, "drawable", "android");
+ if (id != 0) {
+ return id;
+ }
+
+ Log.w(LOGTAG, "Cannot find drawable/" + resource);
+ // Return a "not found" default icon that's easy to spot.
+ return android.R.drawable.sym_def_app_icon;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BufferedImage.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BufferedImage.java
new file mode 100644
index 000000000..4dbcf61bb
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BufferedImage.java
@@ -0,0 +1,94 @@
+/* -*- 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 buffered image that simply saves a buffer of pixel data. */
+public class BufferedImage {
+ private ByteBuffer mBuffer;
+ private Bitmap mBitmap;
+ private IntSize mSize;
+ private int mFormat;
+
+ private static final String LOGTAG = "GeckoBufferedImage";
+
+ /** Creates an empty buffered image */
+ public BufferedImage() {
+ mSize = new IntSize(0, 0);
+ }
+
+ /** Creates a buffered image from an Android bitmap. */
+ public BufferedImage(Bitmap bitmap) {
+ mFormat = bitmapConfigToFormat(bitmap.getConfig());
+ mSize = new IntSize(bitmap.getWidth(), bitmap.getHeight());
+ mBitmap = bitmap;
+ }
+
+ private synchronized void freeBuffer() {
+ if (mBuffer != null) {
+ mBuffer = DirectBufferAllocator.free(mBuffer);
+ }
+ }
+
+ public void destroy() {
+ try {
+ freeBuffer();
+ } catch (Exception ex) {
+ Log.e(LOGTAG, "error clearing buffer: ", ex);
+ }
+ }
+
+ public ByteBuffer getBuffer() {
+ if (mBuffer == null) {
+ int bpp = bitsPerPixelForFormat(mFormat);
+ mBuffer = DirectBufferAllocator.allocate(mSize.getArea() * bpp);
+ mBitmap.copyPixelsToBuffer(mBuffer.asIntBuffer());
+ mBitmap = null;
+ }
+ return mBuffer;
+ }
+
+ public IntSize getSize() { return mSize; }
+ public int getFormat() { return mFormat; }
+
+ 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;
+
+ private static int bitsPerPixelForFormat(int format) {
+ switch (format) {
+ case FORMAT_A1: return 1;
+ case FORMAT_A8: return 8;
+ case FORMAT_RGB16_565: return 16;
+ case FORMAT_RGB24: return 24;
+ case FORMAT_ARGB32: return 32;
+ default:
+ throw new RuntimeException("Unknown Cairo format");
+ }
+ }
+
+ private static int bitmapConfigToFormat(Bitmap.Config config) {
+ if (config == null)
+ return FORMAT_ARGB32; /* Droid Pro fix. */
+
+ switch (config) {
+ case ALPHA_8: return FORMAT_A8;
+ case ARGB_4444: throw new RuntimeException("ARGB_444 unsupported");
+ case ARGB_8888: return FORMAT_ARGB32;
+ case RGB_565: return FORMAT_RGB16_565;
+ default: throw new RuntimeException("Unknown Skia bitmap config");
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BufferedImageGLInfo.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BufferedImageGLInfo.java
new file mode 100644
index 000000000..41f38e1ba
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BufferedImageGLInfo.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 buffered bitmaps using OpenGL ES. */
+public class BufferedImageGLInfo {
+ public final int internalFormat;
+ public final int format;
+ public final int type;
+
+ public BufferedImageGLInfo(int bufferedImageFormat) {
+ switch (bufferedImageFormat) {
+ case BufferedImage.FORMAT_ARGB32:
+ internalFormat = format = GL10.GL_RGBA; type = GL10.GL_UNSIGNED_BYTE;
+ break;
+ case BufferedImage.FORMAT_RGB24:
+ internalFormat = format = GL10.GL_RGB; type = GL10.GL_UNSIGNED_BYTE;
+ break;
+ case BufferedImage.FORMAT_RGB16_565:
+ internalFormat = format = GL10.GL_RGB; type = GL10.GL_UNSIGNED_SHORT_5_6_5;
+ break;
+ case BufferedImage.FORMAT_A8:
+ case BufferedImage.FORMAT_A1:
+ throw new RuntimeException("BufferedImage FORMAT_A1 and FORMAT_A8 unsupported");
+ default:
+ throw new RuntimeException("Unknown BufferedImage format");
+ }
+ }
+}
+
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DynamicToolbarAnimator.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DynamicToolbarAnimator.java
new file mode 100644
index 000000000..e299b5744
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DynamicToolbarAnimator.java
@@ -0,0 +1,605 @@
+/* -*- 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.AppConstants;
+import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.util.FloatUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.graphics.PointF;
+import android.support.v4.view.ViewCompat;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.animation.LinearInterpolator;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+public class DynamicToolbarAnimator {
+ private static final String LOGTAG = "GeckoDynamicToolbarAnimator";
+ private static final String PREF_SCROLL_TOOLBAR_THRESHOLD = "browser.ui.scroll-toolbar-threshold";
+
+ public static enum PinReason {
+ RELAYOUT,
+ ACTION_MODE,
+ FULL_SCREEN,
+ CARET_DRAG
+ }
+
+ private final Set<PinReason> pinFlags = Collections.synchronizedSet(EnumSet.noneOf(PinReason.class));
+
+ // The duration of the animation in ns
+ private static final long ANIMATION_DURATION = 150000000;
+
+ private final GeckoLayerClient mTarget;
+ private final List<LayerView.DynamicToolbarListener> mListeners;
+
+ /* The translation to be applied to the toolbar UI view. This is the
+ * distance from the default/initial location (at the top of the screen,
+ * visible to the user) to where we want it to be. This variable should
+ * always be between 0 (toolbar fully visible) and the height of the toolbar
+ * (toolbar fully hidden), inclusive.
+ */
+ private float mToolbarTranslation;
+
+ /* The translation to be applied to the LayerView. This is the distance from
+ * the default/initial location (just below the toolbar, with the bottom
+ * extending past the bottom of the screen) to where we want it to be.
+ * This variable should always be between 0 and the height of the toolbar,
+ * inclusive.
+ */
+ private float mLayerViewTranslation;
+
+ /* This stores the maximum translation that can be applied to the toolbar
+ * and layerview when scrolling. This is populated with the height of the
+ * toolbar. */
+ private float mMaxTranslation;
+
+ /* This interpolator is used for the above mentioned animation */
+ private LinearInterpolator mInterpolator;
+
+ /* This is the proportion of the viewport rect that needs to be travelled
+ * while scrolling before the translation will start taking effect.
+ */
+ private float SCROLL_TOOLBAR_THRESHOLD = 0.20f;
+ /* The ID of the prefs listener for the scroll-toolbar threshold */
+ private final PrefsHelper.PrefHandler mPrefObserver;
+
+ /* While we are resizing the viewport to account for the toolbar, the Java
+ * code and painted layer metrics in the compositor have different notions
+ * of the CSS viewport height. The Java value is stored in the
+ * GeckoLayerClient's viewport metrics, and the Gecko one is stored here.
+ * This allows us to adjust fixed-pos items correctly.
+ * You must synchronize on mTarget.getLock() to read/write this. */
+ private Integer mHeightDuringResize;
+
+ /* This tracks if we should trigger a "snap" on the next composite. A "snap"
+ * is when we simultaneously move the LayerView and change the scroll offset
+ * in the compositor so that everything looks the same on the screen but
+ * has really been shifted.
+ * You must synchronize on |this| to read/write this. */
+ private boolean mSnapRequired = false;
+
+ /* The task that handles showing/hiding toolbar */
+ private DynamicToolbarAnimationTask mAnimationTask;
+
+ /* The start point of a drag, used for scroll-based dynamic toolbar
+ * behaviour. */
+ private PointF mTouchStart;
+ private float mLastTouch;
+
+ /* Set to true when root content is being scrolled */
+ private boolean mScrollingRootContent;
+
+ public DynamicToolbarAnimator(GeckoLayerClient aTarget) {
+ mTarget = aTarget;
+ mListeners = new ArrayList<LayerView.DynamicToolbarListener>();
+
+ mInterpolator = new LinearInterpolator();
+
+ // Listen to the dynamic toolbar pref
+ mPrefObserver = new PrefsHelper.PrefHandlerBase() {
+ @Override
+ public void prefValue(String pref, int value) {
+ SCROLL_TOOLBAR_THRESHOLD = value / 100.0f;
+ }
+ };
+ PrefsHelper.addObserver(new String[] { PREF_SCROLL_TOOLBAR_THRESHOLD }, mPrefObserver);
+ }
+
+ public void destroy() {
+ PrefsHelper.removeObserver(mPrefObserver);
+ }
+
+ public void addTranslationListener(LayerView.DynamicToolbarListener aListener) {
+ mListeners.add(aListener);
+ }
+
+ public void removeTranslationListener(LayerView.DynamicToolbarListener aListener) {
+ mListeners.remove(aListener);
+ }
+
+ private void fireListeners() {
+ for (LayerView.DynamicToolbarListener listener : mListeners) {
+ listener.onTranslationChanged(mToolbarTranslation, mLayerViewTranslation);
+ }
+ }
+
+ void onPanZoomStopped() {
+ for (LayerView.DynamicToolbarListener listener : mListeners) {
+ listener.onPanZoomStopped();
+ }
+ }
+
+ void onMetricsChanged(ImmutableViewportMetrics aMetrics) {
+ for (LayerView.DynamicToolbarListener listener : mListeners) {
+ listener.onMetricsChanged(aMetrics);
+ }
+ }
+
+ public void setMaxTranslation(float maxTranslation) {
+ ThreadUtils.assertOnUiThread();
+ if (maxTranslation < 0) {
+ Log.e(LOGTAG, "Got a negative max-translation value: " + maxTranslation + "; clamping to zero");
+ mMaxTranslation = 0;
+ } else {
+ mMaxTranslation = maxTranslation;
+ }
+ }
+
+ public float getMaxTranslation() {
+ return mMaxTranslation;
+ }
+
+ public float getToolbarTranslation() {
+ return mToolbarTranslation;
+ }
+
+ /**
+ * If true, scroll changes will not affect translation.
+ */
+ public boolean isPinned() {
+ return !pinFlags.isEmpty();
+ }
+
+ public boolean isPinnedBy(PinReason reason) {
+ return pinFlags.contains(reason);
+ }
+
+ public void setPinned(boolean pinned, PinReason reason) {
+ if (pinned) {
+ pinFlags.add(reason);
+ } else {
+ pinFlags.remove(reason);
+ }
+ }
+
+ public void showToolbar(boolean immediately) {
+ animateToolbar(true, immediately);
+ }
+
+ public void hideToolbar(boolean immediately) {
+ animateToolbar(false, immediately);
+ }
+
+ public void setScrollingRootContent(boolean isRootContent) {
+ mScrollingRootContent = isRootContent;
+ }
+
+ private void animateToolbar(final boolean showToolbar, boolean immediately) {
+ ThreadUtils.assertOnUiThread();
+
+ if (mAnimationTask != null) {
+ mTarget.getView().removeRenderTask(mAnimationTask);
+ mAnimationTask = null;
+ }
+
+ float desiredTranslation = (showToolbar ? 0 : mMaxTranslation);
+ Log.v(LOGTAG, "Requested " + (immediately ? "immediate " : "") + "toolbar animation to translation " + desiredTranslation);
+ if (FloatUtils.fuzzyEquals(mToolbarTranslation, desiredTranslation)) {
+ // If we're already pretty much in the desired position, don't bother
+ // with a full animation; do an immediate jump
+ immediately = true;
+ Log.v(LOGTAG, "Changing animation to immediate jump");
+ }
+
+ if (showToolbar && immediately) {
+ // Special case for showing the toolbar immediately: some of the call
+ // sites expect this to happen synchronously, so let's do that. This
+ // is safe because if we are showing the toolbar from a hidden state
+ // there is no chance of showing garbage
+ mToolbarTranslation = desiredTranslation;
+ fireListeners();
+ // And then proceed with the normal flow (some of which will be
+ // a no-op now)...
+ }
+
+ if (!showToolbar) {
+ // If we are hiding the toolbar, we need to move the LayerView first,
+ // so that we don't end up showing garbage under the toolbar when
+ // it is hidden. In the case that we are showing the toolbar, we
+ // move the LayerView after the toolbar is shown - the
+ // DynamicToolbarAnimationTask calls that upon completion.
+ shiftLayerView(desiredTranslation);
+ }
+
+ mAnimationTask = new DynamicToolbarAnimationTask(desiredTranslation, immediately, showToolbar);
+ mTarget.getView().postRenderTask(mAnimationTask);
+ }
+
+ private synchronized void shiftLayerView(float desiredTranslation) {
+ float layerViewTranslationNeeded = desiredTranslation - mLayerViewTranslation;
+ mLayerViewTranslation = desiredTranslation;
+ synchronized (mTarget.getLock()) {
+ if (layerViewTranslationNeeded == 0 && isResizing()) {
+ // We're already in the middle of a snap, so this new call is
+ // redundant as it's snapping to the same place. Ignore it.
+ return;
+ }
+ mHeightDuringResize = new Integer(mTarget.getViewportMetrics().viewportRectHeight);
+ mSnapRequired = mTarget.setViewportSize(
+ mTarget.getView().getWidth(),
+ mTarget.getView().getHeight() - Math.round(mMaxTranslation - mLayerViewTranslation),
+ new PointF(0, -layerViewTranslationNeeded));
+ if (!mSnapRequired) {
+ mHeightDuringResize = null;
+ ThreadUtils.postToUiThread(new Runnable() {
+ // Post to run it outside of the synchronize blocks. The
+ // delay shouldn't hurt.
+ @Override
+ public void run() {
+ fireListeners();
+ }
+ });
+ }
+ // Request a composite, which will trigger the snap.
+ mTarget.getView().requestRender();
+ }
+ }
+
+ IntSize getViewportSize() {
+ ThreadUtils.assertOnUiThread();
+
+ int viewWidth = mTarget.getView().getWidth();
+ int viewHeight = mTarget.getView().getHeight();
+ float toolbarTranslation = mToolbarTranslation;
+ if (mAnimationTask != null) {
+ // If we have an animation going, mToolbarTranslation may be in flux
+ // and we should use the final value it will settle on.
+ toolbarTranslation = mAnimationTask.getFinalToolbarTranslation();
+ }
+ int viewHeightVisible = viewHeight - Math.round(mMaxTranslation - toolbarTranslation);
+ return new IntSize(viewWidth, viewHeightVisible);
+ }
+
+ boolean isResizing() {
+ return mHeightDuringResize != null;
+ }
+
+ private final Runnable mSnapRunnable = new Runnable() {
+ private int mFrame = 0;
+
+ @Override
+ public final void run() {
+ // It takes 2 frames for the view translation to take effect, at
+ // least on a Nexus 4 device running Android 4.2.2. So we wait for
+ // two frames before doing the notifyAll(), otherwise we get a
+ // short user-visible glitch.
+ // TODO: find a better way to do this, if possible.
+ if (mFrame == 1) {
+ synchronized (this) {
+ this.notifyAll();
+ }
+ mFrame = 0;
+ return;
+ }
+
+ if (mFrame == 0) {
+ fireListeners();
+ }
+
+ ViewCompat.postOnAnimation(mTarget.getView(), this);
+ mFrame++;
+ }
+ };
+
+ void scrollChangeResizeCompleted() {
+ synchronized (mTarget.getLock()) {
+ Log.v(LOGTAG, "Scrollchange resize completed");
+ mHeightDuringResize = null;
+ }
+ }
+
+ /**
+ * "Shrinks" the absolute value of aValue by moving it closer to zero by
+ * aShrinkAmount, but prevents it from crossing over zero. If aShrinkAmount
+ * is negative it is ignored.
+ * @return The shrunken value.
+ */
+ private static float shrinkAbs(float aValue, float aShrinkAmount) {
+ if (aShrinkAmount <= 0) {
+ return aValue;
+ }
+ float shrinkBy = Math.min(Math.abs(aValue), aShrinkAmount);
+ return (aValue < 0 ? aValue + shrinkBy : aValue - shrinkBy);
+ }
+
+ /**
+ * This function takes in a scroll amount and decides how much of that
+ * should be used up to translate things on screen because of the dynamic
+ * toolbar behaviour. It returns the maximum amount that could be used
+ * for translation purposes; the rest must be used for scrolling.
+ */
+ private float decideTranslation(float aDelta,
+ ImmutableViewportMetrics aMetrics,
+ float aTouchTravelDistance) {
+
+ float exposeThreshold = aMetrics.getHeight() * SCROLL_TOOLBAR_THRESHOLD;
+ float translation = aDelta;
+
+ if (translation < 0) { // finger moving upwards
+ translation = shrinkAbs(translation, aMetrics.getOverscroll().top);
+
+ // If the toolbar is in a state between fully hidden and fully shown
+ // (i.e. the user is actively translating it), then we want the
+ // translation to take effect right away. Or if the user has moved
+ // their finger past the required threshold (and is not trying to
+ // scroll past the bottom of the page) then also we want the touch
+ // to cause translation. If the toolbar is fully visible, we only
+ // want the toolbar to hide if the user is scrolling the root content.
+ boolean inBetween = (mToolbarTranslation != 0 && mToolbarTranslation != mMaxTranslation);
+ boolean reachedThreshold = -aTouchTravelDistance >= exposeThreshold;
+ boolean atBottomOfPage = aMetrics.viewportRectBottom() >= aMetrics.pageRectBottom;
+ if (inBetween || (mScrollingRootContent && reachedThreshold && !atBottomOfPage)) {
+ return translation;
+ }
+ } else { // finger moving downwards
+ translation = shrinkAbs(translation, aMetrics.getOverscroll().bottom);
+
+ // Ditto above comment, but in this case if they reached the top and
+ // the toolbar is not shown, then we do want to allow translation
+ // right away.
+ boolean inBetween = (mToolbarTranslation != 0 && mToolbarTranslation != mMaxTranslation);
+ boolean reachedThreshold = aTouchTravelDistance >= exposeThreshold;
+ boolean atTopOfPage = aMetrics.viewportRectTop <= aMetrics.pageRectTop;
+ boolean isToolbarTranslated = (mToolbarTranslation != 0);
+ if (inBetween || reachedThreshold || (atTopOfPage && isToolbarTranslated)) {
+ return translation;
+ }
+ }
+
+ return 0;
+ }
+
+ // Timestamp of the start of the touch event used to calculate toolbar velocity
+ private long mLastEventTime;
+ // Current velocity of the toolbar. Used to populate the velocity queue in C++APZ.
+ private float mVelocity;
+
+ boolean onInterceptTouchEvent(MotionEvent event) {
+ if (isPinned()) {
+ return false;
+ }
+
+ // Animations should never co-exist with the user touching the screen.
+ if (mAnimationTask != null) {
+ mTarget.getView().removeRenderTask(mAnimationTask);
+ mAnimationTask = null;
+ }
+
+ // we only care about single-finger drags here; any other kind of event
+ // should reset and cause us to start over.
+ if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE ||
+ event.getActionMasked() != MotionEvent.ACTION_MOVE ||
+ event.getPointerCount() != 1)
+ {
+ if (mTouchStart != null) {
+ Log.v(LOGTAG, "Resetting touch sequence due to non-move");
+ mTouchStart = null;
+ mVelocity = 0.0f;
+ }
+
+ if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+ // We need to do this even if the toolbar is already fully
+ // visible or fully hidden, because this is what triggers the
+ // viewport resize in content and updates the viewport metrics.
+ boolean toolbarMostlyVisible = mToolbarTranslation < (mMaxTranslation / 2);
+ Log.v(LOGTAG, "All fingers lifted, completing " + (toolbarMostlyVisible ? "show" : "hide"));
+ animateToolbar(toolbarMostlyVisible, false);
+ }
+ return false;
+ }
+
+ if (mTouchStart != null) {
+ float prevDir = mLastTouch - mTouchStart.y;
+ float newDir = event.getRawY() - mLastTouch;
+ if (prevDir != 0 && newDir != 0 && ((prevDir < 0) != (newDir < 0))) {
+ // If the direction of movement changed, reset the travel
+ // distance properties.
+ mTouchStart = null;
+ mVelocity = 0.0f;
+ }
+ }
+
+ if (mTouchStart == null) {
+ mTouchStart = new PointF(event.getRawX(), event.getRawY());
+ mLastTouch = event.getRawY();
+ mLastEventTime = event.getEventTime();
+ return false;
+ }
+
+ float deltaY = event.getRawY() - mLastTouch;
+ long currentTime = event.getEventTime();
+ float deltaTime = (float)(currentTime - mLastEventTime);
+ mLastEventTime = currentTime;
+ if (deltaTime > 0.0f) {
+ mVelocity = -deltaY / deltaTime;
+ } else {
+ mVelocity = 0.0f;
+ }
+ mLastTouch = event.getRawY();
+ float travelDistance = event.getRawY() - mTouchStart.y;
+
+ ImmutableViewportMetrics metrics = mTarget.getViewportMetrics();
+
+ if (metrics.getPageHeight() <= mTarget.getView().getHeight() &&
+ mToolbarTranslation == 0) {
+ // If the page is short and the toolbar is already visible, don't
+ // allow translating it out of view.
+ return false;
+ }
+
+ float translation = decideTranslation(deltaY, metrics, travelDistance);
+
+ float oldToolbarTranslation = mToolbarTranslation;
+ float oldLayerViewTranslation = mLayerViewTranslation;
+ mToolbarTranslation = FloatUtils.clamp(mToolbarTranslation - translation, 0, mMaxTranslation);
+ mLayerViewTranslation = FloatUtils.clamp(mLayerViewTranslation - translation, 0, mMaxTranslation);
+
+ if (oldToolbarTranslation == mToolbarTranslation &&
+ oldLayerViewTranslation == mLayerViewTranslation) {
+ return false;
+ }
+
+ if (mToolbarTranslation == mMaxTranslation) {
+ Log.v(LOGTAG, "Toolbar at maximum translation, calling shiftLayerView(" + mMaxTranslation + ")");
+ shiftLayerView(mMaxTranslation);
+ } else if (mToolbarTranslation == 0) {
+ Log.v(LOGTAG, "Toolbar at minimum translation, calling shiftLayerView(0)");
+ shiftLayerView(0);
+ }
+
+ fireListeners();
+ mTarget.getView().requestRender();
+ return true;
+ }
+
+ // Get the current velocity of the toolbar.
+ float getVelocity() {
+ return mVelocity;
+ }
+
+ public PointF getVisibleEndOfLayerView() {
+ return new PointF(mTarget.getView().getWidth(),
+ mTarget.getView().getHeight() - mMaxTranslation + mLayerViewTranslation);
+ }
+
+ private float bottomOfCssViewport(ImmutableViewportMetrics aMetrics) {
+ return (isResizing() ? mHeightDuringResize : aMetrics.getHeight())
+ + mMaxTranslation - mLayerViewTranslation;
+ }
+
+ private synchronized boolean getAndClearSnapRequired() {
+ boolean snapRequired = mSnapRequired;
+ mSnapRequired = false;
+ return snapRequired;
+ }
+
+ void populateViewTransform(ViewTransform aTransform, ImmutableViewportMetrics aMetrics) {
+ if (getAndClearSnapRequired()) {
+ synchronized (mSnapRunnable) {
+ ViewCompat.postOnAnimation(mTarget.getView(), mSnapRunnable);
+ try {
+ // hold the in-progress composite until the views have been
+ // translated because otherwise there is a visible glitch.
+ // don't hold for more than 100ms just in case.
+ mSnapRunnable.wait(100);
+ } catch (InterruptedException ie) {
+ }
+ }
+ }
+
+ aTransform.x = aMetrics.viewportRectLeft;
+ aTransform.y = aMetrics.viewportRectTop;
+ aTransform.width = aMetrics.viewportRectWidth;
+ aTransform.height = aMetrics.viewportRectHeight;
+ aTransform.scale = aMetrics.zoomFactor;
+
+ aTransform.fixedLayerMarginTop = mLayerViewTranslation - mToolbarTranslation;
+ float bottomOfScreen = mTarget.getView().getHeight();
+ // We want to move a fixed item from "bottomOfCssViewport" to
+ // "bottomOfScreen". But also the bottom margin > 0 means that bottom
+ // fixed-pos items will move upwards.
+ aTransform.fixedLayerMarginBottom = bottomOfCssViewport(aMetrics) - bottomOfScreen;
+ //Log.v(LOGTAG, "ViewTransform is x=" + aTransform.x + " y=" + aTransform.y
+ // + " z=" + aTransform.scale + " t=" + aTransform.fixedLayerMarginTop
+ // + " b=" + aTransform.fixedLayerMarginBottom);
+ }
+
+ class DynamicToolbarAnimationTask extends RenderTask {
+ private final float mStartTranslation;
+ private final float mEndTranslation;
+ private final boolean mImmediate;
+ private final boolean mShiftLayerView;
+ private boolean mContinueAnimation;
+
+ public DynamicToolbarAnimationTask(float aTranslation, boolean aImmediate, boolean aShiftLayerView) {
+ super(false);
+ mContinueAnimation = true;
+ mStartTranslation = mToolbarTranslation;
+ mEndTranslation = aTranslation;
+ mImmediate = aImmediate;
+ mShiftLayerView = aShiftLayerView;
+ }
+
+ float getFinalToolbarTranslation() {
+ return mEndTranslation;
+ }
+
+ @Override
+ public boolean internalRun(long timeDelta, long currentFrameStartTime) {
+ if (!mContinueAnimation) {
+ return false;
+ }
+
+ // Calculate the progress (between 0 and 1)
+ final float progress = mImmediate
+ ? 1.0f
+ : mInterpolator.getInterpolation(
+ Math.min(1.0f, (System.nanoTime() - getStartTime())
+ / (float)ANIMATION_DURATION));
+
+ // This runs on the compositor thread, so we need to post the
+ // actual work to the UI thread.
+ ThreadUtils.assertNotOnUiThread();
+
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // Move the toolbar as per the animation
+ mToolbarTranslation = FloatUtils.interpolate(mStartTranslation, mEndTranslation, progress);
+ fireListeners();
+
+ if (mShiftLayerView && progress >= 1.0f) {
+ shiftLayerView(mEndTranslation);
+ }
+ }
+ });
+
+ mTarget.getView().requestRender();
+ if (progress >= 1.0f) {
+ mContinueAnimation = false;
+ }
+ return mContinueAnimation;
+ }
+ }
+
+ class SnapMetrics {
+ public final int viewportWidth;
+ public final int viewportHeight;
+ public final float scrollChangeY;
+
+ SnapMetrics(ImmutableViewportMetrics aMetrics, float aScrollChange) {
+ viewportWidth = aMetrics.viewportRectWidth;
+ viewportHeight = aMetrics.viewportRectHeight;
+ scrollChangeY = aScrollChange;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FloatSize.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FloatSize.java
new file mode 100644
index 000000000..4b495ab77
--- /dev/null
+++ b/mobile/android/geckoview/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/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FullScreenState.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FullScreenState.java
new file mode 100644
index 000000000..9574bbe0e
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FullScreenState.java
@@ -0,0 +1,12 @@
+/* -*- 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 enum FullScreenState {
+ NONE,
+ ROOT_ELEMENT,
+ NON_ROOT_ELEMENT
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
new file mode 100644
index 000000000..d504fe13e
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
@@ -0,0 +1,694 @@
+/* -*- 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.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.gfx.LayerView.DrawListener;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.FloatUtils;
+import org.mozilla.gecko.AppConstants;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.SystemClock;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class GeckoLayerClient implements LayerView.Listener, PanZoomTarget
+{
+ private static final String LOGTAG = "GeckoLayerClient";
+ private static int sPaintSyncId = 1;
+
+ private LayerRenderer mLayerRenderer;
+ private boolean mLayerRendererInitialized;
+
+ private final Context mContext;
+ private IntSize mScreenSize;
+ private IntSize mWindowSize;
+
+ /*
+ * 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 final List<DrawListener> mDrawListeners;
+
+ /* Used as temporaries by syncViewportInfo */
+ private final ViewTransform mCurrentViewTransform;
+
+ 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) whenever reading multiple fields from mViewportMetrics without synchronization (i.e. in
+ * case 1 above) you should always first 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 volatile boolean mGeckoIsReady;
+
+ /* package */ final PanZoomController mPanZoomController;
+ private final DynamicToolbarAnimator mToolbarAnimator;
+ /* package */ final 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;
+
+ private SynthesizedEventState mPointerState;
+
+ @WrapForJNI(stubName = "ClearColor")
+ private volatile int mClearColor = Color.WHITE;
+
+ 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);
+ mCurrentViewTransform = new ViewTransform(0, 0, 1);
+
+ mForceRedraw = true;
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ mViewportMetrics = new ImmutableViewportMetrics(displayMetrics)
+ .setViewportSize(view.getWidth(), view.getHeight());
+
+ mFrameMetrics = mViewportMetrics;
+
+ mDrawListeners = new ArrayList<DrawListener>();
+ mToolbarAnimator = new DynamicToolbarAnimator(this);
+ mPanZoomController = PanZoomController.Factory.create(this, view, eventDispatcher);
+ mView = view;
+ mView.setListener(this);
+ mContentDocumentIsDisplayed = true;
+ }
+
+ public void setOverscrollHandler(final Overscroll listener) {
+ mPanZoomController.setOverscrollHandler(listener);
+ }
+
+ public void setGeckoReady(boolean ready) {
+ mGeckoIsReady = ready;
+ }
+
+ @Override // PanZoomTarget
+ public boolean isGeckoReady() {
+ return mGeckoIsReady;
+ }
+
+ /** Attaches to root layer so that Gecko appears. */
+ @WrapForJNI(calledFrom = "gecko")
+ private void onGeckoReady() {
+ mGeckoIsReady = true;
+
+ mLayerRenderer = mView.getRenderer();
+
+ sendResizeEventIfNecessary(true, null);
+
+ // 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() {
+ mPanZoomController.attach();
+ mView.updateCompositor();
+ }
+ });
+ }
+
+ public void destroy() {
+ mPanZoomController.destroy();
+ mToolbarAnimator.destroy();
+ mDrawListeners.clear();
+ mGeckoIsReady = false;
+ }
+
+ 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.
+ */
+ boolean setViewportSize(int width, int height, PointF scrollChange) {
+ if (mViewportMetrics.viewportRectWidth == width &&
+ mViewportMetrics.viewportRectHeight == height &&
+ (scrollChange == null || (scrollChange.x == 0 && scrollChange.y == 0))) {
+ return false;
+ }
+ mViewportMetrics = mViewportMetrics.setViewportSize(width, height);
+ if (scrollChange != null) {
+ mViewportMetrics = mPanZoomController.adjustScrollForSurfaceShift(mViewportMetrics, scrollChange);
+ }
+
+ 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, scrollChange);
+
+ // 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();
+ }
+ return true;
+ }
+
+ PanZoomController getPanZoomController() {
+ return mPanZoomController;
+ }
+
+ DynamicToolbarAnimator getDynamicToolbarAnimator() {
+ return mToolbarAnimator;
+ }
+
+ /* Informs Gecko that the screen size has changed. */
+ private void sendResizeEventIfNecessary(boolean force, PointF scrollChange) {
+ DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
+
+ IntSize newScreenSize = new IntSize(metrics.widthPixels, metrics.heightPixels);
+ IntSize newWindowSize = new IntSize(mViewportMetrics.viewportRectWidth,
+ mViewportMetrics.viewportRectHeight);
+
+ 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);
+ }
+
+ if (mView != null) {
+ mView.notifySizeChanged(mWindowSize.width, mWindowSize.height,
+ mScreenSize.width, mScreenSize.height);
+ }
+
+ String json = "";
+ try {
+ if (scrollChange != null) {
+ int id = ++sPaintSyncId;
+ if (id == 0) {
+ // never use 0 as that is the default value for "this is not
+ // a special transaction"
+ id = ++sPaintSyncId;
+ }
+ JSONObject jsonObj = new JSONObject();
+ jsonObj.put("x", scrollChange.x / mViewportMetrics.zoomFactor);
+ jsonObj.put("y", scrollChange.y / mViewportMetrics.zoomFactor);
+ jsonObj.put("id", id);
+ json = jsonObj.toString();
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Unable to convert point to JSON", e);
+ }
+ GeckoAppShell.notifyObservers("Window:Resize", json);
+ }
+
+ /**
+ * 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
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ void contentDocumentChanged() {
+ mContentDocumentIsDisplayed = false;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ boolean isContentDocumentDisplayed() {
+ return mContentDocumentIsDisplayed;
+ }
+
+ /** 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.
+ */
+ @WrapForJNI
+ public void setFirstPaintViewport(float offsetX, float offsetY, float zoom,
+ float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom) {
+ synchronized (getLock()) {
+ ImmutableViewportMetrics currentMetrics = getViewportMetrics();
+
+ 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);
+ // Since we have switched to displaying a different document, we need to update any
+ // viewport-related state we have lying around (i.e. mViewportMetrics).
+ // Usually this information is updated via handleViewportMessage
+ // while we remain on the same document.
+ setViewportMetrics(newMetrics, true);
+
+ // 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);
+ }
+ }
+
+ mContentDocumentIsDisplayed = true;
+ }
+
+ /** 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
+ * every time we're called. NOTE: we might be able to return a ImmutableViewportMetrics
+ * which would avoid the copy into mCurrentViewTransform.
+ */
+ private ViewTransform syncViewportInfo(int x, int y, int width, int height, float resolution, boolean layersUpdated,
+ int paintSyncId) {
+ // 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();
+
+ if (paintSyncId == sPaintSyncId) {
+ mToolbarAnimator.scrollChangeResizeCompleted();
+ }
+ mToolbarAnimator.populateViewTransform(mCurrentViewTransform, mFrameMetrics);
+
+ if (layersUpdated) {
+ for (DrawListener listener : mDrawListeners) {
+ listener.drawFinished();
+ }
+ }
+
+ return mCurrentViewTransform;
+ }
+
+ @WrapForJNI
+ public ViewTransform syncFrameMetrics(float scrollX, float scrollY, float zoom,
+ float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom,
+ int dpX, int dpY, int dpWidth, int dpHeight, float paintedResolution,
+ boolean layersUpdated, int paintSyncId)
+ {
+ // TODO: optimize this so it doesn't create so much garbage - it's a
+ // hot path
+ RectF cssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom);
+ synchronized (getLock()) {
+ mViewportMetrics = mViewportMetrics.setViewportOrigin(scrollX, scrollY)
+ .setZoomFactor(zoom)
+ .setPageRect(RectUtils.scale(cssPageRect, zoom), cssPageRect);
+ }
+ return syncViewportInfo(dpX, dpY, dpWidth, dpHeight, paintedResolution,
+ layersUpdated, paintSyncId);
+ }
+
+ class PointerInfo {
+ // We reserve one pointer ID for the mouse, so that tests don't have
+ // to worry about tracking pointer IDs if they just want to test mouse
+ // event synthesization. If somebody tries to use this ID for a
+ // synthesized touch event we'll throw an exception.
+ public static final int RESERVED_MOUSE_POINTER_ID = 100000;
+
+ public int pointerId;
+ public int source;
+ public int screenX;
+ public int screenY;
+ public double pressure;
+ public int orientation;
+
+ public MotionEvent.PointerCoords getCoords() {
+ MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+ coords.orientation = orientation;
+ coords.pressure = (float)pressure;
+ coords.x = screenX;
+ coords.y = screenY;
+ return coords;
+ }
+ }
+
+ class SynthesizedEventState {
+ public final ArrayList<PointerInfo> pointers;
+ public long downTime;
+
+ SynthesizedEventState() {
+ pointers = new ArrayList<PointerInfo>();
+ }
+
+ int getPointerIndex(int pointerId) {
+ for (int i = 0; i < pointers.size(); i++) {
+ if (pointers.get(i).pointerId == pointerId) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ int addPointer(int pointerId, int source) {
+ PointerInfo info = new PointerInfo();
+ info.pointerId = pointerId;
+ info.source = source;
+ pointers.add(info);
+ return pointers.size() - 1;
+ }
+
+ int getPointerCount(int source) {
+ int count = 0;
+ for (int i = 0; i < pointers.size(); i++) {
+ if (pointers.get(i).source == source) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ MotionEvent.PointerProperties[] getPointerProperties(int source) {
+ MotionEvent.PointerProperties[] props = new MotionEvent.PointerProperties[getPointerCount(source)];
+ int index = 0;
+ for (int i = 0; i < pointers.size(); i++) {
+ if (pointers.get(i).source == source) {
+ MotionEvent.PointerProperties p = new MotionEvent.PointerProperties();
+ p.id = pointers.get(i).pointerId;
+ switch (source) {
+ case InputDevice.SOURCE_TOUCHSCREEN:
+ p.toolType = MotionEvent.TOOL_TYPE_FINGER;
+ break;
+ case InputDevice.SOURCE_MOUSE:
+ p.toolType = MotionEvent.TOOL_TYPE_MOUSE;
+ break;
+ }
+ props[index++] = p;
+ }
+ }
+ return props;
+ }
+
+ MotionEvent.PointerCoords[] getPointerCoords(int source) {
+ MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[getPointerCount(source)];
+ int index = 0;
+ for (int i = 0; i < pointers.size(); i++) {
+ if (pointers.get(i).source == source) {
+ coords[index++] = pointers.get(i).getCoords();
+ }
+ }
+ return coords;
+ }
+ }
+
+ private void synthesizeNativePointer(int source, int pointerId,
+ int eventType, int screenX, int screenY, double pressure,
+ int orientation)
+ {
+ Log.d(LOGTAG, "Synthesizing pointer from " + source + " id " + pointerId + " at " + screenX + ", " + screenY);
+
+ if (mPointerState == null) {
+ mPointerState = new SynthesizedEventState();
+ }
+
+ // Find the pointer if it already exists
+ int pointerIndex = mPointerState.getPointerIndex(pointerId);
+
+ // Event-specific handling
+ switch (eventType) {
+ case MotionEvent.ACTION_POINTER_UP:
+ if (pointerIndex < 0) {
+ Log.d(LOGTAG, "Requested synthesis of a pointer-up for a pointer that doesn't exist!");
+ return;
+ }
+ if (mPointerState.pointers.size() == 1) {
+ // Last pointer is going up
+ eventType = MotionEvent.ACTION_UP;
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ if (pointerIndex < 0) {
+ Log.d(LOGTAG, "Requested synthesis of a pointer-cancel for a pointer that doesn't exist!");
+ return;
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ if (pointerIndex < 0) {
+ // Adding a new pointer
+ pointerIndex = mPointerState.addPointer(pointerId, source);
+ if (pointerIndex == 0) {
+ // first pointer
+ eventType = MotionEvent.ACTION_DOWN;
+ mPointerState.downTime = SystemClock.uptimeMillis();
+ }
+ } else {
+ // We're moving an existing pointer
+ eventType = MotionEvent.ACTION_MOVE;
+ }
+ break;
+ case MotionEvent.ACTION_HOVER_MOVE:
+ if (pointerIndex < 0) {
+ // Mouse-move a pointer without it going "down". However
+ // in order to send the right MotionEvent without a lot of
+ // duplicated code, we add the pointer to mPointerState,
+ // and then remove it at the bottom of this function.
+ pointerIndex = mPointerState.addPointer(pointerId, source);
+ } else {
+ // We're moving an existing mouse pointer that went down.
+ eventType = MotionEvent.ACTION_MOVE;
+ }
+ break;
+ }
+
+ // Update the pointer with the new info
+ PointerInfo info = mPointerState.pointers.get(pointerIndex);
+ info.screenX = screenX;
+ info.screenY = screenY;
+ info.pressure = pressure;
+ info.orientation = orientation;
+
+ // Dispatch the event
+ int action = (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
+ action &= MotionEvent.ACTION_POINTER_INDEX_MASK;
+ action |= (eventType & MotionEvent.ACTION_MASK);
+ boolean isButtonDown = (source == InputDevice.SOURCE_MOUSE) &&
+ (eventType == MotionEvent.ACTION_DOWN || eventType == MotionEvent.ACTION_MOVE);
+ final MotionEvent event = MotionEvent.obtain(
+ /*downTime*/ mPointerState.downTime,
+ /*eventTime*/ SystemClock.uptimeMillis(),
+ /*action*/ action,
+ /*pointerCount*/ mPointerState.getPointerCount(source),
+ /*pointerProperties*/ mPointerState.getPointerProperties(source),
+ /*pointerCoords*/ mPointerState.getPointerCoords(source),
+ /*metaState*/ 0,
+ /*buttonState*/ (isButtonDown ? MotionEvent.BUTTON_PRIMARY : 0),
+ /*xPrecision*/ 0,
+ /*yPrecision*/ 0,
+ /*deviceId*/ 0,
+ /*edgeFlags*/ 0,
+ /*source*/ source,
+ /*flags*/ 0);
+ mView.post(new Runnable() {
+ @Override
+ public void run() {
+ mView.dispatchTouchEvent(event);
+ }
+ });
+
+ // Forget about removed pointers
+ if (eventType == MotionEvent.ACTION_POINTER_UP ||
+ eventType == MotionEvent.ACTION_UP ||
+ eventType == MotionEvent.ACTION_CANCEL ||
+ eventType == MotionEvent.ACTION_HOVER_MOVE)
+ {
+ mPointerState.pointers.remove(pointerIndex);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public void synthesizeNativeTouchPoint(int pointerId, int eventType, int screenX,
+ int screenY, double pressure, int orientation)
+ {
+ if (pointerId == PointerInfo.RESERVED_MOUSE_POINTER_ID) {
+ throw new IllegalArgumentException("Use a different pointer ID in your test, this one is reserved for mouse");
+ }
+ synthesizeNativePointer(InputDevice.SOURCE_TOUCHSCREEN, pointerId,
+ eventType, screenX, screenY, pressure, orientation);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public void synthesizeNativeMouseEvent(int eventType, int screenX, int screenY) {
+ synthesizeNativePointer(InputDevice.SOURCE_MOUSE, PointerInfo.RESERVED_MOUSE_POINTER_ID,
+ eventType, screenX, screenY, 0, 0);
+ }
+
+ @WrapForJNI
+ public LayerRenderer.Frame createFrame() {
+ // Create the shaders and textures if necessary.
+ if (!mLayerRendererInitialized) {
+ if (mLayerRenderer == null) {
+ return null;
+ }
+ mLayerRenderer.createDefaultProgram();
+ mLayerRendererInitialized = true;
+ }
+
+ try {
+ return mLayerRenderer.createFrame(mFrameMetrics);
+ } catch (Exception e) {
+ Log.w(LOGTAG, e);
+ return null;
+ }
+ }
+
+ private void geometryChanged() {
+ /* Let Gecko know if the screensize has changed */
+ sendResizeEventIfNecessary(false, null);
+ }
+
+ /** Implementation of LayerView.Listener */
+ @Override
+ public void surfaceChanged(int width, int height) {
+ IntSize viewportSize = mToolbarAnimator.getViewportSize();
+ setViewportSize(viewportSize.width, viewportSize.height, null);
+ }
+
+ ImmutableViewportMetrics getViewportMetrics() {
+ return mViewportMetrics;
+ }
+
+ /*
+ * 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.viewportRectWidth, mViewportMetrics.viewportRectHeight);
+ mViewportMetrics = metrics;
+
+ viewportMetricsChanged(notifyGecko);
+ }
+
+ /*
+ * You must hold the monitor while calling this.
+ */
+ private void viewportMetricsChanged(boolean notifyGecko) {
+ mToolbarAnimator.onMetricsChanged(mViewportMetrics);
+
+ mView.requestRender();
+ if (notifyGecko && mGeckoIsReady) {
+ geometryChanged();
+ }
+ }
+
+ /*
+ * 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 */
+ @Override
+ public void panZoomStopped() {
+ mToolbarAnimator.onPanZoomStopped();
+ }
+
+ Object getLock() {
+ return this;
+ }
+
+ Matrix getMatrixForLayerRectToViewRect() {
+ if (!mGeckoIsReady) {
+ return null;
+ }
+
+ ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
+ PointF origin = viewportMetrics.getOrigin();
+ float zoom = viewportMetrics.zoomFactor;
+ ImmutableViewportMetrics geckoViewport = mViewportMetrics;
+ PointF geckoOrigin = geckoViewport.getOrigin();
+ float geckoZoom = geckoViewport.zoomFactor;
+
+ Matrix matrix = new Matrix();
+ matrix.postTranslate(geckoOrigin.x / geckoZoom, geckoOrigin.y / geckoZoom);
+ matrix.postScale(zoom, zoom);
+ matrix.postTranslate(-origin.x, -origin.y);
+ return matrix;
+ }
+
+ @Override
+ public void setScrollingRootContent(boolean isRootContent) {
+ mToolbarAnimator.setScrollingRootContent(isRootContent);
+ }
+
+ public void addDrawListener(DrawListener listener) {
+ mDrawListeners.add(listener);
+ }
+
+ public void removeDrawListener(DrawListener listener) {
+ mDrawListeners.remove(listener);
+ }
+
+ public void setClearColor(int color) {
+ mClearColor = color;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java
new file mode 100644
index 000000000..8072deecc
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java
@@ -0,0 +1,282 @@
+/* -*- 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.annotation.WrapForJNI;
+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 int viewportRectWidth;
+ public final int viewportRectHeight;
+
+ public final float zoomFactor;
+
+ public ImmutableViewportMetrics(DisplayMetrics metrics) {
+ viewportRectLeft = pageRectLeft = cssPageRectLeft = 0;
+ viewportRectTop = pageRectTop = cssPageRectTop = 0;
+ viewportRectWidth = metrics.widthPixels;
+ viewportRectHeight = metrics.heightPixels;
+ pageRectRight = cssPageRectRight = metrics.widthPixels;
+ pageRectBottom = cssPageRectBottom = metrics.heightPixels;
+ zoomFactor = 1.0f;
+ }
+
+ /** This constructor is used by native code in AndroidJavaWrappers.cpp, be
+ * careful when modifying the signature.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ private ImmutableViewportMetrics(float aPageRectLeft, float aPageRectTop,
+ float aPageRectRight, float aPageRectBottom, float aCssPageRectLeft,
+ float aCssPageRectTop, float aCssPageRectRight, float aCssPageRectBottom,
+ float aViewportRectLeft, float aViewportRectTop, int aViewportRectWidth,
+ int aViewportRectHeight, float aZoomFactor)
+ {
+ pageRectLeft = aPageRectLeft;
+ pageRectTop = aPageRectTop;
+ pageRectRight = aPageRectRight;
+ pageRectBottom = aPageRectBottom;
+ cssPageRectLeft = aCssPageRectLeft;
+ cssPageRectTop = aCssPageRectTop;
+ cssPageRectRight = aCssPageRectRight;
+ cssPageRectBottom = aCssPageRectBottom;
+ viewportRectLeft = aViewportRectLeft;
+ viewportRectTop = aViewportRectTop;
+ viewportRectWidth = aViewportRectWidth;
+ viewportRectHeight = aViewportRectHeight;
+ zoomFactor = aZoomFactor;
+ }
+
+ public float getWidth() {
+ return viewportRectWidth;
+ }
+
+ public float getHeight() {
+ return viewportRectHeight;
+ }
+
+ public float viewportRectRight() {
+ return viewportRectLeft + viewportRectWidth;
+ }
+
+ public float viewportRectBottom() {
+ return viewportRectTop + viewportRectHeight;
+ }
+
+ public PointF getOrigin() {
+ return new PointF(viewportRectLeft, viewportRectTop);
+ }
+
+ public FloatSize getSize() {
+ return new FloatSize(viewportRectWidth, viewportRectHeight);
+ }
+
+ 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 getPageHeight() {
+ return pageRectBottom - pageRectTop;
+ }
+
+ 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),
+ (int)FloatUtils.interpolate(viewportRectWidth, to.viewportRectWidth, t),
+ (int)FloatUtils.interpolate(viewportRectHeight, to.viewportRectHeight, t),
+ FloatUtils.interpolate(zoomFactor, to.zoomFactor, t));
+ }
+
+ public ImmutableViewportMetrics setViewportSize(int width, int height) {
+ if (width == viewportRectWidth && height == viewportRectHeight) {
+ return this;
+ }
+
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ viewportRectLeft, viewportRectTop, width, height,
+ zoomFactor);
+ }
+
+ public ImmutableViewportMetrics setViewportOrigin(float newOriginX, float newOriginY) {
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ newOriginX, newOriginY, viewportRectWidth, viewportRectHeight,
+ zoomFactor);
+ }
+
+ public ImmutableViewportMetrics setZoomFactor(float newZoomFactor) {
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ viewportRectLeft, viewportRectTop, viewportRectWidth, viewportRectHeight,
+ newZoomFactor);
+ }
+
+ public ImmutableViewportMetrics offsetViewportBy(float dx, float dy) {
+ return setViewportOrigin(viewportRectLeft + dx, viewportRectTop + dy);
+ }
+
+ public ImmutableViewportMetrics offsetViewportByAndClamp(float dx, float dy) {
+ return setViewportOrigin(
+ Math.max(pageRectLeft, Math.min(viewportRectLeft + dx, pageRectRight - getWidth())),
+ Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeight())));
+ }
+
+ 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, viewportRectWidth, viewportRectHeight,
+ zoomFactor);
+ }
+
+ public ImmutableViewportMetrics setPageRectFrom(ImmutableViewportMetrics aMetrics) {
+ if (aMetrics.cssPageRectLeft == cssPageRectLeft &&
+ aMetrics.cssPageRectTop == cssPageRectTop &&
+ aMetrics.cssPageRectRight == cssPageRectRight &&
+ aMetrics.cssPageRectBottom == cssPageRectBottom) {
+ return this;
+ }
+ RectF css = aMetrics.getCssPageRect();
+ return setPageRect(RectUtils.scale(css, zoomFactor), css);
+ }
+
+ /* 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, viewportRectWidth, viewportRectHeight,
+ newZoomFactor);
+ }
+
+ /** Clamps the viewport to remain within the page rect. */
+ public ImmutableViewportMetrics clamp() {
+ RectF newViewport = getViewport();
+
+ // The viewport bounds ought to never exceed the page bounds.
+ if (newViewport.right > pageRectRight)
+ newViewport.offset((pageRectRight) - newViewport.right, 0);
+ if (newViewport.left < pageRectLeft)
+ newViewport.offset(pageRectLeft - newViewport.left, 0);
+
+ if (newViewport.bottom > pageRectBottom)
+ newViewport.offset(0, (pageRectBottom) - newViewport.bottom);
+ if (newViewport.top < pageRectTop)
+ newViewport.offset(0, pageRectTop - newViewport.top);
+
+ // Note that since newViewport is only translated around, the viewport's
+ // width and height are unchanged.
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ newViewport.left, newViewport.top, viewportRectWidth, viewportRectHeight,
+ zoomFactor);
+ }
+
+ 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.
+ 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)
+ && viewportRectWidth == other.viewportRectWidth
+ && viewportRectHeight == other.viewportRectHeight
+ && FloatUtils.fuzzyEquals(zoomFactor, other.zoomFactor);
+ }
+
+ @Override
+ public String toString() {
+ return "ImmutableViewportMetrics v=(" + viewportRectLeft + "," + viewportRectTop + ","
+ + viewportRectWidth + "x" + viewportRectHeight + ") p=(" + pageRectLeft + ","
+ + pageRectTop + "," + pageRectRight + "," + pageRectBottom + ") c=("
+ + cssPageRectLeft + "," + cssPageRectTop + "," + cssPageRectRight + ","
+ + cssPageRectBottom + ") z=" + zoomFactor;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/IntSize.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/IntSize.java
new file mode 100644
index 000000000..0e847158d
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/IntSize.java
@@ -0,0 +1,89 @@
+/* -*- 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;
+
+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) Math.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/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerRenderer.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerRenderer.java
new file mode 100644
index 000000000..1a087cc2a
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerRenderer.java
@@ -0,0 +1,275 @@
+/* -*- 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.AppConstants;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.mozglue.DirectBufferAllocator;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+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.annotation.WrapForJNI;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.microedition.khronos.egl.EGLConfig;
+
+/**
+ * The layer renderer implements the rendering logic for a layer view.
+ */
+public class LayerRenderer {
+ private static final String LOGTAG = "GeckoLayerRenderer";
+
+ /*
+ * 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 long NANOS_PER_MS = 1000000;
+ private static final int MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER = 5;
+
+ private final LayerView mView;
+ private ByteBuffer mCoordByteBuffer;
+ private FloatBuffer mCoordBuffer;
+ private int mMaxTextureSize;
+
+ private long mLastFrameTime;
+ private final CopyOnWriteArrayList<RenderTask> mTasks;
+
+ // Dropped frames display
+ private final int[] mFrameTimings;
+ private int mCurrentFrame, mFrameTimingsSum, mDroppedFrames;
+
+ private IntBuffer mPixelBuffer;
+ private List<LayerView.ZoomedViewListener> mZoomedViewListeners;
+ private float mLastViewLeft;
+ private float mLastViewTop;
+
+ public LayerRenderer(LayerView view) {
+ mView = view;
+
+ mTasks = new CopyOnWriteArrayList<RenderTask>();
+ mLastFrameTime = System.nanoTime();
+
+ mFrameTimings = new int[60];
+ mCurrentFrame = mFrameTimingsSum = mDroppedFrames = 0;
+
+ mZoomedViewListeners = new ArrayList<LayerView.ZoomedViewListener>();
+ }
+
+ public void destroy() {
+ if (mCoordByteBuffer != null) {
+ DirectBufferAllocator.free(mCoordByteBuffer);
+ mCoordByteBuffer = null;
+ mCoordBuffer = null;
+ }
+ mZoomedViewListeners.clear();
+ }
+
+ void onSurfaceCreated(EGLConfig config) {
+ createDefaultProgram();
+ }
+
+ public void createDefaultProgram() {
+ int maxTextureSizeResult[] = new int[1];
+ GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxTextureSizeResult, 0);
+ mMaxTextureSize = maxTextureSizeResult[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);
+ }
+ }
+ }
+
+ /** 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 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;
+ }
+
+ public Frame createFrame(ImmutableViewportMetrics metrics) {
+ return new Frame(metrics);
+ }
+
+ 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 final ImmutableViewportMetrics mFrameMetrics;
+
+ public Frame(ImmutableViewportMetrics metrics) {
+ mFrameMetrics = metrics;
+ }
+
+ /** This function is invoked via JNI; be careful when modifying signature. */
+ @WrapForJNI
+ public void beginDrawing() {
+ mFrameStartTime = System.nanoTime();
+
+ // Run through pre-render tasks
+ runRenderTasks(mTasks, false, mFrameStartTime);
+ }
+
+
+ private void maybeRequestZoomedViewRender() {
+ // Concurrently update of mZoomedViewListeners should not be an issue here
+ // because the following line is just a short-circuit
+ if (mZoomedViewListeners.size() == 0) {
+ return;
+ }
+
+ // When scrolling fast, do not request zoomed view render to avoid to slow down
+ // the scroll in the main view.
+ // Speed is estimated using the offset changes between 2 display frame calls
+ final float viewLeft = Math.round(mFrameMetrics.getViewport().left);
+ final float viewTop = Math.round(mFrameMetrics.getViewport().top);
+ boolean shouldWaitToRender = false;
+
+ if (Math.abs(mLastViewLeft - viewLeft) > MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER ||
+ Math.abs(mLastViewTop - viewTop) > MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER) {
+ shouldWaitToRender = true;
+ }
+
+ mLastViewLeft = viewLeft;
+ mLastViewTop = viewTop;
+
+ if (shouldWaitToRender) {
+ return;
+ }
+
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ for (LayerView.ZoomedViewListener listener : mZoomedViewListeners) {
+ listener.requestZoomedViewRender();
+ }
+ }
+ });
+ }
+
+
+ /** This function is invoked via JNI; be careful when modifying signature. */
+ @WrapForJNI
+ public void endDrawing() {
+ PanningPerfAPI.recordFrameTime();
+
+ runRenderTasks(mTasks, true, mFrameStartTime);
+ maybeRequestZoomedViewRender();
+
+ /* Used by robocop for testing purposes */
+ IntBuffer pixelBuffer = mPixelBuffer;
+ if (pixelBuffer != null) {
+ synchronized (pixelBuffer) {
+ pixelBuffer.position(0);
+ GLES20.glReadPixels(0, 0, Math.round(mFrameMetrics.getWidth()),
+ Math.round(mFrameMetrics.getHeight()), 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.setSurfaceBackgroundColor(Color.TRANSPARENT);
+ }
+ });
+ mView.setPaintState(LayerView.PAINT_AFTER_FIRST);
+ }
+ mLastFrameTime = mFrameStartTime;
+ }
+ }
+
+ public void updateZoomedView(final ByteBuffer data) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ for (LayerView.ZoomedViewListener listener : mZoomedViewListeners) {
+ data.position(0);
+ listener.updateView(data);
+ }
+ }
+ });
+ }
+
+ public void addZoomedViewListener(LayerView.ZoomedViewListener listener) {
+ ThreadUtils.assertOnUiThread();
+ mZoomedViewListeners.add(listener);
+ }
+
+ public void removeZoomedViewListener(LayerView.ZoomedViewListener listener) {
+ ThreadUtils.assertOnUiThread();
+ mZoomedViewListeners.remove(listener);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java
new file mode 100644
index 000000000..969aa3f24
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java
@@ -0,0 +1,711 @@
+/* -*- 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;
+import java.nio.IntBuffer;
+
+import org.mozilla.gecko.AndroidGamepadManager;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.GeckoAccessibility;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+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.InputDevice;
+import android.widget.FrameLayout;
+
+/**
+ * A view rendered by the layer compositor.
+ */
+public class LayerView extends FrameLayout {
+ private static final String LOGTAG = "GeckoLayerView";
+
+ private GeckoLayerClient mLayerClient;
+ private PanZoomController mPanZoomController;
+ private DynamicToolbarAnimator mToolbarAnimator;
+ private LayerRenderer mRenderer;
+ /* Must be a PAINT_xxx constant */
+ private int mPaintState;
+ private FullScreenState mFullScreenState;
+
+ private SurfaceView mSurfaceView;
+ private TextureView mTextureView;
+
+ private Listener mListener;
+
+ /* This should only be modified on the Java UI thread. */
+ private final Overscroll mOverscroll;
+
+ private boolean mServerSurfaceValid;
+ private int mWidth, mHeight;
+
+ private boolean onAttachedToWindowCalled;
+
+ /* This is written by the Gecko thread and the UI thread, and read by the UI thread. */
+ @WrapForJNI(stubName = "CompositorCreated", calledFrom = "ui")
+ /* package */ volatile boolean mCompositorCreated;
+
+ private class Compositor extends JNIObject {
+ public Compositor() {
+ }
+
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ @Override protected native void disposeNative();
+
+ // Gecko thread sets its Java instances; does not block UI thread.
+ @WrapForJNI(calledFrom = "any", dispatchTo = "gecko")
+ /* package */ native void attachToJava(GeckoLayerClient layerClient,
+ NativePanZoomController npzc);
+
+ @WrapForJNI(calledFrom = "any", dispatchTo = "gecko")
+ /* package */ native void onSizeChanged(int windowWidth, int windowHeight,
+ int screenWidth, int screenHeight);
+
+ // Gecko thread creates compositor; blocks UI thread.
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "proxy")
+ /* package */ native void createCompositor(int width, int height, Object surface);
+
+ // Gecko thread pauses compositor; blocks UI thread.
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "current")
+ /* package */ native void syncPauseCompositor();
+
+ // UI thread resumes compositor and notifies Gecko thread; does not block UI thread.
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "current")
+ /* package */ native void syncResumeResizeCompositor(int width, int height, Object surface);
+
+ @WrapForJNI(calledFrom = "any", dispatchTo = "current")
+ /* package */ native void syncInvalidateAndScheduleComposite();
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void reattach() {
+ mCompositorCreated = true;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void destroy() {
+ // The nsWindow has been closed. First mark our compositor as destroyed.
+ LayerView.this.mCompositorCreated = false;
+
+ LayerView.this.mLayerClient.setGeckoReady(false);
+
+ // Then clear out any pending calls on the UI thread by disposing on the UI thread.
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ disposeNative();
+ }
+ });
+ }
+ }
+
+ private final Compositor mCompositor = new Compositor();
+
+ /* 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);
+
+ mPaintState = PAINT_START;
+ mFullScreenState = FullScreenState.NONE;
+
+ mOverscroll = new OverscrollEdgeEffect(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();
+ mToolbarAnimator = mLayerClient.getDynamicToolbarAnimator();
+
+ mRenderer = new LayerRenderer(this);
+
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+
+ GeckoAccessibility.setDelegate(this);
+ }
+
+ /**
+ * MotionEventHelper dragAsync() robocop tests can instruct
+ * PanZoomController not to generate longpress events.
+ */
+ public void setIsLongpressEnabled(boolean isLongpressEnabled) {
+ mPanZoomController.setIsLongpressEnabled(isLongpressEnabled);
+ }
+
+ private static Point getEventRadius(MotionEvent event) {
+ return new Point((int)event.getToolMajor() / 2,
+ (int)event.getToolMinor() / 2);
+ }
+
+ 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();
+ }
+ }
+
+ @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 (mToolbarAnimator != null && mToolbarAnimator.onInterceptTouchEvent(event)) {
+ if (mPanZoomController != null) {
+ mPanZoomController.onMotionEventVelocity(event.getEventTime(), mToolbarAnimator.getVelocity());
+ }
+ return true;
+ }
+ if (!mLayerClient.isGeckoReady()) {
+ // If gecko isn't loaded yet, don't try sending events to the
+ // native code because it's just going to crash
+ return true;
+ }
+ if (mPanZoomController != null && mPanZoomController.onTouchEvent(event)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ // If we get a touchscreen hover event, and accessibility is not enabled,
+ // don't send it to gecko.
+ if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN &&
+ !GeckoAccessibility.isEnabled()) {
+ return false;
+ }
+
+ if (!mLayerClient.isGeckoReady()) {
+ // If gecko isn't loaded yet, don't try sending events to the
+ // native code because it's just going to crash
+ return true;
+ } else if (mPanZoomController != null && mPanZoomController.onMotionEvent(event)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ if (AndroidGamepadManager.handleMotionEvent(event)) {
+ return true;
+ }
+ if (!mLayerClient.isGeckoReady()) {
+ // If gecko isn't loaded yet, don't try sending events to the
+ // native code because it's just going to crash
+ return true;
+ }
+ if (mPanZoomController != null && mPanZoomController.onMotionEvent(event)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(final Parcelable state) {
+ if (onAttachedToWindowCalled) {
+ attachCompositor();
+ }
+ super.onRestoreInstanceState(state);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ // We are adding descendants to this LayerView, but we don't want the
+ // descendants to affect the way LayerView retains its focus.
+ setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
+
+ // 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, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+
+ SurfaceHolder holder = mSurfaceView.getHolder();
+ holder.addCallback(new SurfaceListener());
+ }
+
+ attachCompositor();
+
+ onAttachedToWindowCalled = true;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ onAttachedToWindowCalled = false;
+ }
+
+ // Don't expose GeckoLayerClient to things outside this package; only expose it as an Object
+ GeckoLayerClient getLayerClient() { return mLayerClient; }
+
+ public PanZoomController getPanZoomController() { return mPanZoomController; }
+ public DynamicToolbarAnimator getDynamicToolbarAnimator() { return mToolbarAnimator; }
+
+ public ImmutableViewportMetrics getViewportMetrics() {
+ return mLayerClient.getViewportMetrics();
+ }
+
+ public Matrix getMatrixForLayerRectToViewRect() {
+ return mLayerClient.getMatrixForLayerRectToViewRect();
+ }
+
+ public void setSurfaceBackgroundColor(int newColor) {
+ if (mSurfaceView != null) {
+ mSurfaceView.setBackgroundColor(newColor);
+ }
+ }
+
+ public void requestRender() {
+ if (mCompositorCreated) {
+ mCompositor.syncInvalidateAndScheduleComposite();
+ }
+ }
+
+ 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;
+ }
+
+ private void attachCompositor() {
+ final NativePanZoomController npzc = (NativePanZoomController) mPanZoomController;
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ mCompositor.attachToJava(mLayerClient, npzc);
+ } else {
+ GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
+ mCompositor, "attachToJava",
+ GeckoLayerClient.class, mLayerClient,
+ NativePanZoomController.class, npzc);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "ui")
+ protected Object getCompositor() {
+ return mCompositor;
+ }
+
+ void serverSurfaceChanged(int newWidth, int newHeight) {
+ ThreadUtils.assertOnUiThread();
+
+ mWidth = newWidth;
+ mHeight = newHeight;
+ mServerSurfaceValid = true;
+
+ updateCompositor();
+ }
+
+ void updateCompositor() {
+ ThreadUtils.assertOnUiThread();
+
+ 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).
+ if (!mServerSurfaceValid) {
+ return;
+ }
+ // 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.
+ mCompositor.syncResumeResizeCompositor(mWidth, mHeight, getSurface());
+ 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 anywhere.
+ if (mServerSurfaceValid && getLayerClient().isGeckoReady()) {
+ mCompositorCreated = true;
+ mCompositor.createCompositor(mWidth, mHeight, getSurface());
+ }
+ }
+
+ /* 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 (!mServerSurfaceValid || mSurfaceView == null) {
+ surfaceChanged(width, height);
+ return;
+ }
+
+ if (mCompositorCreated) {
+ mCompositor.syncResumeResizeCompositor(width, height, getSurface());
+ }
+
+ if (mOverscroll != null) {
+ mOverscroll.setSize(width, height);
+ }
+ }
+
+ private void surfaceChanged(int width, int height) {
+ serverSurfaceChanged(width, height);
+
+ if (mListener != null) {
+ mListener.surfaceChanged(width, height);
+ }
+
+ if (mOverscroll != null) {
+ mOverscroll.setSize(width, height);
+ }
+ }
+
+ void notifySizeChanged(int windowWidth, int windowHeight, int screenWidth, int screenHeight) {
+ mCompositor.onSizeChanged(windowWidth, windowHeight, screenWidth, screenHeight);
+ }
+
+ void serverSurfaceDestroyed() {
+ ThreadUtils.assertOnUiThread();
+
+ // 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) {
+ mCompositor.syncPauseCompositor();
+ }
+
+ mServerSurfaceValid = false;
+ }
+
+ private void onDestroyed() {
+ serverSurfaceDestroyed();
+ }
+
+ public Object getNativeWindow() {
+ if (mSurfaceView != null)
+ return mSurfaceView.getHolder();
+
+ return mTextureView.getSurfaceTexture();
+ }
+
+ public Object getSurface() {
+ if (mSurfaceView != null) {
+ return mSurfaceView.getHolder().getSurface();
+ }
+ return null;
+ }
+
+ // This method is called on the Gecko main thread.
+ @WrapForJNI(calledFrom = "gecko")
+ private static void updateZoomedView(ByteBuffer data) {
+ LayerView layerView = GeckoAppShell.getLayerView();
+ if (layerView != null) {
+ LayerRenderer layerRenderer = layerView.getRenderer();
+ if (layerRenderer != null) {
+ layerRenderer.updateZoomedView(data);
+ }
+ }
+ }
+
+ public interface Listener {
+ 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 {
+ private 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) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (changed && mParent.mServerSurfaceValid) {
+ 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) {
+
+ }
+ }
+
+ @RobocopTarget
+ public void addDrawListener(DrawListener listener) {
+ mLayerClient.addDrawListener(listener);
+ }
+
+ @RobocopTarget
+ public void removeDrawListener(DrawListener listener) {
+ mLayerClient.removeDrawListener(listener);
+ }
+
+ @RobocopTarget
+ public static interface DrawListener {
+ public void drawFinished();
+ }
+
+ @Override
+ public void setOverScrollMode(int overscrollMode) {
+ super.setOverScrollMode(overscrollMode);
+ }
+
+ @Override
+ public int getOverScrollMode() {
+ return super.getOverScrollMode();
+ }
+
+ public float getZoomFactor() {
+ return getLayerClient().getViewportMetrics().zoomFactor;
+ }
+
+ @Override
+ public void onFocusChanged (boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+ GeckoAccessibility.onLayerViewFocusChanged(gainFocus);
+ }
+
+ public void setFullScreenState(FullScreenState state) {
+ mFullScreenState = state;
+ }
+
+ public boolean isFullScreen() {
+ return mFullScreenState != FullScreenState.NONE;
+ }
+
+ public void setMaxTranslation(float aMaxTranslation) {
+ mToolbarAnimator.setMaxTranslation(aMaxTranslation);
+ }
+
+ public void setSurfaceTranslation(float translation) {
+ setTranslationY(translation);
+ }
+
+ public float getSurfaceTranslation() {
+ return getTranslationY();
+ }
+
+ // Public hooks for dynamic toolbar translation
+
+ public interface DynamicToolbarListener {
+ public void onTranslationChanged(float aToolbarTranslation, float aLayerViewTranslation);
+ public void onPanZoomStopped();
+ public void onMetricsChanged(ImmutableViewportMetrics viewport);
+ }
+
+ // Public hooks for zoomed view
+
+ public interface ZoomedViewListener {
+ public void requestZoomedViewRender();
+ public void updateView(ByteBuffer data);
+ }
+
+ public void addZoomedViewListener(ZoomedViewListener listener) {
+ mRenderer.addZoomedViewListener(listener);
+ }
+
+ public void removeZoomedViewListener(ZoomedViewListener listener) {
+ mRenderer.removeZoomedViewListener(listener);
+ }
+
+ public void setClearColor(int color) {
+ if (mLayerClient != null) {
+ mLayerClient.setClearColor(color);
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java
new file mode 100644
index 000000000..6b643c85b
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java
@@ -0,0 +1,300 @@
+/* -*- 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.GeckoThread;
+import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import org.json.JSONObject;
+
+import android.graphics.PointF;
+import android.util.TypedValue;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.InputDevice;
+
+class NativePanZoomController extends JNIObject implements PanZoomController {
+ private final PanZoomTarget mTarget;
+ private final LayerView mView;
+ private boolean mDestroyed;
+ private Overscroll mOverscroll;
+ boolean mNegateWheelScroll;
+ private float mPointerScrollFactor;
+ private PrefsHelper.PrefHandler mPrefsObserver;
+ private long mLastDownTime;
+ private static final float MAX_SCROLL = 0.075f * GeckoAppShell.getDpi();
+
+ @WrapForJNI(calledFrom = "ui")
+ private native boolean handleMotionEvent(
+ int action, int actionIndex, long time, int metaState,
+ int pointerId[], float x[], float y[], float orientation[], float pressure[],
+ float toolMajor[], float toolMinor[]);
+
+ @WrapForJNI(calledFrom = "ui")
+ private native boolean handleScrollEvent(
+ long time, int metaState,
+ float x, float y,
+ float hScroll, float vScroll);
+
+ @WrapForJNI(calledFrom = "ui")
+ private native boolean handleMouseEvent(
+ int action, long time, int metaState,
+ float x, float y, int buttons);
+
+ @WrapForJNI(calledFrom = "ui")
+ private native void handleMotionEventVelocity(long time, float ySpeed);
+
+ private boolean handleMotionEvent(MotionEvent event) {
+ if (mDestroyed) {
+ return false;
+ }
+
+ final int action = event.getActionMasked();
+ final int count = event.getPointerCount();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mLastDownTime = event.getDownTime();
+ } else if (mLastDownTime != event.getDownTime()) {
+ return false;
+ }
+
+ final int[] pointerId = new int[count];
+ final float[] x = new float[count];
+ final float[] y = new float[count];
+ final float[] orientation = new float[count];
+ final float[] pressure = new float[count];
+ final float[] toolMajor = new float[count];
+ final float[] toolMinor = new float[count];
+
+ final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+
+ for (int i = 0; i < count; i++) {
+ pointerId[i] = event.getPointerId(i);
+ event.getPointerCoords(i, coords);
+
+ x[i] = coords.x;
+ y[i] = coords.y;
+
+ orientation[i] = coords.orientation;
+ pressure[i] = coords.pressure;
+
+ // If we are converting to CSS pixels, we should adjust the radii as well.
+ toolMajor[i] = coords.toolMajor;
+ toolMinor[i] = coords.toolMinor;
+ }
+
+ return handleMotionEvent(action, event.getActionIndex(), event.getEventTime(),
+ event.getMetaState(), pointerId, x, y, orientation, pressure,
+ toolMajor, toolMinor);
+ }
+
+ private boolean handleScrollEvent(MotionEvent event) {
+ if (mDestroyed) {
+ return false;
+ }
+
+ final int count = event.getPointerCount();
+
+ if (count <= 0) {
+ return false;
+ }
+
+ final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+ event.getPointerCoords(0, coords);
+ final float x = coords.x;
+ final float y = coords.y;
+
+ final float flipFactor = mNegateWheelScroll ? -1.0f : 1.0f;
+ final float hScroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL) * flipFactor * mPointerScrollFactor;
+ final float vScroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL) * flipFactor * mPointerScrollFactor;
+
+ return handleScrollEvent(event.getEventTime(), event.getMetaState(), x, y, hScroll, vScroll);
+ }
+
+ private boolean handleMouseEvent(MotionEvent event) {
+ if (mDestroyed) {
+ return false;
+ }
+
+ final int count = event.getPointerCount();
+
+ if (count <= 0) {
+ return false;
+ }
+
+ final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+ event.getPointerCoords(0, coords);
+ final float x = coords.x;
+ final float y = coords.y;
+
+ return handleMouseEvent(event.getActionMasked(), event.getEventTime(), event.getMetaState(), x, y, event.getButtonState());
+ }
+
+
+ NativePanZoomController(PanZoomTarget target, View view) {
+ mTarget = target;
+ mView = (LayerView) view;
+ mDestroyed = true;
+
+ String[] prefs = { "ui.scrolling.negate_wheel_scroll" };
+ mPrefsObserver = new PrefsHelper.PrefHandlerBase() {
+ @Override public void prefValue(String pref, boolean value) {
+ if (pref.equals("ui.scrolling.negate_wheel_scroll")) {
+ mNegateWheelScroll = value;
+ }
+ }
+ };
+ PrefsHelper.addObserver(prefs, mPrefsObserver);
+
+ TypedValue outValue = new TypedValue();
+ if (view.getContext().getTheme().resolveAttribute(android.R.attr.listPreferredItemHeight, outValue, true)) {
+ mPointerScrollFactor = outValue.getDimension(view.getContext().getResources().getDisplayMetrics());
+ } else {
+ mPointerScrollFactor = MAX_SCROLL;
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+// NOTE: This commented out block of code allows Fennec to generate
+// mouse event instead of converting them to touch events.
+// This gives Fennec similar behaviour to desktop when using
+// a mouse.
+//
+// if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
+// return handleMouseEvent(event);
+// } else {
+// return handleMotionEvent(event);
+// }
+ return handleMotionEvent(event);
+ }
+
+ @Override
+ public boolean onMotionEvent(MotionEvent event) {
+ final int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_SCROLL) {
+ if (event.getDownTime() >= mLastDownTime) {
+ mLastDownTime = event.getDownTime();
+ } else if ((InputDevice.getDevice(event.getDeviceId()).getSources() & InputDevice.SOURCE_TOUCHPAD) == InputDevice.SOURCE_TOUCHPAD) {
+ return false;
+ }
+ return handleScrollEvent(event);
+ } else if ((action == MotionEvent.ACTION_HOVER_MOVE) ||
+ (action == MotionEvent.ACTION_HOVER_ENTER) ||
+ (action == MotionEvent.ACTION_HOVER_EXIT)) {
+ return handleMouseEvent(event);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onMotionEventVelocity(final long aEventTime, final float aSpeedY) {
+ handleMotionEventVelocity(aEventTime, aSpeedY);
+ }
+
+ @Override @WrapForJNI(calledFrom = "ui") // PanZoomController
+ public void destroy() {
+ if (mPrefsObserver != null) {
+ PrefsHelper.removeObserver(mPrefsObserver);
+ mPrefsObserver = null;
+ }
+ if (mDestroyed || !mTarget.isGeckoReady()) {
+ return;
+ }
+ mDestroyed = true;
+ disposeNative();
+ }
+
+ @Override
+ public void attach() {
+ mDestroyed = false;
+ }
+
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko") @Override // JNIObject
+ protected native void disposeNative();
+
+ @Override
+ public void setOverscrollHandler(final Overscroll handler) {
+ mOverscroll = handler;
+ }
+
+ @WrapForJNI(stubName = "SetIsLongpressEnabled") // Called from test thread.
+ private native void nativeSetIsLongpressEnabled(boolean isLongpressEnabled);
+
+ @Override // PanZoomController
+ public void setIsLongpressEnabled(boolean isLongpressEnabled) {
+ if (!mDestroyed) {
+ nativeSetIsLongpressEnabled(isLongpressEnabled);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "ui")
+ private native void adjustScrollForSurfaceShift(float aX, float aY);
+
+ @Override // PanZoomController
+ public ImmutableViewportMetrics adjustScrollForSurfaceShift(ImmutableViewportMetrics aMetrics, PointF aShift) {
+ adjustScrollForSurfaceShift(aShift.x, aShift.y);
+ return aMetrics.offsetViewportByAndClamp(aShift.x, aShift.y);
+ }
+
+ @WrapForJNI
+ private void updateOverscrollVelocity(final float x, final float y) {
+ if (mOverscroll != null) {
+ if (ThreadUtils.isOnUiThread() == true) {
+ mOverscroll.setVelocity(x * 1000.0f, Overscroll.Axis.X);
+ mOverscroll.setVelocity(y * 1000.0f, Overscroll.Axis.Y);
+ } else {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // Multiply the velocity by 1000 to match what was done in JPZ.
+ mOverscroll.setVelocity(x * 1000.0f, Overscroll.Axis.X);
+ mOverscroll.setVelocity(y * 1000.0f, Overscroll.Axis.Y);
+ }
+ });
+ }
+ }
+ }
+
+ @WrapForJNI
+ private void updateOverscrollOffset(final float x, final float y) {
+ if (mOverscroll != null) {
+ if (ThreadUtils.isOnUiThread() == true) {
+ mOverscroll.setDistance(x, Overscroll.Axis.X);
+ mOverscroll.setDistance(y, Overscroll.Axis.Y);
+ } else {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mOverscroll.setDistance(x, Overscroll.Axis.X);
+ mOverscroll.setDistance(y, Overscroll.Axis.Y);
+ }
+ });
+ }
+ }
+ }
+
+ @WrapForJNI(calledFrom = "ui")
+ private void setScrollingRootContent(final boolean isRootContent) {
+ mTarget.setScrollingRootContent(isRootContent);
+ }
+
+ /**
+ * Active SelectionCaretDrag requires DynamicToolbarAnimator to be pinned
+ * to avoid unwanted scroll interactions.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ private void onSelectionDragState(boolean state) {
+ mView.getDynamicToolbarAnimator().setPinned(state, PinReason.CARET_DRAG);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/Overscroll.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/Overscroll.java
new file mode 100644
index 000000000..e442444d5
--- /dev/null
+++ b/mobile/android/geckoview/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/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java
new file mode 100644
index 000000000..85e04d9f2
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java
@@ -0,0 +1,162 @@
+/* -*- 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.AppConstants.Versions;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.widget.EdgeEffect;
+
+import java.lang.reflect.Field;
+
+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 LayerView mView;
+
+ public OverscrollEdgeEffect(final LayerView v) {
+ Field paintField = null;
+ if (Versions.feature21Plus) {
+ try {
+ paintField = EdgeEffect.class.getDeclaredField("mPaint");
+ paintField.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ }
+ }
+
+ mView = v;
+ Context context = v.getContext();
+ for (int i = 0; i < 4; i++) {
+ mEdges[i] = new EdgeEffect(context);
+
+ try {
+ if (paintField != null) {
+ final Paint p = (Paint) paintField.get(mEdges[i]);
+
+ // The Android EdgeEffect class uses a mode of SRC_ATOP here, which means it will only
+ // draw the effect where there are non-transparent pixels in the destination. Since the LayerView
+ // itself is fully transparent, it doesn't display at all. We need to use SRC instead.
+ p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+ }
+ } catch (IllegalAccessException e) {
+ }
+ }
+ }
+
+ @Override
+ 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 (Versions.feature16Plus) {
+ mView.postInvalidateOnAnimation();
+ } else {
+ mView.postInvalidateDelayed(10);
+ }
+ }
+
+ @Override
+ 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();
+ }
+
+ @Override
+ 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();
+ }
+
+ @Override
+ public void draw(final Canvas canvas, final ImmutableViewportMetrics metrics) {
+ if (metrics == null) {
+ return;
+ }
+
+ PointF visibleEnd = mView.getDynamicToolbarAnimator().getVisibleEndOfLayerView();
+
+ // If we're pulling an edge, or fading it out, draw!
+ boolean invalidate = false;
+ if (!mEdges[TOP].isFinished()) {
+ invalidate |= draw(mEdges[TOP], canvas, 0, 0, 0);
+ }
+
+ if (!mEdges[BOTTOM].isFinished()) {
+ invalidate |= draw(mEdges[BOTTOM], canvas, visibleEnd.x, visibleEnd.y, 180);
+ }
+
+ if (!mEdges[LEFT].isFinished()) {
+ invalidate |= draw(mEdges[LEFT], canvas, 0, visibleEnd.y, 270);
+ }
+
+ if (!mEdges[RIGHT].isFinished()) {
+ invalidate |= draw(mEdges[RIGHT], canvas, visibleEnd.x, 0, 90);
+ }
+
+ // If the edge effect is animating off screen, invalidate.
+ if (invalidate) {
+ invalidate();
+ }
+ }
+
+ private static 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/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java
new file mode 100644
index 000000000..fbd07c69b
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java
@@ -0,0 +1,38 @@
+/* -*- 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.EventDispatcher;
+
+import android.graphics.PointF;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+public interface PanZoomController {
+ // Threshold for sending touch move events to content
+ public static final float CLICK_THRESHOLD = 1 / 50f * GeckoAppShell.getDpi();
+
+ static class Factory {
+ static PanZoomController create(PanZoomTarget target, View view, EventDispatcher dispatcher) {
+ return new NativePanZoomController(target, view);
+ }
+ }
+
+ public void destroy();
+ public void attach();
+
+ public boolean onTouchEvent(MotionEvent event);
+ public boolean onMotionEvent(MotionEvent event);
+ public void onMotionEventVelocity(final long aEventTime, final float aSpeedY);
+
+ public void setOverscrollHandler(final Overscroll controller);
+
+ public void setIsLongpressEnabled(boolean isLongpressEnabled);
+
+ public ImmutableViewportMetrics adjustScrollForSurfaceShift(ImmutableViewportMetrics aMetrics, PointF aShift);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomTarget.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomTarget.java
new file mode 100644
index 000000000..0896674fc
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomTarget.java
@@ -0,0 +1,15 @@
+/* -*- 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.Matrix;
+import android.graphics.PointF;
+
+public interface PanZoomTarget {
+ public void panZoomStopped();
+ public boolean isGeckoReady();
+ public void setScrollingRootContent(boolean isRootContent);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java
new file mode 100644
index 000000000..42eb2b88b
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java
@@ -0,0 +1,73 @@
+/* -*- 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.annotation.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;
+ private static List<Long> mFrameTimes;
+ private static long mFrameStartTime;
+
+ private static void initialiseRecordingArrays() {
+ if (mFrameTimes == null) {
+ mFrameTimes = new ArrayList<Long>(EXPECTED_FRAME_COUNT);
+ } else {
+ mFrameTimes.clear();
+ }
+ }
+
+ @RobocopTarget
+ public static void startFrameTimeRecording() {
+ if (mRecordingFrames) {
+ 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);
+ }
+ }
+
+ @RobocopTarget
+ public static void startCheckerboardRecording() {
+ throw new UnsupportedOperationException();
+ }
+
+ @RobocopTarget
+ public static List<Float> stopCheckerboardRecording() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PointUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PointUtils.java
new file mode 100644
index 000000000..8db329c9f
--- /dev/null
+++ b/mobile/android/geckoview/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/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ProgressiveUpdateData.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ProgressiveUpdateData.java
new file mode 100644
index 000000000..d961a2569
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ProgressiveUpdateData.java
@@ -0,0 +1,29 @@
+/* -*- 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.annotation.WrapForJNI;
+
+/**
+ * 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.
+ */
+@WrapForJNI
+public class ProgressiveUpdateData {
+ public float x;
+ public float y;
+ public float scale;
+ public boolean abort;
+
+ public void setViewport(ImmutableViewportMetrics viewport) {
+ this.x = viewport.viewportRectLeft;
+ this.y = viewport.viewportRectTop;
+ this.scale = viewport.zoomFactor;
+ }
+}
+
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RectUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RectUtils.java
new file mode 100644
index 000000000..22151db76
--- /dev/null
+++ b/mobile/android/geckoview/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/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RenderTask.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RenderTask.java
new file mode 100644
index 000000000..80cbf77f0
--- /dev/null
+++ b/mobile/android/geckoview/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 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 RenderTask#run(long, long)} is run.
+ * @see RenderTask#getStartTime()
+ */
+ public void resetStartTime() {
+ mResetStartTime = true;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/StackScroller.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/StackScroller.java
new file mode 100644
index 000000000..293268cba
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/StackScroller.java
@@ -0,0 +1,695 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.mozilla.gecko.gfx;
+
+import android.content.Context;
+import android.hardware.SensorManager;
+import android.util.Log;
+import android.view.ViewConfiguration;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+/**
+ * This class is vastly copied from {@link android.widget.OverScroller} but decouples the time
+ * from the app time so it can be specified manually.
+ */
+@WrapForJNI(exceptionMode = "nsresult")
+public class StackScroller {
+ private int mMode;
+
+ private final SplineStackScroller mScrollerX;
+ private final SplineStackScroller mScrollerY;
+
+ private final boolean mFlywheel;
+
+ private static final int SCROLL_MODE = 0;
+ private static final int FLING_MODE = 1;
+
+ private static float sViscousFluidScale;
+ private static float sViscousFluidNormalize;
+
+ /**
+ * Creates an StackScroller with a viscous fluid scroll interpolator and flywheel.
+ * @param context
+ */
+ public StackScroller(Context context) {
+ mFlywheel = true;
+ mScrollerX = new SplineStackScroller(context);
+ mScrollerY = new SplineStackScroller(context);
+ initContants();
+ }
+
+ private static void initContants() {
+ // This controls the viscous fluid effect (how much of it)
+ sViscousFluidScale = 8.0f;
+ // must be set to 1.0 (used in viscousFluid())
+ sViscousFluidNormalize = 1.0f;
+ sViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
+ }
+
+ /**
+ *
+ * Returns whether the scroller has finished scrolling.
+ *
+ * @return True if the scroller has finished scrolling, false otherwise.
+ */
+ public final boolean isFinished() {
+ return mScrollerX.mFinished && mScrollerY.mFinished;
+ }
+
+ /**
+ * Force the finished field to a particular value. Contrary to
+ * {@link #abortAnimation()}, forcing the animation to finished
+ * does NOT cause the scroller to move to the final x and y
+ * position.
+ *
+ * @param finished The new finished value.
+ */
+ public final void forceFinished(boolean finished) {
+ mScrollerX.mFinished = mScrollerY.mFinished = finished;
+ }
+
+ /**
+ * Returns the current X offset in the scroll.
+ *
+ * @return The new X offset as an absolute distance from the origin.
+ */
+ public final int getCurrX() {
+ return mScrollerX.mCurrentPosition;
+ }
+
+ /**
+ * Returns the current Y offset in the scroll.
+ *
+ * @return The new Y offset as an absolute distance from the origin.
+ */
+ public final int getCurrY() {
+ return mScrollerY.mCurrentPosition;
+ }
+
+ /**
+ * Returns where the scroll will end. Valid only for "fling" scrolls.
+ *
+ * @return The final X offset as an absolute distance from the origin.
+ */
+ public final int getFinalX() {
+ return mScrollerX.mFinal;
+ }
+
+ public final float getCurrSpeedX() {
+ return mScrollerX.mCurrVelocity;
+ }
+
+ public final float getCurrSpeedY() {
+ return mScrollerY.mCurrVelocity;
+ }
+
+ /**
+ * Returns where the scroll will end. Valid only for "fling" scrolls.
+ *
+ * @return The final Y offset as an absolute distance from the origin.
+ */
+ public final int getFinalY() {
+ return mScrollerY.mFinal;
+ }
+
+ /**
+ * Sets where the scroll will end. Valid only for "fling" scrolls.
+ *
+ * @param x The final X offset as an absolute distance from the origin.
+ */
+ public final void setFinalX(int x) {
+ mScrollerX.setFinalPosition(x);
+ }
+
+ private static float viscousFluid(float x) {
+ x *= sViscousFluidScale;
+ if (x < 1.0f) {
+ x -= (1.0f - (float) Math.exp(-x));
+ } else {
+ float start = 0.36787944117f; // 1/e == exp(-1)
+ x = 1.0f - (float) Math.exp(1.0f - x);
+ x = start + x * (1.0f - start);
+ }
+ x *= sViscousFluidNormalize;
+ return x;
+ }
+
+ /**
+ * Call this when you want to know the new location. If it returns true, the
+ * animation is not yet finished.
+ */
+ public boolean computeScrollOffset(long time) {
+ if (isFinished()) {
+ return false;
+ }
+
+ switch (mMode) {
+ case SCROLL_MODE:
+ // Any scroller can be used for time, since they were started
+ // together in scroll mode. We use X here.
+ final long elapsedTime = time - mScrollerX.mStartTime;
+
+ final int duration = mScrollerX.mDuration;
+ if (elapsedTime < duration) {
+ float q = (float) (elapsedTime) / duration;
+ q = viscousFluid(q);
+ mScrollerX.updateScroll(q);
+ mScrollerY.updateScroll(q);
+ } else {
+ abortAnimation();
+ }
+ break;
+
+ case FLING_MODE:
+ if (!mScrollerX.mFinished) {
+ if (!mScrollerX.update(time)) {
+ if (!mScrollerX.continueWhenFinished(time)) {
+ mScrollerX.finish();
+ }
+ }
+ }
+
+ if (!mScrollerY.mFinished) {
+ if (!mScrollerY.update(time)) {
+ if (!mScrollerY.continueWhenFinished(time)) {
+ mScrollerY.finish();
+ }
+ }
+ }
+
+ break;
+
+ default:
+ break;
+ }
+
+ return true;
+ }
+
+ /**
+ * Start scrolling by providing a starting point and the distance to travel.
+ *
+ * @param startX Starting horizontal scroll offset in pixels. Positive
+ * numbers will scroll the content to the left.
+ * @param startY Starting vertical scroll offset in pixels. Positive numbers
+ * will scroll the content up.
+ * @param dx Horizontal distance to travel. Positive numbers will scroll the
+ * content to the left.
+ * @param dy Vertical distance to travel. Positive numbers will scroll the
+ * content up.
+ * @param duration Duration of the scroll in milliseconds.
+ */
+ public void startScroll(int startX, int startY, int dx, int dy, long startTime, int duration) {
+ mMode = SCROLL_MODE;
+ mScrollerX.startScroll(startX, dx, startTime, duration);
+ mScrollerY.startScroll(startY, dy, startTime, duration);
+ }
+
+ /**
+ * Call this when you want to 'spring back' into a valid coordinate range.
+ *
+ * @param startX Starting X coordinate
+ * @param startY Starting Y coordinate
+ * @param minX Minimum valid X value
+ * @param maxX Maximum valid X value
+ * @param minY Minimum valid Y value
+ * @param maxY Minimum valid Y value
+ * @return true if a springback was initiated, false if startX and startY were
+ * already within the valid range.
+ */
+ public boolean springBack(
+ int startX, int startY, int minX, int maxX, int minY, int maxY, long time) {
+ mMode = FLING_MODE;
+
+ // Make sure both methods are called.
+ final boolean spingbackX = mScrollerX.springback(startX, minX, maxX, time);
+ final boolean spingbackY = mScrollerY.springback(startY, minY, maxY, time);
+ return spingbackX || spingbackY;
+ }
+
+ /**
+ * Start scrolling based on a fling gesture. The distance traveled will
+ * depend on the initial velocity of the fling.
+ *
+ * @param startX Starting point of the scroll (X)
+ * @param startY Starting point of the scroll (Y)
+ * @param velocityX Initial velocity of the fling (X) measured in pixels per second.
+ * @param velocityY Initial velocity of the fling (Y) measured in pixels per second
+ * @param minX Minimum X value. The scroller will not scroll past this point
+ * unless overX > 0. If overfling is allowed, it will use minX as
+ * a springback boundary.
+ * @param maxX Maximum X value. The scroller will not scroll past this point
+ * unless overX > 0. If overfling is allowed, it will use maxX as
+ * a springback boundary.
+ * @param minY Minimum Y value. The scroller will not scroll past this point
+ * unless overY > 0. If overfling is allowed, it will use minY as
+ * a springback boundary.
+ * @param maxY Maximum Y value. The scroller will not scroll past this point
+ * unless overY > 0. If overfling is allowed, it will use maxY as
+ * a springback boundary.
+ * @param overX Overfling range. If > 0, horizontal overfling in either
+ * direction will be possible.
+ * @param overY Overfling range. If > 0, vertical overfling in either
+ * direction will be possible.
+ */
+ public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX,
+ int minY, int maxY, int overX, int overY, long time) {
+ // Continue a scroll or fling in progress
+ if (mFlywheel && !isFinished()) {
+ float oldVelocityX = mScrollerX.mCurrVelocity;
+ float oldVelocityY = mScrollerY.mCurrVelocity;
+ boolean sameXDirection = (velocityX == 0) || (oldVelocityX == 0) ||
+ ((velocityX < 0) == (oldVelocityX < 0));
+ boolean sameYDirection = (velocityY == 0) || (oldVelocityY == 0) ||
+ ((velocityY < 0) == (oldVelocityY < 0));
+ if (sameXDirection) {
+ velocityX += oldVelocityX;
+ }
+ if (sameYDirection) {
+ velocityY += oldVelocityY;
+ }
+ }
+
+ mMode = FLING_MODE;
+ mScrollerX.fling(startX, velocityX, minX, maxX, overX, time);
+ mScrollerY.fling(startY, velocityY, minY, maxY, overY, time);
+ }
+
+ /**
+ * Stops the animation. Contrary to {@link #forceFinished(boolean)},
+ * aborting the animating causes the scroller to move to the final x and y
+ * positions.
+ *
+ * @see #forceFinished(boolean)
+ */
+ public void abortAnimation() {
+ mScrollerX.finish();
+ mScrollerY.finish();
+ }
+
+ static class SplineStackScroller {
+ // Initial position
+ private int mStart;
+
+ // Current position
+ private int mCurrentPosition;
+
+ // Final position
+ private int mFinal;
+
+ // Initial velocity
+ private int mVelocity;
+
+ // Current velocity
+ private float mCurrVelocity;
+
+ // Constant current deceleration
+ private float mDeceleration;
+
+ // Animation starting time, in system milliseconds
+ private long mStartTime;
+
+ // Animation duration, in milliseconds
+ private int mDuration;
+
+ // Duration to complete spline component of animation
+ private int mSplineDuration;
+
+ // Distance to travel along spline animation
+ private int mSplineDistance;
+
+ // Whether the animation is currently in progress
+ private boolean mFinished;
+
+ // The allowed overshot distance before boundary is reached.
+ private int mOver;
+
+ // Fling friction
+ private final float mFlingFriction = ViewConfiguration.getScrollFriction();
+
+ // Current state of the animation.
+ private int mState = SPLINE;
+
+ // Constant gravity value, used in the deceleration phase.
+ private static final float GRAVITY = 2000.0f;
+
+ // A context-specific coefficient adjusted to physical values.
+ private final float mPhysicalCoeff;
+
+ private static final float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
+ private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
+ private static final float START_TENSION = 0.5f;
+ private static final float END_TENSION = 1.0f;
+ private static final float P1 = START_TENSION * INFLEXION;
+ private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
+
+ private static final int NB_SAMPLES = 100;
+ private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
+ private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
+
+ private static final int SPLINE = 0;
+ private static final int CUBIC = 1;
+ private static final int BALLISTIC = 2;
+
+ static {
+ float xMin = 0.0f;
+ float yMin = 0.0f;
+ for (int i = 0; i < NB_SAMPLES; i++) {
+ final float alpha = (float) i / NB_SAMPLES;
+
+ float xMax = 1.0f;
+ float x, tx, coef;
+ while (true) {
+ x = xMin + (xMax - xMin) / 2.0f;
+ coef = 3.0f * x * (1.0f - x);
+ tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
+ if (Math.abs(tx - alpha) < 1E-5) break;
+ if (tx > alpha) {
+ xMax = x;
+ } else {
+ xMin = x;
+ }
+ }
+ SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
+
+ float yMax = 1.0f;
+ float y, dy;
+ while (true) {
+ y = yMin + (yMax - yMin) / 2.0f;
+ coef = 3.0f * y * (1.0f - y);
+ dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
+ if (Math.abs(dy - alpha) < 1E-5) break;
+ if (dy > alpha) {
+ yMax = y;
+ } else {
+ yMin = y;
+ }
+ }
+ SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
+ }
+ SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
+ }
+
+ SplineStackScroller(Context context) {
+ mFinished = true;
+ final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
+ mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
+ * 39.37f // inch/meter
+ * ppi * 0.84f; // look and feel tuning
+ }
+
+ void updateScroll(float q) {
+ mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));
+ }
+
+ /*
+ * Get a signed deceleration that will reduce the velocity.
+ */
+ private static float getDeceleration(int velocity) {
+ return velocity > 0 ? -GRAVITY : GRAVITY;
+ }
+
+ /*
+ * Modifies mDuration to the duration it takes to get from start to newFinal using the
+ * spline interpolation. The previous duration was needed to get to oldFinal.
+ */
+ private void adjustDuration(int start, int oldFinal, int newFinal) {
+ final int oldDistance = oldFinal - start;
+ final int newDistance = newFinal - start;
+ final float x = Math.abs((float) newDistance / oldDistance);
+ final int index = (int) (NB_SAMPLES * x);
+ if (index < NB_SAMPLES) {
+ final float xInf = (float) index / NB_SAMPLES;
+ final float xSup = (float) (index + 1) / NB_SAMPLES;
+ final float tInf = SPLINE_TIME[index];
+ final float tSup = SPLINE_TIME[index + 1];
+ final float timeCoef = tInf + (x - xInf) / (xSup - xInf) * (tSup - tInf);
+ mDuration *= timeCoef;
+ }
+ }
+
+ void startScroll(int start, int distance, long startTime, int duration) {
+ mFinished = false;
+
+ mStart = start;
+ mFinal = start + distance;
+
+ mStartTime = startTime;
+ mDuration = duration;
+
+ // Unused
+ mDeceleration = 0.0f;
+ mVelocity = 0;
+ }
+
+ void finish() {
+ mCurrentPosition = mFinal;
+ // Not reset since WebView relies on this value for fast fling.
+ // TODO: restore when WebView uses the fast fling implemented in this class.
+ // mCurrVelocity = 0.0f;
+ mFinished = true;
+ }
+
+ void setFinalPosition(int position) {
+ mFinal = position;
+ mFinished = false;
+ }
+
+ boolean springback(int start, int min, int max, long time) {
+ mFinished = true;
+
+ mStart = mFinal = start;
+ mVelocity = 0;
+
+ mStartTime = time;
+ mDuration = 0;
+
+ if (start < min) {
+ startSpringback(start, min, 0);
+ } else if (start > max) {
+ startSpringback(start, max, 0);
+ }
+
+ return !mFinished;
+ }
+
+ private void startSpringback(int start, int end, int velocity) {
+ // mStartTime has been set
+ mFinished = false;
+ mState = CUBIC;
+ mStart = start;
+ mFinal = end;
+ final int delta = start - end;
+ mDeceleration = getDeceleration(delta);
+ // TODO take velocity into account
+ mVelocity = -delta; // only sign is used
+ mOver = Math.abs(delta);
+ mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration));
+ }
+
+ void fling(int start, int velocity, int min, int max, int over, long time) {
+ mOver = over;
+ mFinished = false;
+ mCurrVelocity = mVelocity = velocity;
+ mDuration = mSplineDuration = 0;
+ mStartTime = time;
+ mCurrentPosition = mStart = start;
+
+ if (start > max || start < min) {
+ startAfterEdge(start, min, max, velocity, time);
+ return;
+ }
+
+ mState = SPLINE;
+ double totalDistance = 0.0;
+
+ if (velocity != 0) {
+ mDuration = mSplineDuration = getSplineFlingDuration(velocity);
+ totalDistance = getSplineFlingDistance(velocity);
+ }
+
+ mSplineDistance = (int) (totalDistance * Math.signum(velocity));
+ mFinal = start + mSplineDistance;
+
+ // Clamp to a valid final position
+ if (mFinal < min) {
+ adjustDuration(mStart, mFinal, min);
+ mFinal = min;
+ }
+
+ if (mFinal > max) {
+ adjustDuration(mStart, mFinal, max);
+ mFinal = max;
+ }
+ }
+
+ private double getSplineDeceleration(int velocity) {
+ return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
+ }
+
+ private double getSplineFlingDistance(int velocity) {
+ final double l = getSplineDeceleration(velocity);
+ final double decelMinusOne = DECELERATION_RATE - 1.0;
+ return mFlingFriction * mPhysicalCoeff
+ * Math.exp(DECELERATION_RATE / decelMinusOne * l);
+ }
+
+ /* Returns the duration, expressed in milliseconds */
+ private int getSplineFlingDuration(int velocity) {
+ final double l = getSplineDeceleration(velocity);
+ final double decelMinusOne = DECELERATION_RATE - 1.0;
+ return (int) (1000.0 * Math.exp(l / decelMinusOne));
+ }
+
+ private void fitOnBounceCurve(int start, int end, int velocity) {
+ // Simulate a bounce that started from edge
+ final float durationToApex = -velocity / mDeceleration;
+ final float distanceToApex = velocity * velocity / 2.0f / Math.abs(mDeceleration);
+ final float distanceToEdge = Math.abs(end - start);
+ final float totalDuration = (float) Math.sqrt(
+ 2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration));
+ mStartTime -= (int) (1000.0f * (totalDuration - durationToApex));
+ mStart = end;
+ mVelocity = (int) (-mDeceleration * totalDuration);
+ }
+
+ private void startBounceAfterEdge(int start, int end, int velocity) {
+ mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity);
+ fitOnBounceCurve(start, end, velocity);
+ onEdgeReached();
+ }
+
+ private void startAfterEdge(int start, int min, int max, int velocity, long time) {
+ if (start > min && start < max) {
+ Log.e("StackScroller", "startAfterEdge called from a valid position");
+ mFinished = true;
+ return;
+ }
+ final boolean positive = start > max;
+ final int edge = positive ? max : min;
+ final int overDistance = start - edge;
+ boolean keepIncreasing = overDistance * velocity >= 0;
+ if (keepIncreasing) {
+ // Will result in a bounce or a to_boundary depending on velocity.
+ startBounceAfterEdge(start, edge, velocity);
+ } else {
+ final double totalDistance = getSplineFlingDistance(velocity);
+ if (totalDistance > Math.abs(overDistance)) {
+ fling(start, velocity, positive ? min : start, positive ? start : max, mOver,
+ time);
+ } else {
+ startSpringback(start, edge, velocity);
+ }
+ }
+ }
+
+ private void onEdgeReached() {
+ // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached.
+ float distance = mVelocity * mVelocity / (2.0f * Math.abs(mDeceleration));
+ final float sign = Math.signum(mVelocity);
+
+ if (distance > mOver) {
+ // Default deceleration is not sufficient to slow us down before boundary
+ mDeceleration = -sign * mVelocity * mVelocity / (2.0f * mOver);
+ distance = mOver;
+ }
+
+ mOver = (int) distance;
+ mState = BALLISTIC;
+ mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance);
+ mDuration = -(int) (1000.0f * mVelocity / mDeceleration);
+ }
+
+ boolean continueWhenFinished(long time) {
+ switch (mState) {
+ case SPLINE:
+ // Duration from start to null velocity
+ if (mDuration < mSplineDuration) {
+ // If the animation was clamped, we reached the edge
+ mStart = mFinal;
+ // TODO Better compute speed when edge was reached
+ mVelocity = (int) mCurrVelocity;
+ mDeceleration = getDeceleration(mVelocity);
+ mStartTime += mDuration;
+ onEdgeReached();
+ } else {
+ // Normal stop, no need to continue
+ return false;
+ }
+ break;
+ case BALLISTIC:
+ mStartTime += mDuration;
+ startSpringback(mFinal, mStart, 0);
+ break;
+ case CUBIC:
+ return false;
+ }
+
+ update(time);
+ return true;
+ }
+
+ /*
+ * Update the current position and velocity for current time. Returns
+ * true if update has been done and false if animation duration has been
+ * reached.
+ */
+ boolean update(long time) {
+ final long currentTime = time - mStartTime;
+
+ if (((mState == SPLINE) && (mSplineDuration <= 0)) ||
+ ((mState == CUBIC) && (mDuration <= 0))) {
+ return false;
+ }
+
+ if (currentTime > mDuration) {
+ return false;
+ }
+
+ double distance = 0.0;
+ switch (mState) {
+ case SPLINE: {
+ final float t = (float) currentTime / mSplineDuration;
+ final int index = (int) (NB_SAMPLES * t);
+ float distanceCoef = 1.f;
+ float velocityCoef = 0.f;
+ if (index < NB_SAMPLES) {
+ final float tInf = (float) index / NB_SAMPLES;
+ final float tSup = (float) (index + 1) / NB_SAMPLES;
+ final float dInf = SPLINE_POSITION[index];
+ final float dSup = SPLINE_POSITION[index + 1];
+ velocityCoef = (dSup - dInf) / (tSup - tInf);
+ distanceCoef = dInf + (t - tInf) * velocityCoef;
+ }
+
+ distance = distanceCoef * mSplineDistance;
+ mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
+ break;
+ }
+
+ case BALLISTIC: {
+ final float t = currentTime / 1000.0f;
+ mCurrVelocity = mVelocity + mDeceleration * t;
+ distance = mVelocity * t + mDeceleration * t * t / 2.0f;
+ break;
+ }
+
+ case CUBIC: {
+ final float t = (float) (currentTime) / mDuration;
+ final float t2 = t * t;
+ final float sign = Math.signum(mVelocity);
+ distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2);
+ mCurrVelocity = sign * mOver * 6.0f * (-t + t2);
+ break;
+ }
+ }
+
+ mCurrentPosition = mStart + (int) Math.round(distance);
+
+ return true;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceTextureListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceTextureListener.java
new file mode 100644
index 000000000..560674e4f
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceTextureListener.java
@@ -0,0 +1,38 @@
+/* -*- 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;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+
+import android.graphics.SurfaceTexture;
+
+final class SurfaceTextureListener
+ extends JNIObject implements SurfaceTexture.OnFrameAvailableListener
+{
+ @WrapForJNI(calledFrom = "gecko")
+ private SurfaceTextureListener() {
+ }
+
+ @Override
+ protected void disposeNative() {
+ // SurfaceTextureListener is disposed inside AndroidSurfaceTexture.
+ throw new IllegalStateException("unreachable code");
+ }
+
+ @WrapForJNI(stubName = "OnFrameAvailable")
+ private native void nativeOnFrameAvailable();
+
+ @Override // SurfaceTexture.OnFrameAvailableListener
+ public void onFrameAvailable(SurfaceTexture surfaceTexture) {
+ try {
+ nativeOnFrameAvailable();
+ } catch (final NullPointerException e) {
+ // Ignore exceptions caused by a disposed object, i.e.
+ // getting a callback after this listener is no longer in use.
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ViewTransform.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ViewTransform.java
new file mode 100644
index 000000000..e6685f066
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ViewTransform.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 org.mozilla.gecko.annotation.WrapForJNI;
+
+@WrapForJNI
+public class ViewTransform {
+ public float x;
+ public float y;
+ public float width;
+ public float height;
+ public float scale;
+ public float fixedLayerMarginLeft;
+ public float fixedLayerMarginTop;
+ public float fixedLayerMarginRight;
+ public float fixedLayerMarginBottom;
+
+ public ViewTransform(float inX, float inY, float inScale) {
+ x = inX;
+ y = inY;
+ scale = inScale;
+ }
+}
+
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/ByteBufferInputStream.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/ByteBufferInputStream.java
new file mode 100644
index 000000000..bc9e0a143
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/ByteBufferInputStream.java
@@ -0,0 +1,64 @@
+/* -*- 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.mozglue;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+class ByteBufferInputStream extends InputStream {
+
+ protected ByteBuffer mBuf;
+ // Reference to a native object holding the data backing the ByteBuffer.
+ private final NativeReference mNativeRef;
+
+ protected ByteBufferInputStream(ByteBuffer buffer, NativeReference ref) {
+ mBuf = buffer;
+ mNativeRef = ref;
+ }
+
+ @Override
+ public int available() {
+ return mBuf.remaining();
+ }
+
+ @Override
+ public void close() {
+ // Do nothing, we need to keep the native references around for child
+ // buffers.
+ }
+
+ @Override
+ public int read() {
+ if (!mBuf.hasRemaining() || mNativeRef.isReleased()) {
+ return -1;
+ }
+
+ return mBuf.get() & 0xff; // Avoid sign extension
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int length) {
+ if (!mBuf.hasRemaining() || mNativeRef.isReleased()) {
+ return -1;
+ }
+
+ length = Math.min(length, mBuf.remaining());
+ mBuf.get(buffer, offset, length);
+ return length;
+ }
+
+ @Override
+ public long skip(long byteCount) {
+ if (byteCount < 0 || mNativeRef.isReleased()) {
+ return 0;
+ }
+
+ byteCount = Math.min(byteCount, mBuf.remaining());
+ mBuf.position(mBuf.position() + (int)byteCount);
+ return byteCount;
+ }
+
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/DirectBufferAllocator.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/DirectBufferAllocator.java
new file mode 100644
index 000000000..b3fb24291
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/DirectBufferAllocator.java
@@ -0,0 +1,52 @@
+/* -*- 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 = nativeAllocateDirectBuffer(size);
+ if (directBuffer == null) {
+ throw new OutOfMemoryError("allocateDirectBuffer() returned null");
+ }
+
+ 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/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java
new file mode 100644
index 000000000..0bef2435b
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java
@@ -0,0 +1,549 @@
+/* -*- 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.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.util.Locale;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Environment;
+import android.util.Log;
+
+import org.mozilla.gecko.annotation.JNITarget;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.AppConstants;
+
+public final class GeckoLoader {
+ private static final String LOGTAG = "GeckoLoader";
+
+ private static volatile SafeIntent sIntent;
+ private static File sCacheFile;
+ private static File sGREDir;
+
+ /* Synchronized on GeckoLoader.class. */
+ private static boolean sSQLiteLibsLoaded;
+ private static boolean sNSSLibsLoaded;
+ private static boolean sMozGlueLoaded;
+
+ private GeckoLoader() {
+ // prevent instantiation
+ }
+
+ public static File getCacheDir(Context context) {
+ if (sCacheFile == null) {
+ sCacheFile = context.getCacheDir();
+ }
+ return sCacheFile;
+ }
+
+ public static File getGREDir(Context context) {
+ if (sGREDir == null) {
+ sGREDir = new File(context.getApplicationInfo().dataDir);
+ }
+ return sGREDir;
+ }
+
+ private static void setupPluginEnvironment(Context context, String[] pluginDirs) {
+ // setup plugin path directories
+ try {
+ // Check to see if plugins were blocked.
+ if (pluginDirs == null) {
+ putenv("MOZ_PLUGINS_BLOCKED=1");
+ putenv("MOZ_PLUGIN_PATH=");
+ return;
+ }
+
+ StringBuilder pluginSearchPath = new StringBuilder();
+ for (int i = 0; i < pluginDirs.length; i++) {
+ pluginSearchPath.append(pluginDirs[i]);
+ pluginSearchPath.append(":");
+ }
+ putenv("MOZ_PLUGIN_PATH=" + pluginSearchPath);
+
+ File pluginDataDir = context.getDir("plugins", 0);
+ putenv("ANDROID_PLUGIN_DATADIR=" + pluginDataDir.getPath());
+
+ File pluginPrivateDataDir = context.getDir("plugins_private", 0);
+ putenv("ANDROID_PLUGIN_DATADIR_PRIVATE=" + pluginPrivateDataDir.getPath());
+
+ } catch (Exception ex) {
+ Log.w(LOGTAG, "Caught exception getting plugin dirs.", ex);
+ }
+ }
+
+ private static void setupDownloadEnvironment(final Context context) {
+ try {
+ File downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+ File updatesDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
+ if (downloadDir == null) {
+ downloadDir = new File(Environment.getExternalStorageDirectory().getPath(), "download");
+ }
+ if (updatesDir == null) {
+ updatesDir = downloadDir;
+ }
+ putenv("DOWNLOADS_DIRECTORY=" + downloadDir.getPath());
+ putenv("UPDATES_DIRECTORY=" + updatesDir.getPath());
+ } catch (Exception e) {
+ Log.w(LOGTAG, "No download directory found.", e);
+ }
+ }
+
+ private static void delTree(File file) {
+ if (file.isDirectory()) {
+ File children[] = file.listFiles();
+ for (File child : children) {
+ delTree(child);
+ }
+ }
+ file.delete();
+ }
+
+ private static File getTmpDir(Context context) {
+ File tmpDir = context.getDir("tmpdir", Context.MODE_PRIVATE);
+ // check if the old tmp dir is there
+ File oldDir = new File(tmpDir.getParentFile(), "app_tmp");
+ if (oldDir.exists()) {
+ delTree(oldDir);
+ }
+ return tmpDir;
+ }
+
+ public static void setLastIntent(SafeIntent intent) {
+ sIntent = intent;
+ }
+
+ public static void setupGeckoEnvironment(Context context, String[] pluginDirs, String profilePath) {
+ // if we have an intent (we're being launched by an activity)
+ // read in any environmental variables from it here
+ final SafeIntent intent = sIntent;
+ if (intent != null) {
+ String env = intent.getStringExtra("env0");
+ Log.d(LOGTAG, "Gecko environment env0: " + env);
+ for (int c = 1; env != null; c++) {
+ putenv(env);
+ env = intent.getStringExtra("env" + c);
+ Log.d(LOGTAG, "env" + c + ": " + env);
+ }
+ }
+
+ putenv("MOZ_ANDROID_PACKAGE_NAME=" + context.getPackageName());
+
+ setupPluginEnvironment(context, pluginDirs);
+ setupDownloadEnvironment(context);
+
+ // profile home path
+ putenv("HOME=" + profilePath);
+
+ // setup the tmp path
+ File f = getTmpDir(context);
+ if (!f.exists()) {
+ f.mkdirs();
+ }
+ putenv("TMPDIR=" + f.getPath());
+
+ // setup the downloads path
+ f = Environment.getDownloadCacheDirectory();
+ putenv("EXTERNAL_STORAGE=" + f.getPath());
+
+ // setup the app-specific cache path
+ f = context.getCacheDir();
+ putenv("CACHE_DIRECTORY=" + f.getPath());
+
+ if (AppConstants.Versions.feature17Plus) {
+ android.os.UserManager um = (android.os.UserManager)context.getSystemService(Context.USER_SERVICE);
+ if (um != null) {
+ putenv("MOZ_ANDROID_USER_SERIAL_NUMBER=" + um.getSerialNumberForUser(android.os.Process.myUserHandle()));
+ } else {
+ Log.d(LOGTAG, "Unable to obtain user manager service on a device with SDK version " + Build.VERSION.SDK_INT);
+ }
+ }
+ setupLocaleEnvironment();
+
+ // We don't need this any more.
+ sIntent = null;
+ }
+
+ private static void loadLibsSetupLocked(Context context) {
+ // The package data lib directory isn't placed in ld.so's
+ // search path, so we have to manually load libraries that
+ // libxul will depend on. Not ideal.
+
+ File cacheFile = getCacheDir(context);
+ putenv("GRE_HOME=" + getGREDir(context).getPath());
+
+ // setup the libs cache
+ String linkerCache = System.getenv("MOZ_LINKER_CACHE");
+ if (linkerCache == null) {
+ linkerCache = cacheFile.getPath();
+ putenv("MOZ_LINKER_CACHE=" + linkerCache);
+ }
+
+ // Disable on-demand decompression of the linker on devices where it
+ // is known to cause crashes.
+ String forced_ondemand = System.getenv("MOZ_LINKER_ONDEMAND");
+ if (forced_ondemand == null) {
+ if ("HTC".equals(android.os.Build.MANUFACTURER) &&
+ "HTC Vision".equals(android.os.Build.MODEL)) {
+ putenv("MOZ_LINKER_ONDEMAND=0");
+ }
+ }
+
+ putenv("MOZ_LINKER_EXTRACT=1");
+ }
+
+ @RobocopTarget
+ public synchronized static void loadSQLiteLibs(final Context context, final String apkName) {
+ if (sSQLiteLibsLoaded) {
+ return;
+ }
+
+ loadMozGlue(context);
+ loadLibsSetupLocked(context);
+ loadSQLiteLibsNative(apkName);
+ sSQLiteLibsLoaded = true;
+ }
+
+ public synchronized static void loadNSSLibs(final Context context, final String apkName) {
+ if (sNSSLibsLoaded) {
+ return;
+ }
+
+ loadMozGlue(context);
+ loadLibsSetupLocked(context);
+ loadNSSLibsNative(apkName);
+ sNSSLibsLoaded = true;
+ }
+
+ @SuppressWarnings("deprecation")
+ private static final String getCPUABI() {
+ return android.os.Build.CPU_ABI;
+ }
+
+ /**
+ * Copy a library out of our APK.
+ *
+ * @param context a Context.
+ * @param lib the name of the library; e.g., "mozglue".
+ * @param outDir the output directory for the .so. No trailing slash.
+ * @return true on success, false on failure.
+ */
+ private static boolean extractLibrary(final Context context, final String lib, final String outDir) {
+ final String apkPath = context.getApplicationInfo().sourceDir;
+
+ // Sanity check.
+ if (!apkPath.endsWith(".apk")) {
+ Log.w(LOGTAG, "sourceDir is not an APK.");
+ return false;
+ }
+
+ // Try to extract the named library from the APK.
+ File outDirFile = new File(outDir);
+ if (!outDirFile.isDirectory()) {
+ if (!outDirFile.mkdirs()) {
+ Log.e(LOGTAG, "Couldn't create " + outDir);
+ return false;
+ }
+ }
+
+ if (AppConstants.Versions.feature21Plus) {
+ String[] abis = Build.SUPPORTED_ABIS;
+ for (String abi : abis) {
+ if (tryLoadWithABI(lib, outDir, apkPath, abi)) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ final String abi = getCPUABI();
+ return tryLoadWithABI(lib, outDir, apkPath, abi);
+ }
+ }
+
+ private static boolean tryLoadWithABI(String lib, String outDir, String apkPath, String abi) {
+ try {
+ final ZipFile zipFile = new ZipFile(new File(apkPath));
+ try {
+ final String libPath = "lib/" + abi + "/lib" + lib + ".so";
+ final ZipEntry entry = zipFile.getEntry(libPath);
+ if (entry == null) {
+ Log.w(LOGTAG, libPath + " not found in APK " + apkPath);
+ return false;
+ }
+
+ final InputStream in = zipFile.getInputStream(entry);
+ try {
+ final String outPath = outDir + "/lib" + lib + ".so";
+ final FileOutputStream out = new FileOutputStream(outPath);
+ final byte[] bytes = new byte[1024];
+ int read;
+
+ Log.d(LOGTAG, "Copying " + libPath + " to " + outPath);
+ boolean failed = false;
+ try {
+ while ((read = in.read(bytes, 0, 1024)) != -1) {
+ out.write(bytes, 0, read);
+ }
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Failing library copy.", e);
+ failed = true;
+ } finally {
+ out.close();
+ }
+
+ if (failed) {
+ // Delete the partial copy so we don't fail to load it.
+ // Don't bother to check the return value -- there's nothing
+ // we can do about a failure.
+ new File(outPath).delete();
+ } else {
+ // Mark the file as executable. This doesn't seem to be
+ // necessary for the loader, but it's the normal state of
+ // affairs.
+ Log.d(LOGTAG, "Marking " + outPath + " as executable.");
+ new File(outPath).setExecutable(true);
+ }
+
+ return !failed;
+ } finally {
+ in.close();
+ }
+ } finally {
+ zipFile.close();
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Failed to extract lib from APK.", e);
+ return false;
+ }
+ }
+
+ private static String getLoadDiagnostics(final Context context, final String lib) {
+ final String androidPackageName = context.getPackageName();
+
+ final StringBuilder message = new StringBuilder("LOAD ");
+ message.append(lib);
+
+ // These might differ. If so, we know why the library won't load!
+ message.append(": ABI: " + AppConstants.MOZ_APP_ABI + ", " + getCPUABI());
+ message.append(": Data: " + context.getApplicationInfo().dataDir);
+ try {
+ final boolean appLibExists = new File("/data/app-lib/" + androidPackageName + "/lib" + lib + ".so").exists();
+ final boolean dataDataExists = new File("/data/data/" + androidPackageName + "/lib/lib" + lib + ".so").exists();
+ message.append(", ax=" + appLibExists);
+ message.append(", ddx=" + dataDataExists);
+ } catch (Throwable e) {
+ message.append(": ax/ddx fail, ");
+ }
+
+ try {
+ final String dashOne = "/data/data/" + androidPackageName + "-1";
+ final String dashTwo = "/data/data/" + androidPackageName + "-2";
+ final boolean dashOneExists = new File(dashOne).exists();
+ final boolean dashTwoExists = new File(dashTwo).exists();
+ message.append(", -1x=" + dashOneExists);
+ message.append(", -2x=" + dashTwoExists);
+ } catch (Throwable e) {
+ message.append(", dash fail, ");
+ }
+
+ try {
+ if (Build.VERSION.SDK_INT >= 9) {
+ final String nativeLibPath = context.getApplicationInfo().nativeLibraryDir;
+ final boolean nativeLibDirExists = new File(nativeLibPath).exists();
+ final boolean nativeLibLibExists = new File(nativeLibPath + "/lib" + lib + ".so").exists();
+
+ message.append(", nativeLib: " + nativeLibPath);
+ message.append(", dirx=" + nativeLibDirExists);
+ message.append(", libx=" + nativeLibLibExists);
+ } else {
+ message.append(", <pre-9>");
+ }
+ } catch (Throwable e) {
+ message.append(", nativeLib fail.");
+ }
+
+ return message.toString();
+ }
+
+ private static final boolean attemptLoad(final String path) {
+ try {
+ System.load(path);
+ return true;
+ } catch (Throwable e) {
+ Log.wtf(LOGTAG, "Couldn't load " + path + ": " + e);
+ }
+
+ return false;
+ }
+
+ /**
+ * The first two attempts at loading a library: directly, and
+ * then using the app library path.
+ *
+ * Returns null or the cause exception.
+ */
+ private static final Throwable doLoadLibraryExpected(final Context context, final String lib) {
+ try {
+ // Attempt 1: the way that should work.
+ System.loadLibrary(lib);
+ return null;
+ } catch (Throwable e) {
+ Log.wtf(LOGTAG, "Couldn't load " + lib + ". Trying native library dir.");
+
+ if (Build.VERSION.SDK_INT < 9) {
+ // We can't use nativeLibraryDir.
+ return e;
+ }
+
+ // Attempt 2: use nativeLibraryDir, which should also work.
+ final String libDir = context.getApplicationInfo().nativeLibraryDir;
+ final String libPath = libDir + "/lib" + lib + ".so";
+
+ // Does it even exist?
+ if (new File(libPath).exists()) {
+ if (attemptLoad(libPath)) {
+ // Success!
+ return null;
+ }
+ Log.wtf(LOGTAG, "Library exists but couldn't load!");
+ } else {
+ Log.wtf(LOGTAG, "Library doesn't exist when it should.");
+ }
+
+ // We failed. Return the original cause.
+ return e;
+ }
+ }
+
+ public static void doLoadLibrary(final Context context, final String lib) {
+ final Throwable e = doLoadLibraryExpected(context, lib);
+ if (e == null) {
+ // Success.
+ return;
+ }
+
+ // If we're in a mismatched UID state (Bug 1042935 Comment 16) there's really
+ // nothing we can do.
+ if (Build.VERSION.SDK_INT >= 9) {
+ final String nativeLibPath = context.getApplicationInfo().nativeLibraryDir;
+ if (nativeLibPath.contains("mismatched_uid")) {
+ throw new RuntimeException("Fatal: mismatched UID: cannot load.");
+ }
+ }
+
+ // Attempt 3: try finding the path the pseudo-supported way using .dataDir.
+ final String dataLibPath = context.getApplicationInfo().dataDir + "/lib/lib" + lib + ".so";
+ if (attemptLoad(dataLibPath)) {
+ return;
+ }
+
+ // Attempt 4: use /data/app-lib directly. This is a last-ditch effort.
+ final String androidPackageName = context.getPackageName();
+ if (attemptLoad("/data/app-lib/" + androidPackageName + "/lib" + lib + ".so")) {
+ return;
+ }
+
+ // Attempt 5: even more optimistic.
+ if (attemptLoad("/data/data/" + androidPackageName + "/lib/lib" + lib + ".so")) {
+ return;
+ }
+
+ // Look in our files directory, copying from the APK first if necessary.
+ final String filesLibDir = context.getFilesDir() + "/lib";
+ final String filesLibPath = filesLibDir + "/lib" + lib + ".so";
+ if (new File(filesLibPath).exists()) {
+ if (attemptLoad(filesLibPath)) {
+ return;
+ }
+ } else {
+ // Try copying.
+ if (extractLibrary(context, lib, filesLibDir)) {
+ // Let's try it!
+ if (attemptLoad(filesLibPath)) {
+ return;
+ }
+ }
+ }
+
+ // Give up loudly, leaking information to debug the failure.
+ final String message = getLoadDiagnostics(context, lib);
+ Log.e(LOGTAG, "Load diagnostics: " + message);
+
+ // Throw the descriptive message, using the original library load
+ // failure as the cause.
+ throw new RuntimeException(message, e);
+ }
+
+ public synchronized static void loadMozGlue(final Context context) {
+ if (sMozGlueLoaded) {
+ return;
+ }
+
+ doLoadLibrary(context, "mozglue");
+ sMozGlueLoaded = true;
+ }
+
+ public synchronized static void loadGeckoLibs(final Context context, final String apkName) {
+ loadLibsSetupLocked(context);
+ loadGeckoLibsNative(apkName);
+ }
+
+ public synchronized static void extractGeckoLibs(final Context context, final String apkName) {
+ loadLibsSetupLocked(context);
+ try {
+ extractGeckoLibsNative(apkName);
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Failing library extraction.", e);
+ }
+ }
+
+ private static void setupLocaleEnvironment() {
+ putenv("LANG=" + Locale.getDefault().toString());
+ NumberFormat nf = NumberFormat.getInstance();
+ if (nf instanceof DecimalFormat) {
+ DecimalFormat df = (DecimalFormat)nf;
+ DecimalFormatSymbols dfs = df.getDecimalFormatSymbols();
+
+ putenv("LOCALE_DECIMAL_POINT=" + dfs.getDecimalSeparator());
+ putenv("LOCALE_THOUSANDS_SEP=" + dfs.getGroupingSeparator());
+ putenv("LOCALE_GROUPING=" + (char)df.getGroupingSize());
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public static class AbortException extends Exception {
+ public AbortException(String msg) {
+ super(msg);
+ }
+ }
+
+ @JNITarget
+ public static void abort(final String msg) {
+ final Thread thread = Thread.currentThread();
+ final Thread.UncaughtExceptionHandler uncaughtHandler =
+ thread.getUncaughtExceptionHandler();
+ if (uncaughtHandler != null) {
+ uncaughtHandler.uncaughtException(thread, new AbortException(msg));
+ }
+ }
+
+ // These methods are implemented in mozglue/android/nsGeckoUtils.cpp
+ private static native void putenv(String map);
+
+ // These methods are implemented in mozglue/android/APKOpen.cpp
+ public static native void nativeRun(String args);
+ private static native void loadGeckoLibsNative(String apkName);
+ private static native void loadSQLiteLibsNative(String apkName);
+ private static native void loadNSSLibsNative(String apkName);
+ private static native void extractGeckoLibsNative(String apkName);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/JNIObject.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/JNIObject.java
new file mode 100644
index 000000000..a3a127a1a
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/JNIObject.java
@@ -0,0 +1,11 @@
+package org.mozilla.gecko.mozglue;
+
+// Class that all classes with native methods extend from.
+public abstract class JNIObject
+{
+ // Pointer to a WeakPtr object that refers to the native object.
+ private long mHandle;
+
+ // Dispose of any reference to a native object.
+ protected abstract void disposeNative();
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeReference.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeReference.java
new file mode 100644
index 000000000..9d897d384
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeReference.java
@@ -0,0 +1,13 @@
+/* -*- 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.mozglue;
+
+public interface NativeReference
+{
+ public void release();
+
+ public boolean isReleased();
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeZip.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeZip.java
new file mode 100644
index 000000000..11241c575
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeZip.java
@@ -0,0 +1,84 @@
+/* -*- 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.mozglue;
+
+import android.support.annotation.Keep;
+import org.mozilla.gecko.annotation.JNITarget;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+public class NativeZip implements NativeReference {
+ private static final int DEFLATE = 8;
+ private static final int STORE = 0;
+
+ private volatile long mObj;
+ @Keep
+ private InputStream mInput;
+
+ public NativeZip(String path) {
+ mObj = getZip(path);
+ }
+
+ public NativeZip(InputStream input) {
+ if (!(input instanceof ByteBufferInputStream)) {
+ throw new IllegalArgumentException("Got " + input.getClass()
+ + ", but expected ByteBufferInputStream!");
+ }
+ ByteBufferInputStream bbinput = (ByteBufferInputStream)input;
+ mObj = getZipFromByteBuffer(bbinput.mBuf);
+ mInput = input;
+ }
+
+ @Override
+ protected void finalize() {
+ release();
+ }
+
+ @Override
+ public void release() {
+ if (mObj != 0) {
+ _release(mObj);
+ mObj = 0;
+ }
+ mInput = null;
+ }
+
+ @Override
+ public boolean isReleased() {
+ return (mObj == 0);
+ }
+
+ public InputStream getInputStream(String path) {
+ if (isReleased()) {
+ throw new IllegalStateException("Can't get path \"" + path
+ + "\" because NativeZip is closed!");
+ }
+ return _getInputStream(mObj, path);
+ }
+
+ private static native long getZip(String path);
+ private static native long getZipFromByteBuffer(ByteBuffer buffer);
+ private static native void _release(long obj);
+ private native InputStream _getInputStream(long obj, String path);
+
+ @JNITarget
+ private InputStream createInputStream(ByteBuffer buffer, int compression) {
+ if (compression != STORE && compression != DEFLATE) {
+ throw new IllegalArgumentException("Unexpected compression: " + compression);
+ }
+
+ InputStream input = new ByteBufferInputStream(buffer, this);
+ if (compression == DEFLATE) {
+ Inflater inflater = new Inflater(true);
+ input = new InflaterInputStream(input, inflater);
+ }
+
+ return input;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/SafeIntent.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/SafeIntent.java
new file mode 100644
index 000000000..6942962fe
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/SafeIntent.java
@@ -0,0 +1,134 @@
+/*
+ * 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/.
+ */
+
+// This should be in util/, but is here because of build dependency issues.
+package org.mozilla.gecko.mozglue;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * External applications can pass values into Intents that can cause us to crash: in defense,
+ * we wrap {@link Intent} and catch the exceptions they may force us to throw. See bug 1090385
+ * for more.
+ */
+public class SafeIntent {
+ private static final String LOGTAG = "Gecko" + SafeIntent.class.getSimpleName();
+
+ private final Intent intent;
+
+ public SafeIntent(final Intent intent) {
+ this.intent = intent;
+ }
+
+ public boolean hasExtra(String name) {
+ try {
+ return intent.hasExtra(name);
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't determine if intent had an extra: OOM. Malformed?");
+ return false;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't determine if intent had an extra.", e);
+ return false;
+ }
+ }
+
+ public boolean getBooleanExtra(final String name, final boolean defaultValue) {
+ try {
+ return intent.getBooleanExtra(name, defaultValue);
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent extras: OOM. Malformed?");
+ return defaultValue;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent extras.", e);
+ return defaultValue;
+ }
+ }
+
+ public int getIntExtra(final String name, final int defaultValue) {
+ try {
+ return intent.getIntExtra(name, defaultValue);
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent extras: OOM. Malformed?");
+ return defaultValue;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent extras.", e);
+ return defaultValue;
+ }
+ }
+
+ public String getStringExtra(final String name) {
+ try {
+ return intent.getStringExtra(name);
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent extras: OOM. Malformed?");
+ return null;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent extras.", e);
+ return null;
+ }
+ }
+
+ public Bundle getBundleExtra(final String name) {
+ try {
+ return intent.getBundleExtra(name);
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent extras: OOM. Malformed?");
+ return null;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent extras.", e);
+ return null;
+ }
+ }
+
+ public String getAction() {
+ return intent.getAction();
+ }
+
+ public String getDataString() {
+ try {
+ return intent.getDataString();
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent data string: OOM. Malformed?");
+ return null;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent data string.", e);
+ return null;
+ }
+ }
+
+ public ArrayList<String> getStringArrayListExtra(final String name) {
+ try {
+ return intent.getStringArrayListExtra(name);
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent data string: OOM. Malformed?");
+ return null;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent data string.", e);
+ return null;
+ }
+ }
+
+ public Uri getData() {
+ try {
+ return intent.getData();
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent data: OOM. Malformed?");
+ return null;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent data.", e);
+ return null;
+ }
+ }
+
+ public Intent getUnsafe() {
+ return intent;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionBlock.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionBlock.java
new file mode 100644
index 000000000..a4d72f258
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionBlock.java
@@ -0,0 +1,133 @@
+/* -*- 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.permissions;
+
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+/**
+ * Helper class to run code blocks depending on whether a user has granted or denied certain runtime permissions.
+ */
+public class PermissionBlock {
+ private final PermissionsHelper helper;
+
+ private Context context;
+ private String[] permissions;
+ private boolean onUIThread;
+ private Runnable onPermissionsGranted;
+ private Runnable onPermissionsDenied;
+ private boolean doNotPrompt;
+
+ /* package-private */ PermissionBlock(Context context, PermissionsHelper helper) {
+ this.context = context;
+ this.helper = helper;
+ }
+
+ /**
+ * Determine whether the app has been granted the specified permissions.
+ */
+ public PermissionBlock withPermissions(@NonNull String... permissions) {
+ this.permissions = permissions;
+ return this;
+ }
+
+ /**
+ * Execute all callbacks on the UI thread.
+ */
+ public PermissionBlock onUIThread() {
+ this.onUIThread = true;
+ return this;
+ }
+
+ /**
+ * Do not prompt the user to accept the permission if it has not been granted yet.
+ */
+ public PermissionBlock doNotPrompt() {
+ doNotPrompt = true;
+ return this;
+ }
+
+ /**
+ * If the condition is true then do not prompt the user to accept the permission if it has not
+ * been granted yet.
+ */
+ public PermissionBlock doNotPromptIf(boolean condition) {
+ if (condition) {
+ doNotPrompt();
+ }
+
+ return this;
+ }
+
+ /**
+ * Execute this permission block. Calling this method will prompt the user if needed.
+ */
+ public void run() {
+ run(null);
+ }
+
+ /**
+ * Execute the specified runnable if the app has been granted all permissions. Calling this method will prompt the
+ * user if needed.
+ */
+ public void run(Runnable onPermissionsGranted) {
+ if (!doNotPrompt && !(context instanceof Activity)) {
+ throw new IllegalStateException("You need to either specify doNotPrompt() or pass in an Activity context");
+ }
+
+ this.onPermissionsGranted = onPermissionsGranted;
+
+ if (hasPermissions(context)) {
+ onPermissionsGranted();
+ } else if (doNotPrompt) {
+ onPermissionsDenied();
+ } else {
+ Permissions.prompt((Activity) context, this);
+ }
+
+ // This reference is no longer needed. Let's clear it now to avoid memory leaks.
+ context = null;
+ }
+
+ /**
+ * Execute this fallback if at least one permission has not been granted.
+ */
+ public PermissionBlock andFallback(@NonNull Runnable onPermissionsDenied) {
+ this.onPermissionsDenied = onPermissionsDenied;
+ return this;
+ }
+
+ /* package-private */ void onPermissionsGranted() {
+ executeRunnable(onPermissionsGranted);
+ }
+
+ /* package-private */ void onPermissionsDenied() {
+ executeRunnable(onPermissionsDenied);
+ }
+
+ private void executeRunnable(Runnable runnable) {
+ if (runnable == null) {
+ return;
+ }
+
+ if (onUIThread) {
+ ThreadUtils.postToUiThread(runnable);
+ } else {
+ runnable.run();
+ }
+ }
+
+ /* package-private */ String[] getPermissions() {
+ return permissions;
+ }
+
+ /* packacge-private */ boolean hasPermissions(Context context) {
+ return helper.hasPermissions(context, permissions);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/Permissions.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/Permissions.java
new file mode 100644
index 000000000..c1b38f61c
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/Permissions.java
@@ -0,0 +1,210 @@
+/* -*- 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.permissions;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.support.annotation.NonNull;
+
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+/**
+ * Convenience class for checking and prompting for runtime permissions.
+ *
+ * Example:
+ *
+ * Permissions.from(activity)
+ * .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ * .onUiThread()
+ * .andFallback(onPermissionDenied())
+ * .run(onPermissionGranted())
+ *
+ * This example will run the runnable returned by onPermissionGranted() if the WRITE_EXTERNAL_STORAGE permission is
+ * already granted. Otherwise it will prompt the user and run the runnable returned by onPermissionGranted() or
+ * onPermissionDenied() depending on whether the user accepted or not. If onUiThread() is specified then all callbacks
+ * will be run on the UI thread.
+ */
+public class Permissions {
+ private static final Queue<PermissionBlock> waiting = new LinkedList<>();
+ private static final Queue<PermissionBlock> prompt = new LinkedList<>();
+
+ private static PermissionsHelper permissionHelper = new PermissionsHelper();
+
+ /**
+ * Entry point for checking (and optionally prompting for) runtime permissions.
+ *
+ * Note: The provided context needs to be an Activity context in order to prompt. Use doNotPrompt()
+ * for all other contexts.
+ */
+ public static PermissionBlock from(@NonNull Context context) {
+ return new PermissionBlock(context, permissionHelper);
+ }
+
+ /**
+ * This method will block until the specified permissions have been granted or denied by the user.
+ * If needed the user will be prompted.
+ *
+ * @return true if all of the permissions have been granted. False if any of the permissions have been denied.
+ */
+ public static boolean waitFor(@NonNull Activity activity, String... permissions) {
+ ThreadUtils.assertNotOnUiThread(); // We do not want to block the UI thread.
+
+ // This task will block until all of the permissions have been granted
+ final FutureTask<Boolean> blockingTask = new FutureTask<>(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ return true;
+ }
+ });
+
+ // This runnable will cancel the task if any of the permissions have been denied
+ Runnable cancelBlockingTask = new Runnable() {
+ @Override
+ public void run() {
+ blockingTask.cancel(true);
+ }
+ };
+
+ Permissions.from(activity)
+ .withPermissions(permissions)
+ .andFallback(cancelBlockingTask)
+ .run(blockingTask);
+
+ try {
+ return blockingTask.get();
+ } catch (InterruptedException | ExecutionException | CancellationException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Determine whether you have been granted particular permissions.
+ */
+ public static boolean has(Context context, String... permissions) {
+ return permissionHelper.hasPermissions(context, permissions);
+ }
+
+ /* package-private */ static void setPermissionHelper(PermissionsHelper permissionHelper) {
+ Permissions.permissionHelper = permissionHelper;
+ }
+
+ /**
+ * Callback for Activity.onRequestPermissionsResult(). All activities that prompt for permissions using this class
+ * should implement onRequestPermissionsResult() and call this method.
+ */
+ public static synchronized void onRequestPermissionsResult(@NonNull Activity activity, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ processGrantResults(permissions, grantResults);
+
+ processQueue(activity, permissions, grantResults);
+ }
+
+ /* package-private */ static synchronized void prompt(Activity activity, PermissionBlock block) {
+ if (prompt.isEmpty()) {
+ prompt.add(block);
+ showPrompt(activity);
+ } else {
+ waiting.add(block);
+ }
+ }
+
+ private static synchronized void processGrantResults(@NonNull String[] permissions, @NonNull int[] grantResults) {
+ final HashSet<String> grantedPermissions = collectGrantedPermissions(permissions, grantResults);
+
+ while (!prompt.isEmpty()) {
+ final PermissionBlock block = prompt.poll();
+
+ if (allPermissionsGranted(block, grantedPermissions)) {
+ block.onPermissionsGranted();
+ } else {
+ block.onPermissionsDenied();
+ }
+ }
+ }
+
+ private static synchronized void processQueue(Activity activity, String[] permissions, int[] grantResults) {
+ final HashSet<String> deniedPermissions = collectDeniedPermissions(permissions, grantResults);
+
+ while (!waiting.isEmpty()) {
+ final PermissionBlock block = waiting.poll();
+
+ if (block.hasPermissions(activity)) {
+ block.onPermissionsGranted();
+ } else {
+ if (atLeastOnePermissionDenied(block, deniedPermissions)) {
+ // We just prompted the user and one of the permissions of this block has been denied:
+ // There's no reason to instantly prompt again; Just reject without prompting.
+ block.onPermissionsDenied();
+ } else {
+ prompt.add(block);
+ }
+ }
+ }
+
+ if (!prompt.isEmpty()) {
+ showPrompt(activity);
+ }
+ }
+
+ private static synchronized void showPrompt(Activity activity) {
+ HashSet<String> permissions = new HashSet<>();
+
+ for (PermissionBlock block : prompt) {
+ Collections.addAll(permissions, block.getPermissions());
+ }
+
+ permissionHelper.prompt(activity, permissions.toArray(new String[permissions.size()]));
+ }
+
+ private static HashSet<String> collectGrantedPermissions(@NonNull String[] permissions, @NonNull int[] grantResults) {
+ return filterPermissionsByResult(permissions, grantResults, PackageManager.PERMISSION_GRANTED);
+ }
+
+ private static HashSet<String> collectDeniedPermissions(@NonNull String[] permissions, @NonNull int[] grantResults) {
+ return filterPermissionsByResult(permissions, grantResults, PackageManager.PERMISSION_DENIED);
+ }
+
+ private static HashSet<String> filterPermissionsByResult(@NonNull String[] permissions, @NonNull int[] grantResults, int result) {
+ HashSet<String> grantedPermissions = new HashSet<>(permissions.length);
+ for (int i = 0; i < permissions.length; i++) {
+ if (grantResults[i] == result) {
+ grantedPermissions.add(permissions[i]);
+ }
+ }
+ return grantedPermissions;
+ }
+
+ private static boolean allPermissionsGranted(PermissionBlock block, HashSet<String> grantedPermissions) {
+ for (String permission : block.getPermissions()) {
+ if (!grantedPermissions.contains(permission)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static boolean atLeastOnePermissionDenied(PermissionBlock block, HashSet<String> deniedPermissions) {
+ for (String permission : block.getPermissions()) {
+ if (deniedPermissions.contains(permission)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionsHelper.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionsHelper.java
new file mode 100644
index 000000000..945a81f43
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionsHelper.java
@@ -0,0 +1,32 @@
+/* -*- 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.permissions;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+
+/* package-private */ class PermissionsHelper {
+ private static final int PERMISSIONS_REQUEST_CODE = 212;
+
+ public boolean hasPermissions(Context context, String... permissions) {
+ for (String permission : permissions) {
+ final int permissionCheck = ContextCompat.checkSelfPermission(context, permission);
+
+ if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public void prompt(Activity activity, String[] permissions) {
+ ActivityCompat.requestPermissions(activity, permissions, PERMISSIONS_REQUEST_CODE);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/ByteBufferInputStream.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/ByteBufferInputStream.java
new file mode 100644
index 000000000..f6b16619f
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/ByteBufferInputStream.java
@@ -0,0 +1,38 @@
+/* -*- 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.sqlite;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+/*
+ * Helper class to make the ByteBuffers returned by SQLite BLOB
+ * easier to use.
+ */
+public class ByteBufferInputStream extends InputStream {
+ private final ByteBuffer mByteBuffer;
+
+ public ByteBufferInputStream(ByteBuffer aByteBuffer) {
+ mByteBuffer = aByteBuffer;
+ }
+
+ @Override
+ public synchronized int read() throws IOException {
+ if (!mByteBuffer.hasRemaining()) {
+ return -1;
+ }
+ return mByteBuffer.get();
+ }
+
+ @Override
+ public synchronized int read(byte[] aBytes, int aOffset, int aLen)
+ throws IOException {
+ int toRead = Math.min(aLen, mByteBuffer.remaining());
+ mByteBuffer.get(aBytes, aOffset, toRead);
+ return toRead;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/MatrixBlobCursor.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/MatrixBlobCursor.java
new file mode 100644
index 000000000..3e2023c86
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/MatrixBlobCursor.java
@@ -0,0 +1,366 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.mozilla.gecko.sqlite;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+
+import org.mozilla.gecko.annotation.JNITarget;
+import org.mozilla.gecko.AppConstants;
+
+import android.database.AbstractCursor;
+import android.database.CursorIndexOutOfBoundsException;
+import android.util.Log;
+
+/**
+ * A mutable cursor implementation backed by an array of {@code Object}s. Use
+ * {@link #newRow()} to add rows. Automatically expands internal capacity
+ * as needed.
+ *
+ * This class provides one missing feature from Android's MatrixCursor:
+ * the implementation of getBlob that was inadvertently omitted from API 9 (and
+ * perhaps later; it's present in 14).
+ *
+ * MatrixCursor is all private, so we entirely duplicate it here.
+ */
+public class MatrixBlobCursor extends AbstractCursor {
+ private static final String LOGTAG = "GeckoMatrixCursor";
+
+ private final String[] columnNames;
+ private final int columnCount;
+
+ private int rowCount;
+ private Throwable allocationStack;
+
+ Object[] data;
+
+ /**
+ * Constructs a new cursor with the given initial capacity.
+ *
+ * @param columnNames names of the columns, the ordering of which
+ * determines column ordering elsewhere in this cursor
+ * @param initialCapacity in rows
+ */
+ @JNITarget
+ public MatrixBlobCursor(String[] columnNames, int initialCapacity) {
+ this.columnNames = columnNames;
+ this.columnCount = columnNames.length;
+
+ if (initialCapacity < 1) {
+ initialCapacity = 1;
+ }
+
+ this.data = new Object[columnCount * initialCapacity];
+ if (AppConstants.DEBUG_BUILD) {
+ this.allocationStack = new Throwable("allocationStack");
+ }
+ }
+
+ /**
+ * Constructs a new cursor.
+ *
+ * @param columnNames names of the columns, the ordering of which
+ * determines column ordering elsewhere in this cursor
+ */
+ @JNITarget
+ public MatrixBlobCursor(String[] columnNames) {
+ this(columnNames, 16);
+ }
+
+ /**
+ * Closes the Cursor, releasing all of its resources.
+ */
+ public void close() {
+ this.allocationStack = null;
+ this.data = null;
+ super.close();
+ }
+
+ /**
+ * Gets value at the given column for the current row.
+ */
+ protected Object get(int column) {
+ if (column < 0 || column >= columnCount) {
+ throw new CursorIndexOutOfBoundsException("Requested column: "
+ + column + ", # of columns: " + columnCount);
+ }
+ if (mPos < 0) {
+ throw new CursorIndexOutOfBoundsException("Before first row.");
+ }
+ if (mPos >= rowCount) {
+ throw new CursorIndexOutOfBoundsException("After last row.");
+ }
+ return data[mPos * columnCount + column];
+ }
+
+ /**
+ * Adds a new row to the end and returns a builder for that row. Not safe
+ * for concurrent use.
+ *
+ * @return builder which can be used to set the column values for the new
+ * row
+ */
+ public RowBuilder newRow() {
+ rowCount++;
+ int endIndex = rowCount * columnCount;
+ ensureCapacity(endIndex);
+ int start = endIndex - columnCount;
+ return new RowBuilder(start, endIndex);
+ }
+
+ /**
+ * Adds a new row to the end with the given column values. Not safe
+ * for concurrent use.
+ *
+ * @throws IllegalArgumentException if {@code columnValues.length !=
+ * columnNames.length}
+ * @param columnValues in the same order as the the column names specified
+ * at cursor construction time
+ */
+ @JNITarget
+ public void addRow(Object[] columnValues) {
+ if (columnValues.length != columnCount) {
+ throw new IllegalArgumentException("columnNames.length = "
+ + columnCount + ", columnValues.length = "
+ + columnValues.length);
+ }
+
+ int start = rowCount++ * columnCount;
+ ensureCapacity(start + columnCount);
+ System.arraycopy(columnValues, 0, data, start, columnCount);
+ }
+
+ /**
+ * Adds a new row to the end with the given column values. Not safe
+ * for concurrent use.
+ *
+ * @throws IllegalArgumentException if {@code columnValues.size() !=
+ * columnNames.length}
+ * @param columnValues in the same order as the the column names specified
+ * at cursor construction time
+ */
+ @JNITarget
+ public void addRow(Iterable<?> columnValues) {
+ final int start = rowCount * columnCount;
+
+ if (columnValues instanceof ArrayList<?>) {
+ addRow((ArrayList<?>) columnValues, start);
+ return;
+ }
+
+ final int end = start + columnCount;
+ int current = start;
+
+ ensureCapacity(end);
+ final Object[] localData = data;
+ for (Object columnValue : columnValues) {
+ if (current == end) {
+ // TODO: null out row?
+ throw new IllegalArgumentException(
+ "columnValues.size() > columnNames.length");
+ }
+ localData[current++] = columnValue;
+ }
+
+ if (current != end) {
+ // TODO: null out row?
+ throw new IllegalArgumentException(
+ "columnValues.size() < columnNames.length");
+ }
+
+ // Increase row count here in case we encounter an exception.
+ rowCount++;
+ }
+
+ /** Optimization for {@link ArrayList}. */
+ @JNITarget
+ private void addRow(ArrayList<?> columnValues, int start) {
+ final int size = columnValues.size();
+ if (size != columnCount) {
+ throw new IllegalArgumentException("columnNames.length = "
+ + columnCount + ", columnValues.size() = " + size);
+ }
+
+ final int end = start + columnCount;
+ ensureCapacity(end);
+
+ // Take a reference just in case someone calls ensureCapacity
+ // and `data` gets replaced by a new array!
+ final Object[] localData = data;
+ for (int i = 0; i < size; i++) {
+ localData[start + i] = columnValues.get(i);
+ }
+
+ rowCount++;
+ }
+
+ /**
+ * Ensures that this cursor has enough capacity. If it needs to allocate
+ * a new array, the existing capacity will be at least doubled.
+ */
+ private void ensureCapacity(final int size) {
+ if (size <= data.length) {
+ return;
+ }
+
+ final Object[] oldData = this.data;
+ this.data = new Object[Math.max(size, data.length * 2)];
+ System.arraycopy(oldData, 0, this.data, 0, oldData.length);
+ }
+
+ /**
+ * Builds a row, starting from the left-most column and adding one column
+ * value at a time. Follows the same ordering as the column names specified
+ * at cursor construction time.
+ *
+ * Not thread-safe.
+ */
+ public class RowBuilder {
+ private int index;
+ private final int endIndex;
+
+ RowBuilder(int index, int endIndex) {
+ this.index = index;
+ this.endIndex = endIndex;
+ }
+
+ /**
+ * Sets the next column value in this row.
+ *
+ * @throws CursorIndexOutOfBoundsException if you try to add too many
+ * values
+ * @return this builder to support chaining
+ */
+ public RowBuilder add(final Object columnValue) {
+ if (index == endIndex) {
+ throw new CursorIndexOutOfBoundsException("No more columns left.");
+ }
+
+ data[index++] = columnValue;
+ return this;
+ }
+ }
+
+ /**
+ * Not thread safe.
+ */
+ public void set(int column, Object value) {
+ if (column < 0 || column >= columnCount) {
+ throw new CursorIndexOutOfBoundsException("Requested column: "
+ + column + ", # of columns: " + columnCount);
+ }
+ if (mPos < 0) {
+ throw new CursorIndexOutOfBoundsException("Before first row.");
+ }
+ if (mPos >= rowCount) {
+ throw new CursorIndexOutOfBoundsException("After last row.");
+ }
+ data[mPos * columnCount + column] = value;
+ }
+
+ // AbstractCursor implementation.
+ @Override
+ public int getCount() {
+ return rowCount;
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return columnNames;
+ }
+
+ @Override
+ public String getString(int column) {
+ Object value = get(column);
+ if (value == null) return null;
+ return value.toString();
+ }
+
+ @Override
+ public short getShort(int column) {
+ final Object value = get(column);
+ if (value == null) return 0;
+ if (value instanceof Number) return ((Number) value).shortValue();
+ return Short.parseShort(value.toString());
+ }
+
+ @Override
+ public int getInt(int column) {
+ Object value = get(column);
+ if (value == null) return 0;
+ if (value instanceof Number) return ((Number) value).intValue();
+ return Integer.parseInt(value.toString());
+ }
+
+ @Override
+ public long getLong(int column) {
+ Object value = get(column);
+ if (value == null) return 0;
+ if (value instanceof Number) return ((Number) value).longValue();
+ return Long.parseLong(value.toString());
+ }
+
+ @Override
+ public float getFloat(int column) {
+ Object value = get(column);
+ if (value == null) return 0.0f;
+ if (value instanceof Number) return ((Number) value).floatValue();
+ return Float.parseFloat(value.toString());
+ }
+
+ @Override
+ public double getDouble(int column) {
+ Object value = get(column);
+ if (value == null) return 0.0d;
+ if (value instanceof Number) return ((Number) value).doubleValue();
+ return Double.parseDouble(value.toString());
+ }
+
+ @Override
+ public byte[] getBlob(int column) {
+ Object value = get(column);
+ if (value == null) return null;
+ if (value instanceof byte[]) {
+ return (byte[]) value;
+ }
+
+ if (value instanceof ByteBuffer) {
+ final ByteBuffer bytes = (ByteBuffer) value;
+ byte[] byteArray = new byte[bytes.remaining()];
+ bytes.get(byteArray);
+ return byteArray;
+ }
+ throw new UnsupportedOperationException("BLOB Object not of known type");
+ }
+
+ @Override
+ public boolean isNull(int column) {
+ return get(column) == null;
+ }
+
+ @Override
+ protected void finalize() {
+ if (AppConstants.DEBUG_BUILD) {
+ if (!isClosed()) {
+ Log.e(LOGTAG, "Cursor finalized without being closed", this.allocationStack);
+ }
+ }
+
+ super.finalize();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridge.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridge.java
new file mode 100644
index 000000000..866b9e286
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridge.java
@@ -0,0 +1,387 @@
+/* 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.sqlite;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Map.Entry;
+
+/*
+ * This class allows using the mozsqlite3 library included with Firefox
+ * to read SQLite databases, instead of the Android SQLiteDataBase API,
+ * which might use whatever outdated DB is present on the Android system.
+ */
+public class SQLiteBridge {
+ private static final String LOGTAG = "SQLiteBridge";
+
+ // Path to the database. If this database was not opened with openDatabase, we reopen it every query.
+ private final String mDb;
+
+ // Pointer to the database if it was opened with openDatabase. 0 implies closed.
+ protected volatile long mDbPointer;
+
+ // Values remembered after a query.
+ private long[] mQueryResults;
+
+ private boolean mTransactionSuccess;
+ private boolean mInTransaction;
+
+ private static final int RESULT_INSERT_ROW_ID = 0;
+ private static final int RESULT_ROWS_CHANGED = 1;
+
+ // Shamelessly cribbed from db/sqlite3/src/moz.build.
+ private static final int DEFAULT_PAGE_SIZE_BYTES = 32768;
+
+ // The same size we use elsewhere.
+ private static final int MAX_WAL_SIZE_BYTES = 524288;
+
+ // JNI code in $(topdir)/mozglue/android/..
+ private static native MatrixBlobCursor sqliteCall(String aDb, String aQuery,
+ String[] aParams,
+ long[] aUpdateResult)
+ throws SQLiteBridgeException;
+ private static native MatrixBlobCursor sqliteCallWithDb(long aDb, String aQuery,
+ String[] aParams,
+ long[] aUpdateResult)
+ throws SQLiteBridgeException;
+ private static native long openDatabase(String aDb)
+ throws SQLiteBridgeException;
+ private static native void closeDatabase(long aDb);
+
+ // Takes the path to the database we want to access.
+ @RobocopTarget
+ public SQLiteBridge(String aDb) throws SQLiteBridgeException {
+ mDb = aDb;
+ }
+
+ // Executes a simple line of sql.
+ public void execSQL(String sql)
+ throws SQLiteBridgeException {
+ Cursor cursor = internalQuery(sql, null);
+ cursor.close();
+ }
+
+ // Executes a simple line of sql. Allow you to bind arguments
+ public void execSQL(String sql, String[] bindArgs)
+ throws SQLiteBridgeException {
+ Cursor cursor = internalQuery(sql, bindArgs);
+ cursor.close();
+ }
+
+ // Executes a DELETE statement on the database
+ public int delete(String table, String whereClause, String[] whereArgs)
+ throws SQLiteBridgeException {
+ StringBuilder sb = new StringBuilder("DELETE from ");
+ sb.append(table);
+ if (whereClause != null) {
+ sb.append(" WHERE " + whereClause);
+ }
+
+ execSQL(sb.toString(), whereArgs);
+ return (int)mQueryResults[RESULT_ROWS_CHANGED];
+ }
+
+ public Cursor query(String table,
+ String[] columns,
+ String selection,
+ String[] selectionArgs,
+ String groupBy,
+ String having,
+ String orderBy,
+ String limit)
+ throws SQLiteBridgeException {
+ StringBuilder sb = new StringBuilder("SELECT ");
+ if (columns != null)
+ sb.append(TextUtils.join(", ", columns));
+ else
+ sb.append(" * ");
+
+ sb.append(" FROM ");
+ sb.append(table);
+
+ if (selection != null) {
+ sb.append(" WHERE " + selection);
+ }
+
+ if (groupBy != null) {
+ sb.append(" GROUP BY " + groupBy);
+ }
+
+ if (having != null) {
+ sb.append(" HAVING " + having);
+ }
+
+ if (orderBy != null) {
+ sb.append(" ORDER BY " + orderBy);
+ }
+
+ if (limit != null) {
+ sb.append(" " + limit);
+ }
+
+ return rawQuery(sb.toString(), selectionArgs);
+ }
+
+ @RobocopTarget
+ public Cursor rawQuery(String sql, String[] selectionArgs)
+ throws SQLiteBridgeException {
+ return internalQuery(sql, selectionArgs);
+ }
+
+ public long insert(String table, String nullColumnHack, ContentValues values)
+ throws SQLiteBridgeException {
+ if (values == null)
+ return 0;
+
+ ArrayList<String> valueNames = new ArrayList<String>();
+ ArrayList<String> valueBinds = new ArrayList<String>();
+ ArrayList<String> keyNames = new ArrayList<String>();
+
+ for (Entry<String, Object> value : values.valueSet()) {
+ keyNames.add(value.getKey());
+
+ Object val = value.getValue();
+ if (val == null) {
+ valueNames.add("NULL");
+ } else {
+ valueNames.add("?");
+ valueBinds.add(val.toString());
+ }
+ }
+
+ StringBuilder sb = new StringBuilder("INSERT into ");
+ sb.append(table);
+
+ sb.append(" (");
+ sb.append(TextUtils.join(", ", keyNames));
+ sb.append(")");
+
+ // XXX - Do we need to bind these values?
+ sb.append(" VALUES (");
+ sb.append(TextUtils.join(", ", valueNames));
+ sb.append(") ");
+
+ String[] binds = new String[valueBinds.size()];
+ valueBinds.toArray(binds);
+ execSQL(sb.toString(), binds);
+ return mQueryResults[RESULT_INSERT_ROW_ID];
+ }
+
+ public int update(String table, ContentValues values, String whereClause, String[] whereArgs)
+ throws SQLiteBridgeException {
+ if (values == null)
+ return 0;
+
+ ArrayList<String> valueNames = new ArrayList<String>();
+
+ StringBuilder sb = new StringBuilder("UPDATE ");
+ sb.append(table);
+ sb.append(" SET ");
+
+ boolean isFirst = true;
+
+ for (Entry<String, Object> value : values.valueSet()) {
+ if (isFirst)
+ isFirst = false;
+ else
+ sb.append(", ");
+
+ sb.append(value.getKey());
+
+ Object val = value.getValue();
+ if (val == null) {
+ sb.append(" = NULL");
+ } else {
+ sb.append(" = ?");
+ valueNames.add(val.toString());
+ }
+ }
+
+ if (!TextUtils.isEmpty(whereClause)) {
+ sb.append(" WHERE ");
+ sb.append(whereClause);
+ valueNames.addAll(Arrays.asList(whereArgs));
+ }
+
+ String[] binds = new String[valueNames.size()];
+ valueNames.toArray(binds);
+
+ execSQL(sb.toString(), binds);
+ return (int)mQueryResults[RESULT_ROWS_CHANGED];
+ }
+
+ public int getVersion()
+ throws SQLiteBridgeException {
+ Cursor cursor = internalQuery("PRAGMA user_version", null);
+ int ret = -1;
+ if (cursor != null) {
+ cursor.moveToFirst();
+ String version = cursor.getString(0);
+ ret = Integer.parseInt(version);
+ cursor.close();
+ }
+ return ret;
+ }
+
+ // Do an SQL query, substituting the parameters in the query with the passed
+ // parameters. The parameters are substituted in order: named parameters
+ // are not supported.
+ private Cursor internalQuery(String aQuery, String[] aParams)
+ throws SQLiteBridgeException {
+
+ mQueryResults = new long[2];
+ if (isOpen()) {
+ return sqliteCallWithDb(mDbPointer, aQuery, aParams, mQueryResults);
+ }
+ return sqliteCall(mDb, aQuery, aParams, mQueryResults);
+ }
+
+ /*
+ * The second two parameters here are just provided for compatibility with SQLiteDatabase
+ * Support for them is not currently implemented.
+ */
+ public static SQLiteBridge openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags)
+ throws SQLiteException {
+ if (factory != null) {
+ throw new RuntimeException("factory not supported.");
+ }
+ if (flags != 0) {
+ throw new RuntimeException("flags not supported.");
+ }
+
+ SQLiteBridge bridge = null;
+ try {
+ bridge = new SQLiteBridge(path);
+ bridge.mDbPointer = SQLiteBridge.openDatabase(path);
+ } catch (SQLiteBridgeException ex) {
+ // Catch and rethrow as a SQLiteException to match SQLiteDatabase.
+ throw new SQLiteException(ex.getMessage());
+ }
+
+ prepareWAL(bridge);
+
+ return bridge;
+ }
+
+ public void close() {
+ if (isOpen()) {
+ closeDatabase(mDbPointer);
+ }
+ mDbPointer = 0L;
+ }
+
+ public boolean isOpen() {
+ return mDbPointer != 0;
+ }
+
+ public void beginTransaction() throws SQLiteBridgeException {
+ if (inTransaction()) {
+ throw new SQLiteBridgeException("Nested transactions are not supported");
+ }
+ execSQL("BEGIN EXCLUSIVE");
+ mTransactionSuccess = false;
+ mInTransaction = true;
+ }
+
+ public void beginTransactionNonExclusive() throws SQLiteBridgeException {
+ if (inTransaction()) {
+ throw new SQLiteBridgeException("Nested transactions are not supported");
+ }
+ execSQL("BEGIN IMMEDIATE");
+ mTransactionSuccess = false;
+ mInTransaction = true;
+ }
+
+ public void endTransaction() {
+ if (!inTransaction())
+ return;
+
+ try {
+ if (mTransactionSuccess) {
+ execSQL("COMMIT TRANSACTION");
+ } else {
+ execSQL("ROLLBACK TRANSACTION");
+ }
+ } catch (SQLiteBridgeException ex) {
+ Log.e(LOGTAG, "Error ending transaction", ex);
+ }
+ mInTransaction = false;
+ mTransactionSuccess = false;
+ }
+
+ public void setTransactionSuccessful() throws SQLiteBridgeException {
+ if (!inTransaction()) {
+ throw new SQLiteBridgeException("setTransactionSuccessful called outside a transaction");
+ }
+ mTransactionSuccess = true;
+ }
+
+ public boolean inTransaction() {
+ return mInTransaction;
+ }
+
+ @Override
+ public void finalize() {
+ if (isOpen()) {
+ Log.e(LOGTAG, "Bridge finalized without closing the database");
+ close();
+ }
+ }
+
+ private static void prepareWAL(final SQLiteBridge bridge) {
+ // Prepare for WAL mode. If we can, we switch to journal_mode=WAL, then
+ // set the checkpoint size appropriately. If we can't, then we fall back
+ // to truncating and synchronous writes.
+ final Cursor cursor = bridge.internalQuery("PRAGMA journal_mode=WAL", null);
+ try {
+ if (cursor.moveToFirst()) {
+ String journalMode = cursor.getString(0);
+ Log.d(LOGTAG, "Journal mode: " + journalMode);
+ if ("wal".equals(journalMode)) {
+ // Success! Let's make sure we autocheckpoint at a reasonable interval.
+ final int pageSizeBytes = bridge.getPageSizeBytes();
+ final int checkpointPageCount = MAX_WAL_SIZE_BYTES / pageSizeBytes;
+ bridge.execSQL("PRAGMA wal_autocheckpoint=" + checkpointPageCount);
+ } else {
+ if (!"truncate".equals(journalMode)) {
+ Log.w(LOGTAG, "Unable to activate WAL journal mode. Using truncate instead.");
+ bridge.execSQL("PRAGMA journal_mode=TRUNCATE");
+ }
+ Log.w(LOGTAG, "Not using WAL mode: using synchronous=FULL instead.");
+ bridge.execSQL("PRAGMA synchronous=FULL");
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private int getPageSizeBytes() {
+ if (!isOpen()) {
+ throw new IllegalStateException("Database not open.");
+ }
+
+ final Cursor cursor = internalQuery("PRAGMA page_size", null);
+ try {
+ if (!cursor.moveToFirst()) {
+ Log.w(LOGTAG, "Unable to retrieve page size.");
+ return DEFAULT_PAGE_SIZE_BYTES;
+ }
+
+ return cursor.getInt(0);
+ } finally {
+ cursor.close();
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridgeException.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridgeException.java
new file mode 100644
index 000000000..c7999fc5c
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridgeException.java
@@ -0,0 +1,18 @@
+/* -*- 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.sqlite;
+
+import org.mozilla.gecko.annotation.JNITarget;
+
+@JNITarget
+public class SQLiteBridgeException extends RuntimeException {
+ static final long serialVersionUID = 1L;
+
+ public SQLiteBridgeException() {}
+ public SQLiteBridgeException(String msg) {
+ super(msg);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandler.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandler.java
new file mode 100644
index 000000000..6f40ee96b
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandler.java
@@ -0,0 +1,11 @@
+/* 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.content.Intent;
+
+public interface ActivityResultHandler {
+ void onActivityResult(int resultCode, Intent data);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandlerMap.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandlerMap.java
new file mode 100644
index 000000000..dc1d26cec
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandlerMap.java
@@ -0,0 +1,24 @@
+/* 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.util.SparseArray;
+
+public final class ActivityResultHandlerMap {
+ private final SparseArray<ActivityResultHandler> mMap = new SparseArray<ActivityResultHandler>();
+ private int mCounter;
+
+ public synchronized int put(ActivityResultHandler handler) {
+ mMap.put(mCounter, handler);
+ return mCounter++;
+ }
+
+ public synchronized ActivityResultHandler getAndRemove(int i) {
+ ActivityResultHandler handler = mMap.get(i);
+ mMap.delete(i);
+
+ return handler;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityUtils.java
new file mode 100644
index 000000000..2f15e7868
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityUtils.java
@@ -0,0 +1,72 @@
+/* -*- 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.app.Activity;
+import android.content.Intent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+import org.mozilla.gecko.AppConstants.Versions;
+
+public class ActivityUtils {
+ private ActivityUtils() {
+ }
+
+ public static void setFullScreen(Activity activity, boolean fullscreen) {
+ // Hide/show the system notification bar
+ Window window = activity.getWindow();
+
+ if (Versions.feature16Plus) {
+ int newVis;
+ if (fullscreen) {
+ newVis = View.SYSTEM_UI_FLAG_FULLSCREEN;
+ if (Versions.feature19Plus) {
+ newVis |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+ } else {
+ newVis |= View.SYSTEM_UI_FLAG_LOW_PROFILE;
+ }
+ } else {
+ newVis = View.SYSTEM_UI_FLAG_VISIBLE;
+ }
+
+ window.getDecorView().setSystemUiVisibility(newVis);
+ } else {
+ window.setFlags(fullscreen ?
+ WindowManager.LayoutParams.FLAG_FULLSCREEN : 0,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+ }
+
+ public static boolean isFullScreen(final Activity activity) {
+ final Window window = activity.getWindow();
+
+ if (Versions.feature16Plus) {
+ final int vis = window.getDecorView().getSystemUiVisibility();
+ return (vis & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
+ }
+
+ final int flags = window.getAttributes().flags;
+ return ((flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0);
+ }
+
+ /**
+ * Finish this activity and launch the default home screen activity.
+ */
+ public static void goToHomeScreen(Activity activity) {
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+
+ intent.addCategory(Intent.CATEGORY_HOME);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ activity.startActivity(intent);
+
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java
new file mode 100644
index 000000000..9e9bb5a9e
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java
@@ -0,0 +1,25 @@
+/* -*- 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.mozilla.gecko.annotation.RobocopTarget;
+
+import android.os.Bundle;
+
+@RobocopTarget
+public interface BundleEventListener {
+ /**
+ * Handles a message sent from Gecko.
+ *
+ * @param event The name of the event being sent.
+ * @param message The message data.
+ * @param callback The callback interface for this message. A callback is provided only if the
+ * originating Messaging.sendRequest call included a callback argument;
+ * otherwise, callback will be null. All listeners for a given event are given
+ * the same callback object, and exactly one listener must handle the callback.
+ */
+ void handleMessage(String event, Bundle message, EventCallback callback);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/Clipboard.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/Clipboard.java
new file mode 100644
index 000000000..02b07674f
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/Clipboard.java
@@ -0,0 +1,117 @@
+/* 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 java.util.concurrent.SynchronousQueue;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.AppConstants.Versions;
+
+import android.content.ClipData;
+import android.content.Context;
+import android.util.Log;
+
+public final class Clipboard {
+ // Volatile but not synchronized: we don't care about the race condition in
+ // init, because both app contexts will be the same, but we do care about a
+ // thread having a stale null value of mContext.
+ volatile static Context mContext;
+ private final static String LOGTAG = "GeckoClipboard";
+ private final static SynchronousQueue<String> sClipboardQueue = new SynchronousQueue<String>();
+
+ private Clipboard() {
+ }
+
+ public static void init(final Context c) {
+ if (mContext != null) {
+ Log.w(LOGTAG, "Clipboard.init() called twice!");
+ return;
+ }
+ mContext = c.getApplicationContext();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static String getText() {
+ // If we're on the UI thread or the background thread, we have a looper on the thread
+ // and can just call this directly. For any other threads, post the call to the
+ // background thread.
+
+ if (ThreadUtils.isOnUiThread() || ThreadUtils.isOnBackgroundThread()) {
+ return getClipboardTextImpl();
+ }
+
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ String text = getClipboardTextImpl();
+ try {
+ sClipboardQueue.put(text != null ? text : "");
+ } catch (InterruptedException ie) { }
+ }
+ });
+
+ try {
+ return sClipboardQueue.take();
+ } catch (InterruptedException ie) {
+ return "";
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static void setText(final CharSequence text) {
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ // In API Level 11 and above, CLIPBOARD_SERVICE returns android.content.ClipboardManager,
+ // which is a subclass of android.text.ClipboardManager.
+ final android.content.ClipboardManager cm = (android.content.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ final ClipData clip = ClipData.newPlainText("Text", text);
+ try {
+ cm.setPrimaryClip(clip);
+ } catch (NullPointerException e) {
+ // Bug 776223: This is a Samsung clipboard bug. setPrimaryClip() can throw
+ // a NullPointerException if Samsung's /data/clipboard directory is full.
+ // Fortunately, the text is still successfully copied to the clipboard.
+ }
+ return;
+ }
+ });
+ }
+
+ /**
+ * @return true if the clipboard is nonempty, false otherwise.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ public static boolean hasText() {
+ android.content.ClipboardManager cm = (android.content.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ return cm.hasPrimaryClip();
+ }
+
+ /**
+ * Deletes all text from the clipboard.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ public static void clearText() {
+ setText(null);
+ }
+
+ /**
+ * On some devices, access to the clipboard service needs to happen
+ * on a thread with a looper, so this function requires a looper is
+ * present on the thread.
+ */
+ @SuppressWarnings("deprecation")
+ static String getClipboardTextImpl() {
+ android.content.ClipboardManager cm = (android.content.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ if (cm.hasPrimaryClip()) {
+ ClipData clip = cm.getPrimaryClip();
+ if (clip != null) {
+ ClipData.Item item = clip.getItemAt(0);
+ return item.coerceToText(mContext).toString();
+ }
+ }
+ return null;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ContextUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ContextUtils.java
new file mode 100644
index 000000000..3a37911b0
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ContextUtils.java
@@ -0,0 +1,51 @@
+/*
+ * 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.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.text.TextUtils;
+
+public class ContextUtils {
+ private static final String INSTALLER_GOOGLE_PLAY = "com.android.vending";
+
+ private ContextUtils() {}
+
+ /**
+ * @return {@link android.content.pm.PackageInfo#firstInstallTime} for the context's package.
+ * @throws PackageManager.NameNotFoundException Unexpected - we get the package name from the context so
+ * it's expected to be found.
+ */
+ public static PackageInfo getCurrentPackageInfo(final Context context) {
+ try {
+ return context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new AssertionError("Should not happen: Can't get package info of own package");
+ }
+ }
+
+ public static boolean isPackageInstalled(final Context context, String packageName) {
+ try {
+ PackageManager pm = context.getPackageManager();
+ pm.getPackageInfo(packageName, 0);
+ return true;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ public static boolean isInstalledFromGooglePlay(final Context context) {
+ final String installerPackageName = context.getPackageManager().getInstallerPackageName(context.getPackageName());
+
+ if (TextUtils.isEmpty(installerPackageName)) {
+ return false;
+ }
+
+ return INSTALLER_GOOGLE_PLAY.equals(installerPackageName);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DateUtil.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DateUtil.java
new file mode 100644
index 000000000..9d34a0fe8
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DateUtil.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.support.annotation.NonNull;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Utilities to help with manipulating Java's dates and calendars.
+ */
+public class DateUtil {
+ private DateUtil() {}
+
+ /**
+ * @param date the date to convert to HTTP format
+ * @return the date as specified in rfc 1123, e.g. "Tue, 01 Feb 2011 14:00:00 GMT"
+ */
+ public static String getDateInHTTPFormat(@NonNull final Date date) {
+ final DateFormat df = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss z", Locale.US);
+ df.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return df.format(date);
+ }
+
+ /**
+ * Returns the timezone offset for the current date in minutes. See
+ * {@link #getTimezoneOffsetInMinutesForGivenDate(Calendar)} for more details.
+ */
+ public static int getTimezoneOffsetInMinutes(@NonNull final TimeZone timezone) {
+ return getTimezoneOffsetInMinutesForGivenDate(Calendar.getInstance(timezone));
+ }
+
+ /**
+ * Returns the time zone offset for the given date in minutes. The date makes a difference due to daylight
+ * savings time in some regions. We return minutes because we can accurately represent time zones that are
+ * offset by non-integer hour values, e.g. parts of New Zealand at UTC+12:45.
+ *
+ * @param calendar A calendar with the appropriate time zone & date already set.
+ */
+ public static int getTimezoneOffsetInMinutesForGivenDate(@NonNull final Calendar calendar) {
+ // via Date.getTimezoneOffset deprecated docs (note: it had incorrect order of operations).
+ // Also, we cast to int because we should never overflow here - the max should be GMT+14 = 840.
+ return (int) TimeUnit.MILLISECONDS.toMinutes(calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET));
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java
new file mode 100644
index 000000000..099542666
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java
@@ -0,0 +1,29 @@
+package org.mozilla.gecko.util;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+/**
+ * Callback interface for Gecko requests.
+ *
+ * For each instance of EventCallback, exactly one of sendResponse, sendError, or sendCancel
+ * must be called to prevent observer leaks. If more than one send* method is called, or if a
+ * single send method is called multiple times, an {@link IllegalStateException} will be thrown.
+ */
+@RobocopTarget
+public interface EventCallback {
+ /**
+ * Sends a success response with the given data.
+ *
+ * @param response The response data to send to Gecko. Can be any of the types accepted by
+ * JSONObject#put(String, Object).
+ */
+ public void sendSuccess(Object response);
+
+ /**
+ * Sends an error response with the given data.
+ *
+ * @param response The response data to send to Gecko. Can be any of the types accepted by
+ * JSONObject#put(String, Object).
+ */
+ public void sendError(Object response);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FileUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FileUtils.java
new file mode 100644
index 000000000..01cdd42bb
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FileUtils.java
@@ -0,0 +1,259 @@
+/* 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.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.FilenameFilter;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+import java.util.Comparator;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+public class FileUtils {
+ private static final String LOGTAG = "GeckoFileUtils";
+
+ /*
+ * A basic Filter for checking a filename and age.
+ **/
+ static public class NameAndAgeFilter implements FilenameFilter {
+ final private String mName;
+ final private double mMaxAge;
+
+ public NameAndAgeFilter(String name, double age) {
+ mName = name;
+ mMaxAge = age;
+ }
+
+ @Override
+ public boolean accept(File dir, String filename) {
+ if (mName == null || mName.matches(filename)) {
+ File f = new File(dir, filename);
+
+ if (mMaxAge < 0 || System.currentTimeMillis() - f.lastModified() > mMaxAge) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ @RobocopTarget
+ public static void delTree(File dir, FilenameFilter filter, boolean recurse) {
+ String[] files = null;
+
+ if (filter != null) {
+ files = dir.list(filter);
+ } else {
+ files = dir.list();
+ }
+
+ if (files == null) {
+ return;
+ }
+
+ for (String file : files) {
+ File f = new File(dir, file);
+ delete(f, recurse);
+ }
+ }
+
+ public static boolean delete(File file) throws IOException {
+ return delete(file, true);
+ }
+
+ public static boolean delete(File file, boolean recurse) {
+ if (file.isDirectory() && recurse) {
+ // If the quick delete failed and this is a dir, recursively delete the contents of the dir
+ String files[] = file.list();
+ for (String temp : files) {
+ File fileDelete = new File(file, temp);
+ try {
+ delete(fileDelete);
+ } catch (IOException ex) {
+ Log.i(LOGTAG, "Error deleting " + fileDelete.getPath(), ex);
+ }
+ }
+ }
+
+ // Even if this is a dir, it should now be empty and delete should work
+ return file.delete();
+ }
+
+ /**
+ * A generic solution to read a JSONObject from a file. See
+ * {@link #readStringFromFile(File)} for more details.
+ *
+ * @throws IOException if the file is empty, or another IOException occurs
+ * @throws JSONException if the file could not be converted to a JSONObject.
+ */
+ public static JSONObject readJSONObjectFromFile(final File file) throws IOException, JSONException {
+ if (file.length() == 0) {
+ // Redirect this exception so it's clearer than when the JSON parser catches it.
+ throw new IOException("Given file is empty - the JSON parser cannot create an object from an empty file");
+ }
+ return new JSONObject(readStringFromFile(file));
+ }
+
+ /**
+ * A generic solution to read from a file. For more details,
+ * see {@link #readStringFromInputStreamAndCloseStream(InputStream, int)}.
+ *
+ * This method loads the entire file into memory so will have the expected performance impact.
+ * If you're trying to read a large file, you should be handling your own reading to avoid
+ * out-of-memory errors.
+ */
+ public static String readStringFromFile(final File file) throws IOException {
+ // FileInputStream will throw FileNotFoundException if the file does not exist, but
+ // File.length will return 0 if the file does not exist so we catch it sooner.
+ if (!file.exists()) {
+ throw new FileNotFoundException("Given file, " + file + ", does not exist");
+ } else if (file.length() == 0) {
+ return "";
+ }
+ final int len = (int) file.length(); // includes potential EOF character.
+ return readStringFromInputStreamAndCloseStream(new FileInputStream(file), len);
+ }
+
+ /**
+ * A generic solution to read from an input stream in UTF-8. This function will read from the stream until it
+ * is finished and close the stream - this is necessary to close the wrapping resources.
+ *
+ * For a higher-level method, see {@link #readStringFromFile(File)}.
+ *
+ * Since this is generic, it may not be the most performant for your use case.
+ *
+ * @param bufferSize Size of the underlying buffer for read optimizations - must be > 0.
+ */
+ public static String readStringFromInputStreamAndCloseStream(final InputStream inputStream, final int bufferSize)
+ throws IOException {
+ if (bufferSize <= 0) {
+ // Safe close: it's more important to alert the programmer of
+ // their error than to let them catch and continue on their way.
+ IOUtils.safeStreamClose(inputStream);
+ throw new IllegalArgumentException("Expected buffer size larger than 0. Got: " + bufferSize);
+ }
+
+ final StringBuilder stringBuilder = new StringBuilder(bufferSize);
+ final InputStreamReader reader = new InputStreamReader(inputStream, Charset.forName("UTF-8"));
+ try {
+ int charsRead;
+ final char[] buffer = new char[bufferSize];
+ while ((charsRead = reader.read(buffer, 0, bufferSize)) != -1) {
+ stringBuilder.append(buffer, 0, charsRead);
+ }
+ } finally {
+ reader.close();
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * A generic solution to write a JSONObject to a file.
+ * See {@link #writeStringToFile(File, String)} for more details.
+ */
+ public static void writeJSONObjectToFile(final File file, final JSONObject obj) throws IOException {
+ writeStringToFile(file, obj.toString());
+ }
+
+ /**
+ * A generic solution to write to a File - the given file will be overwritten. If it does not exist yet, it will
+ * be created. See {@link #writeStringToOutputStreamAndCloseStream(OutputStream, String)} for more details.
+ */
+ public static void writeStringToFile(final File file, final String str) throws IOException {
+ writeStringToOutputStreamAndCloseStream(new FileOutputStream(file, false), str);
+ }
+
+ /**
+ * A generic solution to write to an output stream in UTF-8. The stream will be closed at the
+ * completion of this method - it's necessary in order to close the wrapping resources.
+ *
+ * For a higher-level method, see {@link #writeStringToFile(File, String)}.
+ *
+ * Since this is generic, it may not be the most performant for your use case.
+ */
+ public static void writeStringToOutputStreamAndCloseStream(final OutputStream outputStream, final String str)
+ throws IOException {
+ try {
+ final OutputStreamWriter writer = new OutputStreamWriter(outputStream, Charset.forName("UTF-8"));
+ try {
+ writer.write(str);
+ } finally {
+ writer.close();
+ }
+ } finally {
+ // OutputStreamWriter.close can throw before closing the
+ // underlying stream. For safety, we close here too.
+ outputStream.close();
+ }
+ }
+
+ public static class FilenameWhitelistFilter implements FilenameFilter {
+ private final Set<String> mFilenameWhitelist;
+
+ public FilenameWhitelistFilter(final Set<String> filenameWhitelist) {
+ mFilenameWhitelist = filenameWhitelist;
+ }
+
+ @Override
+ public boolean accept(final File dir, final String filename) {
+ return mFilenameWhitelist.contains(filename);
+ }
+ }
+
+ public static class FilenameRegexFilter implements FilenameFilter {
+ private final Pattern mPattern;
+
+ // Each time `Pattern.matcher` is called, a new matcher is created. We can avoid the excessive object creation
+ // by caching the returned matcher and calling `Matcher.reset` on it. Since Matcher's are not thread safe,
+ // this assumes `FilenameFilter.accept` is not run in parallel (which, according to the source, it is not).
+ private Matcher mCachedMatcher;
+
+ public FilenameRegexFilter(final Pattern pattern) {
+ mPattern = pattern;
+ }
+
+ @Override
+ public boolean accept(final File dir, final String filename) {
+ if (mCachedMatcher == null) {
+ mCachedMatcher = mPattern.matcher(filename);
+ } else {
+ mCachedMatcher.reset(filename);
+ }
+ return mCachedMatcher.matches();
+ }
+ }
+
+ public static class FileLastModifiedComparator implements Comparator<File> {
+ @Override
+ public int compare(final File lhs, final File rhs) {
+ // Long.compare is API 19+.
+ final long lhsModified = lhs.lastModified();
+ final long rhsModified = rhs.lastModified();
+ if (lhsModified < rhsModified) {
+ return -1;
+ } else if (lhsModified == rhsModified) {
+ return 0;
+ } else {
+ return 1;
+ }
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FloatUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FloatUtils.java
new file mode 100644
index 000000000..fbcd7254f
--- /dev/null
+++ b/mobile/android/geckoview/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/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GamepadUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GamepadUtils.java
new file mode 100644
index 000000000..e22be8fd8
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GamepadUtils.java
@@ -0,0 +1,140 @@
+/* -*- 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.annotation.TargetApi;
+import android.os.Build;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+public final class GamepadUtils {
+ private static final int SONY_XPERIA_GAMEPAD_DEVICE_ID = 196611;
+
+ private static View.OnKeyListener sClickDispatcher;
+ private static float sDeadZoneThresholdOverride = 1e-2f;
+
+ private GamepadUtils() {
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
+ private static boolean isGamepadKey(KeyEvent event) {
+ if (Build.VERSION.SDK_INT < 12) {
+ return false;
+ }
+ return (event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD;
+ }
+
+ public static boolean isActionKey(KeyEvent event) {
+ return (isGamepadKey(event) && (event.getKeyCode() == KeyEvent.KEYCODE_BUTTON_A));
+ }
+
+ public static boolean isActionKeyDown(KeyEvent event) {
+ return isActionKey(event) && event.getAction() == KeyEvent.ACTION_DOWN;
+ }
+
+ public static boolean isBackKey(KeyEvent event) {
+ return (isGamepadKey(event) && (event.getKeyCode() == KeyEvent.KEYCODE_BUTTON_B));
+ }
+
+ public static void overrideDeadZoneThreshold(float threshold) {
+ sDeadZoneThresholdOverride = threshold;
+ }
+
+ public static boolean isValueInDeadZone(MotionEvent event, int axis) {
+ float threshold;
+ if (sDeadZoneThresholdOverride >= 0) {
+ threshold = sDeadZoneThresholdOverride;
+ } else {
+ InputDevice.MotionRange range = event.getDevice().getMotionRange(axis);
+ threshold = range.getFlat() + range.getFuzz();
+ }
+ float value = event.getAxisValue(axis);
+ return (Math.abs(value) < threshold);
+ }
+
+ public static boolean isPanningControl(MotionEvent event) {
+ if (Build.VERSION.SDK_INT < 12) {
+ return false;
+ }
+ if ((event.getSource() & InputDevice.SOURCE_CLASS_MASK) != InputDevice.SOURCE_CLASS_JOYSTICK) {
+ return false;
+ }
+ if (isValueInDeadZone(event, MotionEvent.AXIS_X)
+ && isValueInDeadZone(event, MotionEvent.AXIS_Y)
+ && isValueInDeadZone(event, MotionEvent.AXIS_Z)
+ && isValueInDeadZone(event, MotionEvent.AXIS_RZ)) {
+ return false;
+ }
+ return true;
+ }
+
+ public static View.OnKeyListener getClickDispatcher() {
+ if (sClickDispatcher == null) {
+ sClickDispatcher = new View.OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (isActionKeyDown(event)) {
+ return v.performClick();
+ }
+ return false;
+ }
+ };
+ }
+ return sClickDispatcher;
+ }
+
+ public static KeyEvent translateSonyXperiaGamepadKeys(int keyCode, KeyEvent event) {
+ // The cross and circle button mappings may be swapped in the different regions so
+ // determine if they are swapped so the proper key codes can be mapped to the keys
+ boolean areKeysSwapped = areSonyXperiaGamepadKeysSwapped();
+
+ // If a Sony Xperia, remap the cross and circle buttons to buttons
+ // A and B for the gamepad API
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ keyCode = (areKeysSwapped ? KeyEvent.KEYCODE_BUTTON_A : KeyEvent.KEYCODE_BUTTON_B);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ keyCode = (areKeysSwapped ? KeyEvent.KEYCODE_BUTTON_B : KeyEvent.KEYCODE_BUTTON_A);
+ break;
+
+ default:
+ return event;
+ }
+
+ return new KeyEvent(event.getAction(), keyCode);
+ }
+
+ public static boolean isSonyXperiaGamepadKeyEvent(KeyEvent event) {
+ return (event.getDeviceId() == SONY_XPERIA_GAMEPAD_DEVICE_ID &&
+ "Sony Ericsson".equals(Build.MANUFACTURER) &&
+ ("R800".equals(Build.MODEL) || "R800i".equals(Build.MODEL)));
+ }
+
+ private static boolean areSonyXperiaGamepadKeysSwapped() {
+ // The cross and circle buttons on Sony Xperia phones are swapped
+ // in different regions
+ // http://developer.sonymobile.com/2011/02/13/xperia-play-game-keys/
+ final char DEFAULT_O_BUTTON_LABEL = 0x25CB;
+
+ boolean swapped = false;
+ int[] deviceIds = InputDevice.getDeviceIds();
+
+ for (int i = 0; deviceIds != null && i < deviceIds.length; i++) {
+ KeyCharacterMap keyCharacterMap = KeyCharacterMap.load(deviceIds[i]);
+ if (keyCharacterMap != null && DEFAULT_O_BUTTON_LABEL ==
+ keyCharacterMap.getDisplayLabel(KeyEvent.KEYCODE_DPAD_CENTER)) {
+ swapped = true;
+ break;
+ }
+ }
+ return swapped;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java
new file mode 100644
index 000000000..442f782e2
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java
@@ -0,0 +1,76 @@
+/* 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 'GeckoBackgroundThread.class'.
+ private static Handler handler;
+ private static Thread thread;
+
+ // The initial Runnable to run on the new thread. Its purpose
+ // is to avoid us having to wait for the new thread to start.
+ private Runnable initialRunnable;
+
+ // Singleton, so private constructor.
+ private GeckoBackgroundThread(final Runnable initialRunnable) {
+ this.initialRunnable = initialRunnable;
+ }
+
+ @Override
+ public void run() {
+ setName(LOOPER_NAME);
+ Looper.prepare();
+
+ synchronized (GeckoBackgroundThread.class) {
+ handler = new Handler();
+ GeckoBackgroundThread.class.notify();
+ }
+
+ if (initialRunnable != null) {
+ initialRunnable.run();
+ initialRunnable = null;
+ }
+
+ Looper.loop();
+ }
+
+ private static void startThread(final Runnable initialRunnable) {
+ thread = new GeckoBackgroundThread(initialRunnable);
+ ThreadUtils.setBackgroundThread(thread);
+
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ // Get a Handler for a looper thread, or create one if it doesn't yet exist.
+ /*package*/ static synchronized Handler getHandler() {
+ if (thread == null) {
+ startThread(null);
+ }
+
+ while (handler == null) {
+ try {
+ GeckoBackgroundThread.class.wait();
+ } catch (final InterruptedException e) {
+ }
+ }
+ return handler;
+ }
+
+ /*package*/ static synchronized void post(final Runnable runnable) {
+ if (thread == null) {
+ startThread(runnable);
+ return;
+ }
+ getHandler().post(runnable);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoEventListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoEventListener.java
new file mode 100644
index 000000000..10336490b
--- /dev/null
+++ b/mobile/android/geckoview/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.annotation.RobocopTarget;
+
+@RobocopTarget
+public interface GeckoEventListener {
+ void handleMessage(String event, JSONObject message);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoJarReader.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoJarReader.java
new file mode 100644
index 000000000..4e11592a4
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoJarReader.java
@@ -0,0 +1,261 @@
+/* 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.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.Log;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.mozglue.GeckoLoader;
+import org.mozilla.gecko.mozglue.NativeZip;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Stack;
+
+/* Reads out of a multiple level deep jar file such as
+ * jar:jar:file:///data/app/org.mozilla.fennec.apk!/omni.ja!/chrome/chrome/content/branding/favicon32.png
+ */
+public final class GeckoJarReader {
+ private static final String LOGTAG = "GeckoJarReader";
+
+ private GeckoJarReader() {}
+
+ public static Bitmap getBitmap(Context context, Resources resources, String url) {
+ BitmapDrawable drawable = getBitmapDrawable(context, resources, url);
+ return (drawable != null) ? drawable.getBitmap() : null;
+ }
+
+ public static BitmapDrawable getBitmapDrawable(Context context, Resources resources,
+ String url) {
+ Stack<String> jarUrls = parseUrl(url);
+ InputStream inputStream = null;
+ BitmapDrawable bitmap = null;
+
+ NativeZip zip = null;
+ try {
+ // Load the initial jar file as a zip
+ zip = getZipFile(context, jarUrls.pop());
+ inputStream = getStream(zip, jarUrls, url);
+ if (inputStream != null) {
+ bitmap = new BitmapDrawable(resources, inputStream);
+ // BitmapDrawable created from a stream does not set the correct target density from resources.
+ // In fact it discards the resources https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/graphics/java/android/graphics/drawable/BitmapDrawable.java#191
+ bitmap.setTargetDensity(resources.getDisplayMetrics());
+ }
+ } catch (IOException | URISyntaxException ex) {
+ Log.e(LOGTAG, "Exception ", ex);
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException ex) {
+ Log.e(LOGTAG, "Error closing stream", ex);
+ }
+ }
+ }
+
+ return bitmap;
+ }
+
+ public static String getText(Context context, String url) {
+ Stack<String> jarUrls = parseUrl(url);
+
+ NativeZip zip = null;
+ BufferedReader reader = null;
+ String text = null;
+ try {
+ zip = getZipFile(context, jarUrls.pop());
+ InputStream input = getStream(zip, jarUrls, url);
+ if (input != null) {
+ reader = new BufferedReader(new InputStreamReader(input));
+ text = reader.readLine();
+ }
+ } catch (IOException | URISyntaxException ex) {
+ Log.e(LOGTAG, "Exception ", ex);
+ } finally {
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException ex) {
+ Log.e(LOGTAG, "Error closing reader", ex);
+ }
+ }
+ }
+
+ return text;
+ }
+
+ private static NativeZip getZipFile(Context context, String url)
+ throws IOException, URISyntaxException {
+ URI fileUrl = new URI(url);
+ GeckoLoader.loadMozGlue(context);
+ return new NativeZip(fileUrl.getPath());
+ }
+
+ @RobocopTarget
+ /**
+ * Extract a (possibly nested) file from an archive and write it to a temporary file.
+ *
+ * @param context Android context.
+ * @param url to open. Can include jar: to "reach into" nested archives.
+ * @param dir to write temporary file to.
+ * @return a <code>File</code>, if one could be written; otherwise null.
+ * @throws IOException if an error occured.
+ */
+ public static File extractStream(Context context, String url, File dir, String suffix) throws IOException {
+ InputStream input = null;
+ try {
+ try {
+ final URI fileURI = new URI(url);
+ // We don't check the scheme because we want to catch bare files, not just file:// URIs.
+ // If we let bare files through, we'd try to open them as ZIP files later -- and crash in native code.
+ if (fileURI != null && fileURI.getPath() != null) {
+ final File inputFile = new File(fileURI.getPath());
+ if (inputFile != null && inputFile.exists()) {
+ input = new FileInputStream(inputFile);
+ }
+ }
+ } catch (URISyntaxException e) {
+ // Not a file:// URI.
+ }
+ if (input == null) {
+ // No luck with file:// URI; maybe some other URI?
+ input = getStream(context, url);
+ }
+ if (input == null) {
+ // Not found!
+ return null;
+ }
+
+ // n.b.: createTempFile does not in fact delete the file.
+ final File file = File.createTempFile("extractStream", suffix, dir);
+ OutputStream output = null;
+ try {
+ output = new FileOutputStream(file);
+ byte[] buf = new byte[8192];
+ int len;
+ while ((len = input.read(buf)) >= 0) {
+ output.write(buf, 0, len);
+ }
+ return file;
+ } finally {
+ if (output != null) {
+ output.close();
+ }
+ }
+ } finally {
+ if (input != null) {
+ try {
+ input.close();
+ } catch (IOException e) {
+ Log.w(LOGTAG, "Got exception closing stream; ignoring.", e);
+ }
+ }
+ }
+ }
+
+ @RobocopTarget
+ public static InputStream getStream(Context context, String url) {
+ Stack<String> jarUrls = parseUrl(url);
+ try {
+ NativeZip zip = getZipFile(context, jarUrls.pop());
+ return getStream(zip, jarUrls, url);
+ } catch (Exception ex) {
+ // Some JNI code throws IllegalArgumentException on a bad file name;
+ // swallow the error and return null. We could also see legitimate
+ // IOExceptions here.
+ Log.e(LOGTAG, "Exception getting input stream from jar URL: " + url, ex);
+ return null;
+ }
+ }
+
+ private static InputStream getStream(NativeZip zip, Stack<String> jarUrls, String origUrl) {
+ InputStream inputStream = null;
+
+ // loop through children jar files until we reach the innermost one
+ while (!jarUrls.empty()) {
+ String fileName = jarUrls.pop();
+
+ if (inputStream != null) {
+ // intermediate NativeZips and InputStreams will be garbage collected.
+ try {
+ zip = new NativeZip(inputStream);
+ } catch (IllegalArgumentException e) {
+ String description = "!!! BUG 849589 !!! origUrl=" + origUrl;
+ Log.e(LOGTAG, description, e);
+ throw new IllegalArgumentException(description);
+ }
+ }
+
+ inputStream = zip.getInputStream(fileName);
+ if (inputStream == null) {
+ Log.d(LOGTAG, "No Entry for " + fileName);
+ return null;
+ }
+ }
+
+ return inputStream;
+ }
+
+ /* Returns a stack of strings breaking the url up into pieces. Each piece
+ * is assumed to point to a jar file except for the final one. Callers should
+ * pass in the url to parse, and null for the parent parameter (used for recursion)
+ * For example, jar:jar:file:///data/app/org.mozilla.fennec.apk!/omni.ja!/chrome/chrome/content/branding/favicon32.png
+ * will return:
+ * file:///data/app/org.mozilla.fennec.apk
+ * omni.ja
+ * chrome/chrome/content/branding/favicon32.png
+ */
+ private static Stack<String> parseUrl(String url) {
+ return parseUrl(url, null);
+ }
+
+ private static Stack<String> parseUrl(String url, Stack<String> results) {
+ if (results == null) {
+ results = new Stack<String>();
+ }
+
+ if (url.startsWith("jar:")) {
+ int jarEnd = url.lastIndexOf("!");
+ String subStr = url.substring(4, jarEnd);
+ results.push(url.substring(jarEnd + 2)); // remove the !/ characters
+ return parseUrl(subStr, results);
+ } else {
+ results.push(url);
+ return results;
+ }
+ }
+
+ public static String getJarURL(Context context, String pathInsideJAR) {
+ // We need to encode the package resource path, because it might contain illegal characters. For example:
+ // /mnt/asec2/[2]org.mozilla.fennec-1/pkg.apk
+ // The round-trip through a URI does this for us.
+ final String resourcePath = context.getPackageResourcePath();
+ return computeJarURI(resourcePath, pathInsideJAR);
+ }
+
+ /**
+ * Encodes its resource path correctly.
+ */
+ @RobocopTarget
+ public static String computeJarURI(String resourcePath, String pathInsideJAR) {
+ final String resURI = new File(resourcePath).toURI().toString();
+
+ // TODO: do we need to encode the file path, too?
+ return "jar:jar:" + resURI + "!/" + AppConstants.OMNIJAR_NAME + "!/" + pathInsideJAR;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoRequest.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoRequest.java
new file mode 100644
index 000000000..a57ed7f08
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoRequest.java
@@ -0,0 +1,94 @@
+/* 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 java.util.concurrent.atomic.AtomicInteger;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+import android.util.Log;
+
+public abstract class GeckoRequest {
+ private static final String LOGTAG = "GeckoRequest";
+ private static final AtomicInteger currentId = new AtomicInteger(0);
+
+ private final int id = currentId.getAndIncrement();
+ private final String name;
+ private final String data;
+
+ /**
+ * Creates a request that can be dispatched using
+ * {@link GeckoAppShell#sendRequestToGecko(GeckoRequest)}.
+ *
+ * @param name The name of the event associated with this request, which must have a
+ * Gecko-side listener registered to respond to this request.
+ * @param data Data to send with this request, which can be any object serializable by
+ * {@link JSONObject#put(String, Object)}.
+ */
+ @RobocopTarget
+ public GeckoRequest(String name, Object data) {
+ this.name = name;
+ final JSONObject message = new JSONObject();
+ try {
+ message.put("id", id);
+ message.put("data", data);
+ } catch (JSONException e) {
+ Log.e(LOGTAG, "JSON error", e);
+ }
+ this.data = message.toString();
+ }
+
+ /**
+ * Gets the ID for this request.
+ *
+ * @return The request ID
+ */
+ public int getId() {
+ return id;
+ }
+
+ /**
+ * Gets the event name associated with this request.
+ *
+ * @return The name of the event sent to Gecko
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Gets the stringified data associated with this request.
+ *
+ * @return The data being sent with the request
+ */
+ public String getData() {
+ return data;
+ }
+
+ /**
+ * Callback executed when the request succeeds.
+ *
+ * @param nativeJSObject The response data from Gecko
+ */
+ @RobocopTarget
+ public abstract void onResponse(NativeJSObject nativeJSObject);
+
+ /**
+ * Callback executed when the request fails.
+ *
+ * By default, an exception is thrown. This should be overridden if the
+ * GeckoRequest is able to recover from the error.
+ *
+ * @throws RuntimeException
+ */
+ @RobocopTarget
+ public void onError(NativeJSObject error) {
+ final String message = error.optString("message", "<no message>");
+ final String stack = error.optString("stack", "<no stack>");
+ throw new RuntimeException("Unhandled error for GeckoRequest " + name + ": " + message + "\nJS stack:\n" + stack);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java
new file mode 100644
index 000000000..864462d9b
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.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 org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.AppConstants.Versions;
+
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecList;
+import android.util.Log;
+
+public final class HardwareCodecCapabilityUtils {
+ private static final String LOGTAG = "GeckoHardwareCodecCapabilityUtils";
+
+ // List of supported HW VP8 encoders.
+ private static final String[] supportedVp8HwEncCodecPrefixes =
+ {"OMX.qcom.", "OMX.Intel." };
+ // List of supported HW VP8 decoders.
+ private static final String[] supportedVp8HwDecCodecPrefixes =
+ {"OMX.qcom.", "OMX.Nvidia.", "OMX.Exynos.", "OMX.Intel." };
+ private static final String VP8_MIME_TYPE = "video/x-vnd.on2.vp8";
+ private static final String VP9_MIME_TYPE = "video/x-vnd.on2.vp9";
+ // NV12 color format supported by QCOM codec, but not declared in MediaCodec -
+ // see /hardware/qcom/media/mm-core/inc/OMX_QCOMExtns.h
+ private static final int
+ COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m = 0x7FA30C04;
+ // Allowable color formats supported by codec - in order of preference.
+ private static final int[] supportedColorList = {
+ CodecCapabilities.COLOR_FormatYUV420Planar,
+ CodecCapabilities.COLOR_FormatYUV420SemiPlanar,
+ CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar,
+ COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m
+ };
+
+ @WrapForJNI
+ public static boolean findDecoderCodecInfoForMimeType(String aMimeType) {
+ for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
+ MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
+ if (info.isEncoder()) {
+ continue;
+ }
+ for (String mimeType : info.getSupportedTypes()) {
+ if (mimeType.equals(aMimeType)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public static boolean getHWEncoderCapability() {
+ if (Versions.feature20Plus) {
+ for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
+ MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
+ if (!info.isEncoder()) {
+ continue;
+ }
+ String name = null;
+ for (String mimeType : info.getSupportedTypes()) {
+ if (mimeType.equals(VP8_MIME_TYPE)) {
+ name = info.getName();
+ break;
+ }
+ }
+ if (name == null) {
+ continue; // No HW support in this codec; try the next one.
+ }
+ Log.e(LOGTAG, "Found candidate encoder " + name);
+
+ // Check if this is supported encoder.
+ boolean supportedCodec = false;
+ for (String codecPrefix : supportedVp8HwEncCodecPrefixes) {
+ if (name.startsWith(codecPrefix)) {
+ supportedCodec = true;
+ break;
+ }
+ }
+ if (!supportedCodec) {
+ continue;
+ }
+
+ // Check if codec supports either yuv420 or nv12.
+ CodecCapabilities capabilities =
+ info.getCapabilitiesForType(VP8_MIME_TYPE);
+ for (int colorFormat : capabilities.colorFormats) {
+ Log.v(LOGTAG, " Color: 0x" + Integer.toHexString(colorFormat));
+ }
+ for (int supportedColorFormat : supportedColorList) {
+ for (int codecColorFormat : capabilities.colorFormats) {
+ if (codecColorFormat == supportedColorFormat) {
+ // Found supported HW Encoder.
+ Log.e(LOGTAG, "Found target encoder " + name +
+ ". Color: 0x" + Integer.toHexString(codecColorFormat));
+ return true;
+ }
+ }
+ }
+ }
+ }
+ // No HW encoder.
+ return false;
+ }
+
+ public static boolean getHWDecoderCapability() {
+ return getHWDecoderCapability(VP8_MIME_TYPE);
+ }
+
+ @WrapForJNI
+ public static boolean HasHWVP9() {
+ return getHWDecoderCapability(VP9_MIME_TYPE);
+ }
+
+ public static boolean getHWDecoderCapability(String aMimeType) {
+ if (Versions.feature20Plus) {
+ for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
+ MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
+ if (info.isEncoder()) {
+ continue;
+ }
+ String name = null;
+ for (String mimeType : info.getSupportedTypes()) {
+ if (mimeType.equals(aMimeType)) {
+ name = info.getName();
+ break;
+ }
+ }
+ if (name == null) {
+ continue; // No HW support in this codec; try the next one.
+ }
+ Log.e(LOGTAG, "Found candidate decoder " + name);
+
+ // Check if this is supported decoder.
+ boolean supportedCodec = false;
+ for (String codecPrefix : supportedVp8HwDecCodecPrefixes) {
+ if (name.startsWith(codecPrefix)) {
+ supportedCodec = true;
+ break;
+ }
+ }
+ if (!supportedCodec) {
+ continue;
+ }
+
+ // Check if codec supports either yuv420 or nv12.
+ CodecCapabilities capabilities =
+ info.getCapabilitiesForType(aMimeType);
+ for (int colorFormat : capabilities.colorFormats) {
+ Log.v(LOGTAG, " Color: 0x" + Integer.toHexString(colorFormat));
+ }
+ for (int supportedColorFormat : supportedColorList) {
+ for (int codecColorFormat : capabilities.colorFormats) {
+ if (codecColorFormat == supportedColorFormat) {
+ // Found supported HW decoder.
+ Log.e(LOGTAG, "Found target decoder " + name +
+ ". Color: 0x" + Integer.toHexString(codecColorFormat));
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false; // No HW decoder.
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java
new file mode 100644
index 000000000..ba92d08cb
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java
@@ -0,0 +1,117 @@
+/* -*- 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.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.SysInfo;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.util.Log;
+import android.view.ViewConfiguration;
+
+public final class HardwareUtils {
+ private static final String LOGTAG = "GeckoHardwareUtils";
+
+ private static final boolean IS_AMAZON_DEVICE = Build.MANUFACTURER.equalsIgnoreCase("Amazon");
+ public static final boolean IS_KINDLE_DEVICE = IS_AMAZON_DEVICE &&
+ (Build.MODEL.equals("Kindle Fire") ||
+ Build.MODEL.startsWith("KF"));
+
+ private static volatile boolean sInited;
+
+ // These are all set once, during init.
+ private static volatile boolean sIsLargeTablet;
+ private static volatile boolean sIsSmallTablet;
+ private static volatile boolean sIsTelevision;
+
+ private HardwareUtils() {
+ }
+
+ public static void init(Context context) {
+ if (sInited) {
+ // This is unavoidable, given that HardwareUtils is called from background services.
+ Log.d(LOGTAG, "HardwareUtils already inited.");
+ return;
+ }
+
+ // Pre-populate common flags from the context.
+ final int screenLayoutSize = context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
+ if (Build.VERSION.SDK_INT >= 11) {
+ if (screenLayoutSize == Configuration.SCREENLAYOUT_SIZE_XLARGE) {
+ sIsLargeTablet = true;
+ } else if (screenLayoutSize == Configuration.SCREENLAYOUT_SIZE_LARGE) {
+ sIsSmallTablet = true;
+ }
+ if (Build.VERSION.SDK_INT >= 16) {
+ if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
+ sIsTelevision = true;
+ }
+ }
+ }
+
+ sInited = true;
+ }
+
+ public static boolean isTablet() {
+ return sIsLargeTablet || sIsSmallTablet;
+ }
+
+ public static boolean isLargeTablet() {
+ return sIsLargeTablet;
+ }
+
+ public static boolean isSmallTablet() {
+ return sIsSmallTablet;
+ }
+
+ public static boolean isTelevision() {
+ return sIsTelevision;
+ }
+
+ public static int getMemSize() {
+ return SysInfo.getMemSize();
+ }
+
+ public static boolean isARMSystem() {
+ return Build.CPU_ABI != null && Build.CPU_ABI.equals("armeabi-v7a");
+ }
+
+ public static boolean isX86System() {
+ return Build.CPU_ABI != null && Build.CPU_ABI.equals("x86");
+ }
+
+ /**
+ * @return false if the current system is not supported (e.g. APK/system ABI mismatch).
+ */
+ public static boolean isSupportedSystem() {
+ if (Build.VERSION.SDK_INT < AppConstants.Versions.MIN_SDK_VERSION ||
+ Build.VERSION.SDK_INT > AppConstants.Versions.MAX_SDK_VERSION) {
+ return false;
+ }
+
+ // See http://developer.android.com/ndk/guides/abis.html
+ final boolean isSystemARM = isARMSystem();
+ final boolean isSystemX86 = isX86System();
+
+ final boolean isAppARM = AppConstants.ANDROID_CPU_ARCH.startsWith("armeabi-v7a");
+ final boolean isAppX86 = AppConstants.ANDROID_CPU_ARCH.startsWith("x86");
+
+ // Only reject known incompatible ABIs. Better safe than sorry.
+ if ((isSystemX86 && isAppARM) || (isSystemARM && isAppX86)) {
+ return false;
+ }
+
+ if ((isSystemX86 && isAppX86) || (isSystemARM && isAppARM)) {
+ return true;
+ }
+
+ Log.w(LOGTAG, "Unknown app/system ABI combination: " + AppConstants.MOZ_APP_ABI + " / " + Build.CPU_ABI);
+ return true;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INIParser.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INIParser.java
new file mode 100644
index 000000000..ed0706320
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INIParser.java
@@ -0,0 +1,176 @@
+/* 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 java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+public final class INIParser extends INISection {
+ // default file to read and write to
+ private final File mFile;
+
+ // List of sections in the current iniFile. null if the file has not been parsed yet
+ private Hashtable<String, INISection> mSections;
+
+ // create a parser. The file will not be read until you attempt to
+ // access sections or properties inside it. At that point its read synchronously
+ public INIParser(File iniFile) {
+ super("");
+ mFile = iniFile;
+ }
+
+ // write ini data to the default file. Will overwrite anything current inside
+ public void write() {
+ writeTo(mFile);
+ }
+
+ // write to the specified file. Will overwrite anything current inside
+ public void writeTo(File f) {
+ if (f == null)
+ return;
+
+ FileWriter outputStream = null;
+ try {
+ outputStream = new FileWriter(f);
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ }
+
+ BufferedWriter writer = new BufferedWriter(outputStream);
+ try {
+ write(writer);
+ writer.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void write(BufferedWriter writer) throws IOException {
+ super.write(writer);
+
+ if (mSections != null) {
+ for (Enumeration<INISection> e = mSections.elements(); e.hasMoreElements();) {
+ INISection section = e.nextElement();
+ section.write(writer);
+ writer.newLine();
+ }
+ }
+ }
+
+ // return all of the sections inside this file
+ public Hashtable<String, INISection> getSections() {
+ if (mSections == null) {
+ try {
+ parse();
+ } catch (IOException e) {
+ debug("Error parsing: " + e);
+ }
+ }
+ return mSections;
+ }
+
+ // parse the default file
+ @Override
+ protected void parse() throws IOException {
+ super.parse();
+ parse(mFile);
+ }
+
+ // parse a passed in file
+ private void parse(File f) throws IOException {
+ // Set up internal data members
+ mSections = new Hashtable<String, INISection>();
+
+ if (f == null || !f.exists())
+ return;
+
+ FileReader inputStream = null;
+ try {
+ inputStream = new FileReader(f);
+ } catch (FileNotFoundException e1) {
+ // If the file doesn't exist. Just return;
+ return;
+ }
+
+ BufferedReader buf = new BufferedReader(inputStream);
+ String line = null; // current line of text we are parsing
+ INISection currentSection = null; // section we are currently parsing
+
+ while ((line = buf.readLine()) != null) {
+
+ if (line != null)
+ line = line.trim();
+
+ // blank line or a comment. ignore it
+ if (line == null || line.length() == 0 || line.charAt(0) == ';') {
+ debug("Ignore line: " + line);
+ } else if (line.charAt(0) == '[') {
+ debug("Parse as section: " + line);
+ currentSection = new INISection(line.substring(1, line.length() - 1));
+ mSections.put(currentSection.getName(), currentSection);
+ } else {
+ debug("Parse as property: " + line);
+
+ String[] pieces = line.split("=");
+ if (pieces.length != 2)
+ continue;
+
+ String key = pieces[0].trim();
+ String value = pieces[1].trim();
+ if (currentSection != null) {
+ currentSection.setProperty(key, value);
+ } else {
+ mProperties.put(key, value);
+ }
+ }
+ }
+ buf.close();
+ }
+
+ // add a section to the file
+ public void addSection(INISection sect) {
+ // ensure that we have parsed the file
+ getSections();
+ mSections.put(sect.getName(), sect);
+ }
+
+ // get a section from the file. will return null if the section doesn't exist
+ public INISection getSection(String key) {
+ // ensure that we have parsed the file
+ getSections();
+ return mSections.get(key);
+ }
+
+ // remove an entire section from the file
+ public void removeSection(String name) {
+ // ensure that we have parsed the file
+ getSections();
+ mSections.remove(name);
+ }
+
+ // rename a section; nuking any previous section with the new
+ // name in the process
+ public void renameSection(String oldName, String newName) {
+ // ensure that we have parsed the file
+ getSections();
+
+ mSections.remove(newName);
+ INISection section = mSections.get(oldName);
+ if (section == null)
+ return;
+
+ section.setName(newName);
+ mSections.remove(oldName);
+ mSections.put(newName, section);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INISection.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INISection.java
new file mode 100644
index 000000000..af91ad410
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INISection.java
@@ -0,0 +1,123 @@
+/* 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.text.TextUtils;
+import android.util.Log;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+public class INISection {
+ private static final String LOGTAG = "INIParser";
+
+ // default file to read and write to
+ private String mName;
+ public String getName() { return mName; }
+ public void setName(String name) { mName = name; }
+
+ // show or hide debug logging
+ private boolean mDebug;
+
+ // Global properties that aren't inside a section in the file
+ protected Hashtable<String, Object> mProperties;
+
+ // create a parser. The file will not be read until you attempt to
+ // access sections or properties inside it. At that point its read synchronously
+ public INISection(String name) {
+ mName = name;
+ }
+
+ // log a debug string to the console
+ protected void debug(String msg) {
+ if (mDebug) {
+ Log.i(LOGTAG, msg);
+ }
+ }
+
+ // get a global property out of the hash table. will return null if the property doesn't exist
+ public Object getProperty(String key) {
+ getProperties(); // ensure that we have parsed the file
+ return mProperties.get(key);
+ }
+
+ // get a global property out of the hash table. will return null if the property doesn't exist
+ public int getIntProperty(String key) {
+ Object val = getProperty(key);
+ if (val == null)
+ return -1;
+
+ return Integer.parseInt(val.toString());
+ }
+
+ // get a global property out of the hash table. will return null if the property doesn't exist
+ public String getStringProperty(String key) {
+ Object val = getProperty(key);
+ if (val == null)
+ return null;
+
+ return val.toString();
+ }
+
+ // get a hashtable of all the global properties in this file
+ public Hashtable<String, Object> getProperties() {
+ if (mProperties == null) {
+ try {
+ parse();
+ } catch (IOException e) {
+ debug("Error parsing: " + e);
+ }
+ }
+ return mProperties;
+ }
+
+ // do nothing for generic sections
+ protected void parse() throws IOException {
+ mProperties = new Hashtable<String, Object>();
+ }
+
+ // set a property. Will erase the property if value = null
+ public void setProperty(String key, Object value) {
+ getProperties(); // ensure that we have parsed the file
+ if (value == null)
+ removeProperty(key);
+ else
+ mProperties.put(key.trim(), value);
+ }
+
+ // remove a property
+ public void removeProperty(String name) {
+ // ensure that we have parsed the file
+ getProperties();
+ mProperties.remove(name);
+ }
+
+ public void write(BufferedWriter writer) throws IOException {
+ if (!TextUtils.isEmpty(mName)) {
+ writer.write("[" + mName + "]");
+ writer.newLine();
+ }
+
+ if (mProperties != null) {
+ for (Enumeration<String> e = mProperties.keys(); e.hasMoreElements();) {
+ String key = e.nextElement();
+ writeProperty(writer, key, mProperties.get(key));
+ }
+ }
+ writer.newLine();
+ }
+
+ // Helper function to write out a property
+ private void writeProperty(BufferedWriter writer, String key, Object value) {
+ try {
+ writer.write(key + "=" + value);
+ writer.newLine();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IOUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IOUtils.java
new file mode 100644
index 000000000..62eee5192
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IOUtils.java
@@ -0,0 +1,129 @@
+/* -*- 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.util.Log;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Static helper class containing useful methods for manipulating IO objects.
+ */
+public class IOUtils {
+ private static final String LOGTAG = "GeckoIOUtils";
+
+ /**
+ * Represents the result of consuming an input stream, holding the returned data as well
+ * as the length of the data returned.
+ * The byte[] is not guaranteed to be trimmed to the size of the data acquired from the stream:
+ * hence the need for the length field. This strategy avoids the need to copy the data into a
+ * trimmed buffer after consumption.
+ */
+ public static class ConsumedInputStream {
+ public final int consumedLength;
+ // Only reassigned in getTruncatedData.
+ private byte[] consumedData;
+
+ public ConsumedInputStream(int consumedLength, byte[] consumedData) {
+ this.consumedLength = consumedLength;
+ this.consumedData = consumedData;
+ }
+
+ /**
+ * Get the data trimmed to the length of the actual payload read, caching the result.
+ */
+ public byte[] getTruncatedData() {
+ if (consumedData.length == consumedLength) {
+ return consumedData;
+ }
+
+ consumedData = truncateBytes(consumedData, consumedLength);
+ return consumedData;
+ }
+
+ public byte[] getData() {
+ return consumedData;
+ }
+ }
+
+ /**
+ * Fully read an InputStream into a byte array.
+ * @param iStream the InputStream to consume.
+ * @param bufferSize The initial size of the buffer to allocate. It will be grown as
+ * needed, but if the caller knows something about the InputStream then
+ * passing a good value here can improve performance.
+ */
+ public static ConsumedInputStream readFully(InputStream iStream, int bufferSize) {
+ // Allocate a buffer to hold the raw data downloaded.
+ byte[] buffer = new byte[bufferSize];
+
+ // The offset of the start of the buffer's free space.
+ int bPointer = 0;
+
+ // The quantity of bytes the last call to read yielded.
+ int lastRead = 0;
+ try {
+ // Fully read the data into the buffer.
+ while (lastRead != -1) {
+ // Read as many bytes as are currently available into the buffer.
+ lastRead = iStream.read(buffer, bPointer, buffer.length - bPointer);
+ bPointer += lastRead;
+
+ // If buffer has overflowed, double its size and carry on.
+ if (bPointer == buffer.length) {
+ bufferSize *= 2;
+ byte[] newBuffer = new byte[bufferSize];
+
+ // Copy the contents of the old buffer into the new buffer.
+ System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
+ buffer = newBuffer;
+ }
+ }
+
+ return new ConsumedInputStream(bPointer + 1, buffer);
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Error consuming input stream.", e);
+ } finally {
+ try {
+ iStream.close();
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Error closing input stream.", e);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Truncate a given byte[] to a given length. Returns a new byte[] with the first length many
+ * bytes of the input.
+ */
+ public static byte[] truncateBytes(byte[] bytes, int length) {
+ byte[] newBytes = new byte[length];
+ System.arraycopy(bytes, 0, newBytes, 0, length);
+
+ return newBytes;
+ }
+
+ public static void safeStreamClose(Closeable stream) {
+ try {
+ if (stream != null)
+ stream.close();
+ } catch (IOException e) { }
+ }
+
+ public static void copy(InputStream in, OutputStream out) throws IOException {
+ byte[] buffer = new byte[4096];
+ int len;
+
+ while ((len = in.read(buffer)) != -1) {
+ out.write(buffer, 0, len);
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputOptionsUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputOptionsUtils.java
new file mode 100644
index 000000000..55c02e4da
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputOptionsUtils.java
@@ -0,0 +1,45 @@
+/* -*- 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.content.Context;
+import android.content.Intent;
+import android.speech.RecognizerIntent;
+
+public class InputOptionsUtils {
+ public static boolean supportsVoiceRecognizer(Context context, String prompt) {
+ final Intent intent = createVoiceRecognizerIntent(prompt);
+ return intent.resolveActivity(context.getPackageManager()) != null;
+ }
+
+ public static Intent createVoiceRecognizerIntent(String prompt) {
+ final Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+ intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
+ intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1);
+ intent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
+ return intent;
+ }
+
+ public static boolean supportsIntent(Intent intent, Context context) {
+ return intent.resolveActivity(context.getPackageManager()) != null;
+ }
+
+ public static boolean supportsQrCodeReader(Context context) {
+ final Intent intent = createQRCodeReaderIntent();
+ return supportsIntent(intent, context);
+ }
+
+ public static Intent createQRCodeReaderIntent() {
+ // Bug 602818 enables QR code input if you have the particular app below installed in your device
+ final String appPackage = "com.google.zxing.client.android";
+
+ Intent intent = new Intent(appPackage + ".SCAN");
+ intent.setPackage(appPackage);
+ intent.putExtra("SCAN_MODE", "QR_CODE_MODE");
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ return intent;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java
new file mode 100644
index 000000000..d4fe297da
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java
@@ -0,0 +1,109 @@
+/*
+ * 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.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.CheckResult;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import org.mozilla.gecko.mozglue.SafeIntent;
+
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utilities for Intents.
+ */
+public class IntentUtils {
+ public static final String ENV_VAR_IN_AUTOMATION = "MOZ_IN_AUTOMATION";
+
+ private static final String ENV_VAR_REGEX = "(.+)=(.*)";
+
+ private IntentUtils() {}
+
+ /**
+ * Returns a list of environment variables and their values. These are parsed from an Intent extra
+ * with the key -> value format:
+ * env# -> ENV_VAR=VALUE
+ *
+ * # in env# is expected to be increasing from 0.
+ *
+ * @return A Map of environment variable name to value, e.g. ENV_VAR -> VALUE
+ */
+ public static HashMap<String, String> getEnvVarMap(@NonNull final SafeIntent intent) {
+ // Optimization: get matcher for re-use. Pattern.matcher creates a new object every time so it'd be great
+ // to avoid the unnecessary allocation, particularly because we expect to be called on the startup path.
+ final Pattern envVarPattern = Pattern.compile(ENV_VAR_REGEX);
+ final Matcher matcher = envVarPattern.matcher(""); // argument does not matter here.
+
+ // This is expected to be an external intent so we should use SafeIntent to prevent crashing.
+ final HashMap<String, String> out = new HashMap<>();
+ int i = 0;
+ while (true) {
+ final String envKey = "env" + i;
+ i += 1;
+ if (!intent.hasExtra(envKey)) {
+ break;
+ }
+
+ maybeAddEnvVarToEnvVarMap(out, intent, envKey, matcher);
+ }
+ return out;
+ }
+
+ /**
+ * @param envVarMap the map to add the env var to
+ * @param intent the intent from which to extract the env var
+ * @param envKey the key at which the env var resides
+ * @param envVarMatcher a matcher initialized with the env var pattern to extract
+ */
+ private static void maybeAddEnvVarToEnvVarMap(@NonNull final HashMap<String, String> envVarMap,
+ @NonNull final SafeIntent intent, @NonNull final String envKey, @NonNull final Matcher envVarMatcher) {
+ final String envValue = intent.getStringExtra(envKey);
+ if (envValue == null) {
+ return; // nothing to do here!
+ }
+
+ envVarMatcher.reset(envValue);
+ if (envVarMatcher.matches()) {
+ final String envVarName = envVarMatcher.group(1);
+ final String envVarValue = envVarMatcher.group(2);
+ envVarMap.put(envVarName, envVarValue);
+ }
+ }
+
+ public static Bundle getBundleExtraSafe(final Intent intent, final String name) {
+ return new SafeIntent(intent).getBundleExtra(name);
+ }
+
+ public static String getStringExtraSafe(final Intent intent, final String name) {
+ return new SafeIntent(intent).getStringExtra(name);
+ }
+
+ public static boolean getBooleanExtraSafe(final Intent intent, final String name, final boolean defaultValue) {
+ return new SafeIntent(intent).getBooleanExtra(name, defaultValue);
+ }
+
+ /**
+ * Gets whether or not we're in automation from the passed in environment variables.
+ *
+ * We need to read environment variables from the intent string
+ * extra because environment variables from our test harness aren't set
+ * until Gecko is loaded, and we need to know this before then.
+ *
+ * The return value of this method should be used early since other
+ * initialization may depend on its results.
+ */
+ @CheckResult
+ public static boolean getIsInAutomationFromEnvironment(final SafeIntent intent) {
+ final HashMap<String, String> envVars = IntentUtils.getEnvVarMap(intent);
+ return !TextUtils.isEmpty(envVars.get(IntentUtils.ENV_VAR_IN_AUTOMATION));
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/JSONUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/JSONUtils.java
new file mode 100644
index 000000000..4ec98ec9e
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/JSONUtils.java
@@ -0,0 +1,69 @@
+/* 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.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+public final class JSONUtils {
+ private static final String LOGTAG = "GeckoJSONUtils";
+
+ private JSONUtils() {}
+
+ public static UUID getUUID(String name, JSONObject json) {
+ String uuid = json.optString(name, null);
+ return (uuid != null) ? UUID.fromString(uuid) : null;
+ }
+
+ public static void putUUID(String name, UUID uuid, JSONObject json) {
+ String uuidString = uuid.toString();
+ try {
+ json.put(name, uuidString);
+ } catch (JSONException e) {
+ throw new IllegalArgumentException(name + "=" + uuidString, e);
+ }
+ }
+
+ public static JSONObject bundleToJSON(Bundle bundle) {
+ if (bundle == null || bundle.isEmpty()) {
+ return null;
+ }
+
+ JSONObject json = new JSONObject();
+ for (String key : bundle.keySet()) {
+ try {
+ json.put(key, bundle.get(key));
+ } catch (JSONException e) {
+ Log.w(LOGTAG, "Error building JSON response.", e);
+ }
+ }
+
+ return json;
+ }
+
+ // Handles conversions between a JSONArray and a Set<String>
+ public static Set<String> parseStringSet(JSONArray json) {
+ final Set<String> ret = new HashSet<String>();
+
+ for (int i = 0; i < json.length(); i++) {
+ try {
+ ret.add(json.getString(i));
+ } catch (JSONException ex) {
+ Log.i(LOGTAG, "Error parsing json", ex);
+ }
+ }
+
+ return ret;
+ }
+
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/MenuUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/MenuUtils.java
new file mode 100644
index 000000000..e44fdd541
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/MenuUtils.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.util;
+
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class MenuUtils {
+ /*
+ * This method looks for a menuitem and sets it's visible state, if
+ * it exists.
+ */
+ public static void safeSetVisible(Menu menu, int id, boolean visible) {
+ MenuItem item = menu.findItem(id);
+ if (item != null) {
+ item.setVisible(visible);
+ }
+ }
+
+ /*
+ * This method looks for a menuitem and sets it's enabled state, if
+ * it exists.
+ */
+ public static void safeSetEnabled(Menu menu, int id, boolean enabled) {
+ MenuItem item = menu.findItem(id);
+ if (item != null) {
+ item.setEnabled(enabled);
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeEventListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeEventListener.java
new file mode 100644
index 000000000..2a1b6e89a
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeEventListener.java
@@ -0,0 +1,23 @@
+/* -*- 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.mozilla.gecko.annotation.RobocopTarget;
+
+@RobocopTarget
+public interface NativeEventListener {
+ /**
+ * Handles a message sent from Gecko.
+ *
+ * @param event The name of the event being sent.
+ * @param message The message data.
+ * @param callback The callback interface for this message. A callback is provided only if the
+ * originating Messaging.sendRequest call included a callback argument; otherwise,
+ * callback will be null. All listeners for a given event are given the same
+ * callback object, and exactly one listener must handle the callback.
+ */
+ void handleMessage(String event, NativeJSObject message, EventCallback callback);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSContainer.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSContainer.java
new file mode 100644
index 000000000..daefe6de0
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSContainer.java
@@ -0,0 +1,37 @@
+/* -*- 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 org.mozilla.gecko.annotation.WrapForJNI;
+
+/**
+ * NativeJSContainer is a wrapper around the SpiderMonkey JSAPI to make it possible to
+ * access Javascript objects in Java.
+ *
+ * A container must only be used on the thread it is attached to. To use it on another
+ * thread, call {@link #clone()} to make a copy, and use the copy on the other thread.
+ * When a copy is first used, it becomes attached to the thread using it.
+ */
+@WrapForJNI(calledFrom = "gecko")
+public final class NativeJSContainer extends NativeJSObject
+{
+ private NativeJSContainer() {
+ }
+
+ /**
+ * Make a copy of this container for use by another thread. When the copy is first used,
+ * it becomes attached to the thread using it.
+ */
+ @Override
+ public native NativeJSContainer clone();
+
+ /**
+ * Dispose all associated native objects. Subsequent use of any objects derived from
+ * this container will throw a NullPointerException.
+ */
+ @Override
+ public native void disposeNative();
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSObject.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSObject.java
new file mode 100644
index 000000000..0d1f0a037
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSObject.java
@@ -0,0 +1,533 @@
+/* -*- 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 org.mozilla.gecko.annotation.JNITarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+
+import android.os.Bundle;
+
+/**
+ * NativeJSObject is a wrapper around the SpiderMonkey JSAPI to make it possible to
+ * access Javascript objects in Java.
+ */
+@WrapForJNI(calledFrom = "gecko")
+public class NativeJSObject extends JNIObject
+{
+ @SuppressWarnings("serial")
+ @JNITarget
+ public static final class InvalidPropertyException extends RuntimeException {
+ public InvalidPropertyException(final String msg) {
+ super(msg);
+ }
+ }
+
+ protected NativeJSObject() {
+ }
+
+ @Override
+ protected void disposeNative() {
+ // NativeJSObject is disposed as part of NativeJSContainer disposal.
+ }
+
+ /**
+ * Returns the value of a boolean property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native boolean getBoolean(String name);
+
+ /**
+ * Returns the value of a boolean property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native boolean optBoolean(String name, boolean fallback);
+
+ /**
+ * Returns the value of a boolean array property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native boolean[] getBooleanArray(String name);
+
+ /**
+ * Returns the value of a boolean array property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native boolean[] optBooleanArray(String name, boolean[] fallback);
+
+ /**
+ * Returns the value of an object property as a Bundle.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native Bundle getBundle(String name);
+
+ /**
+ * Returns the value of an object property as a Bundle.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native Bundle optBundle(String name, Bundle fallback);
+
+ /**
+ * Returns the value of an object array property as a Bundle array.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native Bundle[] getBundleArray(String name);
+
+ /**
+ * Returns the value of an object array property as a Bundle array.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native Bundle[] optBundleArray(String name, Bundle[] fallback);
+
+ /**
+ * Returns the value of a double property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native double getDouble(String name);
+
+ /**
+ * Returns the value of a double property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native double optDouble(String name, double fallback);
+
+ /**
+ * Returns the value of a double array property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native double[] getDoubleArray(String name);
+
+ /**
+ * Returns the value of a double array property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native double[] optDoubleArray(String name, double[] fallback);
+
+ /**
+ * Returns the value of an int property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native int getInt(String name);
+
+ /**
+ * Returns the value of an int property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native int optInt(String name, int fallback);
+
+ /**
+ * Returns the value of an int array property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native int[] getIntArray(String name);
+
+ /**
+ * Returns the value of an int array property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native int[] optIntArray(String name, int[] fallback);
+
+ /**
+ * Returns the value of an object property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native NativeJSObject getObject(String name);
+
+ /**
+ * Returns the value of an object property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native NativeJSObject optObject(String name, NativeJSObject fallback);
+
+ /**
+ * Returns the value of an object array property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native NativeJSObject[] getObjectArray(String name);
+
+ /**
+ * Returns the value of an object array property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native NativeJSObject[] optObjectArray(String name, NativeJSObject[] fallback);
+
+ /**
+ * Returns the value of a string property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native String getString(String name);
+
+ /**
+ * Returns the value of a string property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native String optString(String name, String fallback);
+
+ /**
+ * Returns the value of a string array property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native String[] getStringArray(String name);
+
+ /**
+ * Returns the value of a string array property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native String[] optStringArray(String name, String[] fallback);
+
+ /**
+ * Returns whether a property exists in this object
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native boolean has(String name);
+
+ /**
+ * Returns the Bundle representation of this object.
+ *
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native Bundle toBundle();
+
+ /**
+ * Returns the JSON representation of this object.
+ *
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ @Override
+ public native String toString();
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.java
new file mode 100644
index 000000000..2210e43ed
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.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.util;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.support.annotation.Nullable;
+import android.support.annotation.NonNull;
+import android.telephony.TelephonyManager;
+
+public class NetworkUtils {
+ /*
+ * Keep the below constants in sync with
+ * http://dxr.mozilla.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
+ */
+ public enum ConnectionSubType {
+ CELL_2G("2g"),
+ CELL_3G("3g"),
+ CELL_4G("4g"),
+ ETHERNET("ethernet"),
+ WIFI("wifi"),
+ WIMAX("wimax"),
+ UNKNOWN("unknown");
+
+ public final String value;
+ ConnectionSubType(String value) {
+ this.value = value;
+ }
+ }
+
+ /*
+ * Keep the below constants in sync with
+ * http://dxr.mozilla.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
+ */
+ public enum NetworkStatus {
+ UP("up"),
+ DOWN("down"),
+ UNKNOWN("unknown");
+
+ public final String value;
+
+ NetworkStatus(String value) {
+ this.value = value;
+ }
+ }
+
+ // Connection Type defined in Network Information API v3.
+ // See Bug 1270401 - current W3C Spec (Editor's Draft) is different, it also contains wimax, mixed, unknown.
+ // W3C spec: http://w3c.github.io/netinfo/#the-connectiontype-enum
+ public enum ConnectionType {
+ CELLULAR(0),
+ BLUETOOTH(1),
+ ETHERNET(2),
+ WIFI(3),
+ OTHER(4),
+ NONE(5);
+
+ public final int value;
+
+ ConnectionType(int value) {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Indicates whether network connectivity exists and it is possible to establish connections and pass data.
+ */
+ public static boolean isConnected(@NonNull Context context) {
+ return isConnected((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
+ }
+
+ public static boolean isConnected(ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return false;
+ }
+
+ final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+ return networkInfo != null && networkInfo.isConnected();
+ }
+
+ /**
+ * For mobile connections, maps particular connection subtype to a general 2G, 3G, 4G bucket.
+ */
+ public static ConnectionSubType getConnectionSubType(ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return ConnectionSubType.UNKNOWN;
+ }
+
+ final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+
+ if (networkInfo == null) {
+ return ConnectionSubType.UNKNOWN;
+ }
+
+ switch (networkInfo.getType()) {
+ case ConnectivityManager.TYPE_ETHERNET:
+ return ConnectionSubType.ETHERNET;
+ case ConnectivityManager.TYPE_MOBILE:
+ return getGenericMobileSubtype(networkInfo.getSubtype());
+ case ConnectivityManager.TYPE_WIMAX:
+ return ConnectionSubType.WIMAX;
+ case ConnectivityManager.TYPE_WIFI:
+ return ConnectionSubType.WIFI;
+ default:
+ return ConnectionSubType.UNKNOWN;
+ }
+ }
+
+ public static ConnectionType getConnectionType(ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return ConnectionType.NONE;
+ }
+
+ final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+ if (networkInfo == null) {
+ return ConnectionType.NONE;
+ }
+
+ switch (networkInfo.getType()) {
+ case ConnectivityManager.TYPE_BLUETOOTH:
+ return ConnectionType.BLUETOOTH;
+ case ConnectivityManager.TYPE_ETHERNET:
+ return ConnectionType.ETHERNET;
+ // Fallthrough, MOBILE and WIMAX both map to CELLULAR.
+ case ConnectivityManager.TYPE_MOBILE:
+ case ConnectivityManager.TYPE_WIMAX:
+ return ConnectionType.CELLULAR;
+ case ConnectivityManager.TYPE_WIFI:
+ return ConnectionType.WIFI;
+ default:
+ return ConnectionType.OTHER;
+ }
+ }
+
+ public static NetworkStatus getNetworkStatus(ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return NetworkStatus.UNKNOWN;
+ }
+
+ if (isConnected(connectivityManager)) {
+ return NetworkStatus.UP;
+ }
+ return NetworkStatus.DOWN;
+ }
+
+ private static ConnectionSubType getGenericMobileSubtype(int subtype) {
+ switch (subtype) {
+ // 2G types: fallthrough 5x
+ case TelephonyManager.NETWORK_TYPE_GPRS:
+ case TelephonyManager.NETWORK_TYPE_EDGE:
+ case TelephonyManager.NETWORK_TYPE_CDMA:
+ case TelephonyManager.NETWORK_TYPE_1xRTT:
+ case TelephonyManager.NETWORK_TYPE_IDEN:
+ return ConnectionSubType.CELL_2G;
+ // 3G types: fallthrough 9x
+ case TelephonyManager.NETWORK_TYPE_UMTS:
+ case TelephonyManager.NETWORK_TYPE_EVDO_0:
+ case TelephonyManager.NETWORK_TYPE_EVDO_A:
+ case TelephonyManager.NETWORK_TYPE_HSDPA:
+ case TelephonyManager.NETWORK_TYPE_HSUPA:
+ case TelephonyManager.NETWORK_TYPE_HSPA:
+ case TelephonyManager.NETWORK_TYPE_EVDO_B:
+ case TelephonyManager.NETWORK_TYPE_EHRPD:
+ case TelephonyManager.NETWORK_TYPE_HSPAP:
+ return ConnectionSubType.CELL_3G;
+ // 4G - just one type!
+ case TelephonyManager.NETWORK_TYPE_LTE:
+ return ConnectionSubType.CELL_4G;
+ default:
+ return ConnectionSubType.UNKNOWN;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NonEvictingLruCache.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NonEvictingLruCache.java
new file mode 100644
index 000000000..793b39b81
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NonEvictingLruCache.java
@@ -0,0 +1,44 @@
+/* -*- 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.util.LruCache;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * An LruCache that also supports a set of items that will never be evicted.
+ *
+ * Alas, LruCache is final, so we compose rather than inherit.
+ */
+public class NonEvictingLruCache<K, V> {
+ private final ConcurrentHashMap<K, V> permanent = new ConcurrentHashMap<K, V>();
+ private final LruCache<K, V> evictable;
+
+ public NonEvictingLruCache(final int evictableSize) {
+ evictable = new LruCache<K, V>(evictableSize);
+ }
+
+ public V get(K key) {
+ V val = permanent.get(key);
+ if (val == null) {
+ return evictable.get(key);
+ }
+ return val;
+ }
+
+ public void putWithoutEviction(K key, V value) {
+ permanent.put(key, value);
+ }
+
+ public void put(K key, V value) {
+ evictable.put(key, value);
+ }
+
+ public void evictAll() {
+ evictable.evictAll();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/PrefUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/PrefUtils.java
new file mode 100644
index 000000000..217e40b91
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/PrefUtils.java
@@ -0,0 +1,70 @@
+/* -*- 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 java.util.HashSet;
+import java.util.Set;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.mozilla.gecko.AppConstants.Versions;
+
+import android.content.SharedPreferences;
+import android.util.Log;
+
+
+public class PrefUtils {
+ private static final String LOGTAG = "GeckoPrefUtils";
+
+ // Cross version compatible way to get a string set from a pref
+ public static Set<String> getStringSet(final SharedPreferences prefs,
+ final String key,
+ final Set<String> defaultVal) {
+ if (!prefs.contains(key)) {
+ return defaultVal;
+ }
+
+ // If this is Android version >= 11, try to use a Set<String>.
+ try {
+ return prefs.getStringSet(key, new HashSet<String>());
+ } catch (ClassCastException ex) {
+ // A ClassCastException means we've upgraded from a pre-v11 Android to a new one
+ final Set<String> val = getFromJSON(prefs, key);
+ SharedPreferences.Editor edit = prefs.edit();
+ putStringSet(edit, key, val).apply();
+ return val;
+ }
+ }
+
+ private static Set<String> getFromJSON(SharedPreferences prefs, String key) {
+ try {
+ final String val = prefs.getString(key, "[]");
+ return JSONUtils.parseStringSet(new JSONArray(val));
+ } catch (JSONException ex) {
+ Log.i(LOGTAG, "Unable to parse JSON", ex);
+ }
+
+ return new HashSet<String>();
+ }
+
+ /**
+ * Cross version compatible way to save a set of strings.
+ * <p>
+ * This method <b>does not commit</b> any transaction. It is up to callers
+ * to commit.
+ *
+ * @param editor to write to.
+ * @param key to write.
+ * @param vals comprising string set.
+ * @return
+ */
+ public static SharedPreferences.Editor putStringSet(final SharedPreferences.Editor editor,
+ final String key,
+ final Set<String> vals) {
+ editor.putStringSet(key, vals);
+ return editor;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java
new file mode 100644
index 000000000..35010242b
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java
@@ -0,0 +1,155 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// This code is based on AOSP /libcore/luni/src/main/java/java/net/ProxySelectorImpl.java
+
+package org.mozilla.gecko.util;
+
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.URI;
+import java.net.URLConnection;
+import java.util.List;
+
+public class ProxySelector {
+ public static URLConnection openConnectionWithProxy(URI uri) throws IOException {
+ java.net.ProxySelector ps = java.net.ProxySelector.getDefault();
+ Proxy proxy = Proxy.NO_PROXY;
+ if (ps != null) {
+ List<Proxy> proxies = ps.select(uri);
+ if (proxies != null && !proxies.isEmpty()) {
+ proxy = proxies.get(0);
+ }
+ }
+
+ return uri.toURL().openConnection(proxy);
+ }
+
+ public ProxySelector() {
+ }
+
+ public Proxy select(String scheme, String host) {
+ int port = -1;
+ Proxy proxy = null;
+ String nonProxyHostsKey = null;
+ boolean httpProxyOkay = true;
+ if ("http".equalsIgnoreCase(scheme)) {
+ port = 80;
+ nonProxyHostsKey = "http.nonProxyHosts";
+ proxy = lookupProxy("http.proxyHost", "http.proxyPort", Proxy.Type.HTTP, port);
+ } else if ("https".equalsIgnoreCase(scheme)) {
+ port = 443;
+ nonProxyHostsKey = "https.nonProxyHosts"; // RI doesn't support this
+ proxy = lookupProxy("https.proxyHost", "https.proxyPort", Proxy.Type.HTTP, port);
+ } else if ("ftp".equalsIgnoreCase(scheme)) {
+ port = 80; // not 21 as you might guess
+ nonProxyHostsKey = "ftp.nonProxyHosts";
+ proxy = lookupProxy("ftp.proxyHost", "ftp.proxyPort", Proxy.Type.HTTP, port);
+ } else if ("socket".equalsIgnoreCase(scheme)) {
+ httpProxyOkay = false;
+ } else {
+ return Proxy.NO_PROXY;
+ }
+
+ if (nonProxyHostsKey != null
+ && isNonProxyHost(host, System.getProperty(nonProxyHostsKey))) {
+ return Proxy.NO_PROXY;
+ }
+
+ if (proxy != null) {
+ return proxy;
+ }
+
+ if (httpProxyOkay) {
+ proxy = lookupProxy("proxyHost", "proxyPort", Proxy.Type.HTTP, port);
+ if (proxy != null) {
+ return proxy;
+ }
+ }
+
+ proxy = lookupProxy("socksProxyHost", "socksProxyPort", Proxy.Type.SOCKS, 1080);
+ if (proxy != null) {
+ return proxy;
+ }
+
+ return Proxy.NO_PROXY;
+ }
+
+ /**
+ * Returns the proxy identified by the {@code hostKey} system property, or
+ * null.
+ */
+ @Nullable
+ private Proxy lookupProxy(String hostKey, String portKey, Proxy.Type type, int defaultPort) {
+ final String host = System.getProperty(hostKey);
+ if (TextUtils.isEmpty(host)) {
+ return null;
+ }
+
+ final int port = getSystemPropertyInt(portKey, defaultPort);
+ if (port == -1) {
+ // Port can be -1. See bug 1270529.
+ return null;
+ }
+
+ return new Proxy(type, InetSocketAddress.createUnresolved(host, port));
+ }
+
+ private int getSystemPropertyInt(String key, int defaultValue) {
+ String string = System.getProperty(key);
+ if (string != null) {
+ try {
+ return Integer.parseInt(string);
+ } catch (NumberFormatException ignored) {
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns true if the {@code nonProxyHosts} system property pattern exists
+ * and matches {@code host}.
+ */
+ private boolean isNonProxyHost(String host, String nonProxyHosts) {
+ if (host == null || nonProxyHosts == null) {
+ return false;
+ }
+
+ // construct pattern
+ StringBuilder patternBuilder = new StringBuilder();
+ for (int i = 0; i < nonProxyHosts.length(); i++) {
+ char c = nonProxyHosts.charAt(i);
+ switch (c) {
+ case '.':
+ patternBuilder.append("\\.");
+ break;
+ case '*':
+ patternBuilder.append(".*");
+ break;
+ default:
+ patternBuilder.append(c);
+ }
+ }
+ // check whether the host is the nonProxyHosts.
+ String pattern = patternBuilder.toString();
+ return host.matches(pattern);
+ }
+}
+
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/RawResource.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/RawResource.java
new file mode 100644
index 000000000..5bcad1c60
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/RawResource.java
@@ -0,0 +1,52 @@
+/* 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.content.Context;
+import android.content.res.Resources;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+
+/**
+ * {@code RawResource} provides API to load raw resources in different
+ * forms. For now, we only load them as strings. We're using raw resources
+ * as localizable 'assets' as opposed to a string that can be directly
+ * translatable e.g. JSON file vs string.
+ *
+ * This is just a utility class to avoid code duplication for the different
+ * cases where need to read such assets.
+ */
+public final class RawResource {
+ public static String getAsString(Context context, int id) throws IOException {
+ InputStreamReader reader = null;
+
+ try {
+ final Resources res = context.getResources();
+ final InputStream is = res.openRawResource(id);
+ if (is == null) {
+ return null;
+ }
+
+ reader = new InputStreamReader(is);
+
+ final char[] buffer = new char[1024];
+ final StringWriter s = new StringWriter();
+
+ int n;
+ while ((n = reader.read(buffer, 0, buffer.length)) != -1) {
+ s.write(buffer, 0, n);
+ }
+
+ return s.toString();
+ } finally {
+ if (reader != null) {
+ reader.close();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StringUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StringUtils.java
new file mode 100644
index 000000000..308168f43
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StringUtils.java
@@ -0,0 +1,293 @@
+/* -*- 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.net.Uri;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import org.mozilla.gecko.AppConstants.Versions;
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+public class StringUtils {
+ private static final String LOGTAG = "GeckoStringUtils";
+
+ private static final String FILTER_URL_PREFIX = "filter://";
+ private static final String USER_ENTERED_URL_PREFIX = "user-entered:";
+
+ /*
+ * This method tries to guess if the given string could be a search query or URL,
+ * and returns a previous result if there is ambiguity
+ *
+ * Search examples:
+ * foo
+ * foo bar.com
+ * foo http://bar.com
+ *
+ * URL examples
+ * foo.com
+ * foo.c
+ * :foo
+ * http://foo.com bar
+ *
+ * wasSearchQuery specifies whether text was a search query before the latest change
+ * in text. In ambiguous cases where the new text can be either a search or a URL,
+ * wasSearchQuery is returned
+ */
+ public static boolean isSearchQuery(String text, boolean wasSearchQuery) {
+ // We remove leading and trailing white spaces when decoding URLs
+ text = text.trim();
+ if (text.length() == 0)
+ return wasSearchQuery;
+
+ int colon = text.indexOf(':');
+ int dot = text.indexOf('.');
+ int space = text.indexOf(' ');
+
+ // If a space is found before any dot and colon, we assume this is a search query
+ if (space > -1 && (colon == -1 || space < colon) && (dot == -1 || space < dot)) {
+ return true;
+ }
+ // Otherwise, if a dot or a colon is found, we assume this is a URL
+ if (dot > -1 || colon > -1) {
+ return false;
+ }
+ // Otherwise, text is ambiguous, and we keep its status unchanged
+ return wasSearchQuery;
+ }
+
+ /**
+ * Strip the ref from a URL, if present
+ *
+ * @return The base URL, without the ref. The original String is returned if it has no ref,
+ * of if the input is malformed.
+ */
+ public static String stripRef(final String inputURL) {
+ if (inputURL == null) {
+ return null;
+ }
+
+ final int refIndex = inputURL.indexOf('#');
+
+ if (refIndex >= 0) {
+ return inputURL.substring(0, refIndex);
+ }
+
+ return inputURL;
+ }
+
+ public static class UrlFlags {
+ public static final int NONE = 0;
+ public static final int STRIP_HTTPS = 1;
+ }
+
+ public static String stripScheme(String url) {
+ return stripScheme(url, UrlFlags.NONE);
+ }
+
+ public static String stripScheme(String url, int flags) {
+ if (url == null) {
+ return url;
+ }
+
+ String newURL = url;
+
+ if (newURL.startsWith("http://")) {
+ newURL = newURL.replace("http://", "");
+ } else if (newURL.startsWith("https://") && flags == UrlFlags.STRIP_HTTPS) {
+ newURL = newURL.replace("https://", "");
+ }
+
+ if (newURL.endsWith("/")) {
+ newURL = newURL.substring(0, newURL.length()-1);
+ }
+
+ return newURL;
+ }
+
+ public static boolean isHttpOrHttps(String url) {
+ if (TextUtils.isEmpty(url)) {
+ return false;
+ }
+
+ return url.startsWith("http://") || url.startsWith("https://");
+ }
+
+ public static String stripCommonSubdomains(String host) {
+ if (host == null) {
+ return host;
+ }
+
+ // In contrast to desktop, we also strip mobile subdomains,
+ // since its unlikely users are intentionally typing them
+ int start = 0;
+
+ if (host.startsWith("www.")) {
+ start = 4;
+ } else if (host.startsWith("mobile.")) {
+ start = 7;
+ } else if (host.startsWith("m.")) {
+ start = 2;
+ }
+
+ return host.substring(start);
+ }
+
+ /**
+ * Searches the url query string for the first value with the given key.
+ */
+ public static String getQueryParameter(String url, String desiredKey) {
+ if (TextUtils.isEmpty(url) || TextUtils.isEmpty(desiredKey)) {
+ return null;
+ }
+
+ final String[] urlParts = url.split("\\?");
+ if (urlParts.length < 2) {
+ return null;
+ }
+
+ final String query = urlParts[1];
+ for (final String param : query.split("&")) {
+ final String pair[] = param.split("=");
+ final String key = Uri.decode(pair[0]);
+
+ // Key is empty or does not match the key we're looking for, discard
+ if (TextUtils.isEmpty(key) || !key.equals(desiredKey)) {
+ continue;
+ }
+ // No value associated with key, discard
+ if (pair.length < 2) {
+ continue;
+ }
+ final String value = Uri.decode(pair[1]);
+ if (TextUtils.isEmpty(value)) {
+ return null;
+ }
+ return value;
+ }
+
+ return null;
+ }
+
+ public static boolean isFilterUrl(String url) {
+ if (TextUtils.isEmpty(url)) {
+ return false;
+ }
+
+ return url.startsWith(FILTER_URL_PREFIX);
+ }
+
+ public static String getFilterFromUrl(String url) {
+ if (TextUtils.isEmpty(url)) {
+ return null;
+ }
+
+ return url.substring(FILTER_URL_PREFIX.length());
+ }
+
+ public static boolean isShareableUrl(final String url) {
+ final String scheme = Uri.parse(url).getScheme();
+ return !("about".equals(scheme) || "chrome".equals(scheme) ||
+ "file".equals(scheme) || "resource".equals(scheme));
+ }
+
+ public static boolean isUserEnteredUrl(String url) {
+ return (url != null && url.startsWith(USER_ENTERED_URL_PREFIX));
+ }
+
+ /**
+ * Given a url with a user-entered scheme, extract the
+ * scheme-specific component. For e.g, given "user-entered://www.google.com",
+ * this method returns "//www.google.com". If the passed url
+ * does not have a user-entered scheme, the same url will be returned.
+ *
+ * @param url to be decoded
+ * @return url component entered by user
+ */
+ public static String decodeUserEnteredUrl(String url) {
+ Uri uri = Uri.parse(url);
+ if ("user-entered".equals(uri.getScheme())) {
+ return uri.getSchemeSpecificPart();
+ }
+ return url;
+ }
+
+ public static String encodeUserEnteredUrl(String url) {
+ return Uri.fromParts("user-entered", url, null).toString();
+ }
+
+ /**
+ * Compatibility layer for API < 11.
+ *
+ * Returns a set of the unique names of all query parameters. Iterating
+ * over the set will return the names in order of their first occurrence.
+ *
+ * @param uri
+ * @throws UnsupportedOperationException if this isn't a hierarchical URI
+ *
+ * @return a set of decoded names
+ */
+ public static Set<String> getQueryParameterNames(Uri uri) {
+ return uri.getQueryParameterNames();
+ }
+
+ public static String safeSubstring(@NonNull final String str, final int start, final int end) {
+ return str.substring(
+ Math.max(0, start),
+ Math.min(end, str.length()));
+ }
+
+ /**
+ * Check if this might be a RTL (right-to-left) text by looking at the first character.
+ */
+ public static boolean isRTL(String text) {
+ if (TextUtils.isEmpty(text)) {
+ return false;
+ }
+
+ final char character = text.charAt(0);
+ final byte directionality = Character.getDirectionality(character);
+
+ return directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT
+ || directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC
+ || directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING
+ || directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE;
+ }
+
+ /**
+ * Force LTR (left-to-right) by prepending the text with the "left-to-right mark" (U+200E) if needed.
+ */
+ public static String forceLTR(String text) {
+ if (!isRTL(text)) {
+ return text;
+ }
+
+ return "\u200E" + text;
+ }
+
+ /**
+ * Joining together a sequence of strings with a separator.
+ */
+ public static String join(@NonNull String separator, @NonNull List<String> parts) {
+ if (parts.size() == 0) {
+ return "";
+ }
+
+ final StringBuilder builder = new StringBuilder();
+ builder.append(parts.get(0));
+
+ for (int i = 1; i < parts.size(); i++) {
+ builder.append(separator);
+ builder.append(parts.get(i));
+ }
+
+ return builder.toString();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java
new file mode 100644
index 000000000..884a56dc4
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java
@@ -0,0 +1,247 @@
+/* -*- 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.mozilla.gecko.annotation.RobocopTarget;
+
+import java.util.Map;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.util.Log;
+
+public final class ThreadUtils {
+ private static final String LOGTAG = "ThreadUtils";
+
+ /**
+ * Controls the action taken when a method like
+ * {@link ThreadUtils#assertOnUiThread(AssertBehavior)} detects a problem.
+ */
+ public static enum AssertBehavior {
+ NONE,
+ THROW,
+ }
+
+ private static final Thread sUiThread = Looper.getMainLooper().getThread();
+ private static final Handler sUiHandler = new Handler(Looper.getMainLooper());
+
+ private static volatile Thread sBackgroundThread;
+
+ // 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 volatile 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 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 void postDelayedToUiThread(Runnable runnable, long timeout) {
+ sUiHandler.postDelayed(runnable, timeout);
+ }
+
+ public static void removeCallbacksFromUiThread(Runnable runnable) {
+ sUiHandler.removeCallbacks(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(final AssertBehavior assertBehavior) {
+ assertOnThread(getUiThread(), assertBehavior);
+ }
+
+ public static void assertOnUiThread() {
+ assertOnThread(getUiThread(), AssertBehavior.THROW);
+ }
+
+ public static void assertNotOnUiThread() {
+ assertNotOnThread(getUiThread(), AssertBehavior.THROW);
+ }
+
+ @RobocopTarget
+ public static void assertOnGeckoThread() {
+ assertOnThread(sGeckoThread, AssertBehavior.THROW);
+ }
+
+ public static void assertNotOnGeckoThread() {
+ if (sGeckoThread == null) {
+ // Cannot be on Gecko thread if Gecko thread is not live yet.
+ return;
+ }
+ assertNotOnThread(sGeckoThread, AssertBehavior.THROW);
+ }
+
+ public static void assertOnBackgroundThread() {
+ assertOnThread(getBackgroundThread(), AssertBehavior.THROW);
+ }
+
+ public static void assertOnThread(final Thread expectedThread) {
+ assertOnThread(expectedThread, AssertBehavior.THROW);
+ }
+
+ public static void assertOnThread(final Thread expectedThread, AssertBehavior behavior) {
+ assertOnThreadComparison(expectedThread, behavior, true);
+ }
+
+ public static void assertNotOnThread(final Thread expectedThread, AssertBehavior behavior) {
+ assertOnThreadComparison(expectedThread, behavior, false);
+ }
+
+ private static void assertOnThreadComparison(final Thread expectedThread, AssertBehavior behavior, boolean expected) {
+ final Thread currentThread = Thread.currentThread();
+ final long currentThreadId = currentThread.getId();
+ final long expectedThreadId = expectedThread.getId();
+
+ if ((currentThreadId == expectedThreadId) == expected) {
+ return;
+ }
+
+ final String message;
+ if (expected) {
+ message = "Expected thread " + expectedThreadId +
+ " (\"" + expectedThread.getName() + "\"), but running on thread " +
+ currentThreadId + " (\"" + currentThread.getName() + "\")";
+ } else {
+ message = "Expected anything but " + expectedThreadId +
+ " (\"" + expectedThread.getName() + "\"), but running there.";
+ }
+
+ final IllegalThreadStateException e = new IllegalThreadStateException(message);
+
+ switch (behavior) {
+ case THROW:
+ throw e;
+ default:
+ Log.e(LOGTAG, "Method called on wrong thread!", e);
+ }
+ }
+
+ public static boolean isOnGeckoThread() {
+ if (sGeckoThread != null) {
+ return isOnThread(sGeckoThread);
+ }
+ return false;
+ }
+
+ public static boolean isOnUiThread() {
+ return isOnThread(getUiThread());
+ }
+
+ @RobocopTarget
+ public static boolean isOnBackgroundThread() {
+ if (sBackgroundThread == null) {
+ return false;
+ }
+
+ return isOnThread(sBackgroundThread);
+ }
+
+ @RobocopTarget
+ 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 (Runtime.getRuntime().availableProcessors() > 1) {
+ // Don't reduce priority for multicore devices. We use availableProcessors()
+ // for its fast performance. It may give false negatives (i.e. multicore
+ // detected as single-core), but we can tolerate this behavior.
+ return;
+ }
+ if (!sIsGeckoPriorityReduced && sGeckoThread != null) {
+ 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/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UIAsyncTask.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UIAsyncTask.java
new file mode 100644
index 000000000..26cc32a99
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UIAsyncTask.java
@@ -0,0 +1,121 @@
+/* -*- 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.
+ *
+ * We use generics differently to Android's AsyncTask.
+ * Android uses a "Params" type parameter to represent the type of all the parameters to this task.
+ * It then uses arguments of type Params... to permit arbitrarily-many of these to be passed
+ * fluently.
+ *
+ * Unfortunately, since Java does not support generic array types (and since varargs desugars to a
+ * single array parameter) that behaviour exposes a hole in the type system. See:
+ * http://docs.oracle.com/javase/tutorial/java/generics/nonReifiableVarargsType.html#vulnerabilities
+ *
+ * Instead, we equivalently have a single type parameter "Param". A UiAsyncTask may take exactly one
+ * parameter of type Param. Since Param can be an array type, this no more restrictive than the
+ * other approach, it just provides additional type safety.
+ */
+public abstract class UIAsyncTask<Param, Result> {
+ /**
+ * Provide a convenient API for parameter-free UiAsyncTasks by wrapping parameter-taking methods
+ * from UiAsyncTask in parameterless equivalents.
+ */
+ public static abstract class WithoutParams<InnerResult> extends UIAsyncTask<Void, InnerResult> {
+ public WithoutParams(Handler backgroundThreadHandler) {
+ super(backgroundThreadHandler);
+ }
+
+ public void execute() {
+ execute(null);
+ }
+
+ @Override
+ protected InnerResult doInBackground(Void unused) {
+ return doInBackground();
+ }
+
+ protected abstract InnerResult doInBackground();
+ }
+
+ final Handler mBackgroundThreadHandler;
+ private volatile boolean mCancelled;
+ 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 final Param mParam;
+
+ public BackgroundTaskRunnable(Param param) {
+ mParam = param;
+ }
+
+ @Override
+ public void run() {
+ final Result result = doInBackground(mParam);
+
+ getUiHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ if (mCancelled) {
+ onCancelled();
+ } else {
+ onPostExecute(result);
+ }
+ }
+ });
+ }
+ }
+
+ protected void execute(final Param param) {
+ getUiHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ onPreExecute();
+ mBackgroundThreadHandler.post(new BackgroundTaskRunnable(param));
+ }
+ });
+ }
+
+ public final boolean cancel() {
+ 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(Param param);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UUIDUtil.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UUIDUtil.java
new file mode 100644
index 000000000..cef303a87
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UUIDUtil.java
@@ -0,0 +1,19 @@
+/*
+ * 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 java.util.regex.Pattern;
+
+/**
+ * Utilities for UUIDs.
+ */
+public class UUIDUtil {
+ private UUIDUtil() {}
+
+ public static final String UUID_REGEX = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
+ public static final Pattern UUID_PATTERN = Pattern.compile(UUID_REGEX);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WeakReferenceHandler.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WeakReferenceHandler.java
new file mode 100644
index 000000000..3e8508bce
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WeakReferenceHandler.java
@@ -0,0 +1,27 @@
+/* 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 java.lang.ref.WeakReference;
+
+/**
+ * A Handler to help prevent memory leaks when using Handlers as inner classes.
+ *
+ * To use, extend the Handler, if it's an inner class, make it static,
+ * and reference `this` via the associated WeakReference.
+ *
+ * For additional context, see the "HandlerLeak" android lint item and this post by Romain Guy:
+ * https://groups.google.com/forum/#!msg/android-developers/1aPZXZG6kWk/lIYDavGYn5UJ
+ */
+public class WeakReferenceHandler<T> extends Handler {
+ public final WeakReference<T> mTarget;
+
+ public WeakReferenceHandler(final T that) {
+ super();
+ mTarget = new WeakReference<>(that);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WindowUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WindowUtils.java
new file mode 100644
index 000000000..5298f846a
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WindowUtils.java
@@ -0,0 +1,59 @@
+/* -*- 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.mozilla.gecko.AppConstants.Versions;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Display;
+import android.view.WindowManager;
+
+import java.lang.reflect.Method;
+
+public class WindowUtils {
+ private static final String LOGTAG = "Gecko" + WindowUtils.class.getSimpleName();
+
+ private WindowUtils() { /* To prevent instantiation */ }
+
+ /**
+ * Returns the best-guess physical device dimensions, including the system status bars. Note
+ * that DisplayMetrics.height/widthPixels does not include the system bars.
+ *
+ * via http://stackoverflow.com/a/23861333
+ *
+ * @param context the calling Activity's Context
+ * @return The number of pixels of the device's largest dimension, ignoring software status bars
+ */
+ public static int getLargestDimension(final Context context) {
+ final Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
+
+ if (Versions.feature17Plus) {
+ final DisplayMetrics realMetrics = new DisplayMetrics();
+ display.getRealMetrics(realMetrics);
+ return Math.max(realMetrics.widthPixels, realMetrics.heightPixels);
+
+ } else {
+ int tempWidth;
+ int tempHeight;
+ try {
+ final Method getRawH = Display.class.getMethod("getRawHeight");
+ final Method getRawW = Display.class.getMethod("getRawWidth");
+ tempWidth = (Integer) getRawW.invoke(display);
+ tempHeight = (Integer) getRawH.invoke(display);
+ } catch (Exception e) {
+ // This is the best we can do.
+ tempWidth = display.getWidth();
+ tempHeight = display.getHeight();
+ Log.w(LOGTAG, "Couldn't use reflection to get the real display metrics.");
+ }
+
+ return Math.max(tempWidth, tempHeight);
+
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/publicsuffix/PublicSuffix.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/publicsuffix/PublicSuffix.java
new file mode 100644
index 000000000..6a146cfcf
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/publicsuffix/PublicSuffix.java
@@ -0,0 +1,121 @@
+/* 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.publicsuffix;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+
+import org.mozilla.gecko.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Helper methods for the public suffix part of a domain.
+ *
+ * A "public suffix" is one under which Internet users can (or historically could) directly register
+ * names. Some examples of public suffixes are .com, .co.uk and pvt.k12.ma.us.
+ *
+ * https://publicsuffix.org/
+ *
+ * Some parts of the implementation of this class are based on InternetDomainName class of the Guava
+ * project: https://github.com/google/guava
+ */
+public class PublicSuffix {
+ /**
+ * Strip the public suffix from the domain. Returns the original domain if no public suffix
+ * could be found.
+ *
+ * www.mozilla.org -> www.mozilla
+ * independent.co.uk -> independent
+ */
+ @NonNull
+ @WorkerThread // This method might need to load data from disk
+ public static String stripPublicSuffix(Context context, @NonNull String domain) {
+ if (domain.length() == 0) {
+ return domain;
+ }
+
+ final int index = findPublicSuffixIndex(context, domain);
+ if (index == -1) {
+ return domain;
+ }
+
+ return domain.substring(0, index);
+ }
+
+ /**
+ * Returns the index of the leftmost part of the public suffix, or -1 if not found.
+ */
+ @WorkerThread
+ private static int findPublicSuffixIndex(Context context, String domain) {
+ final List<String> parts = normalizeAndSplit(domain);
+ final int partsSize = parts.size();
+ final Set<String> exact = PublicSuffixPatterns.getExactSet(context);
+
+ for (int i = 0; i < partsSize; i++) {
+ String ancestorName = StringUtils.join(".", parts.subList(i, partsSize));
+
+ if (exact.contains(ancestorName)) {
+ return joinIndex(parts, i);
+ }
+
+ // Excluded domains (e.g. !nhs.uk) use the next highest
+ // domain as the effective public suffix (e.g. uk).
+ if (PublicSuffixPatterns.EXCLUDED.contains(ancestorName)) {
+ return joinIndex(parts, i + 1);
+ }
+
+ if (matchesWildcardPublicSuffix(ancestorName)) {
+ return joinIndex(parts, i);
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Normalize domain and split into domain parts (www.mozilla.org -> [www, mozilla, org]).
+ */
+ private static List<String> normalizeAndSplit(String domain) {
+ domain = domain.replaceAll("[.\u3002\uFF0E\uFF61]", "."); // All dot-like characters to '.'
+ domain = domain.toLowerCase();
+
+ if (domain.endsWith(".")) {
+ domain = domain.substring(0, domain.length() - 1); // Strip trailing '.'
+ }
+
+ List<String> parts = new ArrayList<>();
+ Collections.addAll(parts, domain.split("\\."));
+
+ return parts;
+ }
+
+ /**
+ * Translate the index of the leftmost part of the public suffix to the index of the domain string.
+ *
+ * [www, mozilla, org] and 2 => 12 (www.mozilla)
+ */
+ private static int joinIndex(List<String> parts, int index) {
+ int actualIndex = parts.get(0).length();
+
+ for (int i = 1; i < index; i++) {
+ actualIndex += parts.get(i).length() + 1; // Add one for the "." that is not part of the list elements
+ }
+
+ return actualIndex;
+ }
+
+ /**
+ * Does the domain name match one of the "wildcard" patterns (e.g. {@code "*.ar"})?
+ */
+ private static boolean matchesWildcardPublicSuffix(String domain) {
+ final String[] pieces = domain.split("\\.", 2);
+ return pieces.length == 2 && PublicSuffixPatterns.UNDER.contains(pieces[1]);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/publicsuffix/PublicSuffixPatterns.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/publicsuffix/PublicSuffixPatterns.java
new file mode 100644
index 000000000..8c4b80ce1
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/publicsuffix/PublicSuffixPatterns.java
@@ -0,0 +1,117 @@
+/* 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.publicsuffix;
+
+import android.content.Context;
+import android.util.Log;
+
+import org.mozilla.gecko.util.IOUtils;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.HashSet;
+import java.util.Set;
+
+class PublicSuffixPatterns {
+ /** If a hostname is contained as a key in this map, it is a public suffix. */
+ private static Set<String> EXACT = null;
+
+ static synchronized Set<String> getExactSet(Context context) {
+ if (EXACT != null) {
+ return EXACT;
+ }
+
+ EXACT = new HashSet<>();
+
+ InputStream stream = null;
+
+ try {
+ stream = context.getAssets().open("publicsuffixlist");
+ BufferedReader reader = new BufferedReader(new InputStreamReader(
+ new BufferedInputStream(stream)));
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ EXACT.add(line);
+ }
+
+ } catch (IOException e) {
+ Log.e("Patterns", "IOException during loading public suffix list");
+ } finally {
+ IOUtils.safeStreamClose(stream);
+ }
+
+ return EXACT;
+ }
+
+
+ /**
+ * If a hostname is not a key in the EXCLUDE map, and if removing its
+ * leftmost component results in a name which is a key in this map, it is a
+ * public suffix.
+ */
+ static final Set<String> UNDER = new HashSet<>();
+ static {
+ UNDER.add("bd");
+ UNDER.add("magentosite.cloud");
+ UNDER.add("ke");
+ UNDER.add("triton.zone");
+ UNDER.add("compute.estate");
+ UNDER.add("ye");
+ UNDER.add("pg");
+ UNDER.add("kh");
+ UNDER.add("platform.sh");
+ UNDER.add("fj");
+ UNDER.add("ck");
+ UNDER.add("fk");
+ UNDER.add("alces.network");
+ UNDER.add("sch.uk");
+ UNDER.add("jm");
+ UNDER.add("mm");
+ UNDER.add("api.githubcloud.com");
+ UNDER.add("ext.githubcloud.com");
+ UNDER.add("0emm.com");
+ UNDER.add("githubcloudusercontent.com");
+ UNDER.add("cns.joyent.com");
+ UNDER.add("bn");
+ UNDER.add("yokohama.jp");
+ UNDER.add("nagoya.jp");
+ UNDER.add("kobe.jp");
+ UNDER.add("sendai.jp");
+ UNDER.add("kawasaki.jp");
+ UNDER.add("sapporo.jp");
+ UNDER.add("kitakyushu.jp");
+ UNDER.add("np");
+ UNDER.add("nom.br");
+ UNDER.add("er");
+ UNDER.add("cryptonomic.net");
+ UNDER.add("gu");
+ UNDER.add("kw");
+ UNDER.add("zw");
+ UNDER.add("mz");
+ }
+
+ /**
+ * The elements in this map would pass the UNDER test, but are known not to
+ * be public suffixes and are thus excluded from consideration. Since it
+ * refers to elements in UNDER of the same type, the type is actually not
+ * important here. The map is simply used for consistency reasons.
+ */
+ static final Set<String> EXCLUDED = new HashSet<>();
+ static {
+ EXCLUDED.add("www.ck");
+ EXCLUDED.add("city.yokohama.jp");
+ EXCLUDED.add("city.nagoya.jp");
+ EXCLUDED.add("city.kobe.jp");
+ EXCLUDED.add("city.sendai.jp");
+ EXCLUDED.add("city.kawasaki.jp");
+ EXCLUDED.add("city.sapporo.jp");
+ EXCLUDED.add("city.kitakyushu.jp");
+ EXCLUDED.add("teledata.mz");
+ }
+}
diff --git a/mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/ISelfBrailleService.java b/mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/ISelfBrailleService.java
new file mode 100644
index 000000000..3bdd8c450
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/ISelfBrailleService.java
@@ -0,0 +1,147 @@
+/* -*- 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 com.googlecode.eyesfree.braille.selfbraille;
+
+/**
+ * Interface for a client to control braille output for a part of the
+ * accessibility node tree.
+ */
+public interface ISelfBrailleService extends android.os.IInterface {
+ /** Local-side IPC implementation stub class. */
+ public static abstract class Stub extends android.os.Binder implements
+ com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService {
+ private static final java.lang.String DESCRIPTOR = "com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService";
+
+ /** Construct the stub at attach it to the interface. */
+ public Stub() {
+ this.attachInterface(this, DESCRIPTOR);
+ }
+
+ /**
+ * Cast an IBinder object into an
+ * com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService
+ * interface, generating a proxy if needed.
+ */
+ public static com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService asInterface(
+ android.os.IBinder obj) {
+ if ((obj == null)) {
+ return null;
+ }
+ android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
+ if (((iin != null) && (iin instanceof com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService))) {
+ return ((com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService) iin);
+ }
+ return new com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService.Stub.Proxy(
+ obj);
+ }
+
+ @Override
+ public android.os.IBinder asBinder() {
+ return this;
+ }
+
+ @Override
+ public boolean onTransact(int code, android.os.Parcel data,
+ android.os.Parcel reply, int flags)
+ throws android.os.RemoteException {
+ switch (code) {
+ case INTERFACE_TRANSACTION: {
+ reply.writeString(DESCRIPTOR);
+ return true;
+ }
+ case TRANSACTION_write: {
+ data.enforceInterface(DESCRIPTOR);
+ android.os.IBinder _arg0;
+ _arg0 = data.readStrongBinder();
+ com.googlecode.eyesfree.braille.selfbraille.WriteData _arg1;
+ if ((0 != data.readInt())) {
+ _arg1 = com.googlecode.eyesfree.braille.selfbraille.WriteData.CREATOR
+ .createFromParcel(data);
+ } else {
+ _arg1 = null;
+ }
+ this.write(_arg0, _arg1);
+ reply.writeNoException();
+ return true;
+ }
+ case TRANSACTION_disconnect: {
+ data.enforceInterface(DESCRIPTOR);
+ android.os.IBinder _arg0;
+ _arg0 = data.readStrongBinder();
+ this.disconnect(_arg0);
+ return true;
+ }
+ }
+ return super.onTransact(code, data, reply, flags);
+ }
+
+ private static class Proxy implements
+ com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService {
+ private android.os.IBinder mRemote;
+
+ Proxy(android.os.IBinder remote) {
+ mRemote = remote;
+ }
+
+ @Override
+ public android.os.IBinder asBinder() {
+ return mRemote;
+ }
+
+ public java.lang.String getInterfaceDescriptor() {
+ return DESCRIPTOR;
+ }
+
+ @Override
+ public void write(
+ android.os.IBinder clientToken,
+ com.googlecode.eyesfree.braille.selfbraille.WriteData writeData)
+ throws android.os.RemoteException {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeStrongBinder(clientToken);
+ if ((writeData != null)) {
+ _data.writeInt(1);
+ writeData.writeToParcel(_data, 0);
+ } else {
+ _data.writeInt(0);
+ }
+ mRemote.transact(Stub.TRANSACTION_write, _data, _reply, 0);
+ _reply.readException();
+ } finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+
+ @Override
+ public void disconnect(android.os.IBinder clientToken)
+ throws android.os.RemoteException {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeStrongBinder(clientToken);
+ mRemote.transact(Stub.TRANSACTION_disconnect, _data, null,
+ android.os.IBinder.FLAG_ONEWAY);
+ } finally {
+ _data.recycle();
+ }
+ }
+ }
+
+ static final int TRANSACTION_write = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
+ static final int TRANSACTION_disconnect = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
+ }
+
+ public void write(android.os.IBinder clientToken,
+ com.googlecode.eyesfree.braille.selfbraille.WriteData writeData)
+ throws android.os.RemoteException;
+
+ public void disconnect(android.os.IBinder clientToken)
+ throws android.os.RemoteException;
+}
diff --git a/mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/SelfBrailleClient.java b/mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/SelfBrailleClient.java
new file mode 100644
index 000000000..e4a363aca
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/SelfBrailleClient.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.googlecode.eyesfree.braille.selfbraille;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.Signature;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Client-side interface to the self brailling interface.
+ *
+ * Threading: Instances of this object should be created and shut down
+ * in a thread with a {@link Looper} associated with it. Other methods may
+ * be called on any thread.
+ */
+public class SelfBrailleClient {
+ private static final String LOG_TAG =
+ SelfBrailleClient.class.getSimpleName();
+ private static final String ACTION_SELF_BRAILLE_SERVICE =
+ "com.googlecode.eyesfree.braille.service.ACTION_SELF_BRAILLE_SERVICE";
+ private static final String BRAILLE_BACK_PACKAGE =
+ "com.googlecode.eyesfree.brailleback";
+ private static final Intent mServiceIntent =
+ new Intent(ACTION_SELF_BRAILLE_SERVICE)
+ .setPackage(BRAILLE_BACK_PACKAGE);
+ /**
+ * SHA-1 hash value of the Eyes-Free release key certificate, used to sign
+ * BrailleBack. It was generated from the keystore with:
+ * $ keytool -exportcert -keystore <keystorefile> -alias android.keystore \
+ * > cert
+ * $ keytool -printcert -file cert
+ */
+ // The typecasts are to silence a compiler warning about loss of precision
+ private static final byte[] EYES_FREE_CERT_SHA1 = new byte[] {
+ (byte) 0x9B, (byte) 0x42, (byte) 0x4C, (byte) 0x2D,
+ (byte) 0x27, (byte) 0xAD, (byte) 0x51, (byte) 0xA4,
+ (byte) 0x2A, (byte) 0x33, (byte) 0x7E, (byte) 0x0B,
+ (byte) 0xB6, (byte) 0x99, (byte) 0x1C, (byte) 0x76,
+ (byte) 0xEC, (byte) 0xA4, (byte) 0x44, (byte) 0x61
+ };
+ /**
+ * Delay before the first rebind attempt on bind error or service
+ * disconnect.
+ */
+ private static final int REBIND_DELAY_MILLIS = 500;
+ private static final int MAX_REBIND_ATTEMPTS = 5;
+
+ private final Binder mIdentity = new Binder();
+ private final Context mContext;
+ private final boolean mAllowDebugService;
+ private final SelfBrailleHandler mHandler = new SelfBrailleHandler();
+ private boolean mShutdown = false;
+
+ /**
+ * Written in handler thread, read in any thread calling methods on the
+ * object.
+ */
+ private volatile Connection mConnection;
+ /** Protected by synchronizing on mHandler. */
+ private int mNumFailedBinds = 0;
+
+ /**
+ * Constructs an instance of this class. {@code context} is used to bind
+ * to the self braille service. The current thread must have a Looper
+ * associated with it. If {@code allowDebugService} is true, this instance
+ * will connect to a BrailleBack service without requiring it to be signed
+ * by the release key used to sign BrailleBack.
+ */
+ public SelfBrailleClient(Context context, boolean allowDebugService) {
+ mContext = context;
+ mAllowDebugService = allowDebugService;
+ doBindService();
+ }
+
+ /**
+ * Shuts this instance down, deallocating any global resources it is using.
+ * This method must be called on the same thread that created this object.
+ */
+ public void shutdown() {
+ mShutdown = true;
+ doUnbindService();
+ }
+
+ public void write(WriteData writeData) {
+ writeData.validate();
+ ISelfBrailleService localService = getSelfBrailleService();
+ if (localService != null) {
+ try {
+ localService.write(mIdentity, writeData);
+ } catch (RemoteException ex) {
+ Log.e(LOG_TAG, "Self braille write failed", ex);
+ }
+ }
+ }
+
+ private void doBindService() {
+ Connection localConnection = new Connection();
+ if (!mContext.bindService(mServiceIntent, localConnection,
+ Context.BIND_AUTO_CREATE)) {
+ Log.e(LOG_TAG, "Failed to bind to service");
+ mHandler.scheduleRebind();
+ return;
+ }
+ mConnection = localConnection;
+ Log.i(LOG_TAG, "Bound to self braille service");
+ }
+
+ private void doUnbindService() {
+ if (mConnection != null) {
+ ISelfBrailleService localService = getSelfBrailleService();
+ if (localService != null) {
+ try {
+ localService.disconnect(mIdentity);
+ } catch (RemoteException ex) {
+ // Nothing to do.
+ }
+ }
+ mContext.unbindService(mConnection);
+ mConnection = null;
+ }
+ }
+
+ private ISelfBrailleService getSelfBrailleService() {
+ Connection localConnection = mConnection;
+ if (localConnection != null) {
+ return localConnection.mService;
+ }
+ return null;
+ }
+
+ private boolean verifyPackage() {
+ PackageManager pm = mContext.getPackageManager();
+ PackageInfo pi;
+ try {
+ pi = pm.getPackageInfo(BRAILLE_BACK_PACKAGE,
+ PackageManager.GET_SIGNATURES);
+ } catch (PackageManager.NameNotFoundException ex) {
+ Log.w(LOG_TAG, "Can't verify package " + BRAILLE_BACK_PACKAGE,
+ ex);
+ return false;
+ }
+ MessageDigest digest;
+ try {
+ digest = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException ex) {
+ Log.e(LOG_TAG, "SHA-1 not supported", ex);
+ return false;
+ }
+ // Check if any of the certificates match our hash.
+ for (Signature signature : pi.signatures) {
+ digest.update(signature.toByteArray());
+ if (MessageDigest.isEqual(EYES_FREE_CERT_SHA1, digest.digest())) {
+ return true;
+ }
+ digest.reset();
+ }
+ if (mAllowDebugService) {
+ Log.w(LOG_TAG, String.format(
+ "*** %s connected to BrailleBack with invalid (debug?) "
+ + "signature ***",
+ mContext.getPackageName()));
+ return true;
+ }
+ return false;
+ }
+ private class Connection implements ServiceConnection {
+ // Read in application threads, written in main thread.
+ private volatile ISelfBrailleService mService;
+
+ @Override
+ public void onServiceConnected(ComponentName className,
+ IBinder binder) {
+ if (!verifyPackage()) {
+ Log.w(LOG_TAG, String.format("Service certificate mismatch "
+ + "for %s, dropping connection",
+ BRAILLE_BACK_PACKAGE));
+ mHandler.unbindService();
+ return;
+ }
+ Log.i(LOG_TAG, "Connected to self braille service");
+ mService = ISelfBrailleService.Stub.asInterface(binder);
+ synchronized (mHandler) {
+ mNumFailedBinds = 0;
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName className) {
+ Log.e(LOG_TAG, "Disconnected from self braille service");
+ mService = null;
+ // Retry by rebinding.
+ mHandler.scheduleRebind();
+ }
+ }
+
+ private class SelfBrailleHandler extends Handler {
+ private static final int MSG_REBIND_SERVICE = 1;
+ private static final int MSG_UNBIND_SERVICE = 2;
+
+ public void scheduleRebind() {
+ synchronized (this) {
+ if (mNumFailedBinds < MAX_REBIND_ATTEMPTS) {
+ int delay = REBIND_DELAY_MILLIS << mNumFailedBinds;
+ sendEmptyMessageDelayed(MSG_REBIND_SERVICE, delay);
+ ++mNumFailedBinds;
+ }
+ }
+ }
+
+ public void unbindService() {
+ sendEmptyMessage(MSG_UNBIND_SERVICE);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_REBIND_SERVICE:
+ handleRebindService();
+ break;
+ case MSG_UNBIND_SERVICE:
+ handleUnbindService();
+ break;
+ }
+ }
+
+ private void handleRebindService() {
+ if (mShutdown) {
+ return;
+ }
+ if (mConnection != null) {
+ doUnbindService();
+ }
+ doBindService();
+ }
+
+ private void handleUnbindService() {
+ doUnbindService();
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/WriteData.java b/mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/WriteData.java
new file mode 100644
index 000000000..ef81a2990
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/WriteData.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.googlecode.eyesfree.braille.selfbraille;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * Represents what should be shown on the braille display for a
+ * part of the accessibility node tree.
+ */
+public class WriteData implements Parcelable {
+
+ private static final String PROP_SELECTION_START = "selectionStart";
+ private static final String PROP_SELECTION_END = "selectionEnd";
+
+ private AccessibilityNodeInfo mAccessibilityNodeInfo;
+ private CharSequence mText;
+ private Bundle mProperties = Bundle.EMPTY;
+
+ /**
+ * Returns a new {@link WriteData} instance for the given {@code view}.
+ */
+ public static WriteData forView(View view) {
+ AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(view);
+ WriteData writeData = new WriteData();
+ writeData.mAccessibilityNodeInfo = node;
+ return writeData;
+ }
+
+ public static WriteData forInfo(AccessibilityNodeInfo info){
+ WriteData writeData = new WriteData();
+ writeData.mAccessibilityNodeInfo = info;
+ return writeData;
+ }
+
+
+ public AccessibilityNodeInfo getAccessibilityNodeInfo() {
+ return mAccessibilityNodeInfo;
+ }
+
+ /**
+ * Sets the text to be displayed when the accessibility node associated
+ * with this instance has focus. If this method is not called (or
+ * {@code text} is {@code null}), this client relinquishes control over
+ * this node.
+ */
+ public WriteData setText(CharSequence text) {
+ mText = text;
+ return this;
+ }
+
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Sets the start position in the text of a text selection or cursor that
+ * should be marked on the display. A negative value (the default) means
+ * no selection will be added.
+ */
+ public WriteData setSelectionStart(int v) {
+ writableProperties().putInt(PROP_SELECTION_START, v);
+ return this;
+ }
+
+ /**
+ * @see {@link #setSelectionStart}.
+ */
+ public int getSelectionStart() {
+ return mProperties.getInt(PROP_SELECTION_START, -1);
+ }
+
+ /**
+ * Sets the end of the text selection to be marked on the display. This
+ * value should only be non-negative if the selection start is
+ * non-negative. If this value is <= the selection start, the selection
+ * is a cursor. Otherwise, the selection covers the range from
+ * start(inclusive) to end (exclusive).
+ *
+ * @see {@link android.text.Selection}.
+ */
+ public WriteData setSelectionEnd(int v) {
+ writableProperties().putInt(PROP_SELECTION_END, v);
+ return this;
+ }
+
+ /**
+ * @see {@link #setSelectionEnd}.
+ */
+ public int getSelectionEnd() {
+ return mProperties.getInt(PROP_SELECTION_END, -1);
+ }
+
+ private Bundle writableProperties() {
+ if (mProperties == Bundle.EMPTY) {
+ mProperties = new Bundle();
+ }
+ return mProperties;
+ }
+
+ /**
+ * Checks constraints on the fields that must be satisfied before sending
+ * this instance to the self braille service.
+ * @throws IllegalStateException
+ */
+ public void validate() throws IllegalStateException {
+ if (mAccessibilityNodeInfo == null) {
+ throw new IllegalStateException(
+ "Accessibility node info can't be null");
+ }
+ int selectionStart = getSelectionStart();
+ int selectionEnd = getSelectionEnd();
+ if (mText == null) {
+ if (selectionStart > 0 || selectionEnd > 0) {
+ throw new IllegalStateException(
+ "Selection can't be set without text");
+ }
+ } else {
+ if (selectionStart < 0 && selectionEnd >= 0) {
+ throw new IllegalStateException(
+ "Selection end without start");
+ }
+ int textLength = mText.length();
+ if (selectionStart > textLength || selectionEnd > textLength) {
+ throw new IllegalStateException("Selection out of bounds");
+ }
+ }
+ }
+
+ // For Parcelable support.
+
+ public static final Parcelable.Creator<WriteData> CREATOR =
+ new Parcelable.Creator<WriteData>() {
+ @Override
+ public WriteData createFromParcel(Parcel in) {
+ return new WriteData(in);
+ }
+
+ @Override
+ public WriteData[] newArray(int size) {
+ return new WriteData[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <strong>Note:</strong> The {@link AccessibilityNodeInfo} will be
+ * recycled by this method, don't try to use this more than once.
+ */
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ mAccessibilityNodeInfo.writeToParcel(out, flags);
+ // The above call recycles the node, so make sure we don't use it
+ // anymore.
+ mAccessibilityNodeInfo = null;
+ out.writeString(mText.toString());
+ out.writeBundle(mProperties);
+ }
+
+ private WriteData() {
+ }
+
+ private WriteData(Parcel in) {
+ mAccessibilityNodeInfo =
+ AccessibilityNodeInfo.CREATOR.createFromParcel(in);
+ mText = in.readString();
+ mProperties = in.readBundle();
+ }
+}