Bug 1258470 - Part 14: Add geckoview, still using org.mozilla.gecko. draft
authorNick Alexander <nalexander@mozilla.com>
Thu, 23 Jun 2016 16:51:08 -0700
changeset 381008 ec97764339c4ff5867cbe1ce1143c774d35631e0
parent 381007 66e0add0e95b44907fd6c7fd0ed3f9e6b61bda57
child 381009 696748091cd51accbcf78bd22a7cf518bf6d90bf
push id21383
push usernalexander@mozilla.com
push dateFri, 24 Jun 2016 00:16:43 +0000
bugs1258470
milestone50.0a1
Bug 1258470 - Part 14: Add geckoview, still using org.mozilla.gecko. MozReview-Commit-ID: F50SpteGDhj
mobile/android/app/build.gradle
mobile/android/base/java/org/mozilla/gecko/ANRReporter.java
mobile/android/base/java/org/mozilla/gecko/AlarmReceiver.java
mobile/android/base/java/org/mozilla/gecko/AndroidGamepadManager.java
mobile/android/base/java/org/mozilla/gecko/BaseGeckoInterface.java
mobile/android/base/java/org/mozilla/gecko/ContextGetter.java
mobile/android/base/java/org/mozilla/gecko/CrashHandler.java
mobile/android/base/java/org/mozilla/gecko/EventDispatcher.java
mobile/android/base/java/org/mozilla/gecko/GeckoAccessibility.java
mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
mobile/android/base/java/org/mozilla/gecko/GeckoBatteryManager.java
mobile/android/base/java/org/mozilla/gecko/GeckoEditable.java
mobile/android/base/java/org/mozilla/gecko/GeckoEditableClient.java
mobile/android/base/java/org/mozilla/gecko/GeckoEditableListener.java
mobile/android/base/java/org/mozilla/gecko/GeckoEvent.java
mobile/android/base/java/org/mozilla/gecko/GeckoHalDefines.java
mobile/android/base/java/org/mozilla/gecko/GeckoInputConnection.java
mobile/android/base/java/org/mozilla/gecko/GeckoJavaSampler.java
mobile/android/base/java/org/mozilla/gecko/GeckoNetworkManager.java
mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
mobile/android/base/java/org/mozilla/gecko/GeckoProfileDirectories.java
mobile/android/base/java/org/mozilla/gecko/GeckoScreenOrientation.java
mobile/android/base/java/org/mozilla/gecko/GeckoService.java
mobile/android/base/java/org/mozilla/gecko/GeckoSharedPrefs.java
mobile/android/base/java/org/mozilla/gecko/GeckoSmsManager.java
mobile/android/base/java/org/mozilla/gecko/GeckoThread.java
mobile/android/base/java/org/mozilla/gecko/GeckoView.java
mobile/android/base/java/org/mozilla/gecko/GeckoViewChrome.java
mobile/android/base/java/org/mozilla/gecko/GeckoViewContent.java
mobile/android/base/java/org/mozilla/gecko/InputConnectionListener.java
mobile/android/base/java/org/mozilla/gecko/InputMethods.java
mobile/android/base/java/org/mozilla/gecko/NSSBridge.java
mobile/android/base/java/org/mozilla/gecko/NotificationClient.java
mobile/android/base/java/org/mozilla/gecko/NotificationHandler.java
mobile/android/base/java/org/mozilla/gecko/NotificationService.java
mobile/android/base/java/org/mozilla/gecko/PrefsHelper.java
mobile/android/base/java/org/mozilla/gecko/ServiceNotificationClient.java
mobile/android/base/java/org/mozilla/gecko/SmsManager.java
mobile/android/base/java/org/mozilla/gecko/SurfaceBits.java
mobile/android/base/java/org/mozilla/gecko/TouchEventInterceptor.java
mobile/android/base/java/org/mozilla/gecko/gfx/BitmapUtils.java
mobile/android/base/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java
mobile/android/base/java/org/mozilla/gecko/gfx/DisplayPortMetrics.java
mobile/android/base/java/org/mozilla/gecko/gfx/DrawTimingQueue.java
mobile/android/base/java/org/mozilla/gecko/gfx/DynamicToolbarAnimator.java
mobile/android/base/java/org/mozilla/gecko/gfx/FloatSize.java
mobile/android/base/java/org/mozilla/gecko/gfx/FullScreenState.java
mobile/android/base/java/org/mozilla/gecko/gfx/GLController.java
mobile/android/base/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
mobile/android/base/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java
mobile/android/base/java/org/mozilla/gecko/gfx/IntSize.java
mobile/android/base/java/org/mozilla/gecko/gfx/Layer.java
mobile/android/base/java/org/mozilla/gecko/gfx/LayerRenderer.java
mobile/android/base/java/org/mozilla/gecko/gfx/LayerView.java
mobile/android/base/java/org/mozilla/gecko/gfx/NativePanZoomController.java
mobile/android/base/java/org/mozilla/gecko/gfx/Overscroll.java
mobile/android/base/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java
mobile/android/base/java/org/mozilla/gecko/gfx/PanZoomController.java
mobile/android/base/java/org/mozilla/gecko/gfx/PanZoomTarget.java
mobile/android/base/java/org/mozilla/gecko/gfx/PanningPerfAPI.java
mobile/android/base/java/org/mozilla/gecko/gfx/PluginLayer.java
mobile/android/base/java/org/mozilla/gecko/gfx/PointUtils.java
mobile/android/base/java/org/mozilla/gecko/gfx/ProgressiveUpdateData.java
mobile/android/base/java/org/mozilla/gecko/gfx/RectUtils.java
mobile/android/base/java/org/mozilla/gecko/gfx/RenderTask.java
mobile/android/base/java/org/mozilla/gecko/gfx/TextureGenerator.java
mobile/android/base/java/org/mozilla/gecko/gfx/TextureReaper.java
mobile/android/base/java/org/mozilla/gecko/gfx/ViewTransform.java
mobile/android/base/java/org/mozilla/gecko/gfx/VirtualLayer.java
mobile/android/base/java/org/mozilla/gecko/mozglue/ByteBufferInputStream.java
mobile/android/base/java/org/mozilla/gecko/mozglue/DirectBufferAllocator.java
mobile/android/base/java/org/mozilla/gecko/mozglue/GeckoLoader.java
mobile/android/base/java/org/mozilla/gecko/mozglue/JNIObject.java
mobile/android/base/java/org/mozilla/gecko/mozglue/NativeReference.java
mobile/android/base/java/org/mozilla/gecko/mozglue/NativeZip.java
mobile/android/base/java/org/mozilla/gecko/mozglue/SafeIntent.java
mobile/android/base/java/org/mozilla/gecko/permissions/PermissionBlock.java
mobile/android/base/java/org/mozilla/gecko/permissions/Permissions.java
mobile/android/base/java/org/mozilla/gecko/permissions/PermissionsHelper.java
mobile/android/base/java/org/mozilla/gecko/restrictions/DefaultConfiguration.java
mobile/android/base/java/org/mozilla/gecko/restrictions/GuestProfileConfiguration.java
mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictable.java
mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java
mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionCache.java
mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionConfiguration.java
mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionProvider.java
mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictions.java
mobile/android/base/java/org/mozilla/gecko/sqlite/ByteBufferInputStream.java
mobile/android/base/java/org/mozilla/gecko/sqlite/MatrixBlobCursor.java
mobile/android/base/java/org/mozilla/gecko/sqlite/SQLiteBridge.java
mobile/android/base/java/org/mozilla/gecko/sqlite/SQLiteBridgeException.java
mobile/android/base/java/org/mozilla/gecko/util/ActivityResultHandler.java
mobile/android/base/java/org/mozilla/gecko/util/ActivityResultHandlerMap.java
mobile/android/base/java/org/mozilla/gecko/util/ActivityUtils.java
mobile/android/base/java/org/mozilla/gecko/util/BundleEventListener.java
mobile/android/base/java/org/mozilla/gecko/util/Clipboard.java
mobile/android/base/java/org/mozilla/gecko/util/ContextUtils.java
mobile/android/base/java/org/mozilla/gecko/util/DateUtil.java
mobile/android/base/java/org/mozilla/gecko/util/DrawableUtil.java
mobile/android/base/java/org/mozilla/gecko/util/EventCallback.java
mobile/android/base/java/org/mozilla/gecko/util/FileUtils.java
mobile/android/base/java/org/mozilla/gecko/util/FloatUtils.java
mobile/android/base/java/org/mozilla/gecko/util/GamepadUtils.java
mobile/android/base/java/org/mozilla/gecko/util/GeckoBackgroundThread.java
mobile/android/base/java/org/mozilla/gecko/util/GeckoEventListener.java
mobile/android/base/java/org/mozilla/gecko/util/GeckoJarReader.java
mobile/android/base/java/org/mozilla/gecko/util/GeckoRequest.java
mobile/android/base/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java
mobile/android/base/java/org/mozilla/gecko/util/HardwareUtils.java
mobile/android/base/java/org/mozilla/gecko/util/INIParser.java
mobile/android/base/java/org/mozilla/gecko/util/INISection.java
mobile/android/base/java/org/mozilla/gecko/util/IOUtils.java
mobile/android/base/java/org/mozilla/gecko/util/InputOptionsUtils.java
mobile/android/base/java/org/mozilla/gecko/util/IntentUtils.java
mobile/android/base/java/org/mozilla/gecko/util/JSONUtils.java
mobile/android/base/java/org/mozilla/gecko/util/MenuUtils.java
mobile/android/base/java/org/mozilla/gecko/util/NativeEventListener.java
mobile/android/base/java/org/mozilla/gecko/util/NativeJSContainer.java
mobile/android/base/java/org/mozilla/gecko/util/NativeJSObject.java
mobile/android/base/java/org/mozilla/gecko/util/NetworkUtils.java
mobile/android/base/java/org/mozilla/gecko/util/NonEvictingLruCache.java
mobile/android/base/java/org/mozilla/gecko/util/PrefUtils.java
mobile/android/base/java/org/mozilla/gecko/util/ProxySelector.java
mobile/android/base/java/org/mozilla/gecko/util/RawResource.java
mobile/android/base/java/org/mozilla/gecko/util/StringUtils.java
mobile/android/base/java/org/mozilla/gecko/util/ThreadUtils.java
mobile/android/base/java/org/mozilla/gecko/util/UIAsyncTask.java
mobile/android/base/java/org/mozilla/gecko/util/UUIDUtil.java
mobile/android/base/java/org/mozilla/gecko/util/WeakReferenceHandler.java
mobile/android/base/java/org/mozilla/gecko/util/WebActivityMapper.java
mobile/android/base/java/org/mozilla/gecko/util/WindowUtils.java
mobile/android/base/java/org/mozilla/gecko/widget/SwipeDismissListViewTouchListener.java
mobile/android/base/moz.build
mobile/android/geckoview/build.gradle
mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko/JNITarget.java
mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko/ReflectionTarget.java
mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko/RobocopTarget.java
mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko/SysInfo.java
mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko/WebRTCJNITarget.java
mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko/WrapForJNI.java
mobile/android/geckoview/src/main/AndroidManifest.xml
mobile/android/geckoview/src/main/java/com/googlecode/eyesfree/braille/selfbraille/ISelfBrailleService.java
mobile/android/geckoview/src/main/java/com/googlecode/eyesfree/braille/selfbraille/SelfBrailleClient.java
mobile/android/geckoview/src/main/java/com/googlecode/eyesfree/braille/selfbraille/WriteData.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/ANRReporter.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/AlarmReceiver.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/BaseGeckoInterface.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/ContextGetter.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/CrashHandler.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAccessibility.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoBatteryManager.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableClient.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableListener.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEvent.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoHalDefines.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoJavaSampler.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfile.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfileDirectories.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenOrientation.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoService.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSharedPrefs.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSmsManager.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoView.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewChrome.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewContent.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputConnectionListener.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputMethods.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/NSSBridge.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/NotificationClient.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/NotificationHandler.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/NotificationService.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/PrefsHelper.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/ServiceNotificationClient.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/SmsManager.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/SurfaceBits.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/TouchEventInterceptor.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BitmapUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DisplayPortMetrics.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DrawTimingQueue.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DynamicToolbarAnimator.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FloatSize.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FullScreenState.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GLController.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/IntSize.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/Layer.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerRenderer.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/Overscroll.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomTarget.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PluginLayer.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PointUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ProgressiveUpdateData.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RectUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RenderTask.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/TextureGenerator.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/TextureReaper.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ViewTransform.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/VirtualLayer.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/ByteBufferInputStream.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/DirectBufferAllocator.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/JNIObject.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeReference.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeZip.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/SafeIntent.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionBlock.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/Permissions.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionsHelper.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/DefaultConfiguration.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/GuestProfileConfiguration.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/Restrictable.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/RestrictionCache.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/RestrictionConfiguration.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/RestrictionProvider.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/Restrictions.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/ByteBufferInputStream.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/MatrixBlobCursor.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridge.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridgeException.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandler.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandlerMap.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/Clipboard.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ContextUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DateUtil.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DrawableUtil.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FileUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FloatUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GamepadUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoEventListener.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoJarReader.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoRequest.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INIParser.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INISection.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IOUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputOptionsUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/JSONUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/MenuUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeEventListener.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSContainer.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSObject.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NonEvictingLruCache.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/PrefUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/RawResource.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StringUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UIAsyncTask.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UUIDUtil.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WeakReferenceHandler.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WebActivityMapper.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WindowUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/widget/SwipeDismissListViewTouchListener.java
mobile/android/geckoview/src/main/res/drawable-hdpi/home_bg.png
mobile/android/geckoview/src/main/res/drawable-hdpi/home_star.png
mobile/android/geckoview/src/main/res/drawable-hdpi/ic_status_logo.png
mobile/android/geckoview/src/main/res/drawable/scrollbar.png
mobile/android/geckoview/src/main/res/values/dimens.xml
mobile/android/geckoview/src/main/res/values/ids.xml
mv_geckoview.sh
settings.gradle
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -66,16 +66,17 @@ android {
         }
     }
 
     sourceSets {
         main {
             manifest.srcFile "${project.buildDir}/generated/source/preprocessed_manifest/AndroidManifest.xml"
 
             java {
+                srcDir "${topsrcdir}/mobile/android/geckoview/src/main/java"
                 srcDir "${topsrcdir}/mobile/android/base/java"
                 srcDir "${topsrcdir}/mobile/android/search/java"
                 srcDir "${topsrcdir}/mobile/android/javaaddons/java"
                 srcDir "${topsrcdir}/mobile/android/services/src/main/java"
 
                 if (mozconfig.substs.MOZ_ANDROID_MLS_STUMBLER) {
                     srcDir "${topsrcdir}/mobile/android/stumbler/java"
                 }
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/ANRReporter.java
+++ /dev/null
@@ -1,596 +0,0 @@
-/* -*- 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.io.BufferedOutputStream;
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.FileReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.Reader;
-import java.util.Locale;
-import java.util.UUID;
-import java.util.regex.Pattern;
-
-import org.json.JSONObject;
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Log;
-
-public final class ANRReporter extends BroadcastReceiver
-{
-    private static final boolean DEBUG = false;
-    private static final String LOGTAG = "GeckoANRReporter";
-
-    private static final String ANR_ACTION = "android.intent.action.ANR";
-    // Number of lines to search traces.txt to decide whether it's a Gecko ANR
-    private static final int LINES_TO_IDENTIFY_TRACES = 10;
-    // ANRs may happen because of memory pressure,
-    //  so don't use up too much memory here
-    // Size of buffer to hold one line of text
-    private static final int TRACES_LINE_SIZE = 100;
-    // Size of block to use when processing traces.txt
-    private static final int TRACES_BLOCK_SIZE = 2000;
-    private static final String TRACES_CHARSET = "utf-8";
-    private static final String PING_CHARSET = "utf-8";
-
-    private static final ANRReporter sInstance = new ANRReporter();
-    private static int sRegisteredCount;
-    private Handler mHandler;
-    private volatile boolean mPendingANR;
-
-    @WrapForJNI
-    private static native boolean requestNativeStack(boolean unwind);
-    @WrapForJNI
-    private static native String getNativeStack();
-    @WrapForJNI
-    private static native void releaseNativeStack();
-
-    public static void register(Context context) {
-        if (sRegisteredCount++ != 0) {
-            // Already registered
-            return;
-        }
-        sInstance.start(context);
-    }
-
-    public static void unregister() {
-        if (sRegisteredCount == 0) {
-            Log.w(LOGTAG, "register/unregister mismatch");
-            return;
-        }
-        if (--sRegisteredCount != 0) {
-            // Should still be registered
-            return;
-        }
-        sInstance.stop();
-    }
-
-    private void start(final Context context) {
-
-        Thread receiverThread = new Thread(new Runnable() {
-            @Override
-            public void run() {
-                Looper.prepare();
-                synchronized (ANRReporter.this) {
-                    mHandler = new Handler();
-                    ANRReporter.this.notify();
-                }
-                if (DEBUG) {
-                    Log.d(LOGTAG, "registering receiver");
-                }
-                context.registerReceiver(ANRReporter.this,
-                                         new IntentFilter(ANR_ACTION),
-                                         null,
-                                         mHandler);
-                Looper.loop();
-
-                if (DEBUG) {
-                    Log.d(LOGTAG, "unregistering receiver");
-                }
-                context.unregisterReceiver(ANRReporter.this);
-                mHandler = null;
-            }
-        }, LOGTAG);
-
-        receiverThread.setDaemon(true);
-        receiverThread.start();
-    }
-
-    private void stop() {
-        synchronized (this) {
-            while (mHandler == null) {
-                try {
-                    wait(1000);
-                    if (mHandler == null) {
-                        // We timed out; just give up. The process is probably
-                        // quitting anyways, so we let the OS do the clean up
-                        Log.w(LOGTAG, "timed out waiting for handler");
-                        return;
-                    }
-                } catch (InterruptedException e) {
-                }
-            }
-        }
-        Looper looper = mHandler.getLooper();
-        looper.quit();
-        try {
-            looper.getThread().join();
-        } catch (InterruptedException e) {
-        }
-    }
-
-    private ANRReporter() {
-    }
-
-    // Return the "traces.txt" file, or null if there is no such file
-    private static File getTracesFile() {
-        // Check most common location first.
-        File tracesFile = new File("/data/anr/traces.txt");
-        if (tracesFile.isFile() && tracesFile.canRead()) {
-            return tracesFile;
-        }
-
-        // Find the traces file name if we can.
-        try {
-            // getprop [prop-name [default-value]]
-            Process propProc = (new ProcessBuilder())
-                .command("/system/bin/getprop", "dalvik.vm.stack-trace-file")
-                .redirectErrorStream(true)
-                .start();
-            try {
-                BufferedReader buf = new BufferedReader(
-                    new InputStreamReader(propProc.getInputStream()), TRACES_LINE_SIZE);
-                String propVal = buf.readLine();
-                if (DEBUG) {
-                    Log.d(LOGTAG, "getprop returned " + String.valueOf(propVal));
-                }
-                // getprop can return empty string when the prop value is empty
-                // or prop is undefined, treat both cases the same way
-                if (propVal != null && propVal.length() != 0) {
-                    tracesFile = new File(propVal);
-                    if (tracesFile.isFile() && tracesFile.canRead()) {
-                        return tracesFile;
-                    } else if (DEBUG) {
-                        Log.d(LOGTAG, "cannot access traces file");
-                    }
-                } else if (DEBUG) {
-                    Log.d(LOGTAG, "empty getprop result");
-                }
-            } finally {
-                propProc.destroy();
-            }
-        } catch (IOException e) {
-            Log.w(LOGTAG, e);
-        } catch (ClassCastException e) {
-            Log.w(LOGTAG, e); // Bug 975436
-        }
-        return null;
-    }
-
-    private static File getPingFile() {
-        if (GeckoAppShell.getContext() == null) {
-            return null;
-        }
-        GeckoProfile profile = GeckoAppShell.getGeckoInterface().getProfile();
-        if (profile == null) {
-            return null;
-        }
-        File profDir = profile.getDir();
-        if (profDir == null) {
-            return null;
-        }
-        File pingDir = new File(profDir, "saved-telemetry-pings");
-        pingDir.mkdirs();
-        if (!(pingDir.exists() && pingDir.isDirectory())) {
-            return null;
-        }
-        return new File(pingDir, UUID.randomUUID().toString());
-    }
-
-    // Return true if the traces file corresponds to a Gecko ANR
-    private static boolean isGeckoTraces(String pkgName, File tracesFile) {
-        try {
-            final String END_OF_PACKAGE_NAME = "([^a-zA-Z0-9_]|$)";
-            // Regex for finding our package name in the traces file
-            Pattern pkgPattern = Pattern.compile(Pattern.quote(pkgName) + END_OF_PACKAGE_NAME);
-            Pattern mangledPattern = null;
-            if (!AppConstants.MANGLED_ANDROID_PACKAGE_NAME.equals(pkgName)) {
-                mangledPattern = Pattern.compile(Pattern.quote(
-                    AppConstants.MANGLED_ANDROID_PACKAGE_NAME) + END_OF_PACKAGE_NAME);
-            }
-            if (DEBUG) {
-                Log.d(LOGTAG, "trying to match package: " + pkgName);
-            }
-            BufferedReader traces = new BufferedReader(
-                new FileReader(tracesFile), TRACES_BLOCK_SIZE);
-            try {
-                for (int count = 0; count < LINES_TO_IDENTIFY_TRACES; count++) {
-                    String line = traces.readLine();
-                    if (DEBUG) {
-                        Log.d(LOGTAG, "identifying line: " + String.valueOf(line));
-                    }
-                    if (line == null) {
-                        if (DEBUG) {
-                            Log.d(LOGTAG, "reached end of traces file");
-                        }
-                        return false;
-                    }
-                    if (pkgPattern.matcher(line).find()) {
-                        // traces.txt file contains our package
-                        return true;
-                    }
-                    if (mangledPattern != null && mangledPattern.matcher(line).find()) {
-                        // traces.txt file contains our alternate package
-                        return true;
-                    }
-                }
-            } finally {
-                traces.close();
-            }
-        } catch (IOException e) {
-            // meh, can't even read from it right. just return false
-        }
-        return false;
-    }
-
-    private static long getUptimeMins() {
-
-        long uptimeMins = (new File("/proc/self/stat")).lastModified();
-        if (uptimeMins != 0L) {
-            uptimeMins = (System.currentTimeMillis() - uptimeMins) / 1000L / 60L;
-            if (DEBUG) {
-                Log.d(LOGTAG, "uptime " + String.valueOf(uptimeMins));
-            }
-            return uptimeMins;
-        }
-        if (DEBUG) {
-            Log.d(LOGTAG, "could not get uptime");
-        }
-        return 0L;
-    }
-
-    /*
-        a saved telemetry ping file consists of JSON in the following format,
-            {
-                "reason": "android-anr-report",
-                "slug": "<uuid-string>",
-                "payload": <json-object>
-            }
-        for Android ANR, our JSON payload should look like,
-            {
-                "ver": 1,
-                "simpleMeasurements": {
-                    "uptime": <uptime>
-                },
-                "info": {
-                    "reason": "android-anr-report",
-                    "OS": "Android",
-                    ...
-                },
-                "androidANR": "...",
-                "androidLogcat": "..."
-            }
-    */
-
-    private static int writePingPayload(OutputStream ping,
-                                        String payload) throws IOException {
-        byte [] data = payload.getBytes(PING_CHARSET);
-        ping.write(data);
-        return data.length;
-    }
-
-    private static void fillPingHeader(OutputStream ping, String slug)
-            throws IOException {
-
-        // ping file header
-        byte [] data = ("{" +
-            "\"reason\":\"android-anr-report\"," +
-            "\"slug\":" + JSONObject.quote(slug) + "," +
-            "\"payload\":").getBytes(PING_CHARSET);
-        ping.write(data);
-        if (DEBUG) {
-            Log.d(LOGTAG, "wrote ping header, size = " + String.valueOf(data.length));
-        }
-
-        // payload start
-        int size = writePingPayload(ping, ("{" +
-            "\"ver\":1," +
-            "\"simpleMeasurements\":{" +
-                "\"uptime\":" + String.valueOf(getUptimeMins()) +
-            "}," +
-            "\"info\":{" +
-                "\"reason\":\"android-anr-report\"," +
-                "\"OS\":" + JSONObject.quote(SysInfo.getName()) + "," +
-                "\"version\":\"" + String.valueOf(SysInfo.getVersion()) + "\"," +
-                "\"appID\":" + JSONObject.quote(AppConstants.MOZ_APP_ID) + "," +
-                "\"appVersion\":" + JSONObject.quote(AppConstants.MOZ_APP_VERSION) + "," +
-                "\"appName\":" + JSONObject.quote(AppConstants.MOZ_APP_BASENAME) + "," +
-                "\"appBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," +
-                "\"appUpdateChannel\":" + JSONObject.quote(AppConstants.MOZ_UPDATE_CHANNEL) + "," +
-                // Technically the platform build ID may be different, but we'll never know
-                "\"platformBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," +
-                "\"locale\":" + JSONObject.quote(Locales.getLanguageTag(Locale.getDefault())) + "," +
-                "\"cpucount\":" + String.valueOf(SysInfo.getCPUCount()) + "," +
-                "\"memsize\":" + String.valueOf(SysInfo.getMemSize()) + "," +
-                "\"arch\":" + JSONObject.quote(SysInfo.getArchABI()) + "," +
-                "\"kernel_version\":" + JSONObject.quote(SysInfo.getKernelVersion()) + "," +
-                "\"device\":" + JSONObject.quote(SysInfo.getDevice()) + "," +
-                "\"manufacturer\":" + JSONObject.quote(SysInfo.getManufacturer()) + "," +
-                "\"hardware\":" + JSONObject.quote(SysInfo.getHardware()) +
-            "}," +
-            "\"androidANR\":\""));
-        if (DEBUG) {
-            Log.d(LOGTAG, "wrote metadata, size = " + String.valueOf(size));
-        }
-
-        // We are at the start of ANR data
-    }
-
-    // Block is a section of the larger input stream, and we want to find pattern within
-    // the stream. This is straightforward if the entire pattern is within one block;
-    // however, if the pattern spans across two blocks, we have to match both the start of
-    // the pattern in the first block and the end of the pattern in the second block.
-    // * If pattern is found in block, this method returns the index at the end of the
-    //   found pattern, which must always be > 0.
-    // * If pattern is not found, it returns 0.
-    // * If the start of the pattern matches the end of the block, it returns a number
-    //   < 0, which equals the negated value of how many characters in pattern are already
-    //   matched; when processing the next block, this number is passed in through
-    //   prevIndex, and the rest of the characters in pattern are matched against the
-    //   start of this second block. The method returns value > 0 if the rest of the
-    //   characters match, or 0 if they do not.
-    private static int getEndPatternIndex(String block, String pattern, int prevIndex) {
-        if (pattern == null || block.length() < pattern.length()) {
-            // Nothing to do
-            return 0;
-        }
-        if (prevIndex < 0) {
-            // Last block ended with a partial start; now match start of block to rest of pattern
-            if (block.startsWith(pattern.substring(-prevIndex, pattern.length()))) {
-                // Rest of pattern matches; return index at end of pattern
-                return pattern.length() + prevIndex;
-            }
-            // Not a match; continue with normal search
-        }
-        // Did not find pattern in last block; see if entire pattern is inside this block
-        int index = block.indexOf(pattern);
-        if (index >= 0) {
-            // Found pattern; return index at end of the pattern
-            return index + pattern.length();
-        }
-        // Block does not contain the entire pattern, but see if the end of the block
-        // contains the start of pattern. To do that, we see if block ends with the
-        // first n-1 characters of pattern, the first n-2 characters of pattern, etc.
-        for (index = block.length() - pattern.length() + 1; index < block.length(); index++) {
-            // Using index as a start, see if the rest of block contains the start of pattern
-            if (block.charAt(index) == pattern.charAt(0) &&
-                block.endsWith(pattern.substring(0, block.length() - index))) {
-                // Found partial match; return -(number of characters matched),
-                // i.e. -1 for 1 character matched, -2 for 2 characters matched, etc.
-                return index - block.length();
-            }
-        }
-        return 0;
-    }
-
-    // Copy the content of reader to ping;
-    // copying stops when endPattern is found in the input stream
-    private static int fillPingBlock(OutputStream ping,
-                                     Reader reader, String endPattern)
-            throws IOException {
-
-        int total = 0;
-        int endIndex = 0;
-        char [] block = new char[TRACES_BLOCK_SIZE];
-        for (int size = reader.read(block); size >= 0; size = reader.read(block)) {
-            String stringBlock = new String(block, 0, size);
-            endIndex = getEndPatternIndex(stringBlock, endPattern, endIndex);
-            if (endIndex > 0) {
-                // Found end pattern; clip the string
-                stringBlock = stringBlock.substring(0, endIndex);
-            }
-            String quoted = JSONObject.quote(stringBlock);
-            total += writePingPayload(ping, quoted.substring(1, quoted.length() - 1));
-            if (endIndex > 0) {
-                // End pattern already found; return now
-                break;
-            }
-        }
-        return total;
-    }
-
-    private static void fillLogcat(final OutputStream ping) {
-        if (Versions.preJB) {
-            // Logcat retrieval is not supported on pre-JB devices.
-            return;
-        }
-
-        try {
-            // get the last 200 lines of logcat
-            Process proc = (new ProcessBuilder())
-                .command("/system/bin/logcat", "-v", "threadtime", "-t", "200", "-d", "*:D")
-                .redirectErrorStream(true)
-                .start();
-            try {
-                Reader procOut = new InputStreamReader(proc.getInputStream(), TRACES_CHARSET);
-                int size = fillPingBlock(ping, procOut, null);
-                if (DEBUG) {
-                    Log.d(LOGTAG, "wrote logcat, size = " + String.valueOf(size));
-                }
-            } finally {
-                proc.destroy();
-            }
-        } catch (IOException e) {
-            // ignore because logcat is not essential
-            Log.w(LOGTAG, e);
-        }
-    }
-
-    private static void fillPingFooter(OutputStream ping,
-                                       boolean haveNativeStack)
-            throws IOException {
-
-        // We are at the end of ANR data
-
-        int total = writePingPayload(ping, ("\"," +
-                "\"androidLogcat\":\""));
-        fillLogcat(ping);
-
-        if (haveNativeStack) {
-            total += writePingPayload(ping, ("\"," +
-                    "\"androidNativeStack\":"));
-
-            String nativeStack = String.valueOf(getNativeStack());
-            int size = writePingPayload(ping, nativeStack);
-            if (DEBUG) {
-                Log.d(LOGTAG, "wrote native stack, size = " + String.valueOf(size));
-            }
-            total += size + writePingPayload(ping, "}");
-        } else {
-            total += writePingPayload(ping, "\"}");
-        }
-
-        byte [] data = (
-            "}").getBytes(PING_CHARSET);
-        ping.write(data);
-        if (DEBUG) {
-            Log.d(LOGTAG, "wrote ping footer, size = " + String.valueOf(data.length + total));
-        }
-    }
-
-    private static void processTraces(Reader traces, File pingFile) {
-
-        // Only get native stack if Gecko is running.
-        // Also, unwinding is memory intensive, so only unwind if we have enough memory.
-        final boolean haveNativeStack =
-            GeckoThread.isRunning() ?
-            requestNativeStack(/* unwind */ SysInfo.getMemSize() >= 640) : false;
-
-        try {
-            OutputStream ping = new BufferedOutputStream(
-                new FileOutputStream(pingFile), TRACES_BLOCK_SIZE);
-            try {
-                fillPingHeader(ping, pingFile.getName());
-                // Traces file has the format
-                //    ----- pid xxx at xxx -----
-                //    Cmd line: org.mozilla.xxx
-                //    * stack trace *
-                //    ----- end xxx -----
-                //    ----- pid xxx at xxx -----
-                //    Cmd line: com.android.xxx
-                //    * stack trace *
-                //    ...
-                // If we end the stack dump at the first end marker,
-                // only Fennec stacks will be dumped
-                int size = fillPingBlock(ping, traces, "\n----- end");
-                if (DEBUG) {
-                    Log.d(LOGTAG, "wrote traces, size = " + String.valueOf(size));
-                }
-                fillPingFooter(ping, haveNativeStack);
-                if (DEBUG) {
-                    Log.d(LOGTAG, "finished creating ping file");
-                }
-                return;
-            } finally {
-                ping.close();
-                if (haveNativeStack) {
-                    releaseNativeStack();
-                }
-            }
-        } catch (IOException e) {
-            Log.w(LOGTAG, e);
-        }
-        // exception; delete ping file
-        if (pingFile.exists()) {
-            pingFile.delete();
-        }
-    }
-
-    private static void processTraces(File tracesFile, File pingFile) {
-        try {
-            Reader traces = new InputStreamReader(
-                new FileInputStream(tracesFile), TRACES_CHARSET);
-            try {
-                processTraces(traces, pingFile);
-            } finally {
-                traces.close();
-            }
-        } catch (IOException e) {
-            Log.w(LOGTAG, e);
-        }
-    }
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        if (mPendingANR) {
-            // we already processed an ANR without getting unstuck; skip this one
-            if (DEBUG) {
-                Log.d(LOGTAG, "skipping duplicate ANR");
-            }
-            return;
-        }
-        if (ThreadUtils.getUiHandler() != null) {
-            mPendingANR = true;
-            // detect when the main thread gets unstuck
-            ThreadUtils.postToUiThread(new Runnable() {
-                @Override
-                public void run() {
-                    // okay to reset mPendingANR on main thread
-                    mPendingANR = false;
-                    if (DEBUG) {
-                        Log.d(LOGTAG, "yay we got unstuck!");
-                    }
-                }
-            });
-        }
-        if (DEBUG) {
-            Log.d(LOGTAG, "receiving " + String.valueOf(intent));
-        }
-        if (!ANR_ACTION.equals(intent.getAction())) {
-            return;
-        }
-
-        // make sure we have a good save location first
-        File pingFile = getPingFile();
-        if (DEBUG) {
-            Log.d(LOGTAG, "using ping file: " + String.valueOf(pingFile));
-        }
-        if (pingFile == null) {
-            return;
-        }
-
-        File tracesFile = getTracesFile();
-        if (DEBUG) {
-            Log.d(LOGTAG, "using traces file: " + String.valueOf(tracesFile));
-        }
-        if (tracesFile == null) {
-            return;
-        }
-
-        // We get ANR intents from all ANRs in the system, but we only want Gecko ANRs
-        if (!isGeckoTraces(context.getPackageName(), tracesFile)) {
-            if (DEBUG) {
-                Log.d(LOGTAG, "traces is not Gecko ANR");
-            }
-            return;
-        }
-        Log.i(LOGTAG, "processing Gecko ANR");
-        processTraces(tracesFile, pingFile);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/AlarmReceiver.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/* -*- 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
-    private static native void notifyAlarmFired();
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/AndroidGamepadManager.java
+++ /dev/null
@@ -1,392 +0,0 @@
-/* -*- 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.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;
-                }
-            }
-        }
-    }
-
-    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() {
-    }
-
-    public static void startup() {
-        ThreadUtils.assertOnUiThread();
-        if (!sStarted) {
-            scanForGamepads();
-            addDeviceListener();
-            sStarted = true;
-        }
-    }
-
-    public static void shutdown() {
-        ThreadUtils.assertOnUiThread();
-        if (sStarted) {
-            removeDeviceListener();
-            sPendingGamepads.clear();
-            sGamepads.clear();
-            sStarted = false;
-        }
-    }
-
-    public static void gamepadAdded(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;
-            GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadButtonEvent(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.
-            GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadAxisEvent(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;
-                    GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadButtonEvent(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;
-        GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadButtonEvent(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) {
-        //TODO: when we're using a newer SDK version, use these.
-        //if (Build.VERSION.SDK_INT >= 12) {
-        //int vid = device.getVendorId();
-        //int pid = device.getProductId();
-        //}
-        sPendingGamepads.put(device.getId(), new ArrayList<KeyEvent>());
-        GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadAddRemoveEvent(device.getId(), true));
-    }
-
-    private static void removeGamepad(int deviceId) {
-        Gamepad gamepad = sGamepads.get(deviceId);
-        GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadAddRemoveEvent(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;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/BaseGeckoInterface.java
+++ /dev/null
@@ -1,186 +0,0 @@
-/* -*- 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;
-
-    public BaseGeckoInterface(Context context) {
-        mContext = context;
-    }
-
-    @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 908772: Implement this
-    @Override
-    public LocationListener getLocationListener() {
-        return null;
-    }
-
-    // Bug 908773: Implement this
-    @Override
-    public SensorEventListener getSensorEventListener() {
-        return null;
-    }
-
-    // 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, final RectF rect, final boolean isFullScreen) {}
-
-    // Bug 908781: Implement this
-    @Override
-    public void removePluginView(final View view, final boolean isFullScreen) {}
-
-    // Bug 908783: Implement this
-    @Override
-    public void enableCameraView() {}
-
-    // Bug 908785: Implement this
-    @Override
-    public void disableCameraView() {}
-
-    // Bug 908786: Implement this
-    @Override
-    public void addAppStateListener(GeckoAppShell.AppStateListener listener) {}
-
-    // Bug 908787: Implement this
-    @Override
-    public void removeAppStateListener(GeckoAppShell.AppStateListener listener) {}
-
-    // Bug 908788: Implement this
-    @Override
-    public View getCameraView() {
-        return null;
-    }
-
-    // Bug 908789: Implement this
-    @Override
-    public void notifyWakeLockChanged(String topic, String state) {}
-
-    // Bug 908790: Implement this
-    @Override
-    public void onInputMethodChanged(String inputMethod) {}
-
-    @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";
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/ContextGetter.java
+++ /dev/null
@@ -1,15 +0,0 @@
-/* -*- 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();
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/CrashHandler.java
+++ /dev/null
@@ -1,469 +0,0 @@
-/* -*- 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();
-    }
-
-    protected String getAppPackageName() {
-        final Context context = getAppContext();
-
-        if (context != null) {
-            return context.getPackageName();
-        }
-
-        try {
-            // Package name is also the command line string in most cases.
-            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) {
-            Log.i(LOGTAG, "Error reading package name", e);
-        }
-
-        // 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();
-
-        extras.putString("ProductName", pkgName);
-        extras.putLong("CrashTime", getCrashTime());
-        extras.putLong("StartupTime", getStartupTime());
-
-        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;
-            }
-        };
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/EventDispatcher.java
+++ /dev/null
@@ -1,409 +0,0 @@
-/* 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.GeckoAppShell;
-import org.mozilla.gecko.GeckoEvent;
-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";
-    private 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);
-
-    public static EventDispatcher getInstance() {
-        return INSTANCE;
-    }
-
-    private 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_BUILD && 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_BUILD) {
-            // 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_BUILD) {
-                    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);
-    }
-
-    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);
-    }
-
-    public void 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;
-        }
-
-        final List<NativeEventListener> listeners;
-        synchronized (mGeckoThreadNativeListeners) {
-            listeners = mGeckoThreadNativeListeners.get(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.  Dispatch an error rather than
-                // looking for JSON listeners.
-                if (callback != null) {
-                    callback.sendError("No listeners for request");
-                }
-            }
-            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;
-        }
-
-        // 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 (dispatchToThread(type, message, callback,
-                             mUiThreadListeners, ThreadUtils.getUiHandler()) ||
-            dispatchToThread(type, message, callback,
-                             mBackgroundThreadListeners, ThreadUtils.getBackgroundHandler())) {
-
-            // 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;
-        }
-
-        try {
-            // If we didn't find native listeners, try JSON listeners.
-            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);
-        }
-    }
-
-    private boolean dispatchToThread(final String type,
-                                     final NativeJSObject message,
-                                     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);
-
-                // There were native listeners, and they're gone.
-                // Dispatch an error rather than looking for more listeners.
-                if (callback != null) {
-                    callback.sendError("No listeners for request");
-                }
-                return true;
-            }
-
-            final Bundle messageAsBundle;
-            try {
-                messageAsBundle = message.toBundle();
-            } 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 void dispatchEvent(final JSONObject message, final EventCallback callback) {
-        // {
-        //   "type": "value",
-        //   "event_specific": "value",
-        //   ...
-        try {
-            final String type = message.getString("type");
-
-            List<GeckoEventListener> listeners;
-            synchronized (mGeckoThreadJSONListeners) {
-                listeners = mGeckoThreadJSONListeners.get(type);
-            }
-            if (listeners == null || listeners.isEmpty()) {
-                Log.w(LOGTAG, "No listeners for " + type);
-
-                // If there are no listeners, dispatch an error.
-                if (callback != null) {
-                    callback.sendError("No listeners for request");
-                }
-                return;
-            }
-            for (final GeckoEventListener listener : listeners) {
-                listener.handleMessage(type, message);
-            }
-        } catch (final JSONException e) {
-            Log.e(LOGTAG, "handleGeckoMessage throws " + e, e);
-        }
-    }
-
-    @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);
-        }
-    }
-
-    private 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);
-            }
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoAccessibility.java
+++ /dev/null
@@ -1,413 +0,0 @@
-/* -*- 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.gfx.LayerView;
-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.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(GeckoAppShell.getContext(), 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"));
-        if (Versions.feature14Plus) {
-            event.setToIndex(message.optInt("toIndex", -1));
-            event.setScrollable(message.optBoolean("scrollable"));
-            event.setScrollX(message.optInt("scrollX", -1));
-            event.setScrollY(message.optInt("scrollY", -1));
-        }
-        if (Versions.feature15Plus) {
-            event.setMaxScrollX(message.optInt("maxScrollX", -1));
-            event.setMaxScrollY(message.optInt("maxScrollY", -1));
-        }
-    }
-
-    private static void sendDirectAccessibilityEvent(int eventType, JSONObject message) {
-        final AccessibilityEvent accEvent = AccessibilityEvent.obtain(eventType);
-        accEvent.setClassName(GeckoAccessibility.class.getName());
-        accEvent.setPackageName(GeckoAppShell.getContext().getPackageName());
-        populateEventFromJSON(accEvent, message);
-        AccessibilityManager accessibilityManager =
-            (AccessibilityManager) GeckoAppShell.getContext().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 LayerView 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.getContext().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);
-                        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(LayerView layerview) {
-        // Only use this delegate in Jelly Bean.
-        if (Versions.feature16Plus) {
-            layerview.setAccessibilityDelegate(new GeckoAccessibilityDelegate());
-            layerview.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(LayerView layerview, 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.getContext().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;
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
+++ /dev/null
@@ -1,2362 +0,0 @@
-/* -*- 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.Sensor;
-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.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.webkit.MimeTypeMap;
-import android.widget.AbsoluteLayout;
-
-public class GeckoAppShell
-{
-    private static final String LOGTAG = "GeckoAppShell";
-
-    // We have static members only.
-    private GeckoAppShell() { }
-
-    public static final String ACTION_ALERT_CALLBACK       = "org.mozilla.gecko.ACTION_ALERT_CALLBACK";
-    public static final String ACTION_HOMESCREEN_SHORTCUT  = "org.mozilla.gecko.BOOKMARK";
-    public static final String PREFS_OOM_EXCEPTION         = "OOMException";
-
-    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(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 final Map<String, String> ALERT_COOKIES = new ConcurrentHashMap<String, String>();
-
-    private static volatile boolean locationHighAccuracyEnabled;
-
-    // Accessed by NotificationHelper. This should be encapsulated.
-    /* package */ static NotificationClient notificationClient;
-
-    // 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://mxr.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://mxr.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;
-
-    /* The Android-side API: API methods that Android calls */
-
-    // Initialization methods
-    public static native void registerJavaUiThread();
-
-    // helper methods
-    public static void callObserver(String observerKey, String topic, String data) {
-        sendEventToGecko(GeckoEvent.createCallObserverEvent(observerKey, topic, data));
-    }
-    public static void removeObserver(String observerKey) {
-        sendEventToGecko(GeckoEvent.createRemoveObserverEvent(observerKey));
-    }
-    public static native void onSurfaceTextureFrameAvailable(Object surfaceTexture, int id);
-    public static native void dispatchMemoryPressure();
-
-    private static native void reportJavaCrash(String stackTrace);
-
-    public static void notifyUriVisited(String uri) {
-        sendEventToGecko(GeckoEvent.createVisitedEvent(uri));
-    }
-
-    public static native void notifyBatteryChange(double aLevel, boolean aCharging, double aRemainingTime);
-
-    public static native void invalidateAndScheduleComposite();
-
-    public static native float computeRenderIntegrity();
-
-    public static native SurfaceBits getSurfaceBits(Surface surface);
-
-    public static native void addPresentationSurface(Surface surface);
-    public static native void removePresentationSurface(Surface surface);
-
-    public static native void onFullScreenPluginHidden(View view);
-
-    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;
-    }
-
-    /**
-     * If the Gecko thread is running, immediately dispatches the event to
-     * Gecko.
-     *
-     * If the Gecko thread is not running, queues the event. If the queue is
-     * full, throws {@link IllegalStateException}.
-     *
-     * Queued events will be dispatched in order of arrival when the Gecko
-     * thread becomes live.
-     *
-     * This method can be called from any thread.
-     *
-     * @param e
-     *            the event to dispatch. Cannot be null.
-     */
-    @RobocopTarget
-    public static void sendEventToGecko(GeckoEvent e) {
-        if (e == null) {
-            throw new IllegalArgumentException("e cannot be null.");
-        }
-
-        if (GeckoThread.isRunning()) {
-            notifyGeckoOfEvent(e);
-            // Gecko will copy the event data into a normal C++ object.
-            // We can recycle the event now.
-            e.recycle();
-            return;
-        }
-
-        GeckoThread.addPendingEvent(e);
-    }
-
-    /**
-     * 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.
-     *
-     * This method follows the same queuing conditions as {@link #sendEventToGecko(GeckoEvent)}.
-     * 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());
-    }
-
-    // Tell the Gecko event loop that an event is available.
-    public static native void notifyGeckoOfEvent(GeckoEvent event);
-
-    // Synchronously notify a Gecko observer; must be called from Gecko thread.
-    @WrapForJNI
-    public static native void syncNotifyObservers(String topic, String data);
-
-    @WrapForJNI(stubName = "NotifyObservers")
-    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(allowMultithread = true, noThrow = true)
-    public static String handleUncaughtException(Throwable e) {
-        if (AppConstants.MOZ_CRASHREPORTER) {
-            final Throwable exc = CrashHandler.getRootException(e);
-            final StackTraceElement[] stack = exc.getStackTrace();
-            if (stack.length >= 1 && stack[0].isNativeMethod()) {
-                // The exception occurred when running native code. Return an exception
-                // string and trigger the crash reporter inside the caller so that we get
-                // a better native stack in Socorro.
-                CrashHandler.logException(Thread.currentThread(), exc);
-                return CrashHandler.getExceptionStackTrace(exc);
-            }
-        }
-        CRASH_HANDLER.uncaughtException(null, e);
-        return null;
-    }
-
-    private static final Runnable sCallbackRunnable = new Runnable() {
-        @Override
-        public void run() {
-            ThreadUtils.assertOnUiThread();
-            long nextDelay = runUiThreadCallback();
-            if (nextDelay >= 0) {
-                ThreadUtils.getUiHandler().postDelayed(this, nextDelay);
-            }
-        }
-    };
-
-    private static native long runUiThreadCallback();
-
-    @WrapForJNI(allowMultithread = true)
-    private static void requestUiThreadCallback(long delay) {
-        ThreadUtils.getUiHandler().postDelayed(sCallbackRunnable, delay);
-    }
-
-    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
-    @SuppressLint("MissingPermission") // Permissions are explicitly checked for within this method
-    public static void enableLocation(final boolean enable) {
-        Permissions
-                .from((Activity) getContext())
-                .withPermissions(Manifest.permission.ACCESS_FINE_LOCATION)
-                .onUIThread()
-                .doNotPromptIf(!enable)
-                .run(new Runnable() {
-                    @Override
-                    public void run() {
-                        LocationManager lm = getLocationManager(getApplicationContext());
-                        if (lm == null) {
-                            return;
-                        }
-
-                        if (enable) {
-                            Location lastKnownLocation = getLastKnownLocation(lm);
-                            if (lastKnownLocation != null) {
-                                getGeckoInterface().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, (float) .5, getGeckoInterface().getLocationListener(), l);
-                        } else {
-                            lm.removeUpdates(getGeckoInterface().getLocationListener());
-                        }
-                    }
-                });
-    }
-
-    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
-    public static void enableLocationHighAccuracy(final boolean enable) {
-        locationHighAccuracyEnabled = enable;
-    }
-
-    @WrapForJNI
-    public 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
-    public 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
-    public 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(gi.getSensorEventListener(),
-                                    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(gi.getSensorEventListener(),
-                                    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(gi.getSensorEventListener(),
-                                    gOrientationSensor,
-                                    SensorManager.SENSOR_DELAY_FASTEST);
-            }
-            break;
-
-        case GeckoHalDefines.SENSOR_ACCELERATION:
-            if (gAccelerometerSensor == null) {
-                gAccelerometerSensor = sm.getDefaultSensor(
-                    Sensor.TYPE_ACCELEROMETER);
-            }
-            if (gAccelerometerSensor != null) {
-                sm.registerListener(gi.getSensorEventListener(),
-                                    gAccelerometerSensor,
-                                    SensorManager.SENSOR_DELAY_FASTEST);
-            }
-            break;
-
-        case GeckoHalDefines.SENSOR_PROXIMITY:
-            if (gProximitySensor == null) {
-                gProximitySensor = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY);
-            }
-            if (gProximitySensor != null) {
-                sm.registerListener(gi.getSensorEventListener(),
-                                    gProximitySensor,
-                                    SensorManager.SENSOR_DELAY_NORMAL);
-            }
-            break;
-
-        case GeckoHalDefines.SENSOR_LIGHT:
-            if (gLightSensor == null) {
-                gLightSensor = sm.getDefaultSensor(Sensor.TYPE_LIGHT);
-            }
-            if (gLightSensor != null) {
-                sm.registerListener(gi.getSensorEventListener(),
-                                    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(gi.getSensorEventListener(),
-                                    gLinearAccelerometerSensor,
-                                    SensorManager.SENSOR_DELAY_FASTEST);
-            }
-            break;
-
-        case GeckoHalDefines.SENSOR_GYROSCOPE:
-            if (gGyroscopeSensor == null) {
-                gGyroscopeSensor = sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
-            }
-            if (gGyroscopeSensor != null) {
-                sm.registerListener(gi.getSensorEventListener(),
-                                    gGyroscopeSensor,
-                                    SensorManager.SENSOR_DELAY_FASTEST);
-            }
-            break;
-
-        default:
-            Log.w(LOGTAG, "Error! Can't enable unknown SENSOR type " +
-                  aSensortype);
-        }
-    }
-
-    @WrapForJNI
-    public 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(gi.getSensorEventListener(), gGameRotationVectorSensor);
-              break;
-            }
-            // Fallthrough
-
-        case GeckoHalDefines.SENSOR_ROTATION_VECTOR:
-            if (gRotationVectorSensor != null) {
-                sm.unregisterListener(gi.getSensorEventListener(), gRotationVectorSensor);
-              break;
-            }
-            // Fallthrough
-
-        case GeckoHalDefines.SENSOR_ORIENTATION:
-            if (gOrientationSensor != null) {
-                sm.unregisterListener(gi.getSensorEventListener(), gOrientationSensor);
-            }
-            break;
-
-        case GeckoHalDefines.SENSOR_ACCELERATION:
-            if (gAccelerometerSensor != null) {
-                sm.unregisterListener(gi.getSensorEventListener(), gAccelerometerSensor);
-            }
-            break;
-
-        case GeckoHalDefines.SENSOR_PROXIMITY:
-            if (gProximitySensor != null) {
-                sm.unregisterListener(gi.getSensorEventListener(), gProximitySensor);
-            }
-            break;
-
-        case GeckoHalDefines.SENSOR_LIGHT:
-            if (gLightSensor != null) {
-                sm.unregisterListener(gi.getSensorEventListener(), gLightSensor);
-            }
-            break;
-
-        case GeckoHalDefines.SENSOR_LINEAR_ACCELERATION:
-            if (gLinearAccelerometerSensor != null) {
-                sm.unregisterListener(gi.getSensorEventListener(), gLinearAccelerometerSensor);
-            }
-            break;
-
-        case GeckoHalDefines.SENSOR_GYROSCOPE:
-            if (gGyroscopeSensor != null) {
-                sm.unregisterListener(gi.getSensorEventListener(), gGyroscopeSensor);
-            }
-            break;
-        default:
-            Log.w(LOGTAG, "Error! Can't disable unknown SENSOR type " + aSensortype);
-        }
-    }
-
-    @WrapForJNI
-    public static void startMonitoringGamepad() {
-        ThreadUtils.postToUiThread(new Runnable() {
-                @Override
-                public void run() {
-                    AndroidGamepadManager.startup();
-                }
-            });
-    }
-
-    @WrapForJNI
-    public static void stopMonitoringGamepad() {
-        ThreadUtils.postToUiThread(new Runnable() {
-                @Override
-                public void run() {
-                    AndroidGamepadManager.shutdown();
-                }
-            });
-    }
-
-    @WrapForJNI
-    public static void gamepadAdded(final int device_id, final int service_id) {
-        ThreadUtils.postToUiThread(new Runnable() {
-                @Override
-                public void run() {
-                    AndroidGamepadManager.gamepadAdded(device_id, service_id);
-                }
-            });
-    }
-
-    @WrapForJNI
-    public static void moveTaskToBack() {
-        if (getGeckoInterface() != null)
-            getGeckoInterface().getActivity().moveTaskToBack(true);
-    }
-
-    @WrapForJNI
-    static void scheduleRestart() {
-        getGeckoInterface().doRestart();
-    }
-
-    // Creates a homescreen shortcut for a web page.
-    // This is the entry point from nsIShellService.
-    @WrapForJNI
-    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() {
-        if (Versions.feature11Plus) {
-            ActivityManager am = (ActivityManager)
-                    getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
-            return am.getLauncherLargeIconSize();
-        } else {
-            switch (getDpi()) {
-                case DisplayMetrics.DENSITY_MEDIUM:
-                    return 48;
-                case DisplayMetrics.DENSITY_XHIGH:
-                    return 96;
-                case DisplayMetrics.DENSITY_HIGH:
-                default:
-                    return 72;
-            }
-        }
-    }
-
-    @WrapForJNI(stubName = "GetHandlersForMimeTypeWrapper")
-    static String[] getHandlersForMimeType(String aMimeType, String aAction) {
-        final GeckoInterface geckoInterface = getGeckoInterface();
-        if (geckoInterface == null) {
-            return new String[] {};
-        }
-        return geckoInterface.getHandlersForMimeType(aMimeType, aAction);
-    }
-
-    @WrapForJNI(stubName = "GetHandlersForURLWrapper")
-    static String[] getHandlersForURL(String aURL, String aAction) {
-        final GeckoInterface geckoInterface = getGeckoInterface();
-        if (geckoInterface == null) {
-            return new String[] {};
-        }
-        return geckoInterface.getHandlersForURL(aURL, aAction);
-    }
-
-    @WrapForJNI(stubName = "GetHWEncoderCapability")
-    static boolean getHWEncoderCapability() {
-      return HardwareCodecCapabilityUtils.getHWEncoderCapability();
-    }
-
-    @WrapForJNI(stubName = "GetHWDecoderCapability")
-    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(stubName = "GetExtensionFromMimeTypeWrapper")
-    static String getExtensionFromMimeType(String aMimeType) {
-        return MimeTypeMap.getSingleton().getExtensionFromMimeType(aMimeType);
-    }
-
-    @WrapForJNI(stubName = "GetMimeTypeFromExtensionsWrapper")
-    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
-    public 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);
-    }
-
-    /**
-     * Only called from GeckoApp.
-     */
-    public static void setNotificationClient(NotificationClient client) {
-        if (notificationClient == null) {
-            notificationClient = client;
-        } else {
-            Log.d(LOGTAG, "Notification client already set");
-        }
-    }
-
-    @WrapForJNI(stubName = "ShowPersistentAlertNotificationWrapper")
-    public static void showPersistentAlertNotification(
-          String aPersistentData,
-          String aImageUrl, String aAlertTitle, String aAlertText,
-          String aAlertCookie, String aAlertName, String aHost) {
-        Intent notificationIntent = GeckoService.getIntentToCreateServices(
-                getApplicationContext(), "persistent-notification-click", aPersistentData);
-        int notificationID = aAlertName.hashCode();
-        PendingIntent contentIntent = PendingIntent.getService(
-                getApplicationContext(), 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-        notificationClient.add(notificationID, aImageUrl, aHost, aAlertTitle, aAlertText, contentIntent);
-    }
-
-    @WrapForJNI(stubName = "ShowAlertNotificationWrapper")
-    public static void showAlertNotification(String aImageUrl, String aAlertTitle, String aAlertText, String aAlertCookie, String aAlertName, String aHost) {
-        // The intent to launch when the user clicks the expanded notification
-        Intent notificationIntent = new Intent(ACTION_ALERT_CALLBACK);
-        notificationIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
-        notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
-        int notificationID = aAlertName.hashCode();
-
-        // Put the strings into the intent as an URI "alert:?name=<alertName>&app=<appName>&cookie=<cookie>"
-        Uri.Builder b = new Uri.Builder();
-        Uri dataUri = b.scheme("alert").path(Integer.toString(notificationID))
-                                       .appendQueryParameter("name", aAlertName)
-                                       .appendQueryParameter("cookie", aAlertCookie)
-                                       .build();
-        notificationIntent.setData(dataUri);
-        PendingIntent contentIntent = PendingIntent.getActivity(
-                getApplicationContext(), 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-
-        ALERT_COOKIES.put(aAlertName, aAlertCookie);
-        callObserver(aAlertName, "alertshow", aAlertCookie);
-
-        notificationClient.add(notificationID, aImageUrl, aHost, aAlertTitle, aAlertText, contentIntent);
-    }
-
-    @WrapForJNI
-    public static void alertsProgressListener_OnProgress(String aAlertName, long aProgress, long aProgressMax, String aAlertText) {
-        int notificationID = aAlertName.hashCode();
-        notificationClient.update(notificationID, aProgress, aProgressMax, aAlertText);
-    }
-
-    @WrapForJNI
-    public static void closeNotification(String aAlertName) {
-        String alertCookie = ALERT_COOKIES.get(aAlertName);
-        if (alertCookie != null) {
-            callObserver(aAlertName, "alertfinished", alertCookie);
-            ALERT_COOKIES.remove(aAlertName);
-        }
-
-        removeObserver(aAlertName);
-
-        int notificationID = aAlertName.hashCode();
-        notificationClient.remove(notificationID);
-    }
-
-    public static void handleNotification(String aAction, String aAlertName, String aAlertCookie) {
-        int notificationID = aAlertName.hashCode();
-
-        if (ACTION_ALERT_CALLBACK.equals(aAction)) {
-            callObserver(aAlertName, "alertclickcallback", aAlertCookie);
-
-            if (notificationClient.isOngoing(notificationID)) {
-                // When clicked, keep the notification if it displays progress
-                return;
-            }
-        }
-        closeNotification(aAlertName);
-    }
-
-    @WrapForJNI(stubName = "GetDpiWrapper")
-    public static int getDpi() {
-        if (sDensityDpi == 0) {
-            sDensityDpi = getApplicationContext().getResources().getDisplayMetrics().densityDpi;
-        }
-
-        return sDensityDpi;
-    }
-
-    @WrapForJNI
-    public 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(stubName = "GetScreenDepthWrapper")
-    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
-    public 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
-    public static void setFullScreen(boolean fullscreen) {
-        if (getGeckoInterface() != null)
-            getGeckoInterface().setFullScreen(fullscreen);
-    }
-
-    @WrapForJNI
-    public 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(stubName = "Vibrate1")
-    public static void vibrate(long milliseconds) {
-        sVibrationEndTime = System.nanoTime() + milliseconds * 1000000;
-        sVibrationMaybePlaying = true;
-        vibrator().vibrate(milliseconds);
-    }
-
-    @WrapForJNI(stubName = "VibrateA")
-    public 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
-    public static void cancelVibrate() {
-        sVibrationMaybePlaying = false;
-        sVibrationEndTime = 0;
-        vibrator().cancel();
-    }
-
-    @WrapForJNI
-    public static void setKeepScreenOn(final boolean on) {
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                // TODO
-            }
-        });
-    }
-
-    @WrapForJNI
-    public static void notifyDefaultPrevented(final boolean defaultPrevented) {
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                LayerView view = getLayerView();
-                PanZoomController controller = (view == null ? null : view.getPanZoomController());
-                if (controller != null) {
-                    controller.notifyDefaultActionPrevented(defaultPrevented);
-                }
-            }
-        });
-    }
-
-    @WrapForJNI
-    public 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
-    public 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
-    public static int networkLinkType() {
-        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(stubName = "GetSystemColoursWrapper")
-    public 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
-    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(stubName = "GetIconForExtensionWrapper")
-    public 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
-    public 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(stubName = "AddPluginViewWrapper")
-    public static void addPluginView(View view,
-                                     float x, float y,
-                                     float w, float h,
-                                     boolean isFullScreen) {
-        if (getGeckoInterface() != null)
-             getGeckoInterface().addPluginView(view, new RectF(x, y, x + w, y + h), isFullScreen);
-    }
-
-    @WrapForJNI
-    public static void removePluginView(View view, boolean isFullScreen) {
-        if (getGeckoInterface() != null)
-            getGeckoInterface().removePluginView(view, isFullScreen);
-    }
-
-    /**
-     * 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_SERVICES | 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(allowMultithread = true)
-    public 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(allowMultithread = true)
-    public static Context getContext() {
-        return sContextGetter.getContext();
-    }
-
-    public static void setContextGetter(ContextGetter cg) {
-        sContextGetter = cg;
-    }
-
-    @WrapForJNI(allowMultithread = true)
-    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 GeckoProfile getProfile();
-        public Activity getActivity();
-        public String getDefaultUAString();
-        public LocationListener getLocationListener();
-        public SensorEventListener getSensorEventListener();
-        public void doRestart();
-        public void setFullScreen(boolean fullscreen);
-        public void addPluginView(View view, final RectF rect, final boolean isFullScreen);
-        public void removePluginView(final View view, final boolean isFullScreen);
-        public void enableCameraView();
-        public void disableCameraView();
-        public void addAppStateListener(AppStateListener listener);
-        public void removeAppStateListener(AppStateListener listener);
-        public View getCameraView();
-        public void notifyWakeLockChanged(String topic, String state);
-        public void onInputMethodChanged(String inputMethod);
-        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;
-    }
-
-    public static android.hardware.Camera sCamera;
-
-    static native void cameraCallbackBridge(byte[] data);
-
-    static final int kPreferredFPS = 25;
-    static byte[] sCameraBuffer;
-
-
-    @WrapForJNI(stubName = "InitCameraWrapper")
-    static int[] initCamera(String aContentType, int aCamera, int aWidth, int aHeight) {
-        ThreadUtils.postToUiThread(new Runnable() {
-                @Override
-                public void run() {
-                    try {
-                        if (getGeckoInterface() != null)
-                            getGeckoInterface().enableCameraView();
-                    } catch (Exception e) { }
-                }
-            });
-
-        // [0] = 0|1 (failure/success)
-        // [1] = width
-        // [2] = height
-        // [3] = fps
-        int[] result = new int[4];
-        result[0] = 0;
-
-        if (android.hardware.Camera.getNumberOfCameras() == 0) {
-            return result;
-        }
-
-        try {
-            sCamera = android.hardware.Camera.open(aCamera);
-
-            android.hardware.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<android.hardware.Camera.Size> sit = params.getSupportedPreviewSizes().iterator();
-            int sizeDelta = 10000000;
-            int bufferSize = 0;
-            while (sit.hasNext()) {
-                android.hardware.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;
-                }
-            }
-
-            try {
-                if (getGeckoInterface() != null) {
-                    View cameraView = getGeckoInterface().getCameraView();
-                    if (cameraView instanceof SurfaceView) {
-                        sCamera.setPreviewDisplay(((SurfaceView)cameraView).getHolder());
-                    } else if (cameraView instanceof TextureView) {
-                        sCamera.setPreviewTexture(((TextureView)cameraView).getSurfaceTexture());
-                    }
-                }
-            } catch (IOException | RuntimeException e) {
-                Log.w(LOGTAG, "Error setPreviewXXX:", e);
-            }
-
-            sCamera.setParameters(params);
-            sCameraBuffer = new byte[(bufferSize * 12) / 8];
-            sCamera.addCallbackBuffer(sCameraBuffer);
-            sCamera.setPreviewCallbackWithBuffer(new android.hardware.Camera.PreviewCallback() {
-                @Override
-                public void onPreviewFrame(byte[] data, android.hardware.Camera camera) {
-                    cameraCallbackBridge(data);
-                    if (sCamera != null)
-                        sCamera.addCallbackBuffer(sCameraBuffer);
-                }
-            });
-            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
-    static synchronized void closeCamera() {
-        ThreadUtils.postToUiThread(new Runnable() {
-                @Override
-                public void run() {
-                    try {
-                        if (getGeckoInterface() != null)
-                            getGeckoInterface().disableCameraView();
-                    } catch (Exception e) { }
-                }
-            });
-        if (sCamera != null) {
-            sCamera.stopPreview();
-            sCamera.release();
-            sCamera = null;
-            sCameraBuffer = null;
-        }
-    }
-
-    /*
-     * Battery API related methods.
-     */
-    @WrapForJNI
-    public static void enableBatteryNotifications() {
-        GeckoBatteryManager.enableNotifications();
-    }
-
-    @WrapForJNI(stubName = "HandleGeckoMessageWrapper")
-    public static void handleGeckoMessage(final NativeJSContainer message) {
-        EventDispatcher.getInstance().dispatchEvent(message);
-        message.disposeNative();
-    }
-
-    @WrapForJNI
-    public static void disableBatteryNotifications() {
-        GeckoBatteryManager.disableNotifications();
-    }
-
-    @WrapForJNI(stubName = "GetCurrentBatteryInformationWrapper")
-    public static double[] getCurrentBatteryInformation() {
-        return GeckoBatteryManager.getCurrentInformation();
-    }
-
-    @WrapForJNI(stubName = "CheckURIVisited")
-    static void checkUriVisited(String uri) {
-        final GeckoInterface geckoInterface = getGeckoInterface();
-        if (geckoInterface == null) {
-            return;
-        }
-        geckoInterface.checkUriVisited(uri);
-    }
-
-    @WrapForJNI(stubName = "MarkURIVisited")
-    static void markUriVisited(final String uri) {
-        final GeckoInterface geckoInterface = getGeckoInterface();
-        if (geckoInterface == null) {
-            return;
-        }
-        geckoInterface.markUriVisited(uri);
-    }
-
-    @WrapForJNI(stubName = "SetURITitle")
-    static void setUriTitle(final String uri, final String title) {
-        final GeckoInterface geckoInterface = getGeckoInterface();
-        if (geckoInterface == null) {
-            return;
-        }
-        geckoInterface.setUriTitle(uri, title);
-    }
-
-    @WrapForJNI
-    static void hideProgressDialog() {
-        // unused stub
-    }
-
-    /*
-     * WebSMS related methods.
-     */
-    public static void sendMessage(String aNumber, String aMessage, int aRequestId, boolean aShouldNotify) {
-        if (!SmsManager.isEnabled()) {
-            return;
-        }
-
-        SmsManager.getInstance().send(aNumber, aMessage, aRequestId, aShouldNotify);
-    }
-
-    @WrapForJNI(stubName = "SendMessageWrapper")
-    public static void sendMessage(String aNumber, String aMessage, int aRequestId) {
-        sendMessage(aNumber, aMessage, aRequestId, /* shouldNotify */ true);
-    }
-
-    @WrapForJNI(stubName = "GetMessageWrapper")
-    public static void getMessage(int aMessageId, int aRequestId) {
-        if (!SmsManager.isEnabled()) {
-            return;
-        }
-
-        SmsManager.getInstance().getMessage(aMessageId, aRequestId);
-    }
-
-    @WrapForJNI(stubName = "DeleteMessageWrapper")
-    public static void deleteMessage(int aMessageId, int aRequestId) {
-        if (!SmsManager.isEnabled()) {
-            return;
-        }
-
-        SmsManager.getInstance().deleteMessage(aMessageId, aRequestId);
-    }
-
-    @WrapForJNI
-    public static void markMessageRead(int aMessageId, boolean aValue, boolean aSendReadReport, int aRequestId) {
-        if (!SmsManager.isEnabled()) {
-            return;
-        }
-
-        SmsManager.getInstance().markMessageRead(aMessageId, aValue, aSendReadReport, aRequestId);
-    }
-
-    @WrapForJNI(stubName = "CreateMessageCursorWrapper")
-    public static void createMessageCursor(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, String aDelivery, boolean aHasRead, boolean aRead, boolean aHasThreadId, long aThreadId, boolean aReverse, int aRequestId) {
-        if (!SmsManager.isEnabled()) {
-            return;
-        }
-
-        SmsManager.getInstance().createMessageCursor(aStartDate, aEndDate, aNumbers, aNumbersCount, aDelivery, aHasRead, aRead, aHasThreadId, aThreadId, aReverse, aRequestId);
-    }
-
-    @WrapForJNI(stubName = "GetNextMessageWrapper")
-    public static void getNextMessage(int aRequestId) {
-        if (!SmsManager.isEnabled()) {
-            return;
-        }
-
-        SmsManager.getInstance().getNextMessage(aRequestId);
-    }
-
-    @WrapForJNI(stubName = "CreateThreadCursorWrapper")
-    public static void createThreadCursor(int aRequestId) {
-        Log.i("GeckoAppShell", "CreateThreadCursorWrapper!");
-
-        if (!SmsManager.isEnabled()) {
-            return;
-        }
-
-        SmsManager.getInstance().createThreadCursor(aRequestId);
-    }
-
-    @WrapForJNI(stubName = "GetNextThreadWrapper")
-    public static void getNextThread(int aRequestId) {
-        if (!SmsManager.isEnabled()) {
-            return;
-        }
-
-        SmsManager.getInstance().getNextThread(aRequestId);
-    }
-
-    /* Called by JNI from AndroidBridge, and by reflection from tests/BaseTest.java.in */
-    @WrapForJNI
-    @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(stubName = "GetCurrentNetworkInformationWrapper")
-    public static double[] getCurrentNetworkInformation() {
-        return GeckoNetworkManager.getInstance().getCurrentInformation();
-    }
-
-    @WrapForJNI
-    public static void enableNetworkNotifications() {
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                GeckoNetworkManager.getInstance().enableNotifications();
-            }
-        });
-    }
-
-    @WrapForJNI
-    public static void disableNetworkNotifications() {
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                GeckoNetworkManager.getInstance().disableNotifications();
-            }
-        });
-    }
-
-    @WrapForJNI(stubName = "GetScreenOrientationWrapper")
-    public static short getScreenOrientation() {
-        return GeckoScreenOrientation.getInstance().getScreenOrientation().value;
-    }
-
-    @WrapForJNI
-    public static int getScreenAngle() {
-        return GeckoScreenOrientation.getInstance().getAngle();
-    }
-
-    @WrapForJNI
-    public static void enableScreenOrientationNotifications() {
-        GeckoScreenOrientation.getInstance().enableNotifications();
-    }
-
-    @WrapForJNI
-    public static void disableScreenOrientationNotifications() {
-        GeckoScreenOrientation.getInstance().disableNotifications();
-    }
-
-    @WrapForJNI
-    public static void lockScreenOrientation(int aOrientation) {
-        GeckoScreenOrientation.getInstance().lock(aOrientation);
-    }
-
-    @WrapForJNI
-    public static void unlockScreenOrientation() {
-        GeckoScreenOrientation.getInstance().unlock();
-    }
-
-    @WrapForJNI
-    public static void notifyWakeLockChanged(String topic, String state) {
-        if (getGeckoInterface() != null)
-            getGeckoInterface().notifyWakeLockChanged(topic, state);
-    }
-
-    @WrapForJNI(allowMultithread = true)
-    public static void registerSurfaceTextureFrameListener(Object surfaceTexture, final int id) {
-        ((SurfaceTexture)surfaceTexture).setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
-            @Override
-            public void onFrameAvailable(SurfaceTexture surfaceTexture) {
-                GeckoAppShell.onSurfaceTextureFrameAvailable(surfaceTexture, id);
-            }
-        });
-    }
-
-    @WrapForJNI(allowMultithread = true)
-    public static void unregisterSurfaceTextureFrameListener(Object surfaceTexture) {
-        ((SurfaceTexture)surfaceTexture).setOnFrameAvailableListener(null);
-    }
-
-    @WrapForJNI
-    public 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(stubName = "GetProxyForURIWrapper")
-    public 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(allowMultithread = true)
-    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(allowMultithread = true, narrowChars = true)
-    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(allowMultithread = true, narrowChars = true)
-    static String connectionGetMimeType(URLConnection connection) {
-        return connection.getContentType();
-    }
-
-    /**
-     * Retrieve the absolute path of an external storage directory.
-     *
-     * @param type The type of directory to return
-     * @return Absolute path of the specified directory or null on failure
-     */
-    @WrapForJNI
-    static String getExternalPublicDirectory(final String type) {
-        final String state = Environment.getExternalStorageState();
-        if (!Environment.MEDIA_MOUNTED.equals(state) &&
-            !Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
-            // External storage is not available.
-            return null;
-        }
-
-        if ("sdcard".equals(type)) {
-            // SD card has a separate path.
-            return Environment.getExternalStorageDirectory().getAbsolutePath();
-        }
-
-        final String systemType;
-        if ("downloads".equals(type)) {
-            systemType = Environment.DIRECTORY_DOWNLOADS;
-        } else if ("pictures".equals(type)) {
-            systemType = Environment.DIRECTORY_PICTURES;
-        } else if ("videos".equals(type)) {
-            systemType = Environment.DIRECTORY_MOVIES;
-        } else if ("music".equals(type)) {
-            systemType = Environment.DIRECTORY_MUSIC;
-        } else if ("apps".equals(type)) {
-            File appInternalStorageDirectory = getApplicationContext().getFilesDir();
-            return new File(appInternalStorageDirectory, "mozilla").getAbsolutePath();
-        } else {
-            return null;
-        }
-        return Environment.getExternalStoragePublicDirectory(systemType).getAbsolutePath();
-    }
-
-    @WrapForJNI
-    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
-    public 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;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoBatteryManager.java
+++ /dev/null
@@ -1,196 +0,0 @@
-/* -*- 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;
-
-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;
-    }
-
-    @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())) {
-            GeckoAppShell.notifyBatteryChange(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() };
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoEditable.java
+++ /dev/null
@@ -1,1439 +0,0 @@
-/* -*- 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 java.util.concurrent.Semaphore;
-
-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.GeckoEventListener;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
-
-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, GeckoEditableListener, GeckoEventListener {
-
-    private static final boolean DEBUG = false;
-    private static final String LOGTAG = "GeckoEditable";
-
-    // Filters to implement Editable's filtering functionality
-    private InputFilter[] mFilters;
-
-    private final SpannableStringBuilder mText;
-    private final Editable mProxy;
-    private final ActionQueue mActionQueue;
-
-    // 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 mNeedCompositionUpdate; // Used by IC thread
-    private boolean mFocused; // Used by IC thread
-    private boolean mGeckoFocused; // Used by Gecko thread
-    private boolean mIgnoreSelectionChange; // Used by Gecko thread
-    private volatile boolean mSuppressCompositions;
-    private volatile boolean mSuppressKeyUp;
-
-    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
-    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
-    private native void onImeSynchronize();
-
-    @WrapForJNI
-    private native void onImeAcknowledgeFocus();
-
-    @WrapForJNI
-    private native void onImeReplaceText(int start, int end, String text);
-
-    @WrapForJNI
-    private native void onImeAddCompositionRange(int start, int end, int rangeType,
-                                                 int rangeStyles, int rangeLineStyle,
-                                                 boolean rangeBoldLine, int rangeForeColor,
-                                                 int rangeBackColor, int rangeLineColor);
-
-    @WrapForJNI
-    private native void onImeUpdateComposition(int start, int end);
-
-    /* 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 focus events (in notifyIME); use with onImeAcknowledgeFocus
-        static final int TYPE_ACKNOWLEDGE_FOCUS = 4;
-        // For switching handler; use with onImeSynchronize
-        static final int TYPE_SET_HANDLER = 5;
-        // For updating composition; use with onImeUpdateComposition
-        static final int TYPE_UPDATE_COMPOSITION = 6;
-
-        final int mType;
-        boolean mUpdateComposition;
-        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, boolean update) {
-            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;
-            action.mUpdateComposition = update;
-            return action;
-        }
-
-        static Action newRemoveSpan(Object object, boolean update) {
-            final Action action = new Action(TYPE_REMOVE_SPAN);
-            action.mSpanObject = object;
-            action.mUpdateComposition = update;
-            return action;
-        }
-
-        static Action newSetHandler(Handler handler) {
-            final Action action = new Action(TYPE_SET_HANDLER);
-            action.mHandler = handler;
-            return action;
-        }
-
-        static Action newUpdateComposition(int start, int end) {
-            final Action action = new Action(TYPE_UPDATE_COMPOSITION);
-            action.mStart = start;
-            action.mEnd = end;
-            return action;
-        }
-    }
-
-    /* Queue of editing actions sent to Gecko thread that
-       the Gecko thread has not responded to yet */
-    private final class ActionQueue {
-        private final ConcurrentLinkedQueue<Action> mActions;
-        private final Semaphore mActionsActive;
-        private KeyCharacterMap mKeyMap;
-
-        ActionQueue() {
-            mActions = new ConcurrentLinkedQueue<Action>();
-            mActionsActive = new Semaphore(1);
-        }
-
-        void offer(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;
-            }
-
-            if (mActions.isEmpty()) {
-                mActionsActive.acquireUninterruptibly();
-                mActions.offer(action);
-            } else synchronized (this) {
-                // tryAcquire here in case Gecko thread has just released it
-                mActionsActive.tryAcquire();
-                mActions.offer(action);
-            }
-
-            switch (action.mType) {
-            case Action.TYPE_EVENT:
-            case Action.TYPE_SET_SPAN:
-            case Action.TYPE_REMOVE_SPAN:
-            case Action.TYPE_SET_HANDLER:
-                onImeSynchronize();
-                break;
-
-            case Action.TYPE_REPLACE_TEXT:
-                // 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)) {
-                    mNeedCompositionUpdate = false;
-                } else {
-                    // Since we don't have a composition, we can try sending key events.
-                    sendCharKeyEvents(action);
-                }
-                onImeReplaceText(action.mStart, action.mEnd, action.mSequence.toString());
-                break;
-
-            case Action.TYPE_ACKNOWLEDGE_FOCUS:
-                onImeAcknowledgeFocus();
-                break;
-
-            case Action.TYPE_UPDATE_COMPOSITION:
-                onImeUpdateComposition(action.mStart, action.mEnd);
-                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);
-            }
-        }
-
-        /**
-         * Remove the head of the queue. Throw if queue is empty.
-         */
-        void poll() {
-            if (DEBUG) {
-                ThreadUtils.assertOnGeckoThread();
-            }
-            if (mActions.poll() == null) {
-                throw new IllegalStateException("empty actions queue");
-            }
-
-            synchronized (this) {
-                if (mActions.isEmpty()) {
-                    mActionsActive.release();
-                }
-            }
-        }
-
-        /**
-         * Return, but don't remove, the head of the queue, or null if queue is empty.
-         *
-         * @return head of the queue or null if empty.
-         */
-        Action peek() {
-            if (DEBUG) {
-                ThreadUtils.assertOnGeckoThread();
-            }
-            return mActions.peek();
-        }
-
-        void syncWithGecko() {
-            if (DEBUG) {
-                assertOnIcThread();
-            }
-            if (mFocused && !mActions.isEmpty()) {
-                if (DEBUG) {
-                    Log.d(LOGTAG, "syncWithGecko blocking on thread " +
-                                  Thread.currentThread().getName());
-                }
-                mActionsActive.acquireUninterruptibly();
-                mActionsActive.release();
-            } else if (DEBUG && !mFocused) {
-                Log.d(LOGTAG, "skipped syncWithGecko (no focus)");
-            }
-        }
-
-        boolean isEmpty() {
-            return mActions.isEmpty();
-        }
-    }
-
-    @WrapForJNI
-    GeckoEditable(final GeckoView v) {
-        if (DEBUG) {
-            // Called by nsWindow.
-            ThreadUtils.assertOnGeckoThread();
-        }
-        mActionQueue = new ActionQueue();
-
-        mText = new SpannableStringBuilder();
-
-        final Class<?>[] PROXY_INTERFACES = { Editable.class };
-        mProxy = (Editable)Proxy.newProxyInstance(
-                Editable.class.getClassLoader(),
-                PROXY_INTERFACES, this);
-
-        mIcRunHandler = mIcPostHandler = ThreadUtils.getUiHandler();
-
-        onViewChange(v);
-    }
-
-    @WrapForJNI @Override
-    protected native void disposeNative();
-
-    @WrapForJNI
-    /* package */ 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)");
-                }
-
-                if (newListener != null) {
-                    // Make sure there are no other things going on.
-                    mActionQueue.syncWithGecko();
-                    mListener = newListener;
-                } else {
-                    // 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.
-                    mListener = null;
-                    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.
-        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;
-                mIcPostHandler.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) {
-        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 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) {
-        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;
-
-            for (Object span : spans) {
-                if ((text.getSpanFlags(span) & Spanned.SPAN_COMPOSING) == 0) {
-                    continue;
-                }
-                if (!useEntireText) {
-                    composingStart = Math.min(composingStart, text.getSpanStart(span));
-                    composingEnd = Math.max(composingEnd, text.getSpanEnd(span));
-                }
-                found = true;
-            }
-
-            if (useEntireText && (selStart < 0 || selEnd < 0)) {
-                selStart = composingEnd;
-                selEnd = composingEnd;
-            }
-
-            if (found) {
-                icSendComposition(text, selStart, selEnd, composingStart, composingEnd);
-                if (notifyGecko) {
-                    mActionQueue.offer(Action.newUpdateComposition(
-                            composingStart, composingEnd));
-                }
-                return true;
-            }
-        }
-
-        if (notifyGecko) {
-            // Set the selection by using a composition without ranges
-            mActionQueue.offer(Action.newUpdateComposition(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.
-                if (Versions.feature14Plus) {
-                    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 mActionQueue.offer 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 selection/composition/etc. accordingly.
-        */
-        if (mNeedCompositionUpdate) {
-            // Make sure Gecko selection is in-sync with Java selection first.
-            icUpdateComposition();
-        }
-        onKeyEvent(event, action, metaState, /* isSynthesizedImeKey */ false);
-        mActionQueue.offer(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;
-        }
-        if (!inBatchMode && mNeedCompositionUpdate) {
-            icUpdateComposition();
-        }
-        mInBatchMode = inBatchMode;
-    }
-
-    private void icUpdateComposition() {
-        if (DEBUG) {
-            assertOnIcThread();
-        }
-        mNeedCompositionUpdate = false;
-        mActionQueue.syncWithGecko();
-        icMaybeSendComposition(mText, /* useEntireText */ false, /* notifyGecko */ true);
-    }
-
-    private void geckoScheduleCompositionUpdate() {
-        if (DEBUG) {
-            ThreadUtils.assertOnGeckoThread();
-        }
-        // May be called from either Gecko or IC thread
-        geckoPostToIc(new Runnable() {
-            @Override
-            public void run() {
-                if (mInBatchMode || !mNeedCompositionUpdate) {
-                    return;
-                }
-                if (!mActionQueue.isEmpty()) {
-                    // We are likely to block here, so wait a little and try again.
-                    mIcPostHandler.postDelayed(this, 10);
-                    return;
-                }
-                icUpdateComposition();
-            }
-        });
-    }
-
-    @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
-    public Handler setInputConnectionHandler(Handler handler) {
-        if (handler == mIcPostHandler || !mFocused) {
-            return mIcPostHandler;
-        }
-        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.
-        mActionQueue.offer(Action.newSetHandler(handler));
-        mActionQueue.syncWithGecko();
-        return handler;
-    }
-
-    @Override // GeckoEditableClient
-    public void postToInputConnection(final Runnable runnable) {
-        mIcPostHandler.post(runnable);
-    }
-
-    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;
-
-        geckoPostToIc(new Runnable() { // posting to new IC thread
-            @Override
-            public void run() {
-                synchronized (newHandler) {
-                    while (mIcRunHandler != newHandler) {
-                        try {
-                            newHandler.wait();
-                        } catch (InterruptedException e) {
-                        }
-                    }
-                }
-            }
-        });
-    }
-
-    // GeckoEditableListener interface
-
-    private void geckoActionReply() {
-        if (DEBUG) {
-            // GeckoEditableListener methods should all be called from the Gecko thread
-            ThreadUtils.assertOnGeckoThread();
-        }
-
-        final Action action = mActionQueue.peek();
-        if (action == null) {
-            throw new IllegalStateException("empty actions queue");
-        }
-
-        if (DEBUG) {
-            Log.d(LOGTAG, "reply: Action(" +
-                          getConstantName(Action.class, "TYPE_", action.mType) + ")");
-        }
-        switch (action.mType) {
-        case Action.TYPE_SET_SPAN:
-            mText.setSpan(action.mSpanObject, action.mStart, action.mEnd, action.mSpanFlags);
-            break;
-
-        case Action.TYPE_REMOVE_SPAN:
-            mText.removeSpan(action.mSpanObject);
-            break;
-
-        case Action.TYPE_SET_HANDLER:
-            geckoSetIcHandler(action.mHandler);
-            break;
-        }
-
-        if (action.mUpdateComposition) {
-            geckoScheduleCompositionUpdate();
-        }
-    }
-
-    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 Object[] spans = mText.getSpans(0, mText.length(), Object.class);
-            for (Object span : spans) {
-                if ((mText.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) {
-                    throw new IllegalStateException("composition not cancelled");
-                }
-            }
-        }
-    }
-
-    @WrapForJNI @Override
-    public 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 != NOTIFY_IME_REPLY_EVENT) {
-                Log.d(LOGTAG, "notifyIME(" +
-                              getConstantName(GeckoEditableListener.class, "NOTIFY_IME_", type) +
-                              ")");
-            }
-        }
-
-        if (type == NOTIFY_IME_REPLY_EVENT) {
-            try {
-                if (mGeckoFocused) {
-                    // When mGeckoFocused is false, the reply is for a stale action,
-                    // and we should not do anything
-                    geckoActionReply();
-                } else if (DEBUG) {
-                    Log.d(LOGTAG, "discarding stale reply");
-                }
-            } finally {
-                // Ensure action is always removed from queue
-                // even if stale action results in exception in geckoActionReply
-                mActionQueue.poll();
-            }
-            return;
-        } else if (type == NOTIFY_IME_TO_COMMIT_COMPOSITION) {
-            notifyCommitComposition();
-            return;
-        } else if (type == NOTIFY_IME_TO_CANCEL_COMPOSITION) {
-            notifyCancelComposition();
-            return;
-        }
-
-        geckoPostToIc(new Runnable() {
-            @Override
-            public void run() {
-                if (type == NOTIFY_IME_OF_FOCUS) {
-                    mFocused = true;
-                    // Unmask events on the Gecko side
-                    mActionQueue.offer(new Action(Action.TYPE_ACKNOWLEDGE_FOCUS));
-                }
-
-                // Make sure there are no other things going on. If we sent
-                // Action.TYPE_ACKNOWLEDGE_FOCUS, this line also makes us
-                // wait for Gecko to update us on the newly focused content
-                mActionQueue.syncWithGecko();
-                if (mListener != null) {
-                    mListener.notifyIME(type);
-                }
-
-                // Unset mFocused after we call syncWithGecko because
-                // syncWithGecko becomes a no-op when mFocused is false.
-                if (type == NOTIFY_IME_OF_BLUR) {
-                    mFocused = false;
-                }
-            }
-        });
-
-        // Register/unregister Gecko-side text selection listeners
-        // and update the mGeckoFocused flag.
-        if (type == NOTIFY_IME_OF_BLUR && mGeckoFocused) {
-            // Check for focus here because Gecko may send us a blur before a focus in some
-            // cases, and we don't want to unregister an event that was not registered.
-            mGeckoFocused = false;
-            mSuppressCompositions = false;
-            EventDispatcher.getInstance().
-                unregisterGeckoThreadListener(this, "TextSelection:DraggingHandle");
-        } else if (type == NOTIFY_IME_OF_FOCUS) {
-            mGeckoFocused = true;
-            mSuppressCompositions = false;
-            EventDispatcher.getInstance().
-                registerGeckoThreadListener(this, "TextSelection:DraggingHandle");
-        }
-    }
-
-    @WrapForJNI @Override
-    public 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 @Override
-    public void onSelectionChange(int start, int end) {
-        if (DEBUG) {
-            // GeckoEditableListener methods should all be called from the Gecko thread
-            ThreadUtils.assertOnGeckoThread();
-            Log.d(LOGTAG, "onSelectionChange(" + start + ", " + end + ")");
-        }
-        if (start < 0 || start > mText.length() || end < 0 || end > mText.length()) {
-            Log.e(LOGTAG, "invalid selection notification range: " +
-                  start + " to " + end + ", length: " + mText.length());
-            throw new IllegalArgumentException("invalid selection notification range");
-        }
-
-        if (mIgnoreSelectionChange) {
-            start = Selection.getSelectionStart(mText);
-            end = Selection.getSelectionEnd(mText);
-            mIgnoreSelectionChange = false;
-
-        } else {
-            Selection.setSelection(mText, start, end);
-        }
-
-        final int newStart = start;
-        final int newEnd = end;
-        geckoPostToIc(new Runnable() {
-            @Override
-            public void run() {
-                if (mListener == null) {
-                    return;
-                }
-                mListener.onSelectionChange(newStart, newEnd);
-            }
-        });
-    }
-
-    private void geckoReplaceText(int start, int oldEnd, CharSequence newText) {
-        mText.replace(start, oldEnd, newText);
-    }
-
-    private boolean geckoIsSameText(int start, int oldEnd, CharSequence newText) {
-        return oldEnd - start == newText.length() &&
-               TextUtils.regionMatches(mText, start, newText, 0, oldEnd - start);
-    }
-
-    @WrapForJNI @Override
-    public 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");
-        }
-        /* For the "end" parameters, Gecko can pass in a large
-           number to denote "end of the text". Fix that here */
-        final int oldEnd = unboundedOldEnd > mText.length() ? mText.length() : unboundedOldEnd;
-        // new end should always match text
-        if (unboundedOldEnd <= mText.length() &&
-                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 = mActionQueue.peek();
-
-        if (action != null && action.mType == Action.TYPE_ACKNOWLEDGE_FOCUS) {
-            // Simply replace the text for newly-focused editors.
-            mText.replace(0, mText.length(), text);
-
-        } 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.
-                geckoReplaceText(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()) {
-                // The new text exactly matches our sequence, so do a direct replace.
-                geckoReplaceText(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.
-                geckoReplaceText(start, action.mStart, text.subSequence(0, indexInText));
-
-                // Then Replace the sequence itself to preserve new spans.
-                final int actionStart = indexInText + start;
-                geckoReplaceText(actionStart, actionStart + action.mEnd - action.mStart,
-                                 action.mSequence);
-
-                // Finally replace part of text after the sequence.
-                final int actionEnd = actionStart + action.mSequence.length();
-                geckoReplaceText(actionEnd, actionEnd + oldEnd - action.mEnd,
-                                 text.subSequence(actionEnd - start, text.length()));
-            }
-
-        } 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_UPDATE_COMPOSITION);
-            return;
-
-        } else {
-            // Gecko side initiated the text change.
-            geckoReplaceText(start, oldEnd, text);
-        }
-
-        geckoPostToIc(new Runnable() {
-            @Override
-            public void run() {
-                if (mListener == null) {
-                    return;
-                }
-                mListener.onTextChange(text, start, oldEnd, newEnd);
-            }
-        });
-    }
-
-    @WrapForJNI @Override
-    public 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);
-            }
-        });
-    }
-
-    // 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 {
-            // Method queries the Editable; must sync with Gecko first
-            // then call on the inner Editable itself
-            mActionQueue.syncWithGecko();
-            target = mText;
-        }
-        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 == Selection.SELECTION_START ||
-                what == Selection.SELECTION_END) {
-            Log.w(LOGTAG, "selection removed with removeSpan()");
-        }
-
-        // "what" could be a composing span (BaseInputConnection.COMPOSING extends
-        // NoCopySpan), so we should update the Gecko composition
-        final boolean update = !mNeedCompositionUpdate && what instanceof NoCopySpan;
-        mActionQueue.offer(Action.newRemoveSpan(what, update));
-        mNeedCompositionUpdate |= update;
-    }
-
-    @Override
-    public void setSpan(Object what, int start, int end, int flags) {
-        // If mUpdateComposition is true, it means something in the queue will be
-        // scheduling an update, so it would be redundant to have this action schedule
-        // another update.
-        final boolean update = !mNeedCompositionUpdate &&
-                (flags & Spanned.SPAN_INTERMEDIATE) == 0 && (
-                (flags & Spanned.SPAN_COMPOSING) != 0 ||
-                what == Selection.SELECTION_START ||
-                what == Selection.SELECTION_END);
-        mActionQueue.offer(Action.newSetSpan(what, start, end, flags, update));
-        mNeedCompositionUpdate |= update;
-    }
-
-    // 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()");
-        mText.clearSpans();
-    }
-
-    @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);
-        }
-        mActionQueue.offer(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");
-    }
-
-    // GeckoEventListener implementation
-
-    @Override
-    public void handleMessage(String event, JSONObject message) {
-        if (!"TextSelection:DraggingHandle".equals(event)) {
-            return;
-        }
-
-        mSuppressCompositions = message.optBoolean("dragging", false);
-    }
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoEditableClient.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/* -*- 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);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoEditableListener.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/* -*- 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.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(int start, int end);
-    void onTextChange(CharSequence text, int start, int oldEnd, int newEnd);
-    void onDefaultKeyEvent(KeyEvent event);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoEvent.java
+++ /dev/null
@@ -1,634 +0,0 @@
-/* -*- 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.nio.ByteBuffer;
-import java.util.concurrent.ArrayBlockingQueue;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.gfx.DisplayPortMetrics;
-import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
-
-import android.graphics.Point;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorManager;
-import android.location.Address;
-import android.location.Location;
-import android.os.SystemClock;
-import android.util.Log;
-import android.util.SparseArray;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import org.mozilla.gecko.annotation.JNITarget;
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-/**
- * We're not allowed to hold on to most events given to us
- * so we save the parts of the events we want to use in GeckoEvent.
- * Fields have different meanings depending on the event type.
- */
-@JNITarget
-public class GeckoEvent {
-    private static final String LOGTAG = "GeckoEvent";
-
-    private static final int EVENT_FACTORY_SIZE = 5;
-
-    // Maybe we're probably better to just make mType non final, and just store GeckoEvents in here...
-    private static final SparseArray<ArrayBlockingQueue<GeckoEvent>> mEvents = new SparseArray<ArrayBlockingQueue<GeckoEvent>>();
-
-    public static GeckoEvent get(NativeGeckoEvent type) {
-        synchronized (mEvents) {
-            ArrayBlockingQueue<GeckoEvent> events = mEvents.get(type.value);
-            if (events != null && events.size() > 0) {
-                return events.poll();
-            }
-        }
-
-        return new GeckoEvent(type);
-    }
-
-    public void recycle() {
-        synchronized (mEvents) {
-            ArrayBlockingQueue<GeckoEvent> events = mEvents.get(mType);
-            if (events == null) {
-                events = new ArrayBlockingQueue<GeckoEvent>(EVENT_FACTORY_SIZE);
-                mEvents.put(mType, events);
-            }
-
-            events.offer(this);
-        }
-    }
-
-    // Make sure to keep these values in sync with the enum in
-    // AndroidGeckoEvent in widget/android/AndroidJavaWrappers.h
-    @JNITarget
-    public enum NativeGeckoEvent {
-        NATIVE_POKE(0),
-        MOTION_EVENT(2),
-        SENSOR_EVENT(3),
-        LOCATION_EVENT(5),
-        LOAD_URI(12),
-        NOOP(15),
-        VIEWPORT(20),
-        VISITED(21),
-        NETWORK_CHANGED(22),
-        THUMBNAIL(25),
-        SCREENORIENTATION_CHANGED(27),
-        NATIVE_GESTURE_EVENT(31),
-        CALL_OBSERVER(33),
-        REMOVE_OBSERVER(34),
-        LOW_MEMORY(35),
-        NETWORK_LINK_CHANGE(36),
-        TELEMETRY_HISTOGRAM_ADD(37),
-        TELEMETRY_UI_SESSION_START(42),
-        TELEMETRY_UI_SESSION_STOP(43),
-        TELEMETRY_UI_EVENT(44),
-        GAMEPAD_ADDREMOVE(45),
-        GAMEPAD_DATA(46),
-        LONG_PRESS(47),
-        ZOOMEDVIEW(48);
-
-        public final int value;
-
-        private NativeGeckoEvent(int value) {
-            this.value = value;
-        }
-    }
-
-    public static final int ACTION_MAGNIFY_START = 11;
-    public static final int ACTION_MAGNIFY = 12;
-    public static final int ACTION_MAGNIFY_END = 13;
-
-    public static final int ACTION_GAMEPAD_ADDED = 1;
-    public static final int ACTION_GAMEPAD_REMOVED = 2;
-
-    public static final int ACTION_GAMEPAD_BUTTON = 1;
-    public static final int ACTION_GAMEPAD_AXES = 2;
-
-    private final int mType;
-    private int mAction;
-    private long mTime;
-    private Point[] mPoints;
-    private int[] mPointIndicies;
-    private int mPointerIndex; // index of the point that has changed
-    private float[] mOrientations;
-    private float[] mPressures;
-    private int[] mToolTypes;
-    private Point[] mPointRadii;
-    private Rect mRect;
-    private double mX;
-    private double mY;
-    private double mZ;
-    private double mW;
-
-    private int mMetaState;
-    private int mFlags;
-    private int mCount;
-    private String mCharacters;
-    private String mCharactersExtra;
-    private String mData;
-    private Location mLocation;
-
-    private int     mConnectionType;
-    private boolean mIsWifi;
-    private int     mDHCPGateway;
-
-    private short mScreenOrientation;
-    private short mScreenAngle;
-
-    private ByteBuffer mBuffer;
-
-    private int mWidth;
-    private int mHeight;
-
-    private int mID;
-    private int mGamepadButton;
-    private boolean mGamepadButtonPressed;
-    private float mGamepadButtonValue;
-    private float[] mGamepadValues;
-
-    private GeckoEvent(NativeGeckoEvent event) {
-        mType = event.value;
-    }
-
-    public static GeckoEvent createNoOpEvent() {
-        return GeckoEvent.get(NativeGeckoEvent.NOOP);
-    }
-
-    /**
-     * This method is a replacement for the the KeyEvent.isGamepadButton method to be
-     * compatible with Build.VERSION.SDK_INT < 12. This is an implementation of the
-     * same method isGamepadButton available after SDK 12.
-     * @param keyCode int with the key code (Android key constant from KeyEvent).
-     * @return True if the keycode is a gamepad button, such as {@link #KEYCODE_BUTTON_A}.
-     */
-    private static boolean isGamepadButton(int keyCode) {
-        switch (keyCode) {
-            case KeyEvent.KEYCODE_BUTTON_A:
-            case KeyEvent.KEYCODE_BUTTON_B:
-            case KeyEvent.KEYCODE_BUTTON_C:
-            case KeyEvent.KEYCODE_BUTTON_X:
-            case KeyEvent.KEYCODE_BUTTON_Y:
-            case KeyEvent.KEYCODE_BUTTON_Z:
-            case KeyEvent.KEYCODE_BUTTON_L1:
-            case KeyEvent.KEYCODE_BUTTON_R1:
-            case KeyEvent.KEYCODE_BUTTON_L2:
-            case KeyEvent.KEYCODE_BUTTON_R2:
-            case KeyEvent.KEYCODE_BUTTON_THUMBL:
-            case KeyEvent.KEYCODE_BUTTON_THUMBR:
-            case KeyEvent.KEYCODE_BUTTON_START:
-            case KeyEvent.KEYCODE_BUTTON_SELECT:
-            case KeyEvent.KEYCODE_BUTTON_MODE:
-            case KeyEvent.KEYCODE_BUTTON_1:
-            case KeyEvent.KEYCODE_BUTTON_2:
-            case KeyEvent.KEYCODE_BUTTON_3:
-            case KeyEvent.KEYCODE_BUTTON_4:
-            case KeyEvent.KEYCODE_BUTTON_5:
-            case KeyEvent.KEYCODE_BUTTON_6:
-            case KeyEvent.KEYCODE_BUTTON_7:
-            case KeyEvent.KEYCODE_BUTTON_8:
-            case KeyEvent.KEYCODE_BUTTON_9:
-            case KeyEvent.KEYCODE_BUTTON_10:
-            case KeyEvent.KEYCODE_BUTTON_11:
-            case KeyEvent.KEYCODE_BUTTON_12:
-            case KeyEvent.KEYCODE_BUTTON_13:
-            case KeyEvent.KEYCODE_BUTTON_14:
-            case KeyEvent.KEYCODE_BUTTON_15:
-            case KeyEvent.KEYCODE_BUTTON_16:
-                return true;
-            default:
-                return false;
-        }
-    }
-
-    public static GeckoEvent createNativeGestureEvent(int action, PointF pt, double size) {
-        try {
-            GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.NATIVE_GESTURE_EVENT);
-            event.mAction = action;
-            event.mCount = 1;
-            event.mPoints = new Point[1];
-
-            PointF geckoPoint = new PointF(pt.x, pt.y);
-            geckoPoint = GeckoAppShell.getLayerView().convertViewPointToLayerPoint(geckoPoint);
-
-            if (geckoPoint == null) {
-                // This could happen if Gecko isn't ready yet.
-                return null;
-            }
-
-            event.mPoints[0] = new Point((int)Math.floor(geckoPoint.x), (int)Math.floor(geckoPoint.y));
-
-            event.mX = size;
-            event.mTime = System.currentTimeMillis();
-            return event;
-        } catch (Exception e) {
-            // This can happen if Gecko isn't ready yet
-            return null;
-        }
-    }
-
-    /**
-     * Creates a GeckoEvent that contains the data from the MotionEvent.
-     * The keepInViewCoordinates parameter can be set to false to convert from the Java
-     * coordinate system (device pixels relative to the LayerView) to a coordinate system
-     * relative to gecko's coordinate system (CSS pixels relative to gecko scroll position).
-     */
-    public static GeckoEvent createMotionEvent(MotionEvent m, boolean keepInViewCoordinates) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.MOTION_EVENT);
-        event.initMotionEvent(m, keepInViewCoordinates);
-        return event;
-    }
-
-    /**
-     * Creates a GeckoEvent that contains the data from the LongPressEvent, to be
-     * dispatched in CSS pixels relative to gecko's scroll position.
-     */
-    public static GeckoEvent createLongPressEvent(MotionEvent m) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LONG_PRESS);
-        event.initMotionEvent(m, false);
-        return event;
-    }
-
-    private void initMotionEvent(MotionEvent m, boolean keepInViewCoordinates) {
-        mAction = m.getActionMasked();
-        mTime = (System.currentTimeMillis() - SystemClock.elapsedRealtime()) + m.getEventTime();
-        mMetaState = m.getMetaState();
-
-        switch (mAction) {
-            case MotionEvent.ACTION_CANCEL:
-            case MotionEvent.ACTION_UP:
-            case MotionEvent.ACTION_POINTER_UP:
-            case MotionEvent.ACTION_POINTER_DOWN:
-            case MotionEvent.ACTION_DOWN:
-            case MotionEvent.ACTION_MOVE:
-            case MotionEvent.ACTION_HOVER_ENTER:
-            case MotionEvent.ACTION_HOVER_MOVE:
-            case MotionEvent.ACTION_HOVER_EXIT: {
-                mCount = m.getPointerCount();
-                mPoints = new Point[mCount];
-                mPointIndicies = new int[mCount];
-                mOrientations = new float[mCount];
-                mPressures = new float[mCount];
-                mToolTypes = new int[mCount];
-                mPointRadii = new Point[mCount];
-                mPointerIndex = m.getActionIndex();
-                for (int i = 0; i < mCount; i++) {
-                    addMotionPoint(i, i, m, keepInViewCoordinates);
-                }
-                break;
-            }
-            default: {
-                mCount = 0;
-                mPointerIndex = -1;
-                mPoints = new Point[mCount];
-                mPointIndicies = new int[mCount];
-                mOrientations = new float[mCount];
-                mPressures = new float[mCount];
-                mToolTypes = new int[mCount];
-                mPointRadii = new Point[mCount];
-            }
-        }
-    }
-
-    private void addMotionPoint(int index, int eventIndex, MotionEvent event, boolean keepInViewCoordinates) {
-        try {
-            PointF geckoPoint = new PointF(event.getX(eventIndex), event.getY(eventIndex));
-            if (!keepInViewCoordinates) {
-                geckoPoint = GeckoAppShell.getLayerView().convertViewPointToLayerPoint(geckoPoint);
-            }
-
-            mPoints[index] = new Point((int)Math.floor(geckoPoint.x), (int)Math.floor(geckoPoint.y));
-            mPointIndicies[index] = event.getPointerId(eventIndex);
-
-            double radians = event.getOrientation(eventIndex);
-            mOrientations[index] = (float) Math.toDegrees(radians);
-            // w3c touchevents spec does not allow orientations == 90
-            // this shifts it to -90, which will be shifted to zero below
-            if (mOrientations[index] == 90)
-                mOrientations[index] = -90;
-
-            // w3c touchevent radius are given by an orientation between 0 and 90
-            // the radius is found by removing the orientation and measuring the x and y
-            // radius of the resulting ellipse
-            // for android orientations >= 0 and < 90, the major axis should correspond to
-            // just reporting the y radius as the major one, and x as minor
-            // however, for a radius < 0, we have to shift the orientation by adding 90, and
-            // reverse which radius is major and minor
-            if (mOrientations[index] < 0) {
-                mOrientations[index] += 90;
-                mPointRadii[index] = new Point((int) event.getToolMajor(eventIndex) / 2,
-                                               (int) event.getToolMinor(eventIndex) / 2);
-            } else {
-                mPointRadii[index] = new Point((int) event.getToolMinor(eventIndex) / 2,
-                                               (int) event.getToolMajor(eventIndex) / 2);
-            }
-
-            if (!keepInViewCoordinates) {
-                // If we are converting to gecko CSS pixels, then we should adjust the
-                // radii as well
-                float zoom = GeckoAppShell.getLayerView().getViewportMetrics().zoomFactor;
-                mPointRadii[index].x /= zoom;
-                mPointRadii[index].y /= zoom;
-            }
-            mPressures[index] = event.getPressure(eventIndex);
-            if (Versions.feature14Plus) {
-                mToolTypes[index] = event.getToolType(index);
-            }
-        } catch (Exception ex) {
-            Log.e(LOGTAG, "Error creating motion point " + index, ex);
-            mPointRadii[index] = new Point(0, 0);
-            mPoints[index] = new Point(0, 0);
-        }
-    }
-
-    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;
-    }
-
-    public static GeckoEvent createSensorEvent(SensorEvent s) {
-        int sensor_type = s.sensor.getType();
-        GeckoEvent event = null;
-
-        switch (sensor_type) {
-
-        case Sensor.TYPE_ACCELEROMETER:
-            event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
-            event.mFlags = GeckoHalDefines.SENSOR_ACCELERATION;
-            event.mMetaState = HalSensorAccuracyFor(s.accuracy);
-            event.mX = s.values[0];
-            event.mY = s.values[1];
-            event.mZ = s.values[2];
-            break;
-
-        case Sensor.TYPE_LINEAR_ACCELERATION:
-            event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
-            event.mFlags = GeckoHalDefines.SENSOR_LINEAR_ACCELERATION;
-            event.mMetaState = HalSensorAccuracyFor(s.accuracy);
-            event.mX = s.values[0];
-            event.mY = s.values[1];
-            event.mZ = s.values[2];
-            break;
-
-        case Sensor.TYPE_ORIENTATION:
-            event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
-            event.mFlags = GeckoHalDefines.SENSOR_ORIENTATION;
-            event.mMetaState = HalSensorAccuracyFor(s.accuracy);
-            event.mX = s.values[0];
-            event.mY = s.values[1];
-            event.mZ = s.values[2];
-            break;
-
-        case Sensor.TYPE_GYROSCOPE:
-            event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
-            event.mFlags = GeckoHalDefines.SENSOR_GYROSCOPE;
-            event.mMetaState = HalSensorAccuracyFor(s.accuracy);
-            event.mX = Math.toDegrees(s.values[0]);
-            event.mY = Math.toDegrees(s.values[1]);
-            event.mZ = Math.toDegrees(s.values[2]);
-            break;
-
-        case Sensor.TYPE_PROXIMITY:
-            event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
-            event.mFlags = GeckoHalDefines.SENSOR_PROXIMITY;
-            event.mMetaState = HalSensorAccuracyFor(s.accuracy);
-            event.mX = s.values[0];
-            event.mY = 0;
-            event.mZ = s.sensor.getMaximumRange();
-            break;
-
-        case Sensor.TYPE_LIGHT:
-            event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
-            event.mFlags = GeckoHalDefines.SENSOR_LIGHT;
-            event.mMetaState = HalSensorAccuracyFor(s.accuracy);
-            event.mX = s.values[0];
-            break;
-
-        case Sensor.TYPE_ROTATION_VECTOR:
-        case Sensor.TYPE_GAME_ROTATION_VECTOR: // API >= 18
-            event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
-            event.mFlags = (sensor_type == Sensor.TYPE_ROTATION_VECTOR ?
-                    GeckoHalDefines.SENSOR_ROTATION_VECTOR :
-                    GeckoHalDefines.SENSOR_GAME_ROTATION_VECTOR);
-            event.mMetaState = HalSensorAccuracyFor(s.accuracy);
-            event.mX = s.values[0];
-            event.mY = s.values[1];
-            event.mZ = s.values[2];
-            if (s.values.length >= 4) {
-                event.mW = 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.
-                event.mW = 1 - s.values[0] * s.values[0] - s.values[1] * s.values[1] - s.values[2] * s.values[2];
-                event.mW = (event.mW > 0.0) ? Math.sqrt(event.mW) : 0.0;
-            }
-            break;
-        }
-
-        // SensorEvent timestamp is in nanoseconds, Gecko expects microseconds.
-        event.mTime = s.timestamp / 1000;
-        return event;
-    }
-
-    public static GeckoEvent createLocationEvent(Location l) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LOCATION_EVENT);
-        event.mLocation = l;
-        return event;
-    }
-
-    public static GeckoEvent createViewportEvent(ImmutableViewportMetrics metrics, DisplayPortMetrics displayPort) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.VIEWPORT);
-        event.mCharacters = "Viewport:Change";
-        StringBuilder sb = new StringBuilder(256);
-        sb.append("{ \"x\" : ").append(metrics.viewportRectLeft)
-          .append(", \"y\" : ").append(metrics.viewportRectTop)
-          .append(", \"zoom\" : ").append(metrics.zoomFactor)
-          .append(", \"displayPort\" :").append(displayPort.toJSON())
-          .append('}');
-        event.mCharactersExtra = sb.toString();
-        return event;
-    }
-
-    public static GeckoEvent createURILoadEvent(String uri) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LOAD_URI);
-        event.mCharacters = uri;
-        event.mCharactersExtra = "";
-        return event;
-    }
-
-    public static GeckoEvent createBookmarkLoadEvent(String uri) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LOAD_URI);
-        event.mCharacters = uri;
-        event.mCharactersExtra = "-bookmark";
-        return event;
-    }
-
-    public static GeckoEvent createVisitedEvent(String data) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.VISITED);
-        event.mCharacters = data;
-        return event;
-    }
-
-    public static GeckoEvent createNetworkEvent(int connectionType, boolean isWifi, int DHCPGateway, String status) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.NETWORK_CHANGED);
-        event.mConnectionType = connectionType;
-        event.mIsWifi = isWifi;
-        event.mDHCPGateway = DHCPGateway;
-        event.mCharacters = status;
-        return event;
-    }
-
-    public static GeckoEvent createThumbnailEvent(int tabId, int bufw, int bufh, ByteBuffer buffer) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.THUMBNAIL);
-        event.mPoints = new Point[1];
-        event.mPoints[0] = new Point(bufw, bufh);
-        event.mMetaState = tabId;
-        event.mBuffer = buffer;
-        return event;
-    }
-
-    public static GeckoEvent createZoomedViewEvent(int tabId, int x, int y, int bufw, int bufh, float scaleFactor, ByteBuffer buffer) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.ZOOMEDVIEW);
-        event.mPoints = new Point[2];
-        event.mPoints[0] = new Point(x, y);
-        event.mPoints[1] = new Point(bufw, bufh);
-        event.mX = (double) scaleFactor;
-        event.mMetaState = tabId;
-        event.mBuffer = buffer;
-        return event;
-    }
-
-    public static GeckoEvent createScreenOrientationEvent(short aScreenOrientation, short aScreenAngle) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.SCREENORIENTATION_CHANGED);
-        event.mScreenOrientation = aScreenOrientation;
-        event.mScreenAngle = aScreenAngle;
-        return event;
-    }
-
-    public static GeckoEvent createCallObserverEvent(String observerKey, String topic, String data) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.CALL_OBSERVER);
-        event.mCharacters = observerKey;
-        event.mCharactersExtra = topic;
-        event.mData = data;
-        return event;
-    }
-
-    public static GeckoEvent createRemoveObserverEvent(String observerKey) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.REMOVE_OBSERVER);
-        event.mCharacters = observerKey;
-        return event;
-    }
-
-    public static GeckoEvent createLowMemoryEvent(int level) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LOW_MEMORY);
-        event.mMetaState = level;
-        return event;
-    }
-
-    public static GeckoEvent createNetworkLinkChangeEvent(String status) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.NETWORK_LINK_CHANGE);
-        event.mCharacters = status;
-        return event;
-    }
-
-    public static GeckoEvent createTelemetryHistogramAddEvent(String histogram,
-                                                              int value) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_HISTOGRAM_ADD);
-        event.mCharacters = histogram;
-        // Set the extras with null so that it cannot be mistaken with a keyed histogram.
-        event.mCharactersExtra = null;
-        event.mCount = value;
-        return event;
-    }
-
-    public static GeckoEvent createTelemetryKeyedHistogramAddEvent(String histogram,
-                                                                   String key,
-                                                                   int value) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_HISTOGRAM_ADD);
-        event.mCharacters = histogram;
-        event.mCharactersExtra = key;
-        event.mCount = value;
-        return event;
-    }
-
-    public static GeckoEvent createTelemetryUISessionStartEvent(String session, long timestamp) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_UI_SESSION_START);
-        event.mCharacters = session;
-        event.mTime = timestamp;
-        return event;
-    }
-
-    public static GeckoEvent createTelemetryUISessionStopEvent(String session, String reason, long timestamp) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_UI_SESSION_STOP);
-        event.mCharacters = session;
-        event.mCharactersExtra = reason;
-        event.mTime = timestamp;
-        return event;
-    }
-
-    public static GeckoEvent createTelemetryUIEvent(String action, String method, long timestamp, String extras) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_UI_EVENT);
-        event.mData = action;
-        event.mCharacters = method;
-        event.mCharactersExtra = extras;
-        event.mTime = timestamp;
-        return event;
-    }
-
-    public static GeckoEvent createGamepadAddRemoveEvent(int id, boolean added) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.GAMEPAD_ADDREMOVE);
-        event.mID = id;
-        event.mAction = added ? ACTION_GAMEPAD_ADDED : ACTION_GAMEPAD_REMOVED;
-        return event;
-    }
-
-    private static int boolArrayToBitfield(boolean[] array) {
-        int bits = 0;
-        for (int i = 0; i < array.length; i++) {
-            if (array[i]) {
-                bits |= 1 << i;
-            }
-        }
-        return bits;
-    }
-
-    public static GeckoEvent createGamepadButtonEvent(int id,
-                                                      int which,
-                                                      boolean pressed,
-                                                      float value) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.GAMEPAD_DATA);
-        event.mID = id;
-        event.mAction = ACTION_GAMEPAD_BUTTON;
-        event.mGamepadButton = which;
-        event.mGamepadButtonPressed = pressed;
-        event.mGamepadButtonValue = value;
-        return event;
-    }
-
-    public static GeckoEvent createGamepadAxisEvent(int id, boolean[] valid,
-                                                    float[] values) {
-        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.GAMEPAD_DATA);
-        event.mID = id;
-        event.mAction = ACTION_GAMEPAD_AXES;
-        event.mFlags = boolArrayToBitfield(valid);
-        event.mCount = values.length;
-        event.mGamepadValues = values;
-        return event;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoHalDefines.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/* -*- 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;
-};
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoInputConnection.java
+++ /dev/null
@@ -1,992 +0,0 @@
-/* -*- 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.util.Clipboard;
-import org.mozilla.gecko.util.GamepadUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
-
-import android.content.Context;
-import android.content.res.Configuration;
-import android.media.AudioManager;
-import android.os.Bundle;
-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.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 String mCurrentInputMethod = "";
-
-    private final View mView;
-    private final GeckoEditableClient mEditableClient;
-    protected int mBatchEditCount;
-    private ExtractedTextRequest mUpdateRequest;
-    private final ExtractedText mUpdateExtract = new ExtractedText();
-    private boolean mBatchSelectionChanged;
-    private boolean mBatchTextChanged;
-    private final InputConnection mKeyInputConnection;
-
-    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++;
-        mEditableClient.setBatchMode(true);
-        return true;
-    }
-
-    @Override
-    public synchronized boolean endBatchEdit() {
-        if (mBatchEditCount > 0) {
-            mBatchEditCount--;
-            if (mBatchEditCount == 0) {
-                if (mBatchTextChanged) {
-                    notifyTextChange();
-                    mBatchTextChanged = false;
-                }
-                if (mBatchSelectionChanged) {
-                    Editable editable = getEditable();
-                    notifySelectionChange(Selection.getSelectionStart(editable),
-                                           Selection.getSelectionEnd(editable));
-                    mBatchSelectionChanged = false;
-                }
-                mEditableClient.setBatchMode(false);
-            }
-        } else {
-            Log.w(LOGTAG, "endBatchEdit() called, but mBatchEditCount == 0?!");
-        }
-        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 boolean performPrivateCommand(final String action, final Bundle data) {
-        switch (action) {
-            case "process-gecko-events":
-                // Process all currently pending Gecko thread events before returning.
-
-                final Editable editable = getEditable();
-                if (editable == null) {
-                    return false;
-                }
-
-                // Removing an invalid span is essentially a no-op, but it does force the
-                // current thread to wait for the Gecko thread when we call length(), in order
-                // to process the removeSpan event. Once Gecko thread processes the removeSpan
-                // event, all previous events in the Gecko event queue would have been
-                // processed as well.
-                editable.removeSpan(null);
-                editable.length();
-                return true;
-        }
-        return false;
-    }
-
-    @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() {
-        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();
-                }
-                imm.showSoftInput(v, 0);
-            }
-        });
-    }
-
-    private void hideSoftInput() {
-        final InputMethodManager imm = getInputMethodManager();
-        if (imm != null) {
-            final View v = getView();
-            imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
-        }
-    }
-
-    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;
-        }
-        mBatchSelectionChanged = false;
-        mBatchTextChanged = false;
-
-        // Do not reset mIMEState here; see comments in notifyIMEContext
-    }
-
-    @Override
-    public void onTextChange(CharSequence text, int start, int oldEnd, int newEnd) {
-
-        if (mUpdateRequest == null) {
-            // Android always expects selection updates when not in extracted mode;
-            // in extracted mode, the selection is reported through updateExtractedText
-            final Editable editable = getEditable();
-            if (editable != null) {
-                onSelectionChange(Selection.getSelectionStart(editable),
-                                  Selection.getSelectionEnd(editable));
-            }
-            return;
-        }
-
-        if (mBatchEditCount > 0) {
-            // Delay notification until after the batch edit
-            mBatchTextChanged = true;
-            return;
-        }
-        notifyTextChange();
-    }
-
-    private void notifyTextChange() {
-
-        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
-    public void onSelectionChange(int start, int end) {
-
-        if (mBatchEditCount > 0) {
-            // Delay notification until after the batch edit
-            mBatchSelectionChanged = true;
-            return;
-        }
-
-        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 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
-    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 the user has changed IMEs, then notify input method observers.
-        if (!mCurrentInputMethod.equals(prevInputMethod) && GeckoAppShell.getGeckoInterface() != null) {
-            GeckoAppShell.getGeckoInterface().onInputMethodChanged(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:
-            case NOTIFY_IME_OF_BLUR:
-                // Showing/hiding vkb is done in notifyIMEContext
-                resetInputConnection();
-                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") ||
-             (Versions.feature11Plus && (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;
-        }
-        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;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoJavaSampler.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/* -*- 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.SystemClock;
-import android.util.Log;
-import android.util.SparseArray;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-
-import java.lang.Thread;
-import java.util.Set;
-
-public class GeckoJavaSampler {
-    private static final String LOGTAG = "JavaSampler";
-    private static Thread sSamplingThread;
-    private static SamplingThread sSamplingRunnable;
-    private static Thread sMainThread;
-
-    // Use the same timer primitive as the profiler
-    // to get a perfect sample syncing.
-    @WrapForJNI
-    private static native double getProfilerTime();
-
-    private static class Sample {
-        public Frame[] mFrames;
-        public double mTime;
-        public long mJavaTime; // non-zero if Android system time is used
-        public Sample(StackTraceElement[] aStack) {
-            mFrames = new Frame[aStack.length];
-            if (GeckoThread.isStateAtLeast(GeckoThread.State.LIBS_READY)) {
-                mTime = getProfilerTime();
-            }
-            if (mTime == 0.0d) {
-                // getProfilerTime is not available yet; either libs are not loaded,
-                // or profiling hasn't started on the Gecko side yet
-                mJavaTime = SystemClock.elapsedRealtime();
-            }
-            for (int i = 0; i < aStack.length; i++) {
-                mFrames[aStack.length - 1 - i] = new Frame();
-                mFrames[aStack.length - 1 - i].fileName = aStack[i].getFileName();
-                mFrames[aStack.length - 1 - i].lineNo = aStack[i].getLineNumber();
-                mFrames[aStack.length - 1 - i].methodName = aStack[i].getMethodName();
-                mFrames[aStack.length - 1 - i].className = aStack[i].getClassName();
-            }
-        }
-    }
-    private static class Frame {
-        public String fileName;
-        public int lineNo;
-        public String methodName;
-        public String className;
-    }
-
-    private static class SamplingThread implements Runnable {
-        private final int mInterval;
-        private final int mSampleCount;
-
-        private boolean mPauseSampler;
-        private boolean mStopSampler;
-
-        private final SparseArray<Sample[]> mSamples = new SparseArray<Sample[]>();
-        private int mSamplePos;
-
-        public SamplingThread(final int aInterval, final int aSampleCount) {
-            // If we sample faster then 10ms we get to many missed samples
-            mInterval = Math.max(10, aInterval);
-            mSampleCount = aSampleCount;
-        }
-
-        @Override
-        public void run() {
-            synchronized (GeckoJavaSampler.class) {
-                mSamples.put(0, new Sample[mSampleCount]);
-                mSamplePos = 0;
-
-                // Find the main thread
-                Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
-                for (Thread t : threadSet) {
-                    if (t.getName().compareToIgnoreCase("main") == 0) {
-                        sMainThread = t;
-                        break;
-                    }
-                }
-
-                if (sMainThread == null) {
-                    Log.e(LOGTAG, "Main thread not found");
-                    return;
-                }
-            }
-
-            while (true) {
-                try {
-                    Thread.sleep(mInterval);
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
-                synchronized (GeckoJavaSampler.class) {
-                    if (!mPauseSampler) {
-                        StackTraceElement[] bt = sMainThread.getStackTrace();
-                        mSamples.get(0)[mSamplePos] = new Sample(bt);
-                        mSamplePos = (mSamplePos + 1) % mSamples.get(0).length;
-                    }
-                    if (mStopSampler) {
-                        break;
-                    }
-                }
-            }
-        }
-
-        private Sample getSample(int aThreadId, int aSampleId) {
-            if (aThreadId < mSamples.size() && aSampleId < mSamples.get(aThreadId).length &&
-                mSamples.get(aThreadId)[aSampleId] != null) {
-                int startPos = 0;
-                if (mSamples.get(aThreadId)[mSamplePos] != null) {
-                    startPos = mSamplePos;
-                }
-                int readPos = (startPos + aSampleId) % mSamples.get(aThreadId).length;
-                return mSamples.get(aThreadId)[readPos];
-            }
-            return null;
-        }
-    }
-
-
-    @WrapForJNI(allowMultithread = true, stubName = "GetThreadNameJavaProfilingWrapper")
-    public synchronized static String getThreadName(int aThreadId) {
-        if (aThreadId == 0 && sMainThread != null) {
-            return sMainThread.getName();
-        }
-        return null;
-    }
-
-    private synchronized static Sample getSample(int aThreadId, int aSampleId) {
-        return sSamplingRunnable.getSample(aThreadId, aSampleId);
-    }
-
-    @WrapForJNI(allowMultithread = true, stubName = "GetSampleTimeJavaProfiling")
-    public synchronized static double getSampleTime(int aThreadId, int aSampleId) {
-        Sample sample = getSample(aThreadId, aSampleId);
-        if (sample != null) {
-            if (sample.mJavaTime != 0) {
-                return (sample.mJavaTime -
-                    SystemClock.elapsedRealtime()) + getProfilerTime();
-            }
-            System.out.println("Sample: " + sample.mTime);
-            return sample.mTime;
-        }
-        return 0;
-    }
-
-    @WrapForJNI(allowMultithread = true, stubName = "GetFrameNameJavaProfilingWrapper")
-    public synchronized static String getFrameName(int aThreadId, int aSampleId, int aFrameId) {
-        Sample sample = getSample(aThreadId, aSampleId);
-        if (sample != null && aFrameId < sample.mFrames.length) {
-            Frame frame = sample.mFrames[aFrameId];
-            if (frame == null) {
-                return null;
-            }
-            return frame.className + "." + frame.methodName + "()";
-        }
-        return null;
-    }
-
-    @WrapForJNI(allowMultithread = true, stubName = "StartJavaProfiling")
-    public static void start(int aInterval, int aSamples) {
-        synchronized (GeckoJavaSampler.class) {
-            if (sSamplingRunnable != null) {
-                return;
-            }
-            sSamplingRunnable = new SamplingThread(aInterval, aSamples);
-            sSamplingThread = new Thread(sSamplingRunnable, "Java Sampler");
-            sSamplingThread.start();
-        }
-    }
-
-    @WrapForJNI(allowMultithread = true, stubName = "PauseJavaProfiling")
-    public static void pause() {
-        synchronized (GeckoJavaSampler.class) {
-            sSamplingRunnable.mPauseSampler = true;
-        }
-    }
-
-    @WrapForJNI(allowMultithread = true, stubName = "UnpauseJavaProfiling")
-    public static void unpause() {
-        synchronized (GeckoJavaSampler.class) {
-            sSamplingRunnable.mPauseSampler = false;
-        }
-    }
-
-    @WrapForJNI(allowMultithread = true, stubName = "StopJavaProfiling")
-    public static void stop() {
-        synchronized (GeckoJavaSampler.class) {
-            if (sSamplingThread == null) {
-                return;
-            }
-
-            sSamplingRunnable.mStopSampler = true;
-            try {
-                sSamplingThread.join();
-            } catch (InterruptedException e) {
-                e.printStackTrace();
-            }
-            sSamplingThread = null;
-            sSamplingRunnable = null;
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoNetworkManager.java
+++ /dev/null
@@ -1,436 +0,0 @@
-/* -*- 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.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;
-
-    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 NetworkStatus currentNetworkStatus = NetworkStatus.UNKNOWN;
-    private ConnectionSubType currentConnectionSubtype = ConnectionSubType.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() {
-        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;
-        }
-
-        performActionsForStateEvent(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(ManagerState currentState, 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();
-                    registerBroadcastReceiver();
-                }
-                if (event == ManagerEvent.enableNotifications) {
-                    updateNetworkStateAndConnectionType();
-                }
-                break;
-            case OnNoListeners:
-                if (event == ManagerEvent.receivedUpdate) {
-                    updateNetworkStateAndConnectionType();
-                    sendNetworkStateToListeners();
-                }
-                if (event == ManagerEvent.enableNotifications) {
-                    updateNetworkStateAndConnectionType();
-                    registerBroadcastReceiver();
-                }
-                if (event == ManagerEvent.stop) {
-                    unregisterBroadcastReceiver();
-                }
-                break;
-            case OnWithListeners:
-                if (event == ManagerEvent.receivedUpdate) {
-                    updateNetworkStateAndConnectionType();
-                    sendNetworkStateToListeners();
-                }
-                if (event == ManagerEvent.stop) {
-                    unregisterBroadcastReceiver();
-                }
-                /* no-op event: ManagerEvent.disableNotifications */
-                break;
-            case OffWithListeners:
-                if (event == ManagerEvent.start) {
-                    registerBroadcastReceiver();
-                }
-                /* 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 applicationContext = GeckoAppShell.getApplicationContext();
-        final ConnectivityManager connectivityManager = (ConnectivityManager) applicationContext.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);
-    }
-
-    /**
-     * Send current network state and connection type as a GeckoEvent, to whomever is listening.
-     */
-    private void sendNetworkStateToListeners() {
-        if (GeckoThread.isRunning()) {
-            final Context applicationContext = GeckoAppShell.getApplicationContext();
-            GeckoAppShell.sendEventToGecko(
-                    GeckoEvent.createNetworkEvent(
-                            currentConnectionType.value,
-                            currentConnectionType == ConnectionType.WIFI,
-                            wifiDhcpGatewayAddress(applicationContext),
-                            currentConnectionSubtype.value
-                    )
-            );
-
-            GeckoAppShell.sendEventToGecko(GeckoEvent.createNetworkLinkChangeEvent(currentNetworkStatus.value));
-            GeckoAppShell.sendEventToGecko(GeckoEvent.createNetworkLinkChangeEvent(LINK_DATA_CHANGED));
-        }
-    }
-
-    /**
-     * Stop listening for network state updates.
-     */
-    private void unregisterBroadcastReceiver() {
-        GeckoAppShell.getApplicationContext().unregisterReceiver(this);
-    }
-
-    /**
-     * Start listening for network state updates.
-     */
-    private void registerBroadcastReceiver() {
-        final IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
-        if (GeckoAppShell.getApplicationContext().registerReceiver(this, filter) == null) {
-            Log.e(LOGTAG, "Registering receiver failed");
-        }
-    }
-
-    private static int wifiDhcpGatewayAddress(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
-    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.
-     */
-    @JNITarget
-    public static int getMCC() {
-        return getNetworkOperator(InfoType.MCC, GeckoAppShell.getApplicationContext());
-    }
-
-    @JNITarget
-    public static int getMNC() {
-        return getNetworkOperator(InfoType.MNC, GeckoAppShell.getApplicationContext());
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
+++ /dev/null
@@ -1,943 +0,0 @@
-/* -*- 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.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_BUILD 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";
-
-    public static final String PREF_FIRSTRUN_ENABLED = "startpane_enabled";
-
-    // Session store
-    private static final String SESSION_FILE = "sessionstore.js";
-    private static final String SESSION_FILE_BACKUP = "sessionstore.bak";
-    private static final long MAX_BACKUP_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;
-
-    /**
-     * 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 GeckoProfile initFromArgs(final Context context, final String args) {
-        if (shouldUse(context)) {
-            final GeckoProfile guestProfile = getGuestProfile(context);
-            if (guestProfile != null) {
-                return guestProfile;
-            }
-            // Failed to create guest profile; leave guest mode.
-            leave(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_BUILD) {
-                    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.");
-        }
-    }
-
-    public static boolean shouldUse(final Context context) {
-        return GeckoSharedPrefs.forApp(context).getBoolean(GUEST_MODE_PREF, false);
-    }
-
-    public static void enter(final Context context) {
-        GeckoSharedPrefs.forApp(context).edit().putBoolean(GUEST_MODE_PREF, true).commit();
-    }
-
-    public static void leave(final Context context) {
-        GeckoSharedPrefs.forApp(context).edit().putBoolean(GUEST_MODE_PREF, false).commit();
-    }
-
-    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;
-    }
-
-    /**
-     * Retrieves the directory backing the profile. This method acts
-     * as a lazy initializer for the GeckoProfile instance.
-     */
-    @RobocopTarget
-    public synchronized File getDir() {
-        forceCreate();
-        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 synchronized GeckoProfile forceCreate() {
-        if (mProfileDir != null) {
-            return this;
-        }
-
-        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);
-        }
-        return this;
-    }
-
-    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://mxr.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.bak 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.bak, so we can display the correct "tabs from last time".
-     * If we *are* restoring tabs, we need to delete outdated copies of sessionstore.bak,
-     * 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 sessionFileBackup = getFile(SESSION_FILE_BACKUP);
-        if (!shouldRestore) {
-            File sessionFile = getFile(SESSION_FILE);
-            if (sessionFile != null && sessionFile.exists()) {
-                sessionFile.renameTo(sessionFileBackup);
-            }
-        } else {
-            if (sessionFileBackup != null && sessionFileBackup.exists() &&
-                    System.currentTimeMillis() - sessionFileBackup.lastModified() > MAX_BACKUP_FILE_AGE) {
-                sessionFileBackup.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 the previous session.
-     *
-     * @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) {
-        File sessionFile = getFile(readBackup ? SESSION_FILE_BACKUP : SESSION_FILE);
-
-        try {
-            if (sessionFile != null && sessionFile.exists()) {
-                return readFile(sessionFile);
-            }
-        } catch (IOException ioe) {
-            Log.e(LOGTAG, "Unable to read session file", ioe);
-        }
-        return null;
-    }
-
-    /**
-     * 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 = GeckoProfileDirectories.saltProfileName(mName);
-        File profileDir = new File(mMozillaDir, saltedName);
-        while (profileDir.exists()) {
-            saltedName = GeckoProfileDirectories.saltProfileName(mName);
-            profileDir = new File(mMozillaDir, saltedName);
-        }
-
-        // 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);
-
-            // We have no intention of stopping this session. The FIRSTRUN session
-            // ends when the browsing session/activity has ended. All events
-            // during firstrun will be tagged as FIRSTRUN.
-            // TODO: local broadcast to tell App that things are happening?
-            // Telemetry.startUISession(TelemetryContract.Session.FIRSTRUN);
-        }
-
-        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());
-
-        // Initialize pref flag for displaying the start pane for a new profile.
-        final SharedPreferences prefs = GeckoSharedPrefs.forProfile(mApplicationContext);
-        prefs.edit().putBoolean(PREF_FIRSTRUN_ENABLED, true).apply();
-
-        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) {
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoProfileDirectories.java
+++ /dev/null
@@ -1,228 +0,0 @@
-/* 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));
-
-      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);
-
-        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);
-
-        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);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoScreenOrientation.java
+++ /dev/null
@@ -1,414 +0,0 @@
-/* -*- 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.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()));
-    }
-
-    /*
-     * 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;
-            }
-            GeckoAppShell.sendEventToGecko(
-                GeckoEvent.createScreenOrientationEvent(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;
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoService.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/* -*- 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.AlarmManager;
-import android.app.Service;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.os.IBinder;
-import android.util.Log;
-
-import java.io.File;
-
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.EventCallback;
-
-public class GeckoService extends Service {
-
-    private static final String LOGTAG = "GeckoService";
-    private static final boolean DEBUG = false;
-
-    private static final String INTENT_PROFILE_NAME = "org.mozilla.gecko.intent.PROFILE_NAME";
-    private static final String INTENT_PROFILE_DIR = "org.mozilla.gecko.intent.PROFILE_DIR";
-
-    private static final String INTENT_ACTION_UPDATE_ADDONS = "update-addons";
-    private static final String INTENT_ACTION_CREATE_SERVICES = "create-services";
-
-    private static final String INTENT_SERVICE_CATEGORY = "category";
-    private static final String INTENT_SERVICE_DATA = "data";
-
-    private static class EventListener implements NativeEventListener {
-        @Override // NativeEventListener
-        public void handleMessage(final String event,
-                                  final NativeJSObject message,
-                                  final EventCallback callback) {
-            final Context context = GeckoAppShell.getApplicationContext();
-            switch (event) {
-            case "Gecko:ScheduleRun":
-                if (DEBUG) {
-                    Log.d(LOGTAG, "Scheduling " + message.getString("action") +
-                                  " @ " + message.getInt("interval") + "ms");
-                }
-
-                final Intent intent = getIntentForAction(context, message.getString("action"));
-                final PendingIntent pendingIntent = PendingIntent.getService(
-                        context, /* requestCode */ 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
-
-                final AlarmManager am = (AlarmManager)
-                    context.getSystemService(Context.ALARM_SERVICE);
-                // Cancel any previous alarm and schedule a new one.
-                am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
-                                       message.getInt("trigger"),
-                                       message.getInt("interval"),
-                                       pendingIntent);
-                break;
-
-            default:
-                throw new UnsupportedOperationException(event);
-            }
-        }
-    }
-
-    private static final EventListener EVENT_LISTENER = new EventListener();
-
-    public static void register() {
-        if (DEBUG) {
-            Log.d(LOGTAG, "Registered listener");
-        }
-        EventDispatcher.getInstance().registerGeckoThreadListener(EVENT_LISTENER,
-                "Gecko:ScheduleRun");
-    }
-
-    public static void unregister() {
-        if (DEBUG) {
-            Log.d(LOGTAG, "Unregistered listener");
-        }
-        EventDispatcher.getInstance().unregisterGeckoThreadListener(EVENT_LISTENER,
-                "Gecko:ScheduleRun");
-    }
-
-    @Override // Service
-    public void onCreate() {
-        GeckoAppShell.ensureCrashHandling();
-        GeckoAppShell.setApplicationContext(getApplicationContext());
-        GeckoAppShell.setNotificationClient(new ServiceNotificationClient(getApplicationContext()));
-        GeckoThread.onResume();
-        super.onCreate();
-
-        if (DEBUG) {
-            Log.d(LOGTAG, "Created");
-        }
-    }
-
-    @Override // Service
-    public void onDestroy() {
-        GeckoThread.onPause();
-
-        // We want to block here if we can, so we don't get killed when Gecko is in the
-        // middle of handling onPause().
-        if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
-            GeckoThread.waitOnGecko();
-        }
-
-        if (DEBUG) {
-            Log.d(LOGTAG, "Destroyed");
-        }
-        super.onDestroy();
-    }
-
-    private static Intent getIntentForAction(final Context context, final String action) {
-        final Intent intent = new Intent(action, /* uri */ null, context, GeckoService.class);
-        final GeckoProfile profile = GeckoThread.getActiveProfile();
-        if (profile != null) {
-            setIntentProfile(intent, profile.getName(), profile.getDir().getAbsolutePath());
-        }
-        return intent;
-    }
-
-    public static Intent getIntentToCreateServices(final Context context, final String category, final String data) {
-        final Intent intent = getIntentForAction(context, INTENT_ACTION_CREATE_SERVICES);
-        intent.putExtra(INTENT_SERVICE_CATEGORY, category);
-        intent.putExtra(INTENT_SERVICE_DATA, data);
-        return intent;
-    }
-
-    public static Intent getIntentToCreateServices(final Context context, final String category) {
-        return getIntentToCreateServices(context, category, /* data */ null);
-    }
-
-    public static void setIntentProfile(final Intent intent, final String profileName,
-                                        final String profileDir) {
-        intent.putExtra(INTENT_PROFILE_NAME, profileName);
-        intent.putExtra(INTENT_PROFILE_DIR, profileDir);
-    }
-
-    private int handleIntent(final Intent intent, final int startId) {
-        if (DEBUG) {
-            Log.d(LOGTAG, "Handling " + intent.getAction());
-        }
-
-        final String profileName = intent.getStringExtra(INTENT_PROFILE_NAME);
-        final String profileDir = intent.getStringExtra(INTENT_PROFILE_DIR);
-
-        if (profileName == null || profileDir == null) {
-            throw new IllegalArgumentException("Intent must specify profile.");
-        }
-
-        if (!GeckoThread.initWithProfile(profileName != null ? profileName : "",
-                                         new File(profileDir))) {
-            Log.w(LOGTAG, "Ignoring due to profile mismatch: " +
-                          profileName + " [" + profileDir + ']');
-
-            final GeckoProfile profile = GeckoThread.getActiveProfile();
-            if (profile != null) {
-                Log.w(LOGTAG, "Current profile is " + profile.getName() +
-                              " [" + profile.getDir().getAbsolutePath() + ']');
-            }
-            stopSelf(startId);
-            return Service.START_NOT_STICKY;
-        }
-
-        GeckoThread.launch();
-
-        switch (intent.getAction()) {
-        case INTENT_ACTION_UPDATE_ADDONS:
-            // Run the add-on update service. Because the service is automatically invoked
-            // when loading Gecko, we don't have to do anything else here.
-            break;
-
-        case INTENT_ACTION_CREATE_SERVICES:
-            final String category = intent.getStringExtra(INTENT_SERVICE_CATEGORY);
-            final String data = intent.getStringExtra(INTENT_SERVICE_DATA);
-
-            if (category == null) {
-                break;
-            }
-            GeckoThread.createServices(category, data);
-            break;
-
-        default:
-            Log.w(LOGTAG, "Unknown request: " + intent);
-        }
-
-        stopSelf(startId);
-        return Service.START_NOT_STICKY;
-    }
-
-    @Override // Service
-    public int onStartCommand(final Intent intent, final int flags, final int startId) {
-        if (intent == null) {
-            return Service.START_NOT_STICKY;
-        }
-        try {
-            return handleIntent(intent, startId);
-        } catch (final Throwable e) {
-            Log.e(LOGTAG, "Cannot handle intent: " + intent, e);
-            return Service.START_NOT_STICKY;
-        }
-    }
-
-    @Override // Service
-    public IBinder onBind(final Intent intent) {
-        return null;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoSharedPrefs.java
+++ /dev/null
@@ -1,318 +0,0 @@
-/* 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
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoSmsManager.java
+++ /dev/null
@@ -1,1244 +0,0 @@
-/* -*- 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.Activity;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.provider.Telephony;
-import android.telephony.SmsManager;
-import android.telephony.SmsMessage;
-import android.util.Log;
-
-import static android.telephony.SmsMessage.MessageClass;
-import static org.mozilla.gecko.SmsManager.ISmsManager;
-
-import java.util.ArrayList;
-import java.util.Formatter;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * The envelope class contains all information that are needed to keep track of
- * a sent SMS.
- */
-class Envelope
-{
-  enum SubParts {
-    SENT_PART,
-    DELIVERED_PART
-  }
-
-  protected int       mId;
-  protected int       mMessageId;
-  protected long      mMessageTimestamp;
-
-  /**
-   * Number of sent/delivered remaining parts.
-   * @note The array has much slots as SubParts items.
-   */
-  protected int[]     mRemainingParts;
-
-  /**
-   * Whether sending/delivering is currently failing.
-   * @note The array has much slots as SubParts items.
-   */
-  protected boolean[] mFailing;
-
-  /**
-   * Error type (only for sent).
-   */
-  protected int       mError;
-
-  public Envelope(int aId, int aParts) {
-    mId = aId;
-    mMessageId = -1;
-    mError = GeckoSmsManager.kNoError;
-
-    int size = SubParts.values().length;
-    mRemainingParts = new int[size];
-    mFailing = new boolean[size];
-
-    for (int i = 0; i < size; ++i) {
-      mRemainingParts[i] = aParts;
-      mFailing[i] = false;
-    }
-  }
-
-  public void decreaseRemainingParts(SubParts aType) {
-    --mRemainingParts[aType.ordinal()];
-
-    if (mRemainingParts[SubParts.SENT_PART.ordinal()] >
-        mRemainingParts[SubParts.DELIVERED_PART.ordinal()]) {
-      Log.e("GeckoSmsManager", "Delivered more parts than we sent!?");
-    }
-  }
-
-  public boolean arePartsRemaining(SubParts aType) {
-    return mRemainingParts[aType.ordinal()] != 0;
-  }
-
-  public void markAsFailed(SubParts aType) {
-    mFailing[aType.ordinal()] = true;
-  }
-
-  public boolean isFailing(SubParts aType) {
-    return mFailing[aType.ordinal()];
-  }
-
-  public int getMessageId() {
-    return mMessageId;
-  }
-
-  public void setMessageId(int aMessageId) {
-    mMessageId = aMessageId;
-  }
-
-  public long getMessageTimestamp() {
-    return mMessageTimestamp;
-  }
-
-  public void setMessageTimestamp(long aMessageTimestamp) {
-    mMessageTimestamp = aMessageTimestamp;
-  }
-
-  public int getError() {
-    return mError;
-  }
-
-  public void setError(int aError) {
-    mError = aError;
-  }
-}
-
-/**
- * Postman class is a singleton that manages Envelope instances.
- */
-class Postman
-{
-  public static final int kUnknownEnvelopeId = -1;
-
-  private static final Postman sInstance = new Postman();
-
-  private final ArrayList<Envelope> mEnvelopes = new ArrayList<>(1);
-
-  private Postman() {}
-
-  public static Postman getInstance() {
-    return sInstance;
-  }
-
-  public int createEnvelope(int aParts) {
-    /*
-     * We are going to create the envelope in the first empty slot in the array
-     * list. If there is no empty slot, we create a new one.
-     */
-    int size = mEnvelopes.size();
-
-    for (int i = 0; i < size; ++i) {
-      if (mEnvelopes.get(i) == null) {
-        mEnvelopes.set(i, new Envelope(i, aParts));
-        return i;
-      }
-    }
-
-    mEnvelopes.add(new Envelope(size, aParts));
-    return size;
-  }
-
-  public Envelope getEnvelope(int aId) {
-    if (aId < 0 || mEnvelopes.size() <= aId) {
-      Log.e("GeckoSmsManager", "Trying to get an unknown Envelope!");
-      return null;
-    }
-
-    Envelope envelope = mEnvelopes.get(aId);
-    if (envelope == null) {
-      Log.e("GeckoSmsManager", "Trying to get an empty Envelope!");
-    }
-
-    return envelope;
-  }
-
-  public void destroyEnvelope(int aId) {
-    if (aId < 0 || mEnvelopes.size() <= aId) {
-      Log.e("GeckoSmsManager", "Trying to destroy an unknown Envelope!");
-      return;
-    }
-
-    if (mEnvelopes.set(aId, null) == null) {
-      Log.e("GeckoSmsManager", "Trying to destroy an empty Envelope!");
-    }
-  }
-}
-
-class SmsIOThread extends HandlerThread {
-  private final static SmsIOThread sInstance = new SmsIOThread();
-
-  private Handler mHandler;
-
-  public static SmsIOThread getInstance() {
-    return sInstance;
-  }
-
-  SmsIOThread() {
-    super("SmsIOThread");
-  }
-
-  @Override
-  public void start() {
-    super.start();
-    mHandler = new Handler(getLooper());
-  }
-
-  public boolean execute(Runnable r) {
-    return mHandler.post(r);
-  }
-}
-
-class MessagesListManager
-{
-  private static final MessagesListManager sInstance = new MessagesListManager();
-
-  public static MessagesListManager getInstance() {
-    return sInstance;
-  }
-
-  private final HashMap<Integer, Cursor> mCursors = new HashMap<>();
-
-  public void add(int id, Cursor aCursor) {
-    if (mCursors.containsKey(id)) {
-      Log.e("GeckoSmsManager", "Trying to overwrite cursor!");
-      return;
-    }
-
-    mCursors.put(id, aCursor);
-  }
-
-  public Cursor get(int aId) {
-    if (!mCursors.containsKey(aId)) {
-      Log.e("GeckoSmsManager", "Cursor doesn't exist!");
-      return null;
-    }
-
-    return mCursors.get(aId);
-  }
-
-  public void remove(int aId) {
-    if (!mCursors.containsKey(aId)) {
-      Log.e("GeckoSmsManager", "Cursor doesn't exist!");
-      return;
-    }
-
-    mCursors.remove(aId);
-  }
-
-  public void clear() {
-    Set<Map.Entry<Integer, Cursor>> entries = mCursors.entrySet();
-    Iterator<Map.Entry<Integer, Cursor>> it = entries.iterator();
-    while (it.hasNext()) {
-      it.next().getValue().close();
-    }
-
-    mCursors.clear();
-  }
-}
-
-public class GeckoSmsManager
-  extends BroadcastReceiver
-  implements ISmsManager
-{
-  public final static String ACTION_SMS_SENT      = "org.mozilla.gecko.SMS_SENT";
-  public final static String ACTION_SMS_DELIVERED = "org.mozilla.gecko.SMS_DELIVERED";
-
-  /*
-   * Make sure that the following error codes are in sync with |ErrorType| in:
-   * dom/mobilemessage/interfaces/nsIMobileMessageCallback.idl
-   * The error codes are owned by the DOM.
-   */
-  public final static int kNoError               = 0;
-  public final static int kNoSignalError         = 1;
-  public final static int kNotFoundError         = 2;
-  public final static int kUnknownError          = 3;
-  public final static int kInternalError         = 4;
-  public final static int kNoSimCardError        = 5;
-  public final static int kRadioDisabledError    = 6;
-  public final static int kInvalidAddressError   = 7;
-  public final static int kFdnCheckError         = 8;
-  public final static int kNonActiveSimCardError = 9;
-  public final static int kStorageFullError      = 10;
-  public final static int kSimNotMatchedError    = 11;
-  public final static int kNetworkProblemsError = 12;
-  public final static int kGeneralProblemsError = 13;
-  public final static int kServiceNotAvailableError      = 14;
-  public final static int kMessageTooLongForNetworkError = 15;
-  public final static int kServiceNotSupportedError      = 16;
-  public final static int kRetryRequiredError   = 17;
-
-  private final static int kMaxMessageSize    = 160;
-
-  private final static Uri kSmsContentUri        = Telephony.Sms.Inbox.CONTENT_URI;
-  private final static Uri kSmsSentContentUri    = Telephony.Sms.Sent.CONTENT_URI;
-  private final static Uri kSmsThreadsContentUri = Telephony.Sms.Conversations.CONTENT_URI;
-
-  private final static int kSmsTypeInbox      = Telephony.Sms.MESSAGE_TYPE_INBOX;
-  private final static int kSmsTypeSentbox    = Telephony.Sms.MESSAGE_TYPE_SENT;
-
-  /*
-   * Keep the following state codes in syng with |DeliveryState| in:
-   * dom/mobilemessage/Types.h
-   */
-  private final static int kDeliveryStateSent          = 0;
-  private final static int kDeliveryStateReceived      = 1;
-  private final static int kDeliveryStateSending       = 2;
-  private final static int kDeliveryStateError         = 3;
-  private final static int kDeliveryStateUnknown       = 4;
-  private final static int kDeliveryStateNotDownloaded = 5;
-  private final static int kDeliveryStateEndGuard      = 6;
-
-  /*
-   * Keep the following status codes in sync with |DeliveryStatus| in:
-   * dom/mobilemessage/Types.h
-   */
-  private final static int kDeliveryStatusNotApplicable = 0;
-  private final static int kDeliveryStatusSuccess       = 1;
-  private final static int kDeliveryStatusPending       = 2;
-  private final static int kDeliveryStatusError         = 3;
-
-  /*
-   * Keep the following values in sync with |MessageClass| in:
-   * dom/mobilemessage/Types.h
-   */
-  private final static int kMessageClassNormal  = 0;
-  private final static int kMessageClassClass0  = 1;
-  private final static int kMessageClassClass1  = 2;
-  private final static int kMessageClassClass2  = 3;
-  private final static int kMessageClassClass3  = 4;
-
-  private final static String[] kRequiredMessageRows = { "_id", "address", "body", "date", "type", "status", "read", "thread_id" };
-  private final static String[] kRequiredMessageRowsForThread = { "_id", "address", "body", "read", "subject", "date" };
-  private final static String[] kThreadProjection = { "thread_id" };
-
-  // Used to generate monotonically increasing GUIDs.
-  private static final AtomicInteger pendingIntentGuid = new AtomicInteger(Integer.MIN_VALUE);
-
-  // The maximum value of a 32 bit signed integer. Used to enforce a limit on ids.
-  private static final long UNSIGNED_INTEGER_MAX_VALUE = Integer.MAX_VALUE * 2L + 1L;
-
-  public GeckoSmsManager() {
-    if (SmsIOThread.getInstance().getState() == Thread.State.NEW) {
-      SmsIOThread.getInstance().start();
-    }
-  }
-
-  private boolean isDefaultSmsApp(Context context) {
-    String myPackageName = context.getPackageName();
-    return Telephony.Sms.getDefaultSmsPackage(context).equals(myPackageName);
-  }
-
-  @Override
-  public void start() {
-    IntentFilter smsFilter = new IntentFilter();
-    smsFilter.addAction(Telephony.Sms.Intents.SMS_RECEIVED_ACTION);
-    smsFilter.addAction(ACTION_SMS_SENT);
-    smsFilter.addAction(ACTION_SMS_DELIVERED);
-
-    GeckoAppShell.getContext().registerReceiver(this, smsFilter);
-  }
-
-  /**
-   * Build up the SMS message body from the SmsMessage array of received SMS
-   *
-   * @param msgs The SmsMessage array of the received SMS
-   * @return The text message body
-   */
-  private static String buildMessageBodyFromPdus(SmsMessage[] msgs) {
-    if (msgs.length == 1) {
-      // There is only one part, so grab the body directly.
-      return replaceFormFeeds(msgs[0].getDisplayMessageBody());
-    } else {
-      // Build up the body from the parts.
-      StringBuilder body = new StringBuilder();
-      for (SmsMessage msg : msgs) {
-        // getDisplayMessageBody() can NPE if mWrappedMessage inside is null.
-        body.append(msg.getDisplayMessageBody());
-      }
-      return replaceFormFeeds(body.toString());
-    }
-  }
-
-  // Some providers send formfeeds in their messages. Convert those formfeeds to newlines.
-  private static String replaceFormFeeds(String s) {
-    return s == null ? "" : s.replace('\f', '\n');
-  }
-
-  @Override
-  public void onReceive(Context context, Intent intent) {
-    if (intent.getAction().equals(Telephony.Sms.Intents.SMS_DELIVER_ACTION) ||
-        intent.getAction().equals(Telephony.Sms.Intents.SMS_RECEIVED_ACTION)) {
-      // If we're the default SMS, ignore SMS_RECEIVED intents since we'll handle
-      // the SMS_DELIVER intent instead.
-      if (isDefaultSmsApp(GeckoAppShell.getContext()) && intent.getAction().equals(Telephony.Sms.Intents.SMS_RECEIVED_ACTION)) {
-        return;
-      }
-
-      // TODO: Try to find the receiver number to be able to populate
-      //       SmsMessage.receiver.
-      SmsMessage[] messages = Telephony.Sms.Intents.getMessagesFromIntent(intent);
-      if (messages == null || messages.length == 0) {
-        return;
-      }
-
-      SmsMessage sms = messages[0];
-      String body = buildMessageBodyFromPdus(messages);
-      long timestamp = System.currentTimeMillis();
-
-      int id = 0;
-      // We only need to save the message if we're the default SMS app
-      if (intent.getAction().equals(Telephony.Sms.Intents.SMS_DELIVER_ACTION)) {
-        id = saveReceivedMessage(context,
-                               sms.getDisplayOriginatingAddress(),
-                               body,
-                               sms.getTimestampMillis(),
-                               timestamp,
-                               sms.getPseudoSubject());
-      }
-
-      notifySmsReceived(id,
-                        sms.getDisplayOriginatingAddress(),
-                        body,
-                        getGeckoMessageClass(sms.getMessageClass()),
-                        sms.getTimestampMillis(),
-                        timestamp);
-
-      return;
-    }
-
-    if (intent.getAction().equals(ACTION_SMS_SENT) ||
-        intent.getAction().equals(ACTION_SMS_DELIVERED)) {
-      Bundle bundle = intent.getExtras();
-
-      if (bundle == null || !bundle.containsKey("envelopeId") ||
-          !bundle.containsKey("number") || !bundle.containsKey("message") ||
-          !bundle.containsKey("requestId")) {
-        Log.e("GeckoSmsManager", "Got an invalid ACTION_SMS_SENT/ACTION_SMS_DELIVERED!");
-        return;
-      }
-
-      int envelopeId = bundle.getInt("envelopeId");
-      Postman postman = Postman.getInstance();
-
-      Envelope envelope = postman.getEnvelope(envelopeId);
-      if (envelope == null) {
-        Log.e("GeckoSmsManager", "Got an invalid envelope id (or Envelope has been destroyed)!");
-        return;
-      }
-
-      Envelope.SubParts part = intent.getAction().equals(ACTION_SMS_SENT)
-                                 ? Envelope.SubParts.SENT_PART
-                                 : Envelope.SubParts.DELIVERED_PART;
-      envelope.decreaseRemainingParts(part);
-
-
-      if (getResultCode() != Activity.RESULT_OK) {
-        switch (getResultCode()) {
-          case SmsManager.RESULT_ERROR_NULL_PDU:
-            envelope.setError(kInternalError);
-            break;
-          case SmsManager.RESULT_ERROR_NO_SERVICE:
-          case SmsManager.RESULT_ERROR_RADIO_OFF:
-            envelope.setError(kNoSignalError);
-            break;
-          case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
-          default:
-            envelope.setError(kUnknownError);
-            break;
-        }
-        envelope.markAsFailed(part);
-        Log.i("GeckoSmsManager", "SMS part sending failed!");
-      }
-
-      if (envelope.arePartsRemaining(part)) {
-        return;
-      }
-
-      if (envelope.isFailing(part)) {
-        if (part == Envelope.SubParts.SENT_PART) {
-          if (bundle.getBoolean("shouldNotify")) {
-            notifySmsSendFailed(envelope.getError(), bundle.getInt("requestId"));
-          }
-          Log.i("GeckoSmsManager", "SMS sending failed!");
-        } else {
-          notifySmsDelivery(envelope.getMessageId(),
-                            kDeliveryStatusError,
-                            bundle.getString("number"),
-                            bundle.getString("message"),
-                            envelope.getMessageTimestamp());
-          Log.i("GeckoSmsManager", "SMS delivery failed!");
-        }
-      } else {
-        if (part == Envelope.SubParts.SENT_PART) {
-          String number = bundle.getString("number");
-          String message = bundle.getString("message");
-          long timestamp = System.currentTimeMillis();
-
-          // save message only if we're default SMS app, otherwise sendTextMessage does it for us
-          int id = 0;
-          if (isDefaultSmsApp(GeckoAppShell.getContext())) {
-            id = saveSentMessage(number, message, timestamp);
-          }
-
-          if (bundle.getBoolean("shouldNotify")) {
-            notifySmsSent(id, number, message, timestamp, bundle.getInt("requestId"));
-          }
-
-          envelope.setMessageId(id);
-          envelope.setMessageTimestamp(timestamp);
-
-          Log.i("GeckoSmsManager", "SMS sending was successful!");
-        } else {
-          Uri id = ContentUris.withAppendedId(kSmsContentUri, envelope.getMessageId());
-          updateMessageStatus(id, Telephony.Sms.STATUS_COMPLETE);
-
-          notifySmsDelivery(envelope.getMessageId(),
-                            kDeliveryStatusSuccess,
-                            bundle.getString("number"),
-                            bundle.getString("message"),
-                            envelope.getMessageTimestamp());
-          Log.i("GeckoSmsManager", "SMS successfully delivered!");
-        }
-      }
-
-      // Destroy the envelope object only if the SMS has been sent and delivered.
-      if (!envelope.arePartsRemaining(Envelope.SubParts.SENT_PART) &&
-          !envelope.arePartsRemaining(Envelope.SubParts.DELIVERED_PART)) {
-        postman.destroyEnvelope(envelopeId);
-      }
-    }
-  }
-
-  @Override
-  public void send(String aNumber, String aMessage, int aRequestId, boolean aShouldNotify) {
-    int envelopeId = Postman.kUnknownEnvelopeId;
-
-    try {
-      SmsManager sm = SmsManager.getDefault();
-
-      Intent sentIntent = new Intent(GeckoAppShell.getContext(), GeckoSmsManager.class);
-      sentIntent.setAction(ACTION_SMS_SENT);
-      Intent deliveredIntent = new Intent(GeckoAppShell.getContext(), GeckoSmsManager.class);
-      deliveredIntent.setAction(ACTION_SMS_DELIVERED);
-
-      Bundle bundle = new Bundle();
-      bundle.putString("number", aNumber);
-      bundle.putString("message", aMessage);
-      bundle.putInt("requestId", aRequestId);
-      bundle.putBoolean("shouldNotify", aShouldNotify);
-
-      if (aMessage.length() <= kMaxMessageSize) {
-        envelopeId = Postman.getInstance().createEnvelope(1);
-        bundle.putInt("envelopeId", envelopeId);
-
-        sentIntent.putExtras(bundle);
-        deliveredIntent.putExtras(bundle);
-
-        /*
-         * There are a few things to know about getBroadcast and pending intents:
-         * - the pending intents are in a shared pool maintained by the system;
-         * - each pending intent is identified by a token;
-         * - when a new pending intent is created, if it has the same token as
-         *   another intent in the pool, one of them has to be removed.
-         *
-         * To prevent having a hard time because of this situation, we give a
-         * unique id to all pending intents we are creating. This unique id is
-         * generated by GetPendingIntentUID().
-         */
-        PendingIntent sentPendingIntent =
-          PendingIntent.getBroadcast(GeckoAppShell.getContext(),
-                                     pendingIntentGuid.incrementAndGet(), sentIntent,
-                                     PendingIntent.FLAG_CANCEL_CURRENT);
-
-        PendingIntent deliveredPendingIntent =
-          PendingIntent.getBroadcast(GeckoAppShell.getContext(),
-                                     pendingIntentGuid.incrementAndGet(), deliveredIntent,
-                                     PendingIntent.FLAG_CANCEL_CURRENT);
-
-        sm.sendTextMessage(aNumber, null, aMessage,
-                           sentPendingIntent, deliveredPendingIntent);
-      } else {
-        ArrayList<String> parts = sm.divideMessage(aMessage);
-        envelopeId = Postman.getInstance().createEnvelope(parts.size());
-        bundle.putInt("envelopeId", envelopeId);
-
-        sentIntent.putExtras(bundle);
-        deliveredIntent.putExtras(bundle);
-
-        ArrayList<PendingIntent> sentPendingIntents =
-          new ArrayList<PendingIntent>(parts.size());
-        ArrayList<PendingIntent> deliveredPendingIntents =
-          new ArrayList<PendingIntent>(parts.size());
-
-        for (int i = 0; i < parts.size(); ++i) {
-          sentPendingIntents.add(
-            PendingIntent.getBroadcast(GeckoAppShell.getContext(),
-                                       pendingIntentGuid.incrementAndGet(), sentIntent,
-                                       PendingIntent.FLAG_CANCEL_CURRENT)
-          );
-
-          deliveredPendingIntents.add(
-            PendingIntent.getBroadcast(GeckoAppShell.getContext(),
-                                       pendingIntentGuid.incrementAndGet(), deliveredIntent,
-                                       PendingIntent.FLAG_CANCEL_CURRENT)
-          );
-        }
-
-        sm.sendMultipartTextMessage(aNumber, null, parts, sentPendingIntents,
-                                    deliveredPendingIntents);
-      }
-    } catch (Exception e) {
-      Log.e("GeckoSmsManager", "Failed to send an SMS: ", e);
-
-      if (envelopeId != Postman.kUnknownEnvelopeId) {
-        Postman.getInstance().destroyEnvelope(envelopeId);
-      }
-
-      notifySmsSendFailed(kUnknownError, aRequestId);
-    }
-  }
-
-  public int saveSentMessage(String aRecipient, String aBody, long aDate) {
-    try {
-      ContentValues values = new ContentValues();
-      values.put(Telephony.Sms.ADDRESS, aRecipient);
-      values.put(Telephony.Sms.BODY, aBody);
-      values.put(Telephony.Sms.DATE, aDate);
-      // Always 'PENDING' because we always request status report.
-      values.put(Telephony.Sms.STATUS, Telephony.Sms.STATUS_PENDING);
-      values.put(Telephony.Sms.SEEN, 0);
-      values.put(Telephony.Sms.READ, 0);
-
-      ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
-      Uri uri = cr.insert(kSmsSentContentUri, values);
-
-      long id = ContentUris.parseId(uri);
-
-      // The DOM API takes a 32bits unsigned int for the id. It's unlikely that
-      // we happen to need more than that but it doesn't cost to check.
-      if (id > UNSIGNED_INTEGER_MAX_VALUE) {
-        throw new IdTooHighException();
-      }
-
-      return (int)id;
-    } catch (IdTooHighException e) {
-      Log.e("GeckoSmsManager", "The id we received is higher than the higher allowed value.");
-      return -1;
-    } catch (Exception e) {
-      Log.e("GeckoSmsManager", "Something went wrong when trying to write a sent message", e);
-      return -1;
-    }
-  }
-
-  public void updateMessageStatus(Uri aId, int aStatus) {
-    ContentValues values = new ContentValues(1);
-    values.put(Telephony.Sms.STATUS, aStatus);
-
-    ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
-    if (cr.update(aId, values, null, null) != 1) {
-      Log.i("GeckoSmsManager", "Failed to update message status!");
-    }
-  }
-
-  public int saveReceivedMessage(Context aContext, String aSender, String aBody, long aDateSent, long aDate, String aSubject) {
-    ContentValues values = new ContentValues();
-    values.put(Telephony.Sms.Inbox.ADDRESS, aSender);
-    values.put(Telephony.Sms.Inbox.BODY, aBody);
-    values.put(Telephony.Sms.Inbox.DATE_SENT, aDateSent);
-    values.put(Telephony.Sms.Inbox.DATE, aDate);
-    values.put(Telephony.Sms.Inbox.STATUS, Telephony.Sms.STATUS_COMPLETE);
-    values.put(Telephony.Sms.Inbox.READ, 0);
-    values.put(Telephony.Sms.Inbox.SEEN, 0);
-
-    ContentResolver cr = aContext.getContentResolver();
-    Uri uri = cr.insert(kSmsContentUri, values);
-
-    long id = ContentUris.parseId(uri);
-
-    // The DOM API takes a 32bits unsigned int for the id. It's unlikely that
-    // we happen to need more than that but it doesn't cost to check.
-    if (id > UNSIGNED_INTEGER_MAX_VALUE) {
-      Log.i("GeckoSmsManager", "The id we received is higher than the higher allowed value.");
-      return -1;
-    }
-
-    return (int)id;
-  }
-
-  @Override
-  public void getMessage(int aMessageId, int aRequestId) {
-    class GetMessageRunnable implements Runnable {
-      private final int mMessageId;
-      private final int mRequestId;
-
-      GetMessageRunnable(int aMessageId, int aRequestId) {
-        mMessageId = aMessageId;
-        mRequestId = aRequestId;
-      }
-
-      @Override
-      public void run() {
-        Cursor cursor = null;
-
-        try {
-          ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
-          Uri message = ContentUris.withAppendedId(kSmsContentUri, mMessageId);
-
-          cursor = cr.query(message, kRequiredMessageRows, null, null, null);
-          if (cursor == null || cursor.getCount() == 0) {
-            throw new NotFoundException();
-          }
-
-          if (cursor.getCount() != 1) {
-            throw new TooManyResultsException();
-          }
-
-          cursor.moveToFirst();
-
-          if (cursor.getInt(cursor.getColumnIndex("_id")) != mMessageId) {
-            throw new UnmatchingIdException();
-          }
-
-          int type = cursor.getInt(cursor.getColumnIndex("type"));
-          int deliveryStatus;
-          String sender = "";
-          String receiver = "";
-
-          if (type == kSmsTypeInbox) {
-            deliveryStatus = kDeliveryStatusSuccess;
-            sender = cursor.getString(cursor.getColumnIndex("address"));
-          } else if (type == kSmsTypeSentbox) {
-            deliveryStatus = getGeckoDeliveryStatus(cursor.getInt(cursor.getColumnIndex("status")));
-            receiver = cursor.getString(cursor.getColumnIndex("address"));
-          } else {
-            throw new InvalidTypeException();
-          }
-
-          boolean read = cursor.getInt(cursor.getColumnIndex("read")) != 0;
-
-          notifyGetSms(cursor.getInt(cursor.getColumnIndex("_id")),
-                       deliveryStatus,
-                       receiver, sender,
-                       cursor.getString(cursor.getColumnIndex("body")),
-                       cursor.getLong(cursor.getColumnIndex("date")),
-                       read,
-                       mRequestId);
-        } catch (NotFoundException e) {
-          Log.i("GeckoSmsManager", "Message id " + mMessageId + " not found");
-          notifyGetSmsFailed(kNotFoundError, mRequestId);
-        } catch (UnmatchingIdException e) {
-          Log.e("GeckoSmsManager", "Requested message id (" + mMessageId +
-                                   ") is different from the one we got.");
-          notifyGetSmsFailed(kUnknownError, mRequestId);
-        } catch (TooManyResultsException e) {
-          Log.e("GeckoSmsManager", "Get too many results for id " + mMessageId);
-          notifyGetSmsFailed(kUnknownError, mRequestId);
-        } catch (InvalidTypeException e) {
-          Log.i("GeckoSmsManager", "Message has an invalid type, we ignore it.");
-          notifyGetSmsFailed(kNotFoundError, mRequestId);
-        } catch (Exception e) {
-          Log.e("GeckoSmsManager", "Error while trying to get message", e);
-          notifyGetSmsFailed(kUnknownError, mRequestId);
-        } finally {
-          if (cursor != null) {
-            cursor.close();
-          }
-        }
-      }
-    }
-
-    if (!SmsIOThread.getInstance().execute(new GetMessageRunnable(aMessageId, aRequestId))) {
-      Log.e("GeckoSmsManager", "Failed to add GetMessageRunnable to the SmsIOThread");
-      notifyGetSmsFailed(kUnknownError, aRequestId);
-    }
-  }
-
-  @Override
-  public void deleteMessage(int aMessageId, int aRequestId) {
-    class DeleteMessageRunnable implements Runnable {
-      private final int mMessageId;
-      private final int mRequestId;
-
-      DeleteMessageRunnable(int aMessageId, int aRequestId) {
-        mMessageId = aMessageId;
-        mRequestId = aRequestId;
-      }
-
-      @Override
-      public void run() {
-        try {
-          ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
-          Uri message = ContentUris.withAppendedId(kSmsContentUri, mMessageId);
-
-          int count = cr.delete(message, null, null);
-
-          if (count > 1) {
-            throw new TooManyResultsException();
-          }
-
-          notifySmsDeleted(count == 1, mRequestId);
-        } catch (TooManyResultsException e) {
-          Log.e("GeckoSmsManager", "Delete more than one message?", e);
-          notifySmsDeleteFailed(kUnknownError, mRequestId);
-        } catch (Exception e) {
-          Log.e("GeckoSmsManager", "Error while trying to delete a message", e);
-          notifySmsDeleteFailed(kUnknownError, mRequestId);
-        }
-      }
-    }
-
-    if (!SmsIOThread.getInstance().execute(new DeleteMessageRunnable(aMessageId, aRequestId))) {
-      Log.e("GeckoSmsManager", "Failed to add GetMessageRunnable to the SmsIOThread");
-      notifySmsDeleteFailed(kUnknownError, aRequestId);
-    }
-  }
-
-  private void getMessageFromCursorAndNotify(Cursor aCursor, int aRequestId) throws Exception {
-    int type = aCursor.getInt(aCursor.getColumnIndex("type"));
-    int deliveryStatus = kDeliveryStateUnknown;
-    String sender = "";
-    String receiver = "";
-
-    if (type == kSmsTypeInbox) {
-      deliveryStatus = kDeliveryStatusSuccess;
-      sender = aCursor.getString(aCursor.getColumnIndex("address"));
-    } else if (type == kSmsTypeSentbox) {
-      deliveryStatus = getGeckoDeliveryStatus(aCursor.getInt(aCursor.getColumnIndex("status")));
-      receiver = aCursor.getString(aCursor.getColumnIndex("address"));
-    } else {
-      throw new Exception("Shouldn't ever get a message here that's not from inbox or sentbox");
-    }
-
-    boolean read = aCursor.getInt(aCursor.getColumnIndex("read")) != 0;
-
-    notifyMessageCursorResult(aCursor.getInt(aCursor.getColumnIndex("_id")),
-                              deliveryStatus,
-                              receiver, sender,
-                              aCursor.getString(aCursor.getColumnIndex("body")),
-                              aCursor.getLong(aCursor.getColumnIndex("date")),
-                              aCursor.getLong(aCursor.getColumnIndex("thread_id")),
-                              read,
-                              aRequestId);
-  }
-
-  @Override
-  public void markMessageRead(int aMessageId, boolean aValue, boolean aSendReadReport, int aRequestId) {
-    class MarkMessageReadRunnable implements Runnable {
-      private final int mMessageId;
-      private final boolean mValue;
-      private final int mRequestId;
-
-      MarkMessageReadRunnable(int aMessageId, boolean aValue, int aRequestId) {
-        mMessageId = aMessageId;
-        mValue = aValue;
-        mRequestId = aRequestId;
-      }
-
-      @Override
-      public void run() {
-        try {
-          ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
-          Uri message = ContentUris.withAppendedId(kSmsContentUri, mMessageId);
-
-          ContentValues updatedProps = new ContentValues();
-          updatedProps.put("read", mValue);
-
-          int count = cr.update(message, updatedProps, null, null);
-
-          notifySmsMarkedAsRead(count == 1, mRequestId);
-        } catch (Exception e) {
-          Log.e("GeckoSmsManager", "Error while trying to mark message as read: " + e);
-          notifySmsMarkAsReadFailed(kUnknownError, mRequestId);
-        }
-      }
-    }
-
-    if (aSendReadReport == true) {
-      Log.w("GeckoSmsManager", "Android SmsManager doesn't suport read receipts for SMS.");
-    }
-
-    if (!SmsIOThread.getInstance().execute(new MarkMessageReadRunnable(aMessageId, aValue, aRequestId))) {
-      Log.e("GeckoSmsManager", "Failed to add MarkMessageReadRunnable to the SmsIOThread");
-      notifySmsMarkAsReadFailed(kUnknownError, aRequestId);
-    }
-  }
-
-  @Override
-  public void createMessageCursor(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, String aDelivery, boolean aHasRead, boolean aRead, boolean aHasThreadId, long aThreadId, boolean aReverse, int aRequestId) {
-    class CreateMessageCursorRunnable implements Runnable {
-      private final long     mStartDate;
-      private final long     mEndDate;
-      private final String[] mNumbers;
-      private final int      mNumbersCount;
-      private final String   mDelivery;
-      private final boolean  mHasThreadId;
-      private final long     mThreadId;
-      private final boolean  mReverse;
-      private final int      mRequestId;
-
-      CreateMessageCursorRunnable(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, String aDelivery, boolean aHasRead, boolean aRead, boolean aHasThreadId, long aThreadId, boolean aReverse, int aRequestId) {
-        mStartDate = aStartDate;
-        mEndDate = aEndDate;
-        mNumbers = aNumbers;
-        mNumbersCount = aNumbersCount;
-        mDelivery = aDelivery;
-        mHasThreadId = aHasThreadId;
-        mThreadId = aThreadId;
-        mReverse = aReverse;
-        mRequestId = aRequestId;
-      }
-
-      @Override
-      public void run() {
-        Cursor cursor = null;
-        boolean closeCursor = true;
-
-        try {
-          StringBuilder restrictions = new StringBuilder();
-          Formatter formatter = new Formatter(restrictions);
-
-          if (mStartDate >= 0) {
-            formatter.format("date >= '%d' AND ", mStartDate);
-          }
-
-          if (mEndDate >= 0) {
-            formatter.format("date <= '%d' AND ", mEndDate);
-          }
-
-          if (mNumbersCount > 0) {
-            formatter.format("address IN ('%s'", mNumbers[0]);
-
-            for (int i = 1; i < mNumbersCount; ++i) {
-              formatter.format(", '%s'", mNumbers[i]);
-            }
-
-            formatter.format(") AND ");
-          }
-
-          if (mDelivery == null || mDelivery.isEmpty()) {
-            formatter.format("type IN ('%d', '%d') AND ", kSmsTypeSentbox, kSmsTypeInbox);
-          } else if (mDelivery.equals("sent")) {
-            formatter.format("type = '%d' AND ", kSmsTypeSentbox);
-          } else if (mDelivery.equals("received")) {
-            formatter.format("type = '%d' AND ", kSmsTypeInbox);
-          } else {
-            throw new Exception("Unexpected delivery state: " + mDelivery);
-          }
-
-          if (mHasThreadId) {
-            formatter.format("thread_id = '%d' AND ", mThreadId);
-          }
-
-          // Final 'AND 1' is a no-op so we don't have to special case the last
-          // condition.
-          formatter.format("1");
-
-          ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
-          cursor = cr.query(kSmsContentUri,
-                            kRequiredMessageRows,
-                            restrictions.toString(),
-                            null,
-                            mReverse ? "date DESC" : "date ASC");
-
-          if (cursor.getCount() == 0) {
-            notifyCursorDone(mRequestId);
-            return;
-          }
-
-          MessagesListManager.getInstance().add(mRequestId, cursor);
-
-          cursor.moveToFirst();
-          getMessageFromCursorAndNotify(cursor, mRequestId);
-          closeCursor = false;
-        } catch (Exception e) {
-          Log.e("GeckoSmsManager", "Error while trying to create a message list cursor", e);
-          notifyCursorError(kUnknownError, mRequestId);
-        } finally {
-          if (closeCursor && cursor != null) {
-            cursor.close();
-          }
-        }
-      }
-    }
-
-    if (!SmsIOThread.getInstance().execute(new CreateMessageCursorRunnable(aStartDate, aEndDate, aNumbers, aNumbersCount, aDelivery, aHasRead, aRead, aHasThreadId, aThreadId, aReverse, aRequestId))) {
-      Log.e("GeckoSmsManager", "Failed to add CreateMessageCursorRunnable to the SmsIOThread");
-      notifyCursorError(kUnknownError, aRequestId);
-    }
-  }
-
-  @Override
-  public void getNextMessage(int aRequestId) {
-    class GetNextMessageRunnable implements Runnable {
-      private final int mRequestId;
-
-      GetNextMessageRunnable(int aRequestId) {
-        mRequestId = aRequestId;
-      }
-
-      @Override
-      public void run() {
-        Cursor cursor = null;
-        boolean closeCursor = true;
-        try {
-          cursor = MessagesListManager.getInstance().get(mRequestId);
-
-          if (!cursor.moveToNext()) {
-            MessagesListManager.getInstance().remove(mRequestId);
-            notifyCursorDone(mRequestId);
-            return;
-          }
-
-          getMessageFromCursorAndNotify(cursor, mRequestId);
-          closeCursor = false;
-        } catch (Exception e) {
-          Log.e("GeckoSmsManager", "Error while trying to get the next message of a list", e);
-          notifyCursorError(kUnknownError, mRequestId);
-        } finally {
-          if (closeCursor) {
-            cursor.close();
-          }
-        }
-      }
-    }
-
-    if (!SmsIOThread.getInstance().execute(new GetNextMessageRunnable(aRequestId))) {
-      Log.e("GeckoSmsManager", "Failed to add GetNextMessageRunnable to the SmsIOThread");
-      notifyCursorError(kUnknownError, aRequestId);
-    }
-  }
-
-  private void getThreadFromCursorAndNotify(Cursor aCursor, int aRequestId) throws Exception {
-    ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
-
-    long id = aCursor.getLong(aCursor.getColumnIndex("thread_id"));
-    Cursor msgCursor = cr.query(kSmsContentUri,
-                                kRequiredMessageRowsForThread,
-                                "thread_id = " + id,
-                                null,
-                                "date DESC");
-
-    if (msgCursor == null || msgCursor.getCount() == 0) {
-      throw new Exception("Empty thread " + id);
-    }
-
-    msgCursor.moveToFirst();
-
-    String lastMessageSubject = msgCursor.getString(msgCursor.getColumnIndex("subject"));
-    String body = msgCursor.getString(msgCursor.getColumnIndex("body"));
-    long timestamp = msgCursor.getLong(msgCursor.getColumnIndex("date"));
-
-    HashSet<String> participants = new HashSet<>();
-    do {
-      String p = msgCursor.getString(msgCursor.getColumnIndex("address"));
-      participants.add(p);
-    } while (msgCursor.moveToNext());
-
-    //TODO: handle MMS
-    String lastMessageType = "sms";
-
-    msgCursor = cr.query(kSmsContentUri,
-                         kRequiredMessageRowsForThread,
-                         "thread_id = " + id + " AND read = 0",
-                         null,
-                         null);
-
-    if (msgCursor == null) {
-      Log.e("GeckoSmsManager", "We should never get here, should have errored before");
-      throw new Exception("Empty thread " + id);
-    }
-
-    long unreadCount = msgCursor.getCount();
-
-    notifyThreadCursorResult(id, lastMessageSubject, body, unreadCount, participants.toArray(), timestamp, lastMessageType, aRequestId);
-  }
-
-  @Override
-  public void createThreadCursor(int aRequestId) {
-    class CreateThreadCursorRunnable implements Runnable {
-      private final int mRequestId;
-
-      CreateThreadCursorRunnable(int aRequestId) {
-        mRequestId = aRequestId;
-      }
-
-      @Override
-      public void run() {
-        try {
-          ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
-          Cursor cursor = cr.query(kSmsThreadsContentUri,
-                                   kThreadProjection,
-                                   null,
-                                   null,
-                                   "date DESC");
-          if (cursor == null || !cursor.moveToFirst()) {
-            notifyCursorDone(mRequestId);
-            return;
-          }
-
-          MessagesListManager.getInstance().add(mRequestId, cursor);
-
-          getThreadFromCursorAndNotify(cursor, mRequestId);
-        } catch (Exception e) {
-          Log.e("GeckoSmsManager", "Error while trying to create thread cursor: " + e);
-          notifyCursorError(kUnknownError, mRequestId);
-        }
-      }
-    }
-
-    if (!SmsIOThread.getInstance().execute(new CreateThreadCursorRunnable(aRequestId))) {
-      Log.e("GeckoSmsManager", "Failed to add CreateThreadCursorRunnable to the SmsIOThread");
-      notifyCursorError(kUnknownError, aRequestId);
-    }
-  }
-
-  @Override
-  public void getNextThread(int aRequestId) {
-    class GetNextThreadRunnable implements Runnable {
-      private final int mRequestId;
-
-      GetNextThreadRunnable(int aRequestId) {
-        mRequestId = aRequestId;
-      }
-
-      @Override
-      public void run() {
-        try {
-          Cursor cursor = MessagesListManager.getInstance().get(mRequestId);
-
-          if (!cursor.moveToNext()) {
-            MessagesListManager.getInstance().remove(mRequestId);
-            notifyCursorDone(mRequestId);
-            return;
-          }
-
-          getThreadFromCursorAndNotify(cursor, mRequestId);
-        } catch (Exception e) {
-          Log.e("GeckoSmsManager", "Error while trying to create thread cursor: " + e);
-          notifyCursorError(kUnknownError, mRequestId);
-        }
-      }
-    }
-
-    if (!SmsIOThread.getInstance().execute(new GetNextThreadRunnable(aRequestId))) {
-      Log.e("GeckoSmsManager", "Failed to add GetNextThreadRunnable to the SmsIOThread");
-      notifyCursorError(kUnknownError, aRequestId);
-    }
-  }
-
-  @Override
-  public void stop() {
-    GeckoAppShell.getContext().unregisterReceiver(this);
-  }
-
-  @Override
-  public void shutdown() {
-    SmsIOThread.getInstance().interrupt();
-    MessagesListManager.getInstance().clear();
-  }
-
-  private int getGeckoDeliveryStatus(int aDeliveryStatus) {
-    if (aDeliveryStatus == Telephony.Sms.STATUS_NONE) {
-      return kDeliveryStatusNotApplicable;
-    }
-    if (aDeliveryStatus >= Telephony.Sms.STATUS_FAILED) {
-      return kDeliveryStatusError;
-    }
-    if (aDeliveryStatus >= Telephony.Sms.STATUS_PENDING) {
-      return kDeliveryStatusPending;
-    }
-    return kDeliveryStatusSuccess;
-  }
-
-  private int getGeckoMessageClass(MessageClass aMessageClass) {
-    switch (aMessageClass) {
-      case CLASS_0:
-        return kMessageClassClass0;
-      case CLASS_1:
-        return kMessageClassClass1;
-      case CLASS_2:
-        return kMessageClassClass2;
-      case CLASS_3:
-        return kMessageClassClass3;
-      default:
-        return kMessageClassNormal;
-    }
-  }
-
-  static class IdTooHighException extends Exception {
-    private static final long serialVersionUID = 29935575131092050L;
-  }
-
-  static class InvalidTypeException extends Exception {
-    private static final long serialVersionUID = 47436856832535912L;
-  }
-
-  static class NotFoundException extends Exception {
-    private static final long serialVersionUID = 1940676816633984L;
-  }
-
-  static class TooManyResultsException extends Exception {
-    private static final long serialVersionUID = 51883196784325305L;
-  }
-
-  static class UnmatchingIdException extends Exception {
-    private static final long serialVersionUID = 158467542575633280L;
-  }
-
-  @WrapForJNI
-  public static native void notifySmsReceived(int aId, String aSender, String aBody, int aMessageClass, long aSentTimestamp, long aTimestamp);
-  @WrapForJNI
-  private static native void notifySmsSent(int aId, String aReceiver, String aBody, long aTimestamp, int aRequestId);
-  @WrapForJNI
-  private static native void notifySmsDelivery(int aId, int aDeliveryStatus, String aReceiver, String aBody, long aTimestamp);
-  @WrapForJNI
-  private static native void notifySmsSendFailed(int aError, int aRequestId);
-  @WrapForJNI
-  private static native void notifyGetSms(int aId, int aDeliveryStatus, String aReceiver, String aSender, String aBody, long aTimestamp, boolean aRead, int aRequestId);
-  @WrapForJNI
-  private static native void notifyGetSmsFailed(int aError, int aRequestId);
-  @WrapForJNI
-  private static native void notifySmsDeleted(boolean aDeleted, int aRequestId);
-  @WrapForJNI
-  private static native void notifySmsDeleteFailed(int aError, int aRequestId);
-  @WrapForJNI
-  private static native void notifySmsMarkedAsRead(boolean aMarkedAsRead, int aRequestId);
-  @WrapForJNI
-  private static native void notifySmsMarkAsReadFailed(int aError, int aRequestId);
-  @WrapForJNI
-  private static native void notifyCursorError(int aError, int aRequestId);
-  @WrapForJNI
-  private static native void notifyThreadCursorResult(long aId, String aLastMessageSubject, String aBody, long aUnreadCount, Object[] aParticipants, long aTimestamp, String aLastMessageType, int aRequestId);
-  @WrapForJNI
-  private static native void notifyMessageCursorResult(int aMessageId, int aDeliveryStatus, String aReceiver, String aSender, String aBody, long aTimestamp, long aThreadId, boolean aRead, int aRequestId);
-  @WrapForJNI
-  private static native void notifyCursorDone(int aRequestId);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoThread.java
+++ /dev/null
@@ -1,680 +0,0 @@
-/* -*- 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;
-
-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 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.shouldUse(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);
-
-            if (call.method == null) {
-                final GeckoEvent e = (GeckoEvent) call.target;
-                GeckoAppShell.notifyGeckoOfEvent(e);
-                e.recycle();
-                continue;
-            }
-            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 static String getTypeFromAction(String action) {
-        if (GeckoAppShell.ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
-            return "-bookmark";
-        }
-        return null;
-    }
-
-    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);
-        }
-
-        final String type = getTypeFromAction(mAction);
-        if (type != null) {
-            args.append(" ").append(type);
-        }
-
-        // 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() {
-        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() {
-                GeckoAppShell.registerJavaUiThread();
-            }
-        });
-
-        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");
-            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);
-    }
-
-    public static void addPendingEvent(final GeckoEvent e) {
-        synchronized (QUEUED_CALLS) {
-            if (isRunning()) {
-                // We may just have switched to running state.
-                GeckoAppShell.notifyGeckoOfEvent(e);
-                e.recycle();
-            } else {
-                QUEUED_CALLS.add(new QueuedCall(null, e, null, State.RUNNING));
-            }
-        }
-    }
-
-    @WrapForJNI
-    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
-    private static void setState(final State newState) {
-        ThreadUtils.assertOnGeckoThread();
-        synchronized (QUEUED_CALLS) {
-            flushQueuedNativeCallsLocked(newState);
-            sState = newState;
-        }
-    }
-
-    @WrapForJNI
-    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")
-    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")
-    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")
-    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);
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoView.java
+++ /dev/null
@@ -1,708 +0,0 @@
-/* -*- 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.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.GLController;
-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;
-
-    @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
-    private static final class Window extends JNIObject {
-        /* package */ final GLController glController = new GLController();
-
-        static native void open(Window instance, GeckoView view, GLController glController,
-                                String chromeURI,
-                                int width, int height);
-
-        @Override protected native void disposeNative();
-        native void close();
-        native void reattach(GeckoView view);
-    }
-
-    // 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];
-                }
-            };
-    }
-
-    private 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;
-        // 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);
-
-        if (stateBinder.window != null) {
-            this.window = stateBinder.window;
-        }
-        stateSaved = false;
-    }
-
-    @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();
-            final String chromeURI = getGeckoInterface().getDefaultChromeURI();
-
-            if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
-                Window.open(window, this, window.glController,
-                            chromeURI,
-                            metrics.widthPixels, metrics.heightPixels);
-            } else {
-                GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY, Window.class,
-                        "open", window, GeckoView.class, this, window.glController,
-                        String.class, chromeURI,
-                        metrics.widthPixels, metrics.heightPixels);
-            }
-        } else {
-            if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
-                window.reattach(this);
-            } else {
-                GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
-                        window, "reattach", GeckoView.class, this);
-            }
-        }
-
-        setGLController(window.glController);
-        super.onAttachedToWindow();
-    }
-
-    @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");
-        }
-    }
-
-    /* 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.");
-    }
-
-    public void connectToGecko() {
-        GeckoAppShell.notifyObservers("Viewport:Flush", null);
-    }
-
-    private void handleReady(final JSONObject message) {
-        connectToGecko();
-
-        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);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/InputConnectionListener.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/* 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();
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/InputMethods.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/* -*- 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);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/NSSBridge.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/* 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);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/NotificationClient.java
+++ /dev/null
@@ -1,212 +0,0 @@
-/* -*- 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.app.Notification;
-import android.app.PendingIntent;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.util.LinkedList;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * Client for posting notifications through a NotificationHandler.
- */
-public abstract class NotificationClient {
-    private static final String LOGTAG = "GeckoNotificationClient";
-
-    private volatile NotificationHandler mHandler;
-    private boolean mReady;
-    private final LinkedList<Runnable> mTaskQueue = new LinkedList<Runnable>();
-    private final ConcurrentHashMap<Integer, UpdateRunnable> mUpdatesMap =
-            new ConcurrentHashMap<Integer, UpdateRunnable>();
-
-    /**
-     * Runnable that is reused between update notifications.
-     *
-     * Updates happen frequently, so reusing Runnables prevents frequent dynamic allocation.
-     */
-    private class UpdateRunnable implements Runnable {
-        private long mProgress;
-        private long mProgressMax;
-        private String mAlertText;
-        final private int mNotificationID;
-
-        public UpdateRunnable(int notificationID) {
-            mNotificationID = notificationID;
-        }
-
-        public synchronized boolean updateProgress(long progress, long progressMax, String alertText) {
-            if (progress == mProgress
-                    && mProgressMax == progressMax
-                    && TextUtils.equals(mAlertText, alertText)) {
-                return false;
-            }
-
-            mProgress = progress;
-            mProgressMax = progressMax;
-            mAlertText = alertText;
-            return true;
-        }
-
-        @Override
-        public void run() {
-            long progress;
-            long progressMax;
-            String alertText;
-
-            synchronized (this) {
-                progress = mProgress;
-                progressMax = mProgressMax;
-                alertText = mAlertText;
-            }
-
-            mHandler.update(mNotificationID, progress, progressMax, alertText);
-        }
-    };
-
-    /**
-     * Adds a notification.
-     *
-     * @see NotificationHandler#add(int, String, String, String, PendingIntent, PendingIntent)
-     */
-    public synchronized void add(final int notificationID, final String aImageUrl, final String aHost,
-            final String aAlertTitle, final String aAlertText, final PendingIntent contentIntent) {
-        mTaskQueue.add(new Runnable() {
-            @Override
-            public void run() {
-                mHandler.add(notificationID, aImageUrl, aHost, aAlertTitle, aAlertText, contentIntent);
-            }
-        });
-        notify();
-
-        if (!mReady) {
-            bind();
-        }
-    }
-
-    /**
-     * Adds a notification.
-     *
-     * @see NotificationHandler#add(int, Notification)
-     */
-    public synchronized void add(final int notificationID, final Notification notification) {
-        mTaskQueue.add(new Runnable() {
-            @Override
-            public void run() {
-                mHandler.add(notificationID, notification);
-            }
-        });
-        notify();
-
-        if (!mReady) {
-            bind();
-        }
-    }
-
-    /**
-     * Updates a notification.
-     *
-     * @see NotificationHandler#update(int, long, long, String)
-     */
-    public void update(final int notificationID, final long aProgress, final long aProgressMax,
-            final String aAlertText) {
-        UpdateRunnable runnable = mUpdatesMap.get(notificationID);
-
-        if (runnable == null) {
-            runnable = new UpdateRunnable(notificationID);
-            mUpdatesMap.put(notificationID, runnable);
-        }
-
-        // If we've already posted an update with these values, there's no
-        // need to do it again.
-        if (!runnable.updateProgress(aProgress, aProgressMax, aAlertText)) {
-            return;
-        }
-
-        synchronized (this) {
-            if (mReady) {
-                mTaskQueue.add(runnable);
-                notify();
-            }
-        }
-    }
-
-    /**
-     * Removes a notification.
-     *
-     * @see NotificationHandler#remove(int)
-     */
-    public synchronized void remove(final int notificationID) {
-        mTaskQueue.add(new Runnable() {
-            @Override
-            public void run() {
-                mHandler.remove(notificationID);
-                mUpdatesMap.remove(notificationID);
-            }
-        });
-
-        // If mReady == false, we haven't added any notifications yet. That can happen if Fennec is being
-        // started in response to clicking a notification. Call bind() to ensure the task we posted above is run.
-        if (!mReady) {
-            bind();
-        }
-
-        notify();
-    }
-
-    /**
-     * Determines whether a notification is showing progress.
-     *
-     * @see NotificationHandler#isProgressStyle(int)
-     */
-    public boolean isOngoing(int notificationID) {
-        final NotificationHandler handler = mHandler;
-        return handler != null && handler.isOngoing(notificationID);
-    }
-
-    protected void bind() {
-        mReady = true;
-    }
-
-    protected void unbind() {
-        mReady = false;
-        mUpdatesMap.clear();
-    }
-
-    protected void connectHandler(NotificationHandler handler) {
-        mHandler = handler;
-        new Thread(new NotificationRunnable()).start();
-    }
-
-    private class NotificationRunnable implements Runnable {
-        @Override
-        public void run() {
-            Runnable r;
-            try {
-                while (true) {
-                    // Synchronize polls to prevent tasks from being added to the queue
-                    // during the isDone check.
-                    synchronized (NotificationClient.this) {
-                        r = mTaskQueue.poll();
-                        while (r == null) {
-                            if (mHandler.isDone()) {
-                                unbind();
-                                return;
-                            }
-                            NotificationClient.this.wait();
-                            r = mTaskQueue.poll();
-                        }
-                    }
-                    r.run();
-                }
-            } catch (InterruptedException e) {
-                Log.e(LOGTAG, "Notification task queue processing interrupted", e);
-            }
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/NotificationHandler.java
+++ /dev/null
@@ -1,193 +0,0 @@
-/* -*- 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.graphics.Bitmap;
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.support.v4.app.NotificationCompat;
-import android.support.v4.app.NotificationManagerCompat;
-import org.mozilla.gecko.gfx.BitmapUtils;
-
-import java.util.concurrent.ConcurrentHashMap;
-
-public class NotificationHandler {
-    private static String LOGTAG = "GeckoNotifHandler";
-    private final ConcurrentHashMap<Integer, Notification>
-            mNotifications = new ConcurrentHashMap<Integer, Notification>();
-    private final Context mContext;
-    private final NotificationManagerCompat mNotificationManager;
-
-    /**
-     * Notification associated with this service's foreground state.
-     *
-     * {@link android.app.Service#startForeground(int, android.app.Notification)}
-     * associates the foreground with exactly one notification from the service.
-     * To keep Fennec alive during downloads (and to make sure it can be killed
-     * once downloads are complete), we make sure that the foreground is always
-     * associated with an active progress notification if and only if at least
-     * one download is in progress.
-     */
-    private Notification mForegroundNotification;
-    private int mForegroundNotificationId;
-
-    public NotificationHandler(Context context) {
-        mContext = context;
-        mNotificationManager = NotificationManagerCompat.from(context);
-    }
-
-    /**
-     * Adds a notification.
-     *
-     * @param notificationID the unique ID of the notification
-     * @param aImageUrl      URL of the image to use
-     * @param aAlertTitle    title of the notification
-     * @param aAlertText     text of the notification
-     * @param contentIntent  Intent used when the notification is clicked
-     */
-    public void add(final int notificationID, String aImageUrl, String aHost, String aAlertTitle,
-                    String aAlertText, PendingIntent contentIntent) {
-        // Remove the old notification with the same ID, if any
-        remove(notificationID);
-
-        final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext)
-                .setContentTitle(aAlertTitle)
-                .setContentText(aAlertText)
-                .setSmallIcon(R.drawable.ic_status_logo)
-                .setContentIntent(contentIntent)
-                .setAutoCancel(true)
-                .setStyle(new NotificationCompat.InboxStyle()
-                          .addLine(aAlertText)
-                          .setSummaryText(aHost));
-
-        // Fetch icon.
-        if (!aImageUrl.isEmpty()) {
-            final Bitmap image = BitmapUtils.decodeUrl(aImageUrl);
-            builder.setLargeIcon(image);
-        }
-
-        builder.setWhen(System.currentTimeMillis());
-        final Notification notification = builder.build();
-
-        mNotificationManager.notify(notificationID, notification);
-        mNotifications.put(notificationID, notification);
-    }
-
-    /**
-     * Adds a notification.
-     *
-     * @param id             the unique ID of the notification
-     * @param notification   the Notification to add
-     */
-    public void add(int id, Notification notification) {
-        mNotificationManager.notify(id, notification);
-        mNotifications.put(id, notification);
-
-        if (mForegroundNotification == null && isOngoing(notification)) {
-            setForegroundNotification(id, notification);
-        }
-    }
-
-    /**
-     * Updates a notification.
-     *
-     * @param notificationID ID of existing notification
-     * @param aProgress      progress of item being updated
-     * @param aProgressMax   max progress of item being updated
-     * @param aAlertText     text of the notification
-     */
-    public void update(int notificationID, long aProgress, long aProgressMax, String aAlertText) {
-        Notification notification = mNotifications.get(notificationID);
-        if (notification == null) {
-            return;
-        }
-
-        notification = new NotificationCompat.Builder(mContext)
-                .setContentText(aAlertText)
-                .setSmallIcon(notification.icon)
-                .setWhen(notification.when)
-                .setContentIntent(notification.contentIntent)
-                .setProgress((int) aProgressMax, (int) aProgress, false)
-                .build();
-
-        add(notificationID, notification);
-    }
-
-    /**
-     * Removes a notification.
-     *
-     * @param notificationID ID of existing notification
-     */
-    public void remove(int notificationID) {
-        final Notification notification = mNotifications.remove(notificationID);
-        if (notification != null) {
-            updateForegroundNotification(notificationID, notification);
-        }
-        mNotificationManager.cancel(notificationID);
-    }
-
-    /**
-     * Determines whether the service is done.
-     *
-     * The service is considered finished when all notifications have been
-     * removed.
-     *
-     * @return whether all notifications have been removed
-     */
-    public boolean isDone() {
-        return mNotifications.isEmpty();
-    }
-
-    /**
-     * Determines whether a notification should hold a foreground service to keep Gecko alive
-     *
-     * @param notificationID the id of the notification to check
-     * @return               whether the notification is ongoing
-     */
-    public boolean isOngoing(int notificationID) {
-        final Notification notification = mNotifications.get(notificationID);
-        return isOngoing(notification);
-    }
-
-    /**
-     * Determines whether a notification should hold a foreground service to keep Gecko alive
-     *
-     * @param notification   the notification to check
-     * @return               whether the notification is ongoing
-     */
-    public boolean isOngoing(Notification notification) {
-        if (notification != null && (notification.flags & Notification.FLAG_ONGOING_EVENT) > 0) {
-            return true;
-        }
-        return false;
-    }
-
-    protected void setForegroundNotification(int id, Notification notification) {
-        mForegroundNotificationId = id;
-        mForegroundNotification = notification;
-    }
-
-    private void updateForegroundNotification(int oldId, Notification oldNotification) {
-        if (mForegroundNotificationId == oldId) {
-            // If we're removing the notification associated with the
-            // foreground, we need to pick another active notification to act
-            // as the foreground notification.
-            Notification foregroundNotification = null;
-            int foregroundId = 0;
-            for (final Integer id : mNotifications.keySet()) {
-                final Notification notification = mNotifications.get(id);
-                if (isOngoing(notification)) {
-                    foregroundNotification = notification;
-                    foregroundId = id;
-                    break;
-                }
-            }
-            setForegroundNotification(foregroundId, foregroundNotification);
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/NotificationService.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/* -*- 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.app.Notification;
-import android.app.Service;
-import android.content.Intent;
-import android.os.Binder;
-import android.os.IBinder;
-
-public class NotificationService extends Service {
-    private final IBinder mBinder = new NotificationBinder();
-    private NotificationHandler mHandler;
-
-    @Override
-    public void onCreate() {
-        // This has to be initialized in onCreate in order to ensure that the NotificationHandler can
-        // access the NotificationManager service.
-        mHandler = new NotificationHandler(this) {
-            @Override
-            protected void setForegroundNotification(int id, Notification notification) {
-                super.setForegroundNotification(id, notification);
-
-                if (notification == null) {
-                    stopForeground(true);
-                } else {
-                    startForeground(id, notification);
-                }
-            }
-        };
-    }
-
-    public class NotificationBinder extends Binder {
-        NotificationService getService() {
-            // Return this instance of NotificationService so clients can call public methods
-            return NotificationService.this;
-        }
-    }
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        return mBinder;
-    }
-
-    public NotificationHandler getNotificationHandler() {
-        return mHandler;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/PrefsHelper.java
+++ /dev/null
@@ -1,308 +0,0 @@
-/* -*- 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")
-    private static native void nativeGetPrefs(String[] prefNames, PrefHandler handler);
-    @WrapForJNI(stubName = "SetPref")
-    private static native void nativeSetPref(String prefName, boolean flush, int type,
-                                             boolean boolVal, int intVal, String strVal);
-    @WrapForJNI(stubName = "AddObserver")
-    private static native void nativeAddObserver(String[] prefNames, PrefHandler handler,
-                                                 String[] prefsToObserve);
-    @WrapForJNI(stubName = "RemoveObserver")
-    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
-    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
-    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() {
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/ServiceNotificationClient.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/* -*- 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.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.os.IBinder;
-import android.util.Log;
-
-/**
- * Client for posting notifications through the NotificationService.
- */
-public class ServiceNotificationClient extends NotificationClient {
-    private static final String LOGTAG = "GeckoServiceNotificationClient";
-
-    private final ServiceConnection mConnection = new NotificationServiceConnection();
-    private boolean mBound;
-    private final Context mContext;
-
-    public ServiceNotificationClient(Context context) {
-        mContext = context;
-    }
-
-    @Override
-    protected void bind() {
-        super.bind();
-        final Intent intent = new Intent(mContext, NotificationService.class);
-        mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
-    }
-
-    @Override
-    protected void unbind() {
-        // If there are no more tasks and no notifications being
-        // displayed, the service is disconnected. Unfortunately,
-        // since completed download notifications are shown by
-        // removing the progress notification and creating a new
-        // static one, this will cause the service to be unbound
-        // and immediately rebound when a download completes.
-        super.unbind();
-
-        if (mBound) {
-            mBound = false;
-            mContext.unbindService(mConnection);
-        }
-    }
-
-    class NotificationServiceConnection implements ServiceConnection {
-        @Override
-        public void onServiceConnected(ComponentName className, IBinder service) {
-            final NotificationService.NotificationBinder binder =
-                    (NotificationService.NotificationBinder) service;
-            connectHandler(binder.getService().getNotificationHandler());
-            mBound = true;
-        }
-
-        @Override
-        public void onServiceDisconnected(ComponentName componentName) {
-            // This is called when the connection with the service has been
-            // unexpectedly disconnected -- that is, its process crashed.
-            // Because it is running in our same process, we should never
-            // see this happen, and the correctness of this class relies on
-            // this never happening.
-            Log.e(LOGTAG, "Notification service disconnected", new Exception());
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/SmsManager.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/* -*- 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 SmsManager {
-    private static final ISmsManager sInstance;
-    static {
-        if (AppConstants.MOZ_WEBSMS_BACKEND) {
-            sInstance = new GeckoSmsManager();
-        } else {
-            sInstance = null;
-        }
-    }
-
-    public static ISmsManager getInstance() {
-        return sInstance;
-    }
-
-    public static boolean isEnabled() {
-        return AppConstants.MOZ_WEBSMS_BACKEND;
-    }
-
-    public interface ISmsManager {
-        void start();
-        void stop();
-        void shutdown();
-
-        void send(String aNumber, String aMessage, int aRequestId, boolean aShouldNotify);
-        void getMessage(int aMessageId, int aRequestId);
-        void deleteMessage(int aMessageId, int aRequestId);
-        void markMessageRead(int aMessageId, boolean aValue, boolean aSendReadReport, int aRequestId);
-        void createMessageCursor(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, String aDelivery, boolean aHasRead, boolean aRead, boolean aHasThreadId, long aThreadId, boolean aReverse, int aRequestId);
-        void createThreadCursor(int aRequestId);
-        void getNextThread(int aRequestId);
-        void getNextMessage(int aRequestId);
-    }
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/SurfaceBits.java
+++ /dev/null
@@ -1,17 +0,0 @@
-/* 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 java.nio.ByteBuffer;
-
-@WrapForJNI
-public class SurfaceBits {
-    public int width;
-    public int height;
-    public int format;
-    public ByteBuffer buffer;
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/TouchEventInterceptor.java
+++ /dev/null
@@ -1,14 +0,0 @@
-/* -*- 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);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/BitmapUtils.java
+++ /dev/null
@@ -1,466 +0,0 @@
-/* -*- 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.lang.reflect.Field;
-import java.net.MalformedURLException;
-import java.net.URL;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.GeckoJarReader;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.UIAsyncTask;
-
-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.Paint;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Base64;
-import android.util.Log;
-
-public final class BitmapUtils {
-    /* Default colors. */
-    private static final float[] DEFAULT_LAUNCHER_ICON_HSV = { 32.0f, 1.0f, 1.0f };
-
-    private static final String LOGTAG = "GeckoBitmapUtils";
-
-    private BitmapUtils() {}
-
-    public interface BitmapLoader {
-        public void onBitmapFound(Drawable d);
-    }
-
-    private static void runOnBitmapFoundOnUiThread(final BitmapLoader loader, final Drawable d) {
-        if (ThreadUtils.isOnUiThread()) {
-            loader.onBitmapFound(d);
-            return;
-        }
-
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                loader.onBitmapFound(d);
-            }
-        });
-    }
-
-    /**
-     * Attempts to find a drawable associated with a given string, using its URI scheme to determine
-     * how to load the drawable. The BitmapLoader's `onBitmapFound` method is always called, and
-     * will be called with `null` if no drawable is found.
-     *
-     * The BitmapLoader `onBitmapFound` method always runs on the UI thread.
-     */
-    public static void getDrawable(final Context context, final String data, final BitmapLoader loader) {
-        if (TextUtils.isEmpty(data)) {
-            runOnBitmapFoundOnUiThread(loader, null);
-            return;
-        }
-
-        if (data.startsWith("data")) {
-            final BitmapDrawable d = new BitmapDrawable(context.getResources(), getBitmapFromDataURI(data));
-            runOnBitmapFoundOnUiThread(loader, d);
-            return;
-        }
-
-        if (data.startsWith("jar:") || data.startsWith("file://")) {
-            (new UIAsyncTask.WithoutParams<Drawable>(ThreadUtils.getBackgroundHandler()) {
-                @Override
-                public Drawable doInBackground() {
-                    try {
-                        if (data.startsWith("jar:jar")) {
-                            return GeckoJarReader.getBitmapDrawable(
-                                    context, context.getResources(), data);
-                        }
-
-                        // Don't attempt to validate the JAR signature when loading an add-on icon
-                        if (data.startsWith("jar:file")) {
-                            return GeckoJarReader.getBitmapDrawable(
-                                    context, context.getResources(), Uri.decode(data));
-                        }
-
-                        final URL url = new URL(data);
-                        final InputStream is = (InputStream) url.getContent();
-                        try {
-                            return Drawable.createFromStream(is, "src");
-                        } finally {
-                            is.close();
-                        }
-                    } catch (Exception e) {
-                        Log.w(LOGTAG, "Unable to set icon", e);
-                    }
-                    return null;
-                }
-
-                @Override
-                public void onPostExecute(Drawable drawable) {
-                    loader.onBitmapFound(drawable);
-                }
-            }).execute();
-            return;
-        }
-
-        if (data.startsWith("-moz-icon://")) {
-            final Uri imageUri = Uri.parse(data);
-            final String ssp = imageUri.getSchemeSpecificPart();
-            final String resource = ssp.substring(ssp.lastIndexOf('/') + 1);
-
-            try {
-                final Drawable d = context.getPackageManager().getApplicationIcon(resource);
-                runOnBitmapFoundOnUiThread(loader, d);
-            } catch (Exception ex) { }
-
-            return;
-        }
-
-        if (data.startsWith("drawable://")) {
-            final Uri imageUri = Uri.parse(data);
-            final int id = getResource(imageUri, R.drawable.ic_status_logo);
-            final Drawable d = context.getResources().getDrawable(id);
-
-            runOnBitmapFoundOnUiThread(loader, d);
-            return;
-        }
-
-        runOnBitmapFoundOnUiThread(loader, null);
-    }
-
-    public static Bitmap decodeByteArray(byte[] bytes) {
-        return decodeByteArray(bytes, null);
-    }
-
-    public static Bitmap decodeByteArray(byte[] bytes, BitmapFactory.Options options) {
-        return decodeByteArray(bytes, 0, bytes.length, options);
-    }
-
-    public static Bitmap decodeByteArray(byte[] bytes, int offset, int length) {
-        return decodeByteArray(bytes, offset, length, null);
-    }
-
-    public static Bitmap decodeByteArray(byte[] bytes, int offset, int length, BitmapFactory.Options options) {
-        if (bytes.length <= 0) {
-            throw new IllegalArgumentException("bytes.length " + bytes.length
-                                               + " must be a positive number");
-        }
-
-        Bitmap bitmap = null;
-        try {
-            bitmap = BitmapFactory.decodeByteArray(bytes, offset, length, options);
-        } catch (OutOfMemoryError e) {
-            Log.e(LOGTAG, "decodeByteArray(bytes.length=" + bytes.length
-                          + ", options= " + options + ") OOM!", e);
-            return null;
-        }
-
-        if (bitmap == null) {
-            Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned null");
-            return null;
-        }
-
-        if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
-            Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned "
-                          + "a bitmap with dimensions " + bitmap.getWidth()
-                          + "x" + bitmap.getHeight());
-            return null;
-        }
-
-        return bitmap;
-    }
-
-    public static Bitmap decodeStream(InputStream inputStream) {
-        try {
-            return BitmapFactory.decodeStream(inputStream);
-        } catch (OutOfMemoryError e) {
-            Log.e(LOGTAG, "decodeStream() OOM!", e);
-            return null;
-        }
-    }
-
-    public static Bitmap decodeUrl(Uri uri) {
-        return decodeUrl(uri.toString());
-    }
-
-    public static Bitmap decodeUrl(String urlString) {
-        URL url;
-
-        try {
-            url = new URL(urlString);
-        } catch (MalformedURLException e) {
-            Log.w(LOGTAG, "decodeUrl: malformed URL " + urlString);
-            return null;
-        }
-
-        return decodeUrl(url);
-    }
-
-    public static Bitmap decodeUrl(URL url) {
-        InputStream stream = null;
-
-        try {
-            stream = url.openStream();
-        } catch (IOException e) {
-            Log.w(LOGTAG, "decodeUrl: IOException downloading " + url);
-            return null;
-        }
-
-        if (stream == null) {
-            Log.w(LOGTAG, "decodeUrl: stream not found downloading " + url);
-            return null;
-        }
-
-        Bitmap bitmap = decodeStream(stream);
-
-        try {
-            stream.close();
-        } catch (IOException e) {
-            Log.w(LOGTAG, "decodeUrl: IOException closing stream " + url, e);
-        }
-
-        return bitmap;
-    }
-
-    public static Bitmap decodeResource(Context context, int id) {
-        return decodeResource(context, id, null);
-    }
-
-    public static Bitmap decodeResource(Context context, int id, BitmapFactory.Options options) {
-        Resources resources = context.getResources();
-        try {
-            return BitmapFactory.decodeResource(resources, id, options);
-        } catch (OutOfMemoryError e) {
-            Log.e(LOGTAG, "decodeResource() OOM! Resource id=" + id, e);
-            return null;
-        }
-    }
-
-    public static int getDominantColor(Bitmap source) {
-        return getDominantColor(source, true);
-    }
-
-    public static int getDominantColor(Bitmap source, boolean applyThreshold) {
-      if (source == null)
-        return Color.argb(255, 255, 255, 255);
-
-      // Keep track of how many times a hue in a given bin appears in the image.
-      // Hue values range [0 .. 360), so dividing by 10, we get 36 bins.
-      int[] colorBins = new int[36];
-
-      // The bin with the most colors. Initialize to -1 to prevent accidentally
-      // thinking the first bin holds the dominant color.
-      int maxBin = -1;
-
-      // Keep track of sum hue/saturation/value per hue bin, which we'll use to
-      // compute an average to for the dominant color.
-      float[] sumHue = new float[36];
-      float[] sumSat = new float[36];
-      float[] sumVal = new float[36];
-      float[] hsv = new float[3];
-
-      int height = source.getHeight();
-      int width = source.getWidth();
-      int[] pixels = new int[width * height];
-      source.getPixels(pixels, 0, width, 0, 0, width, height);
-      for (int row = 0; row < height; row++) {
-        for (int col = 0; col < width; col++) {
-          int c = pixels[col + row * width];
-          // Ignore pixels with a certain transparency.
-          if (Color.alpha(c) < 128)
-            continue;
-
-          Color.colorToHSV(c, hsv);
-
-          // If a threshold is applied, ignore arbitrarily chosen values for "white" and "black".
-          if (applyThreshold && (hsv[1] <= 0.35f || hsv[2] <= 0.35f))
-            continue;
-
-          // We compute the dominant color by putting colors in bins based on their hue.
-          int bin = (int) Math.floor(hsv[0] / 10.0f);
-
-          // Update the sum hue/saturation/value for this bin.
-          sumHue[bin] = sumHue[bin] + hsv[0];
-          sumSat[bin] = sumSat[bin] + hsv[1];
-          sumVal[bin] = sumVal[bin] + hsv[2];
-
-          // Increment the number of colors in this bin.
-          colorBins[bin]++;
-
-          // Keep track of the bin that holds the most colors.
-          if (maxBin < 0 || colorBins[bin] > colorBins[maxBin])
-            maxBin = bin;
-        }
-      }
-
-      // maxBin may never get updated if the image holds only transparent and/or black/white pixels.
-      if (maxBin < 0)
-        return Color.argb(255, 255, 255, 255);
-
-      // Return a color with the average hue/saturation/value of the bin with the most colors.
-      hsv[0] = sumHue[maxBin] / colorBins[maxBin];
-      hsv[1] = sumSat[maxBin] / colorBins[maxBin];
-      hsv[2] = sumVal[maxBin] / colorBins[maxBin];
-      return Color.HSVToColor(hsv);
-    }
-
-    /**
-     * Decodes a bitmap from a Base64 data URI.
-     *
-     * @param dataURI a Base64-encoded data URI string
-     * @return        the decoded bitmap, or null if the data URI is invalid
-     */
-    public static Bitmap getBitmapFromDataURI(String dataURI) {
-        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(Uri resourceUrl, int defaultIcon) {
-        int icon = defaultIcon;
-
-        final String scheme = resourceUrl.getScheme();
-        if ("drawable".equals(scheme)) {
-            String resource = resourceUrl.getSchemeSpecificPart();
-            resource = resource.substring(resource.lastIndexOf('/') + 1);
-
-            try {
-                return Integer.parseInt(resource);
-            } catch (NumberFormatException ex) {
-                // This isn't a resource id, try looking for a string
-            }
-
-            try {
-                final Class<R.drawable> drawableClass = R.drawable.class;
-                final Field f = drawableClass.getField(resource);
-                icon = f.getInt(null);
-            } catch (final NoSuchFieldException e1) {
-
-                // just means the resource doesn't exist for fennec. Check in Android resources
-                try {
-                    final Class<android.R.drawable> drawableClass = android.R.drawable.class;
-                    final Field f = drawableClass.getField(resource);
-                    icon = f.getInt(null);
-                } catch (final NoSuchFieldException e2) {
-                    // This drawable doesn't seem to exist...
-                } catch (Exception e3) {
-                    Log.i(LOGTAG, "Exception getting drawable", e3);
-                }
-
-            } catch (Exception e4) {
-              Log.i(LOGTAG, "Exception getting drawable", e4);
-            }
-
-            resourceUrl = null;
-        }
-        return icon;
-    }
-
-    public static Bitmap getLauncherIcon(Context context, Bitmap aSource, int size) {
-        final int kOffset = 6;
-        final int kRadius = 5;
-        int insetSize = aSource != null ? size * 2 / 3 : size;
-
-        Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
-        Canvas canvas = new Canvas(bitmap);
-
-
-        // draw a base color
-        Paint paint = new Paint();
-        if (aSource == null) {
-            // If we aren't drawing a favicon, just use an orange color.
-            paint.setColor(Color.HSVToColor(DEFAULT_LAUNCHER_ICON_HSV));
-            canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint);
-        } else if (aSource.getWidth() >= insetSize || aSource.getHeight() >= insetSize) {
-            // Otherwise, if the icon is large enough, just draw it.
-            Rect iconBounds = new Rect(0, 0, size, size);
-            canvas.drawBitmap(aSource, null, iconBounds, null);
-            return bitmap;
-        } else {
-            // otherwise use the dominant color from the icon + a layer of transparent white to lighten it somewhat
-            int color = BitmapUtils.getDominantColor(aSource);
-            paint.setColor(color);
-            canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint);
-            paint.setColor(Color.argb(100, 255, 255, 255));
-            canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint);
-        }
-
-        // draw the overlay
-        Bitmap overlay = BitmapUtils.decodeResource(context, R.drawable.home_bg);
-        canvas.drawBitmap(overlay, null, new Rect(0, 0, size, size), null);
-
-        // draw the favicon
-        if (aSource == null)
-            aSource = BitmapUtils.decodeResource(context, R.drawable.home_star);
-
-        // by default, we scale the icon to this size
-        int sWidth = insetSize / 2;
-        int sHeight = sWidth;
-
-        int halfSize = size / 2;
-        canvas.drawBitmap(aSource,
-                null,
-                new Rect(halfSize - sWidth,
-                        halfSize - sHeight,
-                        halfSize + sWidth,
-                        halfSize + sHeight),
-                null);
-
-        return bitmap;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java
+++ /dev/null
@@ -1,771 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.PrefsHelper;
-import org.mozilla.gecko.util.FloatUtils;
-
-import org.json.JSONArray;
-
-import android.graphics.PointF;
-import android.graphics.RectF;
-import android.util.Log;
-
-import java.util.HashMap;
-import java.util.Map;
-
-final class DisplayPortCalculator {
-    private static final String LOGTAG = "GeckoDisplayPort";
-    private static final PointF ZERO_VELOCITY = new PointF(0, 0);
-
-    private static final String PREF_DISPLAYPORT_STRATEGY = "gfx.displayport.strategy";
-    private static final String PREF_DISPLAYPORT_FM_MULTIPLIER = "gfx.displayport.strategy_fm.multiplier";
-    private static final String PREF_DISPLAYPORT_FM_DANGER_X = "gfx.displayport.strategy_fm.danger_x";
-    private static final String PREF_DISPLAYPORT_FM_DANGER_Y = "gfx.displayport.strategy_fm.danger_y";
-    private static final String PREF_DISPLAYPORT_VB_MULTIPLIER = "gfx.displayport.strategy_vb.multiplier";
-    private static final String PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_vb.threshold";
-    private static final String PREF_DISPLAYPORT_VB_REVERSE_BUFFER = "gfx.displayport.strategy_vb.reverse_buffer";
-    private static final String PREF_DISPLAYPORT_VB_DANGER_X_BASE = "gfx.displayport.strategy_vb.danger_x_base";
-    private static final String PREF_DISPLAYPORT_VB_DANGER_Y_BASE = "gfx.displayport.strategy_vb.danger_y_base";
-    private static final String PREF_DISPLAYPORT_VB_DANGER_X_INCR = "gfx.displayport.strategy_vb.danger_x_incr";
-    private static final String PREF_DISPLAYPORT_VB_DANGER_Y_INCR = "gfx.displayport.strategy_vb.danger_y_incr";
-    private static final String PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_pb.threshold";
-
-    private static DisplayPortStrategy sStrategy = new VelocityBiasStrategy(null);
-
-    static DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
-        return sStrategy.calculate(metrics, (velocity == null ? ZERO_VELOCITY : velocity));
-    }
-
-    static boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
-        if (displayPort == null) {
-            return true;
-        }
-        return sStrategy.aboutToCheckerboard(metrics, (velocity == null ? ZERO_VELOCITY : velocity), displayPort);
-    }
-
-    static boolean drawTimeUpdate(long millis, int pixels) {
-        return sStrategy.drawTimeUpdate(millis, pixels);
-    }
-
-    static void resetPageState() {
-        sStrategy.resetPageState();
-    }
-
-    static void initPrefs() {
-        final String[] prefs = { PREF_DISPLAYPORT_STRATEGY,
-                                 PREF_DISPLAYPORT_FM_MULTIPLIER,
-                                 PREF_DISPLAYPORT_FM_DANGER_X,
-                                 PREF_DISPLAYPORT_FM_DANGER_Y,
-                                 PREF_DISPLAYPORT_VB_MULTIPLIER,
-                                 PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD,
-                                 PREF_DISPLAYPORT_VB_REVERSE_BUFFER,
-                                 PREF_DISPLAYPORT_VB_DANGER_X_BASE,
-                                 PREF_DISPLAYPORT_VB_DANGER_Y_BASE,
-                                 PREF_DISPLAYPORT_VB_DANGER_X_INCR,
-                                 PREF_DISPLAYPORT_VB_DANGER_Y_INCR,
-                                 PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD };
-
-        PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() {
-            private final Map<String, Integer> mValues = new HashMap<String, Integer>();
-
-            @Override public void prefValue(String pref, int value) {
-                mValues.put(pref, value);
-            }
-
-            @Override public void finish() {
-                setStrategy(mValues);
-            }
-        });
-    }
-
-    /**
-     * Set the active strategy to use.
-     * See the gfx.displayport.strategy pref in mobile/android/app/mobile.js to see the
-     * mapping between ints and strategies.
-     */
-    static boolean setStrategy(Map<String, Integer> prefs) {
-        Integer strategy = prefs.get(PREF_DISPLAYPORT_STRATEGY);
-        if (strategy == null) {
-            return false;
-        }
-
-        switch (strategy) {
-            case 0:
-                sStrategy = new FixedMarginStrategy(prefs);
-                break;
-            case 1:
-                sStrategy = new VelocityBiasStrategy(prefs);
-                break;
-            case 2:
-                sStrategy = new DynamicResolutionStrategy(prefs);
-                break;
-            case 3:
-                sStrategy = new NoMarginStrategy(prefs);
-                break;
-            case 4:
-                sStrategy = new PredictionBiasStrategy(prefs);
-                break;
-            default:
-                Log.e(LOGTAG, "Invalid strategy index specified");
-                return false;
-        }
-        Log.i(LOGTAG, "Set strategy " + sStrategy.toString());
-        return true;
-    }
-
-    private static float getFloatPref(Map<String, Integer> prefs, String prefName, int defaultValue) {
-        Integer value = (prefs == null ? null : prefs.get(prefName));
-        return (value == null || value < 0 ? defaultValue : value) / 1000f;
-    }
-
-    private static abstract class DisplayPortStrategy {
-        /** Calculates a displayport given a viewport and panning velocity. */
-        public abstract DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity);
-        /** Returns true if a checkerboard is about to be visible and we should not throttle drawing. */
-        public abstract boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort);
-        /** Notify the strategy of a new recorded draw time. Return false to turn off draw time recording. */
-        public boolean drawTimeUpdate(long millis, int pixels) { return false; }
-        /** Reset any page-specific state stored, as the page being displayed has changed. */
-        public void resetPageState() {}
-    }
-
-    /**
-     * Return the dimensions for a rect that has area (width*height) that does not exceed the page size in the
-     * given metrics object. The area in the returned FloatSize may be less than width*height if the page is
-     * small, but it will never be larger than width*height.
-     * Note that this process may change the relative aspect ratio of the given dimensions.
-     */
-    private static FloatSize reshapeForPage(float width, float height, ImmutableViewportMetrics metrics) {
-        // figure out how much of the desired buffer amount we can actually use on the horizontal axis
-        float usableWidth = Math.min(width, metrics.getPageWidth());
-        // if we reduced the buffer amount on the horizontal axis, we should take that saved memory and
-        // use it on the vertical axis
-        float extraUsableHeight = (float)Math.floor(((width - usableWidth) * height) / usableWidth);
-        float usableHeight = Math.min(height + extraUsableHeight, metrics.getPageHeight());
-        if (usableHeight < height && usableWidth == width) {
-            // and the reverse - if we shrunk the buffer on the vertical axis we can add it to the horizontal
-            float extraUsableWidth = (float)Math.floor(((height - usableHeight) * width) / usableHeight);
-            usableWidth = Math.min(width + extraUsableWidth, metrics.getPageWidth());
-        }
-        return new FloatSize(usableWidth, usableHeight);
-    }
-
-    /**
-     * Expand the given rect in all directions by a "danger zone". The size of the danger zone on an axis
-     * is the size of the view on that axis multiplied by the given multiplier. The expanded rect is then
-     * clamped to page bounds and returned.
-     */
-    private static RectF expandByDangerZone(RectF rect, float dangerZoneXMultiplier, float dangerZoneYMultiplier, ImmutableViewportMetrics metrics) {
-        // calculate the danger zone amounts in pixels
-        float dangerZoneX = metrics.getWidth() * dangerZoneXMultiplier;
-        float dangerZoneY = metrics.getHeight() * dangerZoneYMultiplier;
-        rect = RectUtils.expand(rect, dangerZoneX, dangerZoneY);
-        // clamp to page bounds
-        return clampToPageBounds(rect, metrics);
-    }
-
-    /**
-     * Calculate the display port by expanding the viewport by the specified
-     * margins, then clamping to the page size.
-     */
-    private static DisplayPortMetrics getPageClampedDisplayPortMetrics(RectF margins, float zoom, ImmutableViewportMetrics metrics) {
-        float left = metrics.viewportRectLeft - margins.left;
-        float top = metrics.viewportRectTop - margins.top;
-        float right = metrics.viewportRectRight() + margins.right;
-        float bottom = metrics.viewportRectBottom() + margins.bottom;
-        left = Math.max(metrics.pageRectLeft, left);
-        top = Math.max(metrics.pageRectTop, top);
-        right = Math.min(metrics.pageRectRight, right);
-        bottom = Math.min(metrics.pageRectBottom, bottom);
-
-        return new DisplayPortMetrics(left, top, right, bottom, zoom);
-    }
-
-    /**
-     * Adjust the given margins so if they are applied on the viewport in the metrics, the resulting rect
-     * does not exceed the page bounds. This code will maintain the total margin amount for a given axis;
-     * it assumes that margins.left + metrics.getWidth() + margins.right is less than or equal to
-     * metrics.getPageWidth(); and the same for the y axis.
-     */
-    private static RectF shiftMarginsForPageBounds(RectF margins, ImmutableViewportMetrics metrics) {
-        // check how much we're overflowing in each direction. note that at most one of leftOverflow
-        // and rightOverflow can be greater than zero, and at most one of topOverflow and bottomOverflow
-        // can be greater than zero, because of the assumption described in the method javadoc.
-        float leftOverflow = metrics.pageRectLeft - (metrics.viewportRectLeft - margins.left);
-        float rightOverflow = (metrics.viewportRectRight() + margins.right) - metrics.pageRectRight;
-        float topOverflow = metrics.pageRectTop - (metrics.viewportRectTop - margins.top);
-        float bottomOverflow = (metrics.viewportRectBottom() + margins.bottom) - metrics.pageRectBottom;
-
-        // if the margins overflow the page bounds, shift them to other side on the same axis
-        if (leftOverflow > 0) {
-            margins.left -= leftOverflow;
-            margins.right += leftOverflow;
-        } else if (rightOverflow > 0) {
-            margins.right -= rightOverflow;
-            margins.left += rightOverflow;
-        }
-        if (topOverflow > 0) {
-            margins.top -= topOverflow;
-            margins.bottom += topOverflow;
-        } else if (bottomOverflow > 0) {
-            margins.bottom -= bottomOverflow;
-            margins.top += bottomOverflow;
-        }
-        return margins;
-    }
-
-    /**
-     * Clamp the given rect to the page bounds and return it.
-     */
-    private static RectF clampToPageBounds(RectF rect, ImmutableViewportMetrics metrics) {
-        if (rect.top < metrics.pageRectTop) rect.top = metrics.pageRectTop;
-        if (rect.left < metrics.pageRectLeft) rect.left = metrics.pageRectLeft;
-        if (rect.right > metrics.pageRectRight) rect.right = metrics.pageRectRight;
-        if (rect.bottom > metrics.pageRectBottom) rect.bottom = metrics.pageRectBottom;
-        return rect;
-    }
-
-    /**
-     * This class implements the variation where we basically don't bother with a a display port.
-     */
-    private static class NoMarginStrategy extends DisplayPortStrategy {
-        NoMarginStrategy(Map<String, Integer> prefs) {
-            // no prefs in this strategy
-        }
-
-        @Override
-        public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
-            return new DisplayPortMetrics(metrics.viewportRectLeft,
-                    metrics.viewportRectTop,
-                    metrics.viewportRectRight(),
-                    metrics.viewportRectBottom(),
-                    metrics.zoomFactor);
-        }
-
-        @Override
-        public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
-            return true;
-        }
-
-        @Override
-        public String toString() {
-            return "NoMarginStrategy";
-        }
-    }
-
-    /**
-     * This class implements the variation where we use a fixed-size margin on the display port.
-     * The margin is always 300 pixels in all directions, except when we are (a) approaching a page
-     * boundary, and/or (b) if we are limited by the page size. In these cases we try to maintain
-     * the area of the display port by (a) shifting the buffer to the other side on the same axis,
-     * and/or (b) increasing the buffer on the other axis to compensate for the reduced buffer on
-     * one axis.
-     */
-    private static class FixedMarginStrategy extends DisplayPortStrategy {
-        // The length of each axis of the display port will be the corresponding view length
-        // multiplied by this factor.
-        private final float SIZE_MULTIPLIER;
-
-        // If the visible rect is within the danger zone (measured as a fraction of the view size
-        // from the edge of the displayport) we start redrawing to minimize checkerboarding.
-        private final float DANGER_ZONE_X_MULTIPLIER;
-        private final float DANGER_ZONE_Y_MULTIPLIER;
-
-        FixedMarginStrategy(Map<String, Integer> prefs) {
-            SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_MULTIPLIER, 2000);
-            DANGER_ZONE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_X, 100);
-            DANGER_ZONE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_Y, 200);
-        }
-
-        @Override
-        public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
-            float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
-            float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
-
-            // we need to avoid having a display port that is larger than the page, or we will end up
-            // painting things outside the page bounds (bug 729169). we simultaneously need to make
-            // the display port as large as possible so that we redraw less. reshape the display
-            // port dimensions to accomplish this.
-            FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
-            float horizontalBuffer = usableSize.width - metrics.getWidth();
-            float verticalBuffer = usableSize.height - metrics.getHeight();
-
-            // and now calculate the display port margins based on how much buffer we've decided to use and
-            // the page bounds, ensuring we use all of the available buffer amounts on one side or the other
-            // on any given axis. (i.e. if we're scrolled to the top of the page, the vertical buffer is
-            // entirely below the visible viewport, but if we're halfway down the page, the vertical buffer
-            // is split).
-            RectF margins = new RectF();
-            margins.left = horizontalBuffer / 2.0f;
-            margins.right = horizontalBuffer - margins.left;
-            margins.top = verticalBuffer / 2.0f;
-            margins.bottom = verticalBuffer - margins.top;
-            margins = shiftMarginsForPageBounds(margins, metrics);
-
-            return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
-        }
-
-        @Override
-        public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
-            // Increase the size of the viewport based on the danger zone multiplier (and clamp to page
-            // boundaries), and intersect it with the current displayport to determine whether we're
-            // close to checkerboarding.
-            RectF adjustedViewport = expandByDangerZone(metrics.getViewport(), DANGER_ZONE_X_MULTIPLIER, DANGER_ZONE_Y_MULTIPLIER, metrics);
-            return !displayPort.contains(adjustedViewport);
-        }
-
-        @Override
-        public String toString() {
-            return "FixedMarginStrategy mult=" + SIZE_MULTIPLIER + ", dangerX=" + DANGER_ZONE_X_MULTIPLIER + ", dangerY=" + DANGER_ZONE_Y_MULTIPLIER;
-        }
-    }
-
-    /**
-     * This class implements the variation with a small fixed-size margin with velocity bias.
-     * In this variation, the default margins are pretty small relative to the view size, but
-     * they are affected by the panning velocity. Specifically, if we are panning on one axis,
-     * we remove the margins on the other axis because we are likely axis-locked. Also once
-     * we are panning in one direction above a certain threshold velocity, we shift the buffer
-     * so that it is almost entirely in the direction of the pan, with a little bit in the
-     * reverse direction.
-     */
-    private static class VelocityBiasStrategy extends DisplayPortStrategy {
-        // The length of each axis of the display port will be the corresponding view length
-        // multiplied by this factor.
-        private final float SIZE_MULTIPLIER;
-        // The velocity above which we apply the velocity bias
-        private final float VELOCITY_THRESHOLD;
-        // How much of the buffer to keep in the reverse direction of the velocity
-        private final float REVERSE_BUFFER;
-        // If the visible rect is within the danger zone we start redrawing to minimize
-        // checkerboarding. the danger zone amount is a linear function of the form:
-        //    viewportsize * (base + velocity * incr)
-        // where base and incr are configurable values.
-        private final float DANGER_ZONE_BASE_X_MULTIPLIER;
-        private final float DANGER_ZONE_BASE_Y_MULTIPLIER;
-        private final float DANGER_ZONE_INCR_X_MULTIPLIER;
-        private final float DANGER_ZONE_INCR_Y_MULTIPLIER;
-
-        VelocityBiasStrategy(Map<String, Integer> prefs) {
-            SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_MULTIPLIER, 2000);
-            VELOCITY_THRESHOLD = GeckoAppShell.getDpi() * getFloatPref(prefs, PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD, 32);
-            REVERSE_BUFFER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_REVERSE_BUFFER, 200);
-            DANGER_ZONE_BASE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_BASE, 1000);
-            DANGER_ZONE_BASE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_BASE, 1000);
-            DANGER_ZONE_INCR_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_INCR, 0);
-            DANGER_ZONE_INCR_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_INCR, 0);
-        }
-
-        /**
-         * Split the given amounts into margins based on the VELOCITY_THRESHOLD and REVERSE_BUFFER values.
-         * If the velocity is above the VELOCITY_THRESHOLD on an axis, split the amount into REVERSE_BUFFER
-         * and 1.0 - REVERSE_BUFFER fractions. The REVERSE_BUFFER fraction is set as the margin in the
-         * direction opposite to the velocity, and the remaining fraction is set as the margin in the direction
-         * of the velocity. If the velocity is lower than VELOCITY_THRESHOLD, split the amount evenly into the
-         * two margins on that axis.
-         */
-        private RectF velocityBiasedMargins(float xAmount, float yAmount, PointF velocity) {
-            RectF margins = new RectF();
-
-            if (velocity.x > VELOCITY_THRESHOLD) {
-                margins.left = xAmount * REVERSE_BUFFER;
-            } else if (velocity.x < -VELOCITY_THRESHOLD) {
-                margins.left = xAmount * (1.0f - REVERSE_BUFFER);
-            } else {
-                margins.left = xAmount / 2.0f;
-            }
-            margins.right = xAmount - margins.left;
-
-            if (velocity.y > VELOCITY_THRESHOLD) {
-                margins.top = yAmount * REVERSE_BUFFER;
-            } else if (velocity.y < -VELOCITY_THRESHOLD) {
-                margins.top = yAmount * (1.0f - REVERSE_BUFFER);
-            } else {
-                margins.top = yAmount / 2.0f;
-            }
-            margins.bottom = yAmount - margins.top;
-
-            return margins;
-        }
-
-        @Override
-        public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
-            float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
-            float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
-
-            // but if we're panning on one axis, set the margins for the other axis to zero since we are likely
-            // axis locked and won't be displaying that extra area.
-            if (Math.abs(velocity.x) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.y, 0)) {
-                displayPortHeight = metrics.getHeight();
-            } else if (Math.abs(velocity.y) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.x, 0)) {
-                displayPortWidth = metrics.getWidth();
-            }
-
-            // we need to avoid having a display port that is larger than the page, or we will end up
-            // painting things outside the page bounds (bug 729169).
-            displayPortWidth = Math.min(displayPortWidth, metrics.getPageWidth());
-            displayPortHeight = Math.min(displayPortHeight, metrics.getPageHeight());
-            float horizontalBuffer = displayPortWidth - metrics.getWidth();
-            float verticalBuffer = displayPortHeight - metrics.getHeight();
-
-            // split the buffer amounts into margins based on velocity, and shift it to
-            // take into account the page bounds
-            RectF margins = velocityBiasedMargins(horizontalBuffer, verticalBuffer, velocity);
-            margins = shiftMarginsForPageBounds(margins, metrics);
-
-            return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
-        }
-
-        @Override
-        public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
-            // calculate the danger zone amounts based on the prefs
-            float dangerZoneX = metrics.getWidth() * (DANGER_ZONE_BASE_X_MULTIPLIER + (velocity.x * DANGER_ZONE_INCR_X_MULTIPLIER));
-            float dangerZoneY = metrics.getHeight() * (DANGER_ZONE_BASE_Y_MULTIPLIER + (velocity.y * DANGER_ZONE_INCR_Y_MULTIPLIER));
-            // clamp it such that when added to the viewport, they don't exceed page size.
-            // this is a prerequisite to calling shiftMarginsForPageBounds as we do below.
-            dangerZoneX = Math.min(dangerZoneX, metrics.getPageWidth() - metrics.getWidth());
-            dangerZoneY = Math.min(dangerZoneY, metrics.getPageHeight() - metrics.getHeight());
-
-            // split the danger zone into margins based on velocity, and ensure it doesn't exceed
-            // page bounds.
-            RectF dangerMargins = velocityBiasedMargins(dangerZoneX, dangerZoneY, velocity);
-            dangerMargins = shiftMarginsForPageBounds(dangerMargins, metrics);
-
-            // we're about to checkerboard if the current viewport area + the danger zone margins
-            // fall out of the current displayport anywhere.
-            RectF adjustedViewport = new RectF(
-                    metrics.viewportRectLeft - dangerMargins.left,
-                    metrics.viewportRectTop - dangerMargins.top,
-                    metrics.viewportRectRight() + dangerMargins.right,
-                    metrics.viewportRectBottom() + dangerMargins.bottom);
-            return !displayPort.contains(adjustedViewport);
-        }
-
-        @Override
-        public String toString() {
-            return "VelocityBiasStrategy mult=" + SIZE_MULTIPLIER + ", threshold=" + VELOCITY_THRESHOLD + ", reverse=" + REVERSE_BUFFER
-                + ", dangerBaseX=" + DANGER_ZONE_BASE_X_MULTIPLIER + ", dangerBaseY=" + DANGER_ZONE_BASE_Y_MULTIPLIER
-                + ", dangerIncrX=" + DANGER_ZONE_INCR_Y_MULTIPLIER + ", dangerIncrY=" + DANGER_ZONE_INCR_Y_MULTIPLIER;
-        }
-    }
-
-    /**
-     * This class implements the variation where we draw more of the page at low resolution while panning.
-     * In this variation, as we pan faster, we increase the page area we are drawing, but reduce the draw
-     * resolution to compensate. This results in the same device-pixel area drawn; the compositor then
-     * scales this up to the viewport zoom level. This results in a large area of the page drawn but it
-     * looks blurry. The assumption is that drawing extra that we never display is better than checkerboarding,
-     * where we draw less but never even show it on the screen.
-     */
-    private static class DynamicResolutionStrategy extends DisplayPortStrategy {
-        // The length of each axis of the display port will be the corresponding view length
-        // multiplied by this factor.
-        private static final float SIZE_MULTIPLIER = 1.5f;
-
-        // The velocity above which we start zooming out the display port to keep up
-        // with the panning.
-        private static final float VELOCITY_EXPANSION_THRESHOLD = GeckoAppShell.getDpi() / 16f;
-
-        // How much we increase the display port based on velocity. Assuming no friction and
-        // splitting (see below), this should be be the number of frames (@60fps) between us
-        // calculating the display port and the draw of the *next* display port getting composited
-        // and displayed on the screen. This is because the timeline looks like this:
-        //      Java: pan pan pan pan pan pan ! pan pan pan pan pan pan !
-        //     Gecko:   \-> draw -> composite /   \-> draw -> composite /
-        // The display port calculated on the first "pan" gets composited to the screen at the
-        // first exclamation mark, and remains on the screen until the second exclamation mark.
-        // In order to avoid checkerboarding, that display port must be able to contain all of
-        // the panning until the second exclamation mark, which encompasses two entire draw/composite
-        // cycles.
-        // If we take into account friction, our velocity multiplier should be reduced as the
-        // amount of pan will decrease each time. If we take into account display port splitting,
-        // it should be increased as the splitting means some of the display port will be used to
-        // draw in the opposite direction of the velocity. For now I'm assuming these two cancel
-        // each other out.
-        private static final float VELOCITY_MULTIPLIER = 60.0f;
-
-        // The following constants adjust how biased the display port is in the direction of panning.
-        // When panning fast (above the FAST_THRESHOLD) we use the fast split factor to split the
-        // display port "buffer" area, otherwise we use the slow split factor. This is based on the
-        // assumption that if the user is panning fast, they are less likely to reverse directions
-        // and go backwards, so we should spend more of our display port buffer in the direction of
-        // panning.
-        private static final float VELOCITY_FAST_THRESHOLD = VELOCITY_EXPANSION_THRESHOLD * 2.0f;
-        private static final float FAST_SPLIT_FACTOR = 0.95f;
-        private static final float SLOW_SPLIT_FACTOR = 0.8f;
-
-        // The following constants are used for viewport prediction; we use them to estimate where
-        // the viewport will be soon and whether or not we should trigger a draw right now. "soon"
-        // in the previous sentence really refers to the amount of time it would take to draw and
-        // composite from the point at which we do the calculation, and that is not really a known
-        // quantity. The velocity multiplier is how much we multiply the velocity by; it has the
-        // same caveats as the VELOCITY_MULTIPLIER above except that it only needs to take into account
-        // one draw/composite cycle instead of two. The danger zone multiplier is a multiplier of the
-        // viewport size that we use as an extra "danger zone" around the viewport; if this danger
-        // zone falls outside the display port then we are approaching the point at which we will
-        // checkerboard, and hence should start drawing. Note that if DANGER_ZONE_MULTIPLIER is
-        // greater than (SIZE_MULTIPLIER - 1.0f), then at zero velocity we will always be in the
-        // danger zone, and thus will be constantly drawing.
-        private static final float PREDICTION_VELOCITY_MULTIPLIER = 30.0f;
-        private static final float DANGER_ZONE_MULTIPLIER = 0.20f; // must be less than (SIZE_MULTIPLIER - 1.0f)
-
-        DynamicResolutionStrategy(Map<String, Integer> prefs) {
-            // ignore prefs for now
-        }
-
-        @Override
-        public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
-            float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
-            float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
-
-            // for resolution calculation purposes, we need to know what the adjusted display port dimensions
-            // would be if we had zero velocity, so calculate that here before we increase the display port
-            // based on velocity.
-            FloatSize reshapedSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
-
-            // increase displayPortWidth and displayPortHeight based on the velocity, but maintaining their
-            // relative aspect ratio.
-            if (velocity.length() > VELOCITY_EXPANSION_THRESHOLD) {
-                float velocityFactor = Math.max(Math.abs(velocity.x) / displayPortWidth,
-                                                Math.abs(velocity.y) / displayPortHeight);
-                velocityFactor *= VELOCITY_MULTIPLIER;
-
-                displayPortWidth += (displayPortWidth * velocityFactor);
-                displayPortHeight += (displayPortHeight * velocityFactor);
-            }
-
-            // at this point, displayPortWidth and displayPortHeight are how much of the page (in device pixels)
-            // we want to be rendered by Gecko. Note here "device pixels" is equivalent to CSS pixels multiplied
-            // by metrics.zoomFactor
-
-            // we need to avoid having a display port that is larger than the page, or we will end up
-            // painting things outside the page bounds (bug 729169). we simultaneously need to make
-            // the display port as large as possible so that we redraw less. reshape the display
-            // port dimensions to accomplish this. this may change the aspect ratio of the display port,
-            // but we are assuming that this is desirable because the advantages from pre-drawing will
-            // outweigh the disadvantages from any buffer reallocations that might occur.
-            FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
-            float horizontalBuffer = usableSize.width - metrics.getWidth();
-            float verticalBuffer = usableSize.height - metrics.getHeight();
-
-            // at this point, horizontalBuffer and verticalBuffer are the dimensions of the buffer area we have.
-            // the buffer area is the off-screen area that is part of the display port and will be pre-drawn in case
-            // the user scrolls there. we now need to split the buffer area on each axis so that we know
-            // what the exact margins on each side will be. first we split the buffer amount based on the direction
-            // we're moving, so that we have a larger buffer in the direction of travel.
-            RectF margins = new RectF();
-            margins.left = splitBufferByVelocity(horizontalBuffer, velocity.x);
-            margins.right = horizontalBuffer - margins.left;
-            margins.top = splitBufferByVelocity(verticalBuffer, velocity.y);
-            margins.bottom = verticalBuffer - margins.top;
-
-            // then, we account for running into the page bounds - so that if we hit the top of the page, we need
-            // to drop the top margin and move that amount to the bottom margin.
-            margins = shiftMarginsForPageBounds(margins, metrics);
-
-            // finally, we calculate the resolution we want to render the display port area at. We do this
-            // so that as we expand the display port area (because of velocity), we reduce the resolution of
-            // the painted area so as to maintain the size of the buffer Gecko is painting into. we calculate
-            // the reduction in resolution by comparing the display port size with and without the velocity
-            // changes applied.
-            // this effectively means that as we pan faster and faster, the display port grows, but we paint
-            // at lower resolutions. this paints more area to reduce checkerboard at the cost of increasing
-            // compositor-scaling and blurriness. Once we stop panning, the blurriness must be entirely gone.
-            // Note that usable* could be less than base* if we are pinch-zoomed out into overscroll, so we
-            // clamp it to make sure this doesn't increase our display resolution past metrics.zoomFactor.
-            float scaleFactor = Math.min(reshapedSize.width / usableSize.width, reshapedSize.height / usableSize.height);
-            float displayResolution = metrics.zoomFactor * Math.min(1.0f, scaleFactor);
-
-            DisplayPortMetrics dpMetrics = new DisplayPortMetrics(
-                metrics.viewportRectLeft - margins.left,
-                metrics.viewportRectTop - margins.top,
-                metrics.viewportRectRight() + margins.right,
-                metrics.viewportRectBottom() + margins.bottom,
-                displayResolution);
-            return dpMetrics;
-        }
-
-        /**
-         * Split the given buffer amount into two based on the velocity.
-         * Given an amount of total usable buffer on an axis, this will
-         * return the amount that should be used on the left/top side of
-         * the axis (the side which a negative velocity vector corresponds
-         * to).
-         */
-        private float splitBufferByVelocity(float amount, float velocity) {
-            // if no velocity, so split evenly
-            if (FloatUtils.fuzzyEquals(velocity, 0)) {
-                return amount / 2.0f;
-            }
-            // if we're moving quickly, assign more of the amount in that direction
-            // since is is less likely that we will reverse direction immediately
-            if (velocity < -VELOCITY_FAST_THRESHOLD) {
-                return amount * FAST_SPLIT_FACTOR;
-            }
-            if (velocity > VELOCITY_FAST_THRESHOLD) {
-                return amount * (1.0f - FAST_SPLIT_FACTOR);
-            }
-            // if we're moving slowly, then assign less of the amount in that direction
-            if (velocity < 0) {
-                return amount * SLOW_SPLIT_FACTOR;
-            } else {
-                return amount * (1.0f - SLOW_SPLIT_FACTOR);
-            }
-        }
-
-        @Override
-        public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
-            // Expand the viewport based on our velocity (and clamp it to page boundaries).
-            // Then intersect it with the last-requested displayport to determine whether we're
-            // close to checkerboarding.
-
-            RectF predictedViewport = metrics.getViewport();
-
-            // first we expand the viewport in the direction we're moving based on some
-            // multiple of the current velocity.
-            if (velocity.length() > 0) {
-                if (velocity.x < 0) {
-                    predictedViewport.left += velocity.x * PREDICTION_VELOCITY_MULTIPLIER;
-                } else if (velocity.x > 0) {
-                    predictedViewport.right += velocity.x * PREDICTION_VELOCITY_MULTIPLIER;
-                }
-
-                if (velocity.y < 0) {
-                    predictedViewport.top += velocity.y * PREDICTION_VELOCITY_MULTIPLIER;
-                } else if (velocity.y > 0) {
-                    predictedViewport.bottom += velocity.y * PREDICTION_VELOCITY_MULTIPLIER;
-                }
-            }
-
-            // then we expand the viewport evenly in all directions just to have an extra
-            // safety zone. this also clamps it to page bounds.
-            predictedViewport = expandByDangerZone(predictedViewport, DANGER_ZONE_MULTIPLIER, DANGER_ZONE_MULTIPLIER, metrics);
-            return !displayPort.contains(predictedViewport);
-        }
-
-        @Override
-        public String toString() {
-            return "DynamicResolutionStrategy";
-        }
-    }
-
-    /**
-     * This class implements the variation where we use the draw time to predict where we will be when
-     * a draw completes, and draw that instead of where we are now. In this variation, when our panning
-     * speed drops below a certain threshold, we draw 9 viewports' worth of content so that the user can
-     * pan in any direction without encountering checkerboarding.
-     * Once the user is panning, we modify the displayport to encompass an area range of where we think
-     * the user will be when the draw completes. This heuristic relies on both the estimated draw time
-     * the panning velocity; unexpected changes in either of these values will cause the heuristic to
-     * fail and show checkerboard.
-     */
-    private static class PredictionBiasStrategy extends DisplayPortStrategy {
-        private static float VELOCITY_THRESHOLD;
-
-        private int mPixelArea;         // area of the viewport, used in draw time calculations
-        private int mMinFramesToDraw;   // minimum number of frames we take to draw
-        private int mMaxFramesToDraw;   // maximum number of frames we take to draw
-
-        PredictionBiasStrategy(Map<String, Integer> prefs) {
-            VELOCITY_THRESHOLD = GeckoAppShell.getDpi() * getFloatPref(prefs, PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD, 16);
-            resetPageState();
-        }
-
-        @Override
-        public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
-            float width = metrics.getWidth();
-            float height = metrics.getHeight();
-            mPixelArea = (int)(width * height);
-
-            if (velocity.length() < VELOCITY_THRESHOLD) {
-                // if we're going slow, expand the displayport to 9x viewport size
-                RectF margins = new RectF(width, height, width, height);
-                return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
-            }
-
-            // figure out how far we expect to be
-            float minDx = velocity.x * mMinFramesToDraw;
-            float minDy = velocity.y * mMinFramesToDraw;
-            float maxDx = velocity.x * mMaxFramesToDraw;
-            float maxDy = velocity.y * mMaxFramesToDraw;
-
-            // figure out how many pixels we will be drawing when we draw the above-calculated range.
-            // this will be larger than the viewport area.
-            float pixelsToDraw = (width + Math.abs(maxDx - minDx)) * (height + Math.abs(maxDy - minDy));
-            // adjust how far we will get because of the time spent drawing all these extra pixels. this
-            // will again increase the number of pixels drawn so really we could keep iterating this over
-            // and over, but once seems enough for now.
-            maxDx = maxDx * pixelsToDraw / mPixelArea;
-            maxDy = maxDy * pixelsToDraw / mPixelArea;
-
-            // and finally generate the displayport. the min/max stuff takes care of
-            // negative velocities as well as positive.
-            RectF margins = new RectF(
-                -Math.min(minDx, maxDx),
-                -Math.min(minDy, maxDy),
-                Math.max(minDx, maxDx),
-                Math.max(minDy, maxDy));
-            return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
-        }
-
-        @Override
-        public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
-            // the code below is the same as in calculate() but is awkward to refactor since it has multiple outputs.
-            // refer to the comments in calculate() to understand what this is doing.
-            float minDx = velocity.x * mMinFramesToDraw;
-            float minDy = velocity.y * mMinFramesToDraw;
-            float maxDx = velocity.x * mMaxFramesToDraw;
-            float maxDy = velocity.y * mMaxFramesToDraw;
-            float pixelsToDraw = (metrics.getWidth() + Math.abs(maxDx - minDx)) * (metrics.getHeight() + Math.abs(maxDy - minDy));
-            maxDx = maxDx * pixelsToDraw / mPixelArea;
-            maxDy = maxDy * pixelsToDraw / mPixelArea;
-
-            // now that we have an idea of how far we will be when the draw completes, take the farthest
-            // end of that range and see if it falls outside the displayport bounds. if it does, allow
-            // the draw to go through
-            RectF predictedViewport = metrics.getViewport();
-            predictedViewport.left += maxDx;
-            predictedViewport.top += maxDy;
-            predictedViewport.right += maxDx;
-            predictedViewport.bottom += maxDy;
-
-            predictedViewport = clampToPageBounds(predictedViewport, metrics);
-            return !displayPort.contains(predictedViewport);
-        }
-
-        @Override
-        public boolean drawTimeUpdate(long millis, int pixels) {
-            // calculate the number of frames it took to draw a viewport-sized area
-            float normalizedTime = (float)mPixelArea * millis / pixels;
-            int normalizedFrames = (int) Math.ceil(normalizedTime * 60f / 1000f);
-            // broaden our range on how long it takes to draw if the draw falls outside
-            // the range. this allows it to grow gradually. this heuristic may need to
-            // be tweaked into more of a floating window average or something.
-            if (normalizedFrames <= mMinFramesToDraw) {
-                mMinFramesToDraw--;
-            } else if (normalizedFrames > mMaxFramesToDraw) {
-                mMaxFramesToDraw++;
-            } else {
-                return true;
-            }
-            Log.d(LOGTAG, "Widened draw range to [" + mMinFramesToDraw + ", " + mMaxFramesToDraw + "]");
-            return true;
-        }
-
-        @Override
-        public void resetPageState() {
-            mMinFramesToDraw = 0;
-            mMaxFramesToDraw = 2;
-        }
-
-        @Override
-        public String toString() {
-            return "PredictionBiasStrategy threshold=" + VELOCITY_THRESHOLD;
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/DisplayPortMetrics.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/* -*- 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.RectF;
-
-/*
- * This class keeps track of the area we request Gecko to paint, as well
- * as the resolution of the paint. The area may be different from the visible
- * area of the page, and the resolution may be different from the resolution
- * used in the compositor to render the page. This is so that we can ask Gecko
- * to paint a much larger area without using extra memory, and then render some
- * subsection of that with compositor scaling.
- */
-public final class DisplayPortMetrics {
-    @WrapForJNI
-    public final float resolution;
-    @WrapForJNI
-    private final RectF mPosition;
-
-    public DisplayPortMetrics() {
-        this(0, 0, 0, 0, 1);
-    }
-
-    @WrapForJNI
-    public DisplayPortMetrics(float left, float top, float right, float bottom, float resolution) {
-        this.resolution = resolution;
-        mPosition = new RectF(left, top, right, bottom);
-    }
-
-    public float getLeft() {
-        return mPosition.left;
-    }
-
-    public float getTop() {
-        return mPosition.top;
-    }
-
-    public float getRight() {
-        return mPosition.right;
-    }
-
-    public float getBottom() {
-        return mPosition.bottom;
-    }
-
-    public boolean contains(RectF rect) {
-        return mPosition.contains(rect);
-    }
-
-    public boolean fuzzyEquals(DisplayPortMetrics metrics) {
-        return RectUtils.fuzzyEquals(mPosition, metrics.mPosition)
-            && FloatUtils.fuzzyEquals(resolution, metrics.resolution);
-    }
-
-    public String toJSON() {
-        StringBuilder sb = new StringBuilder(256);
-        sb.append("{ \"left\": ").append(mPosition.left)
-          .append(", \"top\": ").append(mPosition.top)
-          .append(", \"right\": ").append(mPosition.right)
-          .append(", \"bottom\": ").append(mPosition.bottom)
-          .append(", \"resolution\": ").append(resolution)
-          .append('}');
-        return sb.toString();
-    }
-
-    @Override
-    public String toString() {
-        return "DisplayPortMetrics v=(" + mPosition.left + "," + mPosition.top + "," + mPosition.right + ","
-                + mPosition.bottom + ") z=" + resolution;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/DrawTimingQueue.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import android.os.SystemClock;
-
-/**
- * A custom-built data structure to assist with measuring draw times.
- *
- * This class maintains a fixed-size circular buffer of DisplayPortMetrics
- * objects and associated timestamps. It provides only three operations, which
- * is all we require for our purposes of measuring draw times. Note
- * in particular that the class is designed so that even though it is
- * accessed from multiple threads, it does not require synchronization;
- * any concurrency errors that result from this are handled gracefully.
- *
- * Assuming an unrolled buffer so that mTail is greater than mHead, the data
- * stored in the buffer at entries [mHead, mTail) will never be modified, and
- * so are "safe" to read. If this reading is done on the same thread that
- * owns mHead, then reading the range [mHead, mTail) is guaranteed to be safe
- * since the range itself will not shrink.
- */
-final class DrawTimingQueue {
-    private static final String LOGTAG = "GeckoDrawTimingQueue";
-    private static final int BUFFER_SIZE = 16;
-
-    private final DisplayPortMetrics[] mMetrics;
-    private final long[] mTimestamps;
-
-    private int mHead;
-    private int mTail;
-
-    DrawTimingQueue() {
-        mMetrics = new DisplayPortMetrics[BUFFER_SIZE];
-        mTimestamps = new long[BUFFER_SIZE];
-        mHead = BUFFER_SIZE - 1;
-    }
-
-    /**
-     * Add a new entry to the tail of the queue. If the buffer is full,
-     * do nothing. This must only be called from the Java UI thread.
-     */
-    boolean add(DisplayPortMetrics metrics) {
-        if (mHead == mTail) {
-            return false;
-        }
-        mMetrics[mTail] = metrics;
-        mTimestamps[mTail] = SystemClock.uptimeMillis();
-        mTail = (mTail + 1) % BUFFER_SIZE;
-        return true;
-    }
-
-    /**
-     * Find the timestamp associated with the given metrics, AND remove
-     * all metrics objects from the start of the queue up to and including
-     * the one provided. Note that because of draw coalescing, the metrics
-     * object passed in here may not be the one at the head of the queue,
-     * and so we must iterate our way through the list to find it.
-     * This must only be called from the compositor thread.
-     */
-    long findTimeFor(DisplayPortMetrics metrics) {
-        // keep a copy of the tail pointer so that we ignore new items
-        // added to the queue while we are searching. this is fine because
-        // the one we are looking for will either have been added already
-        // or will not be in the queue at all.
-        int tail = mTail;
-        // walk through the "safe" range from mHead to tail; these entries
-        // will not be modified unless we change mHead.
-        int i = (mHead + 1) % BUFFER_SIZE;
-        while (i != tail) {
-            if (mMetrics[i].fuzzyEquals(metrics)) {
-                // found it, copy out the timestamp to a local var BEFORE
-                // changing mHead or add could clobber the timestamp.
-                long timestamp = mTimestamps[i];
-                mHead = i;
-                return timestamp;
-            }
-            i = (i + 1) % BUFFER_SIZE;
-        }
-        return -1;
-    }
-
-    /**
-     * Reset the buffer to empty.
-     * This must only be called from the compositor thread.
-     */
-    void reset() {
-        // we can only modify mHead on this thread.
-        mHead = (mTail + BUFFER_SIZE - 1) % BUFFER_SIZE;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/DynamicToolbarAnimator.java
+++ /dev/null
@@ -1,605 +0,0 @@
-/* -*- 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.animation.DecelerateInterpolator;
-import android.view.MotionEvent;
-
-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 = 250000000;
-
-    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 DecelerateInterpolator 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 DecelerateInterpolator();
-
-        // 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);
-
-        // JPZ doesn't notify when scrolling root content. This maintains existing behaviour.
-        if (!AppConstants.MOZ_ANDROID_APZ) {
-            mScrollingRootContent = true;
-        }
-    }
-
-    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()) {
-            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;
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/FloatSize.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/* -*- 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));
-    }
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/FullScreenState.java
+++ /dev/null
@@ -1,12 +0,0 @@
-/* -*- 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
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/GLController.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/* -*- 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.annotation.WrapForJNI;
-import org.mozilla.gecko.mozglue.JNIObject;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.util.Log;
-
-/**
- * This class is a singleton that tracks EGL and compositor things over
- * the lifetime of Fennec running.
- * We only ever create one C++ compositor over Fennec's lifetime, but
- * most of the Java-side objects (e.g. LayerView, GeckoLayerClient,
- * LayerRenderer) can all get destroyed and re-created if the GeckoApp
- * activity is destroyed. This GLController is never destroyed, so that
- * the mCompositorCreated field and other state variables are always
- * accurate.
- */
-public class GLController extends JNIObject {
-    private static final String LOGTAG = "GeckoGLController";
-
-    /* package */ LayerView mView;
-    private boolean mServerSurfaceValid;
-    private int mWidth, mHeight;
-
-    /* This is written by the compositor thread (while the UI thread
-     * is blocked on it) and read by the UI thread. */
-    private volatile boolean mCompositorCreated;
-
-    @WrapForJNI @Override // JNIObject
-    protected native void disposeNative();
-
-    // Gecko thread sets its Java instances; does not block UI thread.
-    @WrapForJNI
-    /* package */ native void attachToJava(GeckoLayerClient layerClient,
-                                           NativePanZoomController npzc);
-
-    @WrapForJNI
-    /* package */ native void onSizeChanged(int windowWidth, int windowHeight,
-                                            int screenWidth, int screenHeight);
-
-    // Gecko thread creates compositor; blocks UI thread.
-    @WrapForJNI
-    private native void createCompositor(int width, int height);
-
-    // Gecko thread pauses compositor; blocks UI thread.
-    @WrapForJNI
-    private native void pauseCompositor();
-
-    // UI thread resumes compositor and notifies Gecko thread; does not block UI thread.
-    @WrapForJNI
-    private native void syncResumeResizeCompositor(int width, int height);
-
-    @WrapForJNI
-    private native void syncInvalidateAndScheduleComposite();
-
-    public GLController() {
-    }
-
-    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) {
-            pauseCompositor();
-        }
-
-        synchronized (this) {
-            mServerSurfaceValid = false;
-        }
-    }
-
-    void serverSurfaceChanged(int newWidth, int newHeight) {
-        ThreadUtils.assertOnUiThread();
-
-        synchronized (this) {
-            mWidth = newWidth;
-            mHeight = newHeight;
-            mServerSurfaceValid = true;
-        }
-
-        updateCompositor();
-    }
-
-    void updateCompositor() {
-        ThreadUtils.assertOnUiThread();
-
-        if (mView == null) {
-            return;
-        }
-
-        if (mCompositorCreated) {
-            // If the compositor has already been created, just resume it instead. We don't need
-            // to block here because if the surface is destroyed before the compositor grabs it,
-            // we can handle that gracefully (i.e. the compositor will remain paused).
-            resumeCompositor(mWidth, mHeight);
-            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. Do it with a synchronous Gecko event so that the
-        // Android doesn't have a chance to destroy our surface in between.
-        if (isServerSurfaceValid() && mView.getLayerClient().isGeckoReady()) {
-            createCompositor(mWidth, mHeight);
-            compositorCreated();
-        }
-    }
-
-    void compositorCreated() {
-        // This is invoked on the compositor thread, while the java UI thread
-        // is blocked on the gecko sync event in updateCompositor() above
-        mCompositorCreated = true;
-    }
-
-    public boolean isServerSurfaceValid() {
-        return mServerSurfaceValid;
-    }
-
-    @WrapForJNI(allowMultithread = true)
-    private synchronized Object getSurface() {
-        if (mView != null && isServerSurfaceValid()) {
-            return mView.getSurface();
-        } else {
-            return null;
-        }
-    }
-
-    void resumeCompositor(int width, int height) {
-        // Asking Gecko to resume the compositor takes too long (see
-        // https://bugzilla.mozilla.org/show_bug.cgi?id=735230#c23), so we
-        // resume the compositor directly. We still need to inform Gecko about
-        // the compositor resuming, so that Gecko knows that it can now draw.
-        // It is important to not notify Gecko until after the compositor has
-        // been resumed, otherwise Gecko may send updates that get dropped.
-        if (isServerSurfaceValid() && mCompositorCreated) {
-            syncResumeResizeCompositor(width, height);
-            mView.requestRender();
-        }
-    }
-
-    /* package */ void invalidateAndScheduleComposite() {
-        if (mCompositorCreated) {
-            syncInvalidateAndScheduleComposite();
-        }
-    }
-
-    @WrapForJNI
-    private void destroy() {
-        // The nsWindow has been closed. First mark our compositor as destroyed.
-        mCompositorCreated = 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() {
-                GLController.this.disposeNative();
-            }
-        });
-    }
-
-    public static class GLControllerException extends RuntimeException {
-        public static final long serialVersionUID = 1L;
-
-        GLControllerException(String e) {
-            super(e);
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
+++ /dev/null
@@ -1,1132 +0,0 @@
-/* -*- 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.GeckoEvent;
-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.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;
-    private DisplayPortMetrics mDisplayPort;
-
-    private boolean mRecordDrawTimes;
-    private final DrawTimingQueue mDrawTimingQueue;
-
-    private VirtualLayer mRootLayer;
-
-    /* The Gecko viewport as per the UI thread. Must be touched only on the UI thread.
-     * If any events being sent to Gecko that are relative to the Gecko viewport position,
-     * they must (a) be relative to this viewport, and (b) be sent on the UI thread to
-     * avoid races. As long as these two conditions are satisfied, and the events being
-     * sent to Gecko are processed in FIFO order, the events will properly be relative
-     * to the Gecko viewport position. Note that if Gecko updates its viewport independently,
-     * we get notified synchronously and also update this on the UI thread.
-     */
-    private ImmutableViewportMetrics mGeckoViewport;
-
-    /*
-     * The viewport metrics being used to draw the current frame. This is only
-     * accessed by the compositor thread, and so needs no synchronisation.
-     */
-    private ImmutableViewportMetrics mFrameMetrics;
-
-    private final List<DrawListener> mDrawListeners;
-
-    /* Used as temporaries by syncViewportInfo */
-    private final ViewTransform mCurrentViewTransform;
-
-    /* Used as the return value of progressiveUpdateCallback */
-    private final ProgressiveUpdateData mProgressiveUpdateData;
-    private DisplayPortMetrics mProgressiveUpdateDisplayPort;
-    private boolean mLastProgressiveUpdateWasLowPrecision;
-    private boolean mProgressiveUpdateWasInDanger;
-
-    private boolean mForceRedraw;
-
-    /* The current viewport metrics.
-     * This is volatile so that we can read and write to it from different threads.
-     * We avoid synchronization to make getting the viewport metrics from
-     * the compositor as cheap as possible. The viewport is immutable so
-     * we don't need to worry about anyone mutating it while we're reading from it.
-     * Specifically:
-     * 1) reading mViewportMetrics from any thread is fine without synchronization
-     * 2) writing to mViewportMetrics requires synchronizing on the layer controller object
-     * 3) 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;
-
-    private final PanZoomController mPanZoomController;
-    private final DynamicToolbarAnimator mToolbarAnimator;
-    private 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;
-
-    public GeckoLayerClient(Context context, LayerView view, EventDispatcher eventDispatcher) {
-        // we can fill these in with dummy values because they are always written
-        // to before being read
-        mContext = context;
-        mScreenSize = new IntSize(0, 0);
-        mWindowSize = new IntSize(0, 0);
-        mDisplayPort = new DisplayPortMetrics();
-        mRecordDrawTimes = true;
-        mDrawTimingQueue = new DrawTimingQueue();
-        mCurrentViewTransform = new ViewTransform(0, 0, 1);
-        mProgressiveUpdateData = new ProgressiveUpdateData();
-        mProgressiveUpdateDisplayPort = new DisplayPortMetrics();
-
-        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);
-    }
-
-    /** Attaches to root layer so that Gecko appears. */
-    /* package */ boolean isGeckoReady() {
-        return mGeckoIsReady;
-    }
-
-    @WrapForJNI
-    private void onGeckoReady() {
-        mGeckoIsReady = true;
-
-        mRootLayer = new VirtualLayer(new IntSize(mView.getWidth(), mView.getHeight()));
-        mLayerRenderer = mView.getRenderer();
-
-        sendResizeEventIfNecessary(true, null);
-
-        DisplayPortCalculator.initPrefs();
-
-        // Gecko being ready is one of the two conditions (along with having an available
-        // surface) that cause us to create the compositor. So here, now that we know gecko
-        // is ready, call updateCompositor() to see if we can actually do the creation.
-        // This needs to run on the UI thread so that the surface validity can't change on
-        // us while we're in the middle of creating the compositor.
-        mView.post(new Runnable() {
-            @Override
-            public void run() {
-                mView.getGLController().updateCompositor();
-            }
-        });
-    }
-
-    public void destroy() {
-        mPanZoomController.destroy();
-        mToolbarAnimator.destroy();
-        mDrawListeners.clear();
-    }
-
-    /**
-     * Returns true if this client is fine with performing a redraw operation or false if it
-     * would prefer that the action didn't take place.
-     */
-    private boolean getRedrawHint() {
-        if (mForceRedraw) {
-            mForceRedraw = false;
-            return true;
-        }
-
-        if (!mPanZoomController.getRedrawHint()) {
-            return false;
-        }
-
-        return DisplayPortCalculator.aboutToCheckerboard(mViewportMetrics,
-                mPanZoomController.getVelocityVector(), mDisplayPort);
-    }
-
-    Layer getRoot() {
-        return mGeckoIsReady ? mRootLayer : null;
-    }
-
-    public LayerView getView() {
-        return mView;
-    }
-
-    public FloatSize getViewportSize() {
-        return mViewportMetrics.getSize();
-    }
-
-    /**
-     * The view calls this function to indicate that the viewport changed size. It must hold the
-     * monitor while calling it.
-     *
-     * TODO: Refactor this to use an interface. Expose that interface only to the view and not
-     * to the layer client. That way, the layer client won't be tempted to call this, which might
-     * result in an infinite loop.
-     */
-    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.getGLController().onSizeChanged(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);
-    }
-
-    /** Sets the current page rect. You must hold the monitor while calling this. */
-    private void setPageRect(RectF rect, RectF cssRect) {
-        // Since the "rect" is always just a multiple of "cssRect" we don't need to
-        // check both; this function assumes that both "rect" and "cssRect" are relative
-        // the zoom factor in mViewportMetrics.
-        if (mViewportMetrics.getCssPageRect().equals(cssRect))
-            return;
-
-        mViewportMetrics = mViewportMetrics.setPageRect(rect, cssRect);
-
-        // Page size is owned by the layer client, so no need to notify it of
-        // this change.
-
-        post(new Runnable() {
-            @Override
-            public void run() {
-                mPanZoomController.pageRectUpdated();
-                mView.requestRender();
-            }
-        });
-    }
-
-    private void adjustViewport(DisplayPortMetrics displayPort) {
-        // TODO: APZ For fennec might need margins information to deal with
-        // the dynamic toolbar.
-        if (AppConstants.MOZ_ANDROID_APZ)
-            return;
-
-        ImmutableViewportMetrics metrics = getViewportMetrics();
-        ImmutableViewportMetrics clampedMetrics = metrics.clamp();
-
-        if (displayPort == null) {
-            displayPort = DisplayPortCalculator.calculate(metrics, mPanZoomController.getVelocityVector());
-        }
-
-        mDisplayPort = displayPort;
-        mGeckoViewport = clampedMetrics;
-
-        if (mRecordDrawTimes) {
-            mDrawTimingQueue.add(displayPort);
-        }
-
-        GeckoAppShell.sendEventToGecko(GeckoEvent.createViewportEvent(clampedMetrics, displayPort));
-    }
-
-    /** Aborts any pan/zoom animation that is currently in progress. */
-    private void abortPanZoomAnimation() {
-        if (mPanZoomController != null) {
-            post(new Runnable() {
-                @Override
-                public void run() {
-                    mPanZoomController.abortAnimation();
-                }
-            });
-        }
-    }
-
-    /**
-     * The different types of Viewport messages handled. All viewport events
-     * expect a display-port to be returned, but can handle one not being
-     * returned.
-     */
-    private enum ViewportMessageType {
-        UPDATE,       // The viewport has changed and should be entirely updated
-        PAGE_SIZE     // The viewport's page-size has changed
-    }
-
-    /** Viewport message handler. */
-    private DisplayPortMetrics handleViewportMessage(ImmutableViewportMetrics messageMetrics, ViewportMessageType type) {
-        synchronized (getLock()) {
-            ImmutableViewportMetrics newMetrics;
-            ImmutableViewportMetrics oldMetrics = getViewportMetrics();
-
-            switch (type) {
-            default:
-            case UPDATE:
-                // Keep the old viewport size
-                newMetrics = messageMetrics.setViewportSize(oldMetrics.viewportRectWidth, oldMetrics.viewportRectHeight);
-                if (mToolbarAnimator.isResizing()) {
-                    // If we're in the middle of a resize, we don't want to clobber
-                    // the scroll offset, so grab the one from the oldMetrics and
-                    // keep using that. We also don't want to abort animations,
-                    // because at that point we're guaranteed to not be animating
-                    // anyway, and calling abortPanZoomAnimation has a nasty
-                    // side-effect of clmaping and clobbering the metrics, which
-                    // we don't want here.
-                    newMetrics = newMetrics.setViewportOrigin(oldMetrics.viewportRectLeft, oldMetrics.viewportRectTop);
-                    break;
-                }
-                if (!oldMetrics.fuzzyEquals(newMetrics)) {
-                    abortPanZoomAnimation();
-                }
-                break;
-            case PAGE_SIZE:
-                // adjust the page dimensions to account for differences in zoom
-                // between the rendered content (which is what Gecko tells us)
-                // and our zoom level (which may have diverged).
-                float scaleFactor = oldMetrics.zoomFactor / messageMetrics.zoomFactor;
-                newMetrics = oldMetrics.setPageRect(RectUtils.scale(messageMetrics.getPageRect(), scaleFactor), messageMetrics.getCssPageRect());
-                break;
-            }
-
-            // Update the Gecko-side viewport metrics. Make sure to do this
-            // before modifying the metrics below.
-            final ImmutableViewportMetrics geckoMetrics = newMetrics.clamp();
-            post(new Runnable() {
-                @Override
-                public void run() {
-                    mGeckoViewport = geckoMetrics;
-                }
-            });
-
-            setViewportMetrics(newMetrics, type == ViewportMessageType.UPDATE);
-            mDisplayPort = DisplayPortCalculator.calculate(getViewportMetrics(), null);
-        }
-        return mDisplayPort;
-    }
-
-    @WrapForJNI
-    DisplayPortMetrics getDisplayPort(boolean pageSizeUpdate, boolean isBrowserContentDisplayed, int tabId, ImmutableViewportMetrics metrics) {
-        return null;
-    }
-
-    @WrapForJNI
-    void contentDocumentChanged() {
-        mContentDocumentIsDisplayed = false;
-    }
-
-    @WrapForJNI
-    boolean isContentDocumentDisplayed() {
-        return mContentDocumentIsDisplayed;
-    }
-
-    // This is called on the Gecko thread to determine if we're still interested
-    // in the update of this display-port to continue. We can return true here
-    // to abort the current update and continue with any subsequent ones. This
-    // is useful for slow-to-render pages when the display-port starts lagging
-    // behind enough that continuing to draw it is wasted effort.
-    @WrapForJNI(allowMultithread = true)
-    public ProgressiveUpdateData progressiveUpdateCallback(boolean aHasPendingNewThebesContent,
-                                                           float x, float y, float width, float height,
-                                                           float resolution, boolean lowPrecision) {
-        // Reset the checkerboard risk flag when switching to low precision
-        // rendering.
-        if (lowPrecision && !mLastProgressiveUpdateWasLowPrecision) {
-            // Skip low precision rendering until we're at risk of checkerboarding.
-            if (!mProgressiveUpdateWasInDanger) {
-                mProgressiveUpdateData.abort = true;
-                return mProgressiveUpdateData;
-            }
-            mProgressiveUpdateWasInDanger = false;
-        }
-        mLastProgressiveUpdateWasLowPrecision = lowPrecision;
-
-        // Grab a local copy of the last display-port sent to Gecko and the
-        // current viewport metrics to avoid races when accessing them.
-        DisplayPortMetrics displayPort = mDisplayPort;
-        ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
-        mProgressiveUpdateData.setViewport(viewportMetrics);
-        mProgressiveUpdateData.abort = false;
-
-        // Always abort updates if the resolution has changed. There's no use
-        // in drawing at the incorrect resolution.
-        if (!FloatUtils.fuzzyEquals(resolution, viewportMetrics.zoomFactor)) {
-            Log.d(LOGTAG, "Aborting draw due to resolution change: " + resolution + " != " + viewportMetrics.zoomFactor);
-            mProgressiveUpdateData.abort = true;
-            return mProgressiveUpdateData;
-        }
-
-        // Store the high precision displayport for comparison when doing low
-        // precision updates.
-        if (!lowPrecision) {
-            if (!FloatUtils.fuzzyEquals(resolution, mProgressiveUpdateDisplayPort.resolution) ||
-                !FloatUtils.fuzzyEquals(x, mProgressiveUpdateDisplayPort.getLeft()) ||
-                !FloatUtils.fuzzyEquals(y, mProgressiveUpdateDisplayPort.getTop()) ||
-                !FloatUtils.fuzzyEquals(x + width, mProgressiveUpdateDisplayPort.getRight()) ||
-                !FloatUtils.fuzzyEquals(y + height, mProgressiveUpdateDisplayPort.getBottom())) {
-                mProgressiveUpdateDisplayPort =
-                    new DisplayPortMetrics(x, y, x + width, y + height, resolution);
-            }
-        }
-
-        // If we're not doing low precision draws and we're about to
-        // checkerboard, enable low precision drawing.
-        if (!lowPrecision && !mProgressiveUpdateWasInDanger) {
-            if (DisplayPortCalculator.aboutToCheckerboard(viewportMetrics,
-                  mPanZoomController.getVelocityVector(), mProgressiveUpdateDisplayPort)) {
-                mProgressiveUpdateWasInDanger = true;
-            }
-        }
-
-        // XXX All sorts of rounding happens inside Gecko that becomes hard to
-        //     account exactly for. Given we align the display-port to tile
-        //     boundaries (and so they rarely vary by sub-pixel amounts), just
-        //     check that values are within a couple of pixels of the
-        //     display-port bounds.
-
-        // Never abort drawing if we can't be sure we've sent a more recent
-        // display-port. If we abort updating when we shouldn't, we can end up
-        // with blank regions on the screen and we open up the risk of entering
-        // an endless updating cycle.
-        if (Math.abs(displayPort.getLeft() - mProgressiveUpdateDisplayPort.getLeft()) <= 2 &&
-            Math.abs(displayPort.getTop() - mProgressiveUpdateDisplayPort.getTop()) <= 2 &&
-            Math.abs(displayPort.getBottom() - mProgressiveUpdateDisplayPort.getBottom()) <= 2 &&
-            Math.abs(displayPort.getRight() - mProgressiveUpdateDisplayPort.getRight()) <= 2) {
-            return mProgressiveUpdateData;
-        }
-
-        // Abort updates when the display-port no longer contains the visible
-        // area of the page (that is, the viewport cropped by the page
-        // boundaries).
-        // XXX This makes the assumption that we never let the visible area of
-        //     the page fall outside of the display-port.
-        if (Math.max(viewportMetrics.viewportRectLeft, viewportMetrics.pageRectLeft) + 1 < x ||
-            Math.max(viewportMetrics.viewportRectTop, viewportMetrics.pageRectTop) + 1 < y ||
-            Math.min(viewportMetrics.viewportRectRight(), viewportMetrics.pageRectRight) - 1 > x + width ||
-            Math.min(viewportMetrics.viewportRectBottom(), viewportMetrics.pageRectBottom) - 1 > y + height) {
-            Log.d(LOGTAG, "Aborting update due to viewport not in display-port");
-            mProgressiveUpdateData.abort = true;
-
-            // Enable low-precision drawing, as we're likely to be in danger if
-            // this situation has been encountered.
-            mProgressiveUpdateWasInDanger = true;
-
-            return mProgressiveUpdateData;
-        }
-
-        // Abort drawing stale low-precision content if there's a more recent
-        // display-port in the pipeline.
-        if (lowPrecision && !aHasPendingNewThebesContent) {
-          mProgressiveUpdateData.abort = true;
-        }
-        return mProgressiveUpdateData;
-    }
-
-    /** The compositor invokes this function just before compositing a frame where the document
-      * is different from the document composited on the last frame. In these cases, the viewport
-      * information we have in Java is no longer valid and needs to be replaced with the new
-      * viewport information provided. setPageRect will never be invoked on the same frame that
-      * this function is invoked on; and this function will always be called prior to syncViewportInfo.
-      */
-    @WrapForJNI(allowMultithread = true)
-    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. This includes mGeckoViewport and
-            // mViewportMetrics. Usually this information is updated via handleViewportMessage
-            // while we remain on the same document.
-            post(new Runnable() {
-                @Override
-                public void run() {
-                    mGeckoViewport = newMetrics;
-                }
-            });
-
-            setViewportMetrics(newMetrics);
-
-            // At this point, we have just switched to displaying a different document than we
-            // we previously displaying. This means we need to abort any panning/zooming animations
-            // that are in progress and send an updated display port request to browser.js as soon
-            // as possible. The call to PanZoomController.abortAnimation accomplishes this by calling the
-            // forceRedraw function, which sends the viewport to gecko. The display port request is
-            // actually a full viewport update, which is fine because if browser.js has somehow moved to
-            // be out of sync with this first-paint viewport, then we force them back in sync.
-            abortPanZoomAnimation();
-
-            // Indicate that the document is about to be composited so the
-            // LayerView background can be removed.
-            if (mView.getPaintState() == LayerView.PAINT_START) {
-                mView.setPaintState(LayerView.PAINT_BEFORE_FIRST);
-            }
-        }
-        DisplayPortCalculator.resetPageState();
-        mDrawTimingQueue.reset();
-
-        mContentDocumentIsDisplayed = true;
-    }
-
-    /** The compositor invokes this function whenever it determines that the page rect
-      * has changed (based on the information it gets from layout). If setFirstPaintViewport
-      * is invoked on a frame, then this function will not be. For any given frame, this
-      * function will be invoked before syncViewportInfo.
-      */
-    @WrapForJNI(allowMultithread = true)
-    public void setPageRect(float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom) {
-        synchronized (getLock()) {
-            RectF cssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom);
-            float ourZoom = getViewportMetrics().zoomFactor;
-            setPageRect(RectUtils.scale(cssPageRect, ourZoom), cssPageRect);
-            // Here the page size of the document has changed, but the document being displayed
-            // is still the same. Therefore, we don't need to send anything to browser.js; any
-            // changes we need to make to the display port will get sent the next time we call
-            // adjustViewport().
-        }
-    }
-
-    /** The compositor invokes this function on every frame to figure out what part of the
-      * page to display, and to inform Java of the current display port. Since it is called
-      * on every frame, it needs to be ultra-fast.
-      * It avoids taking any locks or allocating any objects. We keep around a
-      * mCurrentViewTransform so we don't need to allocate a new ViewTransform
-      * every time we're called. NOTE: we might be able to return a ImmutableViewportMetrics
-      * which would avoid the copy into mCurrentViewTransform.
-      */
-    @WrapForJNI(allowMultithread = true)
-    public 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 (mRootLayer != null) {
-            mRootLayer.setPositionAndResolution(
-                x, y, x + width, y + height,
-                resolution);
-        }
-
-        if (layersUpdated && mRecordDrawTimes) {
-            // If we got a layers update, that means a draw finished. Check to see if the area drawn matches
-            // one of our requested displayports; if it does calculate the draw time and notify the
-            // DisplayPortCalculator
-            DisplayPortMetrics drawn = new DisplayPortMetrics(x, y, x + width, y + height, resolution);
-            long time = mDrawTimingQueue.findTimeFor(drawn);
-            if (time >= 0) {
-                long now = SystemClock.uptimeMillis();
-                time = now - time;
-                mRecordDrawTimes = DisplayPortCalculator.drawTimeUpdate(time, width * height);
-            }
-        }
-
-        if (layersUpdated) {
-            for (DrawListener listener : mDrawListeners) {
-                listener.drawFinished();
-            }
-        }
-
-        return mCurrentViewTransform;
-    }
-
-    @WrapForJNI(allowMultithread = true)
-    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() {
-                event.offsetLocation(0, mView.getSurfaceTranslation());
-                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
-    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
-    public void synthesizeNativeMouseEvent(int eventType, int screenX, int screenY) {
-        synthesizeNativePointer(InputDevice.SOURCE_MOUSE, PointerInfo.RESERVED_MOUSE_POINTER_ID,
-            eventType, screenX, screenY, 0, 0);
-    }
-
-    @WrapForJNI(allowMultithread = true)
-    public LayerRenderer.Frame createFrame() {
-        // Create the shaders and textures if necessary.
-        if (!mLayerRendererInitialized) {
-            if (mLayerRenderer == null) {
-                return null;
-            }
-            mLayerRenderer.checkMonitoringEnabled();
-            mLayerRenderer.createDefaultProgram();
-            mLayerRendererInitialized = true;
-        }
-
-        try {
-            return mLayerRenderer.createFrame(mFrameMetrics);
-        } catch (Exception e) {
-            Log.w(LOGTAG, e);
-            return null;
-        }
-    }
-
-    @WrapForJNI(allowMultithread = true)
-    public void activateProgram() {
-        mLayerRenderer.activateDefaultProgram();
-    }
-
-    @WrapForJNI(allowMultithread = true)
-    public void deactivateProgramAndRestoreState(boolean enableScissor,
-            int scissorX, int scissorY, int scissorW, int scissorH)
-    {
-        mLayerRenderer.deactivateDefaultProgram();
-        mLayerRenderer.restoreState(enableScissor, scissorX, scissorY, scissorW, scissorH);
-    }
-
-    private void geometryChanged(DisplayPortMetrics displayPort) {
-        /* Let Gecko know if the screensize has changed */
-        sendResizeEventIfNecessary(false, null);
-        if (getRedrawHint()) {
-            adjustViewport(displayPort);
-        }
-    }
-
-    /** Implementation of LayerView.Listener */
-    @Override
-    public void renderRequested() {
-        final GLController glController = mView.getGLController();
-        if (glController != null) {
-            glController.invalidateAndScheduleComposite();
-        }
-    }
-
-    /** Implementation of LayerView.Listener */
-    @Override
-    public void sizeChanged(int width, int height) {
-        // We need to make sure a draw happens synchronously at this point,
-        // but resizing the surface before the SurfaceView has resized will
-        // cause a visible jump.
-        mView.getGLController().resumeCompositor(width, height);
-    }
-
-    /** Implementation of LayerView.Listener */
-    @Override
-    public void surfaceChanged(int width, int height) {
-        IntSize viewportSize = mToolbarAnimator.getViewportSize();
-        setViewportSize(viewportSize.width, viewportSize.height, null);
-    }
-
-    /** Implementation of PanZoomTarget */
-    @Override
-    public ImmutableViewportMetrics getViewportMetrics() {
-        return mViewportMetrics;
-    }
-
-    /** Implementation of PanZoomTarget */
-    @Override
-    public FullScreenState getFullScreenState() {
-        return mView.getFullScreenState();
-    }
-
-    /** Implementation of PanZoomTarget */
-    @Override
-    public PointF getVisibleEndOfLayerView() {
-        return mToolbarAnimator.getVisibleEndOfLayerView();
-    }
-
-    /** Implementation of PanZoomTarget */
-    @Override
-    public void setAnimationTarget(ImmutableViewportMetrics metrics) {
-        if (mGeckoIsReady) {
-            // We know what the final viewport of the animation is going to be, so
-            // immediately request a draw of that area by setting the display port
-            // accordingly. This way we should have the content pre-rendered by the
-            // time the animation is done.
-            DisplayPortMetrics displayPort = DisplayPortCalculator.calculate(metrics, null);
-            adjustViewport(displayPort);
-        }
-    }
-
-    /** Implementation of PanZoomTarget
-     * You must hold the monitor while calling this.
-     */
-    @Override
-    public void setViewportMetrics(ImmutableViewportMetrics metrics) {
-        setViewportMetrics(metrics, true);
-    }
-
-    /*
-     * You must hold the monitor while calling this.
-     */
-    private void setViewportMetrics(ImmutableViewportMetrics metrics, boolean notifyGecko) {
-        // This class owns the viewport size and the fixed layer margins; don't let other pieces
-        // of code clobber either of them. The only place the viewport size should ever be
-        // updated is in GeckoLayerClient.setViewportSize, and the only place the margins should
-        // ever be updated is in GeckoLayerClient.setFixedLayerMargins; both of these assign to
-        // mViewportMetrics directly.
-        metrics = metrics.setViewportSize(mViewportMetrics.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(null);
-        }
-    }
-
-    /*
-     * Updates the viewport metrics, overriding the viewport size and margins
-     * which are normally retained when calling setViewportMetrics.
-     * You must hold the monitor while calling this.
-     */
-    void forceViewportMetrics(ImmutableViewportMetrics metrics, boolean notifyGecko, boolean forceRedraw) {
-        if (forceRedraw) {
-            mForceRedraw = true;
-        }
-        mViewportMetrics = metrics;
-        viewportMetricsChanged(notifyGecko);
-    }
-
-    /** Implementation of PanZoomTarget
-     * Scroll the viewport by a certain amount. This will take viewport margins
-     * and margin animation into account. If margins are currently animating,
-     * this will just go ahead and modify the viewport origin, otherwise the
-     * delta will be applied to the margins and the remainder will be applied to
-     * the viewport origin.
-     *
-     * You must hold the monitor while calling this.
-     */
-    @Override
-    public void scrollBy(float dx, float dy) {
-        // Set mViewportMetrics manually so the margin changes take.
-        mViewportMetrics = mViewportMetrics.offsetViewportBy(dx, dy);
-        viewportMetricsChanged(true);
-    }
-
-    /** Implementation of PanZoomTarget */
-    @Override
-    public void panZoomStopped() {
-        mToolbarAnimator.onPanZoomStopped();
-    }
-
-    /** Implementation of PanZoomTarget */
-    @Override
-    public void forceRedraw(DisplayPortMetrics displayPort) {
-        mForceRedraw = true;
-        if (mGeckoIsReady) {
-            geometryChanged(displayPort);
-        }
-    }
-
-    /** Implementation of PanZoomTarget */
-    @Override
-    public boolean post(Runnable action) {
-        return mView.post(action);
-    }
-
-    /** Implementation of PanZoomTarget */
-    @Override
-    public void postRenderTask(RenderTask task) {
-        mView.postRenderTask(task);
-    }
-
-    /** Implementation of PanZoomTarget */
-    @Override
-    public void removeRenderTask(RenderTask task) {
-        mView.removeRenderTask(task);
-    }
-
-    /** Implementation of PanZoomTarget */
-    @Override
-    public Object getLock() {
-        return this;
-    }
-
-    /** Implementation of PanZoomTarget
-     * Converts a point from layer view coordinates to layer coordinates. In other words, given a
-     * point measured in pixels from the top left corner of the layer view, returns the point in
-     * pixels measured from the last scroll position we sent to Gecko, in CSS pixels. Assuming the
-     * events being sent to Gecko are processed in FIFO order, this calculation should always be
-     * correct.
-     */
-    @Override
-    public PointF convertViewPointToLayerPoint(PointF viewPoint) {
-        if (!mGeckoIsReady) {
-            return null;
-        }
-
-        ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
-        PointF origin = viewportMetrics.getOrigin();
-        float zoom = viewportMetrics.zoomFactor;
-        ImmutableViewportMetrics geckoViewport = (AppConstants.MOZ_ANDROID_APZ ? mViewportMetrics : mGeckoViewport);
-        PointF geckoOrigin = geckoViewport.getOrigin();
-        float geckoZoom = geckoViewport.zoomFactor;
-
-        // viewPoint + origin - offset gives the coordinate in device pixels from the top-left corner of the page.
-        // Divided by zoom, this gives us the coordinate in CSS pixels from the top-left corner of the page.
-        // geckoOrigin / geckoZoom is where Gecko thinks it is (scrollTo position) in CSS pixels from
-        // the top-left corner of the page. Subtracting the two gives us the offset of the viewPoint from
-        // the current Gecko coordinate in CSS pixels.
-        PointF layerPoint = new PointF(
-                ((viewPoint.x + origin.x) / zoom) - (geckoOrigin.x / geckoZoom),
-                ((viewPoint.y + origin.y) / zoom) - (geckoOrigin.y / geckoZoom));
-
-        return layerPoint;
-    }
-
-    @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);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java
+++ /dev/null
@@ -1,316 +0,0 @@
-/* -*- 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 final boolean isRTL;
-
-    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;
-        isRTL = false;
-    }
-
-    /** This constructor is used by native code in AndroidJavaWrappers.cpp, be
-     * careful when modifying the signature.
-     */
-    @WrapForJNI(allowMultithread = true)
-    public 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)
-    {
-        this(aPageRectLeft, aPageRectTop,
-             aPageRectRight, aPageRectBottom, aCssPageRectLeft,
-             aCssPageRectTop, aCssPageRectRight, aCssPageRectBottom,
-             aViewportRectLeft, aViewportRectTop, aViewportRectWidth,
-             aViewportRectHeight, aZoomFactor, false);
-    }
-
-    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, boolean aIsRTL)
-    {
-        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;
-        isRTL = aIsRTL;
-    }
-
-    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),
-            t >= 0.5 ? to.isRTL : isRTL);
-    }
-
-    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, isRTL);
-    }
-
-    public ImmutableViewportMetrics setViewportOrigin(float newOriginX, float newOriginY) {
-        return new ImmutableViewportMetrics(
-            pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
-            cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
-            newOriginX, newOriginY, viewportRectWidth, viewportRectHeight,
-            zoomFactor, isRTL);
-    }
-
-    public ImmutableViewportMetrics setZoomFactor(float newZoomFactor) {
-        return new ImmutableViewportMetrics(
-            pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
-            cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
-            viewportRectLeft, viewportRectTop, viewportRectWidth, viewportRectHeight,
-            newZoomFactor, isRTL);
-    }
-
-    public ImmutableViewportMetrics offsetViewportBy(float dx, float dy) {
-        return setViewportOrigin(viewportRectLeft + dx, viewportRectTop + dy);
-    }
-
-    public ImmutableViewportMetrics offsetViewportByAndClamp(float dx, float dy) {
-        if (isRTL) {
-            return setViewportOrigin(
-                Math.min(pageRectRight - getWidth(), Math.max(viewportRectLeft + dx, pageRectLeft)),
-                Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeight())));
-        }
-        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, isRTL);
-    }
-
-    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);
-    }
-
-    public ImmutableViewportMetrics setIsRTL(boolean aIsRTL) {
-        if (isRTL == aIsRTL) {
-            return this;
-        }
-
-        return new ImmutableViewportMetrics(
-            pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
-            cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
-            viewportRectLeft, viewportRectTop, viewportRectWidth, viewportRectHeight,
-            zoomFactor, aIsRTL);
-    }
-
-    /* This will set the zoom factor and re-scale page-size and viewport offset
-     * accordingly. The given focus will remain at the same point on the screen
-     * after scaling.
-     */
-    public ImmutableViewportMetrics scaleTo(float newZoomFactor, PointF focus) {
-        // cssPageRect* is invariant, since we're setting the scale factor
-        // here. The page rect is based on the CSS page rect.
-        float newPageRectLeft = cssPageRectLeft * newZoomFactor;
-        float newPageRectTop = cssPageRectTop * newZoomFactor;
-        float newPageRectRight = cssPageRectLeft + ((cssPageRectRight - cssPageRectLeft) * newZoomFactor);
-        float newPageRectBottom = cssPageRectTop + ((cssPageRectBottom - cssPageRectTop) * newZoomFactor);
-
-        PointF origin = getOrigin();
-        origin.offset(focus.x, focus.y);
-        origin = PointUtils.scale(origin, newZoomFactor / zoomFactor);
-        origin.offset(-focus.x, -focus.y);
-
-        return new ImmutableViewportMetrics(
-            newPageRectLeft, newPageRectTop, newPageRectRight, newPageRectBottom,
-            cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
-            origin.x, origin.y, viewportRectWidth, viewportRectHeight,
-            newZoomFactor, isRTL);
-    }
-
-    /** 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, isRTL);
-    }
-
-    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 + ", rtl=" + isRTL;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/IntSize.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/* -*- 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;
-    }
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/Layer.java
+++ /dev/null
@@ -1,204 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.mozilla.gecko.util.FloatUtils;
-
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.RectF;
-
-import java.nio.FloatBuffer;
-import java.util.concurrent.locks.ReentrantLock;
-
-public abstract class Layer {
-    private final ReentrantLock mTransactionLock;
-    private boolean mInTransaction;
-    private Rect mNewPosition;
-    private float mNewResolution;
-
-    protected Rect mPosition;
-    protected float mResolution;
-
-    public Layer() {
-        this(null);
-    }
-
-    public Layer(IntSize size) {
-        mTransactionLock = new ReentrantLock();
-        if (size == null) {
-            mPosition = new Rect();
-        } else {
-            mPosition = new Rect(0, 0, size.width, size.height);
-        }
-        mResolution = 1.0f;
-    }
-
-    /**
-     * Updates the layer. This returns false if there is still work to be done
-     * after this update.
-     */
-    public final boolean update(RenderContext context) {
-        if (mTransactionLock.isHeldByCurrentThread()) {
-            throw new RuntimeException("draw() called while transaction lock held by this " +
-                                       "thread?!");
-        }
-
-        if (mTransactionLock.tryLock()) {
-            try {
-                performUpdates(context);
-                return true;
-            } finally {
-                mTransactionLock.unlock();
-            }
-        }
-
-        return false;
-    }
-
-    /** Subclasses override this function to draw the layer. */
-    public abstract void draw(RenderContext context);
-
-    /** Given the intrinsic size of the layer, returns the pixel boundaries of the layer rect. */
-    protected RectF getBounds(RenderContext context) {
-        return RectUtils.scale(new RectF(mPosition), context.zoomFactor / mResolution);
-    }
-
-    /**
-     * Call this before modifying the layer. Note that, for TileLayers, "modifying the layer"
-     * includes altering the underlying BufferedImage in any way. Thus you must call this function
-     * before modifying the byte buffer associated with this layer.
-     *
-     * This function may block, so you should never call this on the main UI thread.
-     */
-    public void beginTransaction() {
-        if (mTransactionLock.isHeldByCurrentThread())
-            throw new RuntimeException("Nested transactions are not supported");
-        mTransactionLock.lock();
-        mInTransaction = true;
-        mNewResolution = mResolution;
-    }
-
-    /** Call this when you're done modifying the layer. */
-    public void endTransaction() {
-        if (!mInTransaction)
-            throw new RuntimeException("endTransaction() called outside a transaction");
-        mInTransaction = false;
-        mTransactionLock.unlock();
-    }
-
-    /** Returns true if the layer is currently in a transaction and false otherwise. */
-    protected boolean inTransaction() {
-        return mInTransaction;
-    }
-
-    /** Returns the current layer position. */
-    public Rect getPosition() {
-        return mPosition;
-    }
-
-    /** Sets the position. Only valid inside a transaction. */
-    public void setPosition(Rect newPosition) {
-        if (!mInTransaction)
-            throw new RuntimeException("setPosition() is only valid inside a transaction");
-        mNewPosition = newPosition;
-    }
-
-    /** Returns the current layer's resolution. */
-    public float getResolution() {
-        return mResolution;
-    }
-
-    /**
-     * Sets the layer resolution. This value is used to determine how many pixels per
-     * device pixel this layer was rendered at. This will be reflected by scaling by
-     * the reciprocal of the resolution in the layer's transform() function.
-     * Only valid inside a transaction. */
-    public void setResolution(float newResolution) {
-        if (!mInTransaction)
-            throw new RuntimeException("setResolution() is only valid inside a transaction");
-        mNewResolution = newResolution;
-    }
-
-    /**
-     * Subclasses may override this method to perform custom layer updates. This will be called
-     * with the transaction lock held. Subclass implementations of this method must call the
-     * superclass implementation. Returns false if there is still work to be done after this
-     * update is complete.
-     */
-    protected void performUpdates(RenderContext context) {
-        if (mNewPosition != null) {
-            mPosition = mNewPosition;
-            mNewPosition = null;
-        }
-        if (mNewResolution != 0.0f) {
-            mResolution = mNewResolution;
-            mNewResolution = 0.0f;
-        }
-    }
-
-    /**
-     * This function fills in the provided <tt>dest</tt> array with values to render a texture.
-     * The array is filled with 4 sets of {x, y, z, texture_x, texture_y} values (so 20 values
-     * in total) corresponding to the corners of the rect.
-     */
-    protected final void fillRectCoordBuffer(float[] dest, RectF rect, float viewWidth, float viewHeight,
-                                             Rect cropRect, float texWidth, float texHeight) {
-        //x, y, z, texture_x, texture_y
-        dest[0] = rect.left / viewWidth;
-        dest[1] = rect.bottom / viewHeight;
-        dest[2] = 0;
-        dest[3] = cropRect.left / texWidth;
-        dest[4] = cropRect.top / texHeight;
-
-        dest[5] = rect.left / viewWidth;
-        dest[6] = rect.top / viewHeight;
-        dest[7] = 0;
-        dest[8] = cropRect.left / texWidth;
-        dest[9] = cropRect.bottom / texHeight;
-
-        dest[10] = rect.right / viewWidth;
-        dest[11] = rect.bottom / viewHeight;
-        dest[12] = 0;
-        dest[13] = cropRect.right / texWidth;
-        dest[14] = cropRect.top / texHeight;
-
-        dest[15] = rect.right / viewWidth;
-        dest[16] = rect.top / viewHeight;
-        dest[17] = 0;
-        dest[18] = cropRect.right / texWidth;
-        dest[19] = cropRect.bottom / texHeight;
-    }
-
-    public static class RenderContext {
-        public final RectF viewport;
-        public final RectF pageRect;
-        public final float zoomFactor;
-        public final int positionHandle;
-        public final int textureHandle;
-        public final FloatBuffer coordBuffer;
-
-        public RenderContext(RectF aViewport, RectF aPageRect, float aZoomFactor,
-                             int aPositionHandle, int aTextureHandle, FloatBuffer aCoordBuffer) {
-            viewport = aViewport;
-            pageRect = aPageRect;
-            zoomFactor = aZoomFactor;
-            positionHandle = aPositionHandle;
-            textureHandle = aTextureHandle;
-            coordBuffer = aCoordBuffer;
-        }
-
-        public boolean fuzzyEquals(RenderContext other) {
-            if (other == null) {
-                return false;
-            }
-            return RectUtils.fuzzyEquals(viewport, other.viewport)
-                && RectUtils.fuzzyEquals(pageRect, other.pageRect)
-                && FloatUtils.fuzzyEquals(zoomFactor, other.zoomFactor);
-        }
-    }
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/LayerRenderer.java
+++ /dev/null
@@ -1,510 +0,0 @@
-/* -*- 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.gfx.Layer.RenderContext;
-import org.mozilla.gecko.mozglue.DirectBufferAllocator;
-
-import android.graphics.Color;
-import android.graphics.RectF;
-import android.opengl.GLES20;
-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";
-    private static final String PROFTAG = "GeckoLayerRendererProf";
-
-    private static final int NANOS_PER_SECOND = 1000000000;
-
-    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 int mBackgroundColor;
-
-    private long mLastFrameTime;
-    private final CopyOnWriteArrayList<RenderTask> mTasks;
-
-    private final CopyOnWriteArrayList<Layer> mExtraLayers = new CopyOnWriteArrayList<Layer>();
-
-    // Render profiling output
-    private int mFramesRendered;
-    private float mCompleteFramesRendered;
-    private boolean mProfileRender;
-    private long mProfileOutputTime;
-
-    private IntBuffer mPixelBuffer;
-
-    // Used by GLES 2.0
-    private int mProgram;
-    private int mPositionHandle;
-    private int mTextureHandle;
-    private int mSampleHandle;
-    private int mTMatrixHandle;
-
-    private List<LayerView.ZoomedViewListener> mZoomedViewListeners;
-    private float mLastViewLeft;
-    private float mLastViewTop;
-
-    // column-major matrix applied to each vertex to shift the viewport from
-    // one ranging from (-1, -1),(1,1) to (0,0),(1,1) and to scale all sizes by
-    // a factor of 2 to fill up the screen
-    public static final float[] DEFAULT_TEXTURE_MATRIX = {
-        2.0f, 0.0f, 0.0f, 0.0f,
-        0.0f, 2.0f, 0.0f, 0.0f,
-        0.0f, 0.0f, 2.0f, 0.0f,
-        -1.0f, -1.0f, 0.0f, 1.0f
-    };
-
-    private static final int COORD_BUFFER_SIZE = 20;
-
-    // The shaders run on the GPU directly, the vertex shader is only applying the
-    // matrix transform detailed above
-
-    // Note we flip the y-coordinate in the vertex shader from a
-    // coordinate system with (0,0) in the top left to one with (0,0) in
-    // the bottom left.
-
-    public static final String DEFAULT_VERTEX_SHADER =
-        "uniform mat4 uTMatrix;\n" +
-        "attribute vec4 vPosition;\n" +
-        "attribute vec2 aTexCoord;\n" +
-        "varying vec2 vTexCoord;\n" +
-        "void main() {\n" +
-        "    gl_Position = uTMatrix * vPosition;\n" +
-        "    vTexCoord.x = aTexCoord.x;\n" +
-        "    vTexCoord.y = 1.0 - aTexCoord.y;\n" +
-        "}\n";
-
-    // We use highp because the screenshot textures
-    // we use are large and we stretch them alot
-    // so we need all the precision we can get.
-    // Unfortunately, highp is not required by ES 2.0
-    // so on GPU's like Mali we end up getting mediump
-    public static final String DEFAULT_FRAGMENT_SHADER =
-        "precision highp float;\n" +
-        "varying vec2 vTexCoord;\n" +
-        "uniform sampler2D sTexture;\n" +
-        "void main() {\n" +
-        "    gl_FragColor = texture2D(sTexture, vTexCoord);\n" +
-        "}\n";
-
-    public LayerRenderer(LayerView view) {
-        mView = view;
-
-        mTasks = new CopyOnWriteArrayList<RenderTask>();
-        mLastFrameTime = System.nanoTime();
-
-        mZoomedViewListeners = new ArrayList<LayerView.ZoomedViewListener>();
-    }
-
-    public void destroy() {
-        if (mCoordByteBuffer != null) {
-            DirectBufferAllocator.free(mCoordByteBuffer);
-            mCoordByteBuffer = null;
-            mCoordBuffer = null;
-        }
-        mZoomedViewListeners.clear();
-    }
-
-    void onSurfaceCreated(EGLConfig config) {
-        checkMonitoringEnabled();
-        createDefaultProgram();
-        activateDefaultProgram();
-    }
-
-    public void createDefaultProgram() {
-        int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DEFAULT_VERTEX_SHADER);
-        int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DEFAULT_FRAGMENT_SHADER);
-
-        mProgram = GLES20.glCreateProgram();
-        GLES20.glAttachShader(mProgram, vertexShader);   // add the vertex shader to program
-        GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
-        GLES20.glLinkProgram(mProgram);                  // creates OpenGL program executables
-
-        // Get handles to the vertex shader's vPosition, aTexCoord, sTexture, and uTMatrix members.
-        mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
-        mTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTexCoord");
-        mSampleHandle = GLES20.glGetUniformLocation(mProgram, "sTexture");
-        mTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uTMatrix");
-
-        int maxTextureSizeResult[] = new int[1];
-        GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxTextureSizeResult, 0);
-        mMaxTextureSize = maxTextureSizeResult[0];
-    }
-
-    // Activates the shader program.
-    public void activateDefaultProgram() {
-        // Add the program to the OpenGL environment
-        GLES20.glUseProgram(mProgram);
-
-        // Set the transformation matrix
-        GLES20.glUniformMatrix4fv(mTMatrixHandle, 1, false, DEFAULT_TEXTURE_MATRIX, 0);
-
-        // Enable the arrays from which we get the vertex and texture coordinates
-        GLES20.glEnableVertexAttribArray(mPositionHandle);
-        GLES20.glEnableVertexAttribArray(mTextureHandle);
-
-        GLES20.glUniform1i(mSampleHandle, 0);
-
-        // TODO: Move these calls into a separate deactivate() call that is called after the
-        // underlay and overlay are rendered.
-    }
-
-    // Deactivates the shader program. This must be done to avoid crashes after returning to the
-    // Gecko C++ compositor from Java.
-    public void deactivateDefaultProgram() {
-        GLES20.glDisableVertexAttribArray(mTextureHandle);
-        GLES20.glDisableVertexAttribArray(mPositionHandle);
-        GLES20.glUseProgram(0);
-    }
-
-    void restoreState(boolean enableScissor, int scissorX, int scissorY, int scissorW, int scissorH) {
-        GLES20.glScissor(scissorX, scissorY, scissorW, scissorH);
-        if (enableScissor) {
-            GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
-        } else {
-            GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
-        }
-    }
-
-    public int getMaxTextureSize() {
-        return mMaxTextureSize;
-    }
-
-    public void postRenderTask(RenderTask aTask) {
-        mTasks.add(aTask);
-        mView.requestRender();
-    }
-
-    public void removeRenderTask(RenderTask aTask) {
-        mTasks.remove(aTask);
-    }
-
-    private void runRenderTasks(CopyOnWriteArrayList<RenderTask> tasks, boolean after, long frameStartTime) {
-        for (RenderTask task : tasks) {
-            if (task.runAfter != after) {
-                continue;
-            }
-
-            boolean stillRunning = task.run(frameStartTime - mLastFrameTime, frameStartTime);
-
-            // Remove the task from the list if its finished
-            if (!stillRunning) {
-                tasks.remove(task);
-            }
-        }
-    }
-
-    public void addLayer(Layer layer) {
-        synchronized (mExtraLayers) {
-            if (mExtraLayers.contains(layer)) {
-                mExtraLayers.remove(layer);
-            }
-
-            mExtraLayers.add(layer);
-        }
-    }
-
-    public void removeLayer(Layer layer) {
-        synchronized (mExtraLayers) {
-            mExtraLayers.remove(layer);
-        }
-    }
-
-    private void printCheckerboardStats() {
-        Log.d(PROFTAG, "Frames rendered over last 1000ms: " + mCompleteFramesRendered + "/" + mFramesRendered);
-        mFramesRendered = 0;
-        mCompleteFramesRendered = 0;
-    }
-
-    /** Used by robocop for testing purposes. Not for production use! */
-    IntBuffer getPixels() {
-        IntBuffer pixelBuffer = IntBuffer.allocate(mView.getWidth() * mView.getHeight());
-        synchronized (pixelBuffer) {
-            mPixelBuffer = pixelBuffer;
-            mView.requestRender();
-            try {
-                pixelBuffer.wait();
-            } catch (InterruptedException ie) {
-            }
-            mPixelBuffer = null;
-        }
-        return pixelBuffer;
-    }
-
-    private RenderContext createScreenContext(ImmutableViewportMetrics metrics) {
-        RectF viewport = new RectF(0.0f, 0.0f, metrics.getWidth(), metrics.getHeight());
-        RectF pageRect = metrics.getPageRect();
-
-        return createContext(viewport, pageRect, 1.0f);
-    }
-
-    private RenderContext createPageContext(ImmutableViewportMetrics metrics) {
-        RectF viewport = metrics.getViewport();
-        RectF pageRect = metrics.getPageRect();
-        float zoomFactor = metrics.zoomFactor;
-
-        return createContext(new RectF(RectUtils.round(viewport)), pageRect, zoomFactor);
-    }
-
-    private RenderContext createContext(RectF viewport, RectF pageRect, float zoomFactor) {
-        if (mCoordBuffer == null) {
-            // Initialize the FloatBuffer that will be used to store all vertices and texture
-            // coordinates in draw() commands.
-            mCoordByteBuffer = DirectBufferAllocator.allocate(COORD_BUFFER_SIZE * 4);
-            mCoordByteBuffer.order(ByteOrder.nativeOrder());
-            mCoordBuffer = mCoordByteBuffer.asFloatBuffer();
-            if (mCoordBuffer == null) {
-                throw new IllegalStateException();
-            }
-        }
-        return new RenderContext(viewport, pageRect, zoomFactor,
-                                 mPositionHandle, mTextureHandle, mCoordBuffer);
-    }
-
-    void checkMonitoringEnabled() {
-        mProfileRender = Log.isLoggable(PROFTAG, Log.DEBUG);
-    }
-
-    /*
-     * create a vertex shader type (GLES20.GL_VERTEX_SHADER)
-     * or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
-     */
-    public static int loadShader(int type, String shaderCode) {
-        int shader = GLES20.glCreateShader(type);
-        GLES20.glShaderSource(shader, shaderCode);
-        GLES20.glCompileShader(shader);
-        return shader;
-    }
-
-    public Frame createFrame(ImmutableViewportMetrics metrics) {
-        return new Frame(metrics);
-    }
-
-    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;
-        // A rendering context for page-positioned layers, and one for screen-positioned layers.
-        private final RenderContext mPageContext, mScreenContext;
-        // Whether a layer was updated.
-        private boolean mUpdated;
-
-        public Frame(ImmutableViewportMetrics metrics) {
-            mFrameMetrics = metrics;
-
-            // Work out the offset due to margins
-            mPageContext = createPageContext(metrics);
-            mScreenContext = createScreenContext(metrics);
-        }
-
-        /** This function is invoked via JNI; be careful when modifying signature. */
-        @WrapForJNI(allowMultithread = true)
-        public void beginDrawing() {
-            mFrameStartTime = System.nanoTime();
-
-            TextureReaper.get().reap();
-            TextureGenerator.get().fill();
-
-            mUpdated = true;
-
-            Layer rootLayer = mView.getLayerClient().getRoot();
-
-            // Run through pre-render tasks
-            runRenderTasks(mTasks, false, mFrameStartTime);
-
-            /* Update layers. */
-            if (rootLayer != null) {
-                // Called on compositor thread.
-                mUpdated &= rootLayer.update(mPageContext);
-            }
-
-            for (Layer layer : mExtraLayers) {
-                mUpdated &= layer.update(mPageContext); // called on compositor thread
-            }
-        }
-
-        private void clear(int color) {
-            GLES20.glClearColor(((color >> 16) & 0xFF) / 255.0f,
-                                ((color >> 8) & 0xFF) / 255.0f,
-                                (color & 0xFF) / 255.0f,
-                                0.0f);
-            // The bits set here need to match up with those used
-            // in gfx/layers/opengl/LayerManagerOGL.cpp.
-            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT |
-                           GLES20.GL_DEPTH_BUFFER_BIT);
-        }
-
-        /** This function is invoked via JNI; be careful when modifying signature. */
-        @WrapForJNI(allowMultithread = true)
-        public void drawBackground() {
-            // Any GL state which is changed here must be restored in
-            // restoreState(...)
-
-            GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
-
-            // Update background color.
-            mBackgroundColor = mView.getBackgroundColor();
-
-            // Clear the page area to the page background colour.
-            clear(mBackgroundColor);
-        }
-
-        @WrapForJNI(allowMultithread = true)
-        public void drawForeground() {
-            // Any GL state which is changed here must be restored in
-            // restoreState(...)
-
-            /* Draw any extra layers that were added (likely plugins) */
-            if (mExtraLayers.size() > 0) {
-                for (Layer layer : mExtraLayers) {
-                    layer.draw(mPageContext);
-                }
-            }
-
-            /* Measure how much of the screen is checkerboarding */
-            Layer rootLayer = mView.getLayerClient().getRoot();
-            if ((rootLayer != null) &&
-                (mProfileRender || PanningPerfAPI.isRecordingCheckerboard())) {
-                // Calculate the incompletely rendered area of the page
-                float checkerboard =  1.0f - GeckoAppShell.computeRenderIntegrity();
-
-                PanningPerfAPI.recordCheckerboard(checkerboard);
-                if (checkerboard < 0.0f || checkerboard > 1.0f) {
-                    Log.e(LOGTAG, "Checkerboard value out of bounds: " + checkerboard);
-                }
-
-                mCompleteFramesRendered += 1.0f - checkerboard;
-                mFramesRendered ++;
-
-                if (mFrameStartTime - mProfileOutputTime > NANOS_PER_SECOND) {
-                    mProfileOutputTime = mFrameStartTime;
-                    printCheckerboardStats();
-                }
-            }
-
-            runRenderTasks(mTasks, true, mFrameStartTime);
-
-        }
-
-        private void maybeRequestZoomedViewRender(RenderContext context) {
-            // 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 = context.viewport.left;
-            final float viewTop = context.viewport.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(allowMultithread = true)
-        public void endDrawing() {
-            // If a layer update requires further work, schedule another redraw
-            if (!mUpdated)
-                mView.requestRender();
-
-            PanningPerfAPI.recordFrameTime();
-
-            maybeRequestZoomedViewRender(mPageContext);
-
-            /* Used by robocop for testing purposes */
-            IntBuffer pixelBuffer = mPixelBuffer;
-            if (mUpdated && pixelBuffer != null) {
-                synchronized (pixelBuffer) {
-                    pixelBuffer.position(0);
-                    GLES20.glReadPixels(0, 0, (int)mScreenContext.viewport.width(),
-                                        (int)mScreenContext.viewport.height(), GLES20.GL_RGBA,
-                                        GLES20.GL_UNSIGNED_BYTE, pixelBuffer);
-                    pixelBuffer.notify();
-                }
-            }
-
-            // Remove background color once we've painted. GeckoLayerClient is
-            // responsible for setting this flag before current document is
-            // composited.
-            if (mView.getPaintState() == LayerView.PAINT_BEFORE_FIRST) {
-                mView.post(new Runnable() {
-                    @Override
-                    public void run() {
-                        mView.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);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/LayerView.java
+++ /dev/null
@@ -1,721 +0,0 @@
-/* -*- 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.GeckoEvent;
-import org.mozilla.gecko.GeckoThread;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Point;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.SurfaceTexture;
-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.LinearLayout;
-import android.widget.ScrollView;
-
-/**
- * A view rendered by the layer compositor.
- */
-public class LayerView extends ScrollView {
-    private static final String LOGTAG = "GeckoLayerView";
-
-    private GeckoLayerClient mLayerClient;
-    private PanZoomController mPanZoomController;
-    private DynamicToolbarAnimator mToolbarAnimator;
-    private GLController mGLController;
-    private LayerRenderer mRenderer;
-    /* Must be a PAINT_xxx constant */
-    private int mPaintState;
-    private int mBackgroundColor;
-    private FullScreenState mFullScreenState;
-
-    private SurfaceView mSurfaceView;
-    private TextureView mTextureView;
-    private View mFillerView;
-
-    private Listener mListener;
-
-    private PointF mInitialTouchPoint;
-
-    private float mSurfaceTranslation;
-
-    /* This should only be modified on the Java UI thread. */
-    private final Overscroll mOverscroll;
-
-    /* Flags used to determine when to show the painted surface. */
-    public static final int PAINT_START = 0;
-    public static final int PAINT_BEFORE_FIRST = 1;
-    public static final int PAINT_AFTER_FIRST = 2;
-
-    public boolean shouldUseTextureView() {
-        // Disable TextureView support for now as it causes panning/zooming
-        // performance regressions (see bug 792259). Uncomment the code below
-        // once this bug is fixed.
-        return false;
-
-        /*
-        // we can only use TextureView on ICS or higher
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
-            Log.i(LOGTAG, "Not using TextureView: not on ICS+");
-            return false;
-        }
-
-        try {
-            // and then we can only use it if we have a hardware accelerated window
-            Method m = View.class.getMethod("isHardwareAccelerated", (Class[]) null);
-            return (Boolean) m.invoke(this);
-        } catch (Exception e) {
-            Log.i(LOGTAG, "Not using TextureView: caught exception checking for hw accel: " + e.toString());
-            return false;
-        } */
-    }
-
-    public LayerView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-
-        mPaintState = PAINT_START;
-        mBackgroundColor = Color.WHITE;
-        mFullScreenState = FullScreenState.NONE;
-
-        if (Versions.feature14Plus) {
-            mOverscroll = new OverscrollEdgeEffect(this);
-        } else {
-            mOverscroll = null;
-        }
-    }
-
-    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);
-        GeckoAccessibility.setAccessibilityManagerListeners(getContext());
-    }
-
-    /**
-     * 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);
-    }
-
-    private boolean sendEventToGecko(MotionEvent event) {
-        if (!mLayerClient.isGeckoReady()) {
-            return false;
-        }
-
-        int action = event.getActionMasked();
-        PointF point = new PointF(event.getX(), event.getY());
-        if (action == MotionEvent.ACTION_DOWN) {
-            mInitialTouchPoint = point;
-        }
-
-        if (mInitialTouchPoint != null && action == MotionEvent.ACTION_MOVE) {
-            Point p = getEventRadius(event);
-
-            if (PointUtils.subtract(point, mInitialTouchPoint).length() <
-                Math.max(PanZoomController.CLICK_THRESHOLD, Math.min(Math.min(p.x, p.y), PanZoomController.PAN_THRESHOLD))) {
-                // Don't send the touchmove event if if the users finger hasn't moved far.
-                // Necessary for Google Maps to work correctly. See bug 771099.
-                return true;
-            } else {
-                mInitialTouchPoint = null;
-            }
-        }
-
-        GeckoAppShell.sendEventToGecko(GeckoEvent.createMotionEvent(event, false));
-        return true;
-    }
-
-    public void showSurface() {
-        // Fix this if TextureView support is turned back on above
-        mSurfaceView.setVisibility(View.VISIBLE);
-    }
-
-    public void hideSurface() {
-        // Fix this if TextureView support is turned back on above
-        mSurfaceView.setVisibility(View.INVISIBLE);
-    }
-
-    public void destroy() {
-        if (mLayerClient != null) {
-            mLayerClient.destroy();
-        }
-        if (mRenderer != null) {
-            mRenderer.destroy();
-        }
-        if (mGLController != null) {
-            if (mGLController.mView == this) {
-                mGLController.mView = null;
-            }
-            mGLController = null;
-        }
-    }
-
-    @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();
-        }
-        event.offsetLocation(0, -mSurfaceTranslation);
-
-        if (mToolbarAnimator != null && mToolbarAnimator.onInterceptTouchEvent(event)) {
-            if (mPanZoomController != null) {
-                mPanZoomController.onMotionEventVelocity(event.getEventTime(), mToolbarAnimator.getVelocity());
-            }
-            return true;
-        }
-        if (AppConstants.MOZ_ANDROID_APZ && !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 sendEventToGecko(event);
-    }
-
-    @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;
-        }
-
-        event.offsetLocation(0, -mSurfaceTranslation);
-
-        if (AppConstants.MOZ_ANDROID_APZ) {
-            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 sendEventToGecko(event);
-    }
-
-    @Override
-    public boolean onGenericMotionEvent(MotionEvent event) {
-        event.offsetLocation(0, -mSurfaceTranslation);
-
-        if (AndroidGamepadManager.handleMotionEvent(event)) {
-            return true;
-        }
-        if (AppConstants.MOZ_ANDROID_APZ && !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 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);
-
-            // The "filler" view sits behind the URL bar and should never be
-            // visible. It exists solely to make this LayerView actually
-            // scrollable so that we can shift the surface around on the screen.
-            // Once we drop support for pre-Honeycomb Android versions this
-            // should not be needed; we can just turn LayerView back into a
-            // FrameLayout that holds mSurfaceView and nothing else.
-            mFillerView = new View(getContext()) {
-                @Override protected void onMeasure(int aWidthSpec, int aHeightSpec) {
-                    setMeasuredDimension(0, Math.round(mToolbarAnimator.getMaxTranslation()));
-                }
-            };
-            mFillerView.setBackgroundColor(Color.RED);
-
-            LinearLayout container = new LinearLayout(getContext());
-            container.setOrientation(LinearLayout.VERTICAL);
-            container.addView(mFillerView);
-            container.addView(mSurfaceView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
-            addView(container, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
-
-            SurfaceHolder holder = mSurfaceView.getHolder();
-            holder.addCallback(new SurfaceListener());
-        }
-    }
-
-    // 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 void abortPanning() {
-        if (mPanZoomController != null) {
-            mPanZoomController.abortPanning();
-        }
-    }
-
-    public PointF convertViewPointToLayerPoint(PointF viewPoint) {
-        return mLayerClient.convertViewPointToLayerPoint(viewPoint);
-    }
-
-    int getBackgroundColor() {
-        return mBackgroundColor;
-    }
-
-    @Override
-    public void setBackgroundColor(int newColor) {
-        mBackgroundColor = newColor;
-        requestRender();
-    }
-
-    public void setSurfaceBackgroundColor(int newColor) {
-        if (mSurfaceView != null) {
-            mSurfaceView.setBackgroundColor(newColor);
-        }
-    }
-
-    @Override
-    public boolean onKeyDown(int keyCode, KeyEvent event) {
-        if (AppConstants.MOZ_ANDROID_APZ && !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.onKeyEvent(event)) {
-            return true;
-        }
-        return false;
-    }
-
-    public void requestRender() {
-        if (mListener != null) {
-            mListener.renderRequested();
-        }
-    }
-
-    public void addLayer(Layer layer) {
-        mRenderer.addLayer(layer);
-    }
-
-    public void removeLayer(Layer layer) {
-        mRenderer.removeLayer(layer);
-    }
-
-    public void postRenderTask(RenderTask task) {
-        mRenderer.postRenderTask(task);
-    }
-
-    public void removeRenderTask(RenderTask task) {
-        mRenderer.removeRenderTask(task);
-    }
-
-    public int getMaxTextureSize() {
-        return mRenderer.getMaxTextureSize();
-    }
-
-    /** Used by robocop for testing purposes. Not for production use! */
-    @RobocopTarget
-    public IntBuffer getPixels() {
-        return mRenderer.getPixels();
-    }
-
-    /* paintState must be a PAINT_xxx constant. */
-    public void setPaintState(int paintState) {
-        mPaintState = paintState;
-    }
-
-    public int getPaintState() {
-        return mPaintState;
-    }
-
-    public LayerRenderer getRenderer() {
-        return mRenderer;
-    }
-
-    public void setListener(Listener listener) {
-        mListener = listener;
-    }
-
-    Listener getListener() {
-        return mListener;
-    }
-
-    public void setGLController(final GLController glController) {
-        mGLController = glController;
-        glController.mView = this;
-
-        final NativePanZoomController npzc = AppConstants.MOZ_ANDROID_APZ ?
-                (NativePanZoomController) mPanZoomController : null;
-
-        if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
-            glController.attachToJava(mLayerClient, npzc);
-        } else {
-            GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
-                    glController, "attachToJava",
-                    GeckoLayerClient.class, mLayerClient,
-                    NativePanZoomController.class, npzc);
-        }
-    }
-
-    public GLController getGLController() {
-        return mGLController;
-    }
-
-    /* When using a SurfaceView (mSurfaceView != null), resizing happens in two
-     * phases. First, the LayerView changes size, then, often some frames later,
-     * the SurfaceView changes size. Because of this, we need to split the
-     * resize into two phases to avoid jittering.
-     *
-     * The first phase is the LayerView size change. mListener is notified so
-     * that a synchronous draw can be performed (otherwise a blank frame will
-     * appear).
-     *
-     * The second phase is the SurfaceView size change. At this point, the
-     * backing GL surface is resized and another synchronous draw is performed.
-     * Gecko is also sent the new window size, and this will likely cause an
-     * extra draw a few frames later, after it's re-rendered and caught up.
-     *
-     * In the case that there is no valid GL surface (for example, when
-     * resuming, or when coming back from the awesomescreen), or we're using a
-     * TextureView instead of a SurfaceView, the first phase is skipped.
-     */
-    private void onSizeChanged(int width, int height) {
-        if (!mGLController.isServerSurfaceValid() || mSurfaceView == null) {
-            surfaceChanged(width, height);
-            return;
-        }
-
-        if (mListener != null) {
-            mListener.sizeChanged(width, height);
-        }
-
-        if (mOverscroll != null) {
-            mOverscroll.setSize(width, height);
-        }
-    }
-
-    private void surfaceChanged(int width, int height) {
-        mGLController.serverSurfaceChanged(width, height);
-
-        if (mListener != null) {
-            mListener.surfaceChanged(width, height);
-        }
-
-        if (mOverscroll != null) {
-            mOverscroll.setSize(width, height);
-        }
-    }
-
-    private void onDestroyed() {
-        mGLController.serverSurfaceDestroyed();
-    }
-
-    public Object getNativeWindow() {
-        if (mSurfaceView != null)
-            return mSurfaceView.getHolder();
-
-        return mTextureView.getSurfaceTexture();
-    }
-
-    public Object getSurface() {
-      if (mSurfaceView != null) {
-        return mSurfaceView.getHolder().getSurface();
-      }
-      return null;
-    }
-
-    //This method is called on the Gecko main thread.
-    @WrapForJNI(allowMultithread = true, stubName = "updateZoomedView")
-    public 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 renderRequested();
-        void sizeChanged(int width, int height);
-        void surfaceChanged(int width, int height);
-    }
-
-    private class SurfaceListener implements SurfaceHolder.Callback {
-        @Override
-        public void surfaceChanged(SurfaceHolder holder, int format, int width,
-                                                int height) {
-            onSizeChanged(width, height);
-        }
-
-        @Override
-        public void surfaceCreated(SurfaceHolder holder) {
-        }
-
-        @Override
-        public void surfaceDestroyed(SurfaceHolder holder) {
-            onDestroyed();
-        }
-    }
-
-    @Override
-    protected void onMeasure(int aWidthSpec, int aHeightSpec) {
-        super.onMeasure(aWidthSpec, aHeightSpec);
-        if (mSurfaceView != null) {
-            // Because of the crazy setup where this LayerView is a ScrollView
-            // and the SurfaceView is inside a LinearLayout, the SurfaceView
-            // doesn't get the right information to size itself the way we want.
-            // We always want it to be the same size as this LayerView, so we
-            // use a hack to make sure it sizes itself that way.
-            ((LayerSurfaceView)mSurfaceView).overrideSize(getMeasuredWidth(), getMeasuredHeight());
-        }
-    }
-
-    /* A subclass of SurfaceView to listen to layout changes, as
-     * View.OnLayoutChangeListener requires API level 11.
-     */
-    private class LayerSurfaceView extends SurfaceView {
-        private LayerView mParent;
-        private int mForcedWidth;
-        private int mForcedHeight;
-
-        public LayerSurfaceView(Context aContext, LayerView aParent) {
-            super(aContext);
-            mParent = aParent;
-        }
-
-        void overrideSize(int aWidth, int aHeight) {
-            if (mForcedWidth != aWidth || mForcedHeight != aHeight) {
-                mForcedWidth = aWidth;
-                mForcedHeight = aHeight;
-                requestLayout();
-            }
-        }
-
-        @Override
-        protected void onMeasure(int aWidthSpec, int aHeightSpec) {
-            setMeasuredDimension(mForcedWidth, mForcedHeight);
-        }
-
-        @Override
-        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-            super.onLayout(changed, left, top, right, bottom);
-            if (changed && mParent.mGLController.isServerSurfaceValid()) {
-                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);
-        if (mPanZoomController != null) {
-            mPanZoomController.setOverScrollMode(overscrollMode);
-        }
-    }
-
-    @Override
-    public int getOverScrollMode() {
-        if (mPanZoomController != null) {
-            return mPanZoomController.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(this, gainFocus);
-    }
-
-    public void setFullScreenState(FullScreenState state) {
-        mFullScreenState = state;
-    }
-
-    public boolean isFullScreen() {
-        return mFullScreenState != FullScreenState.NONE;
-    }
-
-    public FullScreenState getFullScreenState() {
-        return mFullScreenState;
-    }
-
-    public void setMaxTranslation(float aMaxTranslation) {
-        mToolbarAnimator.setMaxTranslation(aMaxTranslation);
-        if (mFillerView != null) {
-            mFillerView.requestLayout();
-        }
-    }
-
-    public void setSurfaceTranslation(float translation) {
-        // Once we drop support for pre-Honeycomb Android versions, we can
-        // revert bug 1197811 and just use ViewHelper here.
-        if (mSurfaceTranslation != translation) {
-            mSurfaceTranslation = translation;
-            scrollTo(0, Math.round(mToolbarAnimator.getMaxTranslation() - translation));
-        }
-    }
-
-    public float getSurfaceTranslation() {
-        return mSurfaceTranslation;
-    }
-
-    // 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);
-    }
-
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/NativePanZoomController.java
+++ /dev/null
@@ -1,348 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoEvent;
-import org.mozilla.gecko.GeckoThread;
-import org.mozilla.gecko.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 final PrefsHelper.PrefHandler mPrefsObserver;
-    private long mLastDownTime;
-    private static final float MAX_SCROLL = 0.075f * GeckoAppShell.getDpi();
-
-    @WrapForJNI
-    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
-    private native boolean handleScrollEvent(
-            long time, int metaState,
-            float x, float y,
-            float hScroll, float vScroll);
-
-    @WrapForJNI
-    private native boolean handleMouseEvent(
-            int action, long time, int metaState,
-            float x, float y, int buttons);
-
-    @WrapForJNI
-    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;
-
-        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) {
-        if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
-            return handleMouseEvent(event);
-        } else {
-            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 boolean onKeyEvent(KeyEvent event) {
-        // FIXME implement this
-        return false;
-    }
-
-    @Override
-    public void onMotionEventVelocity(final long aEventTime, final float aSpeedY) {
-        handleMotionEventVelocity(aEventTime, aSpeedY);
-    }
-
-    @Override
-    public PointF getVelocityVector() {
-        // FIXME implement this
-        return new PointF(0, 0);
-    }
-
-    @Override
-    public void pageRectUpdated() {
-        // no-op in APZC, I think
-    }
-
-    @Override
-    public void abortPanning() {
-        // no-op in APZC, I think
-    }
-
-    @Override
-    public void notifyDefaultActionPrevented(boolean prevented) {
-        // no-op: This could get called if accessibility is enabled and the events
-        // are sent to Gecko directly without going through APZ. In this case
-        // we just want to ignore this callback.
-    }
-
-    @WrapForJNI(stubName = "AbortAnimation")
-    private native void nativeAbortAnimation();
-
-    @Override // PanZoomController
-    public void abortAnimation()
-    {
-        if (!mDestroyed) {
-            nativeAbortAnimation();
-        }
-    }
-
-    @Override // PanZoomController
-    public boolean getRedrawHint()
-    {
-        // FIXME implement this
-        return true;
-    }
-
-    @Override @WrapForJNI(allowMultithread = true) // PanZoomController
-    public void destroy() {
-        if (mDestroyed) {
-            return;
-        }
-        mDestroyed = true;
-        disposeNative();
-    }
-
-    @Override @WrapForJNI // JNIObject
-    protected native void disposeNative();
-
-    @Override
-    public void setOverScrollMode(int overscrollMode) {
-        // FIXME implement this
-    }
-
-    @Override
-    public int getOverScrollMode() {
-        // FIXME implement this
-        return 0;
-    }
-
-    @WrapForJNI(allowMultithread = true, stubName = "RequestContentRepaintWrapper")
-    private void requestContentRepaint(float x, float y, float width, float height, float resolution) {
-        mTarget.forceRedraw(new DisplayPortMetrics(x, y, x + width, y + height, resolution));
-    }
-
-    @Override
-    public void setOverscrollHandler(final Overscroll handler) {
-        mOverscroll = handler;
-    }
-
-    @WrapForJNI(stubName = "SetIsLongpressEnabled")
-    private native void nativeSetIsLongpressEnabled(boolean isLongpressEnabled);
-
-    @Override // PanZoomController
-    public void setIsLongpressEnabled(boolean isLongpressEnabled) {
-        if (!mDestroyed) {
-            nativeSetIsLongpressEnabled(isLongpressEnabled);
-        }
-    }
-
-    @WrapForJNI(stubName = "AdjustScrollForSurfaceShift")
-    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(allowMultithread = true)
-    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(allowMultithread = true)
-    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
-    private void setScrollingRootContent(final boolean isRootContent) {
-        mTarget.setScrollingRootContent(isRootContent);
-    }
-
-    /**
-     * Active SelectionCaretDrag requires DynamicToolbarAnimator to be pinned
-     * to avoid unwanted scroll interactions.
-     */
-    @WrapForJNI
-    private void onSelectionDragState(boolean state) {
-        mView.getDynamicToolbarAnimator().setPinned(state, PinReason.CARET_DRAG);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/Overscroll.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/* -*- 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);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/* -*- 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;
-        }
-
-        float fillerSize = mView.getDynamicToolbarAnimator().getMaxTranslation();
-        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, fillerSize, 0);
-        }
-
-        if (!mEdges[BOTTOM].isFinished()) {
-            invalidate |= draw(mEdges[BOTTOM], canvas, visibleEnd.x, fillerSize + visibleEnd.y, 180);
-        }
-
-        if (!mEdges[LEFT].isFinished()) {
-            invalidate |= draw(mEdges[LEFT], canvas, 0, fillerSize + visibleEnd.y, 270);
-        }
-
-        if (!mEdges[RIGHT].isFinished()) {
-            invalidate |= draw(mEdges[RIGHT], canvas, visibleEnd.x, fillerSize, 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;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/PanZoomController.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/* -*- 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.PointF;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoAppShell;
-
-public interface PanZoomController {
-    // The distance the user has to pan before we recognize it as such (e.g. to avoid 1-pixel pans
-    // between the touch-down and touch-up of a click). In units of density-independent pixels.
-    public static final float PAN_THRESHOLD = 1 / 16f * GeckoAppShell.getDpi();
-
-    // 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) {
-            if (org.mozilla.gecko.AppConstants.MOZ_ANDROID_APZ) {
-                return new NativePanZoomController(target, view);
-            } else {
-                throw new IllegalStateException("Must set MOZ_ANDROID_APZ");
-            }
-        }
-    }
-
-    public void destroy();
-
-    public boolean onTouchEvent(MotionEvent event);
-    public boolean onMotionEvent(MotionEvent event);
-    public boolean onKeyEvent(KeyEvent event);
-    public void onMotionEventVelocity(final long aEventTime, final float aSpeedY);
-    public void notifyDefaultActionPrevented(boolean prevented);
-
-    public boolean getRedrawHint();
-    public PointF getVelocityVector();
-
-    public void pageRectUpdated();
-    public void abortPanning();
-    public void abortAnimation();
-
-    public void setOverScrollMode(int overscrollMode);
-    public int getOverScrollMode();
-
-    public void setOverscrollHandler(final Overscroll controller);
-
-    public void setIsLongpressEnabled(boolean isLongpressEnabled);
-
-    public ImmutableViewportMetrics adjustScrollForSurfaceShift(ImmutableViewportMetrics aMetrics, PointF aShift);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/PanZoomTarget.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/* -*- 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.PointF;
-import android.graphics.RectF;
-
-public interface PanZoomTarget {
-    public ImmutableViewportMetrics getViewportMetrics();
-    public FullScreenState getFullScreenState();
-    public PointF getVisibleEndOfLayerView();
-
-    public void setAnimationTarget(ImmutableViewportMetrics viewport);
-    public void setViewportMetrics(ImmutableViewportMetrics viewport);
-    public void scrollBy(float dx, float dy);
-    public void panZoomStopped();
-    /** This triggers an (asynchronous) viewport update/redraw. */
-    public void forceRedraw(DisplayPortMetrics displayPort);
-
-    public boolean post(Runnable action);
-    public void postRenderTask(RenderTask task);
-    public void removeRenderTask(RenderTask task);
-    public Object getLock();
-    public PointF convertViewPointToLayerPoint(PointF viewPoint);
-    public void setScrollingRootContent(boolean isRootContent);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/PanningPerfAPI.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/* -*- 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 boolean mRecordingCheckerboard;
-    private static List<Float> mCheckerboardAmounts;
-    private static long mCheckerboardStartTime;
-
-    private static void initialiseRecordingArrays() {
-        if (mFrameTimes == null) {
-            mFrameTimes = new ArrayList<Long>(EXPECTED_FRAME_COUNT);
-        } else {
-            mFrameTimes.clear();
-        }
-        if (mCheckerboardAmounts == null) {
-            mCheckerboardAmounts = new ArrayList<Float>(EXPECTED_FRAME_COUNT);
-        } else {
-            mCheckerboardAmounts.clear();
-        }
-    }
-
-    @RobocopTarget
-    public static void startFrameTimeRecording() {
-        if (mRecordingFrames || mRecordingCheckerboard) {
-            Log.e(LOGTAG, "Error: startFrameTimeRecording() called while already recording!");
-            return;
-        }
-        mRecordingFrames = true;
-        initialiseRecordingArrays();
-        mFrameStartTime = SystemClock.uptimeMillis();
-    }
-
-    @RobocopTarget
-    public static List<Long> stopFrameTimeRecording() {
-        if (!mRecordingFrames) {
-            Log.e(LOGTAG, "Error: stopFrameTimeRecording() called when not recording!");
-            return null;
-        }
-        mRecordingFrames = false;
-        return mFrameTimes;
-    }
-
-    public static void recordFrameTime() {
-        // this will be called often, so try to make it as quick as possible
-        if (mRecordingFrames) {
-            mFrameTimes.add(SystemClock.uptimeMillis() - mFrameStartTime);
-        }
-    }
-
-    public static boolean isRecordingCheckerboard() {
-        return mRecordingCheckerboard;
-    }
-
-    @RobocopTarget
-    public static void startCheckerboardRecording() {
-        if (mRecordingCheckerboard || mRecordingFrames) {
-            Log.e(LOGTAG, "Error: startCheckerboardRecording() called while already recording!");
-            return;
-        }
-        mRecordingCheckerboard = true;
-        initialiseRecordingArrays();
-        mCheckerboardStartTime = SystemClock.uptimeMillis();
-    }
-
-    @RobocopTarget
-    public static List<Float> stopCheckerboardRecording() {
-        if (!mRecordingCheckerboard) {
-            Log.e(LOGTAG, "Error: stopCheckerboardRecording() called when not recording!");
-            return null;
-        }
-        mRecordingCheckerboard = false;
-
-        // We take the number of values in mCheckerboardAmounts here, as there's
-        // the possibility that this function is called while recordCheckerboard
-        // is still executing. As values are added to this list last, we use
-        // this number as the canonical number of recordings.
-        int values = mCheckerboardAmounts.size();
-        if (values == 0) {
-            Log.w(LOGTAG, "stopCheckerboardRecording() found no checkerboard amounts!");
-            return mCheckerboardAmounts;
-        }
-
-        // The score will be the sum of all the values in mCheckerboardAmounts,
-        // so weight the checkerboard values by time so that frame-rate and
-        // run-length don't affect score.
-        long lastTime = 0;
-        float totalTime = mFrameTimes.get(values - 1);
-        for (int i = 0; i < values; i++) {
-            long elapsedTime = mFrameTimes.get(i) - lastTime;
-            mCheckerboardAmounts.set(i, mCheckerboardAmounts.get(i) * elapsedTime / totalTime);
-            lastTime += elapsedTime;
-        }
-
-        return mCheckerboardAmounts;
-    }
-
-    public static void recordCheckerboard(float amount) {
-        // this will be called often, so try to make it as quick as possible
-        if (mRecordingCheckerboard) {
-            mFrameTimes.add(SystemClock.uptimeMillis() - mCheckerboardStartTime);
-            mCheckerboardAmounts.add(amount);
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/PluginLayer.java
+++ /dev/null
@@ -1,167 +0,0 @@
-/* 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.util.FloatUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.view.SurfaceView;
-import android.view.View;
-import android.widget.AbsoluteLayout;
-
-public class PluginLayer extends Layer {
-    private static final String LOGTAG = "PluginLayer";
-
-    private final View mView;
-    private SurfaceView mSurfaceView;
-    private final PluginLayoutParams mLayoutParams;
-    private final AbsoluteLayout mContainer;
-
-    private boolean mDestroyed;
-    private boolean mViewVisible;
-
-    private RectF mLastViewport;
-    private float mLastZoomFactor;
-
-    private static final float TEXTURE_MAP[] = {
-                0.0f, 1.0f, // top left
-                0.0f, 0.0f, // bottom left
-                1.0f, 1.0f, // top right
-                1.0f, 0.0f, // bottom right
-    };
-
-    public PluginLayer(View view, RectF rect, int maxDimension) {
-        super(new IntSize(0, 0));
-
-        mView = view;
-        mContainer = GeckoAppShell.getGeckoInterface().getPluginContainer();
-
-        mView.setWillNotDraw(false);
-        if (mView instanceof SurfaceView) {
-            mSurfaceView = (SurfaceView)view;
-            mSurfaceView.setZOrderOnTop(false);
-            mSurfaceView.setZOrderMediaOverlay(true);
-        }
-
-        mLayoutParams = new PluginLayoutParams(rect, maxDimension);
-    }
-
-    public void setVisible(boolean visible) {
-        if (visible) {
-            showView();
-        } else {
-            hideView();
-        }
-    }
-
-    private void hideView() {
-        if (mViewVisible) {
-            ThreadUtils.postToUiThread(new Runnable() {
-                @Override
-                public void run() {
-                    mView.setVisibility(View.GONE);
-                    mViewVisible = false;
-                }
-            });
-        }
-    }
-
-    public void showView() {
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                if (mContainer.indexOfChild(mView) < 0) {
-                    mContainer.addView(mView, mLayoutParams);
-                } else {
-                    mContainer.updateViewLayout(mView, mLayoutParams);
-                    mView.setVisibility(View.VISIBLE);
-                }
-                mViewVisible = true;
-            }
-        });
-    }
-
-    public void destroy() {
-        mDestroyed = true;
-
-        mContainer.removeView(mView);
-    }
-
-    public void reset(RectF rect) {
-        mLayoutParams.reset(rect);
-    }
-
-    @Override
-    protected void performUpdates(RenderContext context) {
-        if (mDestroyed)
-            return;
-
-        if (!RectUtils.fuzzyEquals(context.viewport, mLastViewport) ||
-            !FloatUtils.fuzzyEquals(context.zoomFactor, mLastZoomFactor)) {
-
-            mLastZoomFactor = context.zoomFactor;
-            mLastViewport = context.viewport;
-            mLayoutParams.reposition(context.viewport, context.zoomFactor);
-
-            showView();
-        }
-    }
-
-    @Override
-    public void draw(RenderContext context) {
-    }
-
-    class PluginLayoutParams extends AbsoluteLayout.LayoutParams
-    {
-        private static final String LOGTAG = "GeckoApp.PluginLayoutParams";
-
-        private RectF mRect;
-        private final int mMaxDimension;
-        private float mLastResolution;
-
-        public PluginLayoutParams(RectF rect, int maxDimension) {
-            super(0, 0, 0, 0);
-
-            mMaxDimension = maxDimension;
-            reset(rect);
-        }
-
-        private void clampToMaxSize() {
-            if (width > mMaxDimension || height > mMaxDimension) {
-                if (width > height) {
-                    height = Math.round(((float) height / width) * mMaxDimension);
-                    width = mMaxDimension;
-                } else {
-                    width = Math.round(((float) width / height) * mMaxDimension);
-                    height = mMaxDimension;
-                }
-            }
-        }
-
-        public void reset(RectF rect) {
-            mRect = rect;
-        }
-
-        public void reposition(RectF viewport, float zoomFactor) {
-
-            RectF scaled = RectUtils.scale(mRect, zoomFactor);
-
-            this.x = Math.round(scaled.left - viewport.left);
-            this.y = Math.round(scaled.top - viewport.top);
-
-            if (!FloatUtils.fuzzyEquals(mLastResolution, zoomFactor)) {
-                width = Math.round(mRect.width() * zoomFactor);
-                height = Math.round(mRect.height() * zoomFactor);
-                mLastResolution = zoomFactor;
-
-                clampToMaxSize();
-            }
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/PointUtils.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/* -*- 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;
-    }
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/ProgressiveUpdateData.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/* -*- 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;
-    }
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/RectUtils.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/* -*- 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);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/RenderTask.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/* -*- 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;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/TextureGenerator.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import android.opengl.GLES20;
-import android.util.Log;
-
-import java.util.concurrent.ArrayBlockingQueue;
-
-import javax.microedition.khronos.egl.EGL10;
-import javax.microedition.khronos.egl.EGLContext;
-
-public class TextureGenerator {
-    private static final String LOGTAG = "TextureGenerator";
-    private static final int POOL_SIZE = 5;
-
-    private static TextureGenerator sSharedInstance;
-
-    private final ArrayBlockingQueue<Integer> mTextureIds;
-    private EGLContext mContext;
-
-    private TextureGenerator() { mTextureIds = new ArrayBlockingQueue<Integer>(POOL_SIZE); }
-
-    public static TextureGenerator get() {
-        if (sSharedInstance == null)
-            sSharedInstance = new TextureGenerator();
-        return sSharedInstance;
-    }
-
-    public synchronized int take() {
-        try {
-            // Will block until one becomes available
-            return (int)mTextureIds.take();
-        } catch (InterruptedException e) {
-            return 0;
-        }
-    }
-
-    public synchronized void fill() {
-        EGL10 egl = (EGL10)EGLContext.getEGL();
-        EGLContext context = egl.eglGetCurrentContext();
-
-        if (mContext != null && mContext != context) {
-            mTextureIds.clear();
-        }
-
-        mContext = context;
-
-        int numNeeded = mTextureIds.remainingCapacity();
-        if (numNeeded == 0)
-            return;
-
-        // Clear existing GL errors
-        int error;
-        while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
-            Log.w(LOGTAG, String.format("Clearing GL error: %#x", error));
-        }
-
-        int[] textures = new int[numNeeded];
-        GLES20.glGenTextures(numNeeded, textures, 0);
-
-        error = GLES20.glGetError();
-        if (error != GLES20.GL_NO_ERROR) {
-            Log.e(LOGTAG, String.format("Failed to generate textures: %#x", error), new Exception());
-            return;
-        }
-
-        for (int i = 0; i < numNeeded; i++) {
-            mTextureIds.offer(textures[i]);
-        }
-    }
-}
-
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/TextureReaper.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import android.opengl.GLES20;
-
-import java.util.ArrayList;
-
-/** Manages a list of dead tiles, so we don't leak resources. */
-public class TextureReaper {
-    private static TextureReaper sSharedInstance;
-    private final ArrayList<Integer> mDeadTextureIDs;
-
-    private TextureReaper() { mDeadTextureIDs = new ArrayList<Integer>(); }
-
-    public static TextureReaper get() {
-        if (sSharedInstance == null)
-            sSharedInstance = new TextureReaper();
-        return sSharedInstance;
-    }
-
-    public void add(int[] textureIDs) {
-        for (int textureID : textureIDs)
-            add(textureID);
-    }
-
-    public void add(int textureID) {
-        mDeadTextureIDs.add(textureID);
-    }
-
-    public void reap() {
-        int numTextures = mDeadTextureIDs.size();
-        // Adreno 200 will generate INVALID_VALUE if len == 0 is passed to glDeleteTextures,
-        // even though it's not supposed to.
-        if (numTextures == 0)
-            return;
-
-        int[] deadTextureIDs = new int[numTextures];
-        for (int i = 0; i < numTextures; i++) {
-            deadTextureIDs[i] = mDeadTextureIDs.get(i);
-        }
-        mDeadTextureIDs.clear();
-
-        GLES20.glDeleteTextures(deadTextureIDs.length, deadTextureIDs, 0);
-    }
-}
-
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/ViewTransform.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/* -*- 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;
-    }
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/VirtualLayer.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-public class VirtualLayer extends Layer {
-    public VirtualLayer(IntSize size) {
-        super(size);
-    }
-
-    @Override
-    public void draw(RenderContext context) {
-        // No-op.
-    }
-
-    void setPositionAndResolution(int left, int top, int right, int bottom, float newResolution) {
-        // This is an optimized version of the following code:
-        // beginTransaction();
-        // try {
-        //     setPosition(new Rect(left, top, right, bottom));
-        //     setResolution(newResolution);
-        //     performUpdates(null);
-        // } finally {
-        //     endTransaction();
-        // }
-
-        // it is safe to drop the transaction lock in this instance (i.e. for the
-        // VirtualLayer that is just a shadow of what gecko is painting) because
-        // the position and resolution of this layer are always touched on the compositor
-        // thread, and therefore do not require synchronization.
-        mPosition.set(left, top, right, bottom);
-        mResolution = newResolution;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/mozglue/ByteBufferInputStream.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/* -*- 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() {
-        mBuf = null;
-        mNativeRef.release();
-    }
-
-    @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;
-    }
-
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/mozglue/DirectBufferAllocator.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/* -*- 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);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/mozglue/GeckoLoader.java
+++ /dev/null
@@ -1,548 +0,0 @@
-/* -*- 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");
-            }
-        }
-
-        if (AppConstants.MOZ_LINKER_EXTRACT) {
-            putenv("MOZ_LINKER_EXTRACT=1");
-            // Ensure that the cache dir is world-writable
-            File cacheDir = new File(linkerCache);
-            if (cacheDir.isDirectory()) {
-                cacheDir.setWritable(true, false);
-                cacheDir.setExecutable(true, false);
-                cacheDir.setReadable(true, false);
-            }
-        }
-    }
-
-    @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);
-    }
-
-    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);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/mozglue/JNIObject.java
+++ /dev/null
@@ -1,11 +0,0 @@
-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();
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/mozglue/NativeReference.java
+++ /dev/null
@@ -1,13 +0,0 @@
-/* -*- 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();
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/mozglue/NativeZip.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/* -*- 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 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;
-    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
-    public void finalize() {
-        release();
-    }
-
-    public void close() {
-        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;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/mozglue/SafeIntent.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * 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;
-
-/**
- * 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 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 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 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;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/permissions/PermissionBlock.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/* -*- 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);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/permissions/Permissions.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/* -*- 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;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/permissions/PermissionsHelper.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/* -*- 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);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/DefaultConfiguration.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/* -*- 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.restrictions;
-
-/**
- * Default implementation of RestrictionConfiguration interface. Used whenever no restrictions are enforced for the
- * current profile.
- */
-public class DefaultConfiguration implements RestrictionConfiguration {
-    @Override
-    public boolean isAllowed(Restrictable restrictable) {
-        if (restrictable == Restrictable.BLOCK_LIST) {
-            return false;
-        }
-
-        return true;
-    }
-
-    @Override
-    public boolean canLoadUrl(String url) {
-        return true;
-    }
-
-    @Override
-    public boolean isRestricted() {
-        return false;
-    }
-
-    @Override
-    public void update() {}
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/GuestProfileConfiguration.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/* -*- 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.restrictions;
-
-import android.net.Uri;
-
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * RestrictionConfiguration implementation for guest profiles.
- */
-public class GuestProfileConfiguration implements RestrictionConfiguration {
-    static List<Restrictable> DISABLED_FEATURES = Arrays.asList(
-            Restrictable.DOWNLOAD,
-            Restrictable.INSTALL_EXTENSION,
-            Restrictable.INSTALL_APPS,
-            Restrictable.BROWSE,
-            Restrictable.SHARE,
-            Restrictable.BOOKMARK,
-            Restrictable.ADD_CONTACT,
-            Restrictable.SET_IMAGE,
-            Restrictable.MODIFY_ACCOUNTS,
-            Restrictable.REMOTE_DEBUGGING,
-            Restrictable.IMPORT_SETTINGS,
-            Restrictable.BLOCK_LIST,
-            Restrictable.DATA_CHOICES,
-            Restrictable.DEFAULT_THEME
-    );
-
-    @SuppressWarnings("serial")
-    private static final List<String> BANNED_SCHEMES = Arrays.asList(
-            "file",
-            "chrome",
-            "resource",
-            "jar",
-            "wyciwyg"
-    );
-
-    private static final List<String> BANNED_URLS = Arrays.asList(
-            "about:config",
-            "about:addons"
-    );
-
-    @Override
-    public boolean isAllowed(Restrictable restrictable) {
-        return !DISABLED_FEATURES.contains(restrictable);
-    }
-
-    @Override
-    public boolean canLoadUrl(String url) {
-        // Null URLs are always permitted.
-        if (url == null) {
-            return true;
-        }
-
-        final Uri u = Uri.parse(url);
-        final String scheme = u.getScheme();
-        if (BANNED_SCHEMES.contains(scheme)) {
-            return false;
-        }
-
-        url = url.toLowerCase();
-        for (String banned : BANNED_URLS) {
-            if (url.startsWith(banned)) {
-                return false;
-            }
-        }
-
-        return true;
-    }
-
-    @Override
-    public boolean isRestricted() {
-        return true;
-    }
-
-    @Override
-    public void update() {}
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictable.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/* -*- 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.restrictions;
-
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.support.annotation.StringRes;
-
-/**
- * This is a list of things we can restrict you from doing. Some of these are reflected in Android UserManager constants.
- * Others are specific to us.
- * These constants should be in sync with the ones from toolkit/components/parentalcontrols/nsIParentalControlsService.idl
- */
-public enum Restrictable {
-    DOWNLOAD(1, "downloads", 0, 0),
-
-    INSTALL_EXTENSION(
-            2, "no_install_extensions",
-            R.string.restrictable_feature_addons_installation,
-            R.string.restrictable_feature_addons_installation_description),
-
-    // UserManager.DISALLOW_INSTALL_APPS
-    INSTALL_APPS(3, "no_install_apps", 0 , 0),
-
-    BROWSE(4, "browse", 0, 0),
-
-    SHARE(5, "share", 0, 0),
-
-    BOOKMARK(6, "bookmark", 0, 0),
-
-    ADD_CONTACT(7, "add_contact", 0, 0),
-
-    SET_IMAGE(8, "set_image", 0, 0),
-
-    // UserManager.DISALLOW_MODIFY_ACCOUNTS
-    MODIFY_ACCOUNTS(9, "no_modify_accounts", 0, 0),
-
-    REMOTE_DEBUGGING(10, "remote_debugging", 0, 0),
-
-    IMPORT_SETTINGS(11, "import_settings", 0, 0),
-
-    PRIVATE_BROWSING(
-            12, "private_browsing",
-            R.string.restrictable_feature_private_browsing,
-            R.string.restrictable_feature_private_browsing_description),
-
-    DATA_CHOICES(13, "data_coices", 0, 0),
-
-    CLEAR_HISTORY(14, "clear_history",
-            R.string.restrictable_feature_clear_history,
-            R.string.restrictable_feature_clear_history_description),
-
-    MASTER_PASSWORD(15, "master_password", 0, 0),
-
-    GUEST_BROWSING(16, "guest_browsing",  0, 0),
-
-    ADVANCED_SETTINGS(17, "advanced_settings",
-            R.string.restrictable_feature_advanced_settings,
-            R.string.restrictable_feature_advanced_settings_description),
-
-    CAMERA_MICROPHONE(18, "camera_microphone",
-            R.string.restrictable_feature_camera_microphone,
-            R.string.restrictable_feature_camera_microphone_description),
-
-    BLOCK_LIST(19, "block_list",
-            R.string.restrictable_feature_block_list,
-            R.string.restrictable_feature_block_list_description),
-
-    TELEMETRY(20, "telemetry",
-            R.string.datareporting_telemetry_title,
-            R.string.datareporting_telemetry_summary),
-
-    HEALTH_REPORT(21, "health_report",
-            R.string.datareporting_fhr_title,
-            R.string.datareporting_fhr_summary2),
-
-    DEFAULT_THEME(22, "default_theme", 0, 0);
-
-    public final int id;
-    public final String name;
-
-    @StringRes
-    public final int title;
-
-    @StringRes
-    public final int description;
-
-    Restrictable(final int id, final String name, @StringRes int title, @StringRes int description) {
-        this.id = id;
-        this.name = name;
-        this.title = title;
-        this.description = description;
-    }
-
-    public String getTitle(Context context) {
-        if (title == 0) {
-            return toString();
-        }
-        return context.getResources().getString(title);
-    }
-
-    public String getDescription(Context context) {
-        if (description == 0) {
-            return null;
-        }
-        return context.getResources().getString(description);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/* -*- 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.restrictions;
-
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.StrictMode;
-import android.os.UserManager;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
-public class RestrictedProfileConfiguration implements RestrictionConfiguration {
-    // Mapping from restrictable feature to default state (on/off)
-    private static Map<Restrictable, Boolean> configuration = new LinkedHashMap<>();
-    static {
-        configuration.put(Restrictable.INSTALL_EXTENSION, false);
-        configuration.put(Restrictable.PRIVATE_BROWSING, false);
-        configuration.put(Restrictable.CLEAR_HISTORY, false);
-        configuration.put(Restrictable.MASTER_PASSWORD, false);
-        configuration.put(Restrictable.GUEST_BROWSING, false);
-        configuration.put(Restrictable.ADVANCED_SETTINGS, false);
-        configuration.put(Restrictable.CAMERA_MICROPHONE, false);
-        configuration.put(Restrictable.DATA_CHOICES, false);
-        configuration.put(Restrictable.BLOCK_LIST, false);
-        configuration.put(Restrictable.TELEMETRY, false);
-        configuration.put(Restrictable.HEALTH_REPORT, true);
-        configuration.put(Restrictable.DEFAULT_THEME, true);
-    }
-
-    /**
-     * These restrictions are hidden from the admin configuration UI.
-     */
-    private static List<Restrictable> hiddenRestrictions = new ArrayList<>();
-    static {
-        hiddenRestrictions.add(Restrictable.MASTER_PASSWORD);
-        hiddenRestrictions.add(Restrictable.GUEST_BROWSING);
-        hiddenRestrictions.add(Restrictable.DATA_CHOICES);
-        hiddenRestrictions.add(Restrictable.DEFAULT_THEME);
-
-        // Hold behind Nightly flag until we have an actual block list deployed.
-        if (!AppConstants.NIGHTLY_BUILD) {
-            hiddenRestrictions.add(Restrictable.BLOCK_LIST);
-        }
-    }
-
-    /* package-private */ static boolean shouldHide(Restrictable restrictable) {
-        return hiddenRestrictions.contains(restrictable);
-    }
-
-    /* package-private */ static Map<Restrictable, Boolean> getConfiguration() {
-        return configuration;
-    }
-
-    private Context context;
-
-    public RestrictedProfileConfiguration(Context context) {
-        this.context = context.getApplicationContext();
-    }
-
-    @Override
-    public synchronized boolean isAllowed(Restrictable restrictable) {
-        // Special casing system/user restrictions
-        if (restrictable == Restrictable.INSTALL_APPS || restrictable == Restrictable.MODIFY_ACCOUNTS) {
-            return RestrictionCache.getUserRestriction(context, restrictable.name);
-        }
-
-        if (!RestrictionCache.hasApplicationRestriction(context, restrictable.name) && !configuration.containsKey(restrictable)) {
-            // Always allow features that are not in the configuration
-            return true;
-        }
-
-        return RestrictionCache.getApplicationRestriction(context, restrictable.name, configuration.get(restrictable));
-    }
-
-    @Override
-    public boolean canLoadUrl(String url) {
-        if (!isAllowed(Restrictable.INSTALL_EXTENSION) && AboutPages.isAboutAddons(url)) {
-            return false;
-        }
-
-        if (!isAllowed(Restrictable.PRIVATE_BROWSING) && AboutPages.isAboutPrivateBrowsing(url)) {
-            return false;
-        }
-
-        if (AboutPages.isAboutConfig(url)) {
-            // Always block access to about:config to prevent circumventing restrictions (Bug 1189233)
-            return false;
-        }
-
-        return true;
-    }
-
-    @Override
-    public boolean isRestricted() {
-        return true;
-    }
-
-    @Override
-    public synchronized void update() {
-        RestrictionCache.invalidate();
-    }
-
-    public static List<Restrictable> getVisibleRestrictions() {
-        final List<Restrictable> visibleList = new ArrayList<>();
-
-        for (Restrictable restrictable : configuration.keySet()) {
-            if (hiddenRestrictions.contains(restrictable)) {
-                continue;
-            }
-            visibleList.add(restrictable);
-        }
-
-        return visibleList;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionCache.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/* -*- 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.restrictions;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.StrictMode;
-import android.os.UserManager;
-
-import org.mozilla.gecko.util.ThreadUtils;
-
-/**
- * Cache for user and application restrictions.
- */
-public class RestrictionCache {
-    private static Bundle cachedAppRestrictions;
-    private static Bundle cachedUserRestrictions;
-    private static boolean isCacheInvalid = true;
-
-    private RestrictionCache() {}
-
-    public static synchronized boolean getUserRestriction(Context context, String restriction) {
-        updateCacheIfNeeded(context);
-        return cachedUserRestrictions.getBoolean(restriction);
-    }
-
-    public static synchronized boolean hasApplicationRestriction(Context context, String restriction) {
-        updateCacheIfNeeded(context);
-        return cachedAppRestrictions.containsKey(restriction);
-    }
-
-    public static synchronized boolean getApplicationRestriction(Context context, String restriction, boolean defaultValue) {
-        updateCacheIfNeeded(context);
-        return cachedAppRestrictions.getBoolean(restriction, defaultValue);
-    }
-
-    public static synchronized boolean hasApplicationRestrictions(Context context) {
-        updateCacheIfNeeded(context);
-        return !cachedAppRestrictions.isEmpty();
-    }
-
-    public static synchronized void invalidate() {
-        isCacheInvalid = true;
-    }
-
-    private static void updateCacheIfNeeded(Context context) {
-        // If we are not on the UI thread then we can just go ahead and read the values (Bug 1189347).
-        // Otherwise we read from the cache to avoid blocking the UI thread. If the cache is invalid
-        // then we hazard the consequences and just do the read.
-        if (isCacheInvalid || !ThreadUtils.isOnUiThread()) {
-            readRestrictions(context);
-            isCacheInvalid = false;
-        }
-    }
-
-    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
-    private static void readRestrictions(Context context) {
-        final UserManager mgr = (UserManager) context.getSystemService(Context.USER_SERVICE);
-
-        // If we do not have anything in the cache yet then this read might happen on the UI thread (Bug 1189347).
-        final StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads();
-
-        try {
-            Bundle appRestrictions = mgr.getApplicationRestrictions(context.getPackageName());
-            migrateRestrictionsIfNeeded(appRestrictions);
-
-            cachedAppRestrictions = appRestrictions;
-            cachedUserRestrictions = mgr.getUserRestrictions(); // Always implies disk read
-        } finally {
-            StrictMode.setThreadPolicy(policy);
-        }
-    }
-
-    /**
-     * This method migrates the old set of DISALLOW_ restrictions to the new restrictable feature ones (Bug 1189336).
-     */
-    /* package-private */ static void migrateRestrictionsIfNeeded(Bundle bundle) {
-        if (!bundle.containsKey(Restrictable.INSTALL_EXTENSION.name) && bundle.containsKey("no_install_extensions")) {
-            bundle.putBoolean(Restrictable.INSTALL_EXTENSION.name, !bundle.getBoolean("no_install_extensions"));
-        }
-
-        if (!bundle.containsKey(Restrictable.PRIVATE_BROWSING.name) && bundle.containsKey("no_private_browsing")) {
-            bundle.putBoolean(Restrictable.PRIVATE_BROWSING.name, !bundle.getBoolean("no_private_browsing"));
-        }
-
-        if (!bundle.containsKey(Restrictable.CLEAR_HISTORY.name) && bundle.containsKey("no_clear_history")) {
-            bundle.putBoolean(Restrictable.CLEAR_HISTORY.name, !bundle.getBoolean("no_clear_history"));
-        }
-
-        if (!bundle.containsKey(Restrictable.ADVANCED_SETTINGS.name) && bundle.containsKey("no_advanced_settings")) {
-            bundle.putBoolean(Restrictable.ADVANCED_SETTINGS.name, !bundle.getBoolean("no_advanced_settings"));
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionConfiguration.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/* -*- 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.restrictions;
-
-/**
- * Interface for classes that Restrictions will delegate to for making decisions.
- */
-public interface RestrictionConfiguration {
-    /**
-     * Is the user allowed to perform this action?
-     */
-    boolean isAllowed(Restrictable restrictable);
-
-    /**
-     * Is the user allowed to load the given URL?
-     */
-    boolean canLoadUrl(String url);
-
-    /**
-     * Is this user restricted in any way?
-     */
-    boolean isRestricted();
-
-    /**
-     * Update restrictions if needed.
-     */
-    void update();
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionProvider.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/* -*- 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.restrictions;
-
-import org.mozilla.gecko.AppConstants;
-
-import android.annotation.TargetApi;
-import android.app.Activity;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.RestrictionEntry;
-import android.os.Build;
-import android.os.Bundle;
-import android.text.TextUtils;
-
-import java.util.ArrayList;
-import java.util.Map;
-
-/**
- * Broadcast receiver providing supported restrictions to the system.
- */
-@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
-public class RestrictionProvider extends BroadcastReceiver {
-    @Override
-    public void onReceive(final Context context, final Intent intent) {
-        if (AppConstants.Versions.preJBMR2) {
-            // This broadcast does not make any sense prior to Jelly Bean MR2.
-            return;
-        }
-
-        final PendingResult result = goAsync();
-
-        new Thread() {
-            @Override
-            public void run() {
-                final Bundle oldRestrictions = intent.getBundleExtra(Intent.EXTRA_RESTRICTIONS_BUNDLE);
-                RestrictionCache.migrateRestrictionsIfNeeded(oldRestrictions);
-
-                final Bundle extras = new Bundle();
-
-                ArrayList<RestrictionEntry> entries = initRestrictions(context, oldRestrictions);
-                extras.putParcelableArrayList(Intent.EXTRA_RESTRICTIONS_LIST, entries);
-
-                result.setResult(Activity.RESULT_OK, null, extras);
-                result.finish();
-            }
-        }.start();
-    }
-
-    private ArrayList<RestrictionEntry> initRestrictions(Context context, Bundle oldRestrictions) {
-        ArrayList<RestrictionEntry> entries = new ArrayList<RestrictionEntry>();
-
-        final Map<Restrictable, Boolean> configuration = RestrictedProfileConfiguration.getConfiguration();
-
-        for (Restrictable restrictable : configuration.keySet()) {
-            if (RestrictedProfileConfiguration.shouldHide(restrictable)) {
-                continue;
-            }
-
-            RestrictionEntry entry = createRestrictionEntryWithDefaultValue(context, restrictable,
-                    oldRestrictions.getBoolean(restrictable.name, configuration.get(restrictable)));
-            entries.add(entry);
-        }
-
-        return entries;
-    }
-
-    private RestrictionEntry createRestrictionEntryWithDefaultValue(Context context, Restrictable restrictable, boolean defaultValue) {
-        RestrictionEntry entry = new RestrictionEntry(restrictable.name, defaultValue);
-
-        entry.setTitle(restrictable.getTitle(context));
-
-        final String description = restrictable.getDescription(context);
-        if (!TextUtils.isEmpty(description)) {
-            entry.setDescription(description);
-        }
-
-        return entry;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictions.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/* -*- 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.restrictions;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build;
-import android.util.Log;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.restrictions.DefaultConfiguration;
-import org.mozilla.gecko.restrictions.GuestProfileConfiguration;
-import org.mozilla.gecko.restrictions.Restrictable;
-import org.mozilla.gecko.restrictions.RestrictedProfileConfiguration;
-import org.mozilla.gecko.restrictions.RestrictionCache;
-import org.mozilla.gecko.restrictions.RestrictionConfiguration;
-
-@RobocopTarget
-public class Restrictions {
-    private static final String LOGTAG = "GeckoRestrictedProfiles";
-
-    private static RestrictionConfiguration configuration;
-
-    private static RestrictionConfiguration getConfiguration(Context context) {
-        if (configuration == null) {
-            configuration = createConfiguration(context);
-        }
-
-        return configuration;
-    }
-
-    public static synchronized RestrictionConfiguration createConfiguration(Context context) {
-        if (configuration != null) {
-            // This method is synchronized and another thread might already have created the configuration.
-            return configuration;
-        }
-
-        if (isGuestProfile(context)) {
-            return new GuestProfileConfiguration();
-        } else if (isRestrictedProfile(context)) {
-            return new RestrictedProfileConfiguration(context);
-        } else {
-            return new DefaultConfiguration();
-        }
-    }
-
-    private static boolean isGuestProfile(Context context) {
-        if (configuration != null) {
-            return configuration instanceof GuestProfileConfiguration;
-        }
-
-        GeckoAppShell.GeckoInterface geckoInterface = GeckoAppShell.getGeckoInterface();
-        if (geckoInterface != null) {
-            return geckoInterface.getProfile().inGuestMode();
-        }
-
-        return GeckoProfile.get(context).inGuestMode();
-    }
-
-    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
-    public static boolean isRestrictedProfile(Context context) {
-        if (configuration != null) {
-            return configuration instanceof RestrictedProfileConfiguration;
-        }
-
-        if (Versions.preJBMR2) {
-            // Early versions don't support restrictions at all
-            return false;
-        }
-
-        // The user is on a restricted profile if, and only if, we injected application restrictions during account setup.
-        return RestrictionCache.hasApplicationRestrictions(context);
-    }
-
-    public static void update(Context context) {
-        getConfiguration(context).update();
-    }
-
-    private static Restrictable geckoActionToRestriction(int action) {
-        for (Restrictable rest : Restrictable.values()) {
-            if (rest.id == action) {
-                return rest;
-            }
-        }
-
-        throw new IllegalArgumentException("Unknown action " + action);
-    }
-
-    private static boolean canLoadUrl(final Context context, final String url) {
-        return getConfiguration(context).canLoadUrl(url);
-    }
-
-    @WrapForJNI
-    public static boolean isUserRestricted() {
-        return isUserRestricted(GeckoAppShell.getApplicationContext());
-    }
-
-    public static boolean isUserRestricted(final Context context) {
-        return getConfiguration(context).isRestricted();
-    }
-
-    public static boolean isAllowed(final Context context, final Restrictable restrictable) {
-        return getConfiguration(context).isAllowed(restrictable);
-    }
-
-    @WrapForJNI
-    public static boolean isAllowed(int action, String url) {
-        final Restrictable restrictable;
-        try {
-            restrictable = geckoActionToRestriction(action);
-        } catch (IllegalArgumentException ex) {
-            // Unknown actions represent a coding error, so we
-            // refuse the action and log.
-            Log.e(LOGTAG, "Unknown action " + action + "; check calling code.");
-            return false;
-        }
-
-        final Context context = GeckoAppShell.getApplicationContext();
-
-        if (Restrictable.BROWSE == restrictable) {
-            return canLoadUrl(context, url);
-        } else {
-            return isAllowed(context, restrictable);
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/sqlite/ByteBufferInputStream.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/* -*- 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;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/sqlite/MatrixBlobCursor.java
+++ /dev/null
@@ -1,366 +0,0 @@
-/* -*- 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.WrapForJNI;
-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
-     */
-    @WrapForJNI
-    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
-     */
-    @WrapForJNI
-    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
-     */
-    @WrapForJNI
-    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
-     */
-    @WrapForJNI
-    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}. */
-    @WrapForJNI
-    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();
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/sqlite/SQLiteBridge.java
+++ /dev/null
@@ -1,387 +0,0 @@
-/* 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();
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/sqlite/SQLiteBridgeException.java
+++ /dev/null
@@ -1,18 +0,0 @@
-/* -*- 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.WrapForJNI;
-
-@WrapForJNI
-public class SQLiteBridgeException extends RuntimeException {
-    static final long serialVersionUID = 1L;
-
-    public SQLiteBridgeException() {}
-    public SQLiteBridgeException(String msg) {
-        super(msg);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/ActivityResultHandler.java
+++ /dev/null
@@ -1,11 +0,0 @@
-/* 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);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/ActivityResultHandlerMap.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/* 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;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/ActivityUtils.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/* -*- 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.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) {
-            final int newVis;
-            if (fullscreen) {
-                newVis = View.SYSTEM_UI_FLAG_FULLSCREEN |
-                         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);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/BundleEventListener.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/* -*- 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);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/Clipboard.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/* 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(stubName = "GetClipboardTextWrapper")
-    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(stubName = "SetClipboardText")
-    public static void setText(final CharSequence text) {
-        ThreadUtils.postToBackgroundThread(new Runnable() {
-            @Override
-            @SuppressWarnings("deprecation")
-            public void run() {
-                // In API Level 11 and above, CLIPBOARD_SERVICE returns android.content.ClipboardManager,
-                // which is a subclass of android.text.ClipboardManager.
-                if (Versions.feature11Plus) {
-                    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;
-                }
-
-                // Deprecated.
-                android.text.ClipboardManager cm = (android.text.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
-                cm.setText(text);
-            }
-        });
-    }
-
-    /**
-     * @return true if the clipboard is nonempty, false otherwise.
-     */
-    @WrapForJNI
-    public static boolean hasText() {
-        if (Versions.feature11Plus) {
-            android.content.ClipboardManager cm = (android.content.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
-            return cm.hasPrimaryClip();
-        }
-
-        // Deprecated.
-        android.text.ClipboardManager cm = (android.text.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
-        return cm.hasText();
-    }
-
-    /**
-     * Deletes all text from the clipboard.
-     */
-    @WrapForJNI
-    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() {
-        if (Versions.feature11Plus) {
-            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();
-                }
-            }
-        } else {
-            android.text.ClipboardManager cm = (android.text.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
-            if (cm.hasText()) {
-                return cm.getText().toString();
-            }
-        }
-        return null;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/ContextUtils.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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;
-
-public class ContextUtils {
-    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");
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/DateUtil.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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));
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/DrawableUtil.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/* -*- 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.content.res.ColorStateList;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.CheckResult;
-import android.support.annotation.ColorInt;
-import android.support.annotation.ColorRes;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.NonNull;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.graphics.drawable.DrawableCompat;
-
-public class DrawableUtil {
-
-    /**
-     * Tints the given drawable with the given color and returns it.
-     */
-    @CheckResult
-    public static Drawable tintDrawable(@NonNull final Context context,
-                                        @DrawableRes final int drawableID,
-                                        @ColorInt final int color) {
-        final Drawable icon = DrawableCompat.wrap(
-                ContextCompat.getDrawable(context, drawableID).mutate());
-        DrawableCompat.setTint(icon, color);
-        return icon;
-    }
-
-    /**
-     * Tints the given drawable with the given color and returns it.
-     */
-    @CheckResult
-    public static Drawable tintDrawableWithColorRes(@NonNull final Context context,
-                                                    @DrawableRes final int drawableID,
-                                                    @ColorRes final int colorID) {
-        return tintDrawable(context, drawableID, ContextCompat.getColor(context, colorID));
-    }
-
-    /**
-     * Tints the given drawable with the given tint list and returns it. Note that you
-     * should no longer use the argument Drawable because the argument is not mutated
-     * on pre-Lollipop devices but is mutated on L+ due to differences in the Support
-     * Library implementation (bug 1193950).
-     */
-    @CheckResult
-    public static Drawable tintDrawableWithStateList(@NonNull final Drawable drawable,
-            @NonNull final ColorStateList colorList) {
-        final Drawable wrappedDrawable = DrawableCompat.wrap(drawable.mutate());
-        DrawableCompat.setTintList(wrappedDrawable, colorList);
-        return wrappedDrawable;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/EventCallback.java
+++ /dev/null
@@ -1,29 +0,0 @@
-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);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/FileUtils.java
+++ /dev/null
@@ -1,259 +0,0 @@
-/* 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;
-            }
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/FloatUtils.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/* -*- 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));
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/GamepadUtils.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/* -*- 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;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/GeckoBackgroundThread.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/* 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);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/GeckoEventListener.java
+++ /dev/null
@@ -1,14 +0,0 @@
-/* -*- 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);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/GeckoJarReader.java
+++ /dev/null
@@ -1,267 +0,0 @@
-/* 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);
-                }
-            }
-            if (zip != null) {
-                zip.close();
-            }
-        }
-
-        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);
-                }
-            }
-            if (zip != null) {
-                zip.close();
-            }
-        }
-
-        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;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/GeckoRequest.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/* 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);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/* -*- 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";
-  // 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(allowMultithread = true, stubName = "FindDecoderCodecInfoForMimeType")
-  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() {
-    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 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(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 decoder.
-              Log.e(LOGTAG, "Found target decoder " + name +
-                  ". Color: 0x" + Integer.toHexString(codecColorFormat));
-              return true;
-            }
-          }
-        }
-      }
-    }
-    return false;  // No HW decoder.
-  }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/HardwareUtils.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/* -*- 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();
-    }
-
-    /**
-     * @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
-        boolean isSystemARM = Build.CPU_ABI != null && Build.CPU_ABI.startsWith("arm");
-        boolean isSystemX86 = Build.CPU_ABI != null && Build.CPU_ABI.startsWith("x86");
-
-        boolean isAppARM = AppConstants.ANDROID_CPU_ARCH.startsWith("arm");
-        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;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/INIParser.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/* 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);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/INISection.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/* 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();
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/IOUtils.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/* -*- 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);
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/InputOptionsUtils.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/* -*- 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;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/IntentUtils.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * 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.NonNull;
-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 Intent unsafeIntent) {
-        // 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 SafeIntent safeIntent = new SafeIntent(unsafeIntent);
-        final HashMap<String, String> out = new HashMap<>();
-        int i = 0;
-        while (true) {
-            final String envKey = "env" + i;
-            i += 1;
-            if (!unsafeIntent.hasExtra(envKey)) {
-                break;
-            }
-
-            maybeAddEnvVarToEnvVarMap(out, safeIntent, 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);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/JSONUtils.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/* 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;
-    }
-
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/MenuUtils.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/* -*- 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);
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/NativeEventListener.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/* -*- 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);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/NativeJSContainer.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/* -*- 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
-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();
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/NativeJSObject.java
+++ /dev/null
@@ -1,533 +0,0 @@
-/* -*- 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
-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();
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/NetworkUtils.java
+++ /dev/null
@@ -1,177 +0,0 @@
-/* -*- 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;
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/NonEvictingLruCache.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/* -*- 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();
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/PrefUtils.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/* -*- 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;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/ProxySelector.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/* 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);
-    }
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/RawResource.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/* 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
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/StringUtils.java
+++ /dev/null
@@ -1,278 +0,0 @@
-/* -*- 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.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;
-        }
-
-        int start = 0;
-        int end = url.length();
-
-        if (url.startsWith("http://")) {
-            start = 7;
-        } else if (url.startsWith("https://") && flags == UrlFlags.STRIP_HTTPS) {
-            start = 8;
-        }
-
-        if (url.endsWith("/")) {
-            end--;
-        }
-
-        return url.substring(start, end);
-    }
-
-    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) {
-        if (Versions.feature11Plus) {
-            return uri.getQueryParameterNames();
-        }
-
-        // Logic below copied from Uri.java included with Android 5.0.0.
-        if (uri.isOpaque()) {
-            throw new UnsupportedOperationException("This isn't a hierarchical URI.");
-        }
-
-        String query = uri.getEncodedQuery();
-        if (query == null) {
-            return Collections.emptySet();
-        }
-
-        Set<String> names = new LinkedHashSet<String>();
-        int start = 0;
-        do {
-            int next = query.indexOf('&', start);
-            int end = (next == -1) ? query.length() : next;
-
-            int separator = query.indexOf('=', start);
-            if (separator > end || separator == -1) {
-                separator = end;
-            }
-
-            String name = query.substring(start, separator);
-            names.add(Uri.decode(name));
-
-            // Move start to end of name.
-            start = end + 1;
-        } while (start < query.length());
-
-        return Collections.unmodifiableSet(names);
-    }
-
-    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()));
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/ThreadUtils.java
+++ /dev/null
@@ -1,247 +0,0 @@
-/* -*- 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);
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/UIAsyncTask.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/* -*- 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);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/UUIDUtil.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * 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);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/WeakReferenceHandler.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/* 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);
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/WebActivityMapper.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/* -*- 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.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Environment;
-import android.provider.MediaStore;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.io.File;
-import java.util.HashMap;
-import java.util.Map;
-
-public final class WebActivityMapper {
-    private static final String LOGTAG = "Gecko";
-
-    private static final Map<String, WebActivityMapping> activityMap = new HashMap<String, WebActivityMapping>();
-    static {
-        activityMap.put("dial", new DialMapping());
-        activityMap.put("open", new OpenMapping());
-        activityMap.put("pick", new PickMapping());
-        activityMap.put("send", new SendMapping());
-        activityMap.put("view", new ViewMapping());
-        activityMap.put("record", new RecordMapping());
-    };
-
-    private static abstract class WebActivityMapping {
-        protected JSONObject mData;
-
-        public void setData(JSONObject data) {
-            mData = data;
-        }
-
-        // Cannot return null
-        public abstract String getAction();
-
-        public String getMime() throws JSONException {
-            return null;
-        }
-
-        public String getUri() throws JSONException {
-            return null;
-        }
-
-        public void putExtras(Intent intent) throws JSONException {}
-    }
-
-    /**
-     * Provides useful defaults for mime type and uri.
-     */
-    private static abstract class BaseMapping extends WebActivityMapping {
-        /**
-         * If 'type' is present in data object, uses the value as the MIME type.
-         */
-        @Override
-        public String getMime() throws JSONException {
-            return mData.optString("type", null);
-        }
-
-        /**
-         * If 'uri' or 'url' is present in data object, uses the respective value as the Uri.
-         */
-        @Override
-        public String getUri() throws JSONException {
-            // Will return uri or url if present.
-            String uri = mData.optString("uri", null);
-            return uri != null ? uri : mData.optString("url", null);
-        }
-    }
-
-    public static Intent getIntentForWebActivity(JSONObject message) throws JSONException {
-        final String name = message.getString("name").toLowerCase();
-        final JSONObject data = message.getJSONObject("data");
-
-        Log.w(LOGTAG, "Activity is: " + name);
-        final WebActivityMapping mapping = activityMap.get(name);
-        if (mapping == null) {
-            Log.w(LOGTAG, "No mapping found!");
-            return null;
-        }
-
-        mapping.setData(data);
-
-        final Intent intent = new Intent(mapping.getAction());
-
-        final String mime = mapping.getMime();
-        if (!TextUtils.isEmpty(mime)) {
-            intent.setType(mime);
-        }
-
-        final String uri = mapping.getUri();
-        if (!TextUtils.isEmpty(uri)) {
-            intent.setData(Uri.parse(uri));
-        }
-
-        mapping.putExtras(intent);
-
-        return intent;
-    }
-
-    private static class DialMapping extends WebActivityMapping {
-        @Override
-        public String getAction() {
-            return Intent.ACTION_DIAL;
-        }
-
-        @Override
-        public String getUri() throws JSONException {
-            return "tel:" + mData.getString("number");
-        }
-    }
-
-    private static class OpenMapping extends BaseMapping {
-        @Override
-        public String getAction() {
-            return Intent.ACTION_VIEW;
-        }
-    }
-
-    private static class PickMapping extends BaseMapping {
-        @Override
-        public String getAction() {
-            return Intent.ACTION_GET_CONTENT;
-        }
-
-        @Override
-        public String getMime() throws JSONException {
-            // bug 1007112 - pick action needs a mimetype to work
-            String mime = mData.optString("type", null);
-            return !TextUtils.isEmpty(mime) ? mime : "*/*";
-        }
-    }
-
-    private static class SendMapping extends BaseMapping {
-        @Override
-        public String getAction() {
-            return Intent.ACTION_SEND;
-        }
-
-        @Override
-        public void putExtras(Intent intent) throws JSONException {
-            optPutExtra("text", Intent.EXTRA_TEXT, intent);
-            optPutExtra("html_text", Intent.EXTRA_HTML_TEXT, intent);
-            optPutExtra("stream", Intent.EXTRA_STREAM, intent);
-        }
-
-        private void optPutExtra(String key, String extraName, Intent intent) {
-            final String extraValue = mData.optString(key);
-            if (!TextUtils.isEmpty(extraValue)) {
-                intent.putExtra(extraName, extraValue);
-            }
-        }
-    }
-
-    private static class ViewMapping extends BaseMapping {
-        @Override
-        public String getAction() {
-            return Intent.ACTION_VIEW;
-        }
-
-        @Override
-        public String getMime() {
-            // MozActivity adds a type 'url' here, we don't want to set the MIME to 'url'.
-            String type = mData.optString("type", null);
-            if ("url".equals(type) || "uri".equals(type)) {
-                return null;
-            } else {
-                return type;
-            }
-        }
-    }
-
-    private static class RecordMapping extends WebActivityMapping {
-        @Override
-        public String getAction() {
-            String type = mData.optString("type", null);
-            if ("photos".equals(type)) {
-                return "android.media.action.IMAGE_CAPTURE";
-            } else if ("videos".equals(type)) {
-                return "android.media.action.VIDEO_CAPTURE";
-            }
-            return null;
-        }
-
-        // Add an extra to specify where to save the picture/video.
-        @Override
-        public void putExtras(Intent intent) {
-            final String action = getAction();
-
-            final String dirType = action == "android.media.action.IMAGE_CAPTURE"
-                ? Environment.DIRECTORY_PICTURES
-                : Environment.DIRECTORY_MOVIES;
-
-            final String ext = action == "android.media.action.IMAGE_CAPTURE"
-                ? ".jpg"
-                : ".mp4";
-
-            File destDir = Environment.getExternalStoragePublicDirectory(dirType);
-
-            try {
-                File dest = File.createTempFile(
-                    "capture", /* prefix */
-                    ext,       /* suffix */
-                    destDir    /* directory */
-                );
-                intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(dest));
-            } catch (Exception e) {
-                Log.w(LOGTAG, "Failed to add extra for " + action + " : " + e);
-            }
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/WindowUtils.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/* -*- 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 if (Versions.feature14Plus) {
-            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);
-
-        } else {
-            // This should be close, as lower API devices should not have window navigation bars.
-            return Math.max(display.getWidth(), display.getHeight());
-        }
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/widget/SwipeDismissListViewTouchListener.java
+++ /dev/null
@@ -1,356 +0,0 @@
-/*
- * Copyright 2012 Roman Nurik
- *
- * 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.widget;
-
-import android.graphics.Rect;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.widget.AbsListView;
-import android.widget.AbsListView.RecyclerListener;
-import android.widget.ListView;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.view.ViewPropertyAnimator;
-
-import org.mozilla.gecko.R;
-
-/**
- * This code is based off of Jake Wharton's NOA port (https://github.com/JakeWharton/SwipeToDismissNOA)
- * of Roman Nurik's SwipeToDismiss library. It has been modified for better support with async
- * adapters.
- *
- * A {@link android.view.View.OnTouchListener} that makes the list items in a {@link ListView}
- * dismissable. {@link ListView} is given special treatment because by default it handles touches
- * for its list items... i.e. it's in charge of drawing the pressed state (the list selector),
- * handling list item clicks, etc.
- *
- * <p>After creating the listener, the caller should also call
- * {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}, passing
- * in the scroll listener returned by {@link #makeScrollListener()}. If a scroll listener is
- * already assigned, the caller should still pass scroll changes through to this listener. This will
- * ensure that this {@link SwipeDismissListViewTouchListener} is paused during list view
- * scrolling.</p>
- *
- * <p>Example usage:</p>
- *
- * <pre>
- * SwipeDismissListViewTouchListener touchListener =
- *         new SwipeDismissListViewTouchListener(
- *                 listView,
- *                 new SwipeDismissListViewTouchListener.OnDismissCallback() {
- *                     public void onDismiss(ListView listView, int[] reverseSortedPositions) {
- *                         for (int position : reverseSortedPositions) {
- *                             adapter.remove(adapter.getItem(position));
- *                         }
- *                         adapter.notifyDataSetChanged();
- *                     }
- *                 });
- * listView.setOnTouchListener(touchListener);
- * listView.setOnScrollListener(touchListener.makeScrollListener());
- * </pre>
- *
- * <p>For a generalized {@link android.view.View.OnTouchListener} that makes any view dismissable,
- * see {@link SwipeDismissTouchListener}.</p>
- *
- * @see SwipeDismissTouchListener
- */
-public class SwipeDismissListViewTouchListener implements View.OnTouchListener {
-    // Cached ViewConfiguration and system-wide constant values
-    private final int mSlop;
-    private final int mMinFlingVelocity;
-    private final int mMaxFlingVelocity;
-    private final long mAnimationTime;
-
-    // Fixed properties
-    private final ListView mListView;
-    private final OnDismissCallback mCallback;
-    private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero
-
-    // Transient properties
-    private float mDownX;
-    private boolean mSwiping;
-    private VelocityTracker mVelocityTracker;
-    private int mDownPosition;
-    private View mDownView;
-    private boolean mPaused;
-    private boolean mDismissing;
-
-    /**
-     * The callback interface used by {@link SwipeDismissListViewTouchListener} to inform its client
-     * about a successful dismissal of a list item.
-     */
-    public interface OnDismissCallback {
-        /**
-         * Called when the user has indicated they she would like to dismiss one or more list item
-         * positions.
-         *
-         * @param listView The originating {@link ListView}.
-         * @param position The position being dismissed.
-         */
-        void onDismiss(ListView listView, int position);
-    }
-
-    /**
-     * Constructs a new swipe-to-dismiss touch listener for the given list view.
-     *
-     * @param listView The list view whose items should be dismissable.
-     * @param callback The callback to trigger when the user has indicated that she would like to
-     *                 dismiss one or more list items.
-     */
-    public SwipeDismissListViewTouchListener(ListView listView, OnDismissCallback callback) {
-        ViewConfiguration vc = ViewConfiguration.get(listView.getContext());
-        mSlop = vc.getScaledTouchSlop();
-        mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
-        mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
-        mAnimationTime = listView.getContext().getResources().getInteger(
-                android.R.integer.config_shortAnimTime);
-        mListView = listView;
-        mCallback = callback;
-    }
-
-    /**
-     * Enables or disables (pauses or resumes) watching for swipe-to-dismiss gestures.
-     *
-     * @param enabled Whether or not to watch for gestures.
-     */
-    public void setEnabled(boolean enabled) {
-        mPaused = !enabled;
-    }
-
-    /**
-     * Returns an {@link android.widget.AbsListView.OnScrollListener} to be added to the
-     * {@link ListView} using
-     * {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}.
-     * If a scroll listener is already assigned, the caller should still pass scroll changes
-     * through to this listener. This will ensure that this
-     * {@link SwipeDismissListViewTouchListener} is paused during list view scrolling.</p>
-     *
-     * @see {@link SwipeDismissListViewTouchListener}
-     */
-    public AbsListView.OnScrollListener makeScrollListener() {
-        return new AbsListView.OnScrollListener() {
-            @Override
-            public void onScrollStateChanged(AbsListView absListView, int scrollState) {
-                setEnabled(scrollState != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
-            }
-
-            @Override
-            public void onScroll(AbsListView absListView, int i, int i1, int i2) {
-            }
-        };
-    }
-
-    /**
-     * Returns a {@link android.widget.AbsListView.RecyclerListener} to be added to the
-     * {@link ListView} using {@link ListView#setRecyclerListener(RecyclerListener)}.
-     */
-    public AbsListView.RecyclerListener makeRecyclerListener() {
-        return new AbsListView.RecyclerListener() {
-            @Override
-            public void onMovedToScrapHeap(View view) {
-                final Object tag = view.getTag(R.id.original_height);
-
-                // To reset the view to the correct height after its animation, the view's height
-                // is stored in its tag. Reset the view here.
-                if (tag instanceof Integer) {
-                    view.setAlpha(1f);
-                    view.setTranslationX(0);
-                    final ViewGroup.LayoutParams lp = view.getLayoutParams();
-                    lp.height = (int) tag;
-                    view.setLayoutParams(lp);
-                    view.setTag(R.id.original_height, null);
-                }
-            }
-        };
-    }
-
-    @Override
-    public boolean onTouch(View view, MotionEvent motionEvent) {
-        if (mViewWidth < 2) {
-            mViewWidth = mListView.getWidth();
-        }
-
-        switch (motionEvent.getActionMasked()) {
-            case MotionEvent.ACTION_DOWN: {
-                if (mPaused) {
-                    return false;
-                }
-
-                if (mDismissing) {
-                    return true;
-                }
-
-                // TODO: ensure this is a finger, and set a flag
-
-                // Find the child view that was touched (perform a hit test)
-                Rect rect = new Rect();
-                int childCount = mListView.getChildCount();
-                int[] listViewCoords = new int[2];
-                mListView.getLocationOnScreen(listViewCoords);
-                int x = (int) motionEvent.getRawX() - listViewCoords[0];
-                int y = (int) motionEvent.getRawY() - listViewCoords[1];
-                View child;
-                for (int i = 0; i < childCount; i++) {
-                    child = mListView.getChildAt(i);
-                    child.getHitRect(rect);
-                    if (rect.contains(x, y)) {
-                        mDownView = child;
-                        break;
-                    }
-                }
-
-                if (mDownView != null) {
-                    mDownX = motionEvent.getRawX();
-                    mDownPosition = mListView.getPositionForView(mDownView);
-
-                    mVelocityTracker = VelocityTracker.obtain();
-                    mVelocityTracker.addMovement(motionEvent);
-                }
-                view.onTouchEvent(motionEvent);
-                return true;
-            }
-
-            case MotionEvent.ACTION_UP: {
-                if (mVelocityTracker == null) {
-                    break;
-                }
-
-                float deltaX = motionEvent.getRawX() - mDownX;
-                mVelocityTracker.addMovement(motionEvent);
-                mVelocityTracker.computeCurrentVelocity(1000);
-                float velocityX = Math.abs(mVelocityTracker.getXVelocity());
-                float velocityY = Math.abs(mVelocityTracker.getYVelocity());
-                boolean dismiss = false;
-                boolean dismissRight = false;
-                if (Math.abs(deltaX) > mViewWidth / 2) {
-                    dismiss = true;
-                    dismissRight = deltaX > 0;
-                } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity
-                        && velocityY < velocityX) {
-                    dismiss = true;
-                    dismissRight = mVelocityTracker.getXVelocity() > 0;
-                }
-                if (dismiss) {
-                    // dismiss
-                    mDismissing = true;
-                    final View downView = mDownView; // mDownView gets null'd before animation ends
-                    final int downPosition = mDownPosition;
-                    mDownView.animate()
-                            .translationX(dismissRight ? mViewWidth : -mViewWidth)
-                            .alpha(0)
-                            .setDuration(mAnimationTime)
-                            .setListener(new AnimatorListenerAdapter() {
-                                @Override
-                                public void onAnimationEnd(Animator animation) {
-                                    performDismiss(downView, downPosition);
-                                }
-                            });
-                } else {
-                    // cancel
-                    mDownView.animate()
-                            .translationX(0)
-                            .alpha(1)
-                            .setDuration(mAnimationTime)
-                            .setListener(null);
-                }
-
-                if (mVelocityTracker != null) {
-                    mVelocityTracker.recycle();
-                    mVelocityTracker = null;
-                }
-
-                mDownX = 0;
-                mDownView = null;
-                mDownPosition = ListView.INVALID_POSITION;
-                mSwiping = false;
-                break;
-            }
-
-            case MotionEvent.ACTION_MOVE: {
-                if (mVelocityTracker == null || mPaused) {
-                    break;
-                }
-
-                mVelocityTracker.addMovement(motionEvent);
-                float deltaX = motionEvent.getRawX() - mDownX;
-                if (Math.abs(deltaX) > mSlop) {
-                    mSwiping = true;
-                    mListView.requestDisallowInterceptTouchEvent(true);
-
-                    // Cancel ListView's touch (un-highlighting the item)
-                    MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);
-                    cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
-                            (motionEvent.getActionIndex()
-                                    << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
-                    mListView.onTouchEvent(cancelEvent);
-                    cancelEvent.recycle();
-                }
-
-                if (mSwiping) {
-                    mDownView.setTranslationX(deltaX);
-                    mDownView.setAlpha(Math.max(0f, Math.min(1f, 1f - 2f * Math.abs(deltaX) / mViewWidth)));
-                    return true;
-                }
-                break;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Animate the dismissed list item to zero-height and fire the dismiss callback when it finishes.
-     *
-     * @param dismissView     ListView item to dismiss
-     * @param dismissPosition Position of dismissed item
-     */
-    private void performDismiss(final View dismissView, final int dismissPosition) {
-        final ViewGroup.LayoutParams lp = dismissView.getLayoutParams();
-        final int originalHeight = lp.height;
-
-        ValueAnimator animator = ValueAnimator.ofInt(dismissView.getHeight(), 1).setDuration(mAnimationTime);
-
-        animator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                // Since the view is still a part of the ListView, we can't reset the animated
-                // properties yet; otherwise, the view would briefly reappear. Store the original
-                // height in the view's tag to flag it for the recycler. This is racy since the user
-                // could scroll the dismissed view off the screen, then back on the screen, before
-                // it's removed from the adapter, causing the dismissed view to briefly reappear.
-                dismissView.setTag(R.id.original_height, originalHeight);
-
-                mCallback.onDismiss(mListView, dismissPosition);
-                mDismissing = false;
-            }
-        });
-
-        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-            @Override
-            public void onAnimationUpdate(ValueAnimator valueAnimator) {
-                lp.height = (Integer) valueAnimator.getAnimatedValue();
-                dismissView.setLayoutParams(lp);
-            }
-        });
-
-        animator.start();
-    }
-}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -5,16 +5,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += ['locales']
 
 CONFIGURE_SUBST_FILES += ['adjust_sdk_app_token']
 
 include('android-services.mozbuild')
 
+geckoview_source_dir = TOPSRCDIR + '/mobile/android/geckoview/src/main/'
 thirdparty_source_dir = TOPSRCDIR + '/mobile/android/thirdparty/'
 
 constants_jar = add_java_jar('constants')
 constants_jar.sources = ['java/org/mozilla/gecko/' + x for x in [
     'adjust/AdjustHelperInterface.java',
     'annotation/JNITarget.java',
     'annotation/ReflectionTarget.java',
     'annotation/RobocopTarget.java',
@@ -77,33 +78,33 @@ if CONFIG['ANDROID_RECYCLERVIEW_V7_AAR']
 if CONFIG['ANDROID_CUSTOMTABS_AAR']:
     ANDROID_EXTRA_PACKAGES += ['android.support.customtabs']
     ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_CUSTOMTABS_AAR_RES']]
     resjar.generated_sources += ['android/support/customtabs/R.java']
 
 resjar.javac_flags += ['-Xlint:all']
 
 mgjar = add_java_jar('gecko-mozglue')
-mgjar.sources += ['java/org/mozilla/gecko/' + x for x in [
+mgjar.sources += [geckoview_source_dir + 'java/org/mozilla/gecko/' + x for x in [
     'mozglue/ByteBufferInputStream.java',
     'mozglue/DirectBufferAllocator.java',
     'mozglue/GeckoLoader.java',
     'mozglue/JNIObject.java',
     'mozglue/NativeReference.java',
     'mozglue/NativeZip.java',
     'mozglue/SafeIntent.java',
 ]]
 mgjar.generated_sources = [] # Keep it this way.
 mgjar.extra_jars += [
     'constants.jar',
 ]
 mgjar.javac_flags += ['-Xlint:all']
 
 gujar = add_java_jar('gecko-util')
-gujar.sources += ['java/org/mozilla/gecko/' + x for x in [
+gujar.sources += [geckoview_source_dir + 'java/org/mozilla/gecko/' + x for x in [
     'util/ActivityResultHandler.java',
     'util/ActivityResultHandlerMap.java',
     'util/ActivityUtils.java',
     'util/BundleEventListener.java',
     'util/Clipboard.java',
     'util/ContextUtils.java',
     'util/DateUtil.java',
     'util/DrawableUtil.java',
@@ -192,42 +193,124 @@ if CONFIG['MOZ_WEBRTC']:
         'constants.jar',
         'gecko-R.jar',
         'gecko-browser.jar',
         'gecko-util.jar',
         'gecko-mozglue.jar',
     ]
     wrjar.javac_flags += ['-Xlint:all,-deprecation,-cast']
 
+geckoview_java_files = [
+    'AlarmReceiver.java',
+    'AndroidGamepadManager.java',
+    'ANRReporter.java',
+    'BaseGeckoInterface.java',
+    'ContextGetter.java',
+    'CrashHandler.java',
+    'EventDispatcher.java',
+    'GeckoAccessibility.java',
+    'GeckoAppShell.java',
+    'GeckoBatteryManager.java',
+    'GeckoEditable.java',
+    'GeckoEditableClient.java',
+    'GeckoEditableListener.java',
+    'GeckoEvent.java',
+    'GeckoHalDefines.java',
+    'GeckoInputConnection.java',
+    'GeckoJavaSampler.java',
+    'GeckoNetworkManager.java',
+    'GeckoProfile.java',
+    'GeckoProfileDirectories.java',
+    'GeckoScreenOrientation.java',
+    'GeckoService.java',
+    'GeckoSharedPrefs.java',
+    'GeckoSmsManager.java',
+    'GeckoThread.java',
+    'GeckoView.java',
+    'gfx/BitmapUtils.java',
+    'gfx/DisplayPortCalculator.java',
+    'gfx/DisplayPortMetrics.java',
+    'gfx/DrawTimingQueue.java',
+    'gfx/DynamicToolbarAnimator.java',
+    'gfx/FloatSize.java',
+    'gfx/FullScreenState.java',
+    'gfx/GeckoLayerClient.java',
+    'gfx/GLController.java',
+    'gfx/ImmutableViewportMetrics.java',
+    'gfx/IntSize.java',
+    'gfx/Layer.java',
+    'gfx/LayerRenderer.java',
+    'gfx/LayerView.java',
+    'gfx/NativePanZoomController.java',
+    'gfx/Overscroll.java',
+    'gfx/OverscrollEdgeEffect.java',
+    'gfx/PanningPerfAPI.java',
+    'gfx/PanZoomController.java',
+    'gfx/PanZoomTarget.java',
+    'gfx/PluginLayer.java',
+    'gfx/PointUtils.java',
+    'gfx/ProgressiveUpdateData.java',
+    'gfx/RectUtils.java',
+    'gfx/RenderTask.java',
+    'gfx/TextureGenerator.java',
+    'gfx/TextureReaper.java',
+    'gfx/ViewTransform.java',
+    'gfx/VirtualLayer.java',
+    'InputConnectionListener.java',
+    'InputMethods.java',
+    'NotificationClient.java',
+    'NotificationHandler.java',
+    'NotificationService.java',
+    'NSSBridge.java',
+    'permissions/PermissionBlock.java',
+    'permissions/Permissions.java',
+    'permissions/PermissionsHelper.java',
+    'PrefsHelper.java',
+    'restrictions/DefaultConfiguration.java',
+    'restrictions/GuestProfileConfiguration.java',
+    'restrictions/Restrictable.java',
+    'restrictions/RestrictedProfileConfiguration.java',
+    'restrictions/RestrictionCache.java',
+    'restrictions/RestrictionConfiguration.java',
+    'restrictions/RestrictionProvider.java',
+    'restrictions/Restrictions.java',
+    'ServiceNotificationClient.java',
+    'SmsManager.java',
+    'sqlite/ByteBufferInputStream.java',
+    'sqlite/MatrixBlobCursor.java',
+    'sqlite/SQLiteBridge.java',
+    'sqlite/SQLiteBridgeException.java',
+    'SurfaceBits.java',
+    'TouchEventInterceptor.java',
+    'widget/SwipeDismissListViewTouchListener.java',
+]
+
 gbjar = add_java_jar('gecko-browser')
+gbjar.sources += [geckoview_source_dir + 'java/org/mozilla/gecko/' + x
+                  for x in geckoview_java_files]
+
 gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
     'AboutPages.java',
     'AccountsHelper.java',
     'ActionBarTextSelection.java',
     'ActionModeCompat.java',
     'ActionModeCompatView.java',
     'ActivityHandlerHelper.java',
-    'AlarmReceiver.java',
-    'AndroidGamepadManager.java',
     'animation/AnimationUtils.java',
     'animation/HeightChangeAnimation.java',
     'animation/PropertyAnimator.java',
     'animation/Rotate3DAnimation.java',
     'animation/ViewHelper.java',
-    'ANRReporter.java',
     'AppNotificationClient.java',
-    'BaseGeckoInterface.java',
     'BootReceiver.java',
     'BrowserApp.java',
     'BrowserLocaleManager.java',
     'cleanup/FileCleanupController.java',
     'cleanup/FileCleanupService.java',
     'ContactService.java',
-    'ContextGetter.java',
-    'CrashHandler.java',
     'CustomEditText.java',
     'customtabs/CustomTabsActivity.java',
     'customtabs/GeckoCustomTabsService.java',
     'DataReportingNotification.java',
     'db/AbstractPerProfileDatabaseProvider.java',
     'db/AbstractTransactionalProvider.java',
     'db/BaseTable.java',
     'db/BrowserDatabaseHelper.java',
@@ -277,17 +360,16 @@ gbjar.sources += ['java/org/mozilla/geck
     'dlc/DownloadContentService.java',
     'dlc/StudyAction.java',
     'dlc/SyncAction.java',
     'dlc/VerifyAction.java',
     'DoorHangerPopup.java',
     'DownloadsIntegration.java',
     'DynamicToolbar.java',
     'EditBookmarkDialog.java',
-    'EventDispatcher.java',
     'Experiments.java',
     'favicons/cache/FaviconCache.java',
     'favicons/cache/FaviconCacheElement.java',
     'favicons/cache/FaviconsForURL.java',
     'favicons/decoders/FaviconDecoder.java',
     'favicons/decoders/ICODecoder.java',
     'favicons/decoders/IconDirectoryEntry.java',
     'favicons/decoders/LoadFaviconResult.java',
@@ -322,73 +404,23 @@ gbjar.sources += ['java/org/mozilla/geck
     'firstrun/FirstrunAnimationContainer.java',
     'firstrun/FirstrunPager.java',
     'firstrun/FirstrunPagerConfig.java',
     'firstrun/FirstrunPanel.java',
     'firstrun/RestrictedWelcomePanel.java',
     'firstrun/SyncPanel.java',
     'firstrun/TabQueuePanel.java',
     'FormAssistPopup.java',
-    'GeckoAccessibility.java',
     'GeckoActivity.java',
     'GeckoActivityStatus.java',
     'GeckoApp.java',
     'GeckoApplication.java',
-    'GeckoAppShell.java',
-    'GeckoBatteryManager.java',
-    'GeckoEditable.java',
-    'GeckoEditableClient.java',
-    'GeckoEditableListener.java',
-    'GeckoEvent.java',
-    'GeckoHalDefines.java',
-    'GeckoInputConnection.java',
-    'GeckoJavaSampler.java',
     'GeckoMessageReceiver.java',
-    'GeckoNetworkManager.java',
-    'GeckoProfile.java',
-    'GeckoProfileDirectories.java',
     'GeckoProfilesProvider.java',
-    'GeckoScreenOrientation.java',
-    'GeckoService.java',
-    'GeckoSharedPrefs.java',
-    'GeckoSmsManager.java',
-    'GeckoThread.java',
     'GeckoUpdateReceiver.java',
-    'GeckoView.java',
-    'GeckoViewChrome.java',
-    'GeckoViewContent.java',
-    'gfx/BitmapUtils.java',
-    'gfx/DisplayPortCalculator.java',
-    'gfx/DisplayPortMetrics.java',
-    'gfx/DrawTimingQueue.java',
-    'gfx/DynamicToolbarAnimator.java',
-    'gfx/FloatSize.java',
-    'gfx/FullScreenState.java',
-    'gfx/GeckoLayerClient.java',
-    'gfx/GLController.java',
-    'gfx/ImmutableViewportMetrics.java',
-    'gfx/IntSize.java',
-    'gfx/Layer.java',
-    'gfx/LayerRenderer.java',
-    'gfx/LayerView.java',
-    'gfx/NativePanZoomController.java',
-    'gfx/Overscroll.java',
-    'gfx/OverscrollEdgeEffect.java',
-    'gfx/PanningPerfAPI.java',
-    'gfx/PanZoomController.java',
-    'gfx/PanZoomTarget.java',
-    'gfx/PluginLayer.java',
-    'gfx/PointUtils.java',
-    'gfx/ProgressiveUpdateData.java',
-    'gfx/RectUtils.java',
-    'gfx/RenderTask.java',
-    'gfx/TextureGenerator.java',
-    'gfx/TextureReaper.java',
-    'gfx/ViewTransform.java',
-    'gfx/VirtualLayer.java',
     'GlobalHistory.java',
     'GuestSession.java',
     'health/HealthRecorder.java',
     'health/SessionInformation.java',
     'health/StubbedHealthRecorder.java',
     'home/BookmarkFolderView.java',
     'home/BookmarkScreenshotRow.java',
     'home/BookmarksListAdapter.java',
@@ -441,18 +473,16 @@ gbjar.sources += ['java/org/mozilla/geck
     'home/SpacingDecoration.java',
     'home/TabMenuStrip.java',
     'home/TabMenuStripLayout.java',
     'home/TopSitesGridItemView.java',
     'home/TopSitesGridView.java',
     'home/TopSitesPanel.java',
     'home/TopSitesThumbnailView.java',
     'home/TwoLinePageRow.java',
-    'InputConnectionListener.java',
-    'InputMethods.java',
     'IntentHelper.java',
     'javaaddons/JavaAddonManager.java',
     'javaaddons/JavaAddonManagerV1.java',
     'LauncherActivity.java',
     'lwt/LightweightTheme.java',
     'lwt/LightweightThemeDrawable.java',
     'mdns/MulticastDNSManager.java',
     'media/AudioFocusAgent.java',
@@ -464,37 +494,30 @@ gbjar.sources += ['java/org/mozilla/geck
     'menu/GeckoMenuItem.java',
     'menu/GeckoSubMenu.java',
     'menu/MenuItemActionBar.java',
     'menu/MenuItemDefault.java',
     'menu/MenuItemSwitcherLayout.java',
     'menu/MenuPanel.java',
     'menu/MenuPopup.java',
     'MotionEventInterceptor.java',
-    'NotificationClient.java',
-    'NotificationHandler.java',
     'NotificationHelper.java',
     'notifications/WhatsNewReceiver.java',
-    'NotificationService.java',
-    'NSSBridge.java',
     'OrderedBroadcastHelper.java',
     'overlays/OverlayConstants.java',
     'overlays/service/OverlayActionService.java',
     'overlays/service/ShareData.java',
     'overlays/service/sharemethods/AddBookmark.java',
     'overlays/service/sharemethods/SendTab.java',
     'overlays/service/sharemethods/ShareMethod.java',
     'overlays/ui/OverlayDialogButton.java',
     'overlays/ui/SendTabDeviceListArrayAdapter.java',
     'overlays/ui/SendTabList.java',
     'overlays/ui/SendTabTargetSelectedListener.java',
     'overlays/ui/ShareDialog.java',
-    'permissions/PermissionBlock.java',
-    'permissions/Permissions.java',
-    'permissions/PermissionsHelper.java',
     'preferences/AlignRightLinkPreference.java',
     'preferences/AndroidImport.java',
     'preferences/AndroidImportPreference.java',
     'preferences/AppCompatPreferenceActivity.java',
     'preferences/ClearOnShutdownPref.java',
     'preferences/CustomCheckBoxPreference.java',
     'preferences/CustomListCategory.java',
     'preferences/CustomListPreference.java',
@@ -510,17 +533,16 @@ gbjar.sources += ['java/org/mozilla/geck
     'preferences/MultiPrefMultiChoicePreference.java',
     'preferences/PanelsPreference.java',
     'preferences/PanelsPreferenceCategory.java',
     'preferences/PrivateDataPreference.java',
     'preferences/SearchEnginePreference.java',
     'preferences/SearchPreferenceCategory.java',
     'preferences/SetHomepagePreference.java',
     'preferences/SyncPreference.java',
-    'PrefsHelper.java',
     'PrintHelper.java',
     'PrivateTab.java',
     'promotion/AddToHomeScreenPromotion.java',
     'promotion/HomeScreenPrompt.java',
     'promotion/ReaderViewBookmarkPromotion.java',
     'promotion/SimpleHelperUI.java',
     'prompts/ColorPickerInput.java',
     'prompts/IconGridInput.java',
@@ -532,39 +554,24 @@ gbjar.sources += ['java/org/mozilla/geck
     'prompts/PromptListItem.java',
     'prompts/PromptService.java',
     'prompts/TabInput.java',
     'reader/ReaderModeUtils.java',
     'reader/ReadingListHelper.java',
     'reader/SavedReaderViewHelper.java',
     'RemoteClientsDialogFragment.java',
     'Restarter.java',
-    'restrictions/DefaultConfiguration.java',
-    'restrictions/GuestProfileConfiguration.java',
-    'restrictions/Restrictable.java',
-    'restrictions/RestrictedProfileConfiguration.java',
-    'restrictions/RestrictionCache.java',
-    'restrictions/RestrictionConfiguration.java',
-    'restrictions/RestrictionProvider.java',
-    'restrictions/Restrictions.java',
     'ScreenshotObserver.java',
     'search/SearchEngine.java',
     'search/SearchEngineManager.java',
-    'ServiceNotificationClient.java',
     'SessionParser.java',
     'SharedPreferencesHelper.java',
     'SiteIdentity.java',
-    'SmsManager.java',
     'SnackbarHelper.java',
-    'sqlite/ByteBufferInputStream.java',
-    'sqlite/MatrixBlobCursor.java',
-    'sqlite/SQLiteBridge.java',
-    'sqlite/SQLiteBridgeException.java',
     'SuggestClient.java',
-    'SurfaceBits.java',
     'Tab.java',
     'tabqueue/TabQueueHelper.java',
     'tabqueue/TabQueuePrompt.java',
     'tabqueue/TabQueueService.java',
     'tabqueue/TabReceivedService.java',
     'Tabs.java',
     'tabs/PrivateTabsPanel.java',
     'tabs/TabCurve.java',
@@ -617,17 +624,16 @@ gbjar.sources += ['java/org/mozilla/geck
     'toolbar/ShapedButtonFrameLayout.java',
     'toolbar/SiteIdentityPopup.java',
     'toolbar/TabCounter.java',
     'toolbar/ToolbarDisplayLayout.java',
     'toolbar/ToolbarEditLayout.java',
     'toolbar/ToolbarEditText.java',
     'toolbar/ToolbarPrefs.java',
     'toolbar/ToolbarProgressView.java',
-    'TouchEventInterceptor.java',
     'trackingprotection/TrackingProtectionPrompt.java',
     'updater/PostUpdateHandler.java',
     'updater/UpdateService.java',
     'updater/UpdateServiceHelper.java',
     'widget/ActivityChooserModel.java',
     'widget/AllCapsTextView.java',
     'widget/AnchoredPopup.java',
     'widget/AnimatedHeightLayout.java',
@@ -653,17 +659,16 @@ gbjar.sources += ['java/org/mozilla/geck
     'widget/IconTabWidget.java',
     'widget/LoginDoorHanger.java',
     'widget/RecyclerViewClickSupport.java',
     'widget/ResizablePathDrawable.java',
     'widget/RoundedCornerLayout.java',
     'widget/SiteLogins.java',
     'widget/SquaredImageView.java',
     'widget/SquaredRelativeLayout.java',
-    'widget/SwipeDismissListViewTouchListener.java',
     'widget/TabThumbnailWrapper.java',
     'widget/ThumbnailView.java',
     'widget/TwoWayView.java',
     'ZoomedView.java',
 ]]
 # The following sources are checked in to version control but
 # generated by a script (java/org/mozilla/gecko/widget/themed/generate_themed_views.py).
 # If you're editing this list, make sure to edit that script.
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/build.gradle
@@ -0,0 +1,138 @@
+buildDir "${topobjdir}/gradle/build/mobile/android/geckoview"
+
+apply plugin: 'android-sdk-manager' // Must come before 'com.android.*'.
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 23
+    buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
+
+    defaultConfig {
+        targetSdkVersion 23
+        minSdkVersion 15 
+    }
+
+    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/duplicate/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.
+                exclude 'org/mozilla/gecko/AdjustConstants.java'
+            }
+
+            assets {
+            }
+        }
+    }
+}
+
+dependencies {
+    compile "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+}
+
+task syncOmnijarFromDistDir(type: Sync) {
+    into("${project.buildDir}/generated/omnijar")
+    from("${topobjdir}/dist/fennec/assets") {
+        include 'omni.ja'
+    }
+}
+
+task checkLibsExistInDistDir<< {
+    if (syncLibsFromDistDir.source.empty) {
+        throw new GradleException("Required JNI libraries not found in ${topobjdir}/dist/fennec/lib.  Have you built and packaged?")
+    }
+}
+
+task syncLibsFromDistDir(type: Sync, dependsOn: checkLibsExistInDistDir) {
+    into("${project.buildDir}/generated/jniLibs")
+    from("${topobjdir}/dist/fennec/lib")
+}
+
+task checkAssetsExistInDistDir<< {
+    if (syncAssetsFromDistDir.source.empty) {
+        throw new GradleException("Required assets not found in ${topobjdir}/dist/fennec/assets.  Have you built and packaged?")
+    }
+}
+
+task syncAssetsFromDistDir(type: Sync, dependsOn: checkAssetsExistInDistDir) {
+    into("${project.buildDir}/generated/assets")
+    from("${topobjdir}/dist/fennec/assets") {
+        exclude 'omni.ja'
+    }
+}
+
+task syncPreprocessedCode(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
+    into("${project.buildDir}/generated/source/preprocessed_code")
+    from("${topobjdir}/mobile/android/base/generated/preprocessed")
+}
+
+// The omnijar inputs are listed as resource directory inputs to a dummy JAR.
+// That arrangement labels them nicely in IntelliJ.  See the comment in the
+// :omnijar project for more context.
+evaluationDependsOn(':omnijar')
+
+task buildOmnijar(type:Exec) {
+    dependsOn rootProject.generateCodeAndResources
+
+    // See comment in :omnijar project regarding interface mismatches here.
+    inputs.source project(':omnijar').sourceSets.main.resources.srcDirs
+
+    // Produce a single output file.
+    outputs.file "${topobjdir}/dist/fennec/assets/omni.ja"
+
+    workingDir "${topobjdir}"
+
+    commandLine mozconfig.substs.GMAKE
+    args '-C'
+    args "${topobjdir}/mobile/android/base"
+    args 'gradle-omnijar'
+
+    // Only show the output if something went wrong.
+    ignoreExitValue = true
+    standardOutput = new ByteArrayOutputStream()
+    errorOutput = standardOutput
+    doLast {
+        if (execResult.exitValue != 0) {
+            throw new GradleException("Process '${commandLine}' finished with non-zero exit value ${execResult.exitValue}:\n\n${standardOutput.toString()}")
+        }
+    }
+}
+
+android.libraryVariants.all { variant ->
+    variant.preBuild.dependsOn syncPreprocessedCode
+
+    // Like 'debug' or 'release'.
+    def buildType = variant.buildType.name
+
+    syncOmnijarFromDistDir.dependsOn buildOmnijar
+    def generateAssetsTask = tasks.findByName("generate${buildType.capitalize()}Assets")
+    generateAssetsTask.dependsOn syncOmnijarFromDistDir
+    generateAssetsTask.dependsOn syncLibsFromDistDir
+    generateAssetsTask.dependsOn syncAssetsFromDistDir
+
+    android.sourceSets."${buildType}".assets.srcDir syncOmnijarFromDistDir.destinationDir
+    android.sourceSets."${buildType}".assets.srcDir syncAssetsFromDistDir.destinationDir
+    android.sourceSets."${buildType}".jniLibs.srcDir syncLibsFromDistDir.destinationDir
+}
copy from mobile/android/base/java/org/mozilla/gecko/annotation/JNITarget.java
copy to mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko/JNITarget.java
copy from mobile/android/base/java/org/mozilla/gecko/annotation/ReflectionTarget.java
copy to mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko/ReflectionTarget.java
copy from mobile/android/base/java/org/mozilla/gecko/annotation/RobocopTarget.java
copy to mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko/RobocopTarget.java
copy from mobile/android/base/java/org/mozilla/gecko/SysInfo.java
copy to mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko/SysInfo.java
copy from mobile/android/base/java/org/mozilla/gecko/annotation/WebRTCJNITarget.java
copy to mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko/WebRTCJNITarget.java
copy from mobile/android/base/java/org/mozilla/gecko/annotation/WrapForJNI.java
copy to mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko/WrapForJNI.java
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="org.mozilla.gecko">
+
+</manifest>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/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;
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/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();
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/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();
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/ANRReporter.java
@@ -0,0 +1,596 @@
+/* -*- 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.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.util.Locale;
+import java.util.UUID;
+import java.util.regex.Pattern;
+
+import org.json.JSONObject;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+public final class ANRReporter extends BroadcastReceiver
+{
+    private static final boolean DEBUG = false;
+    private static final String LOGTAG = "GeckoANRReporter";
+
+    private static final String ANR_ACTION = "android.intent.action.ANR";
+    // Number of lines to search traces.txt to decide whether it's a Gecko ANR
+    private static final int LINES_TO_IDENTIFY_TRACES = 10;
+    // ANRs may happen because of memory pressure,
+    //  so don't use up too much memory here
+    // Size of buffer to hold one line of text
+    private static final int TRACES_LINE_SIZE = 100;
+    // Size of block to use when processing traces.txt
+    private static final int TRACES_BLOCK_SIZE = 2000;
+    private static final String TRACES_CHARSET = "utf-8";
+    private static final String PING_CHARSET = "utf-8";
+
+    private static final ANRReporter sInstance = new ANRReporter();
+    private static int sRegisteredCount;
+    private Handler mHandler;
+    private volatile boolean mPendingANR;
+
+    @WrapForJNI
+    private static native boolean requestNativeStack(boolean unwind);
+    @WrapForJNI
+    private static native String getNativeStack();
+    @WrapForJNI
+    private static native void releaseNativeStack();
+
+    public static void register(Context context) {
+        if (sRegisteredCount++ != 0) {
+            // Already registered
+            return;
+        }
+        sInstance.start(context);
+    }
+
+    public static void unregister() {
+        if (sRegisteredCount == 0) {
+            Log.w(LOGTAG, "register/unregister mismatch");
+            return;
+        }
+        if (--sRegisteredCount != 0) {
+            // Should still be registered
+            return;
+        }
+        sInstance.stop();
+    }
+
+    private void start(final Context context) {
+
+        Thread receiverThread = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                Looper.prepare();
+                synchronized (ANRReporter.this) {
+                    mHandler = new Handler();
+                    ANRReporter.this.notify();
+                }
+                if (DEBUG) {
+                    Log.d(LOGTAG, "registering receiver");
+                }
+                context.registerReceiver(ANRReporter.this,
+                                         new IntentFilter(ANR_ACTION),
+                                         null,
+                                         mHandler);
+                Looper.loop();
+
+                if (DEBUG) {
+                    Log.d(LOGTAG, "unregistering receiver");
+                }
+                context.unregisterReceiver(ANRReporter.this);
+                mHandler = null;
+            }
+        }, LOGTAG);
+
+        receiverThread.setDaemon(true);
+        receiverThread.start();
+    }
+
+    private void stop() {
+        synchronized (this) {
+            while (mHandler == null) {
+                try {
+                    wait(1000);
+                    if (mHandler == null) {
+                        // We timed out; just give up. The process is probably
+                        // quitting anyways, so we let the OS do the clean up
+                        Log.w(LOGTAG, "timed out waiting for handler");
+                        return;
+                    }
+                } catch (InterruptedException e) {
+                }
+            }
+        }
+        Looper looper = mHandler.getLooper();
+        looper.quit();
+        try {
+            looper.getThread().join();
+        } catch (InterruptedException e) {
+        }
+    }
+
+    private ANRReporter() {
+    }
+
+    // Return the "traces.txt" file, or null if there is no such file
+    private static File getTracesFile() {
+        // Check most common location first.
+        File tracesFile = new File("/data/anr/traces.txt");
+        if (tracesFile.isFile() && tracesFile.canRead()) {
+            return tracesFile;
+        }
+
+        // Find the traces file name if we can.
+        try {
+            // getprop [prop-name [default-value]]
+            Process propProc = (new ProcessBuilder())
+                .command("/system/bin/getprop", "dalvik.vm.stack-trace-file")
+                .redirectErrorStream(true)
+                .start();
+            try {
+                BufferedReader buf = new BufferedReader(
+                    new InputStreamReader(propProc.getInputStream()), TRACES_LINE_SIZE);
+                String propVal = buf.readLine();
+                if (DEBUG) {
+                    Log.d(LOGTAG, "getprop returned " + String.valueOf(propVal));
+                }
+                // getprop can return empty string when the prop value is empty
+                // or prop is undefined, treat both cases the same way
+                if (propVal != null && propVal.length() != 0) {
+                    tracesFile = new File(propVal);
+                    if (tracesFile.isFile() && tracesFile.canRead()) {
+                        return tracesFile;
+                    } else if (DEBUG) {
+                        Log.d(LOGTAG, "cannot access traces file");
+                    }
+                } else if (DEBUG) {
+                    Log.d(LOGTAG, "empty getprop result");
+                }
+            } finally {
+                propProc.destroy();
+            }
+        } catch (IOException e) {
+            Log.w(LOGTAG, e);
+        } catch (ClassCastException e) {
+            Log.w(LOGTAG, e); // Bug 975436
+        }
+        return null;
+    }
+
+    private static File getPingFile() {
+        if (GeckoAppShell.getContext() == null) {
+            return null;
+        }
+        GeckoProfile profile = GeckoAppShell.getGeckoInterface().getProfile();
+        if (profile == null) {
+            return null;
+        }
+        File profDir = profile.getDir();
+        if (profDir == null) {
+            return null;
+        }
+        File pingDir = new File(profDir, "saved-telemetry-pings");
+        pingDir.mkdirs();
+        if (!(pingDir.exists() && pingDir.isDirectory())) {
+            return null;
+        }
+        return new File(pingDir, UUID.randomUUID().toString());
+    }
+
+    // Return true if the traces file corresponds to a Gecko ANR
+    private static boolean isGeckoTraces(String pkgName, File tracesFile) {
+        try {
+            final String END_OF_PACKAGE_NAME = "([^a-zA-Z0-9_]|$)";
+            // Regex for finding our package name in the traces file
+            Pattern pkgPattern = Pattern.compile(Pattern.quote(pkgName) + END_OF_PACKAGE_NAME);
+            Pattern mangledPattern = null;
+            if (!AppConstants.MANGLED_ANDROID_PACKAGE_NAME.equals(pkgName)) {
+                mangledPattern = Pattern.compile(Pattern.quote(
+                    AppConstants.MANGLED_ANDROID_PACKAGE_NAME) + END_OF_PACKAGE_NAME);
+            }
+            if (DEBUG) {
+                Log.d(LOGTAG, "trying to match package: " + pkgName);
+            }
+            BufferedReader traces = new BufferedReader(
+                new FileReader(tracesFile), TRACES_BLOCK_SIZE);
+            try {
+                for (int count = 0; count < LINES_TO_IDENTIFY_TRACES; count++) {
+                    String line = traces.readLine();
+                    if (DEBUG) {
+                        Log.d(LOGTAG, "identifying line: " + String.valueOf(line));
+                    }
+                    if (line == null) {
+                        if (DEBUG) {
+                            Log.d(LOGTAG, "reached end of traces file");
+                        }
+                        return false;
+                    }
+                    if (pkgPattern.matcher(line).find()) {
+                        // traces.txt file contains our package
+                        return true;
+                    }
+                    if (mangledPattern != null && mangledPattern.matcher(line).find()) {
+                        // traces.txt file contains our alternate package
+                        return true;
+                    }
+                }
+            } finally {
+                traces.close();
+            }
+        } catch (IOException e) {
+            // meh, can't even read from it right. just return false
+        }
+        return false;
+    }
+
+    private static long getUptimeMins() {
+
+        long uptimeMins = (new File("/proc/self/stat")).lastModified();
+        if (uptimeMins != 0L) {
+            uptimeMins = (System.currentTimeMillis() - uptimeMins) / 1000L / 60L;
+            if (DEBUG) {
+                Log.d(LOGTAG, "uptime " + String.valueOf(uptimeMins));
+            }
+            return uptimeMins;
+        }
+        if (DEBUG) {
+            Log.d(LOGTAG, "could not get uptime");
+        }
+        return 0L;
+    }
+
+    /*
+        a saved telemetry ping file consists of JSON in the following format,
+            {
+                "reason": "android-anr-report",
+                "slug": "<uuid-string>",
+                "payload": <json-object>
+            }
+        for Android ANR, our JSON payload should look like,
+            {
+                "ver": 1,
+                "simpleMeasurements": {
+                    "uptime": <uptime>
+                },
+                "info": {
+                    "reason": "android-anr-report",
+                    "OS": "Android",
+                    ...
+                },
+                "androidANR": "...",
+                "androidLogcat": "..."
+            }
+    */
+
+    private static int writePingPayload(OutputStream ping,
+                                        String payload) throws IOException {
+        byte [] data = payload.getBytes(PING_CHARSET);
+        ping.write(data);
+        return data.length;
+    }
+
+    private static void fillPingHeader(OutputStream ping, String slug)
+            throws IOException {
+
+        // ping file header
+        byte [] data = ("{" +
+            "\"reason\":\"android-anr-report\"," +
+            "\"slug\":" + JSONObject.quote(slug) + "," +
+            "\"payload\":").getBytes(PING_CHARSET);
+        ping.write(data);
+        if (DEBUG) {
+            Log.d(LOGTAG, "wrote ping header, size = " + String.valueOf(data.length));
+        }
+
+        // payload start
+        int size = writePingPayload(ping, ("{" +
+            "\"ver\":1," +
+            "\"simpleMeasurements\":{" +
+                "\"uptime\":" + String.valueOf(getUptimeMins()) +
+            "}," +
+            "\"info\":{" +
+                "\"reason\":\"android-anr-report\"," +
+                "\"OS\":" + JSONObject.quote(SysInfo.getName()) + "," +
+                "\"version\":\"" + String.valueOf(SysInfo.getVersion()) + "\"," +
+                "\"appID\":" + JSONObject.quote(AppConstants.MOZ_APP_ID) + "," +
+                "\"appVersion\":" + JSONObject.quote(AppConstants.MOZ_APP_VERSION) + "," +
+                "\"appName\":" + JSONObject.quote(AppConstants.MOZ_APP_BASENAME) + "," +
+                "\"appBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," +
+                "\"appUpdateChannel\":" + JSONObject.quote(AppConstants.MOZ_UPDATE_CHANNEL) + "," +
+                // Technically the platform build ID may be different, but we'll never know
+                "\"platformBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," +
+                "\"locale\":" + JSONObject.quote(Locales.getLanguageTag(Locale.getDefault())) + "," +
+                "\"cpucount\":" + String.valueOf(SysInfo.getCPUCount()) + "," +
+                "\"memsize\":" + String.valueOf(SysInfo.getMemSize()) + "," +
+                "\"arch\":" + JSONObject.quote(SysInfo.getArchABI()) + "," +
+                "\"kernel_version\":" + JSONObject.quote(SysInfo.getKernelVersion()) + "," +
+                "\"device\":" + JSONObject.quote(SysInfo.getDevice()) + "," +
+                "\"manufacturer\":" + JSONObject.quote(SysInfo.getManufacturer()) + "," +
+                "\"hardware\":" + JSONObject.quote(SysInfo.getHardware()) +
+            "}," +
+            "\"androidANR\":\""));
+        if (DEBUG) {
+            Log.d(LOGTAG, "wrote metadata, size = " + String.valueOf(size));
+        }
+
+        // We are at the start of ANR data
+    }
+
+    // Block is a section of the larger input stream, and we want to find pattern within
+    // the stream. This is straightforward if the entire pattern is within one block;
+    // however, if the pattern spans across two blocks, we have to match both the start of
+    // the pattern in the first block and the end of the pattern in the second block.
+    // * If pattern is found in block, this method returns the index at the end of the
+    //   found pattern, which must always be > 0.
+    // * If pattern is not found, it returns 0.
+    // * If the start of the pattern matches the end of the block, it returns a number
+    //   < 0, which equals the negated value of how many characters in pattern are already
+    //   matched; when processing the next block, this number is passed in through
+    //   prevIndex, and the rest of the characters in pattern are matched against the
+    //   start of this second block. The method returns value > 0 if the rest of the
+    //   characters match, or 0 if they do not.
+    private static int getEndPatternIndex(String block, String pattern, int prevIndex) {
+        if (pattern == null || block.length() < pattern.length()) {
+            // Nothing to do
+            return 0;
+        }
+        if (prevIndex < 0) {
+            // Last block ended with a partial start; now match start of block to rest of pattern
+            if (block.startsWith(pattern.substring(-prevIndex, pattern.length()))) {
+                // Rest of pattern matches; return index at end of pattern
+                return pattern.length() + prevIndex;
+            }
+            // Not a match; continue with normal search
+        }
+        // Did not find pattern in last block; see if entire pattern is inside this block
+        int index = block.indexOf(pattern);
+        if (index >= 0) {
+            // Found pattern; return index at end of the pattern
+            return index + pattern.length();
+        }
+        // Block does not contain the entire pattern, but see if the end of the block
+        // contains the start of pattern. To do that, we see if block ends with the
+        // first n-1 characters of pattern, the first n-2 characters of pattern, etc.
+        for (index = block.length() - pattern.length() + 1; index < block.length(); index++) {
+            // Using index as a start, see if the rest of block contains the start of pattern
+            if (block.charAt(index) == pattern.charAt(0) &&
+                block.endsWith(pattern.substring(0, block.length() - index))) {
+                // Found partial match; return -(number of characters matched),
+                // i.e. -1 for 1 character matched, -2 for 2 characters matched, etc.
+                return index - block.length();
+            }
+        }
+        return 0;
+    }
+
+    // Copy the content of reader to ping;
+    // copying stops when endPattern is found in the input stream
+    private static int fillPingBlock(OutputStream ping,
+                                     Reader reader, String endPattern)
+            throws IOException {
+
+        int total = 0;
+        int endIndex = 0;
+        char [] block = new char[TRACES_BLOCK_SIZE];
+        for (int size = reader.read(block); size >= 0; size = reader.read(block)) {
+            String stringBlock = new String(block, 0, size);
+            endIndex = getEndPatternIndex(stringBlock, endPattern, endIndex);
+            if (endIndex > 0) {
+                // Found end pattern; clip the string
+                stringBlock = stringBlock.substring(0, endIndex);
+            }
+            String quoted = JSONObject.quote(stringBlock);
+            total += writePingPayload(ping, quoted.substring(1, quoted.length() - 1));
+            if (endIndex > 0) {
+                // End pattern already found; return now
+                break;
+            }
+        }
+        return total;
+    }
+
+    private static void fillLogcat(final OutputStream ping) {
+        if (Versions.preJB) {
+            // Logcat retrieval is not supported on pre-JB devices.
+            return;
+        }
+
+        try {
+            // get the last 200 lines of logcat
+            Process proc = (new ProcessBuilder())
+                .command("/system/bin/logcat", "-v", "threadtime", "-t", "200", "-d", "*:D")
+                .redirectErrorStream(true)
+                .start();
+            try {
+                Reader procOut = new InputStreamReader(proc.getInputStream(), TRACES_CHARSET);
+                int size = fillPingBlock(ping, procOut, null);
+                if (DEBUG) {
+                    Log.d(LOGTAG, "wrote logcat, size = " + String.valueOf(size));
+                }
+            } finally {
+                proc.destroy();
+            }
+        } catch (IOException e) {
+            // ignore because logcat is not essential
+            Log.w(LOGTAG, e);
+        }
+    }
+
+    private static void fillPingFooter(OutputStream ping,
+                                       boolean haveNativeStack)
+            throws IOException {
+
+        // We are at the end of ANR data
+
+        int total = writePingPayload(ping, ("\"," +
+                "\"androidLogcat\":\""));
+        fillLogcat(ping);
+
+        if (haveNativeStack) {
+            total += writePingPayload(ping, ("\"," +
+                    "\"androidNativeStack\":"));
+
+            String nativeStack = String.valueOf(getNativeStack());
+            int size = writePingPayload(ping, nativeStack);
+            if (DEBUG) {
+                Log.d(LOGTAG, "wrote native stack, size = " + String.valueOf(size));
+            }
+            total += size + writePingPayload(ping, "}");
+        } else {
+            total += writePingPayload(ping, "\"}");
+        }
+
+        byte [] data = (
+            "}").getBytes(PING_CHARSET);
+        ping.write(data);
+        if (DEBUG) {
+            Log.d(LOGTAG, "wrote ping footer, size = " + String.valueOf(data.length + total));
+        }
+    }
+
+    private static void processTraces(Reader traces, File pingFile) {
+
+        // Only get native stack if Gecko is running.
+        // Also, unwinding is memory intensive, so only unwind if we have enough memory.
+        final boolean haveNativeStack =
+            GeckoThread.isRunning() ?
+            requestNativeStack(/* unwind */ SysInfo.getMemSize() >= 640) : false;
+
+        try {
+            OutputStream ping = new BufferedOutputStream(
+                new FileOutputStream(pingFile), TRACES_BLOCK_SIZE);
+            try {
+                fillPingHeader(ping, pingFile.getName());
+                // Traces file has the format
+                //    ----- pid xxx at xxx -----
+                //    Cmd line: org.mozilla.xxx
+                //    * stack trace *
+                //    ----- end xxx -----
+                //    ----- pid xxx at xxx -----
+                //    Cmd line: com.android.xxx
+                //    * stack trace *
+                //    ...
+                // If we end the stack dump at the first end marker,
+                // only Fennec stacks will be dumped
+                int size = fillPingBlock(ping, traces, "\n----- end");
+                if (DEBUG) {
+                    Log.d(LOGTAG, "wrote traces, size = " + String.valueOf(size));
+                }
+                fillPingFooter(ping, haveNativeStack);
+                if (DEBUG) {
+                    Log.d(LOGTAG, "finished creating ping file");
+                }
+                return;
+            } finally {
+                ping.close();
+                if (haveNativeStack) {
+                    releaseNativeStack();
+                }
+            }
+        } catch (IOException e) {
+            Log.w(LOGTAG, e);
+        }
+        // exception; delete ping file
+        if (pingFile.exists()) {
+            pingFile.delete();
+        }
+    }
+
+    private static void processTraces(File tracesFile, File pingFile) {
+        try {
+            Reader traces = new InputStreamReader(
+                new FileInputStream(tracesFile), TRACES_CHARSET);
+            try {
+                processTraces(traces, pingFile);
+            } finally {
+                traces.close();
+            }
+        } catch (IOException e) {
+            Log.w(LOGTAG, e);
+        }
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (mPendingANR) {
+            // we already processed an ANR without getting unstuck; skip this one
+            if (DEBUG) {
+                Log.d(LOGTAG, "skipping duplicate ANR");
+            }
+            return;
+        }
+        if (ThreadUtils.getUiHandler() != null) {
+            mPendingANR = true;
+            // detect when the main thread gets unstuck
+            ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    // okay to reset mPendingANR on main thread
+                    mPendingANR = false;
+                    if (DEBUG) {
+                        Log.d(LOGTAG, "yay we got unstuck!");
+                    }
+                }
+            });
+        }
+        if (DEBUG) {
+            Log.d(LOGTAG, "receiving " + String.valueOf(intent));
+        }
+        if (!ANR_ACTION.equals(intent.getAction())) {
+            return;
+        }
+
+        // make sure we have a good save location first
+        File pingFile = getPingFile();
+        if (DEBUG) {
+            Log.d(LOGTAG, "using ping file: " + String.valueOf(pingFile));
+        }
+        if (pingFile == null) {
+            return;
+        }
+
+        File tracesFile = getTracesFile();
+        if (DEBUG) {
+            Log.d(LOGTAG, "using traces file: " + String.valueOf(tracesFile));
+        }
+        if (tracesFile == null) {
+            return;
+        }
+
+        // We get ANR intents from all ANRs in the system, but we only want Gecko ANRs
+        if (!isGeckoTraces(context.getPackageName(), tracesFile)) {
+            if (DEBUG) {
+                Log.d(LOGTAG, "traces is not Gecko ANR");
+            }
+            return;
+        }
+        Log.i(LOGTAG, "processing Gecko ANR");
+        processTraces(tracesFile, pingFile);
+    }
+}
new file mode 100644
--- /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
+    private static native void notifyAlarmFired();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java
@@ -0,0 +1,392 @@
+/* -*- 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.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;
+                }
+            }
+        }
+    }
+
+    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() {
+    }
+
+    public static void startup() {
+        ThreadUtils.assertOnUiThread();
+        if (!sStarted) {
+            scanForGamepads();
+            addDeviceListener();
+            sStarted = true;
+        }
+    }
+
+    public static void shutdown() {
+        ThreadUtils.assertOnUiThread();
+        if (sStarted) {
+            removeDeviceListener();
+            sPendingGamepads.clear();
+            sGamepads.clear();
+            sStarted = false;
+        }
+    }
+
+    public static void gamepadAdded(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;
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadButtonEvent(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.
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadAxisEvent(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;
+                    GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadButtonEvent(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;
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadButtonEvent(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) {
+        //TODO: when we're using a newer SDK version, use these.
+        //if (Build.VERSION.SDK_INT >= 12) {
+        //int vid = device.getVendorId();
+        //int pid = device.getProductId();
+        //}
+        sPendingGamepads.put(device.getId(), new ArrayList<KeyEvent>());
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadAddRemoveEvent(device.getId(), true));
+    }
+
+    private static void removeGamepad(int deviceId) {
+        Gamepad gamepad = sGamepads.get(deviceId);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadAddRemoveEvent(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;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/BaseGeckoInterface.java
@@ -0,0 +1,186 @@
+/* -*- 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;
+
+    public BaseGeckoInterface(Context context) {
+        mContext = context;
+    }
+
+    @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 908772: Implement this
+    @Override
+    public LocationListener getLocationListener() {
+        return null;
+    }
+
+    // Bug 908773: Implement this
+    @Override
+    public SensorEventListener getSensorEventListener() {
+        return null;
+    }
+
+    // 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, final RectF rect, final boolean isFullScreen) {}
+
+    // Bug 908781: Implement this
+    @Override
+    public void removePluginView(final View view, final boolean isFullScreen) {}
+
+    // Bug 908783: Implement this
+    @Override
+    public void enableCameraView() {}
+
+    // Bug 908785: Implement this
+    @Override
+    public void disableCameraView() {}
+
+    // Bug 908786: Implement this
+    @Override
+    public void addAppStateListener(GeckoAppShell.AppStateListener listener) {}
+
+    // Bug 908787: Implement this
+    @Override
+    public void removeAppStateListener(GeckoAppShell.AppStateListener listener) {}
+
+    // Bug 908788: Implement this
+    @Override
+    public View getCameraView() {
+        return null;
+    }
+
+    // Bug 908789: Implement this
+    @Override
+    public void notifyWakeLockChanged(String topic, String state) {}
+
+    // Bug 908790: Implement this
+    @Override
+    public void onInputMethodChanged(String inputMethod) {}
+
+    @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";
+    }
+}
new file mode 100644
--- /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();
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/CrashHandler.java
@@ -0,0 +1,469 @@
+/* -*- 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();
+    }
+
+    protected String getAppPackageName() {
+        final Context context = getAppContext();
+
+        if (context != null) {
+            return context.getPackageName();
+        }
+
+        try {
+            // Package name is also the command line string in most cases.
+            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) {
+            Log.i(LOGTAG, "Error reading package name", e);
+        }
+
+        // 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();
+
+        extras.putString("ProductName", pkgName);
+        extras.putLong("CrashTime", getCrashTime());
+        extras.putLong("StartupTime", getStartupTime());
+
+        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;
+            }
+        };
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java
@@ -0,0 +1,409 @@
+/* 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.GeckoAppShell;
+import org.mozilla.gecko.GeckoEvent;
+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";
+    private 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);
+
+    public static EventDispatcher getInstance() {
+        return INSTANCE;
+    }
+
+    private 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_BUILD && 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_BUILD) {
+            // 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_BUILD) {
+                    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);
+    }
+
+    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);
+    }
+
+    public void 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;
+        }
+
+        final List<NativeEventListener> listeners;
+        synchronized (mGeckoThreadNativeListeners) {
+            listeners = mGeckoThreadNativeListeners.get(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.  Dispatch an error rather than
+                // looking for JSON listeners.
+                if (callback != null) {
+                    callback.sendError("No listeners for request");
+                }
+            }
+            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;
+        }
+
+        // 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 (dispatchToThread(type, message, callback,
+                             mUiThreadListeners, ThreadUtils.getUiHandler()) ||
+            dispatchToThread(type, message, callback,
+                             mBackgroundThreadListeners, ThreadUtils.getBackgroundHandler())) {
+
+            // 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;
+        }
+
+        try {
+            // If we didn't find native listeners, try JSON listeners.
+            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);
+        }
+    }
+
+    private boolean dispatchToThread(final String type,
+                                     final NativeJSObject message,
+                                     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);
+
+                // There were native listeners, and they're gone.
+                // Dispatch an error rather than looking for more listeners.
+                if (callback != null) {
+                    callback.sendError("No listeners for request");
+                }
+                return true;
+            }
+
+            final Bundle messageAsBundle;
+            try {
+                messageAsBundle = message.toBundle();
+            } 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 void dispatchEvent(final JSONObject message, final EventCallback callback) {
+        // {
+        //   "type": "value",
+        //   "event_specific": "value",
+        //   ...
+        try {
+            final String type = message.getString("type");
+
+            List<GeckoEventListener> listeners;
+            synchronized (mGeckoThreadJSONListeners) {
+                listeners = mGeckoThreadJSONListeners.get(type);
+            }
+            if (listeners == null || listeners.isEmpty()) {
+                Log.w(LOGTAG, "No listeners for " + type);
+
+                // If there are no listeners, dispatch an error.
+                if (callback != null) {
+                    callback.sendError("No listeners for request");
+                }
+                return;
+            }
+            for (final GeckoEventListener listener : listeners) {
+                listener.handleMessage(type, message);
+            }
+        } catch (final JSONException e) {
+            Log.e(LOGTAG, "handleGeckoMessage throws " + e, e);
+        }
+    }
+
+    @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);
+        }
+    }
+
+    private 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);
+            }
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAccessibility.java
@@ -0,0 +1,413 @@
+/* -*- 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.gfx.LayerView;
+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.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(GeckoAppShell.getContext(), 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"));
+        if (Versions.feature14Plus) {
+            event.setToIndex(message.optInt("toIndex", -1));
+            event.setScrollable(message.optBoolean("scrollable"));
+            event.setScrollX(message.optInt("scrollX", -1));
+            event.setScrollY(message.optInt("scrollY", -1));
+        }
+        if (Versions.feature15Plus) {
+            event.setMaxScrollX(message.optInt("maxScrollX", -1));
+            event.setMaxScrollY(message.optInt("maxScrollY", -1));
+        }
+    }
+
+    private static void sendDirectAccessibilityEvent(int eventType, JSONObject message) {
+        final AccessibilityEvent accEvent = AccessibilityEvent.obtain(eventType);
+        accEvent.setClassName(GeckoAccessibility.class.getName());
+        accEvent.setPackageName(GeckoAppShell.getContext().getPackageName());
+        populateEventFromJSON(accEvent, message);
+        AccessibilityManager accessibilityManager =
+            (AccessibilityManager) GeckoAppShell.getContext().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 LayerView 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.getContext().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);
+                        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(LayerView layerview) {
+        // Only use this delegate in Jelly Bean.
+        if (Versions.feature16Plus) {
+            layerview.setAccessibilityDelegate(new GeckoAccessibilityDelegate());
+            layerview.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(LayerView layerview, 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.getContext().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;
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java
@@ -0,0 +1,2362 @@
+/* -*- 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.Sensor;
+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.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.webkit.MimeTypeMap;
+import android.widget.AbsoluteLayout;
+
+public class GeckoAppShell
+{
+    private static final String LOGTAG = "GeckoAppShell";
+
+    // We have static members only.
+    private GeckoAppShell() { }
+
+    public static final String ACTION_ALERT_CALLBACK       = "org.mozilla.gecko.ACTION_ALERT_CALLBACK";
+    public static final String ACTION_HOMESCREEN_SHORTCUT  = "org.mozilla.gecko.BOOKMARK";
+    public static final String PREFS_OOM_EXCEPTION         = "OOMException";
+
+    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(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 final Map<String, String> ALERT_COOKIES = new ConcurrentHashMap<String, String>();
+
+    private static volatile boolean locationHighAccuracyEnabled;
+
+    // Accessed by NotificationHelper. This should be encapsulated.
+    /* package */ static NotificationClient notificationClient;
+
+    // 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://mxr.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://mxr.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;
+
+    /* The Android-side API: API methods that Android calls */
+
+    // Initialization methods
+    public static native void registerJavaUiThread();
+
+    // helper methods
+    public static void callObserver(String observerKey, String topic, String data) {
+        sendEventToGecko(GeckoEvent.createCallObserverEvent(observerKey, topic, data));
+    }
+    public static void removeObserver(String observerKey) {
+        sendEventToGecko(GeckoEvent.createRemoveObserverEvent(observerKey));
+    }
+    public static native void onSurfaceTextureFrameAvailable(Object surfaceTexture, int id);
+    public static native void dispatchMemoryPressure();
+
+    private static native void reportJavaCrash(String stackTrace);
+
+    public static void notifyUriVisited(String uri) {
+        sendEventToGecko(GeckoEvent.createVisitedEvent(uri));
+    }
+
+    public static native void notifyBatteryChange(double aLevel, boolean aCharging, double aRemainingTime);
+
+    public static native void invalidateAndScheduleComposite();
+
+    public static native float computeRenderIntegrity();
+
+    public static native SurfaceBits getSurfaceBits(Surface surface);
+
+    public static native void addPresentationSurface(Surface surface);
+    public static native void removePresentationSurface(Surface surface);
+
+    public static native void onFullScreenPluginHidden(View view);
+
+    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;
+    }
+
+    /**
+     * If the Gecko thread is running, immediately dispatches the event to
+     * Gecko.
+     *
+     * If the Gecko thread is not running, queues the event. If the queue is
+     * full, throws {@link IllegalStateException}.
+     *
+     * Queued events will be dispatched in order of arrival when the Gecko
+     * thread becomes live.
+     *
+     * This method can be called from any thread.
+     *
+     * @param e
+     *            the event to dispatch. Cannot be null.
+     */
+    @RobocopTarget
+    public static void sendEventToGecko(GeckoEvent e) {
+        if (e == null) {
+            throw new IllegalArgumentException("e cannot be null.");
+        }
+
+        if (GeckoThread.isRunning()) {
+            notifyGeckoOfEvent(e);
+            // Gecko will copy the event data into a normal C++ object.
+            // We can recycle the event now.
+            e.recycle();
+            return;
+        }
+
+        GeckoThread.addPendingEvent(e);
+    }
+
+    /**
+     * 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.
+     *
+     * This method follows the same queuing conditions as {@link #sendEventToGecko(GeckoEvent)}.
+     * 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());
+    }
+
+    // Tell the Gecko event loop that an event is available.
+    public static native void notifyGeckoOfEvent(GeckoEvent event);
+
+    // Synchronously notify a Gecko observer; must be called from Gecko thread.
+    @WrapForJNI
+    public static native void syncNotifyObservers(String topic, String data);
+
+    @WrapForJNI(stubName = "NotifyObservers")
+    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(allowMultithread = true, noThrow = true)
+    public static String handleUncaughtException(Throwable e) {
+        if (AppConstants.MOZ_CRASHREPORTER) {
+            final Throwable exc = CrashHandler.getRootException(e);
+            final StackTraceElement[] stack = exc.getStackTrace();
+            if (stack.length >= 1 && stack[0].isNativeMethod()) {
+                // The exception occurred when running native code. Return an exception
+                // string and trigger the crash reporter inside the caller so that we get
+                // a better native stack in Socorro.
+                CrashHandler.logException(Thread.currentThread(), exc);
+                return CrashHandler.getExceptionStackTrace(exc);
+            }
+        }
+        CRASH_HANDLER.uncaughtException(null, e);
+        return null;
+    }
+
+    private static final Runnable sCallbackRunnable = new Runnable() {
+        @Override
+        public void run() {
+            ThreadUtils.assertOnUiThread();
+            long nextDelay = runUiThreadCallback();
+            if (nextDelay >= 0) {
+                ThreadUtils.getUiHandler().postDelayed(this, nextDelay);
+            }
+        }
+    };
+
+    private static native long runUiThreadCallback();
+
+    @WrapForJNI(allowMultithread = true)
+    private static void requestUiThreadCallback(long delay) {
+        ThreadUtils.getUiHandler().postDelayed(sCallbackRunnable, delay);
+    }
+
+    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
+    @SuppressLint("MissingPermission") // Permissions are explicitly checked for within this method
+    public static void enableLocation(final boolean enable) {
+        Permissions
+                .from((Activity) getContext())
+                .withPermissions(Manifest.permission.ACCESS_FINE_LOCATION)
+                .onUIThread()
+                .doNotPromptIf(!enable)
+                .run(new Runnable() {
+                    @Override
+                    public void run() {
+                        LocationManager lm = getLocationManager(getApplicationContext());
+                        if (lm == null) {
+                            return;
+                        }
+
+                        if (enable) {
+                            Location lastKnownLocation = getLastKnownLocation(lm);
+                            if (lastKnownLocation != null) {
+                                getGeckoInterface().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, (float) .5, getGeckoInterface().getLocationListener(), l);
+                        } else {
+                            lm.removeUpdates(getGeckoInterface().getLocationListener());
+                        }
+                    }
+                });
+    }
+
+    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
+    public static void enableLocationHighAccuracy(final boolean enable) {
+        locationHighAccuracyEnabled = enable;
+    }
+
+    @WrapForJNI
+    public 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
+    public 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
+    public 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(gi.getSensorEventListener(),
+                                    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(gi.getSensorEventListener(),
+                                    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(gi.getSensorEventListener(),
+                                    gOrientationSensor,
+                                    SensorManager.SENSOR_DELAY_FASTEST);
+            }
+            break;
+
+        case GeckoHalDefines.SENSOR_ACCELERATION:
+            if (gAccelerometerSensor == null) {
+                gAccelerometerSensor = sm.getDefaultSensor(
+                    Sensor.TYPE_ACCELEROMETER);
+            }
+            if (gAccelerometerSensor != null) {
+                sm.registerListener(gi.getSensorEventListener(),
+                                    gAccelerometerSensor,
+                                    SensorManager.SENSOR_DELAY_FASTEST);
+            }
+            break;
+
+        case GeckoHalDefines.SENSOR_PROXIMITY:
+            if (gProximitySensor == null) {
+                gProximitySensor = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY);
+            }
+            if (gProximitySensor != null) {
+                sm.registerListener(gi.getSensorEventListener(),
+                                    gProximitySensor,
+                                    SensorManager.SENSOR_DELAY_NORMAL);
+            }
+            break;
+
+        case GeckoHalDefines.SENSOR_LIGHT:
+            if (gLightSensor == null) {
+                gLightSensor = sm.getDefaultSensor(Sensor.TYPE_LIGHT);
+            }
+            if (gLightSensor != null) {
+                sm.registerListener(gi.getSensorEventListener(),
+                                    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(gi.getSensorEventListener(),
+                                    gLinearAccelerometerSensor,
+                                    SensorManager.SENSOR_DELAY_FASTEST);
+            }
+            break;
+
+        case GeckoHalDefines.SENSOR_GYROSCOPE:
+            if (gGyroscopeSensor == null) {
+                gGyroscopeSensor = sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
+            }
+            if (gGyroscopeSensor != null) {
+                sm.registerListener(gi.getSensorEventListener(),
+                                    gGyroscopeSensor,
+                                    SensorManager.SENSOR_DELAY_FASTEST);
+            }
+            break;
+
+        default:
+            Log.w(LOGTAG, "Error! Can't enable unknown SENSOR type " +
+                  aSensortype);
+        }
+    }
+
+    @WrapForJNI
+    public 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(gi.getSensorEventListener(), gGameRotationVectorSensor);
+              break;
+            }
+            // Fallthrough
+
+        case GeckoHalDefines.SENSOR_ROTATION_VECTOR:
+            if (gRotationVectorSensor != null) {
+                sm.unregisterListener(gi.getSensorEventListener(), gRotationVectorSensor);
+              break;
+            }
+            // Fallthrough
+
+        case GeckoHalDefines.SENSOR_ORIENTATION:
+            if (gOrientationSensor != null) {
+                sm.unregisterListener(gi.getSensorEventListener(), gOrientationSensor);
+            }
+            break;
+
+        case GeckoHalDefines.SENSOR_ACCELERATION:
+            if (gAccelerometerSensor != null) {
+                sm.unregisterListener(gi.getSensorEventListener(), gAccelerometerSensor);
+            }
+            break;
+
+        case GeckoHalDefines.SENSOR_PROXIMITY:
+            if (gProximitySensor != null) {
+                sm.unregisterListener(gi.getSensorEventListener(), gProximitySensor);
+            }
+            break;
+
+        case GeckoHalDefines.SENSOR_LIGHT:
+            if (gLightSensor != null) {
+                sm.unregisterListener(gi.getSensorEventListener(), gLightSensor);
+            }
+            break;
+
+        case GeckoHalDefines.SENSOR_LINEAR_ACCELERATION:
+            if (gLinearAccelerometerSensor != null) {
+                sm.unregisterListener(gi.getSensorEventListener(), gLinearAccelerometerSensor);
+            }
+            break;
+
+        case GeckoHalDefines.SENSOR_GYROSCOPE:
+            if (gGyroscopeSensor != null) {
+                sm.unregisterListener(gi.getSensorEventListener(), gGyroscopeSensor);
+            }
+            break;
+        default:
+            Log.w(LOGTAG, "Error! Can't disable unknown SENSOR type " + aSensortype);
+        }
+    }
+
+    @WrapForJNI
+    public static void startMonitoringGamepad() {
+        ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    AndroidGamepadManager.startup();
+                }
+            });
+    }
+
+    @WrapForJNI
+    public static void stopMonitoringGamepad() {
+        ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    AndroidGamepadManager.shutdown();
+                }
+            });
+    }
+
+    @WrapForJNI
+    public static void gamepadAdded(final int device_id, final int service_id) {
+        ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    AndroidGamepadManager.gamepadAdded(device_id, service_id);
+                }
+            });
+    }
+
+    @WrapForJNI
+    public static void moveTaskToBack() {
+        if (getGeckoInterface() != null)
+            getGeckoInterface().getActivity().moveTaskToBack(true);
+    }
+
+    @WrapForJNI
+    static void scheduleRestart() {
+        getGeckoInterface().doRestart();
+    }
+
+    // Creates a homescreen shortcut for a web page.
+    // This is the entry point from nsIShellService.
+    @WrapForJNI
+    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() {
+        if (Versions.feature11Plus) {
+            ActivityManager am = (ActivityManager)
+                    getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
+            return am.getLauncherLargeIconSize();
+        } else {
+            switch (getDpi()) {
+                case DisplayMetrics.DENSITY_MEDIUM:
+                    return 48;
+                case DisplayMetrics.DENSITY_XHIGH:
+                    return 96;
+                case DisplayMetrics.DENSITY_HIGH:
+                default:
+                    return 72;
+            }
+        }
+    }
+
+    @WrapForJNI(stubName = "GetHandlersForMimeTypeWrapper")
+    static String[] getHandlersForMimeType(String aMimeType, String aAction) {
+        final GeckoInterface geckoInterface = getGeckoInterface();
+        if (geckoInterface == null) {
+            return new String[] {};
+        }
+        return geckoInterface.getHandlersForMimeType(aMimeType, aAction);
+    }
+
+    @WrapForJNI(stubName = "GetHandlersForURLWrapper")
+    static String[] getHandlersForURL(String aURL, String aAction) {
+        final GeckoInterface geckoInterface = getGeckoInterface();
+        if (geckoInterface == null) {
+            return new String[] {};
+        }
+        return geckoInterface.getHandlersForURL(aURL, aAction);
+    }
+
+    @WrapForJNI(stubName = "GetHWEncoderCapability")
+    static boolean getHWEncoderCapability() {
+      return HardwareCodecCapabilityUtils.getHWEncoderCapability();
+    }
+
+    @WrapForJNI(stubName = "GetHWDecoderCapability")
+    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(stubName = "GetExtensionFromMimeTypeWrapper")
+    static String getExtensionFromMimeType(String aMimeType) {
+        return MimeTypeMap.getSingleton().getExtensionFromMimeType(aMimeType);
+    }
+
+    @WrapForJNI(stubName = "GetMimeTypeFromExtensionsWrapper")
+    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
+    public 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);
+    }
+
+    /**
+     * Only called from GeckoApp.
+     */
+    public static void setNotificationClient(NotificationClient client) {
+        if (notificationClient == null) {
+            notificationClient = client;
+        } else {
+            Log.d(LOGTAG, "Notification client already set");
+        }
+    }
+
+    @WrapForJNI(stubName = "ShowPersistentAlertNotificationWrapper")
+    public static void showPersistentAlertNotification(
+          String aPersistentData,
+          String aImageUrl, String aAlertTitle, String aAlertText,
+          String aAlertCookie, String aAlertName, String aHost) {
+        Intent notificationIntent = GeckoService.getIntentToCreateServices(
+                getApplicationContext(), "persistent-notification-click", aPersistentData);
+        int notificationID = aAlertName.hashCode();
+        PendingIntent contentIntent = PendingIntent.getService(
+                getApplicationContext(), 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+        notificationClient.add(notificationID, aImageUrl, aHost, aAlertTitle, aAlertText, contentIntent);
+    }
+
+    @WrapForJNI(stubName = "ShowAlertNotificationWrapper")
+    public static void showAlertNotification(String aImageUrl, String aAlertTitle, String aAlertText, String aAlertCookie, String aAlertName, String aHost) {
+        // The intent to launch when the user clicks the expanded notification
+        Intent notificationIntent = new Intent(ACTION_ALERT_CALLBACK);
+        notificationIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
+        notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+        int notificationID = aAlertName.hashCode();
+
+        // Put the strings into the intent as an URI "alert:?name=<alertName>&app=<appName>&cookie=<cookie>"
+        Uri.Builder b = new Uri.Builder();
+        Uri dataUri = b.scheme("alert").path(Integer.toString(notificationID))
+                                       .appendQueryParameter("name", aAlertName)
+                                       .appendQueryParameter("cookie", aAlertCookie)
+                                       .build();
+        notificationIntent.setData(dataUri);
+        PendingIntent contentIntent = PendingIntent.getActivity(
+                getApplicationContext(), 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+        ALERT_COOKIES.put(aAlertName, aAlertCookie);
+        callObserver(aAlertName, "alertshow", aAlertCookie);
+
+        notificationClient.add(notificationID, aImageUrl, aHost, aAlertTitle, aAlertText, contentIntent);
+    }
+
+    @WrapForJNI
+    public static void alertsProgressListener_OnProgress(String aAlertName, long aProgress, long aProgressMax, String aAlertText) {
+        int notificationID = aAlertName.hashCode();
+        notificationClient.update(notificationID, aProgress, aProgressMax, aAlertText);
+    }
+
+    @WrapForJNI
+    public static void closeNotification(String aAlertName) {
+        String alertCookie = ALERT_COOKIES.get(aAlertName);
+        if (alertCookie != null) {
+            callObserver(aAlertName, "alertfinished", alertCookie);
+            ALERT_COOKIES.remove(aAlertName);
+        }
+
+        removeObserver(aAlertName);
+
+        int notificationID = aAlertName.hashCode();
+        notificationClient.remove(notificationID);
+    }
+
+    public static void handleNotification(String aAction, String aAlertName, String aAlertCookie) {
+        int notificationID = aAlertName.hashCode();
+
+        if (ACTION_ALERT_CALLBACK.equals(aAction)) {
+            callObserver(aAlertName, "alertclickcallback", aAlertCookie);
+
+            if (notificationClient.isOngoing(notificationID)) {
+                // When clicked, keep the notification if it displays progress
+                return;
+            }
+        }
+        closeNotification(aAlertName);
+    }
+
+    @WrapForJNI(stubName = "GetDpiWrapper")
+    public static int getDpi() {
+        if (sDensityDpi == 0) {
+            sDensityDpi = getApplicationContext().getResources().getDisplayMetrics().densityDpi;
+        }
+
+        return sDensityDpi;
+    }
+
+    @WrapForJNI
+    public 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(stubName = "GetScreenDepthWrapper")
+    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
+    public 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
+    public static void setFullScreen(boolean fullscreen) {
+        if (getGeckoInterface() != null)
+            getGeckoInterface().setFullScreen(fullscreen);
+    }
+
+    @WrapForJNI
+    public 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(stubName = "Vibrate1")
+    public static void vibrate(long milliseconds) {
+        sVibrationEndTime = System.nanoTime() + milliseconds * 1000000;
+        sVibrationMaybePlaying = true;
+        vibrator().vibrate(milliseconds);
+    }
+
+    @WrapForJNI(stubName = "VibrateA")
+    public 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
+    public static void cancelVibrate() {
+        sVibrationMaybePlaying = false;
+        sVibrationEndTime = 0;
+        vibrator().cancel();
+    }
+
+    @WrapForJNI
+    public static void setKeepScreenOn(final boolean on) {
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                // TODO
+            }
+        });
+    }
+
+    @WrapForJNI
+    public static void notifyDefaultPrevented(final boolean defaultPrevented) {
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                LayerView view = getLayerView();
+                PanZoomController controller = (view == null ? null : view.getPanZoomController());
+                if (controller != null) {
+                    controller.notifyDefaultActionPrevented(defaultPrevented);
+                }
+            }
+        });
+    }
+
+    @WrapForJNI
+    public 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
+    public 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
+    public static int networkLinkType() {
+        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(stubName = "GetSystemColoursWrapper")
+    public 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
+    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(stubName = "GetIconForExtensionWrapper")
+    public 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
+    public 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(stubName = "AddPluginViewWrapper")
+    public static void addPluginView(View view,
+                                     float x, float y,
+                                     float w, float h,
+                                     boolean isFullScreen) {
+        if (getGeckoInterface() != null)
+             getGeckoInterface().addPluginView(view, new RectF(x, y, x + w, y + h), isFullScreen);
+    }
+
+    @WrapForJNI
+    public static void removePluginView(View view, boolean isFullScreen) {
+        if (getGeckoInterface() != null)
+            getGeckoInterface().removePluginView(view, isFullScreen);
+    }
+
+    /**
+     * 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_SERVICES | 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(allowMultithread = true)
+    public 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(allowMultithread = true)
+    public static Context getContext() {
+        return sContextGetter.getContext();
+    }
+
+    public static void setContextGetter(ContextGetter cg) {
+        sContextGetter = cg;
+    }
+
+    @WrapForJNI(allowMultithread = true)
+    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 GeckoProfile getProfile();
+        public Activity getActivity();
+        public String getDefaultUAString();
+        public LocationListener getLocationListener();
+        public SensorEventListener getSensorEventListener();
+        public void doRestart();
+        public void setFullScreen(boolean fullscreen);
+        public void addPluginView(View view, final RectF rect, final boolean isFullScreen);
+        public void removePluginView(final View view, final boolean isFullScreen);
+        public void enableCameraView();
+        public void disableCameraView();
+        public void addAppStateListener(AppStateListener listener);
+        public void removeAppStateListener(AppStateListener listener);
+        public View getCameraView();
+        public void notifyWakeLockChanged(String topic, String state);
+        public void onInputMethodChanged(String inputMethod);
+        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;
+    }
+
+    public static android.hardware.Camera sCamera;
+
+    static native void cameraCallbackBridge(byte[] data);
+
+    static final int kPreferredFPS = 25;
+    static byte[] sCameraBuffer;
+
+
+    @WrapForJNI(stubName = "InitCameraWrapper")
+    static int[] initCamera(String aContentType, int aCamera, int aWidth, int aHeight) {
+        ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        if (getGeckoInterface() != null)
+                            getGeckoInterface().enableCameraView();
+                    } catch (Exception e) { }
+                }
+            });
+
+        // [0] = 0|1 (failure/success)
+        // [1] = width
+        // [2] = height
+        // [3] = fps
+        int[] result = new int[4];
+        result[0] = 0;
+
+        if (android.hardware.Camera.getNumberOfCameras() == 0) {
+            return result;
+        }
+
+        try {
+            sCamera = android.hardware.Camera.open(aCamera);
+
+            android.hardware.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<android.hardware.Camera.Size> sit = params.getSupportedPreviewSizes().iterator();
+            int sizeDelta = 10000000;
+            int bufferSize = 0;
+            while (sit.hasNext()) {
+                android.hardware.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;
+                }
+            }
+
+            try {
+                if (getGeckoInterface() != null) {
+                    View cameraView = getGeckoInterface().getCameraView();
+                    if (cameraView instanceof SurfaceView) {
+                        sCamera.setPreviewDisplay(((SurfaceView)cameraView).getHolder());
+                    } else if (cameraView instanceof TextureView) {
+                        sCamera.setPreviewTexture(((TextureView)cameraView).getSurfaceTexture());
+                    }
+                }
+            } catch (IOException | RuntimeException e) {
+                Log.w(LOGTAG, "Error setPreviewXXX:", e);
+            }
+
+            sCamera.setParameters(params);
+            sCameraBuffer = new byte[(bufferSize * 12) / 8];
+            sCamera.addCallbackBuffer(sCameraBuffer);
+            sCamera.setPreviewCallbackWithBuffer(new android.hardware.Camera.PreviewCallback() {
+                @Override
+                public void onPreviewFrame(byte[] data, android.hardware.Camera camera) {
+                    cameraCallbackBridge(data);
+                    if (sCamera != null)
+                        sCamera.addCallbackBuffer(sCameraBuffer);
+                }
+            });
+            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
+    static synchronized void closeCamera() {
+        ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        if (getGeckoInterface() != null)
+                            getGeckoInterface().disableCameraView();
+                    } catch (Exception e) { }
+                }
+            });
+        if (sCamera != null) {
+            sCamera.stopPreview();
+            sCamera.release();
+            sCamera = null;
+            sCameraBuffer = null;
+        }
+    }
+
+    /*
+     * Battery API related methods.
+     */
+    @WrapForJNI
+    public static void enableBatteryNotifications() {
+        GeckoBatteryManager.enableNotifications();
+    }
+
+    @WrapForJNI(stubName = "HandleGeckoMessageWrapper")
+    public static void handleGeckoMessage(final NativeJSContainer message) {
+        EventDispatcher.getInstance().dispatchEvent(message);
+        message.disposeNative();
+    }
+
+    @WrapForJNI
+    public static void disableBatteryNotifications() {
+        GeckoBatteryManager.disableNotifications();
+    }
+
+    @WrapForJNI(stubName = "GetCurrentBatteryInformationWrapper")
+    public static double[] getCurrentBatteryInformation() {
+        return GeckoBatteryManager.getCurrentInformation();
+    }
+
+    @WrapForJNI(stubName = "CheckURIVisited")
+    static void checkUriVisited(String uri) {
+        final GeckoInterface geckoInterface = getGeckoInterface();
+        if (geckoInterface == null) {
+            return;
+        }
+        geckoInterface.checkUriVisited(uri);
+    }
+
+    @WrapForJNI(stubName = "MarkURIVisited")
+    static void markUriVisited(final String uri) {
+        final GeckoInterface geckoInterface = getGeckoInterface();
+        if (geckoInterface == null) {
+            return;
+        }
+        geckoInterface.markUriVisited(uri);
+    }
+
+    @WrapForJNI(stubName = "SetURITitle")
+    static void setUriTitle(final String uri, final String title) {
+        final GeckoInterface geckoInterface = getGeckoInterface();
+        if (geckoInterface == null) {
+            return;
+        }
+        geckoInterface.setUriTitle(uri, title);
+    }
+
+    @WrapForJNI
+    static void hideProgressDialog() {
+        // unused stub
+    }
+
+    /*
+     * WebSMS related methods.
+     */
+    public static void sendMessage(String aNumber, String aMessage, int aRequestId, boolean aShouldNotify) {
+        if (!SmsManager.isEnabled()) {
+            return;
+        }
+
+        SmsManager.getInstance().send(aNumber, aMessage, aRequestId, aShouldNotify);
+    }
+
+    @WrapForJNI(stubName = "SendMessageWrapper")
+    public static void sendMessage(String aNumber, String aMessage, int aRequestId) {
+        sendMessage(aNumber, aMessage, aRequestId, /* shouldNotify */ true);
+    }
+
+    @WrapForJNI(stubName = "GetMessageWrapper")
+    public static void getMessage(int aMessageId, int aRequestId) {
+        if (!SmsManager.isEnabled()) {
+            return;
+        }
+
+        SmsManager.getInstance().getMessage(aMessageId, aRequestId);
+    }
+
+    @WrapForJNI(stubName = "DeleteMessageWrapper")
+    public static void deleteMessage(int aMessageId, int aRequestId) {
+        if (!SmsManager.isEnabled()) {
+            return;
+        }
+
+        SmsManager.getInstance().deleteMessage(aMessageId, aRequestId);
+    }
+
+    @WrapForJNI
+    public static void markMessageRead(int aMessageId, boolean aValue, boolean aSendReadReport, int aRequestId) {
+        if (!SmsManager.isEnabled()) {
+            return;
+        }
+
+        SmsManager.getInstance().markMessageRead(aMessageId, aValue, aSendReadReport, aRequestId);
+    }
+
+    @WrapForJNI(stubName = "CreateMessageCursorWrapper")
+    public static void createMessageCursor(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, String aDelivery, boolean aHasRead, boolean aRead, boolean aHasThreadId, long aThreadId, boolean aReverse, int aRequestId) {
+        if (!SmsManager.isEnabled()) {
+            return;
+        }
+
+        SmsManager.getInstance().createMessageCursor(aStartDate, aEndDate, aNumbers, aNumbersCount, aDelivery, aHasRead, aRead, aHasThreadId, aThreadId, aReverse, aRequestId);
+    }
+
+    @WrapForJNI(stubName = "GetNextMessageWrapper")
+    public static void getNextMessage(int aRequestId) {
+        if (!SmsManager.isEnabled()) {
+            return;
+        }
+
+        SmsManager.getInstance().getNextMessage(aRequestId);
+    }
+
+    @WrapForJNI(stubName = "CreateThreadCursorWrapper")
+    public static void createThreadCursor(int aRequestId) {
+        Log.i("GeckoAppShell", "CreateThreadCursorWrapper!");
+
+        if (!SmsManager.isEnabled()) {
+            return;
+        }
+
+        SmsManager.getInstance().createThreadCursor(aRequestId);
+    }
+
+    @WrapForJNI(stubName = "GetNextThreadWrapper")
+    public static void getNextThread(int aRequestId) {
+        if (!SmsManager.isEnabled()) {
+            return;
+        }
+
+        SmsManager.getInstance().getNextThread(aRequestId);
+    }
+
+    /* Called by JNI from AndroidBridge, and by reflection from tests/BaseTest.java.in */
+    @WrapForJNI
+    @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(stubName = "GetCurrentNetworkInformationWrapper")
+    public static double[] getCurrentNetworkInformation() {
+        return GeckoNetworkManager.getInstance().getCurrentInformation();
+    }
+
+    @WrapForJNI
+    public static void enableNetworkNotifications() {
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                GeckoNetworkManager.getInstance().enableNotifications();
+            }
+        });
+    }
+
+    @WrapForJNI
+    public static void disableNetworkNotifications() {
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                GeckoNetworkManager.getInstance().disableNotifications();
+            }
+        });
+    }
+
+    @WrapForJNI(stubName = "GetScreenOrientationWrapper")
+    public static short getScreenOrientation() {
+        return GeckoScreenOrientation.getInstance().getScreenOrientation().value;
+    }
+
+    @WrapForJNI
+    public static int getScreenAngle() {
+        return GeckoScreenOrientation.getInstance().getAngle();
+    }
+
+    @WrapForJNI
+    public static void enableScreenOrientationNotifications() {
+        GeckoScreenOrientation.getInstance().enableNotifications();
+    }
+
+    @WrapForJNI
+    public static void disableScreenOrientationNotifications() {
+        GeckoScreenOrientation.getInstance().disableNotifications();
+    }
+
+    @WrapForJNI
+    public static void lockScreenOrientation(int aOrientation) {
+        GeckoScreenOrientation.getInstance().lock(aOrientation);
+    }
+
+    @WrapForJNI
+    public static void unlockScreenOrientation() {
+        GeckoScreenOrientation.getInstance().unlock();
+    }
+
+    @WrapForJNI
+    public static void notifyWakeLockChanged(String topic, String state) {
+        if (getGeckoInterface() != null)
+            getGeckoInterface().notifyWakeLockChanged(topic, state);
+    }
+
+    @WrapForJNI(allowMultithread = true)
+    public static void registerSurfaceTextureFrameListener(Object surfaceTexture, final int id) {
+        ((SurfaceTexture)surfaceTexture).setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
+            @Override
+            public void onFrameAvailable(SurfaceTexture surfaceTexture) {
+                GeckoAppShell.onSurfaceTextureFrameAvailable(surfaceTexture, id);
+            }
+        });
+    }
+
+    @WrapForJNI(allowMultithread = true)
+    public static void unregisterSurfaceTextureFrameListener(Object surfaceTexture) {
+        ((SurfaceTexture)surfaceTexture).setOnFrameAvailableListener(null);
+    }
+
+    @WrapForJNI
+    public 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(stubName = "GetProxyForURIWrapper")
+    public 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(allowMultithread = true)
+    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(allowMultithread = true, narrowChars = true)
+    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(allowMultithread = true, narrowChars = true)
+    static String connectionGetMimeType(URLConnection connection) {
+        return connection.getContentType();
+    }
+
+    /**
+     * Retrieve the absolute path of an external storage directory.
+     *
+     * @param type The type of directory to return
+     * @return Absolute path of the specified directory or null on failure
+     */
+    @WrapForJNI
+    static String getExternalPublicDirectory(final String type) {
+        final String state = Environment.getExternalStorageState();
+        if (!Environment.MEDIA_MOUNTED.equals(state) &&
+            !Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
+            // External storage is not available.
+            return null;
+        }
+
+        if ("sdcard".equals(type)) {
+            // SD card has a separate path.
+            return Environment.getExternalStorageDirectory().getAbsolutePath();
+        }
+
+        final String systemType;
+        if ("downloads".equals(type)) {
+            systemType = Environment.DIRECTORY_DOWNLOADS;
+        } else if ("pictures".equals(type)) {
+            systemType = Environment.DIRECTORY_PICTURES;
+        } else if ("videos".equals(type)) {
+            systemType = Environment.DIRECTORY_MOVIES;
+        } else if ("music".equals(type)) {
+            systemType = Environment.DIRECTORY_MUSIC;
+        } else if ("apps".equals(type)) {
+            File appInternalStorageDirectory = getApplicationContext().getFilesDir();
+            return new File(appInternalStorageDirectory, "mozilla").getAbsolutePath();
+        } else {
+            return null;
+        }
+        return Environment.getExternalStoragePublicDirectory(systemType).getAbsolutePath();
+    }
+
+    @WrapForJNI
+    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
+    public 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;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoBatteryManager.java
@@ -0,0 +1,196 @@
+/* -*- 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;
+
+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;
+    }
+
+    @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())) {
+            GeckoAppShell.notifyBatteryChange(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() };
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java
@@ -0,0 +1,1439 @@
+/* -*- 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 java.util.concurrent.Semaphore;
+
+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.GeckoEventListener;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
+
+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, GeckoEditableListener, GeckoEventListener {
+
+    private static final boolean DEBUG = false;
+    private static final String LOGTAG = "GeckoEditable";
+
+    // Filters to implement Editable's filtering functionality
+    private InputFilter[] mFilters;
+
+    private final SpannableStringBuilder mText;
+    private final Editable mProxy;
+    private final ActionQueue mActionQueue;
+
+    // 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 mNeedCompositionUpdate; // Used by IC thread
+    private boolean mFocused; // Used by IC thread
+    private boolean mGeckoFocused; // Used by Gecko thread
+    private boolean mIgnoreSelectionChange; // Used by Gecko thread
+    private volatile boolean mSuppressCompositions;
+    private volatile boolean mSuppressKeyUp;
+
+    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
+    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
+    private native void onImeSynchronize();
+
+    @WrapForJNI
+    private native void onImeAcknowledgeFocus();
+
+    @WrapForJNI
+    private native void onImeReplaceText(int start, int end, String text);
+
+    @WrapForJNI
+    private native void onImeAddCompositionRange(int start, int end, int rangeType,
+                                                 int rangeStyles, int rangeLineStyle,
+                                                 boolean rangeBoldLine, int rangeForeColor,
+                                                 int rangeBackColor, int rangeLineColor);
+
+    @WrapForJNI
+    private native void onImeUpdateComposition(int start, int end);
+
+    /* 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 focus events (in notifyIME); use with onImeAcknowledgeFocus
+        static final int TYPE_ACKNOWLEDGE_FOCUS = 4;
+        // For switching handler; use with onImeSynchronize
+        static final int TYPE_SET_HANDLER = 5;
+        // For updating composition; use with onImeUpdateComposition
+        static final int TYPE_UPDATE_COMPOSITION = 6;
+
+        final int mType;
+        boolean mUpdateComposition;
+        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, boolean update) {
+            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;
+            action.mUpdateComposition = update;
+            return action;
+        }
+
+        static Action newRemoveSpan(Object object, boolean update) {
+            final Action action = new Action(TYPE_REMOVE_SPAN);
+            action.mSpanObject = object;
+            action.mUpdateComposition = update;
+            return action;
+        }
+
+        static Action newSetHandler(Handler handler) {
+            final Action action = new Action(TYPE_SET_HANDLER);
+            action.mHandler = handler;
+            return action;
+        }
+
+        static Action newUpdateComposition(int start, int end) {
+            final Action action = new Action(TYPE_UPDATE_COMPOSITION);
+            action.mStart = start;
+            action.mEnd = end;
+            return action;
+        }
+    }
+
+    /* Queue of editing actions sent to Gecko thread that
+       the Gecko thread has not responded to yet */
+    private final class ActionQueue {
+        private final ConcurrentLinkedQueue<Action> mActions;
+        private final Semaphore mActionsActive;
+        private KeyCharacterMap mKeyMap;
+
+        ActionQueue() {
+            mActions = new ConcurrentLinkedQueue<Action>();
+            mActionsActive = new Semaphore(1);
+        }
+
+        void offer(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;
+            }
+
+            if (mActions.isEmpty()) {
+                mActionsActive.acquireUninterruptibly();
+                mActions.offer(action);
+            } else synchronized (this) {
+                // tryAcquire here in case Gecko thread has just released it
+                mActionsActive.tryAcquire();
+                mActions.offer(action);
+            }
+
+            switch (action.mType) {
+            case Action.TYPE_EVENT:
+            case Action.TYPE_SET_SPAN:
+            case Action.TYPE_REMOVE_SPAN:
+            case Action.TYPE_SET_HANDLER:
+                onImeSynchronize();
+                break;
+
+            case Action.TYPE_REPLACE_TEXT:
+                // 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)) {
+                    mNeedCompositionUpdate = false;
+                } else {
+                    // Since we don't have a composition, we can try sending key events.
+                    sendCharKeyEvents(action);
+                }
+                onImeReplaceText(action.mStart, action.mEnd, action.mSequence.toString());
+                break;
+
+            case Action.TYPE_ACKNOWLEDGE_FOCUS:
+                onImeAcknowledgeFocus();
+                break;
+
+            case Action.TYPE_UPDATE_COMPOSITION:
+                onImeUpdateComposition(action.mStart, action.mEnd);
+                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);
+            }
+        }
+
+        /**
+         * Remove the head of the queue. Throw if queue is empty.
+         */
+        void poll() {
+            if (DEBUG) {
+                ThreadUtils.assertOnGeckoThread();
+            }
+            if (mActions.poll() == null) {
+                throw new IllegalStateException("empty actions queue");
+            }
+
+            synchronized (this) {
+                if (mActions.isEmpty()) {
+                    mActionsActive.release();
+                }
+            }
+        }
+
+        /**
+         * Return, but don't remove, the head of the queue, or null if queue is empty.
+         *
+         * @return head of the queue or null if empty.
+         */
+        Action peek() {
+            if (DEBUG) {
+                ThreadUtils.assertOnGeckoThread();
+            }
+            return mActions.peek();
+        }
+
+        void syncWithGecko() {
+            if (DEBUG) {
+                assertOnIcThread();
+            }
+            if (mFocused && !mActions.isEmpty()) {
+                if (DEBUG) {
+                    Log.d(LOGTAG, "syncWithGecko blocking on thread " +
+                                  Thread.currentThread().getName());
+                }
+                mActionsActive.acquireUninterruptibly();
+                mActionsActive.release();
+            } else if (DEBUG && !mFocused) {
+                Log.d(LOGTAG, "skipped syncWithGecko (no focus)");
+            }
+        }
+
+        boolean isEmpty() {
+            return mActions.isEmpty();
+        }
+    }
+
+    @WrapForJNI
+    GeckoEditable(final GeckoView v) {
+        if (DEBUG) {
+            // Called by nsWindow.
+            ThreadUtils.assertOnGeckoThread();
+        }
+        mActionQueue = new ActionQueue();
+
+        mText = new SpannableStringBuilder();
+
+        final Class<?>[] PROXY_INTERFACES = { Editable.class };
+        mProxy = (Editable)Proxy.newProxyInstance(
+                Editable.class.getClassLoader(),
+                PROXY_INTERFACES, this);
+
+        mIcRunHandler = mIcPostHandler = ThreadUtils.getUiHandler();
+
+        onViewChange(v);
+    }
+
+    @WrapForJNI @Override
+    protected native void disposeNative();
+
+    @WrapForJNI
+    /* package */ 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)");
+                }
+
+                if (newListener != null) {
+                    // Make sure there are no other things going on.
+                    mActionQueue.syncWithGecko();
+                    mListener = newListener;
+                } else {
+                    // 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.
+                    mListener = null;
+                    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.
+        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;
+                mIcPostHandler.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) {
+        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 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) {
+        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;
+
+            for (Object span : spans) {
+                if ((text.getSpanFlags(span) & Spanned.SPAN_COMPOSING) == 0) {
+                    continue;
+                }
+                if (!useEntireText) {
+                    composingStart = Math.min(composingStart, text.getSpanStart(span));
+                    composingEnd = Math.max(composingEnd, text.getSpanEnd(span));
+                }
+                found = true;
+            }
+
+            if (useEntireText && (selStart < 0 || selEnd < 0)) {
+                selStart = composingEnd;
+                selEnd = composingEnd;
+            }
+
+            if (found) {
+                icSendComposition(text, selStart, selEnd, composingStart, composingEnd);
+                if (notifyGecko) {
+                    mActionQueue.offer(Action.newUpdateComposition(
+                            composingStart, composingEnd));
+                }
+                return true;
+            }
+        }
+
+        if (notifyGecko) {
+            // Set the selection by using a composition without ranges
+            mActionQueue.offer(Action.newUpdateComposition(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.
+                if (Versions.feature14Plus) {
+                    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 mActionQueue.offer 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 selection/composition/etc. accordingly.
+        */
+        if (mNeedCompositionUpdate) {
+            // Make sure Gecko selection is in-sync with Java selection first.
+            icUpdateComposition();
+        }
+        onKeyEvent(event, action, metaState, /* isSynthesizedImeKey */ false);
+        mActionQueue.offer(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;
+        }
+        if (!inBatchMode && mNeedCompositionUpdate) {
+            icUpdateComposition();
+        }
+        mInBatchMode = inBatchMode;
+    }
+
+    private void icUpdateComposition() {
+        if (DEBUG) {
+            assertOnIcThread();
+        }
+        mNeedCompositionUpdate = false;
+        mActionQueue.syncWithGecko();
+        icMaybeSendComposition(mText, /* useEntireText */ false, /* notifyGecko */ true);
+    }
+
+    private void geckoScheduleCompositionUpdate() {
+        if (DEBUG) {
+            ThreadUtils.assertOnGeckoThread();
+        }
+        // May be called from either Gecko or IC thread
+        geckoPostToIc(new Runnable() {
+            @Override
+            public void run() {
+                if (mInBatchMode || !mNeedCompositionUpdate) {
+                    return;
+                }
+                if (!mActionQueue.isEmpty()) {
+                    // We are likely to block here, so wait a little and try again.
+                    mIcPostHandler.postDelayed(this, 10);
+                    return;
+                }
+                icUpdateComposition();
+            }
+        });
+    }
+
+    @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
+    public Handler setInputConnectionHandler(Handler handler) {
+        if (handler == mIcPostHandler || !mFocused) {
+            return mIcPostHandler;
+        }
+        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.
+        mActionQueue.offer(Action.newSetHandler(handler));
+        mActionQueue.syncWithGecko();
+        return handler;
+    }
+
+    @Override // GeckoEditableClient
+    public void postToInputConnection(final Runnable runnable) {
+        mIcPostHandler.post(runnable);
+    }
+
+    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;
+
+        geckoPostToIc(new Runnable() { // posting to new IC thread
+            @Override
+            public void run() {
+                synchronized (newHandler) {
+                    while (mIcRunHandler != newHandler) {
+                        try {
+                            newHandler.wait();
+                        } catch (InterruptedException e) {
+                        }
+                    }
+                }
+            }
+        });
+    }
+
+    // GeckoEditableListener interface
+
+    private void geckoActionReply() {
+        if (DEBUG) {
+            // GeckoEditableListener methods should all be called from the Gecko thread
+            ThreadUtils.assertOnGeckoThread();
+        }
+
+        final Action action = mActionQueue.peek();
+        if (action == null) {
+            throw new IllegalStateException("empty actions queue");
+        }
+
+        if (DEBUG) {
+            Log.d(LOGTAG, "reply: Action(" +
+                          getConstantName(Action.class, "TYPE_", action.mType) + ")");
+        }
+        switch (action.mType) {
+        case Action.TYPE_SET_SPAN:
+            mText.setSpan(action.mSpanObject, action.mStart, action.mEnd, action.mSpanFlags);
+            break;
+
+        case Action.TYPE_REMOVE_SPAN:
+            mText.removeSpan(action.mSpanObject);
+            break;
+
+        case Action.TYPE_SET_HANDLER:
+            geckoSetIcHandler(action.mHandler);
+            break;
+        }
+
+        if (action.mUpdateComposition) {
+            geckoScheduleCompositionUpdate();
+        }
+    }
+
+    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 Object[] spans = mText.getSpans(0, mText.length(), Object.class);
+            for (Object span : spans) {
+                if ((mText.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) {
+                    throw new IllegalStateException("composition not cancelled");
+                }
+            }
+        }
+    }
+
+    @WrapForJNI @Override
+    public 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 != NOTIFY_IME_REPLY_EVENT) {
+                Log.d(LOGTAG, "notifyIME(" +
+                              getConstantName(GeckoEditableListener.class, "NOTIFY_IME_", type) +
+                              ")");
+            }
+        }
+
+        if (type == NOTIFY_IME_REPLY_EVENT) {
+            try {
+                if (mGeckoFocused) {
+                    // When mGeckoFocused is false, the reply is for a stale action,
+                    // and we should not do anything
+                    geckoActionReply();
+                } else if (DEBUG) {
+                    Log.d(LOGTAG, "discarding stale reply");
+                }
+            } finally {
+                // Ensure action is always removed from queue
+                // even if stale action results in exception in geckoActionReply
+                mActionQueue.poll();
+            }
+            return;
+        } else if (type == NOTIFY_IME_TO_COMMIT_COMPOSITION) {
+            notifyCommitComposition();
+            return;
+        } else if (type == NOTIFY_IME_TO_CANCEL_COMPOSITION) {
+            notifyCancelComposition();
+            return;
+        }
+
+        geckoPostToIc(new Runnable() {
+            @Override
+            public void run() {
+                if (type == NOTIFY_IME_OF_FOCUS) {
+                    mFocused = true;
+                    // Unmask events on the Gecko side
+                    mActionQueue.offer(new Action(Action.TYPE_ACKNOWLEDGE_FOCUS));
+                }
+
+                // Make sure there are no other things going on. If we sent
+                // Action.TYPE_ACKNOWLEDGE_FOCUS, this line also makes us
+                // wait for Gecko to update us on the newly focused content
+                mActionQueue.syncWithGecko();
+                if (mListener != null) {
+                    mListener.notifyIME(type);
+                }
+
+                // Unset mFocused after we call syncWithGecko because
+                // syncWithGecko becomes a no-op when mFocused is false.
+                if (type == NOTIFY_IME_OF_BLUR) {
+                    mFocused = false;
+                }
+            }
+        });
+
+        // Register/unregister Gecko-side text selection listeners
+        // and update the mGeckoFocused flag.
+        if (type == NOTIFY_IME_OF_BLUR && mGeckoFocused) {
+            // Check for focus here because Gecko may send us a blur before a focus in some
+            // cases, and we don't want to unregister an event that was not registered.
+            mGeckoFocused = false;
+            mSuppressCompositions = false;
+            EventDispatcher.getInstance().
+                unregisterGeckoThreadListener(this, "TextSelection:DraggingHandle");
+        } else if (type == NOTIFY_IME_OF_FOCUS) {
+            mGeckoFocused = true;
+            mSuppressCompositions = false;
+            EventDispatcher.getInstance().
+                registerGeckoThreadListener(this, "TextSelection:DraggingHandle");
+        }
+    }
+
+    @WrapForJNI @Override
+    public 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 @Override
+    public void onSelectionChange(int start, int end) {
+        if (DEBUG) {
+            // GeckoEditableListener methods should all be called from the Gecko thread
+            ThreadUtils.assertOnGeckoThread();
+            Log.d(LOGTAG, "onSelectionChange(" + start + ", " + end + ")");
+        }
+        if (start < 0 || start > mText.length() || end < 0 || end > mText.length()) {
+            Log.e(LOGTAG, "invalid selection notification range: " +
+                  start + " to " + end + ", length: " + mText.length());
+            throw new IllegalArgumentException("invalid selection notification range");
+        }
+
+        if (mIgnoreSelectionChange) {
+            start = Selection.getSelectionStart(mText);
+            end = Selection.getSelectionEnd(mText);
+            mIgnoreSelectionChange = false;
+
+        } else {
+            Selection.setSelection(mText, start, end);
+        }
+
+        final int newStart = start;
+        final int newEnd = end;
+        geckoPostToIc(new Runnable() {
+            @Override
+            public void run() {
+                if (mListener == null) {
+                    return;
+                }
+                mListener.onSelectionChange(newStart, newEnd);
+            }
+        });
+    }
+
+    private void geckoReplaceText(int start, int oldEnd, CharSequence newText) {
+        mText.replace(start, oldEnd, newText);
+    }
+
+    private boolean geckoIsSameText(int start, int oldEnd, CharSequence newText) {
+        return oldEnd - start == newText.length() &&
+               TextUtils.regionMatches(mText, start, newText, 0, oldEnd - start);
+    }
+
+    @WrapForJNI @Override
+    public 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");
+        }
+        /* For the "end" parameters, Gecko can pass in a large
+           number to denote "end of the text". Fix that here */
+        final int oldEnd = unboundedOldEnd > mText.length() ? mText.length() : unboundedOldEnd;
+        // new end should always match text
+        if (unboundedOldEnd <= mText.length() &&
+                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 = mActionQueue.peek();
+
+        if (action != null && action.mType == Action.TYPE_ACKNOWLEDGE_FOCUS) {
+            // Simply replace the text for newly-focused editors.
+            mText.replace(0, mText.length(), text);
+
+        } 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.
+                geckoReplaceText(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()) {
+                // The new text exactly matches our sequence, so do a direct replace.
+                geckoReplaceText(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.
+                geckoReplaceText(start, action.mStart, text.subSequence(0, indexInText));
+
+                // Then Replace the sequence itself to preserve new spans.
+                final int actionStart = indexInText + start;
+                geckoReplaceText(actionStart, actionStart + action.mEnd - action.mStart,
+                                 action.mSequence);
+
+                // Finally replace part of text after the sequence.
+                final int actionEnd = actionStart + action.mSequence.length();
+                geckoReplaceText(actionEnd, actionEnd + oldEnd - action.mEnd,
+                                 text.subSequence(actionEnd - start, text.length()));
+            }
+
+        } 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_UPDATE_COMPOSITION);
+            return;
+
+        } else {
+            // Gecko side initiated the text change.
+            geckoReplaceText(start, oldEnd, text);
+        }
+
+        geckoPostToIc(new Runnable() {
+            @Override
+            public void run() {
+                if (mListener == null) {
+                    return;
+                }
+                mListener.onTextChange(text, start, oldEnd, newEnd);
+            }
+        });
+    }
+
+    @WrapForJNI @Override
+    public 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);
+            }
+        });
+    }
+
+    // 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 {
+            // Method queries the Editable; must sync with Gecko first
+            // then call on the inner Editable itself
+            mActionQueue.syncWithGecko();
+            target = mText;
+        }
+        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 == Selection.SELECTION_START ||
+                what == Selection.SELECTION_END) {
+            Log.w(LOGTAG, "selection removed with removeSpan()");
+        }
+
+        // "what" could be a composing span (BaseInputConnection.COMPOSING extends
+        // NoCopySpan), so we should update the Gecko composition
+        final boolean update = !mNeedCompositionUpdate && what instanceof NoCopySpan;
+        mActionQueue.offer(Action.newRemoveSpan(what, update));
+        mNeedCompositionUpdate |= update;
+    }
+
+    @Override
+    public void setSpan(Object what, int start, int end, int flags) {
+        // If mUpdateComposition is true, it means something in the queue will be
+        // scheduling an update, so it would be redundant to have this action schedule
+        // another update.
+        final boolean update = !mNeedCompositionUpdate &&
+                (flags & Spanned.SPAN_INTERMEDIATE) == 0 && (
+                (flags & Spanned.SPAN_COMPOSING) != 0 ||
+                what == Selection.SELECTION_START ||
+                what == Selection.SELECTION_END);
+        mActionQueue.offer(Action.newSetSpan(what, start, end, flags, update));
+        mNeedCompositionUpdate |= update;
+    }
+
+    // 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()");
+        mText.clearSpans();
+    }
+
+    @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);
+        }
+        mActionQueue.offer(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");
+    }
+
+    // GeckoEventListener implementation
+
+    @Override
+    public void handleMessage(String event, JSONObject message) {
+        if (!"TextSelection:DraggingHandle".equals(event)) {
+            return;
+        }
+
+        mSuppressCompositions = message.optBoolean("dragging", false);
+    }
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableClient.java
@@ -0,0 +1,22 @@
+/* -*- 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);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableListener.java
@@ -0,0 +1,41 @@
+/* -*- 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.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(int start, int end);
+    void onTextChange(CharSequence text, int start, int oldEnd, int newEnd);
+    void onDefaultKeyEvent(KeyEvent event);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEvent.java
@@ -0,0 +1,634 @@
+/* -*- 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.nio.ByteBuffer;
+import java.util.concurrent.ArrayBlockingQueue;
+
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.gfx.DisplayPortMetrics;
+import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
+
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorManager;
+import android.location.Address;
+import android.location.Location;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import org.mozilla.gecko.annotation.JNITarget;
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+/**
+ * We're not allowed to hold on to most events given to us
+ * so we save the parts of the events we want to use in GeckoEvent.
+ * Fields have different meanings depending on the event type.
+ */
+@JNITarget
+public class GeckoEvent {
+    private static final String LOGTAG = "GeckoEvent";
+
+    private static final int EVENT_FACTORY_SIZE = 5;
+
+    // Maybe we're probably better to just make mType non final, and just store GeckoEvents in here...
+    private static final SparseArray<ArrayBlockingQueue<GeckoEvent>> mEvents = new SparseArray<ArrayBlockingQueue<GeckoEvent>>();
+
+    public static GeckoEvent get(NativeGeckoEvent type) {
+        synchronized (mEvents) {
+            ArrayBlockingQueue<GeckoEvent> events = mEvents.get(type.value);
+            if (events != null && events.size() > 0) {
+                return events.poll();
+            }
+        }
+
+        return new GeckoEvent(type);
+    }
+
+    public void recycle() {
+        synchronized (mEvents) {
+            ArrayBlockingQueue<GeckoEvent> events = mEvents.get(mType);
+            if (events == null) {
+                events = new ArrayBlockingQueue<GeckoEvent>(EVENT_FACTORY_SIZE);
+                mEvents.put(mType, events);
+            }
+
+            events.offer(this);
+        }
+    }
+
+    // Make sure to keep these values in sync with the enum in
+    // AndroidGeckoEvent in widget/android/AndroidJavaWrappers.h
+    @JNITarget
+    public enum NativeGeckoEvent {
+        NATIVE_POKE(0),
+        MOTION_EVENT(2),
+        SENSOR_EVENT(3),
+        LOCATION_EVENT(5),
+        LOAD_URI(12),
+        NOOP(15),
+        VIEWPORT(20),
+        VISITED(21),
+        NETWORK_CHANGED(22),
+        THUMBNAIL(25),
+        SCREENORIENTATION_CHANGED(27),
+        NATIVE_GESTURE_EVENT(31),
+        CALL_OBSERVER(33),
+        REMOVE_OBSERVER(34),
+        LOW_MEMORY(35),
+        NETWORK_LINK_CHANGE(36),
+        TELEMETRY_HISTOGRAM_ADD(37),
+        TELEMETRY_UI_SESSION_START(42),
+        TELEMETRY_UI_SESSION_STOP(43),
+        TELEMETRY_UI_EVENT(44),
+        GAMEPAD_ADDREMOVE(45),
+        GAMEPAD_DATA(46),
+        LONG_PRESS(47),
+        ZOOMEDVIEW(48);
+
+        public final int value;
+
+        private NativeGeckoEvent(int value) {
+            this.value = value;
+        }
+    }
+
+    public static final int ACTION_MAGNIFY_START = 11;
+    public static final int ACTION_MAGNIFY = 12;
+    public static final int ACTION_MAGNIFY_END = 13;
+
+    public static final int ACTION_GAMEPAD_ADDED = 1;
+    public static final int ACTION_GAMEPAD_REMOVED = 2;
+
+    public static final int ACTION_GAMEPAD_BUTTON = 1;
+    public static final int ACTION_GAMEPAD_AXES = 2;
+
+    private final int mType;
+    private int mAction;
+    private long mTime;
+    private Point[] mPoints;
+    private int[] mPointIndicies;
+    private int mPointerIndex; // index of the point that has changed
+    private float[] mOrientations;
+    private float[] mPressures;
+    private int[] mToolTypes;
+    private Point[] mPointRadii;
+    private Rect mRect;
+    private double mX;
+    private double mY;
+    private double mZ;
+    private double mW;
+
+    private int mMetaState;
+    private int mFlags;
+    private int mCount;
+    private String mCharacters;
+    private String mCharactersExtra;
+    private String mData;
+    private Location mLocation;
+
+    private int     mConnectionType;
+    private boolean mIsWifi;
+    private int     mDHCPGateway;
+
+    private short mScreenOrientation;
+    private short mScreenAngle;
+
+    private ByteBuffer mBuffer;
+
+    private int mWidth;
+    private int mHeight;
+
+    private int mID;
+    private int mGamepadButton;
+    private boolean mGamepadButtonPressed;
+    private float mGamepadButtonValue;
+    private float[] mGamepadValues;
+
+    private GeckoEvent(NativeGeckoEvent event) {
+        mType = event.value;
+    }
+
+    public static GeckoEvent createNoOpEvent() {
+        return GeckoEvent.get(NativeGeckoEvent.NOOP);
+    }
+
+    /**
+     * This method is a replacement for the the KeyEvent.isGamepadButton method to be
+     * compatible with Build.VERSION.SDK_INT < 12. This is an implementation of the
+     * same method isGamepadButton available after SDK 12.
+     * @param keyCode int with the key code (Android key constant from KeyEvent).
+     * @return True if the keycode is a gamepad button, such as {@link #KEYCODE_BUTTON_A}.
+     */
+    private static boolean isGamepadButton(int keyCode) {
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_BUTTON_A:
+            case KeyEvent.KEYCODE_BUTTON_B:
+            case KeyEvent.KEYCODE_BUTTON_C:
+            case KeyEvent.KEYCODE_BUTTON_X:
+            case KeyEvent.KEYCODE_BUTTON_Y:
+            case KeyEvent.KEYCODE_BUTTON_Z:
+            case KeyEvent.KEYCODE_BUTTON_L1:
+            case KeyEvent.KEYCODE_BUTTON_R1:
+            case KeyEvent.KEYCODE_BUTTON_L2:
+            case KeyEvent.KEYCODE_BUTTON_R2:
+            case KeyEvent.KEYCODE_BUTTON_THUMBL:
+            case KeyEvent.KEYCODE_BUTTON_THUMBR:
+            case KeyEvent.KEYCODE_BUTTON_START:
+            case KeyEvent.KEYCODE_BUTTON_SELECT:
+            case KeyEvent.KEYCODE_BUTTON_MODE:
+            case KeyEvent.KEYCODE_BUTTON_1:
+            case KeyEvent.KEYCODE_BUTTON_2:
+            case KeyEvent.KEYCODE_BUTTON_3:
+            case KeyEvent.KEYCODE_BUTTON_4:
+            case KeyEvent.KEYCODE_BUTTON_5:
+            case KeyEvent.KEYCODE_BUTTON_6:
+            case KeyEvent.KEYCODE_BUTTON_7:
+            case KeyEvent.KEYCODE_BUTTON_8:
+            case KeyEvent.KEYCODE_BUTTON_9:
+            case KeyEvent.KEYCODE_BUTTON_10:
+            case KeyEvent.KEYCODE_BUTTON_11:
+            case KeyEvent.KEYCODE_BUTTON_12:
+            case KeyEvent.KEYCODE_BUTTON_13:
+            case KeyEvent.KEYCODE_BUTTON_14:
+            case KeyEvent.KEYCODE_BUTTON_15:
+            case KeyEvent.KEYCODE_BUTTON_16:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    public static GeckoEvent createNativeGestureEvent(int action, PointF pt, double size) {
+        try {
+            GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.NATIVE_GESTURE_EVENT);
+            event.mAction = action;
+            event.mCount = 1;
+            event.mPoints = new Point[1];
+
+            PointF geckoPoint = new PointF(pt.x, pt.y);
+            geckoPoint = GeckoAppShell.getLayerView().convertViewPointToLayerPoint(geckoPoint);
+
+            if (geckoPoint == null) {
+                // This could happen if Gecko isn't ready yet.
+                return null;
+            }
+
+            event.mPoints[0] = new Point((int)Math.floor(geckoPoint.x), (int)Math.floor(geckoPoint.y));
+
+            event.mX = size;
+            event.mTime = System.currentTimeMillis();
+            return event;
+        } catch (Exception e) {
+            // This can happen if Gecko isn't ready yet
+            return null;
+        }
+    }
+
+    /**
+     * Creates a GeckoEvent that contains the data from the MotionEvent.
+     * The keepInViewCoordinates parameter can be set to false to convert from the Java
+     * coordinate system (device pixels relative to the LayerView) to a coordinate system
+     * relative to gecko's coordinate system (CSS pixels relative to gecko scroll position).
+     */
+    public static GeckoEvent createMotionEvent(MotionEvent m, boolean keepInViewCoordinates) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.MOTION_EVENT);
+        event.initMotionEvent(m, keepInViewCoordinates);
+        return event;
+    }
+
+    /**
+     * Creates a GeckoEvent that contains the data from the LongPressEvent, to be
+     * dispatched in CSS pixels relative to gecko's scroll position.
+     */
+    public static GeckoEvent createLongPressEvent(MotionEvent m) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LONG_PRESS);
+        event.initMotionEvent(m, false);
+        return event;
+    }
+
+    private void initMotionEvent(MotionEvent m, boolean keepInViewCoordinates) {
+        mAction = m.getActionMasked();
+        mTime = (System.currentTimeMillis() - SystemClock.elapsedRealtime()) + m.getEventTime();
+        mMetaState = m.getMetaState();
+
+        switch (mAction) {
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_POINTER_UP:
+            case MotionEvent.ACTION_POINTER_DOWN:
+            case MotionEvent.ACTION_DOWN:
+            case MotionEvent.ACTION_MOVE:
+            case MotionEvent.ACTION_HOVER_ENTER:
+            case MotionEvent.ACTION_HOVER_MOVE:
+            case MotionEvent.ACTION_HOVER_EXIT: {
+                mCount = m.getPointerCount();
+                mPoints = new Point[mCount];
+                mPointIndicies = new int[mCount];
+                mOrientations = new float[mCount];
+                mPressures = new float[mCount];
+                mToolTypes = new int[mCount];
+                mPointRadii = new Point[mCount];
+                mPointerIndex = m.getActionIndex();
+                for (int i = 0; i < mCount; i++) {
+                    addMotionPoint(i, i, m, keepInViewCoordinates);
+                }
+                break;
+            }
+            default: {
+                mCount = 0;
+                mPointerIndex = -1;
+                mPoints = new Point[mCount];
+                mPointIndicies = new int[mCount];
+                mOrientations = new float[mCount];
+                mPressures = new float[mCount];
+                mToolTypes = new int[mCount];
+                mPointRadii = new Point[mCount];
+            }
+        }
+    }
+
+    private void addMotionPoint(int index, int eventIndex, MotionEvent event, boolean keepInViewCoordinates) {
+        try {
+            PointF geckoPoint = new PointF(event.getX(eventIndex), event.getY(eventIndex));
+            if (!keepInViewCoordinates) {
+                geckoPoint = GeckoAppShell.getLayerView().convertViewPointToLayerPoint(geckoPoint);
+            }
+
+            mPoints[index] = new Point((int)Math.floor(geckoPoint.x), (int)Math.floor(geckoPoint.y));
+            mPointIndicies[index] = event.getPointerId(eventIndex);
+
+            double radians = event.getOrientation(eventIndex);
+            mOrientations[index] = (float) Math.toDegrees(radians);
+            // w3c touchevents spec does not allow orientations == 90
+            // this shifts it to -90, which will be shifted to zero below
+            if (mOrientations[index] == 90)
+                mOrientations[index] = -90;
+
+            // w3c touchevent radius are given by an orientation between 0 and 90
+            // the radius is found by removing the orientation and measuring the x and y
+            // radius of the resulting ellipse
+            // for android orientations >= 0 and < 90, the major axis should correspond to
+            // just reporting the y radius as the major one, and x as minor
+            // however, for a radius < 0, we have to shift the orientation by adding 90, and
+            // reverse which radius is major and minor
+            if (mOrientations[index] < 0) {
+                mOrientations[index] += 90;
+                mPointRadii[index] = new Point((int) event.getToolMajor(eventIndex) / 2,
+                                               (int) event.getToolMinor(eventIndex) / 2);
+            } else {
+                mPointRadii[index] = new Point((int) event.getToolMinor(eventIndex) / 2,
+                                               (int) event.getToolMajor(eventIndex) / 2);
+            }
+
+            if (!keepInViewCoordinates) {
+                // If we are converting to gecko CSS pixels, then we should adjust the
+                // radii as well
+                float zoom = GeckoAppShell.getLayerView().getViewportMetrics().zoomFactor;
+                mPointRadii[index].x /= zoom;
+                mPointRadii[index].y /= zoom;
+            }
+            mPressures[index] = event.getPressure(eventIndex);
+            if (Versions.feature14Plus) {
+                mToolTypes[index] = event.getToolType(index);
+            }
+        } catch (Exception ex) {
+            Log.e(LOGTAG, "Error creating motion point " + index, ex);
+            mPointRadii[index] = new Point(0, 0);
+            mPoints[index] = new Point(0, 0);
+        }
+    }
+
+    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;
+    }
+
+    public static GeckoEvent createSensorEvent(SensorEvent s) {
+        int sensor_type = s.sensor.getType();
+        GeckoEvent event = null;
+
+        switch (sensor_type) {
+
+        case Sensor.TYPE_ACCELEROMETER:
+            event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
+            event.mFlags = GeckoHalDefines.SENSOR_ACCELERATION;
+            event.mMetaState = HalSensorAccuracyFor(s.accuracy);
+            event.mX = s.values[0];
+            event.mY = s.values[1];
+            event.mZ = s.values[2];
+            break;
+
+        case Sensor.TYPE_LINEAR_ACCELERATION:
+            event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
+            event.mFlags = GeckoHalDefines.SENSOR_LINEAR_ACCELERATION;
+            event.mMetaState = HalSensorAccuracyFor(s.accuracy);
+            event.mX = s.values[0];
+            event.mY = s.values[1];
+            event.mZ = s.values[2];
+            break;
+
+        case Sensor.TYPE_ORIENTATION:
+            event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
+            event.mFlags = GeckoHalDefines.SENSOR_ORIENTATION;
+            event.mMetaState = HalSensorAccuracyFor(s.accuracy);
+            event.mX = s.values[0];
+            event.mY = s.values[1];
+            event.mZ = s.values[2];
+            break;
+
+        case Sensor.TYPE_GYROSCOPE:
+            event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
+            event.mFlags = GeckoHalDefines.SENSOR_GYROSCOPE;
+            event.mMetaState = HalSensorAccuracyFor(s.accuracy);
+            event.mX = Math.toDegrees(s.values[0]);
+            event.mY = Math.toDegrees(s.values[1]);
+            event.mZ = Math.toDegrees(s.values[2]);
+            break;
+
+        case Sensor.TYPE_PROXIMITY:
+            event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
+            event.mFlags = GeckoHalDefines.SENSOR_PROXIMITY;
+            event.mMetaState = HalSensorAccuracyFor(s.accuracy);
+            event.mX = s.values[0];
+            event.mY = 0;
+            event.mZ = s.sensor.getMaximumRange();
+            break;
+
+        case Sensor.TYPE_LIGHT:
+            event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
+            event.mFlags = GeckoHalDefines.SENSOR_LIGHT;
+            event.mMetaState = HalSensorAccuracyFor(s.accuracy);
+            event.mX = s.values[0];
+            break;
+
+        case Sensor.TYPE_ROTATION_VECTOR:
+        case Sensor.TYPE_GAME_ROTATION_VECTOR: // API >= 18
+            event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
+            event.mFlags = (sensor_type == Sensor.TYPE_ROTATION_VECTOR ?
+                    GeckoHalDefines.SENSOR_ROTATION_VECTOR :
+                    GeckoHalDefines.SENSOR_GAME_ROTATION_VECTOR);
+            event.mMetaState = HalSensorAccuracyFor(s.accuracy);
+            event.mX = s.values[0];
+            event.mY = s.values[1];
+            event.mZ = s.values[2];
+            if (s.values.length >= 4) {
+                event.mW = 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.
+                event.mW = 1 - s.values[0] * s.values[0] - s.values[1] * s.values[1] - s.values[2] * s.values[2];
+                event.mW = (event.mW > 0.0) ? Math.sqrt(event.mW) : 0.0;
+            }
+            break;
+        }
+
+        // SensorEvent timestamp is in nanoseconds, Gecko expects microseconds.
+        event.mTime = s.timestamp / 1000;
+        return event;
+    }
+
+    public static GeckoEvent createLocationEvent(Location l) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LOCATION_EVENT);
+        event.mLocation = l;
+        return event;
+    }
+
+    public static GeckoEvent createViewportEvent(ImmutableViewportMetrics metrics, DisplayPortMetrics displayPort) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.VIEWPORT);
+        event.mCharacters = "Viewport:Change";
+        StringBuilder sb = new StringBuilder(256);
+        sb.append("{ \"x\" : ").append(metrics.viewportRectLeft)
+          .append(", \"y\" : ").append(metrics.viewportRectTop)
+          .append(", \"zoom\" : ").append(metrics.zoomFactor)
+          .append(", \"displayPort\" :").append(displayPort.toJSON())
+          .append('}');
+        event.mCharactersExtra = sb.toString();
+        return event;
+    }
+
+    public static GeckoEvent createURILoadEvent(String uri) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LOAD_URI);
+        event.mCharacters = uri;
+        event.mCharactersExtra = "";
+        return event;
+    }
+
+    public static GeckoEvent createBookmarkLoadEvent(String uri) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LOAD_URI);
+        event.mCharacters = uri;
+        event.mCharactersExtra = "-bookmark";
+        return event;
+    }
+
+    public static GeckoEvent createVisitedEvent(String data) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.VISITED);
+        event.mCharacters = data;
+        return event;
+    }
+
+    public static GeckoEvent createNetworkEvent(int connectionType, boolean isWifi, int DHCPGateway, String status) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.NETWORK_CHANGED);
+        event.mConnectionType = connectionType;
+        event.mIsWifi = isWifi;
+        event.mDHCPGateway = DHCPGateway;
+        event.mCharacters = status;
+        return event;
+    }
+
+    public static GeckoEvent createThumbnailEvent(int tabId, int bufw, int bufh, ByteBuffer buffer) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.THUMBNAIL);
+        event.mPoints = new Point[1];
+        event.mPoints[0] = new Point(bufw, bufh);
+        event.mMetaState = tabId;
+        event.mBuffer = buffer;
+        return event;
+    }
+
+    public static GeckoEvent createZoomedViewEvent(int tabId, int x, int y, int bufw, int bufh, float scaleFactor, ByteBuffer buffer) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.ZOOMEDVIEW);
+        event.mPoints = new Point[2];
+        event.mPoints[0] = new Point(x, y);
+        event.mPoints[1] = new Point(bufw, bufh);
+        event.mX = (double) scaleFactor;
+        event.mMetaState = tabId;
+        event.mBuffer = buffer;
+        return event;
+    }
+
+    public static GeckoEvent createScreenOrientationEvent(short aScreenOrientation, short aScreenAngle) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.SCREENORIENTATION_CHANGED);
+        event.mScreenOrientation = aScreenOrientation;
+        event.mScreenAngle = aScreenAngle;
+        return event;
+    }
+
+    public static GeckoEvent createCallObserverEvent(String observerKey, String topic, String data) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.CALL_OBSERVER);
+        event.mCharacters = observerKey;
+        event.mCharactersExtra = topic;
+        event.mData = data;
+        return event;
+    }
+
+    public static GeckoEvent createRemoveObserverEvent(String observerKey) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.REMOVE_OBSERVER);
+        event.mCharacters = observerKey;
+        return event;
+    }
+
+    public static GeckoEvent createLowMemoryEvent(int level) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LOW_MEMORY);
+        event.mMetaState = level;
+        return event;
+    }
+
+    public static GeckoEvent createNetworkLinkChangeEvent(String status) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.NETWORK_LINK_CHANGE);
+        event.mCharacters = status;
+        return event;
+    }
+
+    public static GeckoEvent createTelemetryHistogramAddEvent(String histogram,
+                                                              int value) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_HISTOGRAM_ADD);
+        event.mCharacters = histogram;
+        // Set the extras with null so that it cannot be mistaken with a keyed histogram.
+        event.mCharactersExtra = null;
+        event.mCount = value;
+        return event;
+    }
+
+    public static GeckoEvent createTelemetryKeyedHistogramAddEvent(String histogram,
+                                                                   String key,
+                                                                   int value) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_HISTOGRAM_ADD);
+        event.mCharacters = histogram;
+        event.mCharactersExtra = key;
+        event.mCount = value;
+        return event;
+    }
+
+    public static GeckoEvent createTelemetryUISessionStartEvent(String session, long timestamp) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_UI_SESSION_START);
+        event.mCharacters = session;
+        event.mTime = timestamp;
+        return event;
+    }
+
+    public static GeckoEvent createTelemetryUISessionStopEvent(String session, String reason, long timestamp) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_UI_SESSION_STOP);
+        event.mCharacters = session;
+        event.mCharactersExtra = reason;
+        event.mTime = timestamp;
+        return event;
+    }
+
+    public static GeckoEvent createTelemetryUIEvent(String action, String method, long timestamp, String extras) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_UI_EVENT);
+        event.mData = action;
+        event.mCharacters = method;
+        event.mCharactersExtra = extras;
+        event.mTime = timestamp;
+        return event;
+    }
+
+    public static GeckoEvent createGamepadAddRemoveEvent(int id, boolean added) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.GAMEPAD_ADDREMOVE);
+        event.mID = id;
+        event.mAction = added ? ACTION_GAMEPAD_ADDED : ACTION_GAMEPAD_REMOVED;
+        return event;
+    }
+
+    private static int boolArrayToBitfield(boolean[] array) {
+        int bits = 0;
+        for (int i = 0; i < array.length; i++) {
+            if (array[i]) {
+                bits |= 1 << i;
+            }
+        }
+        return bits;
+    }
+
+    public static GeckoEvent createGamepadButtonEvent(int id,
+                                                      int which,
+                                                      boolean pressed,
+                                                      float value) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.GAMEPAD_DATA);
+        event.mID = id;
+        event.mAction = ACTION_GAMEPAD_BUTTON;
+        event.mGamepadButton = which;
+        event.mGamepadButtonPressed = pressed;
+        event.mGamepadButtonValue = value;
+        return event;
+    }
+
+    public static GeckoEvent createGamepadAxisEvent(int id, boolean[] valid,
+                                                    float[] values) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.GAMEPAD_DATA);
+        event.mID = id;
+        event.mAction = ACTION_GAMEPAD_AXES;
+        event.mFlags = boolArrayToBitfield(valid);
+        event.mCount = values.length;
+        event.mGamepadValues = values;
+        return event;
+    }
+}
new file mode 100644
--- /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;
+};
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
@@ -0,0 +1,992 @@
+/* -*- 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.util.Clipboard;
+import org.mozilla.gecko.util.GamepadUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.media.AudioManager;
+import android.os.Bundle;
+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.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 String mCurrentInputMethod = "";
+
+    private final View mView;
+    private final GeckoEditableClient mEditableClient;
+    protected int mBatchEditCount;
+    private ExtractedTextRequest mUpdateRequest;
+    private final ExtractedText mUpdateExtract = new ExtractedText();
+    private boolean mBatchSelectionChanged;
+    private boolean mBatchTextChanged;
+    private final InputConnection mKeyInputConnection;
+
+    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++;
+        mEditableClient.setBatchMode(true);
+        return true;
+    }
+
+    @Override
+    public synchronized boolean endBatchEdit() {
+        if (mBatchEditCount > 0) {
+            mBatchEditCount--;
+            if (mBatchEditCount == 0) {
+                if (mBatchTextChanged) {
+                    notifyTextChange();
+                    mBatchTextChanged = false;
+                }
+                if (mBatchSelectionChanged) {
+                    Editable editable = getEditable();
+                    notifySelectionChange(Selection.getSelectionStart(editable),
+                                           Selection.getSelectionEnd(editable));
+                    mBatchSelectionChanged = false;
+                }
+                mEditableClient.setBatchMode(false);
+            }
+        } else {
+            Log.w(LOGTAG, "endBatchEdit() called, but mBatchEditCount == 0?!");
+        }
+        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 boolean performPrivateCommand(final String action, final Bundle data) {
+        switch (action) {
+            case "process-gecko-events":
+                // Process all currently pending Gecko thread events before returning.
+
+                final Editable editable = getEditable();
+                if (editable == null) {
+                    return false;
+                }
+
+                // Removing an invalid span is essentially a no-op, but it does force the
+                // current thread to wait for the Gecko thread when we call length(), in order
+                // to process the removeSpan event. Once Gecko thread processes the removeSpan
+                // event, all previous events in the Gecko event queue would have been
+                // processed as well.
+                editable.removeSpan(null);
+                editable.length();
+                return true;
+        }
+        return false;
+    }
+
+    @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() {
+        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();
+                }
+                imm.showSoftInput(v, 0);
+            }
+        });
+    }
+
+    private void hideSoftInput() {
+        final InputMethodManager imm = getInputMethodManager();
+        if (imm != null) {
+            final View v = getView();
+            imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
+        }
+    }
+
+    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;
+        }
+        mBatchSelectionChanged = false;
+        mBatchTextChanged = false;
+
+        // Do not reset mIMEState here; see comments in notifyIMEContext
+    }
+
+    @Override
+    public void onTextChange(CharSequence text, int start, int oldEnd, int newEnd) {
+
+        if (mUpdateRequest == null) {
+            // Android always expects selection updates when not in extracted mode;
+            // in extracted mode, the selection is reported through updateExtractedText
+            final Editable editable = getEditable();
+            if (editable != null) {
+                onSelectionChange(Selection.getSelectionStart(editable),
+                                  Selection.getSelectionEnd(editable));
+            }
+            return;
+        }
+
+        if (mBatchEditCount > 0) {
+            // Delay notification until after the batch edit
+            mBatchTextChanged = true;
+            return;
+        }
+        notifyTextChange();
+    }
+
+    private void notifyTextChange() {
+
+        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
+    public void onSelectionChange(int start, int end) {
+
+        if (mBatchEditCount > 0) {
+            // Delay notification until after the batch edit
+            mBatchSelectionChanged = true;
+            return;
+        }
+
+        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 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
+    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 the user has changed IMEs, then notify input method observers.
+        if (!mCurrentInputMethod.equals(prevInputMethod) && GeckoAppShell.getGeckoInterface() != null) {
+            GeckoAppShell.getGeckoInterface().onInputMethodChanged(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:
+            case NOTIFY_IME_OF_BLUR:
+                // Showing/hiding vkb is done in notifyIMEContext
+                resetInputConnection();
+                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") ||
+             (Versions.feature11Plus && (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;
+        }
+        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;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoJavaSampler.java
@@ -0,0 +1,211 @@
+/* -*- 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.SystemClock;
+import android.util.Log;
+import android.util.SparseArray;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+import java.lang.Thread;
+import java.util.Set;
+
+public class GeckoJavaSampler {
+    private static final String LOGTAG = "JavaSampler";
+    private static Thread sSamplingThread;
+    private static SamplingThread sSamplingRunnable;
+    private static Thread sMainThread;
+
+    // Use the same timer primitive as the profiler
+    // to get a perfect sample syncing.
+    @WrapForJNI
+    private static native double getProfilerTime();
+
+    private static class Sample {
+        public Frame[] mFrames;
+        public double mTime;
+        public long mJavaTime; // non-zero if Android system time is used
+        public Sample(StackTraceElement[] aStack) {
+            mFrames = new Frame[aStack.length];
+            if (GeckoThread.isStateAtLeast(GeckoThread.State.LIBS_READY)) {
+                mTime = getProfilerTime();
+            }
+            if (mTime == 0.0d) {
+                // getProfilerTime is not available yet; either libs are not loaded,
+                // or profiling hasn't started on the Gecko side yet
+                mJavaTime = SystemClock.elapsedRealtime();
+            }
+            for (int i = 0; i < aStack.length; i++) {
+                mFrames[aStack.length - 1 - i] = new Frame();
+                mFrames[aStack.length - 1 - i].fileName = aStack[i].getFileName();
+                mFrames[aStack.length - 1 - i].lineNo = aStack[i].getLineNumber();
+                mFrames[aStack.length - 1 - i].methodName = aStack[i].getMethodName();
+                mFrames[aStack.length - 1 - i].className = aStack[i].getClassName();
+            }
+        }
+    }
+    private static class Frame {
+        public String fileName;
+        public int lineNo;
+        public String methodName;
+        public String className;
+    }
+
+    private static class SamplingThread implements Runnable {
+        private final int mInterval;
+        private final int mSampleCount;
+
+        private boolean mPauseSampler;
+        private boolean mStopSampler;
+
+        private final SparseArray<Sample[]> mSamples = new SparseArray<Sample[]>();
+        private int mSamplePos;
+
+        public SamplingThread(final int aInterval, final int aSampleCount) {
+            // If we sample faster then 10ms we get to many missed samples
+            mInterval = Math.max(10, aInterval);
+            mSampleCount = aSampleCount;
+        }
+
+        @Override
+        public void run() {
+            synchronized (GeckoJavaSampler.class) {
+                mSamples.put(0, new Sample[mSampleCount]);
+                mSamplePos = 0;
+
+                // Find the main thread
+                Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
+                for (Thread t : threadSet) {
+                    if (t.getName().compareToIgnoreCase("main") == 0) {
+                        sMainThread = t;
+                        break;
+                    }
+                }
+
+                if (sMainThread == null) {
+                    Log.e(LOGTAG, "Main thread not found");
+                    return;
+                }
+            }
+
+            while (true) {
+                try {
+                    Thread.sleep(mInterval);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                synchronized (GeckoJavaSampler.class) {
+                    if (!mPauseSampler) {
+                        StackTraceElement[] bt = sMainThread.getStackTrace();
+                        mSamples.get(0)[mSamplePos] = new Sample(bt);
+                        mSamplePos = (mSamplePos + 1) % mSamples.get(0).length;
+                    }
+                    if (mStopSampler) {
+                        break;
+                    }
+                }
+            }
+        }
+
+        private Sample getSample(int aThreadId, int aSampleId) {
+            if (aThreadId < mSamples.size() && aSampleId < mSamples.get(aThreadId).length &&
+                mSamples.get(aThreadId)[aSampleId] != null) {
+                int startPos = 0;
+                if (mSamples.get(aThreadId)[mSamplePos] != null) {
+                    startPos = mSamplePos;
+                }
+                int readPos = (startPos + aSampleId) % mSamples.get(aThreadId).length;
+                return mSamples.get(aThreadId)[readPos];
+            }
+            return null;
+        }
+    }
+
+
+    @WrapForJNI(allowMultithread = true, stubName = "GetThreadNameJavaProfilingWrapper")
+    public synchronized static String getThreadName(int aThreadId) {
+        if (aThreadId == 0 && sMainThread != null) {
+            return sMainThread.getName();
+        }
+        return null;
+    }
+
+    private synchronized static Sample getSample(int aThreadId, int aSampleId) {
+        return sSamplingRunnable.getSample(aThreadId, aSampleId);
+    }
+
+    @WrapForJNI(allowMultithread = true, stubName = "GetSampleTimeJavaProfiling")
+    public synchronized static double getSampleTime(int aThreadId, int aSampleId) {
+        Sample sample = getSample(aThreadId, aSampleId);
+        if (sample != null) {
+            if (sample.mJavaTime != 0) {
+                return (sample.mJavaTime -
+                    SystemClock.elapsedRealtime()) + getProfilerTime();
+            }
+            System.out.println("Sample: " + sample.mTime);
+            return sample.mTime;
+        }
+        return 0;
+    }
+
+    @WrapForJNI(allowMultithread = true, stubName = "GetFrameNameJavaProfilingWrapper")
+    public synchronized static String getFrameName(int aThreadId, int aSampleId, int aFrameId) {
+        Sample sample = getSample(aThreadId, aSampleId);
+        if (sample != null && aFrameId < sample.mFrames.length) {
+            Frame frame = sample.mFrames[aFrameId];
+            if (frame == null) {
+                return null;
+            }
+            return frame.className + "." + frame.methodName + "()";
+        }
+        return null;
+    }
+
+    @WrapForJNI(allowMultithread = true, stubName = "StartJavaProfiling")
+    public static void start(int aInterval, int aSamples) {
+        synchronized (GeckoJavaSampler.class) {
+            if (sSamplingRunnable != null) {
+                return;
+            }
+            sSamplingRunnable = new SamplingThread(aInterval, aSamples);
+            sSamplingThread = new Thread(sSamplingRunnable, "Java Sampler");
+            sSamplingThread.start();
+        }
+    }
+
+    @WrapForJNI(allowMultithread = true, stubName = "PauseJavaProfiling")
+    public static void pause() {
+        synchronized (GeckoJavaSampler.class) {
+            sSamplingRunnable.mPauseSampler = true;
+        }
+    }
+
+    @WrapForJNI(allowMultithread = true, stubName = "UnpauseJavaProfiling")
+    public static void unpause() {
+        synchronized (GeckoJavaSampler.class) {
+            sSamplingRunnable.mPauseSampler = false;
+        }
+    }
+
+    @WrapForJNI(allowMultithread = true, stubName = "StopJavaProfiling")
+    public static void stop() {
+        synchronized (GeckoJavaSampler.class) {
+            if (sSamplingThread == null) {
+                return;
+            }
+
+            sSamplingRunnable.mStopSampler = true;
+            try {
+                sSamplingThread.join();
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            sSamplingThread = null;
+            sSamplingRunnable = null;
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java
@@ -0,0 +1,436 @@
+/* -*- 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.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;
+
+    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 NetworkStatus currentNetworkStatus = NetworkStatus.UNKNOWN;
+    private ConnectionSubType currentConnectionSubtype = ConnectionSubType.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() {
+        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;
+        }
+
+        performActionsForStateEvent(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(ManagerState currentState, 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();
+                    registerBroadcastReceiver();
+                }
+                if (event == ManagerEvent.enableNotifications) {
+                    updateNetworkStateAndConnectionType();
+                }
+                break;
+            case OnNoListeners:
+                if (event == ManagerEvent.receivedUpdate) {
+                    updateNetworkStateAndConnectionType();
+                    sendNetworkStateToListeners();
+                }
+                if (event == ManagerEvent.enableNotifications) {
+                    updateNetworkStateAndConnectionType();
+                    registerBroadcastReceiver();
+                }
+                if (event == ManagerEvent.stop) {
+                    unregisterBroadcastReceiver();
+                }
+                break;
+            case OnWithListeners:
+                if (event == ManagerEvent.receivedUpdate) {
+                    updateNetworkStateAndConnectionType();
+                    sendNetworkStateToListeners();
+                }
+                if (event == ManagerEvent.stop) {
+                    unregisterBroadcastReceiver();
+                }
+                /* no-op event: ManagerEvent.disableNotifications */
+                break;
+            case OffWithListeners:
+                if (event == ManagerEvent.start) {
+                    registerBroadcastReceiver();
+                }
+                /* 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 applicationContext = GeckoAppShell.getApplicationContext();
+        final ConnectivityManager connectivityManager = (ConnectivityManager) applicationContext.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);
+    }
+
+    /**
+     * Send current network state and connection type as a GeckoEvent, to whomever is listening.
+     */
+    private void sendNetworkStateToListeners() {
+        if (GeckoThread.isRunning()) {
+            final Context applicationContext = GeckoAppShell.getApplicationContext();
+            GeckoAppShell.sendEventToGecko(
+                    GeckoEvent.createNetworkEvent(
+                            currentConnectionType.value,
+                            currentConnectionType == ConnectionType.WIFI,
+                            wifiDhcpGatewayAddress(applicationContext),
+                            currentConnectionSubtype.value
+                    )
+            );
+
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createNetworkLinkChangeEvent(currentNetworkStatus.value));
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createNetworkLinkChangeEvent(LINK_DATA_CHANGED));
+        }
+    }
+
+    /**
+     * Stop listening for network state updates.
+     */
+    private void unregisterBroadcastReceiver() {
+        GeckoAppShell.getApplicationContext().unregisterReceiver(this);
+    }
+
+    /**
+     * Start listening for network state updates.
+     */
+    private void registerBroadcastReceiver() {
+        final IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
+        if (GeckoAppShell.getApplicationContext().registerReceiver(this, filter) == null) {
+            Log.e(LOGTAG, "Registering receiver failed");
+        }
+    }
+
+    private static int wifiDhcpGatewayAddress(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
+    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.
+     */
+    @JNITarget
+    public static int getMCC() {
+        return getNetworkOperator(InfoType.MCC, GeckoAppShell.getApplicationContext());
+    }
+
+    @JNITarget
+    public static int getMNC() {
+        return getNetworkOperator(InfoType.MNC, GeckoAppShell.getApplicationContext());
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfile.java
@@ -0,0 +1,943 @@
+/* -*- 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.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_BUILD 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";
+
+    public static final String PREF_FIRSTRUN_ENABLED = "startpane_enabled";
+
+    // Session store
+    private static final String SESSION_FILE = "sessionstore.js";
+    private static final String SESSION_FILE_BACKUP = "sessionstore.bak";
+    private static final long MAX_BACKUP_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;
+
+    /**
+     * 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 GeckoProfile initFromArgs(final Context context, final String args) {
+        if (shouldUse(context)) {
+            final GeckoProfile guestProfile = getGuestProfile(context);
+            if (guestProfile != null) {
+                return guestProfile;
+            }
+            // Failed to create guest profile; leave guest mode.
+            leave(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_BUILD) {
+                    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.");
+        }
+    }
+
+    public static boolean shouldUse(final Context context) {
+        return GeckoSharedPrefs.forApp(context).getBoolean(GUEST_MODE_PREF, false);
+    }
+
+    public static void enter(final Context context) {
+        GeckoSharedPrefs.forApp(context).edit().putBoolean(GUEST_MODE_PREF, true).commit();
+    }
+
+    public static void leave(final Context context) {
+        GeckoSharedPrefs.forApp(context).edit().putBoolean(GUEST_MODE_PREF, false).commit();
+    }
+
+    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;
+    }
+
+    /**
+     * Retrieves the directory backing the profile. This method acts
+     * as a lazy initializer for the GeckoProfile instance.
+     */
+    @RobocopTarget
+    public synchronized File getDir() {
+        forceCreate();
+        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 synchronized GeckoProfile forceCreate() {
+        if (mProfileDir != null) {
+            return this;
+        }
+
+        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);
+        }
+        return this;
+    }
+
+    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://mxr.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.bak 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.bak, so we can display the correct "tabs from last time".
+     * If we *are* restoring tabs, we need to delete outdated copies of sessionstore.bak,
+     * 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 sessionFileBackup = getFile(SESSION_FILE_BACKUP);
+        if (!shouldRestore) {
+            File sessionFile = getFile(SESSION_FILE);
+            if (sessionFile != null && sessionFile.exists()) {
+                sessionFile.renameTo(sessionFileBackup);
+            }
+        } else {
+            if (sessionFileBackup != null && sessionFileBackup.exists() &&
+                    System.currentTimeMillis() - sessionFileBackup.lastModified() > MAX_BACKUP_FILE_AGE) {
+                sessionFileBackup.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 the previous session.
+     *
+     * @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) {
+        File sessionFile = getFile(readBackup ? SESSION_FILE_BACKUP : SESSION_FILE);
+
+        try {
+            if (sessionFile != null && sessionFile.exists()) {
+                return readFile(sessionFile);
+            }
+        } catch (IOException ioe) {
+            Log.e(LOGTAG, "Unable to read session file", ioe);
+        }
+        return null;
+    }
+
+    /**
+     * 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 = GeckoProfileDirectories.saltProfileName(mName);
+        File profileDir = new File(mMozillaDir, saltedName);
+        while (profileDir.exists()) {
+            saltedName = GeckoProfileDirectories.saltProfileName(mName);
+            profileDir = new File(mMozillaDir, saltedName);
+        }
+
+        // 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);
+
+            // We have no intention of stopping this session. The FIRSTRUN session
+            // ends when the browsing session/activity has ended. All events
+            // during firstrun will be tagged as FIRSTRUN.
+            // TODO: local broadcast to tell App that things are happening?
+            // Telemetry.startUISession(TelemetryContract.Session.FIRSTRUN);
+        }
+
+        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());
+
+        // Initialize pref flag for displaying the start pane for a new profile.
+        final SharedPreferences prefs = GeckoSharedPrefs.forProfile(mApplicationContext);
+        prefs.edit().putBoolean(PREF_FIRSTRUN_ENABLED, true).apply();
+
+        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) {
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfileDirectories.java
@@ -0,0 +1,228 @@
+/* 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));
+
+      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);
+
+        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);
+
+        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);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenOrientation.java
@@ -0,0 +1,414 @@
+/* -*- 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.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()));
+    }
+
+    /*
+     * 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;
+            }
+            GeckoAppShell.sendEventToGecko(
+                GeckoEvent.createScreenOrientationEvent(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;
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoService.java
@@ -0,0 +1,210 @@
+/* -*- 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.AlarmManager;
+import android.app.Service;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+
+import java.io.File;
+
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.EventCallback;
+
+public class GeckoService extends Service {
+
+    private static final String LOGTAG = "GeckoService";
+    private static final boolean DEBUG = false;
+
+    private static final String INTENT_PROFILE_NAME = "org.mozilla.gecko.intent.PROFILE_NAME";
+    private static final String INTENT_PROFILE_DIR = "org.mozilla.gecko.intent.PROFILE_DIR";
+
+    private static final String INTENT_ACTION_UPDATE_ADDONS = "update-addons";
+    private static final String INTENT_ACTION_CREATE_SERVICES = "create-services";
+
+    private static final String INTENT_SERVICE_CATEGORY = "category";
+    private static final String INTENT_SERVICE_DATA = "data";
+
+    private static class EventListener implements NativeEventListener {
+        @Override // NativeEventListener
+        public void handleMessage(final String event,
+                                  final NativeJSObject message,
+                                  final EventCallback callback) {
+            final Context context = GeckoAppShell.getApplicationContext();
+            switch (event) {
+            case "Gecko:ScheduleRun":
+                if (DEBUG) {
+                    Log.d(LOGTAG, "Scheduling " + message.getString("action") +
+                                  " @ " + message.getInt("interval") + "ms");
+                }
+
+                final Intent intent = getIntentForAction(context, message.getString("action"));
+                final PendingIntent pendingIntent = PendingIntent.getService(
+                        context, /* requestCode */ 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+
+                final AlarmManager am = (AlarmManager)
+                    context.getSystemService(Context.ALARM_SERVICE);
+                // Cancel any previous alarm and schedule a new one.
+                am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
+                                       message.getInt("trigger"),
+                                       message.getInt("interval"),
+                                       pendingIntent);
+                break;
+
+            default:
+                throw new UnsupportedOperationException(event);
+            }
+        }
+    }
+
+    private static final EventListener EVENT_LISTENER = new EventListener();
+
+    public static void register() {
+        if (DEBUG) {
+            Log.d(LOGTAG, "Registered listener");
+        }
+        EventDispatcher.getInstance().registerGeckoThreadListener(EVENT_LISTENER,
+                "Gecko:ScheduleRun");
+    }
+
+    public static void unregister() {
+        if (DEBUG) {
+            Log.d(LOGTAG, "Unregistered listener");
+        }
+        EventDispatcher.getInstance().unregisterGeckoThreadListener(EVENT_LISTENER,
+                "Gecko:ScheduleRun");
+    }
+
+    @Override // Service
+    public void onCreate() {
+        GeckoAppShell.ensureCrashHandling();
+        GeckoAppShell.setApplicationContext(getApplicationContext());
+        GeckoAppShell.setNotificationClient(new ServiceNotificationClient(getApplicationContext()));
+        GeckoThread.onResume();
+        super.onCreate();
+
+        if (DEBUG) {
+            Log.d(LOGTAG, "Created");
+        }
+    }
+
+    @Override // Service
+    public void onDestroy() {
+        GeckoThread.onPause();
+
+        // We want to block here if we can, so we don't get killed when Gecko is in the
+        // middle of handling onPause().
+        if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+            GeckoThread.waitOnGecko();
+        }
+
+        if (DEBUG) {
+            Log.d(LOGTAG, "Destroyed");
+        }
+        super.onDestroy();
+    }
+
+    private static Intent getIntentForAction(final Context context, final String action) {
+        final Intent intent = new Intent(action, /* uri */ null, context, GeckoService.class);
+        final GeckoProfile profile = GeckoThread.getActiveProfile();
+        if (profile != null) {
+            setIntentProfile(intent, profile.getName(), profile.getDir().getAbsolutePath());
+        }
+        return intent;
+    }
+
+    public static Intent getIntentToCreateServices(final Context context, final String category, final String data) {
+        final Intent intent = getIntentForAction(context, INTENT_ACTION_CREATE_SERVICES);
+        intent.putExtra(INTENT_SERVICE_CATEGORY, category);
+        intent.putExtra(INTENT_SERVICE_DATA, data);
+        return intent;
+    }
+
+    public static Intent getIntentToCreateServices(final Context context, final String category) {
+        return getIntentToCreateServices(context, category, /* data */ null);
+    }
+
+    public static void setIntentProfile(final Intent intent, final String profileName,
+                                        final String profileDir) {
+        intent.putExtra(INTENT_PROFILE_NAME, profileName);
+        intent.putExtra(INTENT_PROFILE_DIR, profileDir);
+    }
+
+    private int handleIntent(final Intent intent, final int startId) {
+        if (DEBUG) {
+            Log.d(LOGTAG, "Handling " + intent.getAction());
+        }
+
+        final String profileName = intent.getStringExtra(INTENT_PROFILE_NAME);
+        final String profileDir = intent.getStringExtra(INTENT_PROFILE_DIR);
+
+        if (profileName == null || profileDir == null) {
+            throw new IllegalArgumentException("Intent must specify profile.");
+        }
+
+        if (!GeckoThread.initWithProfile(profileName != null ? profileName : "",
+                                         new File(profileDir))) {
+            Log.w(LOGTAG, "Ignoring due to profile mismatch: " +
+                          profileName + " [" + profileDir + ']');
+
+            final GeckoProfile profile = GeckoThread.getActiveProfile();
+            if (profile != null) {
+                Log.w(LOGTAG, "Current profile is " + profile.getName() +
+                              " [" + profile.getDir().getAbsolutePath() + ']');
+            }
+            stopSelf(startId);
+            return Service.START_NOT_STICKY;
+        }
+
+        GeckoThread.launch();
+
+        switch (intent.getAction()) {
+        case INTENT_ACTION_UPDATE_ADDONS:
+            // Run the add-on update service. Because the service is automatically invoked
+            // when loading Gecko, we don't have to do anything else here.
+            break;
+
+        case INTENT_ACTION_CREATE_SERVICES:
+            final String category = intent.getStringExtra(INTENT_SERVICE_CATEGORY);
+            final String data = intent.getStringExtra(INTENT_SERVICE_DATA);
+
+            if (category == null) {
+                break;
+            }
+            GeckoThread.createServices(category, data);
+            break;
+
+        default:
+            Log.w(LOGTAG, "Unknown request: " + intent);
+        }
+
+        stopSelf(startId);
+        return Service.START_NOT_STICKY;
+    }
+
+    @Override // Service
+    public int onStartCommand(final Intent intent, final int flags, final int startId) {
+        if (intent == null) {
+            return Service.START_NOT_STICKY;
+        }
+        try {
+            return handleIntent(intent, startId);
+        } catch (final Throwable e) {
+            Log.e(LOGTAG, "Cannot handle intent: " + intent, e);
+            return Service.START_NOT_STICKY;
+        }
+    }
+
+    @Override // Service
+    public IBinder onBind(final Intent intent) {
+        return null;
+    }
+}
new file mode 100644
--- /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
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSmsManager.java
@@ -0,0 +1,1244 @@
+/* -*- 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.Activity;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.provider.Telephony;
+import android.telephony.SmsManager;
+import android.telephony.SmsMessage;
+import android.util.Log;
+
+import static android.telephony.SmsMessage.MessageClass;
+import static org.mozilla.gecko.SmsManager.ISmsManager;
+
+import java.util.ArrayList;
+import java.util.Formatter;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * The envelope class contains all information that are needed to keep track of
+ * a sent SMS.
+ */
+class Envelope
+{
+  enum SubParts {
+    SENT_PART,
+    DELIVERED_PART
+  }
+
+  protected int       mId;
+  protected int       mMessageId;
+  protected long      mMessageTimestamp;
+
+  /**
+   * Number of sent/delivered remaining parts.
+   * @note The array has much slots as SubParts items.
+   */
+  protected int[]     mRemainingParts;
+
+  /**
+   * Whether sending/delivering is currently failing.
+   * @note The array has much slots as SubParts items.
+   */
+  protected boolean[] mFailing;
+
+  /**
+   * Error type (only for sent).
+   */
+  protected int       mError;
+
+  public Envelope(int aId, int aParts) {
+    mId = aId;
+    mMessageId = -1;
+    mError = GeckoSmsManager.kNoError;
+
+    int size = SubParts.values().length;
+    mRemainingParts = new int[size];
+    mFailing = new boolean[size];
+
+    for (int i = 0; i < size; ++i) {
+      mRemainingParts[i] = aParts;
+      mFailing[i] = false;
+    }
+  }
+
+  public void decreaseRemainingParts(SubParts aType) {
+    --mRemainingParts[aType.ordinal()];
+
+    if (mRemainingParts[SubParts.SENT_PART.ordinal()] >
+        mRemainingParts[SubParts.DELIVERED_PART.ordinal()]) {
+      Log.e("GeckoSmsManager", "Delivered more parts than we sent!?");
+    }
+  }
+
+  public boolean arePartsRemaining(SubParts aType) {
+    return mRemainingParts[aType.ordinal()] != 0;
+  }
+
+  public void markAsFailed(SubParts aType) {
+    mFailing[aType.ordinal()] = true;
+  }
+
+  public boolean isFailing(SubParts aType) {
+    return mFailing[aType.ordinal()];
+  }
+
+  public int getMessageId() {
+    return mMessageId;
+  }
+
+  public void setMessageId(int aMessageId) {
+    mMessageId = aMessageId;
+  }
+
+  public long getMessageTimestamp() {
+    return mMessageTimestamp;
+  }
+
+  public void setMessageTimestamp(long aMessageTimestamp) {
+    mMessageTimestamp = aMessageTimestamp;
+  }
+
+  public int getError() {
+    return mError;
+  }
+
+  public void setError(int aError) {
+    mError = aError;
+  }
+}
+
+/**
+ * Postman class is a singleton that manages Envelope instances.
+ */
+class Postman
+{
+  public static final int kUnknownEnvelopeId = -1;
+
+  private static final Postman sInstance = new Postman();
+
+  private final ArrayList<Envelope> mEnvelopes = new ArrayList<>(1);
+
+  private Postman() {}
+
+  public static Postman getInstance() {
+    return sInstance;
+  }
+
+  public int createEnvelope(int aParts) {
+    /*
+     * We are going to create the envelope in the first empty slot in the array
+     * list. If there is no empty slot, we create a new one.
+     */
+    int size = mEnvelopes.size();
+
+    for (int i = 0; i < size; ++i) {
+      if (mEnvelopes.get(i) == null) {
+        mEnvelopes.set(i, new Envelope(i, aParts));
+        return i;
+      }
+    }
+
+    mEnvelopes.add(new Envelope(size, aParts));
+    return size;
+  }
+
+  public Envelope getEnvelope(int aId) {
+    if (aId < 0 || mEnvelopes.size() <= aId) {
+      Log.e("GeckoSmsManager", "Trying to get an unknown Envelope!");
+      return null;
+    }
+
+    Envelope envelope = mEnvelopes.get(aId);
+    if (envelope == null) {
+      Log.e("GeckoSmsManager", "Trying to get an empty Envelope!");
+    }
+
+    return envelope;
+  }
+
+  public void destroyEnvelope(int aId) {
+    if (aId < 0 || mEnvelopes.size() <= aId) {
+      Log.e("GeckoSmsManager", "Trying to destroy an unknown Envelope!");
+      return;
+    }
+
+    if (mEnvelopes.set(aId, null) == null) {
+      Log.e("GeckoSmsManager", "Trying to destroy an empty Envelope!");
+    }
+  }
+}
+
+class SmsIOThread extends HandlerThread {
+  private final static SmsIOThread sInstance = new SmsIOThread();
+
+  private Handler mHandler;
+
+  public static SmsIOThread getInstance() {
+    return sInstance;
+  }
+
+  SmsIOThread() {
+    super("SmsIOThread");
+  }
+
+  @Override
+  public void start() {
+    super.start();
+    mHandler = new Handler(getLooper());
+  }
+
+  public boolean execute(Runnable r) {
+    return mHandler.post(r);
+  }
+}
+
+class MessagesListManager
+{
+  private static final MessagesListManager sInstance = new MessagesListManager();
+
+  public static MessagesListManager getInstance() {
+    return sInstance;
+  }
+
+  private final HashMap<Integer, Cursor> mCursors = new HashMap<>();
+
+  public void add(int id, Cursor aCursor) {
+    if (mCursors.containsKey(id)) {
+      Log.e("GeckoSmsManager", "Trying to overwrite cursor!");
+      return;
+    }
+
+    mCursors.put(id, aCursor);
+  }
+
+  public Cursor get(int aId) {
+    if (!mCursors.containsKey(aId)) {
+      Log.e("GeckoSmsManager", "Cursor doesn't exist!");
+      return null;
+    }
+
+    return mCursors.get(aId);
+  }
+
+  public void remove(int aId) {
+    if (!mCursors.containsKey(aId)) {
+      Log.e("GeckoSmsManager", "Cursor doesn't exist!");
+      return;
+    }
+
+    mCursors.remove(aId);
+  }
+
+  public void clear() {
+    Set<Map.Entry<Integer, Cursor>> entries = mCursors.entrySet();
+    Iterator<Map.Entry<Integer, Cursor>> it = entries.iterator();
+    while (it.hasNext()) {
+      it.next().getValue().close();
+    }
+
+    mCursors.clear();
+  }
+}
+
+public class GeckoSmsManager
+  extends BroadcastReceiver
+  implements ISmsManager
+{
+  public final static String ACTION_SMS_SENT      = "org.mozilla.gecko.SMS_SENT";
+  public final static String ACTION_SMS_DELIVERED = "org.mozilla.gecko.SMS_DELIVERED";
+
+  /*
+   * Make sure that the following error codes are in sync with |ErrorType| in:
+   * dom/mobilemessage/interfaces/nsIMobileMessageCallback.idl
+   * The error codes are owned by the DOM.
+   */
+  public final static int kNoError               = 0;
+  public final static int kNoSignalError         = 1;
+  public final static int kNotFoundError         = 2;
+  public final static int kUnknownError          = 3;
+  public final static int kInternalError         = 4;
+  public final static int kNoSimCardError        = 5;
+  public final static int kRadioDisabledError    = 6;
+  public final static int kInvalidAddressError   = 7;
+  public final static int kFdnCheckError         = 8;
+  public final static int kNonActiveSimCardError = 9;
+  public final static int kStorageFullError      = 10;
+  public final static int kSimNotMatchedError    = 11;
+  public final static int kNetworkProblemsError = 12;
+  public final static int kGeneralProblemsError = 13;
+  public final static int kServiceNotAvailableError      = 14;
+  public final static int kMessageTooLongForNetworkError = 15;
+  public final static int kServiceNotSupportedError      = 16;
+  public final static int kRetryRequiredError   = 17;
+
+  private final static int kMaxMessageSize    = 160;
+
+  private final static Uri kSmsContentUri        = Telephony.Sms.Inbox.CONTENT_URI;
+  private final static Uri kSmsSentContentUri    = Telephony.Sms.Sent.CONTENT_URI;
+  private final static Uri kSmsThreadsContentUri = Telephony.Sms.Conversations.CONTENT_URI;
+
+  private final static int kSmsTypeInbox      = Telephony.Sms.MESSAGE_TYPE_INBOX;
+  private final static int kSmsTypeSentbox    = Telephony.Sms.MESSAGE_TYPE_SENT;
+
+  /*
+   * Keep the following state codes in syng with |DeliveryState| in:
+   * dom/mobilemessage/Types.h
+   */
+  private final static int kDeliveryStateSent          = 0;
+  private final static int kDeliveryStateReceived      = 1;
+  private final static int kDeliveryStateSending       = 2;
+  private final static int kDeliveryStateError         = 3;
+  private final static int kDeliveryStateUnknown       = 4;
+  private final static int kDeliveryStateNotDownloaded = 5;
+  private final static int kDeliveryStateEndGuard      = 6;
+
+  /*
+   * Keep the following status codes in sync with |DeliveryStatus| in:
+   * dom/mobilemessage/Types.h
+   */
+  private final static int kDeliveryStatusNotApplicable = 0;
+  private final static int kDeliveryStatusSuccess       = 1;
+  private final static int kDeliveryStatusPending       = 2;
+  private final static int kDeliveryStatusError         = 3;
+
+  /*
+   * Keep the following values in sync with |MessageClass| in:
+   * dom/mobilemessage/Types.h
+   */
+  private final static int kMessageClassNormal  = 0;
+  private final static int kMessageClassClass0  = 1;
+  private final static int kMessageClassClass1  = 2;
+  private final static int kMessageClassClass2  = 3;
+  private final static int kMessageClassClass3  = 4;
+
+  private final static String[] kRequiredMessageRows = { "_id", "address", "body", "date", "type", "status", "read", "thread_id" };
+  private final static String[] kRequiredMessageRowsForThread = { "_id", "address", "body", "read", "subject", "date" };
+  private final static String[] kThreadProjection = { "thread_id" };
+
+  // Used to generate monotonically increasing GUIDs.
+  private static final AtomicInteger pendingIntentGuid = new AtomicInteger(Integer.MIN_VALUE);
+
+  // The maximum value of a 32 bit signed integer. Used to enforce a limit on ids.
+  private static final long UNSIGNED_INTEGER_MAX_VALUE = Integer.MAX_VALUE * 2L + 1L;
+
+  public GeckoSmsManager() {
+    if (SmsIOThread.getInstance().getState() == Thread.State.NEW) {
+      SmsIOThread.getInstance().start();
+    }
+  }
+
+  private boolean isDefaultSmsApp(Context context) {
+    String myPackageName = context.getPackageName();
+    return Telephony.Sms.getDefaultSmsPackage(context).equals(myPackageName);
+  }
+
+  @Override
+  public void start() {
+    IntentFilter smsFilter = new IntentFilter();
+    smsFilter.addAction(Telephony.Sms.Intents.SMS_RECEIVED_ACTION);
+    smsFilter.addAction(ACTION_SMS_SENT);
+    smsFilter.addAction(ACTION_SMS_DELIVERED);
+
+    GeckoAppShell.getContext().registerReceiver(this, smsFilter);
+  }
+
+  /**
+   * Build up the SMS message body from the SmsMessage array of received SMS
+   *
+   * @param msgs The SmsMessage array of the received SMS
+   * @return The text message body
+   */
+  private static String buildMessageBodyFromPdus(SmsMessage[] msgs) {
+    if (msgs.length == 1) {
+      // There is only one part, so grab the body directly.
+      return replaceFormFeeds(msgs[0].getDisplayMessageBody());
+    } else {
+      // Build up the body from the parts.
+      StringBuilder body = new StringBuilder();
+      for (SmsMessage msg : msgs) {
+        // getDisplayMessageBody() can NPE if mWrappedMessage inside is null.
+        body.append(msg.getDisplayMessageBody());
+      }
+      return replaceFormFeeds(body.toString());
+    }
+  }
+
+  // Some providers send formfeeds in their messages. Convert those formfeeds to newlines.
+  private static String replaceFormFeeds(String s) {
+    return s == null ? "" : s.replace('\f', '\n');
+  }
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+    if (intent.getAction().equals(Telephony.Sms.Intents.SMS_DELIVER_ACTION) ||
+        intent.getAction().equals(Telephony.Sms.Intents.SMS_RECEIVED_ACTION)) {
+      // If we're the default SMS, ignore SMS_RECEIVED intents since we'll handle
+      // the SMS_DELIVER intent instead.
+      if (isDefaultSmsApp(GeckoAppShell.getContext()) && intent.getAction().equals(Telephony.Sms.Intents.SMS_RECEIVED_ACTION)) {
+        return;
+      }
+
+      // TODO: Try to find the receiver number to be able to populate
+      //       SmsMessage.receiver.
+      SmsMessage[] messages = Telephony.Sms.Intents.getMessagesFromIntent(intent);
+      if (messages == null || messages.length == 0) {
+        return;
+      }
+
+      SmsMessage sms = messages[0];
+      String body = buildMessageBodyFromPdus(messages);
+      long timestamp = System.currentTimeMillis();
+
+      int id = 0;
+      // We only need to save the message if we're the default SMS app
+      if (intent.getAction().equals(Telephony.Sms.Intents.SMS_DELIVER_ACTION)) {
+        id = saveReceivedMessage(context,
+                               sms.getDisplayOriginatingAddress(),
+                               body,
+                               sms.getTimestampMillis(),
+                               timestamp,
+                               sms.getPseudoSubject());
+      }
+
+      notifySmsReceived(id,
+                        sms.getDisplayOriginatingAddress(),
+                        body,
+                        getGeckoMessageClass(sms.getMessageClass()),
+                        sms.getTimestampMillis(),
+                        timestamp);
+
+      return;
+    }
+
+    if (intent.getAction().equals(ACTION_SMS_SENT) ||
+        intent.getAction().equals(ACTION_SMS_DELIVERED)) {
+      Bundle bundle = intent.getExtras();
+
+      if (bundle == null || !bundle.containsKey("envelopeId") ||
+          !bundle.containsKey("number") || !bundle.containsKey("message") ||
+          !bundle.containsKey("requestId")) {
+        Log.e("GeckoSmsManager", "Got an invalid ACTION_SMS_SENT/ACTION_SMS_DELIVERED!");
+        return;
+      }
+
+      int envelopeId = bundle.getInt("envelopeId");
+      Postman postman = Postman.getInstance();
+
+      Envelope envelope = postman.getEnvelope(envelopeId);
+      if (envelope == null) {
+        Log.e("GeckoSmsManager", "Got an invalid envelope id (or Envelope has been destroyed)!");
+        return;
+      }
+
+      Envelope.SubParts part = intent.getAction().equals(ACTION_SMS_SENT)
+                                 ? Envelope.SubParts.SENT_PART
+                                 : Envelope.SubParts.DELIVERED_PART;
+      envelope.decreaseRemainingParts(part);
+
+
+      if (getResultCode() != Activity.RESULT_OK) {
+        switch (getResultCode()) {
+          case SmsManager.RESULT_ERROR_NULL_PDU:
+            envelope.setError(kInternalError);
+            break;
+          case SmsManager.RESULT_ERROR_NO_SERVICE:
+          case SmsManager.RESULT_ERROR_RADIO_OFF:
+            envelope.setError(kNoSignalError);
+            break;
+          case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
+          default:
+            envelope.setError(kUnknownError);
+            break;
+        }
+        envelope.markAsFailed(part);
+        Log.i("GeckoSmsManager", "SMS part sending failed!");
+      }
+
+      if (envelope.arePartsRemaining(part)) {
+        return;
+      }
+
+      if (envelope.isFailing(part)) {
+        if (part == Envelope.SubParts.SENT_PART) {
+          if (bundle.getBoolean("shouldNotify")) {
+            notifySmsSendFailed(envelope.getError(), bundle.getInt("requestId"));
+          }
+          Log.i("GeckoSmsManager", "SMS sending failed!");
+        } else {
+          notifySmsDelivery(envelope.getMessageId(),
+                            kDeliveryStatusError,
+                            bundle.getString("number"),
+                            bundle.getString("message"),
+                            envelope.getMessageTimestamp());
+          Log.i("GeckoSmsManager", "SMS delivery failed!");
+        }
+      } else {
+        if (part == Envelope.SubParts.SENT_PART) {
+          String number = bundle.getString("number");
+          String message = bundle.getString("message");
+          long timestamp = System.currentTimeMillis();
+
+          // save message only if we're default SMS app, otherwise sendTextMessage does it for us
+          int id = 0;
+          if (isDefaultSmsApp(GeckoAppShell.getContext())) {
+            id = saveSentMessage(number, message, timestamp);
+          }
+
+          if (bundle.getBoolean("shouldNotify")) {
+            notifySmsSent(id, number, message, timestamp, bundle.getInt("requestId"));
+          }
+
+          envelope.setMessageId(id);
+          envelope.setMessageTimestamp(timestamp);
+
+          Log.i("GeckoSmsManager", "SMS sending was successful!");
+        } else {
+          Uri id = ContentUris.withAppendedId(kSmsContentUri, envelope.getMessageId());
+          updateMessageStatus(id, Telephony.Sms.STATUS_COMPLETE);
+
+          notifySmsDelivery(envelope.getMessageId(),
+                            kDeliveryStatusSuccess,
+                            bundle.getString("number"),
+                            bundle.getString("message"),
+                            envelope.getMessageTimestamp());
+          Log.i("GeckoSmsManager", "SMS successfully delivered!");
+        }
+      }
+
+      // Destroy the envelope object only if the SMS has been sent and delivered.
+      if (!envelope.arePartsRemaining(Envelope.SubParts.SENT_PART) &&
+          !envelope.arePartsRemaining(Envelope.SubParts.DELIVERED_PART)) {
+        postman.destroyEnvelope(envelopeId);
+      }
+    }
+  }
+
+  @Override
+  public void send(String aNumber, String aMessage, int aRequestId, boolean aShouldNotify) {
+    int envelopeId = Postman.kUnknownEnvelopeId;
+
+    try {
+      SmsManager sm = SmsManager.getDefault();
+
+      Intent sentIntent = new Intent(GeckoAppShell.getContext(), GeckoSmsManager.class);
+      sentIntent.setAction(ACTION_SMS_SENT);
+      Intent deliveredIntent = new Intent(GeckoAppShell.getContext(), GeckoSmsManager.class);
+      deliveredIntent.setAction(ACTION_SMS_DELIVERED);
+
+      Bundle bundle = new Bundle();
+      bundle.putString("number", aNumber);
+      bundle.putString("message", aMessage);
+      bundle.putInt("requestId", aRequestId);
+      bundle.putBoolean("shouldNotify", aShouldNotify);
+
+      if (aMessage.length() <= kMaxMessageSize) {
+        envelopeId = Postman.getInstance().createEnvelope(1);
+        bundle.putInt("envelopeId", envelopeId);
+
+        sentIntent.putExtras(bundle);
+        deliveredIntent.putExtras(bundle);
+
+        /*
+         * There are a few things to know about getBroadcast and pending intents:
+         * - the pending intents are in a shared pool maintained by the system;
+         * - each pending intent is identified by a token;
+         * - when a new pending intent is created, if it has the same token as
+         *   another intent in the pool, one of them has to be removed.
+         *
+         * To prevent having a hard time because of this situation, we give a
+         * unique id to all pending intents we are creating. This unique id is
+         * generated by GetPendingIntentUID().
+         */
+        PendingIntent sentPendingIntent =
+          PendingIntent.getBroadcast(GeckoAppShell.getContext(),
+                                     pendingIntentGuid.incrementAndGet(), sentIntent,
+                                     PendingIntent.FLAG_CANCEL_CURRENT);
+
+        PendingIntent deliveredPendingIntent =
+          PendingIntent.getBroadcast(GeckoAppShell.getContext(),
+                                     pendingIntentGuid.incrementAndGet(), deliveredIntent,
+                                     PendingIntent.FLAG_CANCEL_CURRENT);
+
+        sm.sendTextMessage(aNumber, null, aMessage,
+                           sentPendingIntent, deliveredPendingIntent);
+      } else {
+        ArrayList<String> parts = sm.divideMessage(aMessage);
+        envelopeId = Postman.getInstance().createEnvelope(parts.size());
+        bundle.putInt("envelopeId", envelopeId);
+
+        sentIntent.putExtras(bundle);
+        deliveredIntent.putExtras(bundle);
+
+        ArrayList<PendingIntent> sentPendingIntents =
+          new ArrayList<PendingIntent>(parts.size());
+        ArrayList<PendingIntent> deliveredPendingIntents =
+          new ArrayList<PendingIntent>(parts.size());
+
+        for (int i = 0; i < parts.size(); ++i) {
+          sentPendingIntents.add(
+            PendingIntent.getBroadcast(GeckoAppShell.getContext(),
+                                       pendingIntentGuid.incrementAndGet(), sentIntent,
+                                       PendingIntent.FLAG_CANCEL_CURRENT)
+          );
+
+          deliveredPendingIntents.add(
+            PendingIntent.getBroadcast(GeckoAppShell.getContext(),
+                                       pendingIntentGuid.incrementAndGet(), deliveredIntent,
+                                       PendingIntent.FLAG_CANCEL_CURRENT)
+          );
+        }
+
+        sm.sendMultipartTextMessage(aNumber, null, parts, sentPendingIntents,
+                                    deliveredPendingIntents);
+      }
+    } catch (Exception e) {
+      Log.e("GeckoSmsManager", "Failed to send an SMS: ", e);
+
+      if (envelopeId != Postman.kUnknownEnvelopeId) {
+        Postman.getInstance().destroyEnvelope(envelopeId);
+      }
+
+      notifySmsSendFailed(kUnknownError, aRequestId);
+    }
+  }
+
+  public int saveSentMessage(String aRecipient, String aBody, long aDate) {
+    try {
+      ContentValues values = new ContentValues();
+      values.put(Telephony.Sms.ADDRESS, aRecipient);
+      values.put(Telephony.Sms.BODY, aBody);
+      values.put(Telephony.Sms.DATE, aDate);
+      // Always 'PENDING' because we always request status report.
+      values.put(Telephony.Sms.STATUS, Telephony.Sms.STATUS_PENDING);
+      values.put(Telephony.Sms.SEEN, 0);
+      values.put(Telephony.Sms.READ, 0);
+
+      ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
+      Uri uri = cr.insert(kSmsSentContentUri, values);
+
+      long id = ContentUris.parseId(uri);
+
+      // The DOM API takes a 32bits unsigned int for the id. It's unlikely that
+      // we happen to need more than that but it doesn't cost to check.
+      if (id > UNSIGNED_INTEGER_MAX_VALUE) {
+        throw new IdTooHighException();
+      }
+
+      return (int)id;
+    } catch (IdTooHighException e) {
+      Log.e("GeckoSmsManager", "The id we received is higher than the higher allowed value.");
+      return -1;
+    } catch (Exception e) {
+      Log.e("GeckoSmsManager", "Something went wrong when trying to write a sent message", e);
+      return -1;
+    }
+  }
+
+  public void updateMessageStatus(Uri aId, int aStatus) {
+    ContentValues values = new ContentValues(1);
+    values.put(Telephony.Sms.STATUS, aStatus);
+
+    ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
+    if (cr.update(aId, values, null, null) != 1) {
+      Log.i("GeckoSmsManager", "Failed to update message status!");
+    }
+  }
+
+  public int saveReceivedMessage(Context aContext, String aSender, String aBody, long aDateSent, long aDate, String aSubject) {
+    ContentValues values = new ContentValues();
+    values.put(Telephony.Sms.Inbox.ADDRESS, aSender);
+    values.put(Telephony.Sms.Inbox.BODY, aBody);
+    values.put(Telephony.Sms.Inbox.DATE_SENT, aDateSent);
+    values.put(Telephony.Sms.Inbox.DATE, aDate);
+    values.put(Telephony.Sms.Inbox.STATUS, Telephony.Sms.STATUS_COMPLETE);
+    values.put(Telephony.Sms.Inbox.READ, 0);
+    values.put(Telephony.Sms.Inbox.SEEN, 0);
+
+    ContentResolver cr = aContext.getContentResolver();
+    Uri uri = cr.insert(kSmsContentUri, values);
+
+    long id = ContentUris.parseId(uri);
+
+    // The DOM API takes a 32bits unsigned int for the id. It's unlikely that
+    // we happen to need more than that but it doesn't cost to check.
+    if (id > UNSIGNED_INTEGER_MAX_VALUE) {
+      Log.i("GeckoSmsManager", "The id we received is higher than the higher allowed value.");
+      return -1;
+    }
+
+    return (int)id;
+  }
+
+  @Override
+  public void getMessage(int aMessageId, int aRequestId) {
+    class GetMessageRunnable implements Runnable {
+      private final int mMessageId;
+      private final int mRequestId;
+
+      GetMessageRunnable(int aMessageId, int aRequestId) {
+        mMessageId = aMessageId;
+        mRequestId = aRequestId;
+      }
+
+      @Override
+      public void run() {
+        Cursor cursor = null;
+
+        try {
+          ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
+          Uri message = ContentUris.withAppendedId(kSmsContentUri, mMessageId);
+
+          cursor = cr.query(message, kRequiredMessageRows, null, null, null);
+          if (cursor == null || cursor.getCount() == 0) {
+            throw new NotFoundException();
+          }
+
+          if (cursor.getCount() != 1) {
+            throw new TooManyResultsException();
+          }
+
+          cursor.moveToFirst();
+
+          if (cursor.getInt(cursor.getColumnIndex("_id")) != mMessageId) {
+            throw new UnmatchingIdException();
+          }
+
+          int type = cursor.getInt(cursor.getColumnIndex("type"));
+          int deliveryStatus;
+          String sender = "";
+          String receiver = "";
+
+          if (type == kSmsTypeInbox) {
+            deliveryStatus = kDeliveryStatusSuccess;
+            sender = cursor.getString(cursor.getColumnIndex("address"));
+          } else if (type == kSmsTypeSentbox) {
+            deliveryStatus = getGeckoDeliveryStatus(cursor.getInt(cursor.getColumnIndex("status")));
+            receiver = cursor.getString(cursor.getColumnIndex("address"));
+          } else {
+            throw new InvalidTypeException();
+          }
+
+          boolean read = cursor.getInt(cursor.getColumnIndex("read")) != 0;
+
+          notifyGetSms(cursor.getInt(cursor.getColumnIndex("_id")),
+                       deliveryStatus,
+                       receiver, sender,
+                       cursor.getString(cursor.getColumnIndex("body")),
+                       cursor.getLong(cursor.getColumnIndex("date")),
+                       read,
+                       mRequestId);
+        } catch (NotFoundException e) {
+          Log.i("GeckoSmsManager", "Message id " + mMessageId + " not found");
+          notifyGetSmsFailed(kNotFoundError, mRequestId);
+        } catch (UnmatchingIdException e) {
+          Log.e("GeckoSmsManager", "Requested message id (" + mMessageId +
+                                   ") is different from the one we got.");
+          notifyGetSmsFailed(kUnknownError, mRequestId);
+        } catch (TooManyResultsException e) {
+          Log.e("GeckoSmsManager", "Get too many results for id " + mMessageId);
+          notifyGetSmsFailed(kUnknownError, mRequestId);
+        } catch (InvalidTypeException e) {
+          Log.i("GeckoSmsManager", "Message has an invalid type, we ignore it.");
+          notifyGetSmsFailed(kNotFoundError, mRequestId);
+        } catch (Exception e) {
+          Log.e("GeckoSmsManager", "Error while trying to get message", e);
+          notifyGetSmsFailed(kUnknownError, mRequestId);
+        } finally {
+          if (cursor != null) {
+            cursor.close();
+          }
+        }
+      }
+    }
+
+    if (!SmsIOThread.getInstance().execute(new GetMessageRunnable(aMessageId, aRequestId))) {
+      Log.e("GeckoSmsManager", "Failed to add GetMessageRunnable to the SmsIOThread");
+      notifyGetSmsFailed(kUnknownError, aRequestId);
+    }
+  }
+
+  @Override
+  public void deleteMessage(int aMessageId, int aRequestId) {
+    class DeleteMessageRunnable implements Runnable {
+      private final int mMessageId;
+      private final int mRequestId;
+
+      DeleteMessageRunnable(int aMessageId, int aRequestId) {
+        mMessageId = aMessageId;
+        mRequestId = aRequestId;
+      }
+
+      @Override
+      public void run() {
+        try {
+          ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
+          Uri message = ContentUris.withAppendedId(kSmsContentUri, mMessageId);
+
+          int count = cr.delete(message, null, null);
+
+          if (count > 1) {
+            throw new TooManyResultsException();
+          }
+
+          notifySmsDeleted(count == 1, mRequestId);
+        } catch (TooManyResultsException e) {
+          Log.e("GeckoSmsManager", "Delete more than one message?", e);
+          notifySmsDeleteFailed(kUnknownError, mRequestId);
+        } catch (Exception e) {
+          Log.e("GeckoSmsManager", "Error while trying to delete a message", e);
+          notifySmsDeleteFailed(kUnknownError, mRequestId);
+        }
+      }
+    }
+
+    if (!SmsIOThread.getInstance().execute(new DeleteMessageRunnable(aMessageId, aRequestId))) {
+      Log.e("GeckoSmsManager", "Failed to add GetMessageRunnable to the SmsIOThread");
+      notifySmsDeleteFailed(kUnknownError, aRequestId);
+    }
+  }
+
+  private void getMessageFromCursorAndNotify(Cursor aCursor, int aRequestId) throws Exception {
+    int type = aCursor.getInt(aCursor.getColumnIndex("type"));
+    int deliveryStatus = kDeliveryStateUnknown;
+    String sender = "";
+    String receiver = "";
+
+    if (type == kSmsTypeInbox) {
+      deliveryStatus = kDeliveryStatusSuccess;
+      sender = aCursor.getString(aCursor.getColumnIndex("address"));
+    } else if (type == kSmsTypeSentbox) {
+      deliveryStatus = getGeckoDeliveryStatus(aCursor.getInt(aCursor.getColumnIndex("status")));
+      receiver = aCursor.getString(aCursor.getColumnIndex("address"));
+    } else {
+      throw new Exception("Shouldn't ever get a message here that's not from inbox or sentbox");
+    }
+
+    boolean read = aCursor.getInt(aCursor.getColumnIndex("read")) != 0;
+
+    notifyMessageCursorResult(aCursor.getInt(aCursor.getColumnIndex("_id")),
+                              deliveryStatus,
+                              receiver, sender,
+                              aCursor.getString(aCursor.getColumnIndex("body")),
+                              aCursor.getLong(aCursor.getColumnIndex("date")),
+                              aCursor.getLong(aCursor.getColumnIndex("thread_id")),
+                              read,
+                              aRequestId);
+  }
+
+  @Override
+  public void markMessageRead(int aMessageId, boolean aValue, boolean aSendReadReport, int aRequestId) {
+    class MarkMessageReadRunnable implements Runnable {
+      private final int mMessageId;
+      private final boolean mValue;
+      private final int mRequestId;
+
+      MarkMessageReadRunnable(int aMessageId, boolean aValue, int aRequestId) {
+        mMessageId = aMessageId;
+        mValue = aValue;
+        mRequestId = aRequestId;
+      }
+
+      @Override
+      public void run() {
+        try {
+          ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
+          Uri message = ContentUris.withAppendedId(kSmsContentUri, mMessageId);
+
+          ContentValues updatedProps = new ContentValues();
+          updatedProps.put("read", mValue);
+
+          int count = cr.update(message, updatedProps, null, null);
+
+          notifySmsMarkedAsRead(count == 1, mRequestId);
+        } catch (Exception e) {
+          Log.e("GeckoSmsManager", "Error while trying to mark message as read: " + e);
+          notifySmsMarkAsReadFailed(kUnknownError, mRequestId);
+        }
+      }
+    }
+
+    if (aSendReadReport == true) {
+      Log.w("GeckoSmsManager", "Android SmsManager doesn't suport read receipts for SMS.");
+    }
+
+    if (!SmsIOThread.getInstance().execute(new MarkMessageReadRunnable(aMessageId, aValue, aRequestId))) {
+      Log.e("GeckoSmsManager", "Failed to add MarkMessageReadRunnable to the SmsIOThread");
+      notifySmsMarkAsReadFailed(kUnknownError, aRequestId);
+    }
+  }
+
+  @Override
+  public void createMessageCursor(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, String aDelivery, boolean aHasRead, boolean aRead, boolean aHasThreadId, long aThreadId, boolean aReverse, int aRequestId) {
+    class CreateMessageCursorRunnable implements Runnable {
+      private final long     mStartDate;
+      private final long     mEndDate;
+      private final String[] mNumbers;
+      private final int      mNumbersCount;
+      private final String   mDelivery;
+      private final boolean  mHasThreadId;
+      private final long     mThreadId;
+      private final boolean  mReverse;
+      private final int      mRequestId;
+
+      CreateMessageCursorRunnable(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, String aDelivery, boolean aHasRead, boolean aRead, boolean aHasThreadId, long aThreadId, boolean aReverse, int aRequestId) {
+        mStartDate = aStartDate;
+        mEndDate = aEndDate;
+        mNumbers = aNumbers;
+        mNumbersCount = aNumbersCount;
+        mDelivery = aDelivery;
+        mHasThreadId = aHasThreadId;
+        mThreadId = aThreadId;
+        mReverse = aReverse;
+        mRequestId = aRequestId;
+      }
+
+      @Override
+      public void run() {
+        Cursor cursor = null;
+        boolean closeCursor = true;
+
+        try {
+          StringBuilder restrictions = new StringBuilder();
+          Formatter formatter = new Formatter(restrictions);
+
+          if (mStartDate >= 0) {
+            formatter.format("date >= '%d' AND ", mStartDate);
+          }
+
+          if (mEndDate >= 0) {
+            formatter.format("date <= '%d' AND ", mEndDate);
+          }
+
+          if (mNumbersCount > 0) {
+            formatter.format("address IN ('%s'", mNumbers[0]);
+
+            for (int i = 1; i < mNumbersCount; ++i) {
+              formatter.format(", '%s'", mNumbers[i]);
+            }
+
+            formatter.format(") AND ");
+          }
+
+          if (mDelivery == null || mDelivery.isEmpty()) {
+            formatter.format("type IN ('%d', '%d') AND ", kSmsTypeSentbox, kSmsTypeInbox);
+          } else if (mDelivery.equals("sent")) {
+            formatter.format("type = '%d' AND ", kSmsTypeSentbox);
+          } else if (mDelivery.equals("received")) {
+            formatter.format("type = '%d' AND ", kSmsTypeInbox);
+          } else {
+            throw new Exception("Unexpected delivery state: " + mDelivery);
+          }
+
+          if (mHasThreadId) {
+            formatter.format("thread_id = '%d' AND ", mThreadId);
+          }
+
+          // Final 'AND 1' is a no-op so we don't have to special case the last
+          // condition.
+          formatter.format("1");
+
+          ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
+          cursor = cr.query(kSmsContentUri,
+                            kRequiredMessageRows,
+                            restrictions.toString(),
+                            null,
+                            mReverse ? "date DESC" : "date ASC");
+
+          if (cursor.getCount() == 0) {
+            notifyCursorDone(mRequestId);
+            return;
+          }
+
+          MessagesListManager.getInstance().add(mRequestId, cursor);
+
+          cursor.moveToFirst();
+          getMessageFromCursorAndNotify(cursor, mRequestId);
+          closeCursor = false;
+        } catch (Exception e) {
+          Log.e("GeckoSmsManager", "Error while trying to create a message list cursor", e);
+          notifyCursorError(kUnknownError, mRequestId);
+        } finally {
+          if (closeCursor && cursor != null) {
+            cursor.close();
+          }
+        }
+      }
+    }
+
+    if (!SmsIOThread.getInstance().execute(new CreateMessageCursorRunnable(aStartDate, aEndDate, aNumbers, aNumbersCount, aDelivery, aHasRead, aRead, aHasThreadId, aThreadId, aReverse, aRequestId))) {
+      Log.e("GeckoSmsManager", "Failed to add CreateMessageCursorRunnable to the SmsIOThread");
+      notifyCursorError(kUnknownError, aRequestId);
+    }
+  }
+
+  @Override
+  public void getNextMessage(int aRequestId) {
+    class GetNextMessageRunnable implements Runnable {
+      private final int mRequestId;
+
+      GetNextMessageRunnable(int aRequestId) {
+        mRequestId = aRequestId;
+      }
+
+      @Override
+      public void run() {
+        Cursor cursor = null;
+        boolean closeCursor = true;
+        try {
+          cursor = MessagesListManager.getInstance().get(mRequestId);
+
+          if (!cursor.moveToNext()) {
+            MessagesListManager.getInstance().remove(mRequestId);
+            notifyCursorDone(mRequestId);
+            return;
+          }
+
+          getMessageFromCursorAndNotify(cursor, mRequestId);
+          closeCursor = false;
+        } catch (Exception e) {
+          Log.e("GeckoSmsManager", "Error while trying to get the next message of a list", e);
+          notifyCursorError(kUnknownError, mRequestId);
+        } finally {
+          if (closeCursor) {
+            cursor.close();
+          }
+        }
+      }
+    }
+
+    if (!SmsIOThread.getInstance().execute(new GetNextMessageRunnable(aRequestId))) {
+      Log.e("GeckoSmsManager", "Failed to add GetNextMessageRunnable to the SmsIOThread");
+      notifyCursorError(kUnknownError, aRequestId);
+    }
+  }
+
+  private void getThreadFromCursorAndNotify(Cursor aCursor, int aRequestId) throws Exception {
+    ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
+
+    long id = aCursor.getLong(aCursor.getColumnIndex("thread_id"));
+    Cursor msgCursor = cr.query(kSmsContentUri,
+                                kRequiredMessageRowsForThread,
+                                "thread_id = " + id,
+                                null,
+                                "date DESC");
+
+    if (msgCursor == null || msgCursor.getCount() == 0) {
+      throw new Exception("Empty thread " + id);
+    }
+
+    msgCursor.moveToFirst();
+
+    String lastMessageSubject = msgCursor.getString(msgCursor.getColumnIndex("subject"));
+    String body = msgCursor.getString(msgCursor.getColumnIndex("body"));
+    long timestamp = msgCursor.getLong(msgCursor.getColumnIndex("date"));
+
+    HashSet<String> participants = new HashSet<>();
+    do {
+      String p = msgCursor.getString(msgCursor.getColumnIndex("address"));
+      participants.add(p);
+    } while (msgCursor.moveToNext());
+
+    //TODO: handle MMS
+    String lastMessageType = "sms";
+
+    msgCursor = cr.query(kSmsContentUri,
+                         kRequiredMessageRowsForThread,
+                         "thread_id = " + id + " AND read = 0",
+                         null,
+                         null);
+
+    if (msgCursor == null) {
+      Log.e("GeckoSmsManager", "We should never get here, should have errored before");
+      throw new Exception("Empty thread " + id);
+    }
+
+    long unreadCount = msgCursor.getCount();
+
+    notifyThreadCursorResult(id, lastMessageSubject, body, unreadCount, participants.toArray(), timestamp, lastMessageType, aRequestId);
+  }
+
+  @Override
+  public void createThreadCursor(int aRequestId) {
+    class CreateThreadCursorRunnable implements Runnable {
+      private final int mRequestId;
+
+      CreateThreadCursorRunnable(int aRequestId) {
+        mRequestId = aRequestId;
+      }
+
+      @Override
+      public void run() {
+        try {
+          ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
+          Cursor cursor = cr.query(kSmsThreadsContentUri,
+                                   kThreadProjection,
+                                   null,
+                                   null,
+                                   "date DESC");
+          if (cursor == null || !cursor.moveToFirst()) {
+            notifyCursorDone(mRequestId);
+            return;
+          }
+
+          MessagesListManager.getInstance().add(mRequestId, cursor);
+
+          getThreadFromCursorAndNotify(cursor, mRequestId);
+        } catch (Exception e) {
+          Log.e("GeckoSmsManager", "Error while trying to create thread cursor: " + e);
+          notifyCursorError(kUnknownError, mRequestId);
+        }
+      }
+    }
+
+    if (!SmsIOThread.getInstance().execute(new CreateThreadCursorRunnable(aRequestId))) {
+      Log.e("GeckoSmsManager", "Failed to add CreateThreadCursorRunnable to the SmsIOThread");
+      notifyCursorError(kUnknownError, aRequestId);
+    }
+  }
+
+  @Override
+  public void getNextThread(int aRequestId) {
+    class GetNextThreadRunnable implements Runnable {
+      private final int mRequestId;
+
+      GetNextThreadRunnable(int aRequestId) {
+        mRequestId = aRequestId;
+      }
+
+      @Override
+      public void run() {
+        try {
+          Cursor cursor = MessagesListManager.getInstance().get(mRequestId);
+
+          if (!cursor.moveToNext()) {
+            MessagesListManager.getInstance().remove(mRequestId);
+            notifyCursorDone(mRequestId);
+            return;
+          }
+
+          getThreadFromCursorAndNotify(cursor, mRequestId);
+        } catch (Exception e) {
+          Log.e("GeckoSmsManager", "Error while trying to create thread cursor: " + e);
+          notifyCursorError(kUnknownError, mRequestId);
+        }
+      }
+    }
+
+    if (!SmsIOThread.getInstance().execute(new GetNextThreadRunnable(aRequestId))) {
+      Log.e("GeckoSmsManager", "Failed to add GetNextThreadRunnable to the SmsIOThread");
+      notifyCursorError(kUnknownError, aRequestId);
+    }
+  }
+
+  @Override
+  public void stop() {
+    GeckoAppShell.getContext().unregisterReceiver(this);
+  }
+
+  @Override
+  public void shutdown() {
+    SmsIOThread.getInstance().interrupt();
+    MessagesListManager.getInstance().clear();
+  }
+
+  private int getGeckoDeliveryStatus(int aDeliveryStatus) {
+    if (aDeliveryStatus == Telephony.Sms.STATUS_NONE) {
+      return kDeliveryStatusNotApplicable;
+    }
+    if (aDeliveryStatus >= Telephony.Sms.STATUS_FAILED) {
+      return kDeliveryStatusError;
+    }
+    if (aDeliveryStatus >= Telephony.Sms.STATUS_PENDING) {
+      return kDeliveryStatusPending;
+    }
+    return kDeliveryStatusSuccess;
+  }
+
+  private int getGeckoMessageClass(MessageClass aMessageClass) {
+    switch (aMessageClass) {
+      case CLASS_0:
+        return kMessageClassClass0;
+      case CLASS_1:
+        return kMessageClassClass1;
+      case CLASS_2:
+        return kMessageClassClass2;
+      case CLASS_3:
+        return kMessageClassClass3;
+      default:
+        return kMessageClassNormal;
+    }
+  }
+
+  static class IdTooHighException extends Exception {
+    private static final long serialVersionUID = 29935575131092050L;
+  }
+
+  static class InvalidTypeException extends Exception {
+    private static final long serialVersionUID = 47436856832535912L;
+  }
+
+  static class NotFoundException extends Exception {
+    private static final long serialVersionUID = 1940676816633984L;
+  }
+
+  static class TooManyResultsException extends Exception {
+    private static final long serialVersionUID = 51883196784325305L;
+  }
+
+  static class UnmatchingIdException extends Exception {
+    private static final long serialVersionUID = 158467542575633280L;
+  }
+
+  @WrapForJNI
+  public static native void notifySmsReceived(int aId, String aSender, String aBody, int aMessageClass, long aSentTimestamp, long aTimestamp);
+  @WrapForJNI
+  private static native void notifySmsSent(int aId, String aReceiver, String aBody, long aTimestamp, int aRequestId);
+  @WrapForJNI
+  private static native void notifySmsDelivery(int aId, int aDeliveryStatus, String aReceiver, String aBody, long aTimestamp);
+  @WrapForJNI
+  private static native void notifySmsSendFailed(int aError, int aRequestId);
+  @WrapForJNI
+  private static native void notifyGetSms(int aId, int aDeliveryStatus, String aReceiver, String aSender, String aBody, long aTimestamp, boolean aRead, int aRequestId);
+  @WrapForJNI
+  private static native void notifyGetSmsFailed(int aError, int aRequestId);
+  @WrapForJNI
+  private static native void notifySmsDeleted(boolean aDeleted, int aRequestId);
+  @WrapForJNI
+  private static native void notifySmsDeleteFailed(int aError, int aRequestId);
+  @WrapForJNI
+  private static native void notifySmsMarkedAsRead(boolean aMarkedAsRead, int aRequestId);
+  @WrapForJNI
+  private static native void notifySmsMarkAsReadFailed(int aError, int aRequestId);
+  @WrapForJNI
+  private static native void notifyCursorError(int aError, int aRequestId);
+  @WrapForJNI
+  private static native void notifyThreadCursorResult(long aId, String aLastMessageSubject, String aBody, long aUnreadCount, Object[] aParticipants, long aTimestamp, String aLastMessageType, int aRequestId);
+  @WrapForJNI
+  private static native void notifyMessageCursorResult(int aMessageId, int aDeliveryStatus, String aReceiver, String aSender, String aBody, long aTimestamp, long aThreadId, boolean aRead, int aRequestId);
+  @WrapForJNI
+  private static native void notifyCursorDone(int aRequestId);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java
@@ -0,0 +1,680 @@
+/* -*- 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;
+
+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 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.shouldUse(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);
+
+            if (call.method == null) {
+                final GeckoEvent e = (GeckoEvent) call.target;
+                GeckoAppShell.notifyGeckoOfEvent(e);
+                e.recycle();
+                continue;
+            }
+            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 static String getTypeFromAction(String action) {
+        if (GeckoAppShell.ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
+            return "-bookmark";
+        }
+        return null;
+    }
+
+    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);
+        }
+
+        final String type = getTypeFromAction(mAction);
+        if (type != null) {
+            args.append(" ").append(type);
+        }
+
+        // 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() {
+        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() {
+                GeckoAppShell.registerJavaUiThread();
+            }
+        });
+
+        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");
+            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);
+    }
+
+    public static void addPendingEvent(final GeckoEvent e) {
+        synchronized (QUEUED_CALLS) {
+            if (isRunning()) {
+                // We may just have switched to running state.
+                GeckoAppShell.notifyGeckoOfEvent(e);
+                e.recycle();
+            } else {
+                QUEUED_CALLS.add(new QueuedCall(null, e, null, State.RUNNING));
+            }
+        }
+    }
+
+    @WrapForJNI
+    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
+    private static void setState(final State newState) {
+        ThreadUtils.assertOnGeckoThread();
+        synchronized (QUEUED_CALLS) {
+            flushQueuedNativeCallsLocked(newState);
+            sState = newState;
+        }
+    }
+
+    @WrapForJNI
+    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")
+    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")
+    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")
+    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);
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoView.java
@@ -0,0 +1,713 @@
+/* -*- 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.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.GLController;
+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 {
+
+    private static final String DEFAULT_SHARED_PREFERENCES_FILE = "GeckoView";
+    private static final String LOGTAG = "GeckoView";
+
+    private ChromeDelegate mChromeDelegate;
+    private ContentDelegate mContentDelegate;
+
+    private InputConnectionListener mInputConnectionListener;
+
+    private final GeckoEventListener mGeckoEventListener = new GeckoEventListener() {
+        @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);
+                    }
+                }
+            });
+        }
+    };
+
+    private final NativeEventListener mNativeEventListener = new NativeEventListener() {
+        @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
+    private static final class Window extends JNIObject {
+        /* package */ final GLController glController = new GLController();
+
+        static native void open(Window instance, GeckoView view, GLController glController,
+                                String chromeURI,
+                                int width, int height);
+
+        @Override protected native void disposeNative();
+        native void close();
+        native void reattach(GeckoView view);
+    }
+
+    // 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];
+                }
+            };
+    }
+
+    private 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;
+        // 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);
+
+        if (stateBinder.window != null) {
+            this.window = stateBinder.window;
+        }
+        stateSaved = false;
+    }
+
+    @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();
+            final String chromeURI = getGeckoInterface().getDefaultChromeURI();
+
+            if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+                Window.open(window, this, window.glController,
+                            chromeURI,
+                            metrics.widthPixels, metrics.heightPixels);
+            } else {
+                GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY, Window.class,
+                        "open", window, GeckoView.class, this, window.glController,
+                        chromeURI,
+                        metrics.widthPixels, metrics.heightPixels);
+            }
+        } else {
+            if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+                window.reattach(this);
+            } else {
+                GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
+                        window, "reattach", GeckoView.class, this);
+            }
+        }
+
+        setGLController(window.glController);
+        super.onAttachedToWindow();
+    }
+
+    @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");
+        }
+    }
+
+    /* 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 connectToGecko() {
+        GeckoAppShell.notifyObservers("Viewport:Flush", null);
+    }
+
+    private void handleReady(final JSONObject message) {
+        connectToGecko();
+
+        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);
+    }
+
+}
rename from mobile/android/base/java/org/mozilla/gecko/GeckoViewChrome.java
rename to mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewChrome.java
rename from mobile/android/base/java/org/mozilla/gecko/GeckoViewContent.java
rename to mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewContent.java
new file mode 100644
--- /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();
+}
new file mode 100644
--- /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);
+    }
+}
new file mode 100644
--- /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);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NotificationClient.java
@@ -0,0 +1,212 @@
+/* -*- 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.app.Notification;
+import android.app.PendingIntent;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.LinkedList;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Client for posting notifications through a NotificationHandler.
+ */
+public abstract class NotificationClient {
+    private static final String LOGTAG = "GeckoNotificationClient";
+
+    private volatile NotificationHandler mHandler;
+    private boolean mReady;
+    private final LinkedList<Runnable> mTaskQueue = new LinkedList<Runnable>();
+    private final ConcurrentHashMap<Integer, UpdateRunnable> mUpdatesMap =
+            new ConcurrentHashMap<Integer, UpdateRunnable>();
+
+    /**
+     * Runnable that is reused between update notifications.
+     *
+     * Updates happen frequently, so reusing Runnables prevents frequent dynamic allocation.
+     */
+    private class UpdateRunnable implements Runnable {
+        private long mProgress;
+        private long mProgressMax;
+        private String mAlertText;
+        final private int mNotificationID;
+
+        public UpdateRunnable(int notificationID) {
+            mNotificationID = notificationID;
+        }
+
+        public synchronized boolean updateProgress(long progress, long progressMax, String alertText) {
+            if (progress == mProgress
+                    && mProgressMax == progressMax
+                    && TextUtils.equals(mAlertText, alertText)) {
+                return false;
+            }
+
+            mProgress = progress;
+            mProgressMax = progressMax;
+            mAlertText = alertText;
+            return true;
+        }
+
+        @Override
+        public void run() {
+            long progress;
+            long progressMax;
+            String alertText;
+
+            synchronized (this) {
+                progress = mProgress;
+                progressMax = mProgressMax;
+                alertText = mAlertText;
+            }
+
+            mHandler.update(mNotificationID, progress, progressMax, alertText);
+        }
+    };
+
+    /**
+     * Adds a notification.
+     *
+     * @see NotificationHandler#add(int, String, String, String, PendingIntent, PendingIntent)
+     */
+    public synchronized void add(final int notificationID, final String aImageUrl, final String aHost,
+            final String aAlertTitle, final String aAlertText, final PendingIntent contentIntent) {
+        mTaskQueue.add(new Runnable() {
+            @Override
+            public void run() {
+                mHandler.add(notificationID, aImageUrl, aHost, aAlertTitle, aAlertText, contentIntent);
+            }
+        });
+        notify();
+
+        if (!mReady) {
+            bind();
+        }
+    }
+
+    /**
+     * Adds a notification.
+     *
+     * @see NotificationHandler#add(int, Notification)
+     */
+    public synchronized void add(final int notificationID, final Notification notification) {
+        mTaskQueue.add(new Runnable() {
+            @Override
+            public void run() {
+                mHandler.add(notificationID, notification);
+            }
+        });
+        notify();
+
+        if (!mReady) {
+            bind();
+        }
+    }
+
+    /**
+     * Updates a notification.
+     *
+     * @see NotificationHandler#update(int, long, long, String)
+     */
+    public void update(final int notificationID, final long aProgress, final long aProgressMax,
+            final String aAlertText) {
+        UpdateRunnable runnable = mUpdatesMap.get(notificationID);
+
+        if (runnable == null) {
+            runnable = new UpdateRunnable(notificationID);
+            mUpdatesMap.put(notificationID, runnable);
+        }
+
+        // If we've already posted an update with these values, there's no
+        // need to do it again.
+        if (!runnable.updateProgress(aProgress, aProgressMax, aAlertText)) {
+            return;
+        }
+
+        synchronized (this) {
+            if (mReady) {
+                mTaskQueue.add(runnable);
+                notify();
+            }
+        }
+    }
+
+    /**
+     * Removes a notification.
+     *
+     * @see NotificationHandler#remove(int)
+     */
+    public synchronized void remove(final int notificationID) {
+        mTaskQueue.add(new Runnable() {
+            @Override
+            public void run() {
+                mHandler.remove(notificationID);
+                mUpdatesMap.remove(notificationID);
+            }
+        });
+
+        // If mReady == false, we haven't added any notifications yet. That can happen if Fennec is being
+        // started in response to clicking a notification. Call bind() to ensure the task we posted above is run.
+        if (!mReady) {
+            bind();
+        }
+
+        notify();
+    }
+
+    /**
+     * Determines whether a notification is showing progress.
+     *
+     * @see NotificationHandler#isProgressStyle(int)
+     */
+    public boolean isOngoing(int notificationID) {
+        final NotificationHandler handler = mHandler;
+        return handler != null && handler.isOngoing(notificationID);
+    }
+
+    protected void bind() {
+        mReady = true;
+    }
+
+    protected void unbind() {
+        mReady = false;
+        mUpdatesMap.clear();
+    }
+
+    protected void connectHandler(NotificationHandler handler) {
+        mHandler = handler;
+        new Thread(new NotificationRunnable()).start();
+    }
+
+    private class NotificationRunnable implements Runnable {
+        @Override
+        public void run() {
+            Runnable r;
+            try {
+                while (true) {
+                    // Synchronize polls to prevent tasks from being added to the queue
+                    // during the isDone check.
+                    synchronized (NotificationClient.this) {
+                        r = mTaskQueue.poll();
+                        while (r == null) {
+                            if (mHandler.isDone()) {
+                                unbind();
+                                return;
+                            }
+                            NotificationClient.this.wait();
+                            r = mTaskQueue.poll();
+                        }
+                    }
+                    r.run();
+                }
+            } catch (InterruptedException e) {
+                Log.e(LOGTAG, "Notification task queue processing interrupted", e);
+            }
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NotificationHandler.java
@@ -0,0 +1,193 @@
+/* -*- 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.graphics.Bitmap;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationManagerCompat;
+import org.mozilla.gecko.gfx.BitmapUtils;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+public class NotificationHandler {
+    private static String LOGTAG = "GeckoNotifHandler";
+    private final ConcurrentHashMap<Integer, Notification>
+            mNotifications = new ConcurrentHashMap<Integer, Notification>();
+    private final Context mContext;
+    private final NotificationManagerCompat mNotificationManager;
+
+    /**
+     * Notification associated with this service's foreground state.
+     *
+     * {@link android.app.Service#startForeground(int, android.app.Notification)}
+     * associates the foreground with exactly one notification from the service.
+     * To keep Fennec alive during downloads (and to make sure it can be killed
+     * once downloads are complete), we make sure that the foreground is always
+     * associated with an active progress notification if and only if at least
+     * one download is in progress.
+     */
+    private Notification mForegroundNotification;
+    private int mForegroundNotificationId;
+
+    public NotificationHandler(Context context) {
+        mContext = context;
+        mNotificationManager = NotificationManagerCompat.from(context);
+    }
+
+    /**
+     * Adds a notification.
+     *
+     * @param notificationID the unique ID of the notification
+     * @param aImageUrl      URL of the image to use
+     * @param aAlertTitle    title of the notification
+     * @param aAlertText     text of the notification
+     * @param contentIntent  Intent used when the notification is clicked
+     */
+    public void add(final int notificationID, String aImageUrl, String aHost, String aAlertTitle,
+                    String aAlertText, PendingIntent contentIntent) {
+        // Remove the old notification with the same ID, if any
+        remove(notificationID);
+
+        final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext)
+                .setContentTitle(aAlertTitle)
+                .setContentText(aAlertText)
+                .setSmallIcon(R.drawable.ic_status_logo)
+                .setContentIntent(contentIntent)
+                .setAutoCancel(true)
+                .setStyle(new NotificationCompat.InboxStyle()
+                          .addLine(aAlertText)
+                          .setSummaryText(aHost));
+
+        // Fetch icon.
+        if (!aImageUrl.isEmpty()) {
+            final Bitmap image = BitmapUtils.decodeUrl(aImageUrl);
+            builder.setLargeIcon(image);
+        }
+
+        builder.setWhen(System.currentTimeMillis());
+        final Notification notification = builder.build();
+
+        mNotificationManager.notify(notificationID, notification);
+        mNotifications.put(notificationID, notification);
+    }
+
+    /**
+     * Adds a notification.
+     *
+     * @param id             the unique ID of the notification
+     * @param notification   the Notification to add
+     */
+    public void add(int id, Notification notification) {
+        mNotificationManager.notify(id, notification);
+        mNotifications.put(id, notification);
+
+        if (mForegroundNotification == null && isOngoing(notification)) {
+            setForegroundNotification(id, notification);
+        }
+    }
+
+    /**
+     * Updates a notification.
+     *
+     * @param notificationID ID of existing notification
+     * @param aProgress      progress of item being updated
+     * @param aProgressMax   max progress of item being updated
+     * @param aAlertText     text of the notification
+     */
+    public void update(int notificationID, long aProgress, long aProgressMax, String aAlertText) {
+        Notification notification = mNotifications.get(notificationID);
+        if (notification == null) {
+            return;
+        }
+
+        notification = new NotificationCompat.Builder(mContext)
+                .setContentText(aAlertText)
+                .setSmallIcon(notification.icon)
+                .setWhen(notification.when)
+                .setContentIntent(notification.contentIntent)
+                .setProgress((int) aProgressMax, (int) aProgress, false)
+                .build();
+
+        add(notificationID, notification);
+    }
+
+    /**
+     * Removes a notification.
+     *
+     * @param notificationID ID of existing notification
+     */
+    public void remove(int notificationID) {
+        final Notification notification = mNotifications.remove(notificationID);
+        if (notification != null) {
+            updateForegroundNotification(notificationID, notification);
+        }
+        mNotificationManager.cancel(notificationID);
+    }
+
+    /**
+     * Determines whether the service is done.
+     *
+     * The service is considered finished when all notifications have been
+     * removed.
+     *
+     * @return whether all notifications have been removed
+     */
+    public boolean isDone() {
+        return mNotifications.isEmpty();
+    }
+
+    /**
+     * Determines whether a notification should hold a foreground service to keep Gecko alive
+     *
+     * @param notificationID the id of the notification to check
+     * @return               whether the notification is ongoing
+     */
+    public boolean isOngoing(int notificationID) {
+        final Notification notification = mNotifications.get(notificationID);
+        return isOngoing(notification);
+    }
+
+    /**
+     * Determines whether a notification should hold a foreground service to keep Gecko alive
+     *
+     * @param notification   the notification to check
+     * @return               whether the notification is ongoing
+     */
+    public boolean isOngoing(Notification notification) {
+        if (notification != null && (notification.flags & Notification.FLAG_ONGOING_EVENT) > 0) {
+            return true;
+        }
+        return false;
+    }
+
+    protected void setForegroundNotification(int id, Notification notification) {
+        mForegroundNotificationId = id;
+        mForegroundNotification = notification;
+    }
+
+    private void updateForegroundNotification(int oldId, Notification oldNotification) {
+        if (mForegroundNotificationId == oldId) {
+            // If we're removing the notification associated with the
+            // foreground, we need to pick another active notification to act
+            // as the foreground notification.
+            Notification foregroundNotification = null;
+            int foregroundId = 0;
+            for (final Integer id : mNotifications.keySet()) {
+                final Notification notification = mNotifications.get(id);
+                if (isOngoing(notification)) {
+                    foregroundNotification = notification;
+                    foregroundId = id;
+                    break;
+                }
+            }
+            setForegroundNotification(foregroundId, foregroundNotification);
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NotificationService.java
@@ -0,0 +1,51 @@
+/* -*- 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.app.Notification;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+
+public class NotificationService extends Service {
+    private final IBinder mBinder = new NotificationBinder();
+    private NotificationHandler mHandler;
+
+    @Override
+    public void onCreate() {
+        // This has to be initialized in onCreate in order to ensure that the NotificationHandler can
+        // access the NotificationManager service.
+        mHandler = new NotificationHandler(this) {
+            @Override
+            protected void setForegroundNotification(int id, Notification notification) {
+                super.setForegroundNotification(id, notification);
+
+                if (notification == null) {
+                    stopForeground(true);
+                } else {
+                    startForeground(id, notification);
+                }
+            }
+        };
+    }
+
+    public class NotificationBinder extends Binder {
+        NotificationService getService() {
+            // Return this instance of NotificationService so clients can call public methods
+            return NotificationService.this;
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+
+    public NotificationHandler getNotificationHandler() {
+        return mHandler;
+    }
+}
new file mode 100644
--- /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")
+    private static native void nativeGetPrefs(String[] prefNames, PrefHandler handler);
+    @WrapForJNI(stubName = "SetPref")
+    private static native void nativeSetPref(String prefName, boolean flush, int type,
+                                             boolean boolVal, int intVal, String strVal);
+    @WrapForJNI(stubName = "AddObserver")
+    private static native void nativeAddObserver(String[] prefNames, PrefHandler handler,
+                                                 String[] prefsToObserve);
+    @WrapForJNI(stubName = "RemoveObserver")
+    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
+    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
+    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() {
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/ServiceNotificationClient.java
@@ -0,0 +1,71 @@
+/* -*- 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.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.util.Log;
+
+/**
+ * Client for posting notifications through the NotificationService.
+ */
+public class ServiceNotificationClient extends NotificationClient {
+    private static final String LOGTAG = "GeckoServiceNotificationClient";
+
+    private final ServiceConnection mConnection = new NotificationServiceConnection();
+    private boolean mBound;
+    private final Context mContext;
+
+    public ServiceNotificationClient(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    protected void bind() {
+        super.bind();
+        final Intent intent = new Intent(mContext, NotificationService.class);
+        mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+    }
+
+    @Override
+    protected void unbind() {
+        // If there are no more tasks and no notifications being
+        // displayed, the service is disconnected. Unfortunately,
+        // since completed download notifications are shown by
+        // removing the progress notification and creating a new
+        // static one, this will cause the service to be unbound
+        // and immediately rebound when a download completes.
+        super.unbind();
+
+        if (mBound) {
+            mBound = false;
+            mContext.unbindService(mConnection);
+        }
+    }
+
+    class NotificationServiceConnection implements ServiceConnection {
+        @Override
+        public void onServiceConnected(ComponentName className, IBinder service) {
+            final NotificationService.NotificationBinder binder =
+                    (NotificationService.NotificationBinder) service;
+            connectHandler(binder.getService().getNotificationHandler());
+            mBound = true;
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName componentName) {
+            // This is called when the connection with the service has been
+            // unexpectedly disconnected -- that is, its process crashed.
+            // Because it is running in our same process, we should never
+            // see this happen, and the correctness of this class relies on
+            // this never happening.
+            Log.e(LOGTAG, "Notification service disconnected", new Exception());
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SmsManager.java
@@ -0,0 +1,41 @@
+/* -*- 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 SmsManager {
+    private static final ISmsManager sInstance;
+    static {
+        if (AppConstants.MOZ_WEBSMS_BACKEND) {
+            sInstance = new GeckoSmsManager();
+        } else {
+            sInstance = null;
+        }
+    }
+
+    public static ISmsManager getInstance() {
+        return sInstance;
+    }
+
+    public static boolean isEnabled() {
+        return AppConstants.MOZ_WEBSMS_BACKEND;
+    }
+
+    public interface ISmsManager {
+        void start();
+        void stop();
+        void shutdown();
+
+        void send(String aNumber, String aMessage, int aRequestId, boolean aShouldNotify);
+        void getMessage(int aMessageId, int aRequestId);
+        void deleteMessage(int aMessageId, int aRequestId);
+        void markMessageRead(int aMessageId, boolean aValue, boolean aSendReadReport, int aRequestId);
+        void createMessageCursor(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, String aDelivery, boolean aHasRead, boolean aRead, boolean aHasThreadId, long aThreadId, boolean aReverse, int aRequestId);
+        void createThreadCursor(int aRequestId);
+        void getNextThread(int aRequestId);
+        void getNextMessage(int aRequestId);
+    }
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SurfaceBits.java
@@ -0,0 +1,17 @@
+/* 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 java.nio.ByteBuffer;
+
+@WrapForJNI
+public class SurfaceBits {
+    public int width;
+    public int height;
+    public int format;
+    public ByteBuffer buffer;
+}
new file mode 100644
--- /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);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BitmapUtils.java
@@ -0,0 +1,466 @@
+/* -*- 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.lang.reflect.Field;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.util.GeckoJarReader;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.UIAsyncTask;
+
+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.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+
+public final class BitmapUtils {
+    /* Default colors. */
+    private static final float[] DEFAULT_LAUNCHER_ICON_HSV = { 32.0f, 1.0f, 1.0f };
+
+    private static final String LOGTAG = "GeckoBitmapUtils";
+
+    private BitmapUtils() {}
+
+    public interface BitmapLoader {
+        public void onBitmapFound(Drawable d);
+    }
+
+    private static void runOnBitmapFoundOnUiThread(final BitmapLoader loader, final Drawable d) {
+        if (ThreadUtils.isOnUiThread()) {
+            loader.onBitmapFound(d);
+            return;
+        }
+
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                loader.onBitmapFound(d);
+            }
+        });
+    }
+
+    /**
+     * Attempts to find a drawable associated with a given string, using its URI scheme to determine
+     * how to load the drawable. The BitmapLoader's `onBitmapFound` method is always called, and
+     * will be called with `null` if no drawable is found.
+     *
+     * The BitmapLoader `onBitmapFound` method always runs on the UI thread.
+     */
+    public static void getDrawable(final Context context, final String data, final BitmapLoader loader) {
+        if (TextUtils.isEmpty(data)) {
+            runOnBitmapFoundOnUiThread(loader, null);
+            return;
+        }
+
+        if (data.startsWith("data")) {
+            final BitmapDrawable d = new BitmapDrawable(context.getResources(), getBitmapFromDataURI(data));
+            runOnBitmapFoundOnUiThread(loader, d);
+            return;
+        }
+
+        if (data.startsWith("jar:") || data.startsWith("file://")) {
+            (new UIAsyncTask.WithoutParams<Drawable>(ThreadUtils.getBackgroundHandler()) {
+                @Override
+                public Drawable doInBackground() {
+                    try {
+                        if (data.startsWith("jar:jar")) {
+                            return GeckoJarReader.getBitmapDrawable(
+                                    context, context.getResources(), data);
+                        }
+
+                        // Don't attempt to validate the JAR signature when loading an add-on icon
+                        if (data.startsWith("jar:file")) {
+                            return GeckoJarReader.getBitmapDrawable(
+                                    context, context.getResources(), Uri.decode(data));
+                        }
+
+                        final URL url = new URL(data);
+                        final InputStream is = (InputStream) url.getContent();
+                        try {
+                            return Drawable.createFromStream(is, "src");
+                        } finally {
+                            is.close();
+                        }
+                    } catch (Exception e) {
+                        Log.w(LOGTAG, "Unable to set icon", e);
+                    }
+                    return null;
+                }
+
+                @Override
+                public void onPostExecute(Drawable drawable) {
+                    loader.onBitmapFound(drawable);
+                }
+            }).execute();
+            return;
+        }
+
+        if (data.startsWith("-moz-icon://")) {
+            final Uri imageUri = Uri.parse(data);
+            final String ssp = imageUri.getSchemeSpecificPart();
+            final String resource = ssp.substring(ssp.lastIndexOf('/') + 1);
+
+            try {
+                final Drawable d = context.getPackageManager().getApplicationIcon(resource);
+                runOnBitmapFoundOnUiThread(loader, d);
+            } catch (Exception ex) { }
+
+            return;
+        }
+
+        if (data.startsWith("drawable://")) {
+            final Uri imageUri = Uri.parse(data);
+            final int id = getResource(imageUri, R.drawable.ic_status_logo);
+            final Drawable d = context.getResources().getDrawable(id);
+
+            runOnBitmapFoundOnUiThread(loader, d);
+            return;
+        }
+
+        runOnBitmapFoundOnUiThread(loader, null);
+    }
+
+    public static Bitmap decodeByteArray(byte[] bytes) {
+        return decodeByteArray(bytes, null);
+    }
+
+    public static Bitmap decodeByteArray(byte[] bytes, BitmapFactory.Options options) {
+        return decodeByteArray(bytes, 0, bytes.length, options);
+    }
+
+    public static Bitmap decodeByteArray(byte[] bytes, int offset, int length) {
+        return decodeByteArray(bytes, offset, length, null);
+    }
+
+    public static Bitmap decodeByteArray(byte[] bytes, int offset, int length, BitmapFactory.Options options) {
+        if (bytes.length <= 0) {
+            throw new IllegalArgumentException("bytes.length " + bytes.length
+                                               + " must be a positive number");
+        }
+
+        Bitmap bitmap = null;
+        try {
+            bitmap = BitmapFactory.decodeByteArray(bytes, offset, length, options);
+        } catch (OutOfMemoryError e) {
+            Log.e(LOGTAG, "decodeByteArray(bytes.length=" + bytes.length
+                          + ", options= " + options + ") OOM!", e);
+            return null;
+        }
+
+        if (bitmap == null) {
+            Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned null");
+            return null;
+        }
+
+        if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
+            Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned "
+                          + "a bitmap with dimensions " + bitmap.getWidth()
+                          + "x" + bitmap.getHeight());
+            return null;
+        }
+
+        return bitmap;
+    }
+
+    public static Bitmap decodeStream(InputStream inputStream) {
+        try {
+            return BitmapFactory.decodeStream(inputStream);
+        } catch (OutOfMemoryError e) {
+            Log.e(LOGTAG, "decodeStream() OOM!", e);
+            return null;
+        }
+    }
+
+    public static Bitmap decodeUrl(Uri uri) {
+        return decodeUrl(uri.toString());
+    }
+
+    public static Bitmap decodeUrl(String urlString) {
+        URL url;
+
+        try {
+            url = new URL(urlString);
+        } catch (MalformedURLException e) {
+            Log.w(LOGTAG, "decodeUrl: malformed URL " + urlString);
+            return null;
+        }
+
+        return decodeUrl(url);
+    }
+
+    public static Bitmap decodeUrl(URL url) {
+        InputStream stream = null;
+
+        try {
+            stream = url.openStream();
+        } catch (IOException e) {
+            Log.w(LOGTAG, "decodeUrl: IOException downloading " + url);
+            return null;
+        }
+
+        if (stream == null) {
+            Log.w(LOGTAG, "decodeUrl: stream not found downloading " + url);
+            return null;
+        }
+
+        Bitmap bitmap = decodeStream(stream);
+
+        try {
+            stream.close();
+        } catch (IOException e) {
+            Log.w(LOGTAG, "decodeUrl: IOException closing stream " + url, e);
+        }
+
+        return bitmap;
+    }
+
+    public static Bitmap decodeResource(Context context, int id) {
+        return decodeResource(context, id, null);
+    }
+
+    public static Bitmap decodeResource(Context context, int id, BitmapFactory.Options options) {
+        Resources resources = context.getResources();
+        try {
+            return BitmapFactory.decodeResource(resources, id, options);
+        } catch (OutOfMemoryError e) {
+            Log.e(LOGTAG, "decodeResource() OOM! Resource id=" + id, e);
+            return null;
+        }
+    }
+
+    public static int getDominantColor(Bitmap source) {
+        return getDominantColor(source, true);
+    }
+
+    public static int getDominantColor(Bitmap source, boolean applyThreshold) {
+      if (source == null)
+        return Color.argb(255, 255, 255, 255);
+
+      // Keep track of how many times a hue in a given bin appears in the image.
+      // Hue values range [0 .. 360), so dividing by 10, we get 36 bins.
+      int[] colorBins = new int[36];
+
+      // The bin with the most colors. Initialize to -1 to prevent accidentally
+      // thinking the first bin holds the dominant color.
+      int maxBin = -1;
+
+      // Keep track of sum hue/saturation/value per hue bin, which we'll use to
+      // compute an average to for the dominant color.
+      float[] sumHue = new float[36];
+      float[] sumSat = new float[36];
+      float[] sumVal = new float[36];
+      float[] hsv = new float[3];
+
+      int height = source.getHeight();
+      int width = source.getWidth();
+      int[] pixels = new int[width * height];
+      source.getPixels(pixels, 0, width, 0, 0, width, height);
+      for (int row = 0; row < height; row++) {
+        for (int col = 0; col < width; col++) {
+          int c = pixels[col + row * width];
+          // Ignore pixels with a certain transparency.
+          if (Color.alpha(c) < 128)
+            continue;
+
+          Color.colorToHSV(c, hsv);
+
+          // If a threshold is applied, ignore arbitrarily chosen values for "white" and "black".
+          if (applyThreshold && (hsv[1] <= 0.35f || hsv[2] <= 0.35f))
+            continue;
+
+          // We compute the dominant color by putting colors in bins based on their hue.
+          int bin = (int) Math.floor(hsv[0] / 10.0f);
+
+          // Update the sum hue/saturation/value for this bin.
+          sumHue[bin] = sumHue[bin] + hsv[0];
+          sumSat[bin] = sumSat[bin] + hsv[1];
+          sumVal[bin] = sumVal[bin] + hsv[2];
+
+          // Increment the number of colors in this bin.
+          colorBins[bin]++;
+
+          // Keep track of the bin that holds the most colors.
+          if (maxBin < 0 || colorBins[bin] > colorBins[maxBin])
+            maxBin = bin;
+        }
+      }
+
+      // maxBin may never get updated if the image holds only transparent and/or black/white pixels.
+      if (maxBin < 0)
+        return Color.argb(255, 255, 255, 255);
+
+      // Return a color with the average hue/saturation/value of the bin with the most colors.
+      hsv[0] = sumHue[maxBin] / colorBins[maxBin];
+      hsv[1] = sumSat[maxBin] / colorBins[maxBin];
+      hsv[2] = sumVal[maxBin] / colorBins[maxBin];
+      return Color.HSVToColor(hsv);
+    }
+
+    /**
+     * Decodes a bitmap from a Base64 data URI.
+     *
+     * @param dataURI a Base64-encoded data URI string
+     * @return        the decoded bitmap, or null if the data URI is invalid
+     */
+    public static Bitmap getBitmapFromDataURI(String dataURI) {
+        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(Uri resourceUrl, int defaultIcon) {
+        int icon = defaultIcon;
+
+        final String scheme = resourceUrl.getScheme();
+        if ("drawable".equals(scheme)) {
+            String resource = resourceUrl.getSchemeSpecificPart();
+            resource = resource.substring(resource.lastIndexOf('/') + 1);
+
+            try {
+                return Integer.parseInt(resource);
+            } catch (NumberFormatException ex) {
+                // This isn't a resource id, try looking for a string
+            }
+
+            try {
+                final Class<R.drawable> drawableClass = R.drawable.class;
+                final Field f = drawableClass.getField(resource);
+                icon = f.getInt(null);
+            } catch (final NoSuchFieldException e1) {
+
+                // just means the resource doesn't exist for fennec. Check in Android resources
+                try {
+                    final Class<android.R.drawable> drawableClass = android.R.drawable.class;
+                    final Field f = drawableClass.getField(resource);
+                    icon = f.getInt(null);
+                } catch (final NoSuchFieldException e2) {
+                    // This drawable doesn't seem to exist...
+                } catch (Exception e3) {
+                    Log.i(LOGTAG, "Exception getting drawable", e3);
+                }
+
+            } catch (Exception e4) {
+              Log.i(LOGTAG, "Exception getting drawable", e4);
+            }
+
+            resourceUrl = null;
+        }
+        return icon;
+    }
+
+    public static Bitmap getLauncherIcon(Context context, Bitmap aSource, int size) {
+        final int kOffset = 6;
+        final int kRadius = 5;
+        int insetSize = aSource != null ? size * 2 / 3 : size;
+
+        Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(bitmap);
+
+
+        // draw a base color
+        Paint paint = new Paint();
+        if (aSource == null) {
+            // If we aren't drawing a favicon, just use an orange color.
+            paint.setColor(Color.HSVToColor(DEFAULT_LAUNCHER_ICON_HSV));
+            canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint);
+        } else if (aSource.getWidth() >= insetSize || aSource.getHeight() >= insetSize) {
+            // Otherwise, if the icon is large enough, just draw it.
+            Rect iconBounds = new Rect(0, 0, size, size);
+            canvas.drawBitmap(aSource, null, iconBounds, null);
+            return bitmap;
+        } else {
+            // otherwise use the dominant color from the icon + a layer of transparent white to lighten it somewhat
+            int color = BitmapUtils.getDominantColor(aSource);
+            paint.setColor(color);
+            canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint);
+            paint.setColor(Color.argb(100, 255, 255, 255));
+            canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint);
+        }
+
+        // draw the overlay
+        Bitmap overlay = BitmapUtils.decodeResource(context, R.drawable.home_bg);
+        canvas.drawBitmap(overlay, null, new Rect(0, 0, size, size), null);
+
+        // draw the favicon
+        if (aSource == null)
+            aSource = BitmapUtils.decodeResource(context, R.drawable.home_star);
+
+        // by default, we scale the icon to this size
+        int sWidth = insetSize / 2;
+        int sHeight = sWidth;
+
+        int halfSize = size / 2;
+        canvas.drawBitmap(aSource,
+                null,
+                new Rect(halfSize - sWidth,
+                        halfSize - sHeight,
+                        halfSize + sWidth,
+                        halfSize + sHeight),
+                null);
+
+        return bitmap;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java
@@ -0,0 +1,771 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.util.FloatUtils;
+
+import org.json.JSONArray;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+
+final class DisplayPortCalculator {
+    private static final String LOGTAG = "GeckoDisplayPort";
+    private static final PointF ZERO_VELOCITY = new PointF(0, 0);
+
+    private static final String PREF_DISPLAYPORT_STRATEGY = "gfx.displayport.strategy";
+    private static final String PREF_DISPLAYPORT_FM_MULTIPLIER = "gfx.displayport.strategy_fm.multiplier";
+    private static final String PREF_DISPLAYPORT_FM_DANGER_X = "gfx.displayport.strategy_fm.danger_x";
+    private static final String PREF_DISPLAYPORT_FM_DANGER_Y = "gfx.displayport.strategy_fm.danger_y";
+    private static final String PREF_DISPLAYPORT_VB_MULTIPLIER = "gfx.displayport.strategy_vb.multiplier";
+    private static final String PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_vb.threshold";
+    private static final String PREF_DISPLAYPORT_VB_REVERSE_BUFFER = "gfx.displayport.strategy_vb.reverse_buffer";
+    private static final String PREF_DISPLAYPORT_VB_DANGER_X_BASE = "gfx.displayport.strategy_vb.danger_x_base";
+    private static final String PREF_DISPLAYPORT_VB_DANGER_Y_BASE = "gfx.displayport.strategy_vb.danger_y_base";
+    private static final String PREF_DISPLAYPORT_VB_DANGER_X_INCR = "gfx.displayport.strategy_vb.danger_x_incr";
+    private static final String PREF_DISPLAYPORT_VB_DANGER_Y_INCR = "gfx.displayport.strategy_vb.danger_y_incr";
+    private static final String PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_pb.threshold";
+
+    private static DisplayPortStrategy sStrategy = new VelocityBiasStrategy(null);
+
+    static DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+        return sStrategy.calculate(metrics, (velocity == null ? ZERO_VELOCITY : velocity));
+    }
+
+    static boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+        if (displayPort == null) {
+            return true;
+        }
+        return sStrategy.aboutToCheckerboard(metrics, (velocity == null ? ZERO_VELOCITY : velocity), displayPort);
+    }
+
+    static boolean drawTimeUpdate(long millis, int pixels) {
+        return sStrategy.drawTimeUpdate(millis, pixels);
+    }
+
+    static void resetPageState() {
+        sStrategy.resetPageState();
+    }
+
+    static void initPrefs() {
+        final String[] prefs = { PREF_DISPLAYPORT_STRATEGY,
+                                 PREF_DISPLAYPORT_FM_MULTIPLIER,
+                                 PREF_DISPLAYPORT_FM_DANGER_X,
+                                 PREF_DISPLAYPORT_FM_DANGER_Y,
+                                 PREF_DISPLAYPORT_VB_MULTIPLIER,
+                                 PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD,
+                                 PREF_DISPLAYPORT_VB_REVERSE_BUFFER,
+                                 PREF_DISPLAYPORT_VB_DANGER_X_BASE,
+                                 PREF_DISPLAYPORT_VB_DANGER_Y_BASE,
+                                 PREF_DISPLAYPORT_VB_DANGER_X_INCR,
+                                 PREF_DISPLAYPORT_VB_DANGER_Y_INCR,
+                                 PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD };
+
+        PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() {
+            private final Map<String, Integer> mValues = new HashMap<String, Integer>();
+
+            @Override public void prefValue(String pref, int value) {
+                mValues.put(pref, value);
+            }
+
+            @Override public void finish() {
+                setStrategy(mValues);
+            }
+        });
+    }
+
+    /**
+     * Set the active strategy to use.
+     * See the gfx.displayport.strategy pref in mobile/android/app/mobile.js to see the
+     * mapping between ints and strategies.
+     */
+    static boolean setStrategy(Map<String, Integer> prefs) {
+        Integer strategy = prefs.get(PREF_DISPLAYPORT_STRATEGY);
+        if (strategy == null) {
+            return false;
+        }
+
+        switch (strategy) {
+            case 0:
+                sStrategy = new FixedMarginStrategy(prefs);
+                break;
+            case 1:
+                sStrategy = new VelocityBiasStrategy(prefs);
+                break;
+            case 2:
+                sStrategy = new DynamicResolutionStrategy(prefs);
+                break;
+            case 3:
+                sStrategy = new NoMarginStrategy(prefs);
+                break;
+            case 4:
+                sStrategy = new PredictionBiasStrategy(prefs);
+                break;
+            default:
+                Log.e(LOGTAG, "Invalid strategy index specified");
+                return false;
+        }
+        Log.i(LOGTAG, "Set strategy " + sStrategy.toString());
+        return true;
+    }
+
+    private static float getFloatPref(Map<String, Integer> prefs, String prefName, int defaultValue) {
+        Integer value = (prefs == null ? null : prefs.get(prefName));
+        return (value == null || value < 0 ? defaultValue : value) / 1000f;
+    }
+
+    private static abstract class DisplayPortStrategy {
+        /** Calculates a displayport given a viewport and panning velocity. */
+        public abstract DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity);
+        /** Returns true if a checkerboard is about to be visible and we should not throttle drawing. */
+        public abstract boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort);
+        /** Notify the strategy of a new recorded draw time. Return false to turn off draw time recording. */
+        public boolean drawTimeUpdate(long millis, int pixels) { return false; }
+        /** Reset any page-specific state stored, as the page being displayed has changed. */
+        public void resetPageState() {}
+    }
+
+    /**
+     * Return the dimensions for a rect that has area (width*height) that does not exceed the page size in the
+     * given metrics object. The area in the returned FloatSize may be less than width*height if the page is
+     * small, but it will never be larger than width*height.
+     * Note that this process may change the relative aspect ratio of the given dimensions.
+     */
+    private static FloatSize reshapeForPage(float width, float height, ImmutableViewportMetrics metrics) {
+        // figure out how much of the desired buffer amount we can actually use on the horizontal axis
+        float usableWidth = Math.min(width, metrics.getPageWidth());
+        // if we reduced the buffer amount on the horizontal axis, we should take that saved memory and
+        // use it on the vertical axis
+        float extraUsableHeight = (float)Math.floor(((width - usableWidth) * height) / usableWidth);
+        float usableHeight = Math.min(height + extraUsableHeight, metrics.getPageHeight());
+        if (usableHeight < height && usableWidth == width) {
+            // and the reverse - if we shrunk the buffer on the vertical axis we can add it to the horizontal
+            float extraUsableWidth = (float)Math.floor(((height - usableHeight) * width) / usableHeight);
+            usableWidth = Math.min(width + extraUsableWidth, metrics.getPageWidth());
+        }
+        return new FloatSize(usableWidth, usableHeight);
+    }
+
+    /**
+     * Expand the given rect in all directions by a "danger zone". The size of the danger zone on an axis
+     * is the size of the view on that axis multiplied by the given multiplier. The expanded rect is then
+     * clamped to page bounds and returned.
+     */
+    private static RectF expandByDangerZone(RectF rect, float dangerZoneXMultiplier, float dangerZoneYMultiplier, ImmutableViewportMetrics metrics) {
+        // calculate the danger zone amounts in pixels
+        float dangerZoneX = metrics.getWidth() * dangerZoneXMultiplier;
+        float dangerZoneY = metrics.getHeight() * dangerZoneYMultiplier;
+        rect = RectUtils.expand(rect, dangerZoneX, dangerZoneY);
+        // clamp to page bounds
+        return clampToPageBounds(rect, metrics);
+    }
+
+    /**
+     * Calculate the display port by expanding the viewport by the specified
+     * margins, then clamping to the page size.
+     */
+    private static DisplayPortMetrics getPageClampedDisplayPortMetrics(RectF margins, float zoom, ImmutableViewportMetrics metrics) {
+        float left = metrics.viewportRectLeft - margins.left;
+        float top = metrics.viewportRectTop - margins.top;
+        float right = metrics.viewportRectRight() + margins.right;
+        float bottom = metrics.viewportRectBottom() + margins.bottom;
+        left = Math.max(metrics.pageRectLeft, left);
+        top = Math.max(metrics.pageRectTop, top);
+        right = Math.min(metrics.pageRectRight, right);
+        bottom = Math.min(metrics.pageRectBottom, bottom);
+
+        return new DisplayPortMetrics(left, top, right, bottom, zoom);
+    }
+
+    /**
+     * Adjust the given margins so if they are applied on the viewport in the metrics, the resulting rect
+     * does not exceed the page bounds. This code will maintain the total margin amount for a given axis;
+     * it assumes that margins.left + metrics.getWidth() + margins.right is less than or equal to
+     * metrics.getPageWidth(); and the same for the y axis.
+     */
+    private static RectF shiftMarginsForPageBounds(RectF margins, ImmutableViewportMetrics metrics) {
+        // check how much we're overflowing in each direction. note that at most one of leftOverflow
+        // and rightOverflow can be greater than zero, and at most one of topOverflow and bottomOverflow
+        // can be greater than zero, because of the assumption described in the method javadoc.
+        float leftOverflow = metrics.pageRectLeft - (metrics.viewportRectLeft - margins.left);
+        float rightOverflow = (metrics.viewportRectRight() + margins.right) - metrics.pageRectRight;
+        float topOverflow = metrics.pageRectTop - (metrics.viewportRectTop - margins.top);
+        float bottomOverflow = (metrics.viewportRectBottom() + margins.bottom) - metrics.pageRectBottom;
+
+        // if the margins overflow the page bounds, shift them to other side on the same axis
+        if (leftOverflow > 0) {
+            margins.left -= leftOverflow;
+            margins.right += leftOverflow;
+        } else if (rightOverflow > 0) {
+            margins.right -= rightOverflow;
+            margins.left += rightOverflow;
+        }
+        if (topOverflow > 0) {
+            margins.top -= topOverflow;
+            margins.bottom += topOverflow;
+        } else if (bottomOverflow > 0) {
+            margins.bottom -= bottomOverflow;
+            margins.top += bottomOverflow;
+        }
+        return margins;
+    }
+
+    /**
+     * Clamp the given rect to the page bounds and return it.
+     */
+    private static RectF clampToPageBounds(RectF rect, ImmutableViewportMetrics metrics) {
+        if (rect.top < metrics.pageRectTop) rect.top = metrics.pageRectTop;
+        if (rect.left < metrics.pageRectLeft) rect.left = metrics.pageRectLeft;
+        if (rect.right > metrics.pageRectRight) rect.right = metrics.pageRectRight;
+        if (rect.bottom > metrics.pageRectBottom) rect.bottom = metrics.pageRectBottom;
+        return rect;
+    }
+
+    /**
+     * This class implements the variation where we basically don't bother with a a display port.
+     */
+    private static class NoMarginStrategy extends DisplayPortStrategy {
+        NoMarginStrategy(Map<String, Integer> prefs) {
+            // no prefs in this strategy
+        }
+
+        @Override
+        public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+            return new DisplayPortMetrics(metrics.viewportRectLeft,
+                    metrics.viewportRectTop,
+                    metrics.viewportRectRight(),
+                    metrics.viewportRectBottom(),
+                    metrics.zoomFactor);
+        }
+
+        @Override
+        public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+            return true;
+        }
+
+        @Override
+        public String toString() {
+            return "NoMarginStrategy";
+        }
+    }
+
+    /**
+     * This class implements the variation where we use a fixed-size margin on the display port.
+     * The margin is always 300 pixels in all directions, except when we are (a) approaching a page
+     * boundary, and/or (b) if we are limited by the page size. In these cases we try to maintain
+     * the area of the display port by (a) shifting the buffer to the other side on the same axis,
+     * and/or (b) increasing the buffer on the other axis to compensate for the reduced buffer on
+     * one axis.
+     */
+    private static class FixedMarginStrategy extends DisplayPortStrategy {
+        // The length of each axis of the display port will be the corresponding view length
+        // multiplied by this factor.
+        private final float SIZE_MULTIPLIER;
+
+        // If the visible rect is within the danger zone (measured as a fraction of the view size
+        // from the edge of the displayport) we start redrawing to minimize checkerboarding.
+        private final float DANGER_ZONE_X_MULTIPLIER;
+        private final float DANGER_ZONE_Y_MULTIPLIER;
+
+        FixedMarginStrategy(Map<String, Integer> prefs) {
+            SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_MULTIPLIER, 2000);
+            DANGER_ZONE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_X, 100);
+            DANGER_ZONE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_Y, 200);
+        }
+
+        @Override
+        public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+            float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
+            float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
+
+            // we need to avoid having a display port that is larger than the page, or we will end up
+            // painting things outside the page bounds (bug 729169). we simultaneously need to make
+            // the display port as large as possible so that we redraw less. reshape the display
+            // port dimensions to accomplish this.
+            FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
+            float horizontalBuffer = usableSize.width - metrics.getWidth();
+            float verticalBuffer = usableSize.height - metrics.getHeight();
+
+            // and now calculate the display port margins based on how much buffer we've decided to use and
+            // the page bounds, ensuring we use all of the available buffer amounts on one side or the other
+            // on any given axis. (i.e. if we're scrolled to the top of the page, the vertical buffer is
+            // entirely below the visible viewport, but if we're halfway down the page, the vertical buffer
+            // is split).
+            RectF margins = new RectF();
+            margins.left = horizontalBuffer / 2.0f;
+            margins.right = horizontalBuffer - margins.left;
+            margins.top = verticalBuffer / 2.0f;
+            margins.bottom = verticalBuffer - margins.top;
+            margins = shiftMarginsForPageBounds(margins, metrics);
+
+            return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+        }
+
+        @Override
+        public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+            // Increase the size of the viewport based on the danger zone multiplier (and clamp to page
+            // boundaries), and intersect it with the current displayport to determine whether we're
+            // close to checkerboarding.
+            RectF adjustedViewport = expandByDangerZone(metrics.getViewport(), DANGER_ZONE_X_MULTIPLIER, DANGER_ZONE_Y_MULTIPLIER, metrics);
+            return !displayPort.contains(adjustedViewport);
+        }
+
+        @Override
+        public String toString() {
+            return "FixedMarginStrategy mult=" + SIZE_MULTIPLIER + ", dangerX=" + DANGER_ZONE_X_MULTIPLIER + ", dangerY=" + DANGER_ZONE_Y_MULTIPLIER;
+        }
+    }
+
+    /**
+     * This class implements the variation with a small fixed-size margin with velocity bias.
+     * In this variation, the default margins are pretty small relative to the view size, but
+     * they are affected by the panning velocity. Specifically, if we are panning on one axis,
+     * we remove the margins on the other axis because we are likely axis-locked. Also once
+     * we are panning in one direction above a certain threshold velocity, we shift the buffer
+     * so that it is almost entirely in the direction of the pan, with a little bit in the
+     * reverse direction.
+     */
+    private static class VelocityBiasStrategy extends DisplayPortStrategy {
+        // The length of each axis of the display port will be the corresponding view length
+        // multiplied by this factor.
+        private final float SIZE_MULTIPLIER;
+        // The velocity above which we apply the velocity bias
+        private final float VELOCITY_THRESHOLD;
+        // How much of the buffer to keep in the reverse direction of the velocity
+        private final float REVERSE_BUFFER;
+        // If the visible rect is within the danger zone we start redrawing to minimize
+        // checkerboarding. the danger zone amount is a linear function of the form:
+        //    viewportsize * (base + velocity * incr)
+        // where base and incr are configurable values.
+        private final float DANGER_ZONE_BASE_X_MULTIPLIER;
+        private final float DANGER_ZONE_BASE_Y_MULTIPLIER;
+        private final float DANGER_ZONE_INCR_X_MULTIPLIER;
+        private final float DANGER_ZONE_INCR_Y_MULTIPLIER;
+
+        VelocityBiasStrategy(Map<String, Integer> prefs) {
+            SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_MULTIPLIER, 2000);
+            VELOCITY_THRESHOLD = GeckoAppShell.getDpi() * getFloatPref(prefs, PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD, 32);
+            REVERSE_BUFFER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_REVERSE_BUFFER, 200);
+            DANGER_ZONE_BASE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_BASE, 1000);
+            DANGER_ZONE_BASE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_BASE, 1000);
+            DANGER_ZONE_INCR_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_INCR, 0);
+            DANGER_ZONE_INCR_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_INCR, 0);
+        }
+
+        /**
+         * Split the given amounts into margins based on the VELOCITY_THRESHOLD and REVERSE_BUFFER values.
+         * If the velocity is above the VELOCITY_THRESHOLD on an axis, split the amount into REVERSE_BUFFER
+         * and 1.0 - REVERSE_BUFFER fractions. The REVERSE_BUFFER fraction is set as the margin in the
+         * direction opposite to the velocity, and the remaining fraction is set as the margin in the direction
+         * of the velocity. If the velocity is lower than VELOCITY_THRESHOLD, split the amount evenly into the
+         * two margins on that axis.
+         */
+        private RectF velocityBiasedMargins(float xAmount, float yAmount, PointF velocity) {
+            RectF margins = new RectF();
+
+            if (velocity.x > VELOCITY_THRESHOLD) {
+                margins.left = xAmount * REVERSE_BUFFER;
+            } else if (velocity.x < -VELOCITY_THRESHOLD) {
+                margins.left = xAmount * (1.0f - REVERSE_BUFFER);
+            } else {
+                margins.left = xAmount / 2.0f;
+            }
+            margins.right = xAmount - margins.left;
+
+            if (velocity.y > VELOCITY_THRESHOLD) {
+                margins.top = yAmount * REVERSE_BUFFER;
+            } else if (velocity.y < -VELOCITY_THRESHOLD) {
+                margins.top = yAmount * (1.0f - REVERSE_BUFFER);
+            } else {
+                margins.top = yAmount / 2.0f;
+            }
+            margins.bottom = yAmount - margins.top;
+
+            return margins;
+        }
+
+        @Override
+        public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+            float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
+            float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
+
+            // but if we're panning on one axis, set the margins for the other axis to zero since we are likely
+            // axis locked and won't be displaying that extra area.
+            if (Math.abs(velocity.x) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.y, 0)) {
+                displayPortHeight = metrics.getHeight();
+            } else if (Math.abs(velocity.y) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.x, 0)) {
+                displayPortWidth = metrics.getWidth();
+            }
+
+            // we need to avoid having a display port that is larger than the page, or we will end up
+            // painting things outside the page bounds (bug 729169).
+            displayPortWidth = Math.min(displayPortWidth, metrics.getPageWidth());
+            displayPortHeight = Math.min(displayPortHeight, metrics.getPageHeight());
+            float horizontalBuffer = displayPortWidth - metrics.getWidth();
+            float verticalBuffer = displayPortHeight - metrics.getHeight();
+
+            // split the buffer amounts into margins based on velocity, and shift it to
+            // take into account the page bounds
+            RectF margins = velocityBiasedMargins(horizontalBuffer, verticalBuffer, velocity);
+            margins = shiftMarginsForPageBounds(margins, metrics);
+
+            return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+        }
+
+        @Override
+        public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+            // calculate the danger zone amounts based on the prefs
+            float dangerZoneX = metrics.getWidth() * (DANGER_ZONE_BASE_X_MULTIPLIER + (velocity.x * DANGER_ZONE_INCR_X_MULTIPLIER));
+            float dangerZoneY = metrics.getHeight() * (DANGER_ZONE_BASE_Y_MULTIPLIER + (velocity.y * DANGER_ZONE_INCR_Y_MULTIPLIER));
+            // clamp it such that when added to the viewport, they don't exceed page size.
+            // this is a prerequisite to calling shiftMarginsForPageBounds as we do below.
+            dangerZoneX = Math.min(dangerZoneX, metrics.getPageWidth() - metrics.getWidth());
+            dangerZoneY = Math.min(dangerZoneY, metrics.getPageHeight() - metrics.getHeight());
+
+            // split the danger zone into margins based on velocity, and ensure it doesn't exceed
+            // page bounds.
+            RectF dangerMargins = velocityBiasedMargins(dangerZoneX, dangerZoneY, velocity);
+            dangerMargins = shiftMarginsForPageBounds(dangerMargins, metrics);
+
+            // we're about to checkerboard if the current viewport area + the danger zone margins
+            // fall out of the current displayport anywhere.
+            RectF adjustedViewport = new RectF(
+                    metrics.viewportRectLeft - dangerMargins.left,
+                    metrics.viewportRectTop - dangerMargins.top,
+                    metrics.viewportRectRight() + dangerMargins.right,
+                    metrics.viewportRectBottom() + dangerMargins.bottom);
+            return !displayPort.contains(adjustedViewport);
+        }
+
+        @Override
+        public String toString() {
+            return "VelocityBiasStrategy mult=" + SIZE_MULTIPLIER + ", threshold=" + VELOCITY_THRESHOLD + ", reverse=" + REVERSE_BUFFER
+                + ", dangerBaseX=" + DANGER_ZONE_BASE_X_MULTIPLIER + ", dangerBaseY=" + DANGER_ZONE_BASE_Y_MULTIPLIER
+                + ", dangerIncrX=" + DANGER_ZONE_INCR_Y_MULTIPLIER + ", dangerIncrY=" + DANGER_ZONE_INCR_Y_MULTIPLIER;
+        }
+    }
+
+    /**
+     * This class implements the variation where we draw more of the page at low resolution while panning.
+     * In this variation, as we pan faster, we increase the page area we are drawing, but reduce the draw
+     * resolution to compensate. This results in the same device-pixel area drawn; the compositor then
+     * scales this up to the viewport zoom level. This results in a large area of the page drawn but it
+     * looks blurry. The assumption is that drawing extra that we never display is better than checkerboarding,
+     * where we draw less but never even show it on the screen.
+     */
+    private static class DynamicResolutionStrategy extends DisplayPortStrategy {
+        // The length of each axis of the display port will be the corresponding view length
+        // multiplied by this factor.
+        private static final float SIZE_MULTIPLIER = 1.5f;
+
+        // The velocity above which we start zooming out the display port to keep up
+        // with the panning.
+        private static final float VELOCITY_EXPANSION_THRESHOLD = GeckoAppShell.getDpi() / 16f;
+
+        // How much we increase the display port based on velocity. Assuming no friction and
+        // splitting (see below), this should be be the number of frames (@60fps) between us
+        // calculating the display port and the draw of the *next* display port getting composited
+        // and displayed on the screen. This is because the timeline looks like this:
+        //      Java: pan pan pan pan pan pan ! pan pan pan pan pan pan !
+        //     Gecko:   \-> draw -> composite /   \-> draw -> composite /
+        // The display port calculated on the first "pan" gets composited to the screen at the
+        // first exclamation mark, and remains on the screen until the second exclamation mark.
+        // In order to avoid checkerboarding, that display port must be able to contain all of
+        // the panning until the second exclamation mark, which encompasses two entire draw/composite
+        // cycles.
+        // If we take into account friction, our velocity multiplier should be reduced as the
+        // amount of pan will decrease each time. If we take into account display port splitting,
+        // it should be increased as the splitting means some of the display port will be used to
+        // draw in the opposite direction of the velocity. For now I'm assuming these two cancel
+        // each other out.
+        private static final float VELOCITY_MULTIPLIER = 60.0f;
+
+        // The following constants adjust how biased the display port is in the direction of panning.
+        // When panning fast (above the FAST_THRESHOLD) we use the fast split factor to split the
+        // display port "buffer" area, otherwise we use the slow split factor. This is based on the
+        // assumption that if the user is panning fast, they are less likely to reverse directions
+        // and go backwards, so we should spend more of our display port buffer in the direction of
+        // panning.
+        private static final float VELOCITY_FAST_THRESHOLD = VELOCITY_EXPANSION_THRESHOLD * 2.0f;
+        private static final float FAST_SPLIT_FACTOR = 0.95f;
+        private static final float SLOW_SPLIT_FACTOR = 0.8f;
+
+        // The following constants are used for viewport prediction; we use them to estimate where
+        // the viewport will be soon and whether or not we should trigger a draw right now. "soon"
+        // in the previous sentence really refers to the amount of time it would take to draw and
+        // composite from the point at which we do the calculation, and that is not really a known
+        // quantity. The velocity multiplier is how much we multiply the velocity by; it has the
+        // same caveats as the VELOCITY_MULTIPLIER above except that it only needs to take into account
+        // one draw/composite cycle instead of two. The danger zone multiplier is a multiplier of the
+        // viewport size that we use as an extra "danger zone" around the viewport; if this danger
+        // zone falls outside the display port then we are approaching the point at which we will
+        // checkerboard, and hence should start drawing. Note that if DANGER_ZONE_MULTIPLIER is
+        // greater than (SIZE_MULTIPLIER - 1.0f), then at zero velocity we will always be in the
+        // danger zone, and thus will be constantly drawing.
+        private static final float PREDICTION_VELOCITY_MULTIPLIER = 30.0f;
+        private static final float DANGER_ZONE_MULTIPLIER = 0.20f; // must be less than (SIZE_MULTIPLIER - 1.0f)
+
+        DynamicResolutionStrategy(Map<String, Integer> prefs) {
+            // ignore prefs for now
+        }
+
+        @Override
+        public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+            float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
+            float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
+
+            // for resolution calculation purposes, we need to know what the adjusted display port dimensions
+            // would be if we had zero velocity, so calculate that here before we increase the display port
+            // based on velocity.
+            FloatSize reshapedSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
+
+            // increase displayPortWidth and displayPortHeight based on the velocity, but maintaining their
+            // relative aspect ratio.
+            if (velocity.length() > VELOCITY_EXPANSION_THRESHOLD) {
+                float velocityFactor = Math.max(Math.abs(velocity.x) / displayPortWidth,
+                                                Math.abs(velocity.y) / displayPortHeight);
+                velocityFactor *= VELOCITY_MULTIPLIER;
+
+                displayPortWidth += (displayPortWidth * velocityFactor);
+                displayPortHeight += (displayPortHeight * velocityFactor);
+            }
+
+            // at this point, displayPortWidth and displayPortHeight are how much of the page (in device pixels)
+            // we want to be rendered by Gecko. Note here "device pixels" is equivalent to CSS pixels multiplied
+            // by metrics.zoomFactor
+
+            // we need to avoid having a display port that is larger than the page, or we will end up
+            // painting things outside the page bounds (bug 729169). we simultaneously need to make
+            // the display port as large as possible so that we redraw less. reshape the display
+            // port dimensions to accomplish this. this may change the aspect ratio of the display port,
+            // but we are assuming that this is desirable because the advantages from pre-drawing will
+            // outweigh the disadvantages from any buffer reallocations that might occur.
+            FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
+            float horizontalBuffer = usableSize.width - metrics.getWidth();
+            float verticalBuffer = usableSize.height - metrics.getHeight();
+
+            // at this point, horizontalBuffer and verticalBuffer are the dimensions of the buffer area we have.
+            // the buffer area is the off-screen area that is part of the display port and will be pre-drawn in case
+            // the user scrolls there. we now need to split the buffer area on each axis so that we know
+            // what the exact margins on each side will be. first we split the buffer amount based on the direction
+            // we're moving, so that we have a larger buffer in the direction of travel.
+            RectF margins = new RectF();
+            margins.left = splitBufferByVelocity(horizontalBuffer, velocity.x);
+            margins.right = horizontalBuffer - margins.left;
+            margins.top = splitBufferByVelocity(verticalBuffer, velocity.y);
+            margins.bottom = verticalBuffer - margins.top;
+
+            // then, we account for running into the page bounds - so that if we hit the top of the page, we need
+            // to drop the top margin and move that amount to the bottom margin.
+            margins = shiftMarginsForPageBounds(margins, metrics);
+
+            // finally, we calculate the resolution we want to render the display port area at. We do this
+            // so that as we expand the display port area (because of velocity), we reduce the resolution of
+            // the painted area so as to maintain the size of the buffer Gecko is painting into. we calculate
+            // the reduction in resolution by comparing the display port size with and without the velocity
+            // changes applied.
+            // this effectively means that as we pan faster and faster, the display port grows, but we paint
+            // at lower resolutions. this paints more area to reduce checkerboard at the cost of increasing
+            // compositor-scaling and blurriness. Once we stop panning, the blurriness must be entirely gone.
+            // Note that usable* could be less than base* if we are pinch-zoomed out into overscroll, so we
+            // clamp it to make sure this doesn't increase our display resolution past metrics.zoomFactor.
+            float scaleFactor = Math.min(reshapedSize.width / usableSize.width, reshapedSize.height / usableSize.height);
+            float displayResolution = metrics.zoomFactor * Math.min(1.0f, scaleFactor);
+
+            DisplayPortMetrics dpMetrics = new DisplayPortMetrics(
+                metrics.viewportRectLeft - margins.left,
+                metrics.viewportRectTop - margins.top,
+                metrics.viewportRectRight() + margins.right,
+                metrics.viewportRectBottom() + margins.bottom,
+                displayResolution);
+            return dpMetrics;
+        }
+
+        /**
+         * Split the given buffer amount into two based on the velocity.
+         * Given an amount of total usable buffer on an axis, this will
+         * return the amount that should be used on the left/top side of
+         * the axis (the side which a negative velocity vector corresponds
+         * to).
+         */
+        private float splitBufferByVelocity(float amount, float velocity) {
+            // if no velocity, so split evenly
+            if (FloatUtils.fuzzyEquals(velocity, 0)) {
+                return amount / 2.0f;
+            }
+            // if we're moving quickly, assign more of the amount in that direction
+            // since is is less likely that we will reverse direction immediately
+            if (velocity < -VELOCITY_FAST_THRESHOLD) {
+                return amount * FAST_SPLIT_FACTOR;
+            }
+            if (velocity > VELOCITY_FAST_THRESHOLD) {
+                return amount * (1.0f - FAST_SPLIT_FACTOR);
+            }
+            // if we're moving slowly, then assign less of the amount in that direction
+            if (velocity < 0) {
+                return amount * SLOW_SPLIT_FACTOR;
+            } else {
+                return amount * (1.0f - SLOW_SPLIT_FACTOR);
+            }
+        }
+
+        @Override
+        public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+            // Expand the viewport based on our velocity (and clamp it to page boundaries).
+            // Then intersect it with the last-requested displayport to determine whether we're
+            // close to checkerboarding.
+
+            RectF predictedViewport = metrics.getViewport();
+
+            // first we expand the viewport in the direction we're moving based on some
+            // multiple of the current velocity.
+            if (velocity.length() > 0) {
+                if (velocity.x < 0) {
+                    predictedViewport.left += velocity.x * PREDICTION_VELOCITY_MULTIPLIER;
+                } else if (velocity.x > 0) {
+                    predictedViewport.right += velocity.x * PREDICTION_VELOCITY_MULTIPLIER;
+                }
+
+                if (velocity.y < 0) {
+                    predictedViewport.top += velocity.y * PREDICTION_VELOCITY_MULTIPLIER;
+                } else if (velocity.y > 0) {
+                    predictedViewport.bottom += velocity.y * PREDICTION_VELOCITY_MULTIPLIER;
+                }
+            }
+
+            // then we expand the viewport evenly in all directions just to have an extra
+            // safety zone. this also clamps it to page bounds.
+            predictedViewport = expandByDangerZone(predictedViewport, DANGER_ZONE_MULTIPLIER, DANGER_ZONE_MULTIPLIER, metrics);
+            return !displayPort.contains(predictedViewport);
+        }
+
+        @Override
+        public String toString() {
+            return "DynamicResolutionStrategy";
+        }
+    }
+
+    /**
+     * This class implements the variation where we use the draw time to predict where we will be when
+     * a draw completes, and draw that instead of where we are now. In this variation, when our panning
+     * speed drops below a certain threshold, we draw 9 viewports' worth of content so that the user can
+     * pan in any direction without encountering checkerboarding.
+     * Once the user is panning, we modify the displayport to encompass an area range of where we think
+     * the user will be when the draw completes. This heuristic relies on both the estimated draw time
+     * the panning velocity; unexpected changes in either of these values will cause the heuristic to
+     * fail and show checkerboard.
+     */
+    private static class PredictionBiasStrategy extends DisplayPortStrategy {
+        private static float VELOCITY_THRESHOLD;
+
+        private int mPixelArea;         // area of the viewport, used in draw time calculations
+        private int mMinFramesToDraw;   // minimum number of frames we take to draw
+        private int mMaxFramesToDraw;   // maximum number of frames we take to draw
+
+        PredictionBiasStrategy(Map<String, Integer> prefs) {
+            VELOCITY_THRESHOLD = GeckoAppShell.getDpi() * getFloatPref(prefs, PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD, 16);
+            resetPageState();
+        }
+
+        @Override
+        public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+            float width = metrics.getWidth();
+            float height = metrics.getHeight();
+            mPixelArea = (int)(width * height);
+
+            if (velocity.length() < VELOCITY_THRESHOLD) {
+                // if we're going slow, expand the displayport to 9x viewport size
+                RectF margins = new RectF(width, height, width, height);
+                return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+            }
+
+            // figure out how far we expect to be
+            float minDx = velocity.x * mMinFramesToDraw;
+            float minDy = velocity.y * mMinFramesToDraw;
+            float maxDx = velocity.x * mMaxFramesToDraw;
+            float maxDy = velocity.y * mMaxFramesToDraw;
+
+            // figure out how many pixels we will be drawing when we draw the above-calculated range.
+            // this will be larger than the viewport area.
+            float pixelsToDraw = (width + Math.abs(maxDx - minDx)) * (height + Math.abs(maxDy - minDy));
+            // adjust how far we will get because of the time spent drawing all these extra pixels. this
+            // will again increase the number of pixels drawn so really we could keep iterating this over
+            // and over, but once seems enough for now.
+            maxDx = maxDx * pixelsToDraw / mPixelArea;
+            maxDy = maxDy * pixelsToDraw / mPixelArea;
+
+            // and finally generate the displayport. the min/max stuff takes care of
+            // negative velocities as well as positive.
+            RectF margins = new RectF(
+                -Math.min(minDx, maxDx),
+                -Math.min(minDy, maxDy),
+                Math.max(minDx, maxDx),
+                Math.max(minDy, maxDy));
+            return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+        }
+
+        @Override
+        public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+            // the code below is the same as in calculate() but is awkward to refactor since it has multiple outputs.
+            // refer to the comments in calculate() to understand what this is doing.
+            float minDx = velocity.x * mMinFramesToDraw;
+            float minDy = velocity.y * mMinFramesToDraw;
+            float maxDx = velocity.x * mMaxFramesToDraw;
+            float maxDy = velocity.y * mMaxFramesToDraw;
+            float pixelsToDraw = (metrics.getWidth() + Math.abs(maxDx - minDx)) * (metrics.getHeight() + Math.abs(maxDy - minDy));
+            maxDx = maxDx * pixelsToDraw / mPixelArea;
+            maxDy = maxDy * pixelsToDraw / mPixelArea;
+
+            // now that we have an idea of how far we will be when the draw completes, take the farthest
+            // end of that range and see if it falls outside the displayport bounds. if it does, allow
+            // the draw to go through
+            RectF predictedViewport = metrics.getViewport();
+            predictedViewport.left += maxDx;
+            predictedViewport.top += maxDy;
+            predictedViewport.right += maxDx;
+            predictedViewport.bottom += maxDy;
+
+            predictedViewport = clampToPageBounds(predictedViewport, metrics);
+            return !displayPort.contains(predictedViewport);
+        }
+
+        @Override
+        public boolean drawTimeUpdate(long millis, int pixels) {
+            // calculate the number of frames it took to draw a viewport-sized area
+            float normalizedTime = (float)mPixelArea * millis / pixels;
+            int normalizedFrames = (int) Math.ceil(normalizedTime * 60f / 1000f);
+            // broaden our range on how long it takes to draw if the draw falls outside
+            // the range. this allows it to grow gradually. this heuristic may need to
+            // be tweaked into more of a floating window average or something.
+            if (normalizedFrames <= mMinFramesToDraw) {
+                mMinFramesToDraw--;
+            } else if (normalizedFrames > mMaxFramesToDraw) {
+                mMaxFramesToDraw++;
+            } else {
+                return true;
+            }
+            Log.d(LOGTAG, "Widened draw range to [" + mMinFramesToDraw + ", " + mMaxFramesToDraw + "]");
+            return true;
+        }
+
+        @Override
+        public void resetPageState() {
+            mMinFramesToDraw = 0;
+            mMaxFramesToDraw = 2;
+        }
+
+        @Override
+        public String toString() {
+            return "PredictionBiasStrategy threshold=" + VELOCITY_THRESHOLD;
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DisplayPortMetrics.java
@@ -0,0 +1,78 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.FloatUtils;
+
+import android.graphics.RectF;
+
+/*
+ * This class keeps track of the area we request Gecko to paint, as well
+ * as the resolution of the paint. The area may be different from the visible
+ * area of the page, and the resolution may be different from the resolution
+ * used in the compositor to render the page. This is so that we can ask Gecko
+ * to paint a much larger area without using extra memory, and then render some
+ * subsection of that with compositor scaling.
+ */
+public final class DisplayPortMetrics {
+    @WrapForJNI
+    public final float resolution;
+    @WrapForJNI
+    private final RectF mPosition;
+
+    public DisplayPortMetrics() {
+        this(0, 0, 0, 0, 1);
+    }
+
+    @WrapForJNI
+    public DisplayPortMetrics(float left, float top, float right, float bottom, float resolution) {
+        this.resolution = resolution;
+        mPosition = new RectF(left, top, right, bottom);
+    }
+
+    public float getLeft() {
+        return mPosition.left;
+    }
+
+    public float getTop() {
+        return mPosition.top;
+    }
+
+    public float getRight() {
+        return mPosition.right;
+    }
+
+    public float getBottom() {
+        return mPosition.bottom;
+    }
+
+    public boolean contains(RectF rect) {
+        return mPosition.contains(rect);
+    }
+
+    public boolean fuzzyEquals(DisplayPortMetrics metrics) {
+        return RectUtils.fuzzyEquals(mPosition, metrics.mPosition)
+            && FloatUtils.fuzzyEquals(resolution, metrics.resolution);
+    }
+
+    public String toJSON() {
+        StringBuilder sb = new StringBuilder(256);
+        sb.append("{ \"left\": ").append(mPosition.left)
+          .append(", \"top\": ").append(mPosition.top)
+          .append(", \"right\": ").append(mPosition.right)
+          .append(", \"bottom\": ").append(mPosition.bottom)
+          .append(", \"resolution\": ").append(resolution)
+          .append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public String toString() {
+        return "DisplayPortMetrics v=(" + mPosition.left + "," + mPosition.top + "," + mPosition.right + ","
+                + mPosition.bottom + ") z=" + resolution;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DrawTimingQueue.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 android.os.SystemClock;
+
+/**
+ * A custom-built data structure to assist with measuring draw times.
+ *
+ * This class maintains a fixed-size circular buffer of DisplayPortMetrics
+ * objects and associated timestamps. It provides only three operations, which
+ * is all we require for our purposes of measuring draw times. Note
+ * in particular that the class is designed so that even though it is
+ * accessed from multiple threads, it does not require synchronization;
+ * any concurrency errors that result from this are handled gracefully.
+ *
+ * Assuming an unrolled buffer so that mTail is greater than mHead, the data
+ * stored in the buffer at entries [mHead, mTail) will never be modified, and
+ * so are "safe" to read. If this reading is done on the same thread that
+ * owns mHead, then reading the range [mHead, mTail) is guaranteed to be safe
+ * since the range itself will not shrink.
+ */
+final class DrawTimingQueue {
+    private static final String LOGTAG = "GeckoDrawTimingQueue";
+    private static final int BUFFER_SIZE = 16;
+
+    private final DisplayPortMetrics[] mMetrics;
+    private final long[] mTimestamps;
+
+    private int mHead;
+    private int mTail;
+
+    DrawTimingQueue() {
+        mMetrics = new DisplayPortMetrics[BUFFER_SIZE];
+        mTimestamps = new long[BUFFER_SIZE];
+        mHead = BUFFER_SIZE - 1;
+    }
+
+    /**
+     * Add a new entry to the tail of the queue. If the buffer is full,
+     * do nothing. This must only be called from the Java UI thread.
+     */
+    boolean add(DisplayPortMetrics metrics) {
+        if (mHead == mTail) {
+            return false;
+        }
+        mMetrics[mTail] = metrics;
+        mTimestamps[mTail] = SystemClock.uptimeMillis();
+        mTail = (mTail + 1) % BUFFER_SIZE;
+        return true;
+    }
+
+    /**
+     * Find the timestamp associated with the given metrics, AND remove
+     * all metrics objects from the start of the queue up to and including
+     * the one provided. Note that because of draw coalescing, the metrics
+     * object passed in here may not be the one at the head of the queue,
+     * and so we must iterate our way through the list to find it.
+     * This must only be called from the compositor thread.
+     */
+    long findTimeFor(DisplayPortMetrics metrics) {
+        // keep a copy of the tail pointer so that we ignore new items
+        // added to the queue while we are searching. this is fine because
+        // the one we are looking for will either have been added already
+        // or will not be in the queue at all.
+        int tail = mTail;
+        // walk through the "safe" range from mHead to tail; these entries
+        // will not be modified unless we change mHead.
+        int i = (mHead + 1) % BUFFER_SIZE;
+        while (i != tail) {
+            if (mMetrics[i].fuzzyEquals(metrics)) {
+                // found it, copy out the timestamp to a local var BEFORE
+                // changing mHead or add could clobber the timestamp.
+                long timestamp = mTimestamps[i];
+                mHead = i;
+                return timestamp;
+            }
+            i = (i + 1) % BUFFER_SIZE;
+        }
+        return -1;
+    }
+
+    /**
+     * Reset the buffer to empty.
+     * This must only be called from the compositor thread.
+     */
+    void reset() {
+        // we can only modify mHead on this thread.
+        mHead = (mTail + BUFFER_SIZE - 1) % BUFFER_SIZE;
+    }
+}
new file mode 100644
--- /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.animation.DecelerateInterpolator;
+import android.view.MotionEvent;
+
+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 = 250000000;
+
+    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 DecelerateInterpolator 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 DecelerateInterpolator();
+
+        // 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);
+
+        // JPZ doesn't notify when scrolling root content. This maintains existing behaviour.
+        if (!AppConstants.MOZ_ANDROID_APZ) {
+            mScrollingRootContent = true;
+        }
+    }
+
+    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()) {
+            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;
+        }
+    }
+}
new file mode 100644
--- /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));
+    }
+}
+
new file mode 100644
--- /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
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GLController.java
@@ -0,0 +1,183 @@
+/* -*- 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.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.util.Log;
+
+/**
+ * This class is a singleton that tracks EGL and compositor things over
+ * the lifetime of Fennec running.
+ * We only ever create one C++ compositor over Fennec's lifetime, but
+ * most of the Java-side objects (e.g. LayerView, GeckoLayerClient,
+ * LayerRenderer) can all get destroyed and re-created if the GeckoApp
+ * activity is destroyed. This GLController is never destroyed, so that
+ * the mCompositorCreated field and other state variables are always
+ * accurate.
+ */
+public class GLController extends JNIObject {
+    private static final String LOGTAG = "GeckoGLController";
+
+    /* package */ LayerView mView;
+    private boolean mServerSurfaceValid;
+    private int mWidth, mHeight;
+
+    /* This is written by the compositor thread (while the UI thread
+     * is blocked on it) and read by the UI thread. */
+    private volatile boolean mCompositorCreated;
+
+    @WrapForJNI @Override // JNIObject
+    protected native void disposeNative();
+
+    // Gecko thread sets its Java instances; does not block UI thread.
+    @WrapForJNI
+    /* package */ native void attachToJava(GeckoLayerClient layerClient,
+                                           NativePanZoomController npzc);
+
+    @WrapForJNI
+    /* package */ native void onSizeChanged(int windowWidth, int windowHeight,
+                                            int screenWidth, int screenHeight);
+
+    // Gecko thread creates compositor; blocks UI thread.
+    @WrapForJNI
+    private native void createCompositor(int width, int height);
+
+    // Gecko thread pauses compositor; blocks UI thread.
+    @WrapForJNI
+    private native void pauseCompositor();
+
+    // UI thread resumes compositor and notifies Gecko thread; does not block UI thread.
+    @WrapForJNI
+    private native void syncResumeResizeCompositor(int width, int height);
+
+    @WrapForJNI
+    private native void syncInvalidateAndScheduleComposite();
+
+    public GLController() {
+    }
+
+    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) {
+            pauseCompositor();
+        }
+
+        synchronized (this) {
+            mServerSurfaceValid = false;
+        }
+    }
+
+    void serverSurfaceChanged(int newWidth, int newHeight) {
+        ThreadUtils.assertOnUiThread();
+
+        synchronized (this) {
+            mWidth = newWidth;
+            mHeight = newHeight;
+            mServerSurfaceValid = true;
+        }
+
+        updateCompositor();
+    }
+
+    void updateCompositor() {
+        ThreadUtils.assertOnUiThread();
+
+        if (mView == null) {
+            return;
+        }
+
+        if (mCompositorCreated) {
+            // If the compositor has already been created, just resume it instead. We don't need
+            // to block here because if the surface is destroyed before the compositor grabs it,
+            // we can handle that gracefully (i.e. the compositor will remain paused).
+            resumeCompositor(mWidth, mHeight);
+            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. Do it with a synchronous Gecko event so that the
+        // Android doesn't have a chance to destroy our surface in between.
+        if (isServerSurfaceValid() && mView.getLayerClient().isGeckoReady()) {
+            createCompositor(mWidth, mHeight);
+            compositorCreated();
+        }
+    }
+
+    void compositorCreated() {
+        // This is invoked on the compositor thread, while the java UI thread
+        // is blocked on the gecko sync event in updateCompositor() above
+        mCompositorCreated = true;
+    }
+
+    public boolean isServerSurfaceValid() {
+        return mServerSurfaceValid;
+    }
+
+    @WrapForJNI(allowMultithread = true)
+    private synchronized Object getSurface() {
+        if (mView != null && isServerSurfaceValid()) {
+            return mView.getSurface();
+        } else {
+            return null;
+        }
+    }
+
+    void resumeCompositor(int width, int height) {
+        // Asking Gecko to resume the compositor takes too long (see
+        // https://bugzilla.mozilla.org/show_bug.cgi?id=735230#c23), so we
+        // resume the compositor directly. We still need to inform Gecko about
+        // the compositor resuming, so that Gecko knows that it can now draw.
+        // It is important to not notify Gecko until after the compositor has
+        // been resumed, otherwise Gecko may send updates that get dropped.
+        if (isServerSurfaceValid() && mCompositorCreated) {
+            syncResumeResizeCompositor(width, height);
+            mView.requestRender();
+        }
+    }
+
+    /* package */ void invalidateAndScheduleComposite() {
+        if (mCompositorCreated) {
+            syncInvalidateAndScheduleComposite();
+        }
+    }
+
+    @WrapForJNI
+    private void destroy() {
+        // The nsWindow has been closed. First mark our compositor as destroyed.
+        mCompositorCreated = 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() {
+                GLController.this.disposeNative();
+            }
+        });
+    }
+
+    public static class GLControllerException extends RuntimeException {
+        public static final long serialVersionUID = 1L;
+
+        GLControllerException(String e) {
+            super(e);
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
@@ -0,0 +1,1132 @@
+/* -*- 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.GeckoEvent;
+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.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;
+    private DisplayPortMetrics mDisplayPort;
+
+    private boolean mRecordDrawTimes;
+    private final DrawTimingQueue mDrawTimingQueue;
+
+    private VirtualLayer mRootLayer;
+
+    /* The Gecko viewport as per the UI thread. Must be touched only on the UI thread.
+     * If any events being sent to Gecko that are relative to the Gecko viewport position,
+     * they must (a) be relative to this viewport, and (b) be sent on the UI thread to
+     * avoid races. As long as these two conditions are satisfied, and the events being
+     * sent to Gecko are processed in FIFO order, the events will properly be relative
+     * to the Gecko viewport position. Note that if Gecko updates its viewport independently,
+     * we get notified synchronously and also update this on the UI thread.
+     */
+    private ImmutableViewportMetrics mGeckoViewport;
+
+    /*
+     * The viewport metrics being used to draw the current frame. This is only
+     * accessed by the compositor thread, and so needs no synchronisation.
+     */
+    private ImmutableViewportMetrics mFrameMetrics;
+
+    private final List<DrawListener> mDrawListeners;
+
+    /* Used as temporaries by syncViewportInfo */
+    private final ViewTransform mCurrentViewTransform;
+
+    /* Used as the return value of progressiveUpdateCallback */
+    private final ProgressiveUpdateData mProgressiveUpdateData;
+    private DisplayPortMetrics mProgressiveUpdateDisplayPort;
+    private boolean mLastProgressiveUpdateWasLowPrecision;
+    private boolean mProgressiveUpdateWasInDanger;
+
+    private boolean mForceRedraw;
+
+    /* The current viewport metrics.
+     * This is volatile so that we can read and write to it from different threads.
+     * We avoid synchronization to make getting the viewport metrics from
+     * the compositor as cheap as possible. The viewport is immutable so
+     * we don't need to worry about anyone mutating it while we're reading from it.
+     * Specifically:
+     * 1) reading mViewportMetrics from any thread is fine without synchronization
+     * 2) writing to mViewportMetrics requires synchronizing on the layer controller object
+     * 3) 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;
+
+    private final PanZoomController mPanZoomController;
+    private final DynamicToolbarAnimator mToolbarAnimator;
+    private 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;
+
+    public GeckoLayerClient(Context context, LayerView view, EventDispatcher eventDispatcher) {
+        // we can fill these in with dummy values because they are always written
+        // to before being read
+        mContext = context;
+        mScreenSize = new IntSize(0, 0);
+        mWindowSize = new IntSize(0, 0);
+        mDisplayPort = new DisplayPortMetrics();
+        mRecordDrawTimes = true;
+        mDrawTimingQueue = new DrawTimingQueue();
+        mCurrentViewTransform = new ViewTransform(0, 0, 1);
+        mProgressiveUpdateData = new ProgressiveUpdateData();
+        mProgressiveUpdateDisplayPort = new DisplayPortMetrics();
+
+        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);
+    }
+
+    /** Attaches to root layer so that Gecko appears. */
+    /* package */ boolean isGeckoReady() {
+        return mGeckoIsReady;
+    }
+
+    @WrapForJNI
+    private void onGeckoReady() {
+        mGeckoIsReady = true;
+
+        mRootLayer = new VirtualLayer(new IntSize(mView.getWidth(), mView.getHeight()));
+        mLayerRenderer = mView.getRenderer();
+
+        sendResizeEventIfNecessary(true, null);
+
+        DisplayPortCalculator.initPrefs();
+
+        // Gecko being ready is one of the two conditions (along with having an available
+        // surface) that cause us to create the compositor. So here, now that we know gecko
+        // is ready, call updateCompositor() to see if we can actually do the creation.
+        // This needs to run on the UI thread so that the surface validity can't change on
+        // us while we're in the middle of creating the compositor.
+        mView.post(new Runnable() {
+            @Override
+            public void run() {
+                mView.getGLController().updateCompositor();
+            }
+        });
+    }
+
+    public void destroy() {
+        mPanZoomController.destroy();
+        mToolbarAnimator.destroy();
+        mDrawListeners.clear();
+    }
+
+    /**
+     * Returns true if this client is fine with performing a redraw operation or false if it
+     * would prefer that the action didn't take place.
+     */
+    private boolean getRedrawHint() {
+        if (mForceRedraw) {
+            mForceRedraw = false;
+            return true;
+        }
+
+        if (!mPanZoomController.getRedrawHint()) {
+            return false;
+        }
+
+        return DisplayPortCalculator.aboutToCheckerboard(mViewportMetrics,
+                mPanZoomController.getVelocityVector(), mDisplayPort);
+    }
+
+    Layer getRoot() {
+        return mGeckoIsReady ? mRootLayer : null;
+    }
+
+    public LayerView getView() {
+        return mView;
+    }
+
+    public FloatSize getViewportSize() {
+        return mViewportMetrics.getSize();
+    }
+
+    /**
+     * The view calls this function to indicate that the viewport changed size. It must hold the
+     * monitor while calling it.
+     *
+     * TODO: Refactor this to use an interface. Expose that interface only to the view and not
+     * to the layer client. That way, the layer client won't be tempted to call this, which might
+     * result in an infinite loop.
+     */
+    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.getGLController().onSizeChanged(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);
+    }
+
+    /** Sets the current page rect. You must hold the monitor while calling this. */
+    private void setPageRect(RectF rect, RectF cssRect) {
+        // Since the "rect" is always just a multiple of "cssRect" we don't need to
+        // check both; this function assumes that both "rect" and "cssRect" are relative
+        // the zoom factor in mViewportMetrics.
+        if (mViewportMetrics.getCssPageRect().equals(cssRect))
+            return;
+
+        mViewportMetrics = mViewportMetrics.setPageRect(rect, cssRect);
+
+        // Page size is owned by the layer client, so no need to notify it of
+        // this change.
+
+        post(new Runnable() {
+            @Override
+            public void run() {
+                mPanZoomController.pageRectUpdated();
+                mView.requestRender();
+            }
+        });
+    }
+
+    private void adjustViewport(DisplayPortMetrics displayPort) {
+        // TODO: APZ For fennec might need margins information to deal with
+        // the dynamic toolbar.
+        if (AppConstants.MOZ_ANDROID_APZ)
+            return;
+
+        ImmutableViewportMetrics metrics = getViewportMetrics();
+        ImmutableViewportMetrics clampedMetrics = metrics.clamp();
+
+        if (displayPort == null) {
+            displayPort = DisplayPortCalculator.calculate(metrics, mPanZoomController.getVelocityVector());
+        }
+
+        mDisplayPort = displayPort;
+        mGeckoViewport = clampedMetrics;
+
+        if (mRecordDrawTimes) {
+            mDrawTimingQueue.add(displayPort);
+        }
+
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createViewportEvent(clampedMetrics, displayPort));
+    }
+
+    /** Aborts any pan/zoom animation that is currently in progress. */
+    private void abortPanZoomAnimation() {
+        if (mPanZoomController != null) {
+            post(new Runnable() {
+                @Override
+                public void run() {
+                    mPanZoomController.abortAnimation();
+                }
+            });
+        }
+    }
+
+    /**
+     * The different types of Viewport messages handled. All viewport events
+     * expect a display-port to be returned, but can handle one not being
+     * returned.
+     */
+    private enum ViewportMessageType {
+        UPDATE,       // The viewport has changed and should be entirely updated
+        PAGE_SIZE     // The viewport's page-size has changed
+    }
+
+    /** Viewport message handler. */
+    private DisplayPortMetrics handleViewportMessage(ImmutableViewportMetrics messageMetrics, ViewportMessageType type) {
+        synchronized (getLock()) {
+            ImmutableViewportMetrics newMetrics;
+            ImmutableViewportMetrics oldMetrics = getViewportMetrics();
+
+            switch (type) {
+            default:
+            case UPDATE:
+                // Keep the old viewport size
+                newMetrics = messageMetrics.setViewportSize(oldMetrics.viewportRectWidth, oldMetrics.viewportRectHeight);
+                if (mToolbarAnimator.isResizing()) {
+                    // If we're in the middle of a resize, we don't want to clobber
+                    // the scroll offset, so grab the one from the oldMetrics and
+                    // keep using that. We also don't want to abort animations,
+                    // because at that point we're guaranteed to not be animating
+                    // anyway, and calling abortPanZoomAnimation has a nasty
+                    // side-effect of clmaping and clobbering the metrics, which
+                    // we don't want here.
+                    newMetrics = newMetrics.setViewportOrigin(oldMetrics.viewportRectLeft, oldMetrics.viewportRectTop);
+                    break;
+                }
+                if (!oldMetrics.fuzzyEquals(newMetrics)) {
+                    abortPanZoomAnimation();
+                }
+                break;
+            case PAGE_SIZE:
+                // adjust the page dimensions to account for differences in zoom
+                // between the rendered content (which is what Gecko tells us)
+                // and our zoom level (which may have diverged).
+                float scaleFactor = oldMetrics.zoomFactor / messageMetrics.zoomFactor;
+                newMetrics = oldMetrics.setPageRect(RectUtils.scale(messageMetrics.getPageRect(), scaleFactor), messageMetrics.getCssPageRect());
+                break;
+            }
+
+            // Update the Gecko-side viewport metrics. Make sure to do this
+            // before modifying the metrics below.
+            final ImmutableViewportMetrics geckoMetrics = newMetrics.clamp();
+            post(new Runnable() {
+                @Override
+                public void run() {
+                    mGeckoViewport = geckoMetrics;
+                }
+            });
+
+            setViewportMetrics(newMetrics, type == ViewportMessageType.UPDATE);
+            mDisplayPort = DisplayPortCalculator.calculate(getViewportMetrics(), null);
+        }
+        return mDisplayPort;
+    }
+
+    @WrapForJNI
+    DisplayPortMetrics getDisplayPort(boolean pageSizeUpdate, boolean isBrowserContentDisplayed, int tabId, ImmutableViewportMetrics metrics) {
+        return null;
+    }
+
+    @WrapForJNI
+    void contentDocumentChanged() {
+        mContentDocumentIsDisplayed = false;
+    }
+
+    @WrapForJNI
+    boolean isContentDocumentDisplayed() {
+        return mContentDocumentIsDisplayed;
+    }
+
+    // This is called on the Gecko thread to determine if we're still interested
+    // in the update of this display-port to continue. We can return true here
+    // to abort the current update and continue with any subsequent ones. This
+    // is useful for slow-to-render pages when the display-port starts lagging
+    // behind enough that continuing to draw it is wasted effort.
+    @WrapForJNI(allowMultithread = true)
+    public ProgressiveUpdateData progressiveUpdateCallback(boolean aHasPendingNewThebesContent,
+                                                           float x, float y, float width, float height,
+                                                           float resolution, boolean lowPrecision) {
+        // Reset the checkerboard risk flag when switching to low precision
+        // rendering.
+        if (lowPrecision && !mLastProgressiveUpdateWasLowPrecision) {
+            // Skip low precision rendering until we're at risk of checkerboarding.
+            if (!mProgressiveUpdateWasInDanger) {
+                mProgressiveUpdateData.abort = true;
+                return mProgressiveUpdateData;
+            }
+            mProgressiveUpdateWasInDanger = false;
+        }
+        mLastProgressiveUpdateWasLowPrecision = lowPrecision;
+
+        // Grab a local copy of the last display-port sent to Gecko and the
+        // current viewport metrics to avoid races when accessing them.
+        DisplayPortMetrics displayPort = mDisplayPort;
+        ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
+        mProgressiveUpdateData.setViewport(viewportMetrics);
+        mProgressiveUpdateData.abort = false;
+
+        // Always abort updates if the resolution has changed. There's no use
+        // in drawing at the incorrect resolution.
+        if (!FloatUtils.fuzzyEquals(resolution, viewportMetrics.zoomFactor)) {
+            Log.d(LOGTAG, "Aborting draw due to resolution change: " + resolution + " != " + viewportMetrics.zoomFactor);
+            mProgressiveUpdateData.abort = true;
+            return mProgressiveUpdateData;
+        }
+
+        // Store the high precision displayport for comparison when doing low
+        // precision updates.
+        if (!lowPrecision) {
+            if (!FloatUtils.fuzzyEquals(resolution, mProgressiveUpdateDisplayPort.resolution) ||
+                !FloatUtils.fuzzyEquals(x, mProgressiveUpdateDisplayPort.getLeft()) ||
+                !FloatUtils.fuzzyEquals(y, mProgressiveUpdateDisplayPort.getTop()) ||
+                !FloatUtils.fuzzyEquals(x + width, mProgressiveUpdateDisplayPort.getRight()) ||
+                !FloatUtils.fuzzyEquals(y + height, mProgressiveUpdateDisplayPort.getBottom())) {
+                mProgressiveUpdateDisplayPort =
+                    new DisplayPortMetrics(x, y, x + width, y + height, resolution);
+            }
+        }
+
+        // If we're not doing low precision draws and we're about to
+        // checkerboard, enable low precision drawing.
+        if (!lowPrecision && !mProgressiveUpdateWasInDanger) {
+            if (DisplayPortCalculator.aboutToCheckerboard(viewportMetrics,
+                  mPanZoomController.getVelocityVector(), mProgressiveUpdateDisplayPort)) {
+                mProgressiveUpdateWasInDanger = true;
+            }
+        }
+
+        // XXX All sorts of rounding happens inside Gecko that becomes hard to
+        //     account exactly for. Given we align the display-port to tile
+        //     boundaries (and so they rarely vary by sub-pixel amounts), just
+        //     check that values are within a couple of pixels of the
+        //     display-port bounds.
+
+        // Never abort drawing if we can't be sure we've sent a more recent
+        // display-port. If we abort updating when we shouldn't, we can end up
+        // with blank regions on the screen and we open up the risk of entering
+        // an endless updating cycle.
+        if (Math.abs(displayPort.getLeft() - mProgressiveUpdateDisplayPort.getLeft()) <= 2 &&
+            Math.abs(displayPort.getTop() - mProgressiveUpdateDisplayPort.getTop()) <= 2 &&
+            Math.abs(displayPort.getBottom() - mProgressiveUpdateDisplayPort.getBottom()) <= 2 &&
+            Math.abs(displayPort.getRight() - mProgressiveUpdateDisplayPort.getRight()) <= 2) {
+            return mProgressiveUpdateData;
+        }
+
+        // Abort updates when the display-port no longer contains the visible
+        // area of the page (that is, the viewport cropped by the page
+        // boundaries).
+        // XXX This makes the assumption that we never let the visible area of
+        //     the page fall outside of the display-port.
+        if (Math.max(viewportMetrics.viewportRectLeft, viewportMetrics.pageRectLeft) + 1 < x ||
+            Math.max(viewportMetrics.viewportRectTop, viewportMetrics.pageRectTop) + 1 < y ||
+            Math.min(viewportMetrics.viewportRectRight(), viewportMetrics.pageRectRight) - 1 > x + width ||
+            Math.min(viewportMetrics.viewportRectBottom(), viewportMetrics.pageRectBottom) - 1 > y + height) {
+            Log.d(LOGTAG, "Aborting update due to viewport not in display-port");
+            mProgressiveUpdateData.abort = true;
+
+            // Enable low-precision drawing, as we're likely to be in danger if
+            // this situation has been encountered.
+            mProgressiveUpdateWasInDanger = true;
+
+            return mProgressiveUpdateData;
+        }
+
+        // Abort drawing stale low-precision content if there's a more recent
+        // display-port in the pipeline.
+        if (lowPrecision && !aHasPendingNewThebesContent) {
+          mProgressiveUpdateData.abort = true;
+        }
+        return mProgressiveUpdateData;
+    }
+
+    /** The compositor invokes this function just before compositing a frame where the document
+      * is different from the document composited on the last frame. In these cases, the viewport
+      * information we have in Java is no longer valid and needs to be replaced with the new
+      * viewport information provided. setPageRect will never be invoked on the same frame that
+      * this function is invoked on; and this function will always be called prior to syncViewportInfo.
+      */
+    @WrapForJNI(allowMultithread = true)
+    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. This includes mGeckoViewport and
+            // mViewportMetrics. Usually this information is updated via handleViewportMessage
+            // while we remain on the same document.
+            post(new Runnable() {
+                @Override
+                public void run() {
+                    mGeckoViewport = newMetrics;
+                }
+            });
+
+            setViewportMetrics(newMetrics);
+
+            // At this point, we have just switched to displaying a different document than we
+            // we previously displaying. This means we need to abort any panning/zooming animations
+            // that are in progress and send an updated display port request to browser.js as soon
+            // as possible. The call to PanZoomController.abortAnimation accomplishes this by calling the
+            // forceRedraw function, which sends the viewport to gecko. The display port request is
+            // actually a full viewport update, which is fine because if browser.js has somehow moved to
+            // be out of sync with this first-paint viewport, then we force them back in sync.
+            abortPanZoomAnimation();
+
+            // Indicate that the document is about to be composited so the
+            // LayerView background can be removed.
+            if (mView.getPaintState() == LayerView.PAINT_START) {
+                mView.setPaintState(LayerView.PAINT_BEFORE_FIRST);
+            }
+        }
+        DisplayPortCalculator.resetPageState();
+        mDrawTimingQueue.reset();
+
+        mContentDocumentIsDisplayed = true;
+    }
+
+    /** The compositor invokes this function whenever it determines that the page rect
+      * has changed (based on the information it gets from layout). If setFirstPaintViewport
+      * is invoked on a frame, then this function will not be. For any given frame, this
+      * function will be invoked before syncViewportInfo.
+      */
+    @WrapForJNI(allowMultithread = true)
+    public void setPageRect(float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom) {
+        synchronized (getLock()) {
+            RectF cssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom);
+            float ourZoom = getViewportMetrics().zoomFactor;
+            setPageRect(RectUtils.scale(cssPageRect, ourZoom), cssPageRect);
+            // Here the page size of the document has changed, but the document being displayed
+            // is still the same. Therefore, we don't need to send anything to browser.js; any
+            // changes we need to make to the display port will get sent the next time we call
+            // adjustViewport().
+        }
+    }
+
+    /** The compositor invokes this function on every frame to figure out what part of the
+      * page to display, and to inform Java of the current display port. Since it is called
+      * on every frame, it needs to be ultra-fast.
+      * It avoids taking any locks or allocating any objects. We keep around a
+      * mCurrentViewTransform so we don't need to allocate a new ViewTransform
+      * every time we're called. NOTE: we might be able to return a ImmutableViewportMetrics
+      * which would avoid the copy into mCurrentViewTransform.
+      */
+    @WrapForJNI(allowMultithread = true)
+    public 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 (mRootLayer != null) {
+            mRootLayer.setPositionAndResolution(
+                x, y, x + width, y + height,
+                resolution);
+        }
+
+        if (layersUpdated && mRecordDrawTimes) {
+            // If we got a layers update, that means a draw finished. Check to see if the area drawn matches
+            // one of our requested displayports; if it does calculate the draw time and notify the
+            // DisplayPortCalculator
+            DisplayPortMetrics drawn = new DisplayPortMetrics(x, y, x + width, y + height, resolution);
+            long time = mDrawTimingQueue.findTimeFor(drawn);
+            if (time >= 0) {
+                long now = SystemClock.uptimeMillis();
+                time = now - time;
+                mRecordDrawTimes = DisplayPortCalculator.drawTimeUpdate(time, width * height);
+            }
+        }
+
+        if (layersUpdated) {
+            for (DrawListener listener : mDrawListeners) {
+                listener.drawFinished();
+            }
+        }
+
+        return mCurrentViewTransform;
+    }
+
+    @WrapForJNI(allowMultithread = true)
+    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() {
+                event.offsetLocation(0, mView.getSurfaceTranslation());
+                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
+    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
+    public void synthesizeNativeMouseEvent(int eventType, int screenX, int screenY) {
+        synthesizeNativePointer(InputDevice.SOURCE_MOUSE, PointerInfo.RESERVED_MOUSE_POINTER_ID,
+            eventType, screenX, screenY, 0, 0);
+    }
+
+    @WrapForJNI(allowMultithread = true)
+    public LayerRenderer.Frame createFrame() {
+        // Create the shaders and textures if necessary.
+        if (!mLayerRendererInitialized) {
+            if (mLayerRenderer == null) {
+                return null;
+            }
+            mLayerRenderer.checkMonitoringEnabled();
+            mLayerRenderer.createDefaultProgram();
+            mLayerRendererInitialized = true;
+        }
+
+        try {
+            return mLayerRenderer.createFrame(mFrameMetrics);
+        } catch (Exception e) {
+            Log.w(LOGTAG, e);
+            return null;
+        }
+    }
+
+    @WrapForJNI(allowMultithread = true)
+    public void activateProgram() {
+        mLayerRenderer.activateDefaultProgram();
+    }
+
+    @WrapForJNI(allowMultithread = true)
+    public void deactivateProgramAndRestoreState(boolean enableScissor,
+            int scissorX, int scissorY, int scissorW, int scissorH)
+    {
+        mLayerRenderer.deactivateDefaultProgram();
+        mLayerRenderer.restoreState(enableScissor, scissorX, scissorY, scissorW, scissorH);
+    }
+
+    private void geometryChanged(DisplayPortMetrics displayPort) {
+        /* Let Gecko know if the screensize has changed */
+        sendResizeEventIfNecessary(false, null);
+        if (getRedrawHint()) {
+            adjustViewport(displayPort);
+        }
+    }
+
+    /** Implementation of LayerView.Listener */
+    @Override
+    public void renderRequested() {
+        final GLController glController = mView.getGLController();
+        if (glController != null) {
+            glController.invalidateAndScheduleComposite();
+        }
+    }
+
+    /** Implementation of LayerView.Listener */
+    @Override
+    public void sizeChanged(int width, int height) {
+        // We need to make sure a draw happens synchronously at this point,
+        // but resizing the surface before the SurfaceView has resized will
+        // cause a visible jump.
+        mView.getGLController().resumeCompositor(width, height);
+    }
+
+    /** Implementation of LayerView.Listener */
+    @Override
+    public void surfaceChanged(int width, int height) {
+        IntSize viewportSize = mToolbarAnimator.getViewportSize();
+        setViewportSize(viewportSize.width, viewportSize.height, null);
+    }
+
+    /** Implementation of PanZoomTarget */
+    @Override
+    public ImmutableViewportMetrics getViewportMetrics() {
+        return mViewportMetrics;
+    }
+
+    /** Implementation of PanZoomTarget */
+    @Override
+    public FullScreenState getFullScreenState() {
+        return mView.getFullScreenState();
+    }
+
+    /** Implementation of PanZoomTarget */
+    @Override
+    public PointF getVisibleEndOfLayerView() {
+        return mToolbarAnimator.getVisibleEndOfLayerView();
+    }
+
+    /** Implementation of PanZoomTarget */
+    @Override
+    public void setAnimationTarget(ImmutableViewportMetrics metrics) {
+        if (mGeckoIsReady) {
+            // We know what the final viewport of the animation is going to be, so
+            // immediately request a draw of that area by setting the display port
+            // accordingly. This way we should have the content pre-rendered by the
+            // time the animation is done.
+            DisplayPortMetrics displayPort = DisplayPortCalculator.calculate(metrics, null);
+            adjustViewport(displayPort);
+        }
+    }
+
+    /** Implementation of PanZoomTarget
+     * You must hold the monitor while calling this.
+     */
+    @Override
+    public void setViewportMetrics(ImmutableViewportMetrics metrics) {
+        setViewportMetrics(metrics, true);
+    }
+
+    /*
+     * You must hold the monitor while calling this.
+     */
+    private void setViewportMetrics(ImmutableViewportMetrics metrics, boolean notifyGecko) {
+        // This class owns the viewport size and the fixed layer margins; don't let other pieces
+        // of code clobber either of them. The only place the viewport size should ever be
+        // updated is in GeckoLayerClient.setViewportSize, and the only place the margins should
+        // ever be updated is in GeckoLayerClient.setFixedLayerMargins; both of these assign to
+        // mViewportMetrics directly.
+        metrics = metrics.setViewportSize(mViewportMetrics.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(null);
+        }
+    }
+
+    /*
+     * Updates the viewport metrics, overriding the viewport size and margins
+     * which are normally retained when calling setViewportMetrics.
+     * You must hold the monitor while calling this.
+     */
+    void forceViewportMetrics(ImmutableViewportMetrics metrics, boolean notifyGecko, boolean forceRedraw) {
+        if (forceRedraw) {
+            mForceRedraw = true;
+        }
+        mViewportMetrics = metrics;
+        viewportMetricsChanged(notifyGecko);
+    }
+
+    /** Implementation of PanZoomTarget
+     * Scroll the viewport by a certain amount. This will take viewport margins
+     * and margin animation into account. If margins are currently animating,
+     * this will just go ahead and modify the viewport origin, otherwise the
+     * delta will be applied to the margins and the remainder will be applied to
+     * the viewport origin.
+     *
+     * You must hold the monitor while calling this.
+     */
+    @Override
+    public void scrollBy(float dx, float dy) {
+        // Set mViewportMetrics manually so the margin changes take.
+        mViewportMetrics = mViewportMetrics.offsetViewportBy(dx, dy);
+        viewportMetricsChanged(true);
+    }
+
+    /** Implementation of PanZoomTarget */
+    @Override
+    public void panZoomStopped() {
+        mToolbarAnimator.onPanZoomStopped();
+    }
+
+    /** Implementation of PanZoomTarget */
+    @Override
+    public void forceRedraw(DisplayPortMetrics displayPort) {
+        mForceRedraw = true;
+        if (mGeckoIsReady) {
+            geometryChanged(displayPort);
+        }
+    }
+
+    /** Implementation of PanZoomTarget */
+    @Override
+    public boolean post(Runnable action) {
+        return mView.post(action);
+    }
+
+    /** Implementation of PanZoomTarget */
+    @Override
+    public void postRenderTask(RenderTask task) {
+        mView.postRenderTask(task);
+    }
+
+    /** Implementation of PanZoomTarget */
+    @Override
+    public void removeRenderTask(RenderTask task) {
+        mView.removeRenderTask(task);
+    }
+
+    /** Implementation of PanZoomTarget */
+    @Override
+    public Object getLock() {
+        return this;
+    }
+
+    /** Implementation of PanZoomTarget
+     * Converts a point from layer view coordinates to layer coordinates. In other words, given a
+     * point measured in pixels from the top left corner of the layer view, returns the point in
+     * pixels measured from the last scroll position we sent to Gecko, in CSS pixels. Assuming the
+     * events being sent to Gecko are processed in FIFO order, this calculation should always be
+     * correct.
+     */
+    @Override
+    public PointF convertViewPointToLayerPoint(PointF viewPoint) {
+        if (!mGeckoIsReady) {
+            return null;
+        }
+
+        ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
+        PointF origin = viewportMetrics.getOrigin();
+        float zoom = viewportMetrics.zoomFactor;
+        ImmutableViewportMetrics geckoViewport = (AppConstants.MOZ_ANDROID_APZ ? mViewportMetrics : mGeckoViewport);
+        PointF geckoOrigin = geckoViewport.getOrigin();
+        float geckoZoom = geckoViewport.zoomFactor;
+
+        // viewPoint + origin - offset gives the coordinate in device pixels from the top-left corner of the page.
+        // Divided by zoom, this gives us the coordinate in CSS pixels from the top-left corner of the page.
+        // geckoOrigin / geckoZoom is where Gecko thinks it is (scrollTo position) in CSS pixels from
+        // the top-left corner of the page. Subtracting the two gives us the offset of the viewPoint from
+        // the current Gecko coordinate in CSS pixels.
+        PointF layerPoint = new PointF(
+                ((viewPoint.x + origin.x) / zoom) - (geckoOrigin.x / geckoZoom),
+                ((viewPoint.y + origin.y) / zoom) - (geckoOrigin.y / geckoZoom));
+
+        return layerPoint;
+    }
+
+    @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);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java
@@ -0,0 +1,316 @@
+/* -*- 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 final boolean isRTL;
+
+    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;
+        isRTL = false;
+    }
+
+    /** This constructor is used by native code in AndroidJavaWrappers.cpp, be
+     * careful when modifying the signature.
+     */
+    @WrapForJNI(allowMultithread = true)
+    public 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)
+    {
+        this(aPageRectLeft, aPageRectTop,
+             aPageRectRight, aPageRectBottom, aCssPageRectLeft,
+             aCssPageRectTop, aCssPageRectRight, aCssPageRectBottom,
+             aViewportRectLeft, aViewportRectTop, aViewportRectWidth,
+             aViewportRectHeight, aZoomFactor, false);
+    }
+
+    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, boolean aIsRTL)
+    {
+        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;
+        isRTL = aIsRTL;
+    }
+
+    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),
+            t >= 0.5 ? to.isRTL : isRTL);
+    }
+
+    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, isRTL);
+    }
+
+    public ImmutableViewportMetrics setViewportOrigin(float newOriginX, float newOriginY) {
+        return new ImmutableViewportMetrics(
+            pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+            cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+            newOriginX, newOriginY, viewportRectWidth, viewportRectHeight,
+            zoomFactor, isRTL);
+    }
+
+    public ImmutableViewportMetrics setZoomFactor(float newZoomFactor) {
+        return new ImmutableViewportMetrics(
+            pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+            cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+            viewportRectLeft, viewportRectTop, viewportRectWidth, viewportRectHeight,
+            newZoomFactor, isRTL);
+    }
+
+    public ImmutableViewportMetrics offsetViewportBy(float dx, float dy) {
+        return setViewportOrigin(viewportRectLeft + dx, viewportRectTop + dy);
+    }
+
+    public ImmutableViewportMetrics offsetViewportByAndClamp(float dx, float dy) {
+        if (isRTL) {
+            return setViewportOrigin(
+                Math.min(pageRectRight - getWidth(), Math.max(viewportRectLeft + dx, pageRectLeft)),
+                Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeight())));
+        }
+        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, isRTL);
+    }
+
+    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);
+    }
+
+    public ImmutableViewportMetrics setIsRTL(boolean aIsRTL) {
+        if (isRTL == aIsRTL) {
+            return this;
+        }
+
+        return new ImmutableViewportMetrics(
+            pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+            cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+            viewportRectLeft, viewportRectTop, viewportRectWidth, viewportRectHeight,
+            zoomFactor, aIsRTL);
+    }
+
+    /* This will set the zoom factor and re-scale page-size and viewport offset
+     * accordingly. The given focus will remain at the same point on the screen
+     * after scaling.
+     */
+    public ImmutableViewportMetrics scaleTo(float newZoomFactor, PointF focus) {
+        // cssPageRect* is invariant, since we're setting the scale factor
+        // here. The page rect is based on the CSS page rect.
+        float newPageRectLeft = cssPageRectLeft * newZoomFactor;
+        float newPageRectTop = cssPageRectTop * newZoomFactor;
+        float newPageRectRight = cssPageRectLeft + ((cssPageRectRight - cssPageRectLeft) * newZoomFactor);
+        float newPageRectBottom = cssPageRectTop + ((cssPageRectBottom - cssPageRectTop) * newZoomFactor);
+
+        PointF origin = getOrigin();
+        origin.offset(focus.x, focus.y);
+        origin = PointUtils.scale(origin, newZoomFactor / zoomFactor);
+        origin.offset(-focus.x, -focus.y);
+
+        return new ImmutableViewportMetrics(
+            newPageRectLeft, newPageRectTop, newPageRectRight, newPageRectBottom,
+            cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+            origin.x, origin.y, viewportRectWidth, viewportRectHeight,
+            newZoomFactor, isRTL);
+    }
+
+    /** 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, isRTL);
+    }
+
+    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 + ", rtl=" + isRTL;
+    }
+}
new file mode 100644
--- /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;
+    }
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/Layer.java
@@ -0,0 +1,204 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import org.mozilla.gecko.util.FloatUtils;
+
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import java.nio.FloatBuffer;
+import java.util.concurrent.locks.ReentrantLock;
+
+public abstract class Layer {
+    private final ReentrantLock mTransactionLock;
+    private boolean mInTransaction;
+    private Rect mNewPosition;
+    private float mNewResolution;
+
+    protected Rect mPosition;
+    protected float mResolution;
+
+    public Layer() {
+        this(null);
+    }
+
+    public Layer(IntSize size) {
+        mTransactionLock = new ReentrantLock();
+        if (size == null) {
+            mPosition = new Rect();
+        } else {
+            mPosition = new Rect(0, 0, size.width, size.height);
+        }
+        mResolution = 1.0f;
+    }
+
+    /**
+     * Updates the layer. This returns false if there is still work to be done
+     * after this update.
+     */
+    public final boolean update(RenderContext context) {
+        if (mTransactionLock.isHeldByCurrentThread()) {
+            throw new RuntimeException("draw() called while transaction lock held by this " +
+                                       "thread?!");
+        }
+
+        if (mTransactionLock.tryLock()) {
+            try {
+                performUpdates(context);
+                return true;
+            } finally {
+                mTransactionLock.unlock();
+            }
+        }
+
+        return false;
+    }
+
+    /** Subclasses override this function to draw the layer. */
+    public abstract void draw(RenderContext context);
+
+    /** Given the intrinsic size of the layer, returns the pixel boundaries of the layer rect. */
+    protected RectF getBounds(RenderContext context) {
+        return RectUtils.scale(new RectF(mPosition), context.zoomFactor / mResolution);
+    }
+
+    /**
+     * Call this before modifying the layer. Note that, for TileLayers, "modifying the layer"
+     * includes altering the underlying BufferedImage in any way. Thus you must call this function
+     * before modifying the byte buffer associated with this layer.
+     *
+     * This function may block, so you should never call this on the main UI thread.
+     */
+    public void beginTransaction() {
+        if (mTransactionLock.isHeldByCurrentThread())
+            throw new RuntimeException("Nested transactions are not supported");
+        mTransactionLock.lock();
+        mInTransaction = true;
+        mNewResolution = mResolution;
+    }
+
+    /** Call this when you're done modifying the layer. */
+    public void endTransaction() {
+        if (!mInTransaction)
+            throw new RuntimeException("endTransaction() called outside a transaction");
+        mInTransaction = false;
+        mTransactionLock.unlock();
+    }
+
+    /** Returns true if the layer is currently in a transaction and false otherwise. */
+    protected boolean inTransaction() {
+        return mInTransaction;
+    }
+
+    /** Returns the current layer position. */
+    public Rect getPosition() {
+        return mPosition;
+    }
+
+    /** Sets the position. Only valid inside a transaction. */
+    public void setPosition(Rect newPosition) {
+        if (!mInTransaction)
+            throw new RuntimeException("setPosition() is only valid inside a transaction");
+        mNewPosition = newPosition;
+    }
+
+    /** Returns the current layer's resolution. */
+    public float getResolution() {
+        return mResolution;
+    }
+
+    /**
+     * Sets the layer resolution. This value is used to determine how many pixels per
+     * device pixel this layer was rendered at. This will be reflected by scaling by
+     * the reciprocal of the resolution in the layer's transform() function.
+     * Only valid inside a transaction. */
+    public void setResolution(float newResolution) {
+        if (!mInTransaction)
+            throw new RuntimeException("setResolution() is only valid inside a transaction");
+        mNewResolution = newResolution;
+    }
+
+    /**
+     * Subclasses may override this method to perform custom layer updates. This will be called
+     * with the transaction lock held. Subclass implementations of this method must call the
+     * superclass implementation. Returns false if there is still work to be done after this
+     * update is complete.
+     */
+    protected void performUpdates(RenderContext context) {
+        if (mNewPosition != null) {
+            mPosition = mNewPosition;
+            mNewPosition = null;
+        }
+        if (mNewResolution != 0.0f) {
+            mResolution = mNewResolution;
+            mNewResolution = 0.0f;
+        }
+    }
+
+    /**
+     * This function fills in the provided <tt>dest</tt> array with values to render a texture.
+     * The array is filled with 4 sets of {x, y, z, texture_x, texture_y} values (so 20 values
+     * in total) corresponding to the corners of the rect.
+     */
+    protected final void fillRectCoordBuffer(float[] dest, RectF rect, float viewWidth, float viewHeight,
+                                             Rect cropRect, float texWidth, float texHeight) {
+        //x, y, z, texture_x, texture_y
+        dest[0] = rect.left / viewWidth;
+        dest[1] = rect.bottom / viewHeight;
+        dest[2] = 0;
+        dest[3] = cropRect.left / texWidth;
+        dest[4] = cropRect.top / texHeight;
+
+        dest[5] = rect.left / viewWidth;
+        dest[6] = rect.top / viewHeight;
+        dest[7] = 0;
+        dest[8] = cropRect.left / texWidth;
+        dest[9] = cropRect.bottom / texHeight;
+
+        dest[10] = rect.right / viewWidth;
+        dest[11] = rect.bottom / viewHeight;
+        dest[12] = 0;
+        dest[13] = cropRect.right / texWidth;
+        dest[14] = cropRect.top / texHeight;
+
+        dest[15] = rect.right / viewWidth;
+        dest[16] = rect.top / viewHeight;
+        dest[17] = 0;
+        dest[18] = cropRect.right / texWidth;
+        dest[19] = cropRect.bottom / texHeight;
+    }
+
+    public static class RenderContext {
+        public final RectF viewport;
+        public final RectF pageRect;
+        public final float zoomFactor;
+        public final int positionHandle;
+        public final int textureHandle;
+        public final FloatBuffer coordBuffer;
+
+        public RenderContext(RectF aViewport, RectF aPageRect, float aZoomFactor,
+                             int aPositionHandle, int aTextureHandle, FloatBuffer aCoordBuffer) {
+            viewport = aViewport;
+            pageRect = aPageRect;
+            zoomFactor = aZoomFactor;
+            positionHandle = aPositionHandle;
+            textureHandle = aTextureHandle;
+            coordBuffer = aCoordBuffer;
+        }
+
+        public boolean fuzzyEquals(RenderContext other) {
+            if (other == null) {
+                return false;
+            }
+            return RectUtils.fuzzyEquals(viewport, other.viewport)
+                && RectUtils.fuzzyEquals(pageRect, other.pageRect)
+                && FloatUtils.fuzzyEquals(zoomFactor, other.zoomFactor);
+        }
+    }
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerRenderer.java
@@ -0,0 +1,510 @@
+/* -*- 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.gfx.Layer.RenderContext;
+import org.mozilla.gecko.mozglue.DirectBufferAllocator;
+
+import android.graphics.Color;
+import android.graphics.RectF;
+import android.opengl.GLES20;
+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";
+    private static final String PROFTAG = "GeckoLayerRendererProf";
+
+    private static final int NANOS_PER_SECOND = 1000000000;
+
+    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 int mBackgroundColor;
+
+    private long mLastFrameTime;
+    private final CopyOnWriteArrayList<RenderTask> mTasks;
+
+    private final CopyOnWriteArrayList<Layer> mExtraLayers = new CopyOnWriteArrayList<Layer>();
+
+    // Render profiling output
+    private int mFramesRendered;
+    private float mCompleteFramesRendered;
+    private boolean mProfileRender;
+    private long mProfileOutputTime;
+
+    private IntBuffer mPixelBuffer;
+
+    // Used by GLES 2.0
+    private int mProgram;
+    private int mPositionHandle;
+    private int mTextureHandle;
+    private int mSampleHandle;
+    private int mTMatrixHandle;
+
+    private List<LayerView.ZoomedViewListener> mZoomedViewListeners;
+    private float mLastViewLeft;
+    private float mLastViewTop;
+
+    // column-major matrix applied to each vertex to shift the viewport from
+    // one ranging from (-1, -1),(1,1) to (0,0),(1,1) and to scale all sizes by
+    // a factor of 2 to fill up the screen
+    public static final float[] DEFAULT_TEXTURE_MATRIX = {
+        2.0f, 0.0f, 0.0f, 0.0f,
+        0.0f, 2.0f, 0.0f, 0.0f,
+        0.0f, 0.0f, 2.0f, 0.0f,
+        -1.0f, -1.0f, 0.0f, 1.0f
+    };
+
+    private static final int COORD_BUFFER_SIZE = 20;
+
+    // The shaders run on the GPU directly, the vertex shader is only applying the
+    // matrix transform detailed above
+
+    // Note we flip the y-coordinate in the vertex shader from a
+    // coordinate system with (0,0) in the top left to one with (0,0) in
+    // the bottom left.
+
+    public static final String DEFAULT_VERTEX_SHADER =
+        "uniform mat4 uTMatrix;\n" +
+        "attribute vec4 vPosition;\n" +
+        "attribute vec2 aTexCoord;\n" +
+        "varying vec2 vTexCoord;\n" +
+        "void main() {\n" +
+        "    gl_Position = uTMatrix * vPosition;\n" +
+        "    vTexCoord.x = aTexCoord.x;\n" +
+        "    vTexCoord.y = 1.0 - aTexCoord.y;\n" +
+        "}\n";
+
+    // We use highp because the screenshot textures
+    // we use are large and we stretch them alot
+    // so we need all the precision we can get.
+    // Unfortunately, highp is not required by ES 2.0
+    // so on GPU's like Mali we end up getting mediump
+    public static final String DEFAULT_FRAGMENT_SHADER =
+        "precision highp float;\n" +
+        "varying vec2 vTexCoord;\n" +
+        "uniform sampler2D sTexture;\n" +
+        "void main() {\n" +
+        "    gl_FragColor = texture2D(sTexture, vTexCoord);\n" +
+        "}\n";
+
+    public LayerRenderer(LayerView view) {
+        mView = view;
+
+        mTasks = new CopyOnWriteArrayList<RenderTask>();
+        mLastFrameTime = System.nanoTime();
+
+        mZoomedViewListeners = new ArrayList<LayerView.ZoomedViewListener>();
+    }
+
+    public void destroy() {
+        if (mCoordByteBuffer != null) {
+            DirectBufferAllocator.free(mCoordByteBuffer);
+            mCoordByteBuffer = null;
+            mCoordBuffer = null;
+        }
+        mZoomedViewListeners.clear();
+    }
+
+    void onSurfaceCreated(EGLConfig config) {
+        checkMonitoringEnabled();
+        createDefaultProgram();
+        activateDefaultProgram();
+    }
+
+    public void createDefaultProgram() {
+        int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DEFAULT_VERTEX_SHADER);
+        int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DEFAULT_FRAGMENT_SHADER);
+
+        mProgram = GLES20.glCreateProgram();
+        GLES20.glAttachShader(mProgram, vertexShader);   // add the vertex shader to program
+        GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
+        GLES20.glLinkProgram(mProgram);                  // creates OpenGL program executables
+
+        // Get handles to the vertex shader's vPosition, aTexCoord, sTexture, and uTMatrix members.
+        mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
+        mTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTexCoord");
+        mSampleHandle = GLES20.glGetUniformLocation(mProgram, "sTexture");
+        mTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uTMatrix");
+
+        int maxTextureSizeResult[] = new int[1];
+        GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxTextureSizeResult, 0);
+        mMaxTextureSize = maxTextureSizeResult[0];
+    }
+
+    // Activates the shader program.
+    public void activateDefaultProgram() {
+        // Add the program to the OpenGL environment
+        GLES20.glUseProgram(mProgram);
+
+        // Set the transformation matrix
+        GLES20.glUniformMatrix4fv(mTMatrixHandle, 1, false, DEFAULT_TEXTURE_MATRIX, 0);
+
+        // Enable the arrays from which we get the vertex and texture coordinates
+        GLES20.glEnableVertexAttribArray(mPositionHandle);
+        GLES20.glEnableVertexAttribArray(mTextureHandle);
+
+        GLES20.glUniform1i(mSampleHandle, 0);
+
+        // TODO: Move these calls into a separate deactivate() call that is called after the
+        // underlay and overlay are rendered.
+    }
+
+    // Deactivates the shader program. This must be done to avoid crashes after returning to the
+    // Gecko C++ compositor from Java.
+    public void deactivateDefaultProgram() {
+        GLES20.glDisableVertexAttribArray(mTextureHandle);
+        GLES20.glDisableVertexAttribArray(mPositionHandle);
+        GLES20.glUseProgram(0);
+    }
+
+    void restoreState(boolean enableScissor, int scissorX, int scissorY, int scissorW, int scissorH) {
+        GLES20.glScissor(scissorX, scissorY, scissorW, scissorH);
+        if (enableScissor) {
+            GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
+        } else {
+            GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
+        }
+    }
+
+    public int getMaxTextureSize() {
+        return mMaxTextureSize;
+    }
+
+    public void postRenderTask(RenderTask aTask) {
+        mTasks.add(aTask);
+        mView.requestRender();
+    }
+
+    public void removeRenderTask(RenderTask aTask) {
+        mTasks.remove(aTask);
+    }
+
+    private void runRenderTasks(CopyOnWriteArrayList<RenderTask> tasks, boolean after, long frameStartTime) {
+        for (RenderTask task : tasks) {
+            if (task.runAfter != after) {
+                continue;
+            }
+
+            boolean stillRunning = task.run(frameStartTime - mLastFrameTime, frameStartTime);
+
+            // Remove the task from the list if its finished
+            if (!stillRunning) {
+                tasks.remove(task);
+            }
+        }
+    }
+
+    public void addLayer(Layer layer) {
+        synchronized (mExtraLayers) {
+            if (mExtraLayers.contains(layer)) {
+                mExtraLayers.remove(layer);
+            }
+
+            mExtraLayers.add(layer);
+        }
+    }
+
+    public void removeLayer(Layer layer) {
+        synchronized (mExtraLayers) {
+            mExtraLayers.remove(layer);
+        }
+    }
+
+    private void printCheckerboardStats() {
+        Log.d(PROFTAG, "Frames rendered over last 1000ms: " + mCompleteFramesRendered + "/" + mFramesRendered);
+        mFramesRendered = 0;
+        mCompleteFramesRendered = 0;
+    }
+
+    /** Used by robocop for testing purposes. Not for production use! */
+    IntBuffer getPixels() {
+        IntBuffer pixelBuffer = IntBuffer.allocate(mView.getWidth() * mView.getHeight());
+        synchronized (pixelBuffer) {
+            mPixelBuffer = pixelBuffer;
+            mView.requestRender();
+            try {
+                pixelBuffer.wait();
+            } catch (InterruptedException ie) {
+            }
+            mPixelBuffer = null;
+        }
+        return pixelBuffer;
+    }
+
+    private RenderContext createScreenContext(ImmutableViewportMetrics metrics) {
+        RectF viewport = new RectF(0.0f, 0.0f, metrics.getWidth(), metrics.getHeight());
+        RectF pageRect = metrics.getPageRect();
+
+        return createContext(viewport, pageRect, 1.0f);
+    }
+
+    private RenderContext createPageContext(ImmutableViewportMetrics metrics) {
+        RectF viewport = metrics.getViewport();
+        RectF pageRect = metrics.getPageRect();
+        float zoomFactor = metrics.zoomFactor;
+
+        return createContext(new RectF(RectUtils.round(viewport)), pageRect, zoomFactor);
+    }
+
+    private RenderContext createContext(RectF viewport, RectF pageRect, float zoomFactor) {
+        if (mCoordBuffer == null) {
+            // Initialize the FloatBuffer that will be used to store all vertices and texture
+            // coordinates in draw() commands.
+            mCoordByteBuffer = DirectBufferAllocator.allocate(COORD_BUFFER_SIZE * 4);
+            mCoordByteBuffer.order(ByteOrder.nativeOrder());
+            mCoordBuffer = mCoordByteBuffer.asFloatBuffer();
+            if (mCoordBuffer == null) {
+                throw new IllegalStateException();
+            }
+        }
+        return new RenderContext(viewport, pageRect, zoomFactor,
+                                 mPositionHandle, mTextureHandle, mCoordBuffer);
+    }
+
+    void checkMonitoringEnabled() {
+        mProfileRender = Log.isLoggable(PROFTAG, Log.DEBUG);
+    }
+
+    /*
+     * create a vertex shader type (GLES20.GL_VERTEX_SHADER)
+     * or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
+     */
+    public static int loadShader(int type, String shaderCode) {
+        int shader = GLES20.glCreateShader(type);
+        GLES20.glShaderSource(shader, shaderCode);
+        GLES20.glCompileShader(shader);
+        return shader;
+    }
+
+    public Frame createFrame(ImmutableViewportMetrics metrics) {
+        return new Frame(metrics);
+    }
+
+    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;
+        // A rendering context for page-positioned layers, and one for screen-positioned layers.
+        private final RenderContext mPageContext, mScreenContext;
+        // Whether a layer was updated.
+        private boolean mUpdated;
+
+        public Frame(ImmutableViewportMetrics metrics) {
+            mFrameMetrics = metrics;
+
+            // Work out the offset due to margins
+            mPageContext = createPageContext(metrics);
+            mScreenContext = createScreenContext(metrics);
+        }
+
+        /** This function is invoked via JNI; be careful when modifying signature. */
+        @WrapForJNI(allowMultithread = true)
+        public void beginDrawing() {
+            mFrameStartTime = System.nanoTime();
+
+            TextureReaper.get().reap();
+            TextureGenerator.get().fill();
+
+            mUpdated = true;
+
+            Layer rootLayer = mView.getLayerClient().getRoot();
+
+            // Run through pre-render tasks
+            runRenderTasks(mTasks, false, mFrameStartTime);
+
+            /* Update layers. */
+            if (rootLayer != null) {
+                // Called on compositor thread.
+                mUpdated &= rootLayer.update(mPageContext);
+            }
+
+            for (Layer layer : mExtraLayers) {
+                mUpdated &= layer.update(mPageContext); // called on compositor thread
+            }
+        }
+
+        private void clear(int color) {
+            GLES20.glClearColor(((color >> 16) & 0xFF) / 255.0f,
+                                ((color >> 8) & 0xFF) / 255.0f,
+                                (color & 0xFF) / 255.0f,
+                                0.0f);
+            // The bits set here need to match up with those used
+            // in gfx/layers/opengl/LayerManagerOGL.cpp.
+            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT |
+                           GLES20.GL_DEPTH_BUFFER_BIT);
+        }
+
+        /** This function is invoked via JNI; be careful when modifying signature. */
+        @WrapForJNI(allowMultithread = true)
+        public void drawBackground() {
+            // Any GL state which is changed here must be restored in
+            // restoreState(...)
+
+            GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
+
+            // Update background color.
+            mBackgroundColor = mView.getBackgroundColor();
+
+            // Clear the page area to the page background colour.
+            clear(mBackgroundColor);
+        }
+
+        @WrapForJNI(allowMultithread = true)
+        public void drawForeground() {
+            // Any GL state which is changed here must be restored in
+            // restoreState(...)
+
+            /* Draw any extra layers that were added (likely plugins) */
+            if (mExtraLayers.size() > 0) {
+                for (Layer layer : mExtraLayers) {
+                    layer.draw(mPageContext);
+                }
+            }
+
+            /* Measure how much of the screen is checkerboarding */
+            Layer rootLayer = mView.getLayerClient().getRoot();
+            if ((rootLayer != null) &&
+                (mProfileRender || PanningPerfAPI.isRecordingCheckerboard())) {
+                // Calculate the incompletely rendered area of the page
+                float checkerboard =  1.0f - GeckoAppShell.computeRenderIntegrity();
+
+                PanningPerfAPI.recordCheckerboard(checkerboard);
+                if (checkerboard < 0.0f || checkerboard > 1.0f) {
+                    Log.e(LOGTAG, "Checkerboard value out of bounds: " + checkerboard);
+                }
+
+                mCompleteFramesRendered += 1.0f - checkerboard;
+                mFramesRendered ++;
+
+                if (mFrameStartTime - mProfileOutputTime > NANOS_PER_SECOND) {
+                    mProfileOutputTime = mFrameStartTime;
+                    printCheckerboardStats();
+                }
+            }
+
+            runRenderTasks(mTasks, true, mFrameStartTime);
+
+        }
+
+        private void maybeRequestZoomedViewRender(RenderContext context) {
+            // 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 = context.viewport.left;
+            final float viewTop = context.viewport.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(allowMultithread = true)
+        public void endDrawing() {
+            // If a layer update requires further work, schedule another redraw
+            if (!mUpdated)
+                mView.requestRender();
+
+            PanningPerfAPI.recordFrameTime();
+
+            maybeRequestZoomedViewRender(mPageContext);
+
+            /* Used by robocop for testing purposes */
+            IntBuffer pixelBuffer = mPixelBuffer;
+            if (mUpdated && pixelBuffer != null) {
+                synchronized (pixelBuffer) {
+                    pixelBuffer.position(0);
+                    GLES20.glReadPixels(0, 0, (int)mScreenContext.viewport.width(),
+                                        (int)mScreenContext.viewport.height(), GLES20.GL_RGBA,
+                                        GLES20.GL_UNSIGNED_BYTE, pixelBuffer);
+                    pixelBuffer.notify();
+                }
+            }
+
+            // Remove background color once we've painted. GeckoLayerClient is
+            // responsible for setting this flag before current document is
+            // composited.
+            if (mView.getPaintState() == LayerView.PAINT_BEFORE_FIRST) {
+                mView.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        mView.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);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java
@@ -0,0 +1,721 @@
+/* -*- 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.GeckoEvent;
+import org.mozilla.gecko.GeckoThread;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+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.LinearLayout;
+import android.widget.ScrollView;
+
+/**
+ * A view rendered by the layer compositor.
+ */
+public class LayerView extends ScrollView {
+    private static final String LOGTAG = "GeckoLayerView";
+
+    private GeckoLayerClient mLayerClient;
+    private PanZoomController mPanZoomController;
+    private DynamicToolbarAnimator mToolbarAnimator;
+    private GLController mGLController;
+    private LayerRenderer mRenderer;
+    /* Must be a PAINT_xxx constant */
+    private int mPaintState;
+    private int mBackgroundColor;
+    private FullScreenState mFullScreenState;
+
+    private SurfaceView mSurfaceView;
+    private TextureView mTextureView;
+    private View mFillerView;
+
+    private Listener mListener;
+
+    private PointF mInitialTouchPoint;
+
+    private float mSurfaceTranslation;
+
+    /* This should only be modified on the Java UI thread. */
+    private final Overscroll mOverscroll;
+
+    /* Flags used to determine when to show the painted surface. */
+    public static final int PAINT_START = 0;
+    public static final int PAINT_BEFORE_FIRST = 1;
+    public static final int PAINT_AFTER_FIRST = 2;
+
+    public boolean shouldUseTextureView() {
+        // Disable TextureView support for now as it causes panning/zooming
+        // performance regressions (see bug 792259). Uncomment the code below
+        // once this bug is fixed.
+        return false;
+
+        /*
+        // we can only use TextureView on ICS or higher
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+            Log.i(LOGTAG, "Not using TextureView: not on ICS+");
+            return false;
+        }
+
+        try {
+            // and then we can only use it if we have a hardware accelerated window
+            Method m = View.class.getMethod("isHardwareAccelerated", (Class[]) null);
+            return (Boolean) m.invoke(this);
+        } catch (Exception e) {
+            Log.i(LOGTAG, "Not using TextureView: caught exception checking for hw accel: " + e.toString());
+            return false;
+        } */
+    }
+
+    public LayerView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        mPaintState = PAINT_START;
+        mBackgroundColor = Color.WHITE;
+        mFullScreenState = FullScreenState.NONE;
+
+        if (Versions.feature14Plus) {
+            mOverscroll = new OverscrollEdgeEffect(this);
+        } else {
+            mOverscroll = null;
+        }
+    }
+
+    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);
+        GeckoAccessibility.setAccessibilityManagerListeners(getContext());
+    }
+
+    /**
+     * 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);
+    }
+
+    private boolean sendEventToGecko(MotionEvent event) {
+        if (!mLayerClient.isGeckoReady()) {
+            return false;
+        }
+
+        int action = event.getActionMasked();
+        PointF point = new PointF(event.getX(), event.getY());
+        if (action == MotionEvent.ACTION_DOWN) {
+            mInitialTouchPoint = point;
+        }
+
+        if (mInitialTouchPoint != null && action == MotionEvent.ACTION_MOVE) {
+            Point p = getEventRadius(event);
+
+            if (PointUtils.subtract(point, mInitialTouchPoint).length() <
+                Math.max(PanZoomController.CLICK_THRESHOLD, Math.min(Math.min(p.x, p.y), PanZoomController.PAN_THRESHOLD))) {
+                // Don't send the touchmove event if if the users finger hasn't moved far.
+                // Necessary for Google Maps to work correctly. See bug 771099.
+                return true;
+            } else {
+                mInitialTouchPoint = null;
+            }
+        }
+
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createMotionEvent(event, false));
+        return true;
+    }
+
+    public void showSurface() {
+        // Fix this if TextureView support is turned back on above
+        mSurfaceView.setVisibility(View.VISIBLE);
+    }
+
+    public void hideSurface() {
+        // Fix this if TextureView support is turned back on above
+        mSurfaceView.setVisibility(View.INVISIBLE);
+    }
+
+    public void destroy() {
+        if (mLayerClient != null) {
+            mLayerClient.destroy();
+        }
+        if (mRenderer != null) {
+            mRenderer.destroy();
+        }
+        if (mGLController != null) {
+            if (mGLController.mView == this) {
+                mGLController.mView = null;
+            }
+            mGLController = null;
+        }
+    }
+
+    @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();
+        }
+        event.offsetLocation(0, -mSurfaceTranslation);
+
+        if (mToolbarAnimator != null && mToolbarAnimator.onInterceptTouchEvent(event)) {
+            if (mPanZoomController != null) {
+                mPanZoomController.onMotionEventVelocity(event.getEventTime(), mToolbarAnimator.getVelocity());
+            }
+            return true;
+        }
+        if (AppConstants.MOZ_ANDROID_APZ && !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 sendEventToGecko(event);
+    }
+
+    @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;
+        }
+
+        event.offsetLocation(0, -mSurfaceTranslation);
+
+        if (AppConstants.MOZ_ANDROID_APZ) {
+            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 sendEventToGecko(event);
+    }
+
+    @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        event.offsetLocation(0, -mSurfaceTranslation);
+
+        if (AndroidGamepadManager.handleMotionEvent(event)) {
+            return true;
+        }
+        if (AppConstants.MOZ_ANDROID_APZ && !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 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);
+
+            // The "filler" view sits behind the URL bar and should never be
+            // visible. It exists solely to make this LayerView actually
+            // scrollable so that we can shift the surface around on the screen.
+            // Once we drop support for pre-Honeycomb Android versions this
+            // should not be needed; we can just turn LayerView back into a
+            // FrameLayout that holds mSurfaceView and nothing else.
+            mFillerView = new View(getContext()) {
+                @Override protected void onMeasure(int aWidthSpec, int aHeightSpec) {
+                    setMeasuredDimension(0, Math.round(mToolbarAnimator.getMaxTranslation()));
+                }
+            };
+            mFillerView.setBackgroundColor(Color.RED);
+
+            LinearLayout container = new LinearLayout(getContext());
+            container.setOrientation(LinearLayout.VERTICAL);
+            container.addView(mFillerView);
+            container.addView(mSurfaceView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+            addView(container, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+
+            SurfaceHolder holder = mSurfaceView.getHolder();
+            holder.addCallback(new SurfaceListener());
+        }
+    }
+
+    // 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 void abortPanning() {
+        if (mPanZoomController != null) {
+            mPanZoomController.abortPanning();
+        }
+    }
+
+    public PointF convertViewPointToLayerPoint(PointF viewPoint) {
+        return mLayerClient.convertViewPointToLayerPoint(viewPoint);
+    }
+
+    int getBackgroundColor() {
+        return mBackgroundColor;
+    }
+
+    @Override
+    public void setBackgroundColor(int newColor) {
+        mBackgroundColor = newColor;
+        requestRender();
+    }
+
+    public void setSurfaceBackgroundColor(int newColor) {
+        if (mSurfaceView != null) {
+            mSurfaceView.setBackgroundColor(newColor);
+        }
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (AppConstants.MOZ_ANDROID_APZ && !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.onKeyEvent(event)) {
+            return true;
+        }
+        return false;
+    }
+
+    public void requestRender() {
+        if (mListener != null) {
+            mListener.renderRequested();
+        }
+    }
+
+    public void addLayer(Layer layer) {
+        mRenderer.addLayer(layer);
+    }
+
+    public void removeLayer(Layer layer) {
+        mRenderer.removeLayer(layer);
+    }
+
+    public void postRenderTask(RenderTask task) {
+        mRenderer.postRenderTask(task);
+    }
+
+    public void removeRenderTask(RenderTask task) {
+        mRenderer.removeRenderTask(task);
+    }
+
+    public int getMaxTextureSize() {
+        return mRenderer.getMaxTextureSize();
+    }
+
+    /** Used by robocop for testing purposes. Not for production use! */
+    @RobocopTarget
+    public IntBuffer getPixels() {
+        return mRenderer.getPixels();
+    }
+
+    /* paintState must be a PAINT_xxx constant. */
+    public void setPaintState(int paintState) {
+        mPaintState = paintState;
+    }
+
+    public int getPaintState() {
+        return mPaintState;
+    }
+
+    public LayerRenderer getRenderer() {
+        return mRenderer;
+    }
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    Listener getListener() {
+        return mListener;
+    }
+
+    public void setGLController(final GLController glController) {
+        mGLController = glController;
+        glController.mView = this;
+
+        final NativePanZoomController npzc = AppConstants.MOZ_ANDROID_APZ ?
+                (NativePanZoomController) mPanZoomController : null;
+
+        if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+            glController.attachToJava(mLayerClient, npzc);
+        } else {
+            GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
+                    glController, "attachToJava",
+                    GeckoLayerClient.class, mLayerClient,
+                    NativePanZoomController.class, npzc);
+        }
+    }
+
+    public GLController getGLController() {
+        return mGLController;
+    }
+
+    /* When using a SurfaceView (mSurfaceView != null), resizing happens in two
+     * phases. First, the LayerView changes size, then, often some frames later,
+     * the SurfaceView changes size. Because of this, we need to split the
+     * resize into two phases to avoid jittering.
+     *
+     * The first phase is the LayerView size change. mListener is notified so
+     * that a synchronous draw can be performed (otherwise a blank frame will
+     * appear).
+     *
+     * The second phase is the SurfaceView size change. At this point, the
+     * backing GL surface is resized and another synchronous draw is performed.
+     * Gecko is also sent the new window size, and this will likely cause an
+     * extra draw a few frames later, after it's re-rendered and caught up.
+     *
+     * In the case that there is no valid GL surface (for example, when
+     * resuming, or when coming back from the awesomescreen), or we're using a
+     * TextureView instead of a SurfaceView, the first phase is skipped.
+     */
+    private void onSizeChanged(int width, int height) {
+        if (!mGLController.isServerSurfaceValid() || mSurfaceView == null) {
+            surfaceChanged(width, height);
+            return;
+        }
+
+        if (mListener != null) {
+            mListener.sizeChanged(width, height);
+        }
+
+        if (mOverscroll != null) {
+            mOverscroll.setSize(width, height);
+        }
+    }
+
+    private void surfaceChanged(int width, int height) {
+        mGLController.serverSurfaceChanged(width, height);
+
+        if (mListener != null) {
+            mListener.surfaceChanged(width, height);
+        }
+
+        if (mOverscroll != null) {
+            mOverscroll.setSize(width, height);
+        }
+    }
+
+    private void onDestroyed() {
+        mGLController.serverSurfaceDestroyed();
+    }
+
+    public Object getNativeWindow() {
+        if (mSurfaceView != null)
+            return mSurfaceView.getHolder();
+
+        return mTextureView.getSurfaceTexture();
+    }
+
+    public Object getSurface() {
+      if (mSurfaceView != null) {
+        return mSurfaceView.getHolder().getSurface();
+      }
+      return null;
+    }
+
+    //This method is called on the Gecko main thread.
+    @WrapForJNI(allowMultithread = true, stubName = "updateZoomedView")
+    public 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 renderRequested();
+        void sizeChanged(int width, int height);
+        void surfaceChanged(int width, int height);
+    }
+
+    private class SurfaceListener implements SurfaceHolder.Callback {
+        @Override
+        public void surfaceChanged(SurfaceHolder holder, int format, int width,
+                                                int height) {
+            onSizeChanged(width, height);
+        }
+
+        @Override
+        public void surfaceCreated(SurfaceHolder holder) {
+        }
+
+        @Override
+        public void surfaceDestroyed(SurfaceHolder holder) {
+            onDestroyed();
+        }
+    }
+
+    @Override
+    protected void onMeasure(int aWidthSpec, int aHeightSpec) {
+        super.onMeasure(aWidthSpec, aHeightSpec);
+        if (mSurfaceView != null) {
+            // Because of the crazy setup where this LayerView is a ScrollView
+            // and the SurfaceView is inside a LinearLayout, the SurfaceView
+            // doesn't get the right information to size itself the way we want.
+            // We always want it to be the same size as this LayerView, so we
+            // use a hack to make sure it sizes itself that way.
+            ((LayerSurfaceView)mSurfaceView).overrideSize(getMeasuredWidth(), getMeasuredHeight());
+        }
+    }
+
+    /* A subclass of SurfaceView to listen to layout changes, as
+     * View.OnLayoutChangeListener requires API level 11.
+     */
+    private class LayerSurfaceView extends SurfaceView {
+        private LayerView mParent;
+        private int mForcedWidth;
+        private int mForcedHeight;
+
+        public LayerSurfaceView(Context aContext, LayerView aParent) {
+            super(aContext);
+            mParent = aParent;
+        }
+
+        void overrideSize(int aWidth, int aHeight) {
+            if (mForcedWidth != aWidth || mForcedHeight != aHeight) {
+                mForcedWidth = aWidth;
+                mForcedHeight = aHeight;
+                requestLayout();
+            }
+        }
+
+        @Override
+        protected void onMeasure(int aWidthSpec, int aHeightSpec) {
+            setMeasuredDimension(mForcedWidth, mForcedHeight);
+        }
+
+        @Override
+        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+            super.onLayout(changed, left, top, right, bottom);
+            if (changed && mParent.mGLController.isServerSurfaceValid()) {
+                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);
+        if (mPanZoomController != null) {
+            mPanZoomController.setOverScrollMode(overscrollMode);
+        }
+    }
+
+    @Override
+    public int getOverScrollMode() {
+        if (mPanZoomController != null) {
+            return mPanZoomController.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(this, gainFocus);
+    }
+
+    public void setFullScreenState(FullScreenState state) {
+        mFullScreenState = state;
+    }
+
+    public boolean isFullScreen() {
+        return mFullScreenState != FullScreenState.NONE;
+    }
+
+    public FullScreenState getFullScreenState() {
+        return mFullScreenState;
+    }
+
+    public void setMaxTranslation(float aMaxTranslation) {
+        mToolbarAnimator.setMaxTranslation(aMaxTranslation);
+        if (mFillerView != null) {
+            mFillerView.requestLayout();
+        }
+    }
+
+    public void setSurfaceTranslation(float translation) {
+        // Once we drop support for pre-Honeycomb Android versions, we can
+        // revert bug 1197811 and just use ViewHelper here.
+        if (mSurfaceTranslation != translation) {
+            mSurfaceTranslation = translation;
+            scrollTo(0, Math.round(mToolbarAnimator.getMaxTranslation() - translation));
+        }
+    }
+
+    public float getSurfaceTranslation() {
+        return mSurfaceTranslation;
+    }
+
+    // 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);
+    }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java
@@ -0,0 +1,348 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoEvent;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.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 final PrefsHelper.PrefHandler mPrefsObserver;
+    private long mLastDownTime;
+    private static final float MAX_SCROLL = 0.075f * GeckoAppShell.getDpi();
+
+    @WrapForJNI
+    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
+    private native boolean handleScrollEvent(
+            long time, int metaState,
+            float x, float y,
+            float hScroll, float vScroll);
+
+    @WrapForJNI
+    private native boolean handleMouseEvent(
+            int action, long time, int metaState,
+            float x, float y, int buttons);
+
+    @WrapForJNI
+    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;
+
+        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) {
+        if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
+            return handleMouseEvent(event);
+        } else {
+            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 boolean onKeyEvent(KeyEvent event) {
+        // FIXME implement this
+        return false;
+    }
+
+    @Override
+    public void onMotionEventVelocity(final long aEventTime, final float aSpeedY) {
+        handleMotionEventVelocity(aEventTime, aSpeedY);
+    }
+
+    @Override
+    public PointF getVelocityVector() {
+        // FIXME implement this
+        return new PointF(0, 0);
+    }
+
+    @Override
+    public void pageRectUpdated() {
+        // no-op in APZC, I think
+    }
+
+    @Override
+    public void abortPanning() {
+        // no-op in APZC, I think
+    }
+
+    @Override
+    public void notifyDefaultActionPrevented(boolean prevented) {
+        // no-op: This could get called if accessibility is enabled and the events
+        // are sent to Gecko directly without going through APZ. In this case
+        // we just want to ignore this callback.
+    }
+
+    @WrapForJNI(stubName = "AbortAnimation")
+    private native void nativeAbortAnimation();
+
+    @Override // PanZoomController
+    public void abortAnimation()
+    {
+        if (!mDestroyed) {
+            nativeAbortAnimation();
+        }
+    }
+
+    @Override // PanZoomController
+    public boolean getRedrawHint()
+    {
+        // FIXME implement this
+        return true;
+    }
+
+    @Override @WrapForJNI(allowMultithread = true) // PanZoomController
+    public void destroy() {
+        if (mDestroyed) {
+            return;
+        }
+        mDestroyed = true;
+        disposeNative();
+    }
+
+    @Override @WrapForJNI // JNIObject
+    protected native void disposeNative();
+
+    @Override
+    public void setOverScrollMode(int overscrollMode) {
+        // FIXME implement this
+    }
+
+    @Override
+    public int getOverScrollMode() {
+        // FIXME implement this
+        return 0;
+    }
+
+    @WrapForJNI(allowMultithread = true, stubName = "RequestContentRepaintWrapper")
+    private void requestContentRepaint(float x, float y, float width, float height, float resolution) {
+        mTarget.forceRedraw(new DisplayPortMetrics(x, y, x + width, y + height, resolution));
+    }
+
+    @Override
+    public void setOverscrollHandler(final Overscroll handler) {
+        mOverscroll = handler;
+    }
+
+    @WrapForJNI(stubName = "SetIsLongpressEnabled")
+    private native void nativeSetIsLongpressEnabled(boolean isLongpressEnabled);
+
+    @Override // PanZoomController
+    public void setIsLongpressEnabled(boolean isLongpressEnabled) {
+        if (!mDestroyed) {
+            nativeSetIsLongpressEnabled(isLongpressEnabled);
+        }
+    }
+
+    @WrapForJNI(stubName = "AdjustScrollForSurfaceShift")
+    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(allowMultithread = true)
+    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(allowMultithread = true)
+    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
+    private void setScrollingRootContent(final boolean isRootContent) {
+        mTarget.setScrollingRootContent(isRootContent);
+    }
+
+    /**
+     * Active SelectionCaretDrag requires DynamicToolbarAnimator to be pinned
+     * to avoid unwanted scroll interactions.
+     */
+    @WrapForJNI
+    private void onSelectionDragState(boolean state) {
+        mView.getDynamicToolbarAnimator().setPinned(state, PinReason.CARET_DRAG);
+    }
+}
new file mode 100644
--- /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);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java
@@ -0,0 +1,163 @@
+/* -*- 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;
+        }
+
+        float fillerSize = mView.getDynamicToolbarAnimator().getMaxTranslation();
+        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, fillerSize, 0);
+        }
+
+        if (!mEdges[BOTTOM].isFinished()) {
+            invalidate |= draw(mEdges[BOTTOM], canvas, visibleEnd.x, fillerSize + visibleEnd.y, 180);
+        }
+
+        if (!mEdges[LEFT].isFinished()) {
+            invalidate |= draw(mEdges[LEFT], canvas, 0, fillerSize + visibleEnd.y, 270);
+        }
+
+        if (!mEdges[RIGHT].isFinished()) {
+            invalidate |= draw(mEdges[RIGHT], canvas, visibleEnd.x, fillerSize, 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;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomController.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.gfx;
+
+import android.graphics.PointF;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.GeckoAppShell;
+
+public interface PanZoomController {
+    // The distance the user has to pan before we recognize it as such (e.g. to avoid 1-pixel pans
+    // between the touch-down and touch-up of a click). In units of density-independent pixels.
+    public static final float PAN_THRESHOLD = 1 / 16f * GeckoAppShell.getDpi();
+
+    // 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) {
+            if (org.mozilla.gecko.AppConstants.MOZ_ANDROID_APZ) {
+                return new NativePanZoomController(target, view);
+            } else {
+                throw new IllegalStateException("Must set MOZ_ANDROID_APZ");
+            }
+        }
+    }
+
+    public void destroy();
+
+    public boolean onTouchEvent(MotionEvent event);
+    public boolean onMotionEvent(MotionEvent event);
+    public boolean onKeyEvent(KeyEvent event);
+    public void onMotionEventVelocity(final long aEventTime, final float aSpeedY);
+    public void notifyDefaultActionPrevented(boolean prevented);
+
+    public boolean getRedrawHint();
+    public PointF getVelocityVector();
+
+    public void pageRectUpdated();
+    public void abortPanning();
+    public void abortAnimation();
+
+    public void setOverScrollMode(int overscrollMode);
+    public int getOverScrollMode();
+
+    public void setOverscrollHandler(final Overscroll controller);
+
+    public void setIsLongpressEnabled(boolean isLongpressEnabled);
+
+    public ImmutableViewportMetrics adjustScrollForSurfaceShift(ImmutableViewportMetrics aMetrics, PointF aShift);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomTarget.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 android.graphics.PointF;
+import android.graphics.RectF;
+
+public interface PanZoomTarget {
+    public ImmutableViewportMetrics getViewportMetrics();
+    public FullScreenState getFullScreenState();
+    public PointF getVisibleEndOfLayerView();
+
+    public void setAnimationTarget(ImmutableViewportMetrics viewport);
+    public void setViewportMetrics(ImmutableViewportMetrics viewport);
+    public void scrollBy(float dx, float dy);
+    public void panZoomStopped();
+    /** This triggers an (asynchronous) viewport update/redraw. */
+    public void forceRedraw(DisplayPortMetrics displayPort);
+
+    public boolean post(Runnable action);
+    public void postRenderTask(RenderTask task);
+    public void removeRenderTask(RenderTask task);
+    public Object getLock();
+    public PointF convertViewPointToLayerPoint(PointF viewPoint);
+    public void setScrollingRootContent(boolean isRootContent);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java
@@ -0,0 +1,127 @@
+/* -*- 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 boolean mRecordingCheckerboard;
+    private static List<Float> mCheckerboardAmounts;
+    private static long mCheckerboardStartTime;
+
+    private static void initialiseRecordingArrays() {
+        if (mFrameTimes == null) {
+            mFrameTimes = new ArrayList<Long>(EXPECTED_FRAME_COUNT);
+        } else {
+            mFrameTimes.clear();
+        }
+        if (mCheckerboardAmounts == null) {
+            mCheckerboardAmounts = new ArrayList<Float>(EXPECTED_FRAME_COUNT);
+        } else {
+            mCheckerboardAmounts.clear();
+        }
+    }
+
+    @RobocopTarget
+    public static void startFrameTimeRecording() {
+        if (mRecordingFrames || mRecordingCheckerboard) {
+            Log.e(LOGTAG, "Error: startFrameTimeRecording() called while already recording!");
+            return;
+        }
+        mRecordingFrames = true;
+        initialiseRecordingArrays();
+        mFrameStartTime = SystemClock.uptimeMillis();
+    }
+
+    @RobocopTarget
+    public static List<Long> stopFrameTimeRecording() {
+        if (!mRecordingFrames) {
+            Log.e(LOGTAG, "Error: stopFrameTimeRecording() called when not recording!");
+            return null;
+        }
+        mRecordingFrames = false;
+        return mFrameTimes;
+    }
+
+    public static void recordFrameTime() {
+        // this will be called often, so try to make it as quick as possible
+        if (mRecordingFrames) {
+            mFrameTimes.add(SystemClock.uptimeMillis() - mFrameStartTime);
+        }
+    }
+
+    public static boolean isRecordingCheckerboard() {
+        return mRecordingCheckerboard;
+    }
+
+    @RobocopTarget
+    public static void startCheckerboardRecording() {
+        if (mRecordingCheckerboard || mRecordingFrames) {
+            Log.e(LOGTAG, "Error: startCheckerboardRecording() called while already recording!");
+            return;
+        }
+        mRecordingCheckerboard = true;
+        initialiseRecordingArrays();
+        mCheckerboardStartTime = SystemClock.uptimeMillis();
+    }
+
+    @RobocopTarget
+    public static List<Float> stopCheckerboardRecording() {
+        if (!mRecordingCheckerboard) {
+            Log.e(LOGTAG, "Error: stopCheckerboardRecording() called when not recording!");
+            return null;
+        }
+        mRecordingCheckerboard = false;
+
+        // We take the number of values in mCheckerboardAmounts here, as there's
+        // the possibility that this function is called while recordCheckerboard
+        // is still executing. As values are added to this list last, we use
+        // this number as the canonical number of recordings.
+        int values = mCheckerboardAmounts.size();
+        if (values == 0) {
+            Log.w(LOGTAG, "stopCheckerboardRecording() found no checkerboard amounts!");
+            return mCheckerboardAmounts;
+        }
+
+        // The score will be the sum of all the values in mCheckerboardAmounts,
+        // so weight the checkerboard values by time so that frame-rate and
+        // run-length don't affect score.
+        long lastTime = 0;
+        float totalTime = mFrameTimes.get(values - 1);
+        for (int i = 0; i < values; i++) {
+            long elapsedTime = mFrameTimes.get(i) - lastTime;
+            mCheckerboardAmounts.set(i, mCheckerboardAmounts.get(i) * elapsedTime / totalTime);
+            lastTime += elapsedTime;
+        }
+
+        return mCheckerboardAmounts;
+    }
+
+    public static void recordCheckerboard(float amount) {
+        // this will be called often, so try to make it as quick as possible
+        if (mRecordingCheckerboard) {
+            mFrameTimes.add(SystemClock.uptimeMillis() - mCheckerboardStartTime);
+            mCheckerboardAmounts.add(amount);
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PluginLayer.java
@@ -0,0 +1,167 @@
+/* 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.util.FloatUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.SurfaceView;
+import android.view.View;
+import android.widget.AbsoluteLayout;
+
+public class PluginLayer extends Layer {
+    private static final String LOGTAG = "PluginLayer";
+
+    private final View mView;
+    private SurfaceView mSurfaceView;
+    private final PluginLayoutParams mLayoutParams;
+    private final AbsoluteLayout mContainer;
+
+    private boolean mDestroyed;
+    private boolean mViewVisible;
+
+    private RectF mLastViewport;
+    private float mLastZoomFactor;
+
+    private static final float TEXTURE_MAP[] = {
+                0.0f, 1.0f, // top left
+                0.0f, 0.0f, // bottom left
+                1.0f, 1.0f, // top right
+                1.0f, 0.0f, // bottom right
+    };
+
+    public PluginLayer(View view, RectF rect, int maxDimension) {
+        super(new IntSize(0, 0));
+
+        mView = view;
+        mContainer = GeckoAppShell.getGeckoInterface().getPluginContainer();
+
+        mView.setWillNotDraw(false);
+        if (mView instanceof SurfaceView) {
+            mSurfaceView = (SurfaceView)view;
+            mSurfaceView.setZOrderOnTop(false);
+            mSurfaceView.setZOrderMediaOverlay(true);
+        }
+
+        mLayoutParams = new PluginLayoutParams(rect, maxDimension);
+    }
+
+    public void setVisible(boolean visible) {
+        if (visible) {
+            showView();
+        } else {
+            hideView();
+        }
+    }
+
+    private void hideView() {
+        if (mViewVisible) {
+            ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    mView.setVisibility(View.GONE);
+                    mViewVisible = false;
+                }
+            });
+        }
+    }
+
+    public void showView() {
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                if (mContainer.indexOfChild(mView) < 0) {
+                    mContainer.addView(mView, mLayoutParams);
+                } else {
+                    mContainer.updateViewLayout(mView, mLayoutParams);
+                    mView.setVisibility(View.VISIBLE);
+                }
+                mViewVisible = true;
+            }
+        });
+    }
+
+    public void destroy() {
+        mDestroyed = true;
+
+        mContainer.removeView(mView);
+    }
+
+    public void reset(RectF rect) {
+        mLayoutParams.reset(rect);
+    }
+
+    @Override
+    protected void performUpdates(RenderContext context) {
+        if (mDestroyed)
+            return;
+
+        if (!RectUtils.fuzzyEquals(context.viewport, mLastViewport) ||
+            !FloatUtils.fuzzyEquals(context.zoomFactor, mLastZoomFactor)) {
+
+            mLastZoomFactor = context.zoomFactor;
+            mLastViewport = context.viewport;
+            mLayoutParams.reposition(context.viewport, context.zoomFactor);
+
+            showView();
+        }
+    }
+
+    @Override
+    public void draw(RenderContext context) {
+    }
+
+    class PluginLayoutParams extends AbsoluteLayout.LayoutParams
+    {
+        private static final String LOGTAG = "GeckoApp.PluginLayoutParams";
+
+        private RectF mRect;
+        private final int mMaxDimension;
+        private float mLastResolution;
+
+        public PluginLayoutParams(RectF rect, int maxDimension) {
+            super(0, 0, 0, 0);
+
+            mMaxDimension = maxDimension;
+            reset(rect);
+        }
+
+        private void clampToMaxSize() {
+            if (width > mMaxDimension || height > mMaxDimension) {
+                if (width > height) {
+                    height = Math.round(((float) height / width) * mMaxDimension);
+                    width = mMaxDimension;
+                } else {
+                    width = Math.round(((float) width / height) * mMaxDimension);
+                    height = mMaxDimension;
+                }
+            }
+        }
+
+        public void reset(RectF rect) {
+            mRect = rect;
+        }
+
+        public void reposition(RectF viewport, float zoomFactor) {
+
+            RectF scaled = RectUtils.scale(mRect, zoomFactor);
+
+            this.x = Math.round(scaled.left - viewport.left);
+            this.y = Math.round(scaled.top - viewport.top);
+
+            if (!FloatUtils.fuzzyEquals(mLastResolution, zoomFactor)) {
+                width = Math.round(mRect.width() * zoomFactor);
+                height = Math.round(mRect.height() * zoomFactor);
+                mLastResolution = zoomFactor;
+
+                clampToMaxSize();
+            }
+        }
+    }
+}
new file mode 100644
--- /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;
+    }
+}
+
new file mode 100644
--- /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;
+    }
+}
+
new file mode 100644
--- /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);
+    }
+}
new file mode 100644
--- /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;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/TextureGenerator.java
@@ -0,0 +1,77 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import android.opengl.GLES20;
+import android.util.Log;
+
+import java.util.concurrent.ArrayBlockingQueue;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLContext;
+
+public class TextureGenerator {
+    private static final String LOGTAG = "TextureGenerator";
+    private static final int POOL_SIZE = 5;
+
+    private static TextureGenerator sSharedInstance;
+
+    private final ArrayBlockingQueue<Integer> mTextureIds;
+    private EGLContext mContext;
+
+    private TextureGenerator() { mTextureIds = new ArrayBlockingQueue<Integer>(POOL_SIZE); }
+
+    public static TextureGenerator get() {
+        if (sSharedInstance == null)
+            sSharedInstance = new TextureGenerator();
+        return sSharedInstance;
+    }
+
+    public synchronized int take() {
+        try {
+            // Will block until one becomes available
+            return (int)mTextureIds.take();
+        } catch (InterruptedException e) {
+            return 0;
+        }
+    }
+
+    public synchronized void fill() {
+        EGL10 egl = (EGL10)EGLContext.getEGL();
+        EGLContext context = egl.eglGetCurrentContext();
+
+        if (mContext != null && mContext != context) {
+            mTextureIds.clear();
+        }
+
+        mContext = context;
+
+        int numNeeded = mTextureIds.remainingCapacity();
+        if (numNeeded == 0)
+            return;
+
+        // Clear existing GL errors
+        int error;
+        while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
+            Log.w(LOGTAG, String.format("Clearing GL error: %#x", error));
+        }
+
+        int[] textures = new int[numNeeded];
+        GLES20.glGenTextures(numNeeded, textures, 0);
+
+        error = GLES20.glGetError();
+        if (error != GLES20.GL_NO_ERROR) {
+            Log.e(LOGTAG, String.format("Failed to generate textures: %#x", error), new Exception());
+            return;
+        }
+
+        for (int i = 0; i < numNeeded; i++) {
+            mTextureIds.offer(textures[i]);
+        }
+    }
+}
+
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/TextureReaper.java
@@ -0,0 +1,51 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import android.opengl.GLES20;
+
+import java.util.ArrayList;
+
+/** Manages a list of dead tiles, so we don't leak resources. */
+public class TextureReaper {
+    private static TextureReaper sSharedInstance;
+    private final ArrayList<Integer> mDeadTextureIDs;
+
+    private TextureReaper() { mDeadTextureIDs = new ArrayList<Integer>(); }
+
+    public static TextureReaper get() {
+        if (sSharedInstance == null)
+            sSharedInstance = new TextureReaper();
+        return sSharedInstance;
+    }
+
+    public void add(int[] textureIDs) {
+        for (int textureID : textureIDs)
+            add(textureID);
+    }
+
+    public void add(int textureID) {
+        mDeadTextureIDs.add(textureID);
+    }
+
+    public void reap() {
+        int numTextures = mDeadTextureIDs.size();
+        // Adreno 200 will generate INVALID_VALUE if len == 0 is passed to glDeleteTextures,
+        // even though it's not supposed to.
+        if (numTextures == 0)
+            return;
+
+        int[] deadTextureIDs = new int[numTextures];
+        for (int i = 0; i < numTextures; i++) {
+            deadTextureIDs[i] = mDeadTextureIDs.get(i);
+        }
+        mDeadTextureIDs.clear();
+
+        GLES20.glDeleteTextures(deadTextureIDs.length, deadTextureIDs, 0);
+    }
+}
+
+
new file mode 100644
--- /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;
+    }
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/VirtualLayer.java
@@ -0,0 +1,36 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+public class VirtualLayer extends Layer {
+    public VirtualLayer(IntSize size) {
+        super(size);
+    }
+
+    @Override
+    public void draw(RenderContext context) {
+        // No-op.
+    }
+
+    void setPositionAndResolution(int left, int top, int right, int bottom, float newResolution) {
+        // This is an optimized version of the following code:
+        // beginTransaction();
+        // try {
+        //     setPosition(new Rect(left, top, right, bottom));
+        //     setResolution(newResolution);
+        //     performUpdates(null);
+        // } finally {
+        //     endTransaction();
+        // }
+
+        // it is safe to drop the transaction lock in this instance (i.e. for the
+        // VirtualLayer that is just a shadow of what gecko is painting) because
+        // the position and resolution of this layer are always touched on the compositor
+        // thread, and therefore do not require synchronization.
+        mPosition.set(left, top, right, bottom);
+        mResolution = newResolution;
+    }
+}
new file mode 100644
--- /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() {
+        mBuf = null;
+        mNativeRef.release();
+    }
+
+    @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;
+    }
+
+}
new file mode 100644
--- /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);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java
@@ -0,0 +1,548 @@
+/* -*- 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");
+            }
+        }
+
+        if (AppConstants.MOZ_LINKER_EXTRACT) {
+            putenv("MOZ_LINKER_EXTRACT=1");
+            // Ensure that the cache dir is world-writable
+            File cacheDir = new File(linkerCache);
+            if (cacheDir.isDirectory()) {
+                cacheDir.setWritable(true, false);
+                cacheDir.setExecutable(true, false);
+                cacheDir.setReadable(true, false);
+            }
+        }
+    }
+
+    @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);
+    }
+
+    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);
+}
new file mode 100644
--- /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();
+}
new file mode 100644
--- /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();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeZip.java
@@ -0,0 +1,86 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.mozglue;
+
+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;
+    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
+    public void finalize() {
+        release();
+    }
+
+    public void close() {
+        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;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/SafeIntent.java
@@ -0,0 +1,96 @@
+/*
+ * 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;
+
+/**
+ * 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 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 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 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;
+    }
+}
new file mode 100644
--- /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);
+    }
+}
new file mode 100644
--- /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;
+    }
+}
new file mode 100644
--- /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);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/DefaultConfiguration.java
@@ -0,0 +1,34 @@
+/* -*- 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.restrictions;
+
+/**
+ * Default implementation of RestrictionConfiguration interface. Used whenever no restrictions are enforced for the
+ * current profile.
+ */
+public class DefaultConfiguration implements RestrictionConfiguration {
+    @Override
+    public boolean isAllowed(Restrictable restrictable) {
+        if (restrictable == Restrictable.BLOCK_LIST) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean canLoadUrl(String url) {
+        return true;
+    }
+
+    @Override
+    public boolean isRestricted() {
+        return false;
+    }
+
+    @Override
+    public void update() {}
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/GuestProfileConfiguration.java
@@ -0,0 +1,83 @@
+/* -*- 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.restrictions;
+
+import android.net.Uri;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * RestrictionConfiguration implementation for guest profiles.
+ */
+public class GuestProfileConfiguration implements RestrictionConfiguration {
+    static List<Restrictable> DISABLED_FEATURES = Arrays.asList(
+            Restrictable.DOWNLOAD,
+            Restrictable.INSTALL_EXTENSION,
+            Restrictable.INSTALL_APPS,
+            Restrictable.BROWSE,
+            Restrictable.SHARE,
+            Restrictable.BOOKMARK,
+            Restrictable.ADD_CONTACT,
+            Restrictable.SET_IMAGE,
+            Restrictable.MODIFY_ACCOUNTS,
+            Restrictable.REMOTE_DEBUGGING,
+            Restrictable.IMPORT_SETTINGS,
+            Restrictable.BLOCK_LIST,
+            Restrictable.DATA_CHOICES,
+            Restrictable.DEFAULT_THEME
+    );
+
+    @SuppressWarnings("serial")
+    private static final List<String> BANNED_SCHEMES = Arrays.asList(
+            "file",
+            "chrome",
+            "resource",
+            "jar",
+            "wyciwyg"
+    );
+
+    private static final List<String> BANNED_URLS = Arrays.asList(
+            "about:config",
+            "about:addons"
+    );
+
+    @Override
+    public boolean isAllowed(Restrictable restrictable) {
+        return !DISABLED_FEATURES.contains(restrictable);
+    }
+
+    @Override
+    public boolean canLoadUrl(String url) {
+        // Null URLs are always permitted.
+        if (url == null) {
+            return true;
+        }
+
+        final Uri u = Uri.parse(url);
+        final String scheme = u.getScheme();
+        if (BANNED_SCHEMES.contains(scheme)) {
+            return false;
+        }
+
+        url = url.toLowerCase();
+        for (String banned : BANNED_URLS) {
+            if (url.startsWith(banned)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean isRestricted() {
+        return true;
+    }
+
+    @Override
+    public void update() {}
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/Restrictable.java
@@ -0,0 +1,112 @@
+/* -*- 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.restrictions;
+
+import org.mozilla.gecko.R;
+
+import android.content.Context;
+import android.support.annotation.StringRes;
+
+/**
+ * This is a list of things we can restrict you from doing. Some of these are reflected in Android UserManager constants.
+ * Others are specific to us.
+ * These constants should be in sync with the ones from toolkit/components/parentalcontrols/nsIParentalControlsService.idl
+ */
+public enum Restrictable {
+    DOWNLOAD(1, "downloads", 0, 0),
+
+    INSTALL_EXTENSION(
+            2, "no_install_extensions",
+            R.string.restrictable_feature_addons_installation,
+            R.string.restrictable_feature_addons_installation_description),
+
+    // UserManager.DISALLOW_INSTALL_APPS
+    INSTALL_APPS(3, "no_install_apps", 0 , 0),
+
+    BROWSE(4, "browse", 0, 0),
+
+    SHARE(5, "share", 0, 0),
+
+    BOOKMARK(6, "bookmark", 0, 0),
+
+    ADD_CONTACT(7, "add_contact", 0, 0),
+
+    SET_IMAGE(8, "set_image", 0, 0),
+
+    // UserManager.DISALLOW_MODIFY_ACCOUNTS
+    MODIFY_ACCOUNTS(9, "no_modify_accounts", 0, 0),
+
+    REMOTE_DEBUGGING(10, "remote_debugging", 0, 0),
+
+    IMPORT_SETTINGS(11, "import_settings", 0, 0),
+
+    PRIVATE_BROWSING(
+            12, "private_browsing",
+            R.string.restrictable_feature_private_browsing,
+            R.string.restrictable_feature_private_browsing_description),
+
+    DATA_CHOICES(13, "data_coices", 0, 0),
+
+    CLEAR_HISTORY(14, "clear_history",
+            R.string.restrictable_feature_clear_history,
+            R.string.restrictable_feature_clear_history_description),
+
+    MASTER_PASSWORD(15, "master_password", 0, 0),
+
+    GUEST_BROWSING(16, "guest_browsing",  0, 0),
+
+    ADVANCED_SETTINGS(17, "advanced_settings",
+            R.string.restrictable_feature_advanced_settings,
+            R.string.restrictable_feature_advanced_settings_description),
+
+    CAMERA_MICROPHONE(18, "camera_microphone",
+            R.string.restrictable_feature_camera_microphone,
+            R.string.restrictable_feature_camera_microphone_description),
+
+    BLOCK_LIST(19, "block_list",
+            R.string.restrictable_feature_block_list,
+            R.string.restrictable_feature_block_list_description),
+
+    TELEMETRY(20, "telemetry",
+            R.string.datareporting_telemetry_title,
+            R.string.datareporting_telemetry_summary),
+
+    HEALTH_REPORT(21, "health_report",
+            R.string.datareporting_fhr_title,
+            R.string.datareporting_fhr_summary2),
+
+    DEFAULT_THEME(22, "default_theme", 0, 0);
+
+    public final int id;
+    public final String name;
+
+    @StringRes
+    public final int title;
+
+    @StringRes
+    public final int description;
+
+    Restrictable(final int id, final String name, @StringRes int title, @StringRes int description) {
+        this.id = id;
+        this.name = name;
+        this.title = title;
+        this.description = description;
+    }
+
+    public String getTitle(Context context) {
+        if (title == 0) {
+            return toString();
+        }
+        return context.getResources().getString(title);
+    }
+
+    public String getDescription(Context context) {
+        if (description == 0) {
+            return null;
+        }
+        return context.getResources().getString(description);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.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.restrictions;
+
+import org.mozilla.gecko.AboutPages;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.StrictMode;
+import android.os.UserManager;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+public class RestrictedProfileConfiguration implements RestrictionConfiguration {
+    // Mapping from restrictable feature to default state (on/off)
+    private static Map<Restrictable, Boolean> configuration = new LinkedHashMap<>();
+    static {
+        configuration.put(Restrictable.INSTALL_EXTENSION, false);
+        configuration.put(Restrictable.PRIVATE_BROWSING, false);
+        configuration.put(Restrictable.CLEAR_HISTORY, false);
+        configuration.put(Restrictable.MASTER_PASSWORD, false);
+        configuration.put(Restrictable.GUEST_BROWSING, false);
+        configuration.put(Restrictable.ADVANCED_SETTINGS, false);
+        configuration.put(Restrictable.CAMERA_MICROPHONE, false);
+        configuration.put(Restrictable.DATA_CHOICES, false);
+        configuration.put(Restrictable.BLOCK_LIST, false);
+        configuration.put(Restrictable.TELEMETRY, false);
+        configuration.put(Restrictable.HEALTH_REPORT, true);
+        configuration.put(Restrictable.DEFAULT_THEME, true);
+    }
+
+    /**
+     * These restrictions are hidden from the admin configuration UI.
+     */
+    private static List<Restrictable> hiddenRestrictions = new ArrayList<>();
+    static {
+        hiddenRestrictions.add(Restrictable.MASTER_PASSWORD);
+        hiddenRestrictions.add(Restrictable.GUEST_BROWSING);
+        hiddenRestrictions.add(Restrictable.DATA_CHOICES);
+        hiddenRestrictions.add(Restrictable.DEFAULT_THEME);
+
+        // Hold behind Nightly flag until we have an actual block list deployed.
+        if (!AppConstants.NIGHTLY_BUILD) {
+            hiddenRestrictions.add(Restrictable.BLOCK_LIST);
+        }
+    }
+
+    /* package-private */ static boolean shouldHide(Restrictable restrictable) {
+        return hiddenRestrictions.contains(restrictable);
+    }
+
+    /* package-private */ static Map<Restrictable, Boolean> getConfiguration() {
+        return configuration;
+    }
+
+    private Context context;
+
+    public RestrictedProfileConfiguration(Context context) {
+        this.context = context.getApplicationContext();
+    }
+
+    @Override
+    public synchronized boolean isAllowed(Restrictable restrictable) {
+        // Special casing system/user restrictions
+        if (restrictable == Restrictable.INSTALL_APPS || restrictable == Restrictable.MODIFY_ACCOUNTS) {
+            return RestrictionCache.getUserRestriction(context, restrictable.name);
+        }
+
+        if (!RestrictionCache.hasApplicationRestriction(context, restrictable.name) && !configuration.containsKey(restrictable)) {
+            // Always allow features that are not in the configuration
+            return true;
+        }
+
+        return RestrictionCache.getApplicationRestriction(context, restrictable.name, configuration.get(restrictable));
+    }
+
+    @Override
+    public boolean canLoadUrl(String url) {
+        if (!isAllowed(Restrictable.INSTALL_EXTENSION) && AboutPages.isAboutAddons(url)) {
+            return false;
+        }
+
+        if (!isAllowed(Restrictable.PRIVATE_BROWSING) && AboutPages.isAboutPrivateBrowsing(url)) {
+            return false;
+        }
+
+        if (AboutPages.isAboutConfig(url)) {
+            // Always block access to about:config to prevent circumventing restrictions (Bug 1189233)
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean isRestricted() {
+        return true;
+    }
+
+    @Override
+    public synchronized void update() {
+        RestrictionCache.invalidate();
+    }
+
+    public static List<Restrictable> getVisibleRestrictions() {
+        final List<Restrictable> visibleList = new ArrayList<>();
+
+        for (Restrictable restrictable : configuration.keySet()) {
+            if (hiddenRestrictions.contains(restrictable)) {
+                continue;
+            }
+            visibleList.add(restrictable);
+        }
+
+        return visibleList;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/RestrictionCache.java
@@ -0,0 +1,99 @@
+/* -*- 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.restrictions;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.StrictMode;
+import android.os.UserManager;
+
+import org.mozilla.gecko.util.ThreadUtils;
+
+/**
+ * Cache for user and application restrictions.
+ */
+public class RestrictionCache {
+    private static Bundle cachedAppRestrictions;
+    private static Bundle cachedUserRestrictions;
+    private static boolean isCacheInvalid = true;
+
+    private RestrictionCache() {}
+
+    public static synchronized boolean getUserRestriction(Context context, String restriction) {
+        updateCacheIfNeeded(context);
+        return cachedUserRestrictions.getBoolean(restriction);
+    }
+
+    public static synchronized boolean hasApplicationRestriction(Context context, String restriction) {
+        updateCacheIfNeeded(context);
+        return cachedAppRestrictions.containsKey(restriction);
+    }
+
+    public static synchronized boolean getApplicationRestriction(Context context, String restriction, boolean defaultValue) {
+        updateCacheIfNeeded(context);
+        return cachedAppRestrictions.getBoolean(restriction, defaultValue);
+    }
+
+    public static synchronized boolean hasApplicationRestrictions(Context context) {
+        updateCacheIfNeeded(context);
+        return !cachedAppRestrictions.isEmpty();
+    }
+
+    public static synchronized void invalidate() {
+        isCacheInvalid = true;
+    }
+
+    private static void updateCacheIfNeeded(Context context) {
+        // If we are not on the UI thread then we can just go ahead and read the values (Bug 1189347).
+        // Otherwise we read from the cache to avoid blocking the UI thread. If the cache is invalid
+        // then we hazard the consequences and just do the read.
+        if (isCacheInvalid || !ThreadUtils.isOnUiThread()) {
+            readRestrictions(context);
+            isCacheInvalid = false;
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+    private static void readRestrictions(Context context) {
+        final UserManager mgr = (UserManager) context.getSystemService(Context.USER_SERVICE);
+
+        // If we do not have anything in the cache yet then this read might happen on the UI thread (Bug 1189347).
+        final StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads();
+
+        try {
+            Bundle appRestrictions = mgr.getApplicationRestrictions(context.getPackageName());
+            migrateRestrictionsIfNeeded(appRestrictions);
+
+            cachedAppRestrictions = appRestrictions;
+            cachedUserRestrictions = mgr.getUserRestrictions(); // Always implies disk read
+        } finally {
+            StrictMode.setThreadPolicy(policy);
+        }
+    }
+
+    /**
+     * This method migrates the old set of DISALLOW_ restrictions to the new restrictable feature ones (Bug 1189336).
+     */
+    /* package-private */ static void migrateRestrictionsIfNeeded(Bundle bundle) {
+        if (!bundle.containsKey(Restrictable.INSTALL_EXTENSION.name) && bundle.containsKey("no_install_extensions")) {
+            bundle.putBoolean(Restrictable.INSTALL_EXTENSION.name, !bundle.getBoolean("no_install_extensions"));
+        }
+
+        if (!bundle.containsKey(Restrictable.PRIVATE_BROWSING.name) && bundle.containsKey("no_private_browsing")) {
+            bundle.putBoolean(Restrictable.PRIVATE_BROWSING.name, !bundle.getBoolean("no_private_browsing"));
+        }
+
+        if (!bundle.containsKey(Restrictable.CLEAR_HISTORY.name) && bundle.containsKey("no_clear_history")) {
+            bundle.putBoolean(Restrictable.CLEAR_HISTORY.name, !bundle.getBoolean("no_clear_history"));
+        }
+
+        if (!bundle.containsKey(Restrictable.ADVANCED_SETTINGS.name) && bundle.containsKey("no_advanced_settings")) {
+            bundle.putBoolean(Restrictable.ADVANCED_SETTINGS.name, !bundle.getBoolean("no_advanced_settings"));
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/RestrictionConfiguration.java
@@ -0,0 +1,31 @@
+/* -*- 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.restrictions;
+
+/**
+ * Interface for classes that Restrictions will delegate to for making decisions.
+ */
+public interface RestrictionConfiguration {
+    /**
+     * Is the user allowed to perform this action?
+     */
+    boolean isAllowed(Restrictable restrictable);
+
+    /**
+     * Is the user allowed to load the given URL?
+     */
+    boolean canLoadUrl(String url);
+
+    /**
+     * Is this user restricted in any way?
+     */
+    boolean isRestricted();
+
+    /**
+     * Update restrictions if needed.
+     */
+    void update();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/RestrictionProvider.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.restrictions;
+
+import org.mozilla.gecko.AppConstants;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.RestrictionEntry;
+import android.os.Build;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Map;
+
+/**
+ * Broadcast receiver providing supported restrictions to the system.
+ */
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+public class RestrictionProvider extends BroadcastReceiver {
+    @Override
+    public void onReceive(final Context context, final Intent intent) {
+        if (AppConstants.Versions.preJBMR2) {
+            // This broadcast does not make any sense prior to Jelly Bean MR2.
+            return;
+        }
+
+        final PendingResult result = goAsync();
+
+        new Thread() {
+            @Override
+            public void run() {
+                final Bundle oldRestrictions = intent.getBundleExtra(Intent.EXTRA_RESTRICTIONS_BUNDLE);
+                RestrictionCache.migrateRestrictionsIfNeeded(oldRestrictions);
+
+                final Bundle extras = new Bundle();
+
+                ArrayList<RestrictionEntry> entries = initRestrictions(context, oldRestrictions);
+                extras.putParcelableArrayList(Intent.EXTRA_RESTRICTIONS_LIST, entries);
+
+                result.setResult(Activity.RESULT_OK, null, extras);
+                result.finish();
+            }
+        }.start();
+    }
+
+    private ArrayList<RestrictionEntry> initRestrictions(Context context, Bundle oldRestrictions) {
+        ArrayList<RestrictionEntry> entries = new ArrayList<RestrictionEntry>();
+
+        final Map<Restrictable, Boolean> configuration = RestrictedProfileConfiguration.getConfiguration();
+
+        for (Restrictable restrictable : configuration.keySet()) {
+            if (RestrictedProfileConfiguration.shouldHide(restrictable)) {
+                continue;
+            }
+
+            RestrictionEntry entry = createRestrictionEntryWithDefaultValue(context, restrictable,
+                    oldRestrictions.getBoolean(restrictable.name, configuration.get(restrictable)));
+            entries.add(entry);
+        }
+
+        return entries;
+    }
+
+    private RestrictionEntry createRestrictionEntryWithDefaultValue(Context context, Restrictable restrictable, boolean defaultValue) {
+        RestrictionEntry entry = new RestrictionEntry(restrictable.name, defaultValue);
+
+        entry.setTitle(restrictable.getTitle(context));
+
+        final String description = restrictable.getDescription(context);
+        if (!TextUtils.isEmpty(description)) {
+            entry.setDescription(description);
+        }
+
+        return entry;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/Restrictions.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.restrictions;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.restrictions.DefaultConfiguration;
+import org.mozilla.gecko.restrictions.GuestProfileConfiguration;
+import org.mozilla.gecko.restrictions.Restrictable;
+import org.mozilla.gecko.restrictions.RestrictedProfileConfiguration;
+import org.mozilla.gecko.restrictions.RestrictionCache;
+import org.mozilla.gecko.restrictions.RestrictionConfiguration;
+
+@RobocopTarget
+public class Restrictions {
+    private static final String LOGTAG = "GeckoRestrictedProfiles";
+
+    private static RestrictionConfiguration configuration;
+
+    private static RestrictionConfiguration getConfiguration(Context context) {
+        if (configuration == null) {
+            configuration = createConfiguration(context);
+        }
+
+        return configuration;
+    }
+
+    public static synchronized RestrictionConfiguration createConfiguration(Context context) {
+        if (configuration != null) {
+            // This method is synchronized and another thread might already have created the configuration.
+            return configuration;
+        }
+
+        if (isGuestProfile(context)) {
+            return new GuestProfileConfiguration();
+        } else if (isRestrictedProfile(context)) {
+            return new RestrictedProfileConfiguration(context);
+        } else {
+            return new DefaultConfiguration();
+        }
+    }
+
+    private static boolean isGuestProfile(Context context) {
+        if (configuration != null) {
+            return configuration instanceof GuestProfileConfiguration;
+        }
+
+        GeckoAppShell.GeckoInterface geckoInterface = GeckoAppShell.getGeckoInterface();
+        if (geckoInterface != null) {
+            return geckoInterface.getProfile().inGuestMode();
+        }
+
+        return GeckoProfile.get(context).inGuestMode();
+    }
+
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+    public static boolean isRestrictedProfile(Context context) {
+        if (configuration != null) {
+            return configuration instanceof RestrictedProfileConfiguration;
+        }
+
+        if (Versions.preJBMR2) {
+            // Early versions don't support restrictions at all
+            return false;
+        }
+
+        // The user is on a restricted profile if, and only if, we injected application restrictions during account setup.
+        return RestrictionCache.hasApplicationRestrictions(context);
+    }
+
+    public static void update(Context context) {
+        getConfiguration(context).update();
+    }
+
+    private static Restrictable geckoActionToRestriction(int action) {
+        for (Restrictable rest : Restrictable.values()) {
+            if (rest.id == action) {
+                return rest;
+            }
+        }
+
+        throw new IllegalArgumentException("Unknown action " + action);
+    }
+
+    private static boolean canLoadUrl(final Context context, final String url) {
+        return getConfiguration(context).canLoadUrl(url);
+    }
+
+    @WrapForJNI
+    public static boolean isUserRestricted() {
+        return isUserRestricted(GeckoAppShell.getApplicationContext());
+    }
+
+    public static boolean isUserRestricted(final Context context) {
+        return getConfiguration(context).isRestricted();
+    }
+
+    public static boolean isAllowed(final Context context, final Restrictable restrictable) {
+        return getConfiguration(context).isAllowed(restrictable);
+    }
+
+    @WrapForJNI
+    public static boolean isAllowed(int action, String url) {
+        final Restrictable restrictable;
+        try {
+            restrictable = geckoActionToRestriction(action);
+        } catch (IllegalArgumentException ex) {
+            // Unknown actions represent a coding error, so we
+            // refuse the action and log.
+            Log.e(LOGTAG, "Unknown action " + action + "; check calling code.");
+            return false;
+        }
+
+        final Context context = GeckoAppShell.getApplicationContext();
+
+        if (Restrictable.BROWSE == restrictable) {
+            return canLoadUrl(context, url);
+        } else {
+            return isAllowed(context, restrictable);
+        }
+    }
+}
new file mode 100644
--- /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;
+    }
+}
new file mode 100644
--- /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.WrapForJNI;
+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
+     */
+    @WrapForJNI
+    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
+     */
+    @WrapForJNI
+    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
+     */
+    @WrapForJNI
+    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
+     */
+    @WrapForJNI
+    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}. */
+    @WrapForJNI
+    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();
+    }
+}
new file mode 100644
--- /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();
+        }
+    }
+}
new file mode 100644
--- /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.WrapForJNI;
+
+@WrapForJNI
+public class SQLiteBridgeException extends RuntimeException {
+    static final long serialVersionUID = 1L;
+
+    public SQLiteBridgeException() {}
+    public SQLiteBridgeException(String msg) {
+        super(msg);
+    }
+}
new file mode 100644
--- /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);
+}
new file mode 100644
--- /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;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityUtils.java
@@ -0,0 +1,51 @@
+/* -*- 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.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) {
+            final int newVis;
+            if (fullscreen) {
+                newVis = View.SYSTEM_UI_FLAG_FULLSCREEN |
+                         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);
+    }
+}
new file mode 100644
--- /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);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/Clipboard.java
@@ -0,0 +1,137 @@
+/* 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(stubName = "GetClipboardTextWrapper")
+    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(stubName = "SetClipboardText")
+    public static void setText(final CharSequence text) {
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            @SuppressWarnings("deprecation")
+            public void run() {
+                // In API Level 11 and above, CLIPBOARD_SERVICE returns android.content.ClipboardManager,
+                // which is a subclass of android.text.ClipboardManager.
+                if (Versions.feature11Plus) {
+                    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;
+                }
+
+                // Deprecated.
+                android.text.ClipboardManager cm = (android.text.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+                cm.setText(text);
+            }
+        });
+    }
+
+    /**
+     * @return true if the clipboard is nonempty, false otherwise.
+     */
+    @WrapForJNI
+    public static boolean hasText() {
+        if (Versions.feature11Plus) {
+            android.content.ClipboardManager cm = (android.content.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+            return cm.hasPrimaryClip();
+        }
+
+        // Deprecated.
+        android.text.ClipboardManager cm = (android.text.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+        return cm.hasText();
+    }
+
+    /**
+     * Deletes all text from the clipboard.
+     */
+    @WrapForJNI
+    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() {
+        if (Versions.feature11Plus) {
+            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();
+                }
+            }
+        } else {
+            android.text.ClipboardManager cm = (android.text.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+            if (cm.hasText()) {
+                return cm.getText().toString();
+            }
+        }
+        return null;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ContextUtils.java
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+public class ContextUtils {
+    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");
+        }
+    }
+}
new file mode 100644
--- /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));
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DrawableUtil.java
@@ -0,0 +1,58 @@
+/* -*- 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.content.res.ColorStateList;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.CheckResult;
+import android.support.annotation.ColorInt;
+import android.support.annotation.ColorRes;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.graphics.drawable.DrawableCompat;
+
+public class DrawableUtil {
+
+    /**
+     * Tints the given drawable with the given color and returns it.
+     */
+    @CheckResult
+    public static Drawable tintDrawable(@NonNull final Context context,
+                                        @DrawableRes final int drawableID,
+                                        @ColorInt final int color) {
+        final Drawable icon = DrawableCompat.wrap(
+                ContextCompat.getDrawable(context, drawableID).mutate());
+        DrawableCompat.setTint(icon, color);
+        return icon;
+    }
+
+    /**
+     * Tints the given drawable with the given color and returns it.
+     */
+    @CheckResult
+    public static Drawable tintDrawableWithColorRes(@NonNull final Context context,
+                                                    @DrawableRes final int drawableID,
+                                                    @ColorRes final int colorID) {
+        return tintDrawable(context, drawableID, ContextCompat.getColor(context, colorID));
+    }
+
+    /**
+     * Tints the given drawable with the given tint list and returns it. Note that you
+     * should no longer use the argument Drawable because the argument is not mutated
+     * on pre-Lollipop devices but is mutated on L+ due to differences in the Support
+     * Library implementation (bug 1193950).
+     */
+    @CheckResult
+    public static Drawable tintDrawableWithStateList(@NonNull final Drawable drawable,
+            @NonNull final ColorStateList colorList) {
+        final Drawable wrappedDrawable = DrawableCompat.wrap(drawable.mutate());
+        DrawableCompat.setTintList(wrappedDrawable, colorList);
+        return wrappedDrawable;
+    }
+}
new file mode 100644
--- /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);
+}
new file mode 100644
--- /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;
+            }
+        }
+    }
+}
new file mode 100644
--- /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));
+    }
+}
new file mode 100644
--- /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;
+    }
+}
new file mode 100644
--- /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);
+    }
+}
new file mode 100644
--- /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);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoJarReader.java
@@ -0,0 +1,267 @@
+/* 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);
+                }
+            }
+            if (zip != null) {
+                zip.close();
+            }
+        }
+
+        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);
+                }
+            }
+            if (zip != null) {
+                zip.close();
+            }
+        }
+
+        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;
+    }
+}
new file mode 100644
--- /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);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java
@@ -0,0 +1,159 @@
+/* -*- 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";
+  // 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(allowMultithread = true, stubName = "FindDecoderCodecInfoForMimeType")
+  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() {
+    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 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(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 decoder.
+              Log.e(LOGTAG, "Found target decoder " + name +
+                  ". Color: 0x" + Integer.toHexString(codecColorFormat));
+              return true;
+            }
+          }
+        }
+      }
+    }
+    return false;  // No HW decoder.
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java
@@ -0,0 +1,109 @@
+/* -*- 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();
+    }
+
+    /**
+     * @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
+        boolean isSystemARM = Build.CPU_ABI != null && Build.CPU_ABI.startsWith("arm");
+        boolean isSystemX86 = Build.CPU_ABI != null && Build.CPU_ABI.startsWith("x86");
+
+        boolean isAppARM = AppConstants.ANDROID_CPU_ARCH.startsWith("arm");
+        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;
+    }
+}
new file mode 100644
--- /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);
+    }
+}
new file mode 100644
--- /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();
+        }
+    }
+}
new file mode 100644
--- /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);
+        }
+    }
+}
new file mode 100644
--- /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;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java
@@ -0,0 +1,91 @@
+/*
+ * 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.NonNull;
+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 Intent unsafeIntent) {
+        // 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 SafeIntent safeIntent = new SafeIntent(unsafeIntent);
+        final HashMap<String, String> out = new HashMap<>();
+        int i = 0;
+        while (true) {
+            final String envKey = "env" + i;
+            i += 1;
+            if (!unsafeIntent.hasExtra(envKey)) {
+                break;
+            }
+
+            maybeAddEnvVarToEnvVarMap(out, safeIntent, 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);
+    }
+}
new file mode 100644
--- /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;
+    }
+
+}
new file mode 100644
--- /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);
+        }
+    }
+}
new file mode 100644
--- /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);
+}
new file mode 100644
--- /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
+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();
+}
new file mode 100644
--- /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
+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();
+}
new file mode 100644
--- /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;
+        }
+    }
+}
new file mode 100644
--- /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();
+    }
+}
new file mode 100644
--- /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;
+    }
+}
new file mode 100644
--- /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);
+    }
+}
+
new file mode 100644
--- /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
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StringUtils.java
@@ -0,0 +1,278 @@
+/* -*- 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.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;
+        }
+
+        int start = 0;
+        int end = url.length();
+
+        if (url.startsWith("http://")) {
+            start = 7;
+        } else if (url.startsWith("https://") && flags == UrlFlags.STRIP_HTTPS) {
+            start = 8;
+        }
+
+        if (url.endsWith("/")) {
+            end--;
+        }
+
+        return url.substring(start, end);
+    }
+
+    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) {
+        if (Versions.feature11Plus) {
+            return uri.getQueryParameterNames();
+        }
+
+        // Logic below copied from Uri.java included with Android 5.0.0.
+        if (uri.isOpaque()) {
+            throw new UnsupportedOperationException("This isn't a hierarchical URI.");
+        }
+
+        String query = uri.getEncodedQuery();
+        if (query == null) {
+            return Collections.emptySet();
+        }
+
+        Set<String> names = new LinkedHashSet<String>();
+        int start = 0;
+        do {
+            int next = query.indexOf('&', start);
+            int end = (next == -1) ? query.length() : next;
+
+            int separator = query.indexOf('=', start);
+            if (separator > end || separator == -1) {
+                separator = end;
+            }
+
+            String name = query.substring(start, separator);
+            names.add(Uri.decode(name));
+
+            // Move start to end of name.
+            start = end + 1;
+        } while (start < query.length());
+
+        return Collections.unmodifiableSet(names);
+    }
+
+    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()));
+    }
+}
new file mode 100644
--- /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);
+        }
+    }
+}
new file mode 100644
--- /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);
+}
new file mode 100644
--- /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);
+}
new file mode 100644
--- /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);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WebActivityMapper.java
@@ -0,0 +1,221 @@
+/* -*- 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.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+public final class WebActivityMapper {
+    private static final String LOGTAG = "Gecko";
+
+    private static final Map<String, WebActivityMapping> activityMap = new HashMap<String, WebActivityMapping>();
+    static {
+        activityMap.put("dial", new DialMapping());
+        activityMap.put("open", new OpenMapping());
+        activityMap.put("pick", new PickMapping());
+        activityMap.put("send", new SendMapping());
+        activityMap.put("view", new ViewMapping());
+        activityMap.put("record", new RecordMapping());
+    };
+
+    private static abstract class WebActivityMapping {
+        protected JSONObject mData;
+
+        public void setData(JSONObject data) {
+            mData = data;
+        }
+
+        // Cannot return null
+        public abstract String getAction();
+
+        public String getMime() throws JSONException {
+            return null;
+        }
+
+        public String getUri() throws JSONException {
+            return null;
+        }
+
+        public void putExtras(Intent intent) throws JSONException {}
+    }
+
+    /**
+     * Provides useful defaults for mime type and uri.
+     */
+    private static abstract class BaseMapping extends WebActivityMapping {
+        /**
+         * If 'type' is present in data object, uses the value as the MIME type.
+         */
+        @Override
+        public String getMime() throws JSONException {
+            return mData.optString("type", null);
+        }
+
+        /**
+         * If 'uri' or 'url' is present in data object, uses the respective value as the Uri.
+         */
+        @Override
+        public String getUri() throws JSONException {
+            // Will return uri or url if present.
+            String uri = mData.optString("uri", null);
+            return uri != null ? uri : mData.optString("url", null);
+        }
+    }
+
+    public static Intent getIntentForWebActivity(JSONObject message) throws JSONException {
+        final String name = message.getString("name").toLowerCase();
+        final JSONObject data = message.getJSONObject("data");
+
+        Log.w(LOGTAG, "Activity is: " + name);
+        final WebActivityMapping mapping = activityMap.get(name);
+        if (mapping == null) {
+            Log.w(LOGTAG, "No mapping found!");
+            return null;
+        }
+
+        mapping.setData(data);
+
+        final Intent intent = new Intent(mapping.getAction());
+
+        final String mime = mapping.getMime();
+        if (!TextUtils.isEmpty(mime)) {
+            intent.setType(mime);
+        }
+
+        final String uri = mapping.getUri();
+        if (!TextUtils.isEmpty(uri)) {
+            intent.setData(Uri.parse(uri));
+        }
+
+        mapping.putExtras(intent);
+
+        return intent;
+    }
+
+    private static class DialMapping extends WebActivityMapping {
+        @Override
+        public String getAction() {
+            return Intent.ACTION_DIAL;
+        }
+
+        @Override
+        public String getUri() throws JSONException {
+            return "tel:" + mData.getString("number");
+        }
+    }
+
+    private static class OpenMapping extends BaseMapping {
+        @Override
+        public String getAction() {
+            return Intent.ACTION_VIEW;
+        }
+    }
+
+    private static class PickMapping extends BaseMapping {
+        @Override
+        public String getAction() {
+            return Intent.ACTION_GET_CONTENT;
+        }
+
+        @Override
+        public String getMime() throws JSONException {
+            // bug 1007112 - pick action needs a mimetype to work
+            String mime = mData.optString("type", null);
+            return !TextUtils.isEmpty(mime) ? mime : "*/*";
+        }
+    }
+
+    private static class SendMapping extends BaseMapping {
+        @Override
+        public String getAction() {
+            return Intent.ACTION_SEND;
+        }
+
+        @Override
+        public void putExtras(Intent intent) throws JSONException {
+            optPutExtra("text", Intent.EXTRA_TEXT, intent);
+            optPutExtra("html_text", Intent.EXTRA_HTML_TEXT, intent);
+            optPutExtra("stream", Intent.EXTRA_STREAM, intent);
+        }
+
+        private void optPutExtra(String key, String extraName, Intent intent) {
+            final String extraValue = mData.optString(key);
+            if (!TextUtils.isEmpty(extraValue)) {
+                intent.putExtra(extraName, extraValue);
+            }
+        }
+    }
+
+    private static class ViewMapping extends BaseMapping {
+        @Override
+        public String getAction() {
+            return Intent.ACTION_VIEW;
+        }
+
+        @Override
+        public String getMime() {
+            // MozActivity adds a type 'url' here, we don't want to set the MIME to 'url'.
+            String type = mData.optString("type", null);
+            if ("url".equals(type) || "uri".equals(type)) {
+                return null;
+            } else {
+                return type;
+            }
+        }
+    }
+
+    private static class RecordMapping extends WebActivityMapping {
+        @Override
+        public String getAction() {
+            String type = mData.optString("type", null);
+            if ("photos".equals(type)) {
+                return "android.media.action.IMAGE_CAPTURE";
+            } else if ("videos".equals(type)) {
+                return "android.media.action.VIDEO_CAPTURE";
+            }
+            return null;
+        }
+
+        // Add an extra to specify where to save the picture/video.
+        @Override
+        public void putExtras(Intent intent) {
+            final String action = getAction();
+
+            final String dirType = action == "android.media.action.IMAGE_CAPTURE"
+                ? Environment.DIRECTORY_PICTURES
+                : Environment.DIRECTORY_MOVIES;
+
+            final String ext = action == "android.media.action.IMAGE_CAPTURE"
+                ? ".jpg"
+                : ".mp4";
+
+            File destDir = Environment.getExternalStoragePublicDirectory(dirType);
+
+            try {
+                File dest = File.createTempFile(
+                    "capture", /* prefix */
+                    ext,       /* suffix */
+                    destDir    /* directory */
+                );
+                intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(dest));
+            } catch (Exception e) {
+                Log.w(LOGTAG, "Failed to add extra for " + action + " : " + e);
+            }
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WindowUtils.java
@@ -0,0 +1,62 @@
+/* -*- 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 if (Versions.feature14Plus) {
+            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);
+
+        } else {
+            // This should be close, as lower API devices should not have window navigation bars.
+            return Math.max(display.getWidth(), display.getHeight());
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/widget/SwipeDismissListViewTouchListener.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright 2012 Roman Nurik
+ *
+ * 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.widget;
+
+import android.graphics.Rect;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.RecyclerListener;
+import android.widget.ListView;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.view.ViewPropertyAnimator;
+
+import org.mozilla.gecko.R;
+
+/**
+ * This code is based off of Jake Wharton's NOA port (https://github.com/JakeWharton/SwipeToDismissNOA)
+ * of Roman Nurik's SwipeToDismiss library. It has been modified for better support with async
+ * adapters.
+ *
+ * A {@link android.view.View.OnTouchListener} that makes the list items in a {@link ListView}
+ * dismissable. {@link ListView} is given special treatment because by default it handles touches
+ * for its list items... i.e. it's in charge of drawing the pressed state (the list selector),
+ * handling list item clicks, etc.
+ *
+ * <p>After creating the listener, the caller should also call
+ * {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}, passing
+ * in the scroll listener returned by {@link #makeScrollListener()}. If a scroll listener is
+ * already assigned, the caller should still pass scroll changes through to this listener. This will
+ * ensure that this {@link SwipeDismissListViewTouchListener} is paused during list view
+ * scrolling.</p>
+ *
+ * <p>Example usage:</p>
+ *
+ * <pre>
+ * SwipeDismissListViewTouchListener touchListener =
+ *         new SwipeDismissListViewTouchListener(
+ *                 listView,
+ *                 new SwipeDismissListViewTouchListener.OnDismissCallback() {
+ *                     public void onDismiss(ListView listView, int[] reverseSortedPositions) {
+ *                         for (int position : reverseSortedPositions) {
+ *                             adapter.remove(adapter.getItem(position));
+ *                         }
+ *                         adapter.notifyDataSetChanged();
+ *                     }
+ *                 });
+ * listView.setOnTouchListener(touchListener);
+ * listView.setOnScrollListener(touchListener.makeScrollListener());
+ * </pre>
+ *
+ * <p>For a generalized {@link android.view.View.OnTouchListener} that makes any view dismissable,
+ * see {@link SwipeDismissTouchListener}.</p>
+ *
+ * @see SwipeDismissTouchListener
+ */
+public class SwipeDismissListViewTouchListener implements View.OnTouchListener {
+    // Cached ViewConfiguration and system-wide constant values
+    private final int mSlop;
+    private final int mMinFlingVelocity;
+    private final int mMaxFlingVelocity;
+    private final long mAnimationTime;
+
+    // Fixed properties
+    private final ListView mListView;
+    private final OnDismissCallback mCallback;
+    private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero
+
+    // Transient properties
+    private float mDownX;
+    private boolean mSwiping;
+    private VelocityTracker mVelocityTracker;
+    private int mDownPosition;
+    private View mDownView;
+    private boolean mPaused;
+    private boolean mDismissing;
+
+    /**
+     * The callback interface used by {@link SwipeDismissListViewTouchListener} to inform its client
+     * about a successful dismissal of a list item.
+     */
+    public interface OnDismissCallback {
+        /**
+         * Called when the user has indicated they she would like to dismiss one or more list item
+         * positions.
+         *
+         * @param listView The originating {@link ListView}.
+         * @param position The position being dismissed.
+         */
+        void onDismiss(ListView listView, int position);
+    }
+
+    /**
+     * Constructs a new swipe-to-dismiss touch listener for the given list view.
+     *
+     * @param listView The list view whose items should be dismissable.
+     * @param callback The callback to trigger when the user has indicated that she would like to
+     *                 dismiss one or more list items.
+     */
+    public SwipeDismissListViewTouchListener(ListView listView, OnDismissCallback callback) {
+        ViewConfiguration vc = ViewConfiguration.get(listView.getContext());
+        mSlop = vc.getScaledTouchSlop();
+        mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
+        mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
+        mAnimationTime = listView.getContext().getResources().getInteger(
+                android.R.integer.config_shortAnimTime);
+        mListView = listView;
+        mCallback = callback;
+    }
+
+    /**
+     * Enables or disables (pauses or resumes) watching for swipe-to-dismiss gestures.
+     *
+     * @param enabled Whether or not to watch for gestures.
+     */
+    public void setEnabled(boolean enabled) {
+        mPaused = !enabled;
+    }
+
+    /**
+     * Returns an {@link android.widget.AbsListView.OnScrollListener} to be added to the
+     * {@link ListView} using
+     * {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}.
+     * If a scroll listener is already assigned, the caller should still pass scroll changes
+     * through to this listener. This will ensure that this
+     * {@link SwipeDismissListViewTouchListener} is paused during list view scrolling.</p>
+     *
+     * @see {@link SwipeDismissListViewTouchListener}
+     */
+    public AbsListView.OnScrollListener makeScrollListener() {
+        return new AbsListView.OnScrollListener() {
+            @Override
+            public void onScrollStateChanged(AbsListView absListView, int scrollState) {
+                setEnabled(scrollState != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
+            }
+
+            @Override
+            public void onScroll(AbsListView absListView, int i, int i1, int i2) {
+            }
+        };
+    }
+
+    /**
+     * Returns a {@link android.widget.AbsListView.RecyclerListener} to be added to the
+     * {@link ListView} using {@link ListView#setRecyclerListener(RecyclerListener)}.
+     */
+    public AbsListView.RecyclerListener makeRecyclerListener() {
+        return new AbsListView.RecyclerListener() {
+            @Override
+            public void onMovedToScrapHeap(View view) {
+                final Object tag = view.getTag(R.id.original_height);
+
+                // To reset the view to the correct height after its animation, the view's height
+                // is stored in its tag. Reset the view here.
+                if (tag instanceof Integer) {
+                    view.setAlpha(1f);
+                    view.setTranslationX(0);
+                    final ViewGroup.LayoutParams lp = view.getLayoutParams();
+                    lp.height = (int) tag;
+                    view.setLayoutParams(lp);
+                    view.setTag(R.id.original_height, null);
+                }
+            }
+        };
+    }
+
+    @Override
+    public boolean onTouch(View view, MotionEvent motionEvent) {
+        if (mViewWidth < 2) {
+            mViewWidth = mListView.getWidth();
+        }
+
+        switch (motionEvent.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN: {
+                if (mPaused) {
+                    return false;
+                }
+
+                if (mDismissing) {
+                    return true;
+                }
+
+                // TODO: ensure this is a finger, and set a flag
+
+                // Find the child view that was touched (perform a hit test)
+                Rect rect = new Rect();
+                int childCount = mListView.getChildCount();
+                int[] listViewCoords = new int[2];
+                mListView.getLocationOnScreen(listViewCoords);
+                int x = (int) motionEvent.getRawX() - listViewCoords[0];
+                int y = (int) motionEvent.getRawY() - listViewCoords[1];
+                View child;
+                for (int i = 0; i < childCount; i++) {
+                    child = mListView.getChildAt(i);
+                    child.getHitRect(rect);
+                    if (rect.contains(x, y)) {
+                        mDownView = child;
+                        break;
+                    }
+                }
+
+                if (mDownView != null) {
+                    mDownX = motionEvent.getRawX();
+                    mDownPosition = mListView.getPositionForView(mDownView);
+
+                    mVelocityTracker = VelocityTracker.obtain();
+                    mVelocityTracker.addMovement(motionEvent);
+                }
+                view.onTouchEvent(motionEvent);
+                return true;
+            }
+
+            case MotionEvent.ACTION_UP: {
+                if (mVelocityTracker == null) {
+                    break;
+                }
+
+                float deltaX = motionEvent.getRawX() - mDownX;
+                mVelocityTracker.addMovement(motionEvent);
+                mVelocityTracker.computeCurrentVelocity(1000);
+                float velocityX = Math.abs(mVelocityTracker.getXVelocity());
+                float velocityY = Math.abs(mVelocityTracker.getYVelocity());
+                boolean dismiss = false;
+                boolean dismissRight = false;
+                if (Math.abs(deltaX) > mViewWidth / 2) {
+                    dismiss = true;
+                    dismissRight = deltaX > 0;
+                } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity
+                        && velocityY < velocityX) {
+                    dismiss = true;
+                    dismissRight = mVelocityTracker.getXVelocity() > 0;
+                }
+                if (dismiss) {
+                    // dismiss
+                    mDismissing = true;
+                    final View downView = mDownView; // mDownView gets null'd before animation ends
+                    final int downPosition = mDownPosition;
+                    mDownView.animate()
+                            .translationX(dismissRight ? mViewWidth : -mViewWidth)
+                            .alpha(0)
+                            .setDuration(mAnimationTime)
+                            .setListener(new AnimatorListenerAdapter() {
+                                @Override
+                                public void onAnimationEnd(Animator animation) {
+                                    performDismiss(downView, downPosition);
+                                }
+                            });
+                } else {
+                    // cancel
+                    mDownView.animate()
+                            .translationX(0)
+                            .alpha(1)
+                            .setDuration(mAnimationTime)
+                            .setListener(null);
+                }
+
+                if (mVelocityTracker != null) {
+                    mVelocityTracker.recycle();
+                    mVelocityTracker = null;
+                }
+
+                mDownX = 0;
+                mDownView = null;
+                mDownPosition = ListView.INVALID_POSITION;
+                mSwiping = false;
+                break;
+            }
+
+            case MotionEvent.ACTION_MOVE: {
+                if (mVelocityTracker == null || mPaused) {
+                    break;
+                }
+
+                mVelocityTracker.addMovement(motionEvent);
+                float deltaX = motionEvent.getRawX() - mDownX;
+                if (Math.abs(deltaX) > mSlop) {
+                    mSwiping = true;
+                    mListView.requestDisallowInterceptTouchEvent(true);
+
+                    // Cancel ListView's touch (un-highlighting the item)
+                    MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);
+                    cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
+                            (motionEvent.getActionIndex()
+                                    << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
+                    mListView.onTouchEvent(cancelEvent);
+                    cancelEvent.recycle();
+                }
+
+                if (mSwiping) {
+                    mDownView.setTranslationX(deltaX);
+                    mDownView.setAlpha(Math.max(0f, Math.min(1f, 1f - 2f * Math.abs(deltaX) / mViewWidth)));
+                    return true;
+                }
+                break;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Animate the dismissed list item to zero-height and fire the dismiss callback when it finishes.
+     *
+     * @param dismissView     ListView item to dismiss
+     * @param dismissPosition Position of dismissed item
+     */
+    private void performDismiss(final View dismissView, final int dismissPosition) {
+        final ViewGroup.LayoutParams lp = dismissView.getLayoutParams();
+        final int originalHeight = lp.height;
+
+        ValueAnimator animator = ValueAnimator.ofInt(dismissView.getHeight(), 1).setDuration(mAnimationTime);
+
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                // Since the view is still a part of the ListView, we can't reset the animated
+                // properties yet; otherwise, the view would briefly reappear. Store the original
+                // height in the view's tag to flag it for the recycler. This is racy since the user
+                // could scroll the dismissed view off the screen, then back on the screen, before
+                // it's removed from the adapter, causing the dismissed view to briefly reappear.
+                dismissView.setTag(R.id.original_height, originalHeight);
+
+                mCallback.onDismiss(mListView, dismissPosition);
+                mDismissing = false;
+            }
+        });
+
+        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                lp.height = (Integer) valueAnimator.getAnimatedValue();
+                dismissView.setLayoutParams(lp);
+            }
+        });
+
+        animator.start();
+    }
+}
new file mode 100644
index 0000000000000000000000000000000000000000..6cde348254185728ce5e7c11acb08a8d27d85831
GIT binary patch
literal 1645
zc$@)k29o)SP)<h;3K|Lk000e1NJLTq002k;002k`1ONa4|Kxkj000IwNkl<ZcmeI&
z#g-hu702;URrRok%@j^n;pfOMyD)45+rT!Qr^r65JVzK-e+J|HVX)nHLrDtP=~G>!
zKF*B8vOShcQs3|W-7Dzs@&EGw^50ep)1_2dTI!8@3eUczrFv8w31T<Uf~9ZY{^nOV
zK2Yt5NKJ17Q0p;+o0?t-G^N4<%En=U9`;Y}{OAWizGI}8+CZh%AAjgu-^9qec8M(|
zXZxC;>S?2T%BglUJN1gn$WjVF`R<p0;juV91S*@K{-nNvkpmZ|G)$sWY+#}aNg*ka
zK&175iL${dP&TS5D$pxdKk;eL8_fbORhyTtVr1Y@P{A}-%g|qdTD|RdfkLTO8r0H?
za`#rX8BR=EKuf*R`eNnCiJ@TMwuGY5pecf+ppDglG_BVEZLmTmS}Zrh`cgI00vh!!
z8X6iK3&vJeip+|HkX^Z{)s9W8!B-j=V~eAPqc_c~HZ&1L9NTWa<a8@_VYbF;z`8M$
zq&7BIDQ1CSY$6DfLf@*zjzBm^%t91I=mx3<wTOfRmr=}`IY-PW=754AAc7|bR=ZR7
zoG=SSbth26n}lZKfhA?9sq?`s5jh<w38K^t8auYxE%!NLCQ&#Yh%~Jb4z21z=MHmL
z%t8sd9E#F3vBF!fbnilE#cWoYyh1^mp6Ib->)i&=hS{hjx9o{RYuJHhy>5eN#7qh)
zmqUdhNMbVuk~dxLHhDJ8`5}u!dtYe6w$W|!OqkJvB+lE1(#)|Kyk)UDNHlWeOqhiz
zNcp6m%^~2S8_I4|XTVJ4lR7o0OWB$_vgew-(WrbX%u>vUEQxASLYNV|Z`o$X*TUGq
z)|4yr)lBn%f+UE_&X;*p&x+#Ikn}up=%RHLc{Q8fm<?I8=%cPudf=ActU66?%RPHe
z>{`sIGmRP4xqT>_bl{XztMEaS++BC)1#9{S26k+t$R>CxNXq<K7EPp<Pt!^M#C<n9
zQ#OGeeKp?Jcga#l%_J%2--n_o&3I01YL7g0<<wLyY&t;n#2puHovJ2<!2J7AoV$}8
z;qr{X02@w>0dMNNrtDI)30gG&aGmi5N|3;Tp3P2uVB7uX$s@10W~EcjQX+pri_KP*
z3mSzztfTBubJ>QYsd;zp+UZm?isXTUAa?uSsy%SkM#lyU+fD?DsnA_lwO%pJ1FF@c
zDCSX`+I`Wc?rgi{zUe0euexEmrB9K+Q#ZSH9%UA(<L%9g4Tq*jkG<~l)M!d1^7~Ne
zPU`L_T}VxB$H36^yy$@^F1Ib_Z;VlDg=`+weG@x$tj5dkh^^84M$JZ$L`gAk+1F$N
zW@~$%xYj%gO>My?4`)W77`vgTkW~4dI*Edq4_Rm5p^=-anT3791t&A}9(&nME6RBa
zrGF@7K;LVwS*8`dt41Erj1PYqz16s{8z@Ro8M5}mzG@4Y3f>K`8+OdQXW*U8yD>(D
z96$y$y6cgv3e(A5xb+vL+3`nS@gXs<P*6)*kbG@l-wmrRw{CmQu)~>0UbUG~r~z{Z
zq@kB>xS^VMtWl{wKHAGEG*Sy9XF}r0BfEB$&>-D2m>#EA84Ed}LhL*`o*rpz&%Vpn
z^_rer-u7nGA_++OWuGK*zR7w0O_Eo3?N|WpT6W(A6e?-nXN&|v${2rI%xKS^4cpei
zviEq~zJNj`zshP8aXt`Fol{48Hmq5*=%P(;I}%JxGCA~@?jjF#hNlzz_OW8cnk|<+
zGEf_z4m4DY=MOZqqYv!EU-Y_QDF-xGBj!I#JvYq9hWRV(iIHfM$2mvLXu;5l#cm)5
zh9+X(>^Wi<Y9oWwf%?XRl>N5id@+-e!ge<hBlS#5H(<{MGhvup_Cd;<KYz^J(J@x!
zzjS*hn4$X%?<9PFQJnfDHi@lvu6Mc>+pXNVpH+f1llMzoqYqU5`bD(+^6h-@{8sVt
zcl~B%>G{!7OQBJy`o;S{zbrL>8m~I_N#~>H*O&kB?y5fuv#bj)dw=oH-9PkS9`>`o
zH&4#0=GQO$^!EOT(raGurb)-ITDa`C+b&vFdG;lE>}_}4@utV(?B3b4WV6}~>lXh>
rK;x<(jvX7RrGL|3*ZF_>k1Kx!zN3VBQJn+S00000NkvXXu0mjfdU-nq
new file mode 100644
index 0000000000000000000000000000000000000000..29ff5b0f1d90a1e419f85579c80a50a7d48730ed
GIT binary patch
literal 3167
zc$@)W450IgP)<h;3K|Lk000e1NJLTq002k;002k`1ONa4|Kxkj000aoNkl<ZcmeFw
zh0^o58U*3zm1M~B{om&g`C@BMmCi!ZGQ-3SaaWJzzx<a!FKMJwkiO2=>Q|+H83-uq
z&RF{wq_n!zYNneCsrbE<4%tXcCwum^46>zVXRVb=_p{Ud%wz~@T6WR!a%)D0j+=S|
z%_QS@tSlW}>XlJfn%$+lZ7!oeklo7=&4sO=ey^l5$WB^{oH^I_k(QA?J!jggPLA~S
zY><)hyBV?!RC0QPL-$n5jt{y`d~_^Zr&ryn<uTO!4T09oDO<f|S7MqA=T?Txw42^+
zYVCKtnV=MP{f2`&3#XzobT7?eb8AY|up5R?7EWh9H4TR>7#V55k)brDG`DCN3uc&x
zr)_O_x9(EnmUVn<UJf7ix_*Z-v^))`r&AMU^U;ahX0j^dsSF=Q6VoM1YLZel{rt+(
zNK1V;N3z@UuzB+^m!HGsRJw=5uKPlX&n%-(Lu|@kw~;-Eg_Hg>45fjn<>NNcy=-e;
zf3I*^KrT($TuKRUmkHiu+C5!1<*HYPDM}Nm`NM`*EgIBJtMyUSQ`da13_};?^zfx&
z^M%bPKGT(^Je*F4xvqbzG!HyoDfEh>?waXoKL}KlrM0Jo`e+s8)Rm61&TWP`ef2-a
z%mGZYB#Xj-#6!Ajs<ZZ+wr#I%+v8c=wr$(CZQFLuwtHHsr-<D>Uwp&NosO)kbiVWM
zJ-CqtOntHQwJtR}wCLdRB!ZgimXp@1a1x0rd>vz?JX2x39pAE`CR6$dOcm0*fq?)e
zAtJ_7==_a%1Wj_HTRsRr|7x|T#Ly?E)UAw48q6ulusVs-<e_niV#r>$ahJ7q!SGgp
zBO)EnQvwYEu_RbAs75OORAN;8tEu`u;8!2sI+V5?XVBN{N@-F_2bY*a>7`6lkON=@
zX$&B*ns^@*gBkfI1qg^&eKHzK_9;QD;b`j>&G(<RqYdvrYwO!+F(jp+1@uB?hZL$&
zOu|rAs;IC&Ree&Nsm5~1crUt{QqUR{IwLJ*R7+qI3A{#4@@_Hx*Q+(|w2f^v?)29)
zWdVqBjs{I2rA3R9CJljdYIj0xa*R(+Zpw+W_g1aRi6|8Yg$`hYwrn_1Tb9?Rk_*{q
zpSAoRJAt3^xA#B&WAup_5Rp^pdZ4&<fmek-9a%zRa!@3VQ<=9uHqPWY)g_TSB7F!d
zaT#E>#Hn^L!zL|zljWSrnJg#1DGpUd@+Z_X0)dx5>DrQ0)2ta2dn@r@o<s5-jbJ9%
zlT}bvr3`E1=n#k3*W@)7(<Jye+;rjWnY4j3SvdQ~o8A9=7*!_$1xO@vr^<e-%B09l
z^%qm1Kp9gZ-S(h9r^@t7ic;TIoroqKQrgs+miX)YZ@ZisQc`AE&c3@~bhAlIHEhtt
z1_+v#L}|q{@_ldBG=F84d65fgEX9)wt`Vyuyc*^ao%L7^h~IF{CEfY7u|S*oEa|@C
z1`qf?C{S$(+$K)yizq;iG-8z^1W0^g{FN#c98<=n7dnAS)ig0j)shDX+1KEI^_fQ+
zjx&$t|9wRHUYCm-)fEY&p_e4Vi*%yF7M$^8NNpU}*Yr|^ULBUQk}z^b>P(_b3*hb7
zT;5&8l%7Umzli1Cw_jT@I%-Tqf#GDSQD9&qH**25#rR~L$|6&mEM&VSj13?%rr1|(
zdciecwf8<3Jm_u@I_F+bJ@3(HJ?}wp?4P%eS-pr*qgmF`Kkq?rJnMPqJ^Efxdf<5v
zxz~N}^WrVf*!7ByRB|Jv-nsw+f#`0G4_z`k;cK|dgr}!@$GaSP?gD0Lp%~ZSCNnHx
zAv4s`(i(A$eeCBL14fL<^taD3_A~qBkAH{}IYtJc%>e~yPOnTgb{>*7mVyHo>3|l=
zt8P_1bxC`gCJDMoKn?1$oJ*GG`w>HiWE9~t-~@BMc~3b1yA(oKF``Bv3k*X@yGei&
zrHfHz)OQ`ZM)ISb{bwyYbDDaPoz-Rha=KI^;)oMm^w;)__WpvN9vYFULa0`mfH0Or
z8Z521i21~zmMN<-d;hfcp}k*2O`((u)}&=6&jw8;aPJ3OAGh}}R8p*$isf)Z>p@#f
zGkJtfsA#GLDNgZV=#Ypuou54P%<IikJFVT5W5^MXd~w$s=;Js=&VT_ir-=24VJO6&
z5@V_>rM))DA~G(#+$qHxM5H45X!n~=ymKEzDz0i(^w>W*_0C;y!zpS@)yIZG#sed{
zU14%yj4{d@gI7RB4gwKVhOQ5ZVe@yYm;P&TO71ync?wSQ??L?1&0i1EjB-&!8%0Pm
zz+_Uk9h>&XVN7O5lBUvH9+ZjRgzd3swDxz6m+qVEO|Ft=js0_rpRoKQoru9fh+0D~
zXvj9buX&>p&5d(vM6ocOW0Y4Fu|N$Jw~hf8mjA2yj-wPzS_KzVe!7zZggtMjDqC5K
zSQGYaIg!bY^L9+jd?Zj6PQ5mIdzgg6{!>jOhCeJiE<ls{^S7m{Ly1$p!@fg8@r1ou
z*i={;Qz7(yp^VWfjWa>=C=3O#AZDmTxPPvn3`aHMz#1SEoI-+2wO<+Ou@T^LW`qH}
zk6;WSZ45!X4%z!iA}w$_f)D-o42VbTl(-XCM4fV-h+FBjay48@rbi;lz3?gCr^Zhh
z@<Y=KKvsywg=j6+^-7mh*k4Rdbvq<XS_y5sd~y{9Ms-1TvU<M@b+3%aY1G#m*D|Lp
z!%GG7AkEdvN5>#|2aJBWdPZBd2}?7}JPt0ARrIJ-9ZM#~#dtv~e2m5U*hdopq1`r-
zsYuYS9^F#NA`VsJ#i+M#I!uZ39Hsd&`syp7DD2Q*ucno<k&2O+sQLhzIF#c}X{!JK
zWrJF#<D>#%$t7CHfJ!D!yljf}5(C`F-1poQ+!N-$w~vzog~sk^J(-$?%r(M@0A=`V
z!VJ$<eA(b55V;-bV}?4vSY%Z5{#I+P=n!23ru~HV@9ZAw9$Wv`_9dVqTAb1P&J06P
zbxKM-T#<_iHB}4DctBU$-;01y1?E`#(p*W*;7c2)r2l=)eJg#)=GS)*t1S-GJ+S$W
z=|kqewNLV#=Ev(9D8)3XVME>fAqTvUb{b#lX*cO>AT=~Z!lpvEfgyeNW4GL6CFaZX
zKD6iv4rr-%MnyB^*tzNBXZxG}i=TbwY0t#b!{g|yRY9%#ntkjt6X4YG5GJE0X#bgL
zb7&v+!R@^b@80usAkpZfKo&IQTG}Z7VAG%O^PBdI?je;$s!=4>MH(i-O8BP(NT5=a
z`$V-#lx;cfFo;xd*?OP9hAGyDZE}rFoQUwstVBE3(OZ_^=P%^+Ks0v2OuY904srH0
z04ev;e=`!Gi4LS1Rm2(@#Q|bH{-1WuLx+(N2BIQjN%{XTy}s3#_cYgguSnang~1qm
z7=n$Euz*B`QxXQgQen-TupeU=B0X^0Mh%~7A;B=N)j$A(xDpU}6-StWW96liUsm0c
zW@aFUSDo%w>Y-$_x^@%Pdl=07!)?)JPKcId{Y&(F62THHY&dYlgp0)VNE$vuRFcyj
zbc?X{O5n7$+85~5ZHa{pXQ5af7!kvFf}Y?Ot9mGFnzhfyrad0X4xWaBbkhea>;W8H
z`tzL{XWr>&K@a2g&G}T3$}>p<bO{=2vL(7;2^|}b7$K$${6a320m&>r8&KSOior|g
zyl@sJI$EsOLVBY5CvZWAc2uAfjfjj46hKm(0MS&4>*rgj6D649CK8nuFk%E2?b(h@
z;vHd8dl-06&f>^7h-FVzh<2!8bo|fqAv&yDNiBEnF}EkC)q~IJNodhHHP=rKdb=Kj
zH6PvWGZTGsAI+rjIBH`nWpm(s<|xYoi2{>Cy5e~lbqiHILUqrPrN+y6T%qrC))~)k
z3twt;HHDp!j?c7^ZP?p_@oO^Oj%Yd9gG7oGFQM@QCvjrI#9&fsMrC^<yi@g9==l2o
zEp(36&`CYJaAaSjZdnArIk&o1bLqEvXVjJZb)_jR4@c(VYGjuDbp=R}OpfgNsGse^
zyyWi#L-^;qaGxdsoDGn?1q{!o>fikTj<kpm4%>Umg1MUjrjm<mp<W$;)pCcY^PN{`
z-w4X;tWtZW1KGHu8n)bjN!oKqTa~{F^B0%@<$w8Ko&&dWTq@l3@?ro0002ovPDHLk
FV1i0m8GZl&
new file mode 100644
index 0000000000000000000000000000000000000000..4cd8e3c07123f4599ef408db0389e2b63d9eba4f
GIT binary patch
literal 537
zc$@(l0_OdRP)<h;3K|Lk000e1NJLTq000;O001Tk1ONa4bPkiQ0005tNkl<ZcmeH_
z1MJ;U6o=1j+m88y*$d`iwxiik^=)6>Mhj=#Mx4wx|Fdn~<vx{DG=5)o&iSQ=|NJ~`
zmDB`%r}fOnHntvU>r*3`((pBy(u!_lqV>!;ZXCDXWhyxee)u{7Vs8suSU)PXBUJ{j
znKa9MO}b)l&zX%YY-6FF97inxQj@)Bq>9QaIhDFOe{(`N6YCdIFVt7)q8~Le8v!5s
zMH7<@RTK!~R1-VOaR`9MWM^}LAk8wcqjnyRLj#3w;6Xc`nKI=`abCxM<(=R`oSNyg
z5kP%|acN$nB6#4MWhO#9kXC5eD;g90oWgYV4Vg*vK-#HdN9CP-ooyWQNgu19(uq$X
zZdI_OU;so*KmhrAW-h-0@wki~(QXJux{;YnmjSJlv0Jo@ZqG)zgm^$aK()-o>Gtx+
z>~s^*!&5k1=`B4L(ul`%J$e|#Yf9MTTe21JQj;KqD}i36|0iyrh#k=@YErBLxL_B%
zW<|*vT9Cp~C}6Z`9TH7cq@(-^*W_I89BUEw%b(y53eUa0L~qD2vp9ECz~3`LfkU(o
zckx?c{!M%oc!`#v*YNmj&h7wV#7D8$gJI@GtQUNZnZ;w1k}Ij*>~-c0wLUpNL6LL+
be*EVP1h@O8Hf<mg00000NkvXXu0mjf07LZ9
new file mode 100644
index 0000000000000000000000000000000000000000..55462df1dc0e7aeecf6cff11acb9ce48364358e8
GIT binary patch
literal 151
zc%17D@N?(olHy`uVBq!ia0vp^96-#?!2%>-Ok!CFq~bhX978G?dpnLYIynkB7iM-^
zznl6nic9b7gR-XY<>ux#!m{%n8X7noEe;=lTerYHrvKigDLW0fe7O<kG2`^%NPgpz
z5ATzsOE)vH+rG-?diLzXb!De+;q+feYS({YVEi5|E5OnMvWdad)z4*}Q$iB}StvK;
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/res/values/dimens.xml
@@ -0,0 +1,222 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+    <dimen name="standard_corner_radius">4dp</dimen>
+
+    <dimen name="autocomplete_min_width">200dp</dimen>
+    <dimen name="autocomplete_row_height">32dp</dimen>
+
+    <dimen name="browser_toolbar_height">48dp</dimen>
+    <!-- This value is the height of the Tabs Panel header view
+         (browser_toolbar_height) minus the height of the indicator
+         (6dp). This value should change when the height of the view changes. -->
+    <dimen name="tabs_panel_indicator_selected_padding_top">42dp</dimen>
+
+    <!-- We use two different values for browser_toolbar_height on tablet
+         which is inconsistent. Temporary value until bug 1150730 is fixed. -->
+    <dimen name="browser_toolbar_height_flipper">48dp</dimen>
+    <dimen name="browser_toolbar_button_padding">12dp</dimen>
+    <dimen name="browser_toolbar_icon_width">48dp</dimen>
+    <dimen name="browser_toolbar_menu_icon_height">16dp</dimen>
+
+    <!-- favicon_size includes 4dp of right padding. We can't use margin (which would allow us to
+         specify the actual size) because that would decrease the size of our hit target. -->
+    <dimen name="browser_toolbar_favicon_size">21.33dip</dimen>
+    <dimen name="browser_toolbar_shadow_size">2dp</dimen>
+
+    <!-- If you update one of these values, update the others. -->
+    <dimen name="tablet_nav_button_width">42dp</dimen>
+    <dimen name="tablet_nav_button_width_half">21dp</dimen>
+    <dimen name="tablet_nav_button_width_plus_half">63dp</dimen>
+
+    <!-- This is the system default for the vertical padding for the divider of the TabWidget.
+         Used to mimic the divider padding on the tablet tabs panel back button. -->
+    <dimen name="tab_panel_divider_vertical_padding">12dp</dimen>
+
+    <dimen name="tablet_tab_strip_height">48dp</dimen>
+    <dimen name="tablet_tab_strip_item_width">208dp</dimen>
+    <dimen name="tablet_tab_strip_item_margin">-28dp</dimen>
+    <dimen name="tablet_tab_strip_fading_edge_size">15dp</dimen>
+    <dimen name="tablet_browser_toolbar_menu_item_width">56dp</dimen>
+    <!-- Padding combines with an 18dp image to form the menu item width and height. -->
+    <dimen name="tablet_browser_toolbar_menu_item_padding_horizontal">19dp</dimen>
+    <dimen name="tablet_browser_toolbar_menu_item_inset_vertical">5dp</dimen>
+    <dimen name="tablet_browser_toolbar_menu_item_inset_horizontal">3dp</dimen>
+    <dimen name="tablet_tab_strip_button_inset">5dp</dimen>
+
+    <!-- Dimensions used by Favicons and FaviconView -->
+    <dimen name="favicon_bg">32dp</dimen>
+    <dimen name="favicon_corner_radius">4dp</dimen>
+    <!-- Set the upper limit on the size of favicon that will be processed. Favicons larger than
+         this will be downscaled to this value. If you need to use larger Favicons (Due to a UI
+         redesign sometime after this is written) you should increase this value to the largest
+         commonly-used size of favicon and, performance permitting, fetch the remainder from the
+         database. The largest available size is always stored in the database, regardless of this
+         value.-->
+    <dimen name="favicon_largest_interesting_size">32dp</dimen>
+
+    <dimen name="firstrun_content_width">300dp</dimen>
+    <dimen name="firstrun_min_height">120dp</dimen>
+    <dimen name="firstrun_background_height">120dp</dimen>
+
+    <dimen name="overlay_prompt_content_width">260dp</dimen>
+    <dimen name="overlay_prompt_button_width">148dp</dimen>
+    <dimen name="overlay_prompt_container_width">@dimen/match_parent</dimen>
+
+    <!-- Site security icon -->
+    <dimen name="browser_toolbar_site_security_height">32dp</dimen>
+    <dimen name="browser_toolbar_site_security_width">32dp</dimen>
+    <dimen name="browser_toolbar_site_security_margin_right">0dp</dimen>
+    <dimen name="browser_toolbar_site_security_padding_vertical">7dp</dimen>
+    <dimen name="browser_toolbar_site_security_padding_horizontal">7dp</dimen>
+
+    <!-- If one of these values changes, they all should. -->
+    <dimen name="browser_toolbar_site_security_margin_bottom">.5dp</dimen>
+    <dimen name="site_security_unknown_inset_top">1dp</dimen>
+    <dimen name="site_security_unknown_inset_bottom">-1dp</dimen>
+
+    <dimen name="home_folder_title_oneline_textsize">16sp</dimen>
+    <dimen name="home_folder_title_twoline_textsize">14sp</dimen>
+    <dimen name="home_twolinepagerow_title_textsize">16sp</dimen>
+
+    <dimen name="page_row_edge_padding">16dp</dimen>
+
+    <!-- Regular page row on about:home -->
+    <dimen name="page_row_height">64dp</dimen>
+
+    <!-- Group/heading page row on about:home -->
+    <dimen name="page_group_height">56dp</dimen>
+    <dimen name="home_header_item_height">56dp</dimen>
+    <dimen name="page_row_divider_height">1dp</dimen>
+
+    <!-- Remote Tabs static view top padding. Less in landscape on phones. -->
+    <dimen name="home_remote_tabs_top_padding">48dp</dimen>
+
+    <!-- Remote Tabs Hidden devices row height -->
+    <dimen name="home_remote_tabs_hidden_footer_height">40dp</dimen>
+
+    <!-- Search Engine Row height -->
+    <dimen name="search_row_height">48dp</dimen>
+
+    <dimen name="doorhanger_width">300dp</dimen>
+    <dimen name="doorhanger_input_width">250dp</dimen>
+    <dimen name="doorhanger_offsetX">12dp</dimen>
+    <dimen name="doorhanger_offsetY">67dp</dimen>
+    <dimen name="doorhanger_drawable_padding">5dp</dimen>
+    <dimen name="doorhanger_subsection_padding">8dp</dimen>
+    <dimen name="doorhanger_section_padding_small">10dp</dimen>
+    <dimen name="doorhanger_section_padding_medium">20dp</dimen>
+    <dimen name="doorhanger_section_padding_large">30dp</dimen>
+    <dimen name="doorhanger_icon_size">60dp</dimen>
+    <dimen name="doorhanger_rounded_corner_radius">4dp</dimen>
+
+    <dimen name="context_menu_item_horizontal_padding">10dp</dimen>
+
+    <dimen name="flow_layout_spacing">6dp</dimen>
+    <dimen name="menu_item_icon">21dp</dimen>
+    <dimen name="menu_item_textsize">16sp</dimen>
+    <dimen name="menu_item_state_icon">18dp</dimen>
+    <!-- This is chosen to match Android's listPreferredItemHeight.
+         TODO: We should inherit these from the system.
+         http://androidxref.com/4.2.2_r1/xref/frameworks/base/core/res/res/values/themes.xml#123 -->
+    <dimen name="menu_item_row_height">64dip</dimen>
+    <dimen name="menu_item_row_width">240dp</dimen>
+    <dimen name="menu_popup_width">256dp</dimen>
+    <dimen name="nav_button_border_width">1dp</dimen>
+    <dimen name="prompt_service_group_padding_size">32dp</dimen>
+    <dimen name="prompt_service_icon_size">36dp</dimen>
+    <dimen name="prompt_service_icon_text_padding">10dp</dimen>
+    <dimen name="prompt_service_inputs_padding">16dp</dimen>
+    <dimen name="prompt_service_left_right_text_with_icon_padding">10dp</dimen>
+    <dimen name="prompt_service_top_bottom_text_with_icon_padding">8dp</dimen>
+    <dimen name="tabs_panel_indicator_width">60dp</dimen>
+    <dimen name="tabs_panel_button_width">48dp</dimen>
+    <dimen name="tabs_strip_height">48dp</dimen>
+    <dimen name="tabs_strip_button_width">100dp</dimen>
+    <dimen name="tabs_strip_button_padding">18dp</dimen>
+    <dimen name="tabs_strip_shadow_size">1dp</dimen>
+    <dimen name="text_selection_handle_width">47dp</dimen>
+    <dimen name="text_selection_handle_height">58dp</dimen>
+    <dimen name="text_selection_handle_shadow">11dp</dimen>
+    <dimen name="validation_message_height">50dp</dimen>
+    <dimen name="validation_message_margin_top">6dp</dimen>
+
+    <dimen name="tab_thumbnail_width">121dp</dimen>
+    <dimen name="tab_thumbnail_height">90dp</dimen>
+    <dimen name="tab_panel_column_width">129dp</dimen>
+    <dimen name="tab_panel_grid_padding">20dp</dimen>
+    <dimen name="tab_panel_grid_vspacing">20dp</dimen>
+    <dimen name="tab_panel_grid_padding_top">19dp</dimen>
+
+    <dimen name="tab_highlight_stroke_width">4dp</dimen>
+
+    <!-- PageActionButtons dimensions -->
+    <dimen name="page_action_button_width">32dp</dimen>
+
+    <!-- Banner -->
+    <dimen name="home_banner_height">72dp</dimen>
+    <dimen name="home_banner_close_width">42dp</dimen>
+    <dimen name="home_banner_icon_height">48dip</dimen>
+    <dimen name="home_banner_icon_width">48dip</dimen>
+
+    <!-- Icon Grid -->
+    <dimen name="icongrid_columnwidth">128dp</dimen>
+    <dimen name="icongrid_padding">16dp</dimen>
+
+    <!-- PanelRecyclerView dimensions -->
+    <dimen name="panel_grid_view_column_width">150dp</dimen>
+    <dimen name="panel_grid_view_horizontal_spacing">3dp</dimen>
+    <dimen name="panel_grid_view_vertical_spacing">3dp</dimen>
+    <dimen name="panel_grid_view_outer_spacing">3dp</dimen>
+
+    <!-- PanelItemView dimensions -->
+    <dimen name="panel_article_item_height">95dp</dimen>
+
+    <!-- Button toast dimenstions. -->
+    <dimen name="toast_button_corner_radius">2dp</dimen>
+
+    <!-- TabHistoryItemRow dimensions. -->
+    <dimen name="tab_history_timeline_width">3dp</dimen>
+    <dimen name="tab_history_timeline_height">14dp</dimen>
+    <dimen name="tab_history_favicon_bg">32dp</dimen>
+    <dimen name="tab_history_favicon_padding">5dp</dimen>
+    <dimen name="tab_history_favicon_border_enabled">3dp</dimen>
+    <dimen name="tab_history_favicon_border_disabled">1dp</dimen>
+    <dimen name="tab_history_combo_margin_left">15dp</dimen>
+    <dimen name="tab_history_combo_margin_right">15dp</dimen>
+    <dimen name="tab_history_title_fading_width">50dp</dimen>
+    <dimen name="tab_history_title_margin_right">15dp</dimen>
+    <dimen name="tab_history_title_text_size">14sp</dimen>
+    <dimen name="tab_history_bg_width">2dp</dimen>
+    <dimen name="tab_history_border_padding">2dp</dimen>
+
+    <!-- ZoomedView dimensions. -->
+    <dimen name="zoomed_view_toolbar_height">44dp</dimen>
+    <dimen name="drawable_dropshadow_size">3dp</dimen>
+
+    <!-- Find-In-Page dialog dimensions. -->
+    <dimen name="find_in_page_text_margin_left">5dip</dimen>
+    <dimen name="find_in_page_text_margin_right">12dip</dimen>
+    <dimen name="find_in_page_text_padding_left">10dip</dimen>
+    <dimen name="find_in_page_text_padding_right">10dip</dimen>
+    <dimen name="find_in_page_status_margin_right">10dip</dimen>
+    <dimen name="find_in_page_control_margin_top">2dip</dimen>
+    <dimen name="progress_bar_scroll_offset">1.5dp</dimen>
+
+    <!-- Matches the built-in divider height. fwiw, in the framework
+         I suspect this is a drawable rather than a dimen.  -->
+    <dimen name="action_bar_divider_height">2dp</dimen>
+
+    <!-- http://blog.danlew.net/2015/01/06/handling-android-resources-with-non-standard-formats/ -->
+    <item name="match_parent" type="dimen">-1</item>
+    <item name="wrap_content" type="dimen">-2</item>
+
+    <item name="tab_strip_content_start" type="dimen">12dp</item>
+    <item name="firstrun_tab_strip_content_start" type="dimen">15dp</item>
+
+    <item name="notification_media_cover" type="dimen">128dp</item>
+</resources>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/res/values/ids.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+    <item type="id" name="tabQueueNotification"/>
+    <item type="id" name="tabQueueSettingsNotification" />
+    <item type="id" name="guestNotification"/>
+    <item type="id" name="original_height"/>
+    <item type="id" name="menu_items"/>
+    <item type="id" name="menu_margin"/>
+    <item type="id" name="recycler_view_click_support" />
+    <item type="id" name="range_list"/>
+    <item type="id" name="pref_header_general"/>
+    <item type="id" name="pref_header_privacy"/>
+    <item type="id" name="pref_header_search"/>
+    <item type="id" name="updateServicePermissionNotification" />
+    <item type="id" name="websiteContentNotification" />
+
+</resources>
new file mode 100644
--- /dev/null
+++ b/mv_geckoview.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+mkdir -p mobile/android/geckoview/src/main/java/org/mozilla/gecko
+for i in gfx mozglue permissions restrictions sqlite util ; do
+  hg mv mobile/android/base/java/org/mozilla/gecko/$i mobile/android/geckoview/src/main/java/org/mozilla/gecko
+done
+
+for i in \
+  AlarmReceiver.java \
+  AndroidGamepadManager.java \
+  ANRReporter.java \
+  BaseGeckoInterface.java \
+  ContextGetter.java \
+  CrashHandler.java \
+  EventDispatcher.java \
+  GeckoAccessibility.java \
+  GeckoAppShell.java \
+  GeckoBatteryManager.java \
+  GeckoEditable.java \
+  GeckoEditableClient.java \
+  GeckoEditableListener.java \
+  GeckoEvent.java \
+  GeckoHalDefines.java \
+  GeckoInputConnection.java \
+  GeckoJavaSampler.java \
+  GeckoNetworkManager.java \
+  GeckoProfile.java \
+  GeckoProfileDirectories.java \
+  GeckoScreenOrientation.java \
+  GeckoService.java \
+  GeckoSharedPrefs.java \
+  GeckoSmsManager.java \
+  GeckoThread.java \
+  GeckoView.java \
+  GeckoViewChrome.java \
+  GeckoViewContent.java \
+  InputMethods.java \
+  InputConnectionListener.java \
+  NSSBridge.java \
+  NotificationClient.java \
+  NotificationHandler.java \
+  NotificationService.java \
+  PrefsHelper.java \
+  ServiceNotificationClient.java \
+  SmsManager.java \
+  SurfaceBits.java \
+  TouchEventInterceptor.java \
+; do
+  hg mv mobile/android/base/java/org/mozilla/gecko/$i mobile/android/geckoview/src/main/java/org/mozilla/gecko/$i
+done
+
+mkdir -p mobile/android/geckoview/src/main/java/org/mozilla/gecko/widget
+hg mv mobile/android/base/java/org/mozilla/gecko/widget/SwipeDismissListViewTouchListener.java mobile/android/geckoview/src/main/java/org/mozilla/gecko/widget
+
+mkdir -p mobile/android/geckoview/src/main/java/com/googlecode/eyesfree/braille/selfbraille
+hg cp mobile/android/thirdparty/com/googlecode/eyesfree/braille/selfbraille/*.java mobile/android/geckoview/src/main/java/com/googlecode/eyesfree/braille/selfbraille
+
+hg cp mobile/android/base/java/org/mozilla/gecko/annotation mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko
+hg cp mobile/android/base/java/org/mozilla/gecko/SysInfo.java mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko
+
+mkdir -p mobile/android/geckoview/src/main/res/drawable-hdpi
+hg cp mobile/android/base/resources/drawable-hdpi/{home_bg,home_star,ic_status_logo}.png mobile/android/geckoview/src/main/res/drawable-hdpi
+
+mkdir -p mobile/android/geckoview/src/main/res/values
+hg cp mobile/android/base/resources/values/{dimens,ids}.xml mobile/android/geckoview/src/main/res/values
--- a/settings.gradle
+++ b/settings.gradle
@@ -24,20 +24,22 @@ if (json.substs.MOZ_BUILD_APP != 'mobile
 // Set the Android SDK location.  This is the *least specific* mechanism, which
 // is unfortunate: we'd prefer to use the *most specific* mechanism.  That is,
 // local.properties (first 'sdk.dir', then 'android.dir') and then the
 // environment variable ANDROID_HOME will override this.  That's unfortunate,
 // but it's hard to automatically arrange better.
 System.setProperty('android.home', json.substs.ANDROID_SDK_ROOT)
 
 include ':app'
+include ':geckoview'
 include ':omnijar'
 include ':thirdparty'
 
 project(':app').projectDir = new File("${json.topsrcdir}/mobile/android/app")
+project(':geckoview').projectDir = new File("${json.topsrcdir}/mobile/android/geckoview")
 project(':omnijar').projectDir = new File("${json.topsrcdir}/mobile/android/app/omnijar")
 project(':thirdparty').projectDir = new File("${json.topsrcdir}/mobile/android/thirdparty")
 
 if (json.substs.MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER) {
     include ':bouncer'
     project(':bouncer').projectDir = new File("${json.topsrcdir}/mobile/android/bouncer")
 }