Merge GECKOVIEW_62_RELBRANCH with default. a=merge GECKOVIEW_62_RELBRANCH
authorRyan VanderMeulen <ryanvm@gmail.com>
Fri, 07 Sep 2018 11:25:27 -0400
branchGECKOVIEW_62_RELBRANCH
changeset 481123 494b2c115bc15aea56c8cace079c5be4e2654096
parent 481113 31683bdf39d7bcacf8d9bf265fc2365024473fbe (diff)
parent 481122 7340ee1824da27ad0d2b3dab7d2e00bc2cc08d86 (current diff)
child 481124 ece2f65470020c3ddecefcb6e285889c1c460ece
push id1779
push userryanvm@gmail.com
push dateFri, 07 Sep 2018 15:27:46 +0000
treeherdermozilla-release@ece2f6547002 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone62.0.2
Merge GECKOVIEW_62_RELBRANCH with default. a=merge
dom/canvas/test/webgl-mochitest/test_has_rbab.html
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -9035,87 +9035,92 @@ nsDocShell::CopyFavicon(nsIURI* aOldURI,
   if (favSvc) {
     favSvc->CopyFavicons(aOldURI, aNewURI,
       aInPrivateBrowsing ? nsIFaviconService::FAVICON_LOAD_PRIVATE
                          : nsIFaviconService::FAVICON_LOAD_NON_PRIVATE, nullptr);
   }
 #endif
 }
 
-class InternalLoadEvent : public Runnable
+struct InternalLoadData 
 {
 public:
-  InternalLoadEvent(nsDocShell* aDocShell,
-                    nsIURI* aURI,
-                    nsIURI* aOriginalURI,
-                    Maybe<nsCOMPtr<nsIURI>> const& aResultPrincipalURI,
-                    bool aKeepResultPrincipalURIIfSet,
-                    bool aLoadReplace,
-                    nsIURI* aReferrer, uint32_t aReferrerPolicy,
-                    nsIPrincipal* aTriggeringPrincipal,
-                    nsIPrincipal* aPrincipalToInherit,
-                    uint32_t aFlags,
-                    const char* aTypeHint,
-                    nsIInputStream* aPostData,
-                    nsIInputStream* aHeadersData,
-                    uint32_t aLoadType,
-                    nsISHEntry* aSHEntry,
-                    bool aFirstParty,
-                    const nsAString& aSrcdoc,
-                    nsIDocShell* aSourceDocShell,
-                    nsIURI* aBaseURI)
-    : mozilla::Runnable("InternalLoadEvent")
-    , mSrcdoc(aSrcdoc)
+  InternalLoadData(nsDocShell* aDocShell,
+                   nsIURI* aURI,
+                   nsIURI* aOriginalURI,
+                   Maybe<nsCOMPtr<nsIURI>> const& aResultPrincipalURI,
+                   bool aKeepResultPrincipalURIIfSet,
+                   bool aLoadReplace,
+                   nsIURI* aReferrer, uint32_t aReferrerPolicy,
+                   nsIPrincipal* aTriggeringPrincipal,
+                   nsIPrincipal* aPrincipalToInherit,
+                   uint32_t aFlags,
+                   const nsAString& aWindowTarget,
+                   const char* aTypeHint,
+                   const nsAString& aFileName,
+                   nsIInputStream* aPostData,
+                   nsIInputStream* aHeadersData,
+                   uint32_t aLoadType,
+                   nsISHEntry* aSHEntry,
+                   bool aFirstParty,
+                   const nsAString& aSrcdoc,
+                   nsIDocShell* aSourceDocShell,
+                   nsIURI* aBaseURI,
+                   nsIDocShell** aDocShell2,
+                   nsIRequest** aRequest)
+    : mSrcdoc(aSrcdoc)
     , mDocShell(aDocShell)
     , mURI(aURI)
     , mOriginalURI(aOriginalURI)
     , mResultPrincipalURI(aResultPrincipalURI)
     , mKeepResultPrincipalURIIfSet(aKeepResultPrincipalURIIfSet)
     , mLoadReplace(aLoadReplace)
     , mReferrer(aReferrer)
     , mReferrerPolicy(aReferrerPolicy)
     , mTriggeringPrincipal(aTriggeringPrincipal)
     , mPrincipalToInherit(aPrincipalToInherit)
     , mPostData(aPostData)
     , mHeadersData(aHeadersData)
     , mSHEntry(aSHEntry)
     , mFlags(aFlags)
+    , mWindowTarget(aWindowTarget)
+    , mFileName(aFileName)
     , mLoadType(aLoadType)
     , mFirstParty(aFirstParty)
     , mSourceDocShell(aSourceDocShell)
     , mBaseURI(aBaseURI)
+    , mDocShell2(aDocShell2)
+    , mRequest(aRequest)
   {
     // Make sure to keep null things null as needed
     if (aTypeHint) {
       mTypeHint = aTypeHint;
     } else {
       mTypeHint.SetIsVoid(true);
     }
   }
 
-  NS_IMETHOD
-  Run() override
+  nsresult Run()
   {
     return mDocShell->InternalLoad(mURI, mOriginalURI, mResultPrincipalURI,
                                    mKeepResultPrincipalURIIfSet,
                                    mLoadReplace,
                                    mReferrer,
                                    mReferrerPolicy,
                                    mTriggeringPrincipal, mPrincipalToInherit,
-                                   mFlags, EmptyString(),
+                                   mFlags, mWindowTarget,
                                    mTypeHint.IsVoid() ? nullptr
                                                       : mTypeHint.get(),
-                                   VoidString(), mPostData,
+                                   mFileName, mPostData,
                                    mHeadersData, mLoadType, mSHEntry,
                                    mFirstParty, mSrcdoc, mSourceDocShell,
-                                   mBaseURI, nullptr,
-                                   nullptr);
-  }
-
-private:
+                                   mBaseURI, mDocShell2,
+                                   mRequest);
+  }
+
   nsCString mTypeHint;
   nsString mSrcdoc;
 
   RefPtr<nsDocShell> mDocShell;
   nsCOMPtr<nsIURI> mURI;
   nsCOMPtr<nsIURI> mOriginalURI;
   Maybe<nsCOMPtr<nsIURI>> mResultPrincipalURI;
   bool mKeepResultPrincipalURIIfSet;
@@ -9123,22 +9128,184 @@ private:
   nsCOMPtr<nsIURI> mReferrer;
   uint32_t mReferrerPolicy;
   nsCOMPtr<nsIPrincipal> mTriggeringPrincipal;
   nsCOMPtr<nsIPrincipal> mPrincipalToInherit;
   nsCOMPtr<nsIInputStream> mPostData;
   nsCOMPtr<nsIInputStream> mHeadersData;
   nsCOMPtr<nsISHEntry> mSHEntry;
   uint32_t mFlags;
+  nsString mWindowTarget;
+  nsString mFileName;
   uint32_t mLoadType;
   bool mFirstParty;
   nsCOMPtr<nsIDocShell> mSourceDocShell;
   nsCOMPtr<nsIURI> mBaseURI;
+  nsIDocShell** mDocShell2;
+  nsIRequest** mRequest;
 };
 
+class InternalLoadEvent : public Runnable
+{
+public:
+  InternalLoadEvent(nsDocShell* aDocShell,
+                    nsIURI* aURI,
+                    nsIURI* aOriginalURI,
+                    Maybe<nsCOMPtr<nsIURI>> const& aResultPrincipalURI,
+                    bool aKeepResultPrincipalURIIfSet,
+                    bool aLoadReplace,
+                    nsIURI* aReferrer,
+                    uint32_t aReferrerPolicy,
+                    nsIPrincipal* aTriggeringPrincipal,
+                    nsIPrincipal* aPrincipalToInherit,
+                    uint32_t aFlags,
+                    const char* aTypeHint,
+                    nsIInputStream* aPostData,
+                    nsIInputStream* aHeadersData,
+                    uint32_t aLoadType,
+                    nsISHEntry* aSHEntry,
+                    bool aFirstParty,
+                    const nsAString& aSrcdoc,
+                    nsIDocShell* aSourceDocShell,
+                    nsIURI* aBaseURI)
+    : mozilla::Runnable("InternalLoadEvent")
+    , mLoadData(aDocShell,
+                aURI,
+                aOriginalURI,
+                aResultPrincipalURI,
+                aKeepResultPrincipalURIIfSet,
+                aLoadReplace,
+                aReferrer,
+                aReferrerPolicy,
+                aTriggeringPrincipal,
+                aPrincipalToInherit,
+                aFlags,
+                EmptyString(),
+                aTypeHint,
+                VoidString(),
+                aPostData,
+                aHeadersData,
+                aLoadType,
+                aSHEntry,
+                aFirstParty,
+                aSrcdoc,
+                aSourceDocShell,
+                aBaseURI,
+                nullptr,
+                nullptr) 
+  {}
+
+  NS_IMETHOD
+  Run() override
+  {
+    return mLoadData.Run();
+  }
+
+private:
+  InternalLoadData mLoadData;
+};
+
+class LoadURIDelegateHandler final : public PromiseNativeHandler
+{
+public:
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_CLASS(LoadURIDelegateHandler)
+
+  LoadURIDelegateHandler(nsDocShell* aDocShell,
+                         nsIURI* aURI,
+                         nsIURI* aOriginalURI,
+                         Maybe<nsCOMPtr<nsIURI>> const& aResultPrincipalURI,
+                         bool aKeepResultPrincipalURIIfSet,
+                         bool aLoadReplace,
+                         nsIURI* aReferrer,
+                         uint32_t aReferrerPolicy,
+                         nsIPrincipal* aTriggeringPrincipal,
+                         nsIPrincipal* aPrincipalToInherit,
+                         uint32_t aFlags,
+                         const nsAString& aWindowTarget,
+                         const char* aTypeHint,
+                         const nsAString& aFileName,
+                         nsIInputStream* aPostData,
+                         nsIInputStream* aHeadersData,
+                         uint32_t aLoadType,
+                         nsISHEntry* aSHEntry,
+                         bool aFirstParty,
+                         const nsAString& aSrcdoc,
+                         nsIDocShell* aSourceDocShell,
+                         nsIURI* aBaseURI,
+                         nsIDocShell** aDocShell2,
+                         nsIRequest** aRequest)
+    : mLoadData(aDocShell,
+                aURI,
+                aOriginalURI,
+                aResultPrincipalURI,
+                aKeepResultPrincipalURIIfSet,
+                aLoadReplace,
+                aReferrer,
+                aReferrerPolicy,
+                aTriggeringPrincipal,
+                aPrincipalToInherit,
+                aFlags,
+                aWindowTarget,
+                aTypeHint,
+                aFileName,
+                aPostData,
+                aHeadersData,
+                aLoadType,
+                aSHEntry,
+                aFirstParty,
+                aSrcdoc,
+                aSourceDocShell,
+                aBaseURI,
+                aDocShell2,
+                aRequest)
+  {}
+
+  void
+  ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override
+  {
+    if (aValue.isBoolean() && !aValue.toBoolean()) {
+      // Things went fine, not handled by app, let Gecko do its thing
+      mLoadData.Run();
+    } else if (!aValue.isBoolean()) {
+      // If the promise resolves to a non-boolean, let Gecko handle the load
+      mLoadData.Run();
+    }
+  }
+
+  void
+  RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override
+  {
+    // In the event of a rejected callback, let Gecko handle the load
+    mLoadData.Run();
+  }
+
+private:
+  ~LoadURIDelegateHandler()
+  {}
+
+  InternalLoadData mLoadData;
+};
+
+NS_IMPL_CYCLE_COLLECTION(LoadURIDelegateHandler, mLoadData.mDocShell,
+                         mLoadData.mURI, mLoadData.mOriginalURI,
+                         mLoadData.mResultPrincipalURI, mLoadData.mReferrer,
+                         mLoadData.mTriggeringPrincipal,
+                         mLoadData.mPrincipalToInherit, 
+                         mLoadData.mPostData, mLoadData.mHeadersData,
+                         mLoadData.mSHEntry, mLoadData.mSourceDocShell,
+                         mLoadData.mBaseURI)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LoadURIDelegateHandler)
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(LoadURIDelegateHandler)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(LoadURIDelegateHandler)
+
 /**
  * Returns true if we started an asynchronous load (i.e., from the network), but
  * the document we're loading there hasn't yet become this docshell's active
  * document.
  *
  * When JustStartedNetworkLoad is true, you should be careful about modifying
  * mLoadType and mLSHE.  These are both set when the asynchronous load first
  * starts, and the load expects that, when it eventually runs InternalLoad,
@@ -9376,34 +9543,50 @@ nsDocShell::InternalLoad(nsIURI* aURI,
   }
 
   nsIDocument* doc = mContentViewer ? mContentViewer->GetDocument()
                                     : nullptr;
 
   const bool isDocumentAuxSandboxed = doc &&
     (doc->GetSandboxFlags() & SANDBOXED_AUXILIARY_NAVIGATION);
 
-  if (aURI && mLoadURIDelegate &&
+  const bool checkLoadDelegates = !(aFlags & INTERNAL_LOAD_FLAGS_DELEGATES_CHECKED);
+  aFlags = aFlags & ~INTERNAL_LOAD_FLAGS_DELEGATES_CHECKED;
+
+  if (aURI && mLoadURIDelegate && checkLoadDelegates &&
       (!targetDocShell || targetDocShell == static_cast<nsIDocShell*>(this))) {
     // Dispatch only load requests for the current or a new window to the
     // delegate, e.g., to allow for GeckoView apps to handle the load event
     // outside of Gecko.
     const int where = (aWindowTarget.IsEmpty() || targetDocShell)
                       ? nsIBrowserDOMWindow::OPEN_CURRENTWINDOW
                       : nsIBrowserDOMWindow::OPEN_NEWWINDOW;
 
     if (where == nsIBrowserDOMWindow::OPEN_NEWWINDOW && isDocumentAuxSandboxed) {
       return NS_ERROR_DOM_INVALID_ACCESS_ERR;
     }
 
-    bool loadURIHandled = false;
+    RefPtr<dom::Promise> promise;
     rv = mLoadURIDelegate->LoadURI(aURI, where, aFlags, aTriggeringPrincipal,
-                                   &loadURIHandled);
-    if (NS_SUCCEEDED(rv) && loadURIHandled) {
-      // The request has been handled, nothing to do here.
+                                   getter_AddRefs(promise));
+    if (NS_SUCCEEDED(rv) && promise) {
+      const uint32_t flags = aFlags | INTERNAL_LOAD_FLAGS_DELEGATES_CHECKED;
+
+      RefPtr<LoadURIDelegateHandler> handler = 
+        new LoadURIDelegateHandler(this, aURI, aOriginalURI, aResultPrincipalURI,
+                                   aKeepResultPrincipalURIIfSet,
+                                   aLoadReplace, aReferrer, aReferrerPolicy,
+                                   aTriggeringPrincipal, aPrincipalToInherit,
+                                   flags, aWindowTarget, aTypeHint, aFileName, aPostData,
+                                   aHeadersData, aLoadType, aSHEntry, aFirstParty,
+                                   aSrcdoc, aSourceDocShell, aBaseURI, nullptr, nullptr);
+
+      promise->AppendNativeHandler(handler);
+
+      // Checking for load delegates; InternalLoad will be re-called if needed.
       return NS_OK;
     }
   }
 
   //
   // Resolve the window target before going any further...
   // If the load has been targeted to another DocShell, then transfer the
   // load to it...
--- a/docshell/base/nsIDocShell.idl
+++ b/docshell/base/nsIDocShell.idl
@@ -109,16 +109,19 @@ interface nsIDocShell : nsIDocShellTreeI
   // Whether this is the load of a frame's original src attribute
   const long INTERNAL_LOAD_FLAGS_ORIGINAL_FRAME_SRC      = 0x80;
 
   const long INTERNAL_LOAD_FLAGS_NO_OPENER               = 0x100;
 
   // Whether a top-level data URI navigation is allowed for that load
   const long INTERNAL_LOAD_FLAGS_FORCE_ALLOW_DATA_URI    = 0x200;
 
+  // Whether load delegates have already been checked for this load
+  const long INTERNAL_LOAD_FLAGS_DELEGATES_CHECKED       = 0x400;
+
   // Whether the load was triggered by user interaction.
   const long INTERNAL_LOAD_FLAGS_IS_USER_TRIGGERED       = 0x1000;
 
   /**
    * Loads the given URI.  This method is identical to loadURI(...) except
    * that its parameter list is broken out instead of being packaged inside
    * of an nsIDocShellLoadInfo object...
    *
--- a/mfbt/Maybe.h
+++ b/mfbt/Maybe.h
@@ -17,16 +17,18 @@
 #include "mozilla/OperatorNewExtensions.h"
 #include "mozilla/Poison.h"
 #include "mozilla/TypeTraits.h"
 
 #include <new>  // for placement new
 #include <ostream>
 #include <type_traits>
 
+class nsCycleCollectionTraversalCallback;
+
 namespace mozilla {
 
 struct Nothing { };
 
 namespace detail {
 
 // You would think that poisoning Maybe instances could just be a call
 // to mozWritePoison.  Unfortunately, using a simple call to
@@ -708,11 +710,33 @@ operator<=(const Maybe<T>& aLHS, const M
 }
 
 template<typename T> bool
 operator>=(const Maybe<T>& aLHS, const Maybe<T>& aRHS)
 {
   return !(aLHS < aRHS);
 }
 
+
+template<typename T>
+void
+ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback,
+                            Maybe<T>& aMaybe,
+                            const char* aName,
+                            uint32_t aFlags = 0)
+{
+  if (aMaybe.isSome()) {
+    ImplCycleCollectionTraverse(aCallback, aMaybe.ref(), aName, aFlags);
+  }
+}
+
+template<typename T>
+void
+ImplCycleCollectionUnlink(Maybe<T>& aMaybe)
+{
+  if (aMaybe.isSome()) {
+    ImplCycleCollectionUnlink(aMaybe.ref());
+  }
+}
+
 } // namespace mozilla
 
 #endif /* mozilla_Maybe_h */
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
@@ -151,28 +151,16 @@ public class CustomTabsActivity extends 
             finish();
         }
 
         sendTelemetry();
         recordCustomTabUsage(getReferrerHost());
     }
 
     @Override
-    public void onResume() {
-        mGeckoSession.setActive(true);
-        super.onResume();
-    }
-
-    @Override
-    public void onPause() {
-        mGeckoSession.setActive(false);
-        super.onPause();
-    }
-
-    @Override
     public void onDestroy() {
         mGeckoSession.close();
         mTextSelection.destroy();
         mFormAssistPopup.destroy();
         mDoorHangerPopup.destroy();
         mPromptService.destroy();
 
         super.onDestroy();
--- a/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
@@ -206,28 +206,16 @@ public class WebAppActivity extends AppC
         if (android.os.Build.VERSION.SDK_INT >= 21) {
             finishAndRemoveTask();
         } else {
             finish();
         }
     }
 
     @Override
-    public void onResume() {
-        mGeckoSession.setActive(true);
-        super.onResume();
-    }
-
-    @Override
-    public void onPause() {
-        mGeckoSession.setActive(false);
-        super.onPause();
-    }
-
-    @Override
     public void onDestroy() {
         mGeckoSession.close();
         mTextSelection.destroy();
         mFormAssistPopup.destroy();
         mDoorHangerPopup.destroy();
         mPromptService.destroy();
         super.onDestroy();
     }
--- a/mobile/android/chrome/geckoview/GeckoViewContent.js
+++ b/mobile/android/chrome/geckoview/GeckoViewContent.js
@@ -2,21 +2,23 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 ChromeUtils.import("resource://gre/modules/GeckoViewContentModule.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
+  DeferredTask: "resource://gre/modules/DeferredTask.jsm",
+  FormData: "resource://gre/modules/FormData.jsm",
+  FormLikeFactory: "resource://gre/modules/FormLikeFactory.jsm",
+  PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.jsm",
+  ScrollPosition: "resource://gre/modules/ScrollPosition.jsm",
   Services: "resource://gre/modules/Services.jsm",
   SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.jsm",
-  FormData: "resource://gre/modules/FormData.jsm",
-  PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.jsm",
-  ScrollPosition: "resource://gre/modules/ScrollPosition.jsm",
 });
 
 class GeckoViewContent extends GeckoViewContentModule {
   onInit() {
     debug `onInit`;
 
     // We don't load this in the global namespace because
     // a Utils.jsm in a11y will clobber us.
@@ -29,16 +31,29 @@ class GeckoViewContent extends GeckoView
     this.messageManager.addMessageListener("GeckoView:RestoreState",
                                            this);
     this.messageManager.addMessageListener("GeckoView:DOMFullscreenEntered",
                                            this);
     this.messageManager.addMessageListener("GeckoView:DOMFullscreenExited",
                                            this);
     this.messageManager.addMessageListener("GeckoView:ZoomToInput",
                                            this);
+    this.messageManager.addMessageListener("GeckoView:SetActive",
+                                           this);
+
+    const options = {
+        mozSystemGroup: true,
+        capture: false,
+    };
+    addEventListener("DOMFormHasPassword", this, options);
+    addEventListener("DOMInputPasswordAdded", this, options);
+    addEventListener("pagehide", this, options);
+    addEventListener("pageshow", this, options);
+    addEventListener("focusin", this, options);
+    addEventListener("focusout", this, options);
   }
 
   onEnable() {
     debug `onEnable`;
 
     addEventListener("DOMTitleChanged", this, false);
     addEventListener("DOMWindowFocus", this, false);
     addEventListener("DOMWindowClose", this, false);
@@ -174,34 +189,52 @@ class GeckoViewContent extends GeckoView
         break;
 
       case "GeckoView:RestoreState":
         this._savedState = JSON.parse(aMsg.data.state);
 
         if (this._savedState.history) {
           let restoredHistory = SessionHistory.restore(docShell, this._savedState.history);
 
-          addEventListener("load", this, {capture: true, mozSystemGroup: true, once: true});
-          addEventListener("pageshow", this, {capture: true, mozSystemGroup: true, once: true});
+          addEventListener("load", _ => {
+            const formdata = this._savedState.formdata;
+            if (formdata) {
+              FormData.restoreTree(content, formdata);
+            }
+          }, {capture: true, mozSystemGroup: true, once: true});
+
+          addEventListener("pageshow", _ => {
+            const scrolldata = this._savedState.scrolldata;
+            if (scrolldata) {
+              ScrollPosition.restoreTree(content, scrolldata);
+            }
+            delete this._savedState;
+          }, {capture: true, mozSystemGroup: true, once: true});
 
           if (!this.progressFilter) {
             this.progressFilter =
               Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
               .createInstance(Ci.nsIWebProgress);
             this.flags = Ci.nsIWebProgress.NOTIFY_LOCATION;
           }
 
           this.progressFilter.addProgressListener(this, this.flags);
           let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                                     .getInterface(Ci.nsIWebProgress);
           webProgress.addProgressListener(this.progressFilter, this.flags);
 
           restoredHistory.QueryInterface(Ci.nsISHistory).reloadCurrentEntry();
         }
         break;
+
+      case "GeckoView:SetActive":
+          if (content && aMsg.data.suspendMedia) {
+              content.windowUtils.mediaSuspend = aMsg.data.active ? Ci.nsISuspendedTypes.NONE_SUSPENDED : Ci.nsISuspendedTypes.SUSPENDED_PAUSE;
+          }
+        break;
     }
   }
 
   handleEvent(aEvent) {
     debug `handleEvent: ${aEvent.type}`;
 
     switch (aEvent.type) {
       case "contextmenu":
@@ -228,16 +261,27 @@ class GeckoViewContent extends GeckoView
             elementType,
             elementSrc: (isImage || isMedia)
                         ? node.currentSrc || node.src
                         : null
           });
           aEvent.preventDefault();
         }
         break;
+      case "DOMFormHasPassword":
+        this._addAutoFillElement(
+            FormLikeFactory.createFromForm(aEvent.composedTarget));
+        break;
+      case "DOMInputPasswordAdded": {
+        const input = aEvent.composedTarget;
+        if (!input.form) {
+          this._addAutoFillElement(FormLikeFactory.createFromField(input));
+        }
+        break;
+      }
       case "MozDOMFullscreen:Request":
         sendAsyncMessage("GeckoView:DOMFullscreenRequest");
         break;
       case "MozDOMFullscreen:Entered":
       case "MozDOMFullscreen:Exited":
         // Content may change fullscreen state by itself, and we should ensure
         // that the parent always exits fullscreen when content has left
         // full screen mode.
@@ -264,31 +308,36 @@ class GeckoViewContent extends GeckoView
           return;
         }
 
         aEvent.preventDefault();
         this.eventDispatcher.sendRequest({
           type: "GeckoView:DOMWindowClose"
         });
         break;
-      case "load": {
-        const formdata = this._savedState.formdata;
-        if (formdata) {
-          FormData.restoreTree(content, formdata);
+      case "focusin":
+        if (aEvent.composedTarget instanceof content.HTMLInputElement) {
+          this._onAutoFillFocus(aEvent.composedTarget);
+        }
+        break;
+      case "focusout":
+        if (aEvent.composedTarget instanceof content.HTMLInputElement) {
+          this._onAutoFillFocus(null);
         }
         break;
-      }
-      case "pageshow": {
-        const scrolldata = this._savedState.scrolldata;
-        if (scrolldata) {
-          ScrollPosition.restoreTree(content, scrolldata);
+      case "pagehide":
+        if (aEvent.target === content.document) {
+          this._clearAutoFillElements();
         }
-        delete this._savedState;
         break;
-      }
+      case "pageshow":
+        if (aEvent.target === content.document && aEvent.persisted) {
+          this._scanAutoFillDocument(aEvent.target);
+        }
+        break;
     }
   }
 
   // WebProgress event handler.
   onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) {
     debug `onLocationChange`;
 
     if (this._savedState) {
@@ -302,12 +351,206 @@ class GeckoViewContent extends GeckoView
       }
     }
 
     this.progressFilter.removeProgressListener(this);
     let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                               .getInterface(Ci.nsIWebProgress);
     webProgress.removeProgressListener(this.progressFilter);
   }
+
+  /**
+   * Process an auto-fillable form and send the relevant details of the form
+   * to Java. Multiple calls within a short time period for the same form are
+   * coalesced, so that, e.g., if multiple inputs are added to a form in
+   * succession, we will only perform one processing pass. Note that for inputs
+   * without forms, FormLikeFactory treats the document as the "form", but
+   * there is no difference in how we process them.
+   *
+   * @param aFormLike A FormLike object produced by FormLikeFactory.
+   * @param aFromDeferredTask Signal that this call came from our internal
+   *                          coalescing task. Other caller must not use this
+   *                          parameter.
+   */
+  _addAutoFillElement(aFormLike, aFromDeferredTask) {
+    let task = this._autoFillTasks &&
+               this._autoFillTasks.get(aFormLike.rootElement);
+    if (task && !aFromDeferredTask) {
+      // We already have a pending task; cancel that and start a new one.
+      debug `Canceling previous auto-fill task`;
+      task.disarm();
+      task = null;
+    }
+
+    if (!task) {
+      if (aFromDeferredTask) {
+        // Canceled before we could run the task.
+        debug `Auto-fill task canceled`;
+        return;
+      }
+      // Start a new task so we can coalesce adding elements in one batch.
+      debug `Deferring auto-fill task`;
+      task = new DeferredTask(
+          () => this._addAutoFillElement(aFormLike, true), 100);
+      task.arm();
+      if (!this._autoFillTasks) {
+        this._autoFillTasks = new WeakMap();
+      }
+      this._autoFillTasks.set(aFormLike.rootElement, task);
+      return;
+    }
+
+    debug `Adding auto-fill ${aFormLike}`;
+
+    this._autoFillTasks.delete(aFormLike.rootElement);
+    this._autoFillId = this._autoFillId || 0;
+
+    if (!this._autoFillInfos) {
+      this._autoFillInfos = new WeakMap();
+      this._autoFillElements = new Map();
+    }
+
+    let sendFocusEvent = false;
+    const getInfo = (element, parent) => {
+      let info = this._autoFillInfos.get(element);
+      if (info) {
+        return info;
+      }
+      info = {
+        id: ++this._autoFillId,
+        parent,
+        tag: element.tagName,
+        type: element instanceof content.HTMLInputElement ? element.type : null,
+        editable: (element instanceof content.HTMLInputElement) &&
+                  ["color", "date", "datetime-local", "email", "month",
+                   "number", "password", "range", "search", "tel", "text",
+                   "time", "url", "week"].includes(element.type),
+        disabled: element instanceof content.HTMLInputElement ? element.disabled
+                                                              : null,
+        attributes: Object.assign({}, ...Array.from(element.attributes)
+            .filter(attr => attr.localName !== "value")
+            .map(attr => ({[attr.localName]: attr.value}))),
+        origin: element.ownerDocument.location.origin,
+      };
+      this._autoFillInfos.set(element, info);
+      this._autoFillElements.set(info.id, Cu.getWeakReference(element));
+      sendFocusEvent |= (element === element.ownerDocument.activeElement);
+      return info;
+    };
+
+    const rootInfo = getInfo(aFormLike.rootElement, null);
+    rootInfo.children = aFormLike.elements.map(
+        element => getInfo(element, rootInfo.id));
+
+    this.eventDispatcher.dispatch("GeckoView:AddAutoFill", rootInfo, {
+      onSuccess: responses => {
+        // `responses` is an object with IDs as keys.
+        debug `Performing auto-fill ${responses}`;
+
+        const AUTOFILL_STATE = "-moz-autofill";
+        const winUtils = content.windowUtils;
+
+        for (let id in responses) {
+          const entry = this._autoFillElements &&
+                        this._autoFillElements.get(+id);
+          const element = entry && entry.get();
+          const value = responses[id] || "";
+
+          if (element instanceof content.HTMLInputElement &&
+              !element.disabled && element.parentElement) {
+            element.value = value;
+
+            // Fire both "input" and "change" events.
+            element.dispatchEvent(new element.ownerGlobal.Event(
+                "input", { bubbles: true }));
+            element.dispatchEvent(new element.ownerGlobal.Event(
+                "change", { bubbles: true }));
+
+            if (winUtils && element.value === value) {
+              // Add highlighting for autofilled fields.
+              winUtils.addManuallyManagedState(element, AUTOFILL_STATE);
+
+              // Remove highlighting when the field is changed.
+              element.addEventListener("input", _ =>
+                  winUtils.removeManuallyManagedState(element, AUTOFILL_STATE),
+                  { mozSystemGroup: true, once: true });
+            }
+
+          } else if (element) {
+            warn `Don't know how to auto-fill ${element.tagName}`;
+          }
+        }
+      },
+      onError: error => {
+        warn `Cannot perform autofill ${error}`;
+      },
+    });
+
+    if (sendFocusEvent) {
+      // We might have missed sending a focus event for the active element.
+      this._onAutoFillFocus(aFormLike.ownerDocument.activeElement);
+    }
+  }
+
+  /**
+   * Called when an auto-fillable field is focused or blurred.
+   *
+   * @param aTarget Focused element, or null if an element has lost focus.
+   */
+  _onAutoFillFocus(aTarget) {
+    debug `Auto-fill focus on ${aTarget && aTarget.tagName}`;
+
+    let info = aTarget && this._autoFillInfos &&
+               this._autoFillInfos.get(aTarget);
+    if (!aTarget || info) {
+      this.eventDispatcher.dispatch("GeckoView:OnAutoFillFocus", info);
+    }
+  }
+
+  /**
+   * Clear all tracked auto-fill forms and notify Java.
+   */
+  _clearAutoFillElements() {
+    debug `Clearing auto-fill`;
+
+    this._autoFillTasks = undefined;
+    this._autoFillInfos = undefined;
+    this._autoFillElements = undefined;
+
+    this.eventDispatcher.sendRequest({
+      type: "GeckoView:ClearAutoFill",
+    });
+  }
+
+  /**
+   * Scan for auto-fillable forms and add them if necessary. Called when a page
+   * is navigated to through history, in which case we don't get our typical
+   * "input added" notifications.
+   *
+   * @param aDoc Document to scan.
+   */
+  _scanAutoFillDocument(aDoc) {
+    // Add forms first; only check forms with password inputs.
+    const inputs = aDoc.querySelectorAll("input[type=password]");
+    let inputAdded = false;
+    for (let i = 0; i < inputs.length; i++) {
+      if (inputs[i].form) {
+        // Let _addAutoFillElement coalesce multiple calls for the same form.
+        this._addAutoFillElement(
+            FormLikeFactory.createFromForm(inputs[i].form));
+      } else if (!inputAdded) {
+        // Treat inputs without forms as one unit, and process them only once.
+        inputAdded = true;
+        this._addAutoFillElement(
+            FormLikeFactory.createFromField(inputs[i]));
+      }
+    }
+
+    // Finally add frames.
+    const frames = aDoc.defaultView.frames;
+    for (let i = 0; i < frames.length; i++) {
+      this._scanAutoFillDocument(frames[i].document);
+    }
+  }
 }
 
 let {debug, warn} = GeckoViewContent.initLogging("GeckoViewContent");
 let module = GeckoViewContent.create(this);
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/www/forms.html
@@ -0,0 +1,26 @@
+<html>
+    <head><title>Forms</title></head>
+    <body>
+        <form>
+            <input type="text" id="user1" value="foo">
+            <input type="password" id="pass1", value="foo">
+            <input type="email" id="email1", value="@">
+            <input type="number" id="number1", value="0">
+            <input type="tel" id="tel1", value="0">
+        </form>
+        <input type="Text" id="user2" value="foo">
+        <input type="PassWord" id="pass2" maxlength="8" value="foo">
+        <input type="button" id="button1" value="foo"/>
+        <input type="checkbox" id="checkbox1"/>
+        <input type="hidden" id="hidden1" value="foo"/>
+
+        <iframe id="iframe"></iframe>
+    </body>
+    <script>
+        addEventListener("load", function(e) {
+            if (window.parent === window) {
+                document.getElementById("iframe").contentWindow.location.href = window.location.href;
+            }
+        });
+    </script>
+</html>
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
@@ -1,30 +1,37 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.geckoview.test
 
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
+import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.ReuseSession
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDevToolsAPI
 
+import android.graphics.Rect
+
 import android.os.Build
+import android.os.Bundle
 
 import android.support.test.filters.MediumTest
 import android.support.test.InstrumentationRegistry
 import android.support.test.runner.AndroidJUnit4
+import android.text.InputType
+import android.util.SparseLongArray
 
 import android.view.accessibility.AccessibilityNodeInfo
 import android.view.accessibility.AccessibilityNodeProvider
 import android.view.accessibility.AccessibilityEvent
 import android.view.accessibility.AccessibilityRecord
 import android.view.View
 import android.view.ViewGroup
+import android.widget.EditText
 
 import android.widget.FrameLayout
 
 import org.hamcrest.Matchers.*
 import org.junit.Test
 import org.junit.Before
 import org.junit.After
 import org.junit.runner.RunWith
@@ -54,21 +61,32 @@ class AccessibilityTest : BaseSessionTes
             val getSourceIdMethod =
                 AccessibilityRecord::class.java.getMethod("getSourceNodeId")
             return getVirtualDescendantId(getSourceIdMethod.invoke(event) as Long)
         } catch (ex: Exception) {
             return 0
         }
     }
 
+    // Get a child ID by index.
+    private fun AccessibilityNodeInfo.getChildId(index: Int): Int =
+            getVirtualDescendantId(
+                    if (Build.VERSION.SDK_INT >= 21)
+                        AccessibilityNodeInfo::class.java.getMethod(
+                                "getChildId", Int::class.java).invoke(this, index) as Long
+                    else
+                        (AccessibilityNodeInfo::class.java.getMethod("getChildNodeIds")
+                                .invoke(this) as SparseLongArray).get(index))
+
     private interface EventDelegate {
         fun onAccessibilityFocused(event: AccessibilityEvent) { }
         fun onFocused(event: AccessibilityEvent) { }
         fun onTextSelectionChanged(event: AccessibilityEvent) { }
         fun onTextChanged(event: AccessibilityEvent) { }
+        fun onWinContentChanged(event: AccessibilityEvent) { }
     }
 
     @Before fun setup() {
         // We initialize a view with a parent and grandparent so that the
         // accessibility events propagate up at least to the parent.
         view = FrameLayout(InstrumentationRegistry.getTargetContext())
         FrameLayout(InstrumentationRegistry.getTargetContext()).addView(view)
         FrameLayout(InstrumentationRegistry.getTargetContext()).addView(view.parent as View)
@@ -83,16 +101,17 @@ class AccessibilityTest : BaseSessionTes
             EventDelegate::class,
         { newDelegate -> (view.parent as View).setAccessibilityDelegate(object : View.AccessibilityDelegate() {
             override fun onRequestSendAccessibilityEvent(host: ViewGroup, child: View, event: AccessibilityEvent): Boolean {
                 when (event.eventType) {
                     AccessibilityEvent.TYPE_VIEW_FOCUSED -> newDelegate.onFocused(event)
                     AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED -> newDelegate.onAccessibilityFocused(event)
                     AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED -> newDelegate.onTextSelectionChanged(event)
                     AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED -> newDelegate.onTextChanged(event)
+                    AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> newDelegate.onWinContentChanged(event)
                     else -> {}
                 }
                 return false
             }
         }) },
         { (view.parent as View).setAccessibilityDelegate(null) },
         object : EventDelegate { })
     }
@@ -163,9 +182,161 @@ class AccessibilityTest : BaseSessionTes
                 if (Build.VERSION.SDK_INT >= 19) {
                     assertThat("Hint has field name",
                             node.extras.getString("AccessibilityNodeInfo.hint"),
                             equalTo("Name"))
                 }
             }
         })
     }
-}
\ No newline at end of file
+
+    @ReuseSession(false) // XXX automation crash fix (bug 1485107)
+    @WithDevToolsAPI
+    @Test fun autoFill() {
+        // Wait for the accessibility nodes to populate.
+        mainSession.loadTestPath(FORMS_HTML_PATH)
+        sessionRule.waitUntilCalled(object : EventDelegate {
+            // For the root document and the iframe document, each has a form group and
+            // a group for inputs outside of forms, so the total count is 4.
+            @AssertCalled(count = 4)
+            override fun onWinContentChanged(event: AccessibilityEvent) {
+            }
+        })
+
+        val autoFills = mapOf(
+                "#user1" to "bar", "#pass1" to "baz", "#user2" to "bar", "#pass2" to "baz") +
+                if (Build.VERSION.SDK_INT >= 19) mapOf(
+                        "#email1" to "a@b.c", "#number1" to "24", "#tel1" to "42")
+                else mapOf(
+                        "#email1" to "bar", "#number1" to "", "#tel1" to "bar")
+
+        // Set up promises to monitor the values changing.
+        val promises = autoFills.flatMap { entry ->
+            // Repeat each test with both the top document and the iframe document.
+            arrayOf("document", "$('#iframe').contentDocument").map { doc ->
+                mainSession.evaluateJS("""new Promise(resolve =>
+                    $doc.querySelector('${entry.key}').addEventListener(
+                        'input', event => resolve([event.target.value, '${entry.value}']),
+                        { once: true }))""").asJSPromise()
+            }
+        }
+
+        // Perform auto-fill and return number of auto-fills performed.
+        fun autoFillChild(id: Int, child: AccessibilityNodeInfo) {
+            // Seal the node info instance so we can perform actions on it.
+            if (child.childCount > 0) {
+                for (i in 0 until child.childCount) {
+                    val childId = child.getChildId(i)
+                    autoFillChild(childId, provider.createAccessibilityNodeInfo(childId))
+                }
+            }
+
+            if (EditText::class.java.name == child.className) {
+                assertThat("Input should be enabled", child.isEnabled, equalTo(true))
+                assertThat("Input should be focusable", child.isFocusable, equalTo(true))
+                if (Build.VERSION.SDK_INT >= 19) {
+                    assertThat("Password type should match", child.isPassword, equalTo(
+                            child.inputType == InputType.TYPE_CLASS_TEXT or
+                                    InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD))
+                }
+
+                val args = Bundle(1)
+                val value = if (child.isPassword) "baz" else
+                    if (Build.VERSION.SDK_INT < 19) "bar" else
+                        when (child.inputType) {
+                            InputType.TYPE_CLASS_TEXT or
+                                    InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> "a@b.c"
+                            InputType.TYPE_CLASS_NUMBER -> "24"
+                            InputType.TYPE_CLASS_PHONE -> "42"
+                            else -> "bar"
+                        }
+
+                val ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE = if (Build.VERSION.SDK_INT >= 21)
+                    AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE else
+                    "ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE"
+                val ACTION_SET_TEXT = if (Build.VERSION.SDK_INT >= 21)
+                    AccessibilityNodeInfo.ACTION_SET_TEXT else 0x200000
+
+                args.putCharSequence(ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, value)
+                assertThat("Can perform auto-fill",
+                           provider.performAction(id, ACTION_SET_TEXT, args), equalTo(true))
+            }
+            child.recycle()
+        }
+
+        autoFillChild(View.NO_ID, provider.createAccessibilityNodeInfo(View.NO_ID))
+
+        // Wait on the promises and check for correct values.
+        for ((actual, expected) in promises.map { it.value.asJSList<String>() }) {
+            assertThat("Auto-filled value must match", actual, equalTo(expected))
+        }
+    }
+
+    @ReuseSession(false) // XXX automation crash fix (bug 1485107)
+    @Test fun autoFill_navigation() {
+        fun countAutoFillNodes(cond: (AccessibilityNodeInfo) -> Boolean =
+                                       { it.className == "android.widget.EditText" },
+                               id: Int = View.NO_ID): Int {
+            val info = provider.createAccessibilityNodeInfo(id)
+            try {
+                return (if (cond(info)) 1 else 0) + (if (info.childCount > 0)
+                    (0 until info.childCount).sumBy {
+                        countAutoFillNodes(cond, info.getChildId(it))
+                    } else 0)
+            } finally {
+                info.recycle()
+            }
+        }
+
+        // Wait for the accessibility nodes to populate.
+        mainSession.loadTestPath(FORMS_HTML_PATH)
+        sessionRule.waitUntilCalled(object : EventDelegate {
+            @AssertCalled(count = 4)
+            override fun onWinContentChanged(event: AccessibilityEvent) {
+            }
+        })
+        assertThat("Initial auto-fill count should match",
+                   countAutoFillNodes(), equalTo(14))
+        assertThat("Password auto-fill count should match",
+                   countAutoFillNodes({ it.isPassword }), equalTo(4))
+
+        // Now wait for the nodes to clear.
+        mainSession.loadTestPath(HELLO_HTML_PATH)
+        mainSession.waitForPageStop()
+        assertThat("Should not have auto-fill fields",
+                   countAutoFillNodes(), equalTo(0))
+
+        // Now wait for the nodes to reappear.
+        mainSession.goBack()
+        sessionRule.waitUntilCalled(object : EventDelegate {
+            @AssertCalled(count = 4)
+            override fun onWinContentChanged(event: AccessibilityEvent) {
+            }
+        })
+        assertThat("Should have auto-fill fields again",
+                   countAutoFillNodes(), equalTo(14))
+        assertThat("Should not have focused field",
+                   countAutoFillNodes({ it.isFocused }), equalTo(0))
+
+        mainSession.evaluateJS("$('#pass1').focus()")
+        sessionRule.waitUntilCalled(object : EventDelegate {
+            @AssertCalled
+            override fun onFocused(event: AccessibilityEvent) {
+            }
+        })
+        assertThat("Should have one focused field",
+                   countAutoFillNodes({ it.isFocused }), equalTo(1))
+        // The focused field, its siblings, and its parent should be visible.
+        assertThat("Should have at least six visible fields",
+                   countAutoFillNodes({ node -> node.isVisibleToUser &&
+                           !(Rect().also({ node.getBoundsInScreen(it) }).isEmpty) }),
+                   greaterThanOrEqualTo(6))
+
+        mainSession.evaluateJS("$('#pass1').blur()")
+        sessionRule.waitUntilCalled(object : EventDelegate {
+            @AssertCalled
+            override fun onFocused(event: AccessibilityEvent) {
+            }
+        })
+        assertThat("Should not have focused field",
+                   countAutoFillNodes({ it.isFocused }), equalTo(0))
+    }
+}
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
@@ -23,16 +23,17 @@ import kotlin.reflect.KClass
  * Common base class for tests using GeckoSessionTestRule,
  * providing the test rule and other utilities.
  */
 open class BaseSessionTest(noErrorCollector: Boolean = false) {
     companion object {
         const val CLICK_TO_RELOAD_HTML_PATH = "/assets/www/clickToReload.html"
         const val CONTENT_CRASH_URL = "about:crashcontent"
         const val DOWNLOAD_HTML_PATH = "/assets/www/download.html"
+        const val FORMS_HTML_PATH = "/assets/www/forms.html"
         const val HELLO_HTML_PATH = "/assets/www/hello.html"
         const val HELLO2_HTML_PATH = "/assets/www/hello2.html"
         const val INPUTS_PATH = "/assets/www/inputs.html"
         const val INVALID_URI = "http://www.test.invalid/"
         const val LOREM_IPSUM_HTML_PATH = "/assets/www/loremIpsum.html"
         const val NEW_SESSION_HTML_PATH = "/assets/www/newSession.html"
         const val NEW_SESSION_CHILD_HTML_PATH = "/assets/www/newSession_child.html"
         const val SAVE_STATE_PATH = "/assets/www/saveState.html"
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
@@ -1,25 +1,33 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.geckoview.test
 
+import android.app.assist.AssistStructure
+import android.os.Build
 import org.mozilla.geckoview.GeckoResult
 import org.mozilla.geckoview.GeckoSession
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.IgnoreCrash
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.ReuseSession
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDevToolsAPI
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
 import org.mozilla.geckoview.test.util.Callbacks
 
 import android.support.test.filters.MediumTest
+import android.support.test.filters.SdkSuppress
 import android.support.test.runner.AndroidJUnit4
+import android.text.InputType
+import android.util.SparseArray
+import android.view.View
+import android.view.ViewStructure
+import android.widget.EditText
 import org.hamcrest.Matchers.*
 import org.junit.Assume.assumeThat
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 class ContentDelegateTest : BaseSessionTest() {
@@ -150,9 +158,232 @@ class ContentDelegateTest : BaseSessionT
         assertThat("'name' field should match",
                 mainSession.evaluateJS("$('#name').value").toString(),
                 equalTo("the name"))
 
         assertThat("Scroll position should match",
                 mainSession.evaluateJS("window.scrollY") as Double,
                 closeTo(100.0, .5))
     }
+
+    val ViewNode by lazy {
+        AssistStructure.ViewNode::class.java.getDeclaredConstructor().apply { isAccessible = true }
+    }
+
+    val ViewNodeBuilder by lazy {
+        Class.forName("android.app.assist.AssistStructure\$ViewNodeBuilder")
+                .getDeclaredConstructor(AssistStructure::class.java,
+                                        AssistStructure.ViewNode::class.java,
+                                        Boolean::class.javaPrimitiveType)
+                .apply { isAccessible = true }
+    }
+
+    // TextInputDelegateTest is parameterized, so we put this test under ContentDelegateTest.
+    @SdkSuppress(minSdkVersion = 23)
+    @WithDevToolsAPI
+    @Test fun autofill() {
+        // Test parts of the Oreo auto-fill API; there is another autofill test in
+        // SessionAccessibility for a11y auto-fill support.
+        mainSession.loadTestPath(FORMS_HTML_PATH)
+        // Wait for the auto-fill nodes to populate.
+        sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
+            // For the root document and the iframe document, each has a form group and
+            // a group for inputs outside of forms, so the total count is 4.
+            @AssertCalled(count = 4)
+            override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
+            }
+        })
+
+        val autoFills = mapOf(
+                "#user1" to "bar", "#user2" to "bar") +
+                if (Build.VERSION.SDK_INT >= 26) mapOf(
+                        "#pass1" to "baz", "#pass2" to "baz", "#email1" to "a@b.c",
+                        "#number1" to "24", "#tel1" to "42")
+                else mapOf(
+                        "#pass1" to "bar", "#pass2" to "bar", "#email1" to "bar",
+                        "#number1" to "", "#tel1" to "bar")
+
+        // Set up promises to monitor the values changing.
+        val promises = autoFills.flatMap { entry ->
+            // Repeat each test with both the top document and the iframe document.
+            arrayOf("document", "$('#iframe').contentDocument").map { doc ->
+                mainSession.evaluateJS("""new Promise(resolve =>
+                $doc.querySelector('${entry.key}').addEventListener(
+                    'input', event => resolve([event.target.value, '${entry.value}']),
+                    { once: true }))""").asJSPromise()
+            }
+        }
+
+        val rootNode = ViewNode.newInstance()
+        val rootStructure = ViewNodeBuilder.newInstance(AssistStructure(), rootNode,
+                /* async */ false) as ViewStructure
+        val autoFillValues = SparseArray<CharSequence>()
+
+        // Perform auto-fill and return number of auto-fills performed.
+        fun checkAutoFillChild(child: AssistStructure.ViewNode) {
+            // Seal the node info instance so we can perform actions on it.
+            if (child.childCount > 0) {
+                for (i in 0 until child.childCount) {
+                    checkAutoFillChild(child.getChildAt(i))
+                }
+            }
+
+            if (child === rootNode) {
+                return
+            }
+
+            assertThat("ID should be valid", child.id, not(equalTo(View.NO_ID)))
+
+            if (Build.VERSION.SDK_INT >= 26) {
+                assertThat("Should have HTML tag",
+                           child.htmlInfo.tag, not(isEmptyOrNullString()))
+                assertThat("Web domain should match",
+                           child.webDomain, equalTo("android"))
+            }
+
+            if (EditText::class.java.name == child.className) {
+                assertThat("Input should be enabled", child.isEnabled, equalTo(true))
+                assertThat("Input should be focusable",
+                           child.isFocusable, equalTo(true))
+                assertThat("Input should be visible",
+                           child.visibility, equalTo(View.VISIBLE))
+
+                if (Build.VERSION.SDK_INT < 26) {
+                    autoFillValues.append(child.id, "bar")
+                    return
+                }
+
+                val htmlInfo = child.htmlInfo
+                assertThat("Should have HTML tag", htmlInfo.tag, equalTo("input"))
+                assertThat("Should have ID attribute",
+                           htmlInfo.attributes.map { it.first }, hasItem("id"))
+
+                assertThat("Autofill type should match",
+                           child.autofillType, equalTo(View.AUTOFILL_TYPE_TEXT))
+
+                assertThat("Autofill hints should match", child.autofillHints, equalTo(
+                        when (child.inputType) {
+                            InputType.TYPE_CLASS_TEXT or
+                                    InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD ->
+                                arrayOf(View.AUTOFILL_HINT_PASSWORD)
+                            InputType.TYPE_CLASS_TEXT or
+                                    InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ->
+                                arrayOf(View.AUTOFILL_HINT_EMAIL_ADDRESS)
+                            InputType.TYPE_CLASS_PHONE -> arrayOf(View.AUTOFILL_HINT_PHONE)
+                            else -> null
+                        }))
+
+                autoFillValues.append(child.id, when (child.inputType) {
+                    InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> "baz"
+                    InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> "a@b.c"
+                    InputType.TYPE_CLASS_NUMBER -> "24"
+                    InputType.TYPE_CLASS_PHONE -> "42"
+                    else -> "bar"
+                })
+            }
+        }
+
+        mainSession.textInput.onProvideAutofillVirtualStructure(rootStructure, 0)
+        checkAutoFillChild(rootNode)
+        mainSession.textInput.autofill(autoFillValues)
+
+        // Wait on the promises and check for correct values.
+        for ((actual, expected) in promises.map { it.value.asJSList<String>() }) {
+            assertThat("Auto-filled value must match", actual, equalTo(expected))
+        }
+    }
+
+    // TextInputDelegateTest is parameterized, so we put this test under ContentDelegateTest.
+    @SdkSuppress(minSdkVersion = 23)
+    @WithDevToolsAPI
+    @WithDisplay(width = 100, height = 100)
+    @Test fun autoFill_navigation() {
+
+        fun countAutoFillNodes(cond: (AssistStructure.ViewNode) -> Boolean =
+                                       { it.className == "android.widget.EditText" },
+                               root: AssistStructure.ViewNode? = null): Int {
+            val node = if (root !== null) root else ViewNode.newInstance().also {
+                // Fill the nodes first.
+                val structure = ViewNodeBuilder.newInstance(
+                        AssistStructure(), it, /* async */ false) as ViewStructure
+                mainSession.textInput.onProvideAutofillVirtualStructure(structure, 0)
+            }
+            return (if (cond(node)) 1 else 0) +
+                    (if (node.childCount > 0) (0 until node.childCount).sumBy {
+                        countAutoFillNodes(cond, node.getChildAt(it)) } else 0)
+        }
+
+        // Wait for the accessibility nodes to populate.
+        mainSession.loadTestPath(FORMS_HTML_PATH)
+        sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
+            @AssertCalled(count = 4)
+            override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Should be starting auto-fill", notification, equalTo(forEachCall(
+                        GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_STARTED,
+                        GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_ADDED)))
+                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
+            }
+        })
+        assertThat("Initial auto-fill count should match",
+                   countAutoFillNodes(), equalTo(14))
+
+        // Now wait for the nodes to clear.
+        mainSession.loadTestPath(HELLO_HTML_PATH)
+        sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
+            @AssertCalled(count = 1)
+            override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Should be canceling auto-fill",
+                           notification,
+                           equalTo(GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_CANCELED))
+                assertThat("ID should be valid", virtualId, equalTo(View.NO_ID))
+            }
+        })
+        assertThat("Should not have auto-fill fields",
+                   countAutoFillNodes(), equalTo(0))
+
+        // Now wait for the nodes to reappear.
+        mainSession.waitForPageStop()
+        mainSession.goBack()
+        sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
+            @AssertCalled(count = 4)
+            override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Should be starting auto-fill", notification, equalTo(forEachCall(
+                        GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_STARTED,
+                        GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_ADDED)))
+                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
+            }
+        })
+        assertThat("Should have auto-fill fields again",
+                   countAutoFillNodes(), equalTo(14))
+        assertThat("Should not have focused field",
+                   countAutoFillNodes({ it.isFocused }), equalTo(0))
+
+        mainSession.evaluateJS("$('#pass1').focus()")
+        sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
+            @AssertCalled(count = 1)
+            override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Should be entering auto-fill view",
+                           notification,
+                           equalTo(GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED))
+                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
+            }
+        })
+        assertThat("Should have one focused field",
+                   countAutoFillNodes({ it.isFocused }), equalTo(1))
+        // The focused field, its siblings, and its parent should be visible.
+        assertThat("Should have at least six visible fields",
+                   countAutoFillNodes({ node -> node.width > 0 && node.height > 0 }),
+                   greaterThanOrEqualTo(6))
+
+        mainSession.evaluateJS("$('#pass1').blur()")
+        sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
+            @AssertCalled(count = 1)
+            override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Should be exiting auto-fill view",
+                           notification,
+                           equalTo(GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_EXITED))
+                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
+            }
+        })
+        assertThat("Should not have focused field",
+                   countAutoFillNodes({ it.isFocused }), equalTo(0))
+    }
 }
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ProgressDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ProgressDelegateTest.kt
@@ -81,17 +81,16 @@ class ProgressDelegateTest : BaseSession
     @Test fun loadUnknownHost() {
         loadExpectNetError(INVALID_URI)
     }
 
     @Test fun loadBadPort() {
         loadExpectNetError("http://localhost:1/")
     }
 
-    @Ignore
     @Test fun multipleLoads() {
         sessionRule.session.loadUri(INVALID_URI)
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitForPageStops(2)
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressDelegate {
             @AssertCalled(count = 2, order = [1, 3])
             override fun onPageStart(session: GeckoSession, url: String) {
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.kt
@@ -156,10 +156,13 @@ class Callbacks private constructor() {
         override fun updateSelection(session: GeckoSession, selStart: Int, selEnd: Int, compositionStart: Int, compositionEnd: Int) {
         }
 
         override fun updateExtractedText(session: GeckoSession, request: ExtractedTextRequest, text: ExtractedText) {
         }
 
         override fun updateCursorAnchorInfo(session: GeckoSession, info: CursorAnchorInfo) {
         }
+
+        override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
+        }
     }
 }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSharedPrefs.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSharedPrefs.java
@@ -168,16 +168,20 @@ public final class GeckoSharedPrefs {
     }
 
     /**
      * 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 (!GeckoAppShell.isFennec()) {
+            return;
+        }
+
         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.
@@ -250,17 +254,17 @@ public final class GeckoSharedPrefs {
         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,
+    private 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();
@@ -279,17 +283,17 @@ public final class GeckoSharedPrefs {
         // 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,
+    private 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());
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntimeSettings.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntimeSettings.java
@@ -209,16 +209,47 @@ public final class GeckoRuntimeSettings 
          * @return This Builder instance.
          **/
         public @NonNull Builder trackingProtectionCategories(
                 @TrackingProtectionDelegate.Category int categories) {
             mSettings.mTrackingProtection
                      .set(TrackingProtection.buildPrefValue(categories));
             return this;
         }
+
+        /** Set whether or not known malware sites should be blocked.
+         *
+         * Note: For each blocked site, {@link NavigationDelegate#onLoadError}
+         * with error category {@link NavigationDelegate#ERROR_CATEGORY_SAFEBROWSING}
+         * is called.
+         *
+         * @param enabled A flag determining whether or not to block malware
+         *                sites.
+         * @return The builder instance.
+         */
+        public @NonNull Builder blockMalware(boolean enabled) {
+            mSettings.mSafebrowsingMalware.set(enabled);
+            return this;
+        }
+
+        /**
+         * Set whether or not known phishing sites should be blocked.
+         *
+         * Note: For each blocked site, {@link NavigationDelegate#onLoadError}
+         * with error category {@link NavigationDelegate#ERROR_CATEGORY_SAFEBROWSING}
+         * is called.
+         *
+         * @param enabled A flag determining whether or not to block phishing
+         *                sites.
+         * @return The builder instance.
+         */
+        public @NonNull Builder blockPhishing(boolean enabled) {
+            mSettings.mSafebrowsingPhishing.set(enabled);
+            return this;
+        }
     }
 
     /* package */ GeckoRuntime runtime;
     /* package */ boolean mUseContentProcess;
     /* package */ String[] mArgs;
     /* package */ Bundle mExtras;
     /* package */ int prefCount;
 
@@ -267,24 +298,29 @@ public final class GeckoRuntimeSettings 
         "network.cookie.lifetime.days", 90);
     /* package */ Pref<String> mTrackingProtection = new Pref<String>(
         "urlclassifier.trackingTable",
         TrackingProtection.buildPrefValue(
             TrackingProtectionDelegate.CATEGORY_TEST |
             TrackingProtectionDelegate.CATEGORY_ANALYTIC |
             TrackingProtectionDelegate.CATEGORY_SOCIAL |
             TrackingProtectionDelegate.CATEGORY_AD));
+    /* package */ Pref<Boolean> mSafebrowsingMalware = new Pref<Boolean>(
+        "browser.safebrowsing.malware.enabled", true);
+    /* package */ Pref<Boolean> mSafebrowsingPhishing = new Pref<Boolean>(
+        "browser.safebrowsing.phishing.enabled", true);
 
     /* package */ boolean mNativeCrashReporting;
     /* package */ boolean mJavaCrashReporting;
     /* package */ boolean mDebugPause;
 
     private final Pref<?>[] mPrefs = new Pref<?>[] {
         mCookieBehavior, mCookieLifetime, mCookieLifetimeDays, mJavaScript,
-        mRemoteDebugging, mTrackingProtection, mWebFonts
+        mRemoteDebugging, mSafebrowsingMalware, mSafebrowsingPhishing,
+        mTrackingProtection, mWebFonts
     };
 
     /* package */ GeckoRuntimeSettings() {
         this(null);
     }
 
     /* package */ GeckoRuntimeSettings(final @Nullable GeckoRuntimeSettings settings) {
         if (BuildConfig.DEBUG && prefCount != mPrefs.length) {
@@ -571,16 +607,64 @@ public final class GeckoRuntimeSettings 
      * @return This GeckoRuntimeSettings instance.
      **/
     public @NonNull GeckoRuntimeSettings setTrackingProtectionCategories(
             @TrackingProtectionDelegate.Category int categories) {
         mTrackingProtection.set(TrackingProtection.buildPrefValue(categories));
         return this;
     }
 
+    /**
+     * Set whether or not known malware sites should be blocked.
+     *
+     * Note: For each blocked site, {@link NavigationDelegate#onLoadError}
+     * with error category {@link NavigationDelegate#ERROR_CATEGORY_SAFEBROWSING}
+     * is called.
+     *
+     * @param enabled A flag determining whether or not to block malware sites.
+     * @return The GeckoRuntimeSettings instance.
+     */
+    public @NonNull GeckoRuntimeSettings setBlockMalware(boolean enabled) {
+        mSafebrowsingMalware.set(enabled);
+        return this;
+    }
+
+    /**
+     * Get whether or not known malware sites are blocked.
+     *
+     * @return True if malware site blocking is enabled.
+     */
+    public boolean getBlockMalware() {
+        return mSafebrowsingMalware.get();
+    }
+
+    /**
+     * Set whether or not known phishing sites should be blocked.
+     *
+     * Note: For each blocked site, {@link NavigationDelegate#onLoadError}
+     * with error category {@link NavigationDelegate#ERROR_CATEGORY_SAFEBROWSING}
+     * is called.
+     *
+     * @param enabled A flag determining whether or not to block phishing sites.
+     * @return The GeckoRuntimeSettings instance.
+     */
+    public @NonNull GeckoRuntimeSettings setBlockPhishing(boolean enabled) {
+        mSafebrowsingPhishing.set(enabled);
+        return this;
+    }
+
+    /**
+     * Get whether or not known phishing sites are blocked.
+     *
+     * @return True if phishing site blocking is enabled.
+     */
+    public boolean getBlockPhishing() {
+        return mSafebrowsingPhishing.get();
+    }
+
     @Override // Parcelable
     public int describeContents() {
         return 0;
     }
 
     @Override // Parcelable
     public void writeToParcel(Parcel out, int flags) {
         ParcelableUtils.writeBoolean(out, mUseContentProcess);
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -883,16 +883,22 @@ public class GeckoSession extends LayerS
 
         onWindowChanged(WINDOW_CLOSE, /* inProgress */ false);
     }
 
     private void onWindowChanged(int change, boolean inProgress) {
         if ((change == WINDOW_OPEN || change == WINDOW_TRANSFER_IN) && !inProgress) {
             mTextInput.onWindowChanged(mWindow);
         }
+        if ((change == WINDOW_CLOSE || change == WINDOW_TRANSFER_OUT) && !inProgress) {
+            if (mAccessibility != null) {
+                mAccessibility.clearAutoFill();
+            }
+            mTextInput.clearAutoFill();
+        }
     }
 
     /**
      * Get the SessionTextInput instance for this session. May be called on any thread.
      *
      * @return SessionTextInput instance.
      */
     public @NonNull SessionTextInput getTextInput() {
@@ -1205,28 +1211,51 @@ public class GeckoSession extends LayerS
     public SessionFinder getFinder() {
         if (mFinder == null) {
             mFinder = new SessionFinder(getEventDispatcher());
         }
         return mFinder;
     }
 
     /**
-    * Set this GeckoSession as active or inactive. Setting a GeckoSession to inactive will
-    * significantly reduce its memory footprint, but should only be done if the
-    * GeckoSession is not currently visible.
-    * @param active A boolean determining whether the GeckoSession is active
-    */
+     * Set this GeckoSession as active or inactive, which represents if the session is currently
+     * visible or not. Setting a GeckoSession to inactive will significantly reduce its memory
+     * footprint, but should only be done if the GeckoSession is not currently visible. Note that
+     * a session can be active (i.e. visible) but not focused.
+     *
+     * @param active A boolean determining whether the GeckoSession is active.
+     *
+     * @see #setFocused
+     */
     public void setActive(boolean active) {
-        final GeckoBundle msg = new GeckoBundle();
+        final GeckoBundle msg = new GeckoBundle(1);
         msg.putBoolean("active", active);
         mEventDispatcher.dispatch("GeckoView:SetActive", msg);
     }
 
     /**
+     * Move focus to this session or away from this session. Only one session has focus at
+     * a given time. Note that a session can be unfocused but still active (i.e. visible).
+     *
+     * @param focused True if the session should gain focus or
+     *                false if the session should lose focus.
+     *
+     * @see #setActive
+     */
+    public void setFocused(boolean focused) {
+        final GeckoBundle msg = new GeckoBundle(1);
+        msg.putBoolean("focused", focused);
+        mEventDispatcher.dispatch("GeckoView:SetFocused", msg);
+
+        if (focused && mAccessibility != null) {
+            mAccessibility.onWindowFocus();
+        }
+    }
+
+    /**
      * Class representing a saved session state.
      */
     public static class SessionState implements Parcelable {
         private String mState;
 
         /**
          * Construct a SessionState from a String.
          *
@@ -3168,10 +3197,50 @@ public class GeckoSession extends LayerS
          * Update the cursor-anchor information as requested through
          * {@link android.view.inputmethod.InputConnection#requestCursorUpdates}.
          * Consequently, this method is <i>not</i> called in viewless mode.
          *
          * @param session Session instance.
          * @param info Cursor-anchor information.
          */
         void updateCursorAnchorInfo(@NonNull GeckoSession session, @NonNull CursorAnchorInfo info);
+
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef({AUTO_FILL_NOTIFY_STARTED, AUTO_FILL_NOTIFY_COMMITTED, AUTO_FILL_NOTIFY_CANCELED,
+                AUTO_FILL_NOTIFY_VIEW_ADDED, AUTO_FILL_NOTIFY_VIEW_REMOVED,
+                AUTO_FILL_NOTIFY_VIEW_UPDATED, AUTO_FILL_NOTIFY_VIEW_ENTERED,
+                AUTO_FILL_NOTIFY_VIEW_EXITED})
+        /* package */ @interface AutoFillNotification {}
+
+        /** An auto-fill session has started, usually as a result of loading a page. */
+        int AUTO_FILL_NOTIFY_STARTED = 0;
+        /** An auto-fill session has been committed, usually as a result of submitting a form. */
+        int AUTO_FILL_NOTIFY_COMMITTED = 1;
+        /** An auto-fill session has been canceled, usually as a result of unloading a page. */
+        int AUTO_FILL_NOTIFY_CANCELED = 2;
+        /** A view within the auto-fill session has been added. */
+        int AUTO_FILL_NOTIFY_VIEW_ADDED = 3;
+        /** A view within the auto-fill session has been removed. */
+        int AUTO_FILL_NOTIFY_VIEW_REMOVED = 4;
+        /** A view within the auto-fill session has been updated (e.g. change in state). */
+        int AUTO_FILL_NOTIFY_VIEW_UPDATED = 5;
+        /** A view within the auto-fill session has gained focus. */
+        int AUTO_FILL_NOTIFY_VIEW_ENTERED = 6;
+        /** A view within the auto-fill session has lost focus. */
+        int AUTO_FILL_NOTIFY_VIEW_EXITED = 7;
+
+        /**
+         * Notify that an auto-fill event has occurred. The default implementation forwards the
+         * notification to the system {@link android.view.autofill.AutofillManager}. This method is
+         * only called on Android 6.0 and above, and it is called in viewless mode as well.
+         *
+         * @param session Session instance.
+         * @param notification Notification type as one of the {@link #AUTO_FILL_NOTIFY_STARTED
+         *                     AUTO_FILL_NOTIFY_*} constants.
+         * @param virtualId Virtual ID of the target, or {@link android.view.View#NO_ID} if not
+         *                  applicable. The ID matches one of the virtual IDs provided by {@link
+         *                  SessionTextInput#onProvideAutofillVirtualStructure} and can be used
+         *                  with {@link SessionTextInput#autofill}.
+         */
+        void notifyAutoFill(@NonNull GeckoSession session, @AutoFillNotification int notification,
+                            int virtualId);
     }
 }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSessionSettings.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSessionSettings.java
@@ -85,16 +85,23 @@ public final class GeckoSessionSettings 
     /*
      * Key to specify which display-mode we should use
      */
     public static final Key<Integer> DISPLAY_MODE =
         new Key<Integer>("displayMode", /* initOnly */ false,
                          Arrays.asList(DISPLAY_MODE_BROWSER, DISPLAY_MODE_MINIMAL_UI,
                                        DISPLAY_MODE_STANDALONE, DISPLAY_MODE_FULLSCREEN));
 
+    /*
+     * Key to specify if media should be suspended when the session is inactive.
+     */
+    public static final Key<Boolean> SUSPEND_MEDIA_WHEN_INACTIVE =
+        new Key<Boolean>("suspendMediaWhenInactive", /* initOnly */ false, /* values */ null);
+
+
     private final GeckoSession mSession;
     private final GeckoBundle mBundle;
 
     public GeckoSessionSettings() {
         this(null, null);
     }
 
     public GeckoSessionSettings(final @NonNull GeckoSessionSettings settings) {
@@ -111,16 +118,17 @@ public final class GeckoSessionSettings 
         }
 
         mBundle = new GeckoBundle();
         mBundle.putString(CHROME_URI.name, null);
         mBundle.putInt(SCREEN_ID.name, 0);
         mBundle.putBoolean(USE_TRACKING_PROTECTION.name, false);
         mBundle.putBoolean(USE_PRIVATE_MODE.name, false);
         mBundle.putBoolean(USE_MULTIPROCESS.name, true);
+        mBundle.putBoolean(SUSPEND_MEDIA_WHEN_INACTIVE.name, false);
         mBundle.putBoolean(USE_DESKTOP_MODE.name, false);
         mBundle.putInt(DISPLAY_MODE.name, DISPLAY_MODE_BROWSER);
     }
 
     public void setBoolean(final Key<Boolean> key, final boolean value) {
         synchronized (mBundle) {
             if (valueChangedLocked(key, value)) {
                 mBundle.putBoolean(key.name, value);
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoView.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoView.java
@@ -9,38 +9,42 @@ package org.mozilla.geckoview;
 import org.mozilla.gecko.AndroidGamepadManager;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
 import org.mozilla.gecko.gfx.PanZoomController;
 import org.mozilla.gecko.gfx.GeckoDisplay;
 import org.mozilla.gecko.InputMethods;
 import org.mozilla.gecko.util.ActivityUtils;
 
+import android.annotation.TargetApi;
 import android.app.Activity;
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.support.annotation.Nullable;
 import android.support.annotation.NonNull;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
+import android.util.SparseArray;
 import android.util.TypedValue;
 import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewStructure;
+import android.view.autofill.AutofillValue;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.FrameLayout;
 
 public class GeckoView extends FrameLayout {
     private static final String LOGTAG = "GeckoView";
     private static final boolean DEBUG = false;
@@ -103,21 +107,23 @@ public class GeckoView extends FrameLayo
 
             // Tell display there is already a surface.
             onGlobalLayout();
             if (GeckoView.this.mSurfaceView != null) {
                 final SurfaceHolder holder = GeckoView.this.mSurfaceView.getHolder();
                 final Rect frame = holder.getSurfaceFrame();
                 mDisplay.surfaceChanged(holder.getSurface(), frame.right, frame.bottom);
             }
+            GeckoView.this.setActive(true);
         }
 
         public GeckoDisplay release() {
             if (mValid) {
                 mDisplay.surfaceDestroyed();
+                GeckoView.this.setActive(false);
             }
 
             final GeckoDisplay display = mDisplay;
             mDisplay = null;
             return display;
         }
 
         @Override // SurfaceHolder.Callback
@@ -125,24 +131,26 @@ public class GeckoView extends FrameLayo
         }
 
         @Override // SurfaceHolder.Callback
         public void surfaceChanged(final SurfaceHolder holder, final int format,
                                    final int width, final int height) {
             if (mDisplay != null) {
                 mDisplay.surfaceChanged(holder.getSurface(), width, height);
             }
+            GeckoView.this.setActive(true);
             mValid = true;
         }
 
         @Override // SurfaceHolder.Callback
         public void surfaceDestroyed(final SurfaceHolder holder) {
             if (mDisplay != null) {
                 mDisplay.surfaceDestroyed();
             }
+            GeckoView.this.setActive(false);
             mValid = false;
         }
 
         public void onGlobalLayout() {
             if (mDisplay == null) {
                 return;
             }
             if (GeckoView.this.mSurfaceView != null) {
@@ -198,16 +206,22 @@ public class GeckoView extends FrameLayo
      * @param color Cover color.
      */
     public void coverUntilFirstPaint(final int color) {
         if (mSurfaceView != null) {
             mSurfaceView.setBackgroundColor(color);
         }
     }
 
+    /* package */ void setActive(final boolean active) {
+        if (mSession != null) {
+            mSession.setActive(active);
+        }
+    }
+
     public GeckoSession releaseSession() {
         if (mSession == null) {
             return null;
         }
 
         // Cover the view while we are not drawing to the surface.
         coverUntilFirstPaint(Color.WHITE);
 
@@ -219,20 +233,23 @@ public class GeckoView extends FrameLayo
         if (mSession.getAccessibility().getView() == this) {
             mSession.getAccessibility().setView(null);
         }
 
         if (mSession.getTextInput().getView() == this) {
             mSession.getTextInput().setView(null);
         }
 
-        if (session.getSelectionActionDelegate() == mSelectionActionDelegate) {
+        if (mSession.getSelectionActionDelegate() == mSelectionActionDelegate) {
             mSession.setSelectionActionDelegate(null);
         }
 
+        if (isFocused()) {
+            mSession.setFocused(false);
+        }
         mSession = null;
         return session;
     }
 
     /**
      * Attach a session to this view. The session should be opened before
      * attaching.
      *
@@ -310,16 +327,20 @@ public class GeckoView extends FrameLayo
 
         if (session.getAccessibility().getView() == null) {
             session.getAccessibility().setView(this);
         }
 
         if (session.getSelectionActionDelegate() == null && mSelectionActionDelegate != null) {
             session.setSelectionActionDelegate(mSelectionActionDelegate);
         }
+
+        if (isFocused()) {
+            session.setFocused(true);
+        }
     }
 
     public GeckoSession getSession() {
         return mSession;
     }
 
     public EventDispatcher getEventDispatcher() {
         return mSession.getEventDispatcher();
@@ -393,20 +414,37 @@ public class GeckoView extends FrameLayo
             setSession(ss.session, ss.session.getRuntime());
         } else if (ss.session != null) {
             mSession.transferFrom(ss.session);
             mRuntime = ss.session.getRuntime();
         }
     }
 
     @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        super.onWindowFocusChanged(hasWindowFocus);
+
+        if (mSession != null) {
+            mSession.setFocused(hasWindowFocus && isFocused());
+        }
+    }
+
+    @Override
     public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
 
-        if (!gainFocus || mIsResettingFocus) {
+        if (mIsResettingFocus) {
+            return;
+        }
+
+        if (mSession != null) {
+            mSession.setFocused(gainFocus);
+        }
+
+        if (!gainFocus) {
             return;
         }
 
         post(new Runnable() {
             @Override
             public void run() {
                 if (!isFocused()) {
                     return;
@@ -529,9 +567,37 @@ public class GeckoView extends FrameLayo
 
         if (mSession == null) {
             return false;
         }
 
         return mSession.getAccessibility().onMotionEvent(event) ||
                mSession.getPanZoomController().onMotionEvent(event);
     }
+
+    @Override
+    public void onProvideAutofillVirtualStructure(final ViewStructure structure, int flags) {
+        super.onProvideAutofillVirtualStructure(structure, flags);
+
+        if (mSession != null) {
+            mSession.getTextInput().onProvideAutofillVirtualStructure(structure, flags);
+        }
+    }
+
+    @Override
+    @TargetApi(26)
+    public void autofill(@NonNull final SparseArray<AutofillValue> values) {
+        super.autofill(values);
+
+        if (mSession == null) {
+            return;
+        }
+        final SparseArray<CharSequence> strValues = new SparseArray<>(values.size());
+        for (int i = 0; i < values.size(); i++) {
+            final AutofillValue value = values.valueAt(i);
+            if (value.isText()) {
+                // Only text is currently supported.
+                strValues.put(values.keyAt(i), value.getTextValue());
+            }
+        }
+        mSession.getTextInput().autofill(strValues);
+    }
 }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
@@ -12,56 +12,243 @@ import org.mozilla.gecko.util.BundleEven
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
 
 import android.content.Context;
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.os.Build;
 import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.InputType;
 import android.util.Log;
+import android.util.SparseArray;
 import android.view.InputDevice;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewParent;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityNodeProvider;
 
 public class SessionAccessibility {
     private static final String LOGTAG = "GeckoAccessibility";
-    // This is a special ID we use for nodes that are eent sources.
+    private static final boolean DEBUG = false;
+
+    // This is a special ID we use for nodes that are event sources.
     // We expose it as a fragment and not an actual child of the View node.
     private static final int VIRTUAL_CONTENT_ID = -2;
 
     // This is the number BrailleBack uses to start indexing routing keys.
     private static final int BRAILLE_CLICK_BASE_INDEX = -275000000;
 
+    private static final int ACTION_SET_TEXT = 0x200000;
+    private static final String ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE =
+            "ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE";
+
+    /* package */ final class NodeProvider extends AccessibilityNodeProvider {
+        @Override
+        public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualDescendantId) {
+            AccessibilityNodeInfo info = getAutoFillNode(virtualDescendantId);
+            if (info != null) {
+                // Try auto-fill nodes first.
+                return info;
+            }
+
+            info = (virtualDescendantId == VIRTUAL_CONTENT_ID && mVirtualContentNode != null)
+                   ? AccessibilityNodeInfo.obtain(mVirtualContentNode)
+                   : AccessibilityNodeInfo.obtain(mView, virtualDescendantId);
+
+            switch (virtualDescendantId) {
+            case View.NO_ID:
+                // This is the parent View node.
+                // We intentionally don't add VIRTUAL_CONTENT_ID
+                // as a child. It is a source for events,
+                // but not a member of the tree you
+                // can get to by traversing down.
+                if (Build.VERSION.SDK_INT < 17 || mView.getDisplay() != null) {
+                    // When running junit tests we don't have a display
+                    mView.onInitializeAccessibilityNodeInfo(info);
+                }
+                info.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
+                info.setClassName("android.webkit.WebView"); // TODO: WTF
+
+                if (Build.VERSION.SDK_INT >= 19) {
+                    Bundle bundle = info.getExtras();
+                    bundle.putCharSequence(
+                        "ACTION_ARGUMENT_HTML_ELEMENT_STRING_VALUES",
+                        "ARTICLE,BUTTON,CHECKBOX,COMBOBOX,CONTROL," +
+                        "FOCUSABLE,FRAME,GRAPHIC,H1,H2,H3,H4,H5,H6," +
+                        "HEADING,LANDMARK,LINK,LIST,LIST_ITEM,MAIN," +
+                        "MEDIA,RADIO,SECTION,TABLE,TEXT_FIELD," +
+                        "UNVISITED_LINK,VISITED_LINK");
+                }
+                info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
+
+                if (mAutoFillRoots != null) {
+                    // Add auto-fill nodes.
+                    if (DEBUG) {
+                        Log.d(LOGTAG, "Adding roots " + mAutoFillRoots);
+                    }
+                    for (int i = 0; i < mAutoFillRoots.size(); i++) {
+                        info.addChild(mView, mAutoFillRoots.keyAt(i));
+                    }
+                }
+                break;
+            default:
+                info.setParent(mView);
+                info.setSource(mView, virtualDescendantId);
+                info.setVisibleToUser(mView.isShown());
+                info.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
+                info.setEnabled(true);
+                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 (virtualViewId == View.NO_ID) {
+                return performRootAction(action, arguments);
+            }
+            if (action == AccessibilityNodeInfo.ACTION_SET_TEXT) {
+                final String value = arguments.getString(Build.VERSION.SDK_INT >= 21
+                        ? AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE
+                        : ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE);
+                return performAutoFill(virtualViewId, value);
+            }
+            return performContentAction(action, arguments);
+        }
+
+        private boolean performRootAction(int action, Bundle arguments) {
+            switch (action) {
+            case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
+            case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
+                final GeckoBundle data = new GeckoBundle(1);
+                data.putBoolean("gainFocus", action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+                mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityViewFocused", data);
+                return true;
+            }
+
+            return mView.performAccessibilityAction(action, arguments);
+        }
+
+        @SuppressWarnings("fallthrough")
+        private boolean performContentAction(int action, Bundle arguments) {
+            final GeckoBundle data;
+            switch (action) {
+            case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
+                final AccessibilityEvent event = obtainEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, VIRTUAL_CONTENT_ID);
+                ((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
+                return true;
+            case AccessibilityNodeInfo.ACTION_CLICK:
+                mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityActivate", null);
+                return true;
+            case AccessibilityNodeInfo.ACTION_LONG_CLICK:
+                mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityLongPress", null);
+                return true;
+            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+                mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityScrollForward", null);
+                return true;
+            case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
+                mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityScrollBackward", null);
+                return true;
+            case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT:
+                if (mLastItem) {
+                    return false;
+                }
+                // fall-through
+            case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT:
+                if (arguments != null) {
+                    data = new GeckoBundle(1);
+                    data.putString("rule", arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING));
+                } else {
+                    data = null;
+                }
+                mSession.getEventDispatcher().dispatch(action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT ?
+                                                       "GeckoView:AccessibilityNext" : "GeckoView:AccessibilityPrevious", data);
+                return true;
+            case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY:
+            case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY:
+                // 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;
+                    data = new GeckoBundle(1);
+                    data.putInt("keyIndex", keyIndex);
+                    mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityActivate", data);
+                } else if (granularity > 0) {
+                    data = new GeckoBundle(2);
+                    data.putString("direction", action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY ? "Next" : "Previous");
+                    data.putInt("granularity", granularity);
+                    mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityByGranularity", data);
+                }
+                return true;
+            }
+
+            return mView.performAccessibilityAction(action, arguments);
+        }
+    };
+
     // Gecko session we are proxying
     /* package */  final GeckoSession mSession;
     // This is the view that delegates accessibility to us. We also sends event through it.
     private View mView;
-    // Aave we reached the last item in content?
+    // Have we reached the last item in content?
     private boolean mLastItem;
     // Used to store the JSON message and populate the event later in the code path.
     private AccessibilityNodeInfo mVirtualContentNode;
+    // Auto-fill nodes.
+    private SparseArray<GeckoBundle> mAutoFillNodes;
+    private SparseArray<EventCallback> mAutoFillRoots;
+    private int mAutoFillFocusedId = View.NO_ID;
 
     /* package */ SessionAccessibility(final GeckoSession session) {
         mSession = session;
 
-        Settings.getInstance().dispatch();
+        Settings.updateAccessibilitySettings();
 
         session.getEventDispatcher().registerUiThreadListener(new BundleEventListener() {
-            @Override
-            public void handleMessage(final String event, final GeckoBundle message,
-                                      final EventCallback callback) {
-                sendAccessibilityEvent(message);
-            }
-        }, "GeckoView:AccessibilityEvent", null);
+                @Override
+                public void handleMessage(final String event, final GeckoBundle message,
+                                          final EventCallback callback) {
+                    if ("GeckoView:AccessibilityEvent".equals(event)) {
+                        sendAccessibilityEvent(message);
+                    } else if ("GeckoView:AddAutoFill".equals(event)) {
+                        addAutoFill(message, callback);
+                    } else if ("GeckoView:ClearAutoFill".equals(event)) {
+                        clearAutoFill();
+                    } else if ("GeckoView:OnAutoFillFocus".equals(event)) {
+                        onAutoFillFocus(message);
+                    }
+                }
+            },
+            "GeckoView:AccessibilityEvent",
+            "GeckoView:AddAutoFill",
+            "GeckoView:ClearAutoFill",
+            "GeckoView:OnAutoFillFocus",
+            null);
     }
 
     /**
       * Get the View instance that delegates accessibility to this session.
       *
       * @return View instance.
       */
     public View getView() {
@@ -81,190 +268,43 @@ public class SessionAccessibility {
         mView = view;
         mLastItem = false;
 
         if (mView == null) {
             return;
         }
 
         mView.setAccessibilityDelegate(new View.AccessibilityDelegate() {
-            private AccessibilityNodeProvider mAccessibilityNodeProvider;
+            private NodeProvider mProvider;
 
             @Override
             public AccessibilityNodeProvider getAccessibilityNodeProvider(final View hostView) {
-
-                if (mAccessibilityNodeProvider == null)
-                    mAccessibilityNodeProvider = new AccessibilityNodeProvider() {
-                    @Override
-                    public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualDescendantId) {
-                        assertAttachedView(hostView);
-
-                        AccessibilityNodeInfo info = (virtualDescendantId == VIRTUAL_CONTENT_ID && mVirtualContentNode != null) ?
-                                                     AccessibilityNodeInfo.obtain(mVirtualContentNode) :
-                                                     AccessibilityNodeInfo.obtain(mView, virtualDescendantId);
-
-                        switch (virtualDescendantId) {
-                        case View.NO_ID:
-                            // This is the parent View node.
-                            // We intentionally don't add VIRTUAL_CONTENT_ID
-                            // as a child. It is a source for events,
-                            // but not a member of the tree you
-                            // can get to by traversing down.
-                            if (mView.getDisplay() != null) {
-                                // When running junit tests we don't have a display
-                                onInitializeAccessibilityNodeInfo(mView, info);
-                            }
-                            info.setClassName("android.webkit.WebView"); // TODO: WTF
-                            if (Build.VERSION.SDK_INT >= 19) {
-                                Bundle bundle = info.getExtras();
-                                bundle.putCharSequence(
-                                    "ACTION_ARGUMENT_HTML_ELEMENT_STRING_VALUES",
-                                    "ARTICLE,BUTTON,CHECKBOX,COMBOBOX,CONTROL," +
-                                    "FOCUSABLE,FRAME,GRAPHIC,H1,H2,H3,H4,H5,H6," +
-                                    "HEADING,LANDMARK,LINK,LIST,LIST_ITEM,MAIN," +
-                                    "MEDIA,RADIO,SECTION,TABLE,TEXT_FIELD," +
-                                    "UNVISITED_LINK,VISITED_LINK");
-                            }
-                            info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
-                            info.addChild(hostView, VIRTUAL_CONTENT_ID);
-                            break;
-                        default:
-                            info.setParent(mView);
-                            info.setSource(mView, virtualDescendantId);
-                            info.setVisibleToUser(mView.isShown());
-                            info.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
-                            info.setEnabled(true);
-                            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) {
-                        assertAttachedView(hostView);
-
-                        if (virtualViewId == View.NO_ID) {
-                            return performRootAction(action, arguments);
-                        }
-                        return performContentAction(action, arguments);
-                    }
-
-                    private boolean performRootAction(int action, Bundle arguments) {
-                        switch (action) {
-                        case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
-                        case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
-                            final GeckoBundle data = new GeckoBundle(1);
-                            data.putBoolean("gainFocus", action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
-                            mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityViewFocused", data);
-                            return true;
-                        }
-
-                        return mView.performAccessibilityAction(action, arguments);
-                    }
-
-                    @SuppressWarnings("fallthrough")
-                    private boolean performContentAction(int action, Bundle arguments) {
-                        final GeckoBundle data;
-                        switch (action) {
-                        case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
-                            final AccessibilityEvent event = obtainEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, VIRTUAL_CONTENT_ID);
-                            ((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
-                            return true;
-                        case AccessibilityNodeInfo.ACTION_CLICK:
-                            mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityActivate", null);
-                            return true;
-                        case AccessibilityNodeInfo.ACTION_LONG_CLICK:
-                            mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityLongPress", null);
-                            return true;
-                        case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
-                            mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityScrollForward", null);
-                            return true;
-                        case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
-                            mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityScrollBackward", null);
-                            return true;
-                        case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT:
-                            if (mLastItem) {
-                                return false;
-                            }
-                            // fall-through
-                        case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT:
-                            if (arguments != null) {
-                                data = new GeckoBundle(1);
-                                data.putString("rule", arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING));
-                            } else {
-                                data = null;
-                            }
-                            mSession.getEventDispatcher().dispatch(action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT ?
-                                                                   "GeckoView:AccessibilityNext" : "GeckoView:AccessibilityPrevious", data);
-                            return true;
-                        case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY:
-                        case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY:
-                            // 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;
-                                data = new GeckoBundle(1);
-                                data.putInt("keyIndex", keyIndex);
-                                mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityActivate", data);
-                            } else if (granularity > 0) {
-                                data = new GeckoBundle(2);
-                                data.putString("direction", action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY ? "Next" : "Previous");
-                                data.putInt("granularity", granularity);
-                                mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityByGranularity", data);
-                            }
-                            return true;
-                        }
-
-                        return mView.performAccessibilityAction(action, arguments);
-                    }
-
-                    private void assertAttachedView(final View view) {
-                        if (view != mView) {
-                            throw new AssertionError("delegate used with wrong view.");
-                        }
-                    }
-                };
-
-                return mAccessibilityNodeProvider;
+                if (hostView != mView) {
+                    return null;
+                }
+                if (mProvider == null) {
+                    mProvider = new NodeProvider();
+                }
+                return mProvider;
             }
-
         });
     }
 
     private static class Settings {
-        private static final Settings INSTANCE = new Settings();
         private static final String FORCE_ACCESSIBILITY_PREF = "accessibility.force_disabled";
 
-        private volatile boolean mEnabled;
-        /* package */ volatile boolean mForceEnabled;
+        private static volatile boolean sEnabled;
+        private static volatile boolean sTouchExplorationEnabled;
+        /* package */ static volatile boolean sForceEnabled;
 
-        public Settings() {
+        static {
             final Context context = GeckoAppShell.getApplicationContext();
             AccessibilityManager accessibilityManager =
                 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
 
-            mEnabled = accessibilityManager.isEnabled() &&
-                       accessibilityManager.isTouchExplorationEnabled();
-
             accessibilityManager.addAccessibilityStateChangeListener(
             new AccessibilityManager.AccessibilityStateChangeListener() {
                 @Override
                 public void onAccessibilityStateChanged(boolean enabled) {
                     updateAccessibilitySettings();
                 }
             }
             );
@@ -279,43 +319,43 @@ public class SessionAccessibility {
                 }
                 );
             }
 
             PrefsHelper.PrefHandler prefHandler = new PrefsHelper.PrefHandlerBase() {
                 @Override
                 public void prefValue(String pref, int value) {
                     if (pref.equals(FORCE_ACCESSIBILITY_PREF)) {
-                        mForceEnabled = value < 0;
+                        sForceEnabled = value < 0;
                         dispatch();
                     }
                 }
             };
             PrefsHelper.addObserver(new String[]{ FORCE_ACCESSIBILITY_PREF }, prefHandler);
         }
 
-        public static Settings getInstance() {
-            return INSTANCE;
+        public static boolean isEnabled() {
+            return sEnabled || sForceEnabled;
         }
 
-        public static boolean isEnabled() {
-            return INSTANCE.mEnabled || INSTANCE.mForceEnabled;
+        public static boolean isTouchExplorationEnabled() {
+            return sTouchExplorationEnabled || sForceEnabled;
         }
 
-        private void updateAccessibilitySettings() {
+        public static void updateAccessibilitySettings() {
             final AccessibilityManager accessibilityManager = (AccessibilityManager)
                     GeckoAppShell.getApplicationContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
-            mEnabled = accessibilityManager.isEnabled() && accessibilityManager.isTouchExplorationEnabled();
-
+            sEnabled = accessibilityManager.isEnabled();
+            sTouchExplorationEnabled = sEnabled && accessibilityManager.isTouchExplorationEnabled();
             dispatch();
         }
 
-        private void dispatch() {
+        /* package */ static void dispatch() {
             final GeckoBundle ret = new GeckoBundle(1);
-            ret.putBoolean("enabled", mEnabled || mForceEnabled);
+            ret.putBoolean("enabled", isTouchExplorationEnabled());
             // "GeckoView:AccessibilitySettings" is dispatched to the Gecko thread.
             EventDispatcher.getInstance().dispatch("GeckoView:AccessibilitySettings", ret);
             // "GeckoView:AccessibilityEnabled" is dispatched to the UI thread.
             EventDispatcher.getInstance().dispatch("GeckoView:AccessibilityEnabled", ret);
         }
     }
 
     private AccessibilityEvent obtainEvent(final int eventType, final int sourceId) {
@@ -403,17 +443,17 @@ public class SessionAccessibility {
         mSession.getClientToScreenMatrix(matrix);
         matrix.mapPoints(origin);
 
         screenBounds.offset((int) -origin[0], (int) -origin[1]);
         node.setBoundsInParent(screenBounds);
     }
 
     private void sendAccessibilityEvent(final GeckoBundle message) {
-        if (mView == null || !Settings.isEnabled())
+        if (mView == null || !Settings.isTouchExplorationEnabled())
             return;
 
         final int eventType = message.getInt("eventType", -1);
         if (eventType < 0) {
             Log.e(LOGTAG, "No accessibility event type provided");
             return;
         }
 
@@ -451,17 +491,17 @@ public class SessionAccessibility {
         }
 
         final AccessibilityEvent accessibilityEvent = obtainEvent(eventType, eventSource);
         populateEventFromJSON(accessibilityEvent, message);
         ((ViewParent) mView).requestSendAccessibilityEvent(mView, accessibilityEvent);
     }
 
     public boolean onMotionEvent(final MotionEvent event) {
-        if (!Settings.isEnabled()) {
+        if (!Settings.isTouchExplorationEnabled()) {
             return false;
         }
 
         if (event.getSource() != InputDevice.SOURCE_TOUCHSCREEN) {
             return false;
         }
 
         final int action = event.getActionMasked();
@@ -471,9 +511,253 @@ public class SessionAccessibility {
             return false;
         }
 
         final GeckoBundle data = new GeckoBundle(2);
         data.putDoubleArray("coordinates", new double[] {event.getRawX(), event.getRawY()});
         mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityExploreByTouch", data);
         return true;
     }
+
+    private int getAutoFillRootId(final int id) {
+        int root = View.NO_ID;
+        for (int newId = id; newId != View.NO_ID;) {
+            root = newId;
+            newId = mAutoFillNodes.get(newId).getInt("parent", View.NO_ID);
+        }
+        return root;
+    }
+
+    /* package */ AccessibilityNodeInfo getAutoFillNode(final int id) {
+        if (mView == null || mAutoFillRoots == null) {
+            return null;
+        }
+
+        final GeckoBundle bundle = mAutoFillNodes.get(id);
+        if (bundle == null) {
+            return null;
+        }
+
+        if (DEBUG) {
+            Log.d(LOGTAG, "getAutoFillNode(" + id + ')');
+        }
+
+        final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(mView, id);
+        node.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
+        node.setParent(mView, bundle.getInt("parent", View.NO_ID));
+        node.setEnabled(true);
+
+        if (getAutoFillRootId(mAutoFillFocusedId) == getAutoFillRootId(id)) {
+            // Some auto-fill clients require a dummy rect for the focused View.
+            final Rect rect = new Rect();
+            mSession.getSurfaceBounds(rect);
+            node.setVisibleToUser(!rect.isEmpty());
+            node.setBoundsInParent(rect);
+
+            final int[] offset = new int[2];
+            mView.getLocationOnScreen(offset);
+            rect.offset(offset[0], offset[1]);
+            node.setBoundsInScreen(rect);
+        }
+
+        final GeckoBundle[] children = bundle.getBundleArray("children");
+        if (children != null) {
+            for (final GeckoBundle child : children) {
+                final int childId = child.getInt("id");
+                node.addChild(mView, childId);
+                mAutoFillNodes.append(childId, child);
+            }
+        }
+
+        String tag = bundle.getString("tag", "");
+        final String type = bundle.getString("type", "text");
+        final GeckoBundle attrs = bundle.getBundle("attributes");
+
+        if ("INPUT".equals(tag) && !bundle.getBoolean("editable", false)) {
+            tag = ""; // Don't process non-editable inputs (e.g. type="button").
+        }
+        switch (tag) {
+            case "INPUT":
+            case "TEXTAREA": {
+                final boolean disabled = bundle.getBoolean("disabled");
+                node.setClassName("android.widget.EditText");
+                node.setEnabled(!disabled);
+                node.setFocusable(!disabled);
+                node.setFocused(id == mAutoFillFocusedId);
+
+                if ("password".equals(type)) {
+                    node.setPassword(true);
+                }
+                if (Build.VERSION.SDK_INT >= 18) {
+                    node.setEditable(!disabled);
+                }
+                if (Build.VERSION.SDK_INT >= 19) {
+                    node.setMultiLine("TEXTAREA".equals(tag));
+                }
+                if (Build.VERSION.SDK_INT >= 21) {
+                    try {
+                        node.setMaxTextLength(Integer.parseInt(
+                                String.valueOf(attrs.get("maxlength"))));
+                    } catch (final NumberFormatException ignore) {
+                    }
+                }
+
+                if (!disabled) {
+                    if (Build.VERSION.SDK_INT >= 21) {
+                        node.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT);
+                    } else {
+                        node.addAction(ACTION_SET_TEXT);
+                    }
+                }
+                break;
+            }
+            default:
+                if (children != null) {
+                    node.setClassName("android.view.ViewGroup");
+                } else {
+                    node.setClassName("android.view.View");
+                }
+                break;
+        }
+
+        if (Build.VERSION.SDK_INT >= 19 && "INPUT".equals(tag)) {
+            switch (type) {
+                case "email":
+                    node.setInputType(InputType.TYPE_CLASS_TEXT |
+                                      InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS);
+                    break;
+                case "number":
+                    node.setInputType(InputType.TYPE_CLASS_NUMBER);
+                    break;
+                case "password":
+                    node.setInputType(InputType.TYPE_CLASS_TEXT |
+                                      InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD);
+                    break;
+                case "tel":
+                    node.setInputType(InputType.TYPE_CLASS_PHONE);
+                    break;
+                case "text":
+                    node.setInputType(InputType.TYPE_CLASS_TEXT |
+                                      InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
+                    break;
+                case "url":
+                    node.setInputType(InputType.TYPE_CLASS_TEXT |
+                                      InputType.TYPE_TEXT_VARIATION_URI);
+                    break;
+            }
+        }
+        return node;
+    }
+
+    /* package */ boolean performAutoFill(final int id, final String value) {
+        if (mAutoFillRoots == null) {
+            return false;
+        }
+
+        int rootId = id;
+        for (int currentId = id; currentId != View.NO_ID;) {
+            final GeckoBundle bundle = mAutoFillNodes.get(currentId);
+            if (bundle == null) {
+                return false;
+            }
+            rootId = currentId;
+            currentId = bundle.getInt("parent", View.NO_ID);
+        }
+
+        if (DEBUG) {
+            Log.d(LOGTAG, "performAutoFill(" + id + ", " + value + ')');
+        }
+
+        final EventCallback callback = mAutoFillRoots.get(rootId);
+        if (callback == null) {
+            return false;
+        }
+
+        final GeckoBundle response = new GeckoBundle(1);
+        response.putString(String.valueOf(id), value);
+        callback.sendSuccess(response);
+        return true;
+    }
+
+    private void fireWindowChangedEvent(final int id) {
+        if (mView instanceof ViewParent) {
+            final AccessibilityEvent event = obtainEvent(
+                    AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, id);
+            if (Build.VERSION.SDK_INT >= 19) {
+                event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
+            }
+            ((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
+        }
+    }
+
+    /* package */ void addAutoFill(final GeckoBundle message, final EventCallback callback) {
+        if (!Settings.isEnabled()) {
+            return;
+        }
+
+        if (mAutoFillRoots == null) {
+            mAutoFillRoots = new SparseArray<>();
+            mAutoFillNodes = new SparseArray<>();
+        }
+
+        final int id = message.getInt("id");
+        if (DEBUG) {
+            Log.d(LOGTAG, "addAutoFill(" + id + ')');
+        }
+
+        mAutoFillRoots.append(id, callback);
+        mAutoFillNodes.append(id, message);
+        fireWindowChangedEvent(id);
+    }
+
+    /* package */ void clearAutoFill() {
+        if (mAutoFillRoots != null) {
+            if (DEBUG) {
+                Log.d(LOGTAG, "clearAutoFill()");
+            }
+            mAutoFillRoots = null;
+            mAutoFillNodes = null;
+            mAutoFillFocusedId = View.NO_ID;
+            fireWindowChangedEvent(View.NO_ID);
+        }
+    }
+
+    /* package */ void onAutoFillFocus(final GeckoBundle message) {
+        if (!Settings.isEnabled() || !(mView instanceof ViewParent) || mAutoFillNodes == null) {
+            return;
+        }
+
+        final int id;
+        if (message != null) {
+            id = message.getInt("id");
+            mAutoFillNodes.put(id, message);
+        } else {
+            id = View.NO_ID;
+        }
+
+        if (DEBUG) {
+            Log.d(LOGTAG, "onAutoFillFocus(" + id + ')');
+        }
+        if (mAutoFillFocusedId == id) {
+            return;
+        }
+        mAutoFillFocusedId = id;
+
+        // We already send "TYPE_VIEW_FOCUSED" in touch exploration mode,
+        // so in that case don't send it here.
+        if (!Settings.isTouchExplorationEnabled()) {
+            AccessibilityEvent event = obtainEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED, id);
+            ((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
+        }
+    }
+
+    /* package */ void onWindowFocus() {
+        // Auto-fill clients expect a state change event on focus.
+        if (Settings.isEnabled() && mView instanceof ViewParent) {
+            if (DEBUG) {
+                Log.d(LOGTAG, "onWindowFocus()");
+            }
+            final AccessibilityEvent event = obtainEvent(
+                    AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, View.NO_ID);
+            ((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
+        }
+    }
 }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionTextInput.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionTextInput.java
@@ -6,50 +6,62 @@
 package org.mozilla.geckoview;
 
 import org.mozilla.gecko.InputMethods;
 import org.mozilla.gecko.annotation.WrapForJNI;
 import org.mozilla.gecko.GeckoEditableChild;
 import org.mozilla.gecko.IGeckoEditableParent;
 import org.mozilla.gecko.NativeQueue;
 import org.mozilla.gecko.util.ActivityUtils;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.annotation.TargetApi;
 import android.app.Activity;
 import android.content.Context;
+import android.graphics.Rect;
 import android.graphics.RectF;
+import android.os.Build;
 import android.os.Handler;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.text.Editable;
+import android.text.InputType;
 import android.util.Log;
+import android.util.SparseArray;
 import android.view.KeyEvent;
 import android.view.View;
+import android.view.ViewStructure;
+import android.view.autofill.AutofillManager;
 import android.view.inputmethod.CursorAnchorInfo;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethodManager;
 
+import java.util.Locale;
+
 /**
  * {@code SessionTextInput} handles text input for {@code GeckoSession} through key events or input
  * methods. It is typically used to implement certain methods in {@link android.view.View}
  * such as {@link android.view.View#onCreateInputConnection}, by forwarding such calls to
  * corresponding methods in {@code SessionTextInput}.
  * <p>
  * For full functionality, {@code SessionTextInput} requires a {@link android.view.View} to be set
  * first through {@link #setView}. When a {@link android.view.View} is not set or set to null,
  * {@code SessionTextInput} will operate in a reduced functionality mode. See {@link
  * #onCreateInputConnection} and methods in {@link GeckoSession.TextInputDelegate} for changes in
  * behavior in this viewless mode.
  */
 public final class SessionTextInput {
     /* package */ static final String LOGTAG = "GeckoSessionTextInput";
+    private static final boolean DEBUG = false;
 
     // Interface to access GeckoInputConnection from SessionTextInput.
     /* package */ interface InputConnectionClient {
         View getView();
         Handler getHandler(Handler defHandler);
         InputConnection onCreateInputConnection(EditorInfo attrs);
     }
 
@@ -213,32 +225,93 @@ public final class SessionTextInput {
                                            @NonNull final CursorAnchorInfo info) {
             ThreadUtils.assertOnUiThread();
             final View view = session.getTextInput().getView();
             final InputMethodManager imm = getInputMethodManager(view);
             if (imm != null) {
                 imm.updateCursorAnchorInfo(view, info);
             }
         }
+
+        @Override
+        public void notifyAutoFill(@NonNull final GeckoSession session,
+                                   @AutoFillNotification final int notification,
+                                   final int virtualId) {
+            ThreadUtils.assertOnUiThread();
+            final View view = session.getTextInput().getView();
+            if (Build.VERSION.SDK_INT < 26 || view == null) {
+                return;
+            }
+
+            final AutofillManager manager =
+                    view.getContext().getSystemService(AutofillManager.class);
+            if (manager == null) {
+                return;
+            }
+
+            switch (notification) {
+                case AUTO_FILL_NOTIFY_STARTED:
+                    // This line seems necessary for auto-fill to work on the initial page.
+                    manager.cancel();
+                    break;
+                case AUTO_FILL_NOTIFY_COMMITTED:
+                    manager.commit();
+                    break;
+                case AUTO_FILL_NOTIFY_CANCELED:
+                    manager.cancel();
+                    break;
+                case AUTO_FILL_NOTIFY_VIEW_ENTERED:
+                    // Use a dummy rect for the View.
+                    manager.notifyViewEntered(view, virtualId, getDummyAutoFillRect(
+                            session, /* screen */ true, view));
+                    break;
+                case AUTO_FILL_NOTIFY_VIEW_EXITED:
+                    manager.notifyViewExited(view, virtualId);
+                    break;
+            }
+        }
     }
 
     private final GeckoSession mSession;
     private final NativeQueue mQueue;
     private final GeckoEditable mEditable;
     private final GeckoEditableChild mEditableChild;
     private InputConnectionClient mInputConnection;
     private GeckoSession.TextInputDelegate mDelegate;
+    // Auto-fill nodes.
+    private SparseArray<GeckoBundle> mAutoFillNodes;
+    private SparseArray<EventCallback> mAutoFillRoots;
+    private int mAutoFillFocusedId = View.NO_ID;
 
     /* package */ SessionTextInput(final @NonNull GeckoSession session,
                                    final @NonNull NativeQueue queue) {
         mSession = session;
         mQueue = queue;
         mEditable = new GeckoEditable(session);
         mEditableChild = new GeckoEditableChild(mEditable);
         mEditable.setDefaultEditableChild(mEditableChild);
+
+        session.getEventDispatcher().registerUiThreadListener(
+                new BundleEventListener() {
+                    @Override
+                    public void handleMessage(final String event, final GeckoBundle message,
+                                              final EventCallback callback) {
+                        if ("GeckoView:AddAutoFill".equals(event)) {
+                            addAutoFill(message, callback);
+                        } else if ("GeckoView:ClearAutoFill".equals(event)) {
+                            clearAutoFill();
+                        } else if ("GeckoView:OnAutoFillFocus".equals(event)) {
+                            onAutoFillFocus(message);
+                        }
+                    }
+                },
+                "GeckoView:AddAutoFill",
+                "GeckoView:ClearAutoFill",
+                "GeckoView:OnAutoFillFocus",
+                null);
     }
 
     /* package */ void onWindowChanged(final GeckoSession.Window window) {
         if (mQueue.isReady()) {
             window.attachEditable(mEditable, mEditableChild);
         } else {
             mQueue.queueUntilReady(window, "attachEditable",
                                    IGeckoEditableParent.class, mEditable,
@@ -402,9 +475,302 @@ public final class SessionTextInput {
      */
     public GeckoSession.TextInputDelegate getDelegate() {
         ThreadUtils.assertOnUiThread();
         if (mDelegate == null) {
             mDelegate = DefaultDelegate.INSTANCE;
         }
         return mDelegate;
     }
+
+    /**
+     * Fill the specified {@link ViewStructure} with auto-fill fields from the current page.
+     *
+     * @param structure Structure to be filled.
+     * @param flags Zero or a combination of {@link View#AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
+     *              AUTOFILL_FLAG_*} constants.
+     */
+    @TargetApi(23)
+    public void onProvideAutofillVirtualStructure(@NonNull final ViewStructure structure,
+                                                  final int flags) {
+        final View view = getView();
+        if (view != null) {
+            structure.setClassName(view.getClass().getName());
+        }
+        structure.setEnabled(true);
+        structure.setVisibility(View.VISIBLE);
+
+        final Rect rect = getDummyAutoFillRect(mSession, /* screen */ false,
+                                               /* view */ null);
+        structure.setDimens(rect.left, rect.top, 0, 0, rect.width(), rect.height());
+
+        if (mAutoFillRoots == null) {
+            structure.setChildCount(0);
+            return;
+        }
+
+        final int size = mAutoFillRoots.size();
+        structure.setChildCount(size);
+
+        int focusedRoot = View.NO_ID;
+        for (int newId = mAutoFillFocusedId; newId != View.NO_ID;) {
+            focusedRoot = newId;
+            newId = mAutoFillNodes.get(newId).getInt("parent", View.NO_ID);
+        }
+
+        for (int i = 0; i < size; i++) {
+            final int id = mAutoFillRoots.keyAt(i);
+            final GeckoBundle root = mAutoFillNodes.get(id);
+            fillAutoFillStructure(view, id, root, structure.newChild(i),
+                                  (focusedRoot == id) ? rect : null);
+        }
+    }
+
+    /**
+     * Perform auto-fill using the specified values.
+     *
+     * @param values Map of auto-fill IDs to values.
+     */
+    public void autofill(final SparseArray<CharSequence> values) {
+        if (mAutoFillRoots == null) {
+            return;
+        }
+
+        GeckoBundle response = null;
+        EventCallback callback = null;
+
+        for (int i = 0; i < values.size(); i++) {
+            final int id = values.keyAt(i);
+            final CharSequence value = values.valueAt(i);
+
+            if (DEBUG) {
+                Log.d(LOGTAG,
+                      "performAutoFill(" + id + ", " + values + ')');
+            }
+            int rootId = id;
+            for (int currentId = id; currentId != View.NO_ID; ) {
+                final GeckoBundle bundle = mAutoFillNodes.get(currentId);
+                if (bundle == null) {
+                    return;
+                }
+                rootId = currentId;
+                currentId = bundle.getInt("parent", View.NO_ID);
+            }
+
+            final EventCallback newCallback = mAutoFillRoots.get(rootId);
+            if (callback == null || newCallback != callback) {
+                if (callback != null) {
+                    callback.sendSuccess(response);
+                }
+                response = new GeckoBundle(values.size() - i);
+                callback = newCallback;
+            }
+            response.putString(String.valueOf(id), String.valueOf(value));
+        }
+
+        if (callback != null) {
+            callback.sendSuccess(response);
+        }
+    }
+
+    @TargetApi(23)
+    private void fillAutoFillStructure(@Nullable final View view, final int id,
+                                       @NonNull final GeckoBundle bundle,
+                                       @NonNull final ViewStructure structure,
+                                       @Nullable final Rect rect) {
+        if (mAutoFillRoots == null) {
+            return;
+        }
+
+        if (DEBUG) {
+            Log.d(LOGTAG, "fillAutoFillStructure(" + id + ')');
+        }
+
+        if (Build.VERSION.SDK_INT >= 26) {
+            if (view != null) {
+                structure.setAutofillId(view.getAutofillId(), id);
+            }
+            structure.setWebDomain(bundle.getString("origin"));
+        }
+        structure.setId(id, null, null, null);
+
+        if (rect != null) {
+            structure.setDimens(0, 0, 0, 0, rect.width(), rect.height());
+        }
+
+        final GeckoBundle[] children = bundle.getBundleArray("children");
+        if (children != null) {
+            structure.setChildCount(children.length);
+            for (int i = 0; i < children.length; i++) {
+                final GeckoBundle childBundle = children[i];
+                final int childId = childBundle.getInt("id");
+                final ViewStructure childStructure = structure.newChild(i);
+                fillAutoFillStructure(view, childId, childBundle, childStructure, rect);
+                mAutoFillNodes.append(childId, childBundle);
+            }
+        }
+
+        String tag = bundle.getString("tag", "");
+        final String type = bundle.getString("type", "text");
+
+        if (Build.VERSION.SDK_INT >= 26) {
+            final GeckoBundle attrs = bundle.getBundle("attributes");
+            final ViewStructure.HtmlInfo.Builder builder =
+                    structure.newHtmlInfoBuilder(tag.toLowerCase(Locale.US));
+            for (final String key : attrs.keys()) {
+                builder.addAttribute(key, String.valueOf(attrs.get(key)));
+            }
+            structure.setHtmlInfo(builder.build());
+        }
+
+        if ("INPUT".equals(tag) && !bundle.getBoolean("editable", false)) {
+            tag = ""; // Don't process non-editable inputs (e.g. type="button").
+        }
+        switch (tag) {
+            case "INPUT":
+            case "TEXTAREA": {
+                final boolean disabled = bundle.getBoolean("disabled");
+                structure.setClassName("android.widget.EditText");
+                structure.setEnabled(!disabled);
+                structure.setFocusable(!disabled);
+                structure.setFocused(id == mAutoFillFocusedId);
+                structure.setVisibility(View.VISIBLE);
+
+                if (Build.VERSION.SDK_INT >= 26) {
+                    structure.setAutofillType(View.AUTOFILL_TYPE_TEXT);
+                }
+                break;
+            }
+            default:
+                if (children != null) {
+                    structure.setClassName("android.view.ViewGroup");
+                } else {
+                    structure.setClassName("android.view.View");
+                }
+                break;
+        }
+
+        if (Build.VERSION.SDK_INT >= 26 && "INPUT".equals(tag)) {
+            switch (type) {
+                case "email":
+                    structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_EMAIL_ADDRESS });
+                    structure.setInputType(InputType.TYPE_CLASS_TEXT |
+                                              InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS);
+                    break;
+                case "number":
+                    structure.setInputType(InputType.TYPE_CLASS_NUMBER);
+                    break;
+                case "password":
+                    structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_PASSWORD });
+                    structure.setInputType(InputType.TYPE_CLASS_TEXT |
+                                           InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD);
+                    break;
+                case "tel":
+                    structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_PHONE });
+                    structure.setInputType(InputType.TYPE_CLASS_PHONE);
+                    break;
+                case "text":
+                    structure.setInputType(InputType.TYPE_CLASS_TEXT |
+                                           InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
+                    break;
+                case "url":
+                    structure.setInputType(InputType.TYPE_CLASS_TEXT |
+                                           InputType.TYPE_TEXT_VARIATION_URI);
+                    break;
+            }
+        }
+    }
+
+    /* package */ void addAutoFill(@NonNull final GeckoBundle message,
+                                   @NonNull final EventCallback callback) {
+        if (Build.VERSION.SDK_INT < 23) {
+            return;
+        }
+
+        final boolean initializing;
+        if (mAutoFillRoots == null) {
+            mAutoFillRoots = new SparseArray<>();
+            mAutoFillNodes = new SparseArray<>();
+            initializing = true;
+        } else {
+            initializing = false;
+        }
+
+        final int id = message.getInt("id");
+        if (DEBUG) {
+            Log.d(LOGTAG, "addAutoFill(" + id + ')');
+        }
+
+        mAutoFillRoots.append(id, callback);
+        mAutoFillNodes.append(id, message);
+
+        if (initializing) {
+            getDelegate().notifyAutoFill(
+                    mSession, GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_STARTED, id);
+        } else {
+            getDelegate().notifyAutoFill(
+                    mSession, GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_ADDED, id);
+        }
+    }
+
+    /* package */ void clearAutoFill() {
+        if (mAutoFillRoots == null) {
+            return;
+        }
+        if (DEBUG) {
+            Log.d(LOGTAG, "clearAutoFill()");
+        }
+        mAutoFillRoots = null;
+        mAutoFillNodes = null;
+        mAutoFillFocusedId = View.NO_ID;
+
+        getDelegate().notifyAutoFill(
+                mSession, GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_CANCELED, View.NO_ID);
+    }
+
+    /* package */ void onAutoFillFocus(@Nullable final GeckoBundle message) {
+        if (mAutoFillRoots == null) {
+            return;
+        }
+
+        final int id;
+        if (message != null) {
+            id = message.getInt("id");
+            mAutoFillNodes.put(id, message);
+        } else {
+            id = View.NO_ID;
+        }
+
+        if (DEBUG) {
+            Log.d(LOGTAG, "onAutoFillFocus(" + id + ')');
+        }
+        if (mAutoFillFocusedId == id) {
+            return;
+        }
+        if (mAutoFillFocusedId != View.NO_ID) {
+            getDelegate().notifyAutoFill(
+                    mSession, GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_EXITED,
+                    mAutoFillFocusedId);
+        }
+
+        mAutoFillFocusedId = id;
+        if (id != View.NO_ID) {
+            getDelegate().notifyAutoFill(
+                    mSession, GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED, id);
+        }
+    }
+
+    /* package */ static Rect getDummyAutoFillRect(@NonNull final GeckoSession session,
+                                                   final boolean screen,
+                                                   @Nullable final View view) {
+        final Rect rect = new Rect();
+        session.getSurfaceBounds(rect);
+        if (screen) {
+            if (view == null) {
+                throw new IllegalArgumentException();
+            }
+            final int[] offset = new int[2];
+            view.getLocationOnScreen(offset);
+            rect.offset(offset[0], offset[1]);
+        }
+        return rect;
+    }
 }
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -182,28 +182,16 @@ public class GeckoViewActivity extends A
     }
 
     private void updateTrackingProtection(GeckoSession session) {
         session.getSettings().setBoolean(
             GeckoSessionSettings.USE_TRACKING_PROTECTION, mUseTrackingProtection);
     }
 
     @Override
-    protected void onPause() {
-        mGeckoSession.setActive(false);
-        super.onPause();
-    }
-
-    @Override
-    protected void onResume() {
-        mGeckoSession.setActive(true);
-        super.onResume();
-    }
-
-    @Override
     public void onBackPressed() {
         if (mFullScreen) {
             mGeckoSession.exitFullScreen();
             return;
         }
 
         if (mCanGoBack && mGeckoSession != null) {
             mGeckoSession.goBack();
--- a/mobile/android/modules/geckoview/GeckoViewContent.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewContent.jsm
@@ -18,16 +18,17 @@ class GeckoViewContent extends GeckoView
     this.registerListener([
         "GeckoViewContent:ExitFullScreen",
         "GeckoView:ClearMatches",
         "GeckoView:DisplayMatches",
         "GeckoView:FindInPage",
         "GeckoView:RestoreState",
         "GeckoView:SaveState",
         "GeckoView:SetActive",
+        "GeckoView:SetFocused",
         "GeckoView:ZoomToInput",
     ]);
 
     this.messageManager.addMessageListener("GeckoView:SaveStateFinish", this);
   }
 
   onEnable() {
     this.window.addEventListener("MozDOMFullscreen:Entered", this,
@@ -73,22 +74,32 @@ class GeckoViewContent extends GeckoView
         this._findInPage(aData, aCallback);
         break;
       }
       case "GeckoView:ZoomToInput":
         this.messageManager.sendAsyncMessage(aEvent);
         break;
       case "GeckoView:SetActive":
         if (aData.active) {
-          this.browser.setAttribute("primary", "true");
-          this.browser.focus();
           this.browser.docShellIsActive = true;
         } else {
+          this.browser.docShellIsActive = false;
+        }
+        var msgData = {
+          active: aData.active,
+          suspendMedia: this.settings.suspendMediaWhenInactive
+        };
+        this.messageManager.sendAsyncMessage("GeckoView:SetActive", msgData);
+        break;
+      case "GeckoView:SetFocused":
+        if (aData.focused) {
+          this.browser.focus();
+          this.browser.setAttribute("primary", "true");
+        } else {
           this.browser.removeAttribute("primary");
-          this.browser.docShellIsActive = false;
           this.browser.blur();
         }
         break;
       case "GeckoView:SaveState":
         if (!this._saveStateCallbacks) {
           this._saveStateCallbacks = new Map();
           this._saveStateNextId = 0;
         }
--- a/mobile/android/modules/geckoview/GeckoViewNavigation.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewNavigation.jsm
@@ -145,23 +145,34 @@ class GeckoViewNavigation extends GeckoV
     });
 
     // Wait indefinitely for app to respond with a browser or null
     Services.tm.spinEventLoopUntil(() =>
         this.window.closed || browser !== undefined);
     return browser || null;
   }
 
+  isURIHandled(aUri, aWhere, aFlags) {
+    debug `isURIHandled: uri=${aUri} where=${aWhere} flags=${aFlags}`;
+
+    let handled = undefined;
+    LoadURIDelegate.load(this.window, this.eventDispatcher, aUri, aWhere, aFlags).then((response) => {
+      handled = response;
+    });
+
+    Services.tm.spinEventLoopUntil(() => this.window.closed || handled !== undefined);
+    return handled;
+  }
+
   // nsIBrowserDOMWindow.
   createContentWindow(aUri, aOpener, aWhere, aFlags, aTriggeringPrincipal) {
     debug `createContentWindow: uri=${aUri && aUri.spec}
                                 where=${aWhere} flags=${aFlags}`;
 
-    if (LoadURIDelegate.load(this.window, this.eventDispatcher,
-                             aUri, aWhere, aFlags)) {
+    if (this.isURIHandled(aUri, aWhere, aFlags)) {
       // The app has handled the load, abort open-window handling.
       Components.returnCode = Cr.NS_ERROR_ABORT;
       return null;
     }
 
     const browser = this.handleNewSession(aUri, aOpener, aWhere, aFlags, null);
     if (!browser) {
       Components.returnCode = Cr.NS_ERROR_ABORT;
@@ -174,18 +185,17 @@ class GeckoViewNavigation extends GeckoV
   // nsIBrowserDOMWindow.
   createContentWindowInFrame(aUri, aParams, aWhere, aFlags, aNextTabParentId,
                              aName) {
     debug `createContentWindowInFrame: uri=${aUri && aUri.spec}
                                        where=${aWhere} flags=${aFlags}
                                        nextTabParentId=${aNextTabParentId}
                                        name=${aName}`;
 
-    if (LoadURIDelegate.load(this.window, this.eventDispatcher,
-                             aUri, aWhere, aFlags)) {
+    if (this.isURIHandled(aUri, aWhere, aFlags)) {
       // The app has handled the load, abort open-window handling.
       Components.returnCode = Cr.NS_ERROR_ABORT;
       return null;
     }
 
     const browser = this.handleNewSession(aUri, null, aWhere, aFlags, aNextTabParentId);
     if (!browser) {
       Components.returnCode = Cr.NS_ERROR_ABORT;
@@ -195,18 +205,17 @@ class GeckoViewNavigation extends GeckoV
     return browser;
   }
 
   handleOpenUri(aUri, aOpener, aWhere, aFlags, aTriggeringPrincipal,
                 aNextTabParentId) {
     debug `handleOpenUri: uri=${aUri && aUri.spec}
                           where=${aWhere} flags=${aFlags}`;
 
-    if (LoadURIDelegate.load(this.window, this.eventDispatcher,
-                             aUri, aWhere, aFlags)) {
+    if (this.isURIHandled(aUri, aWhere, aFlags)) {
       return null;
     }
 
     let browser = this.browser;
 
     if (aWhere === Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW ||
         aWhere === Ci.nsIBrowserDOMWindow.OPEN_NEWTAB ||
         aWhere === Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB) {
--- a/mobile/android/modules/geckoview/GeckoViewSettings.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewSettings.jsm
@@ -2,29 +2,23 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["GeckoViewSettings"];
 
 ChromeUtils.import("resource://gre/modules/GeckoViewModule.jsm");
-ChromeUtils.import("resource://gre/modules/GeckoViewUtils.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
+  SafeBrowsing: "resource://gre/modules/SafeBrowsing.jsm",
   Services: "resource://gre/modules/Services.jsm",
 });
 
-/* global SafeBrowsing:false */
-GeckoViewUtils.addLazyGetter(this, "SafeBrowsing", {
-  module: "resource://gre/modules/SafeBrowsing.jsm",
-  init: sb => sb.init(),
-});
-
 XPCOMUtils.defineLazyGetter(
   this, "DESKTOP_USER_AGENT",
   function() {
     return Cc["@mozilla.org/network/protocol;1?name=http"]
            .getService(Ci.nsIHttpProtocolHandler).userAgent
            .replace(/Android \d.+?; [a-zA-Z]+/, "X11; Linux x86_64")
            .replace(/Gecko\/[0-9\.]+/, "Gecko/20100101");
   });
@@ -37,40 +31,32 @@ class GeckoViewSettings extends GeckoVie
     if (this.settings.useMultiprocess) {
       this.browser.setAttribute("remote", "true");
     }
   }
 
   onInit() {
     this._useTrackingProtection = false;
     this._useDesktopMode = false;
+    // Required for safe browsing and tracking protection.
+    SafeBrowsing.init();
   }
 
   onSettingsUpdate() {
     const settings = this.settings;
     debug `onSettingsUpdate: ${settings}`;
 
     this.displayMode = settings.displayMode;
-    this.useTrackingProtection = !!settings.useTrackingProtection;
     this.useDesktopMode = !!settings.useDesktopMode;
   }
 
   get useMultiprocess() {
     return this.browser.isRemoteBrowser;
   }
 
-  get useTrackingProtection() {
-    return this._useTrackingProtection;
-  }
-
-  set useTrackingProtection(aUse) {
-    aUse && SafeBrowsing;
-    this._useTrackingProtection = aUse;
-  }
-
   onUserAgentRequest(aSubject, aTopic, aData) {
     debug `onUserAgentRequest`;
 
     let channel = aSubject.QueryInterface(Ci.nsIHttpChannel);
 
     if (this.browser.outerWindowID !== channel.topLevelOuterContentWindowId) {
       return;
     }
--- a/mobile/android/modules/geckoview/LoadURIDelegate.jsm
+++ b/mobile/android/modules/geckoview/LoadURIDelegate.jsm
@@ -3,41 +3,26 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 var EXPORTED_SYMBOLS = ["LoadURIDelegate"];
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetters(this, {
-  Services: "resource://gre/modules/Services.jsm",
-});
-
 var LoadURIDelegate = {
   // Delegate URI loading to the app.
   // Return whether the loading has been handled.
   load: function(aWindow, aEventDispatcher, aUri, aWhere, aFlags) {
     if (!aWindow) {
-      return false;
+      return Promise.resolve(false);
     }
 
     const message = {
       type: "GeckoView:OnLoadRequest",
       uri: aUri ? aUri.displaySpec : "",
       where: aWhere,
       flags: aFlags
     };
 
-    let handled = undefined;
-    aEventDispatcher.sendRequestForResult(message).then(response => {
-      handled = response;
-    }, () => {
-      // There was an error or listener was not registered in GeckoSession,
-      // treat as unhandled.
-      handled = false;
-    });
-    Services.tm.spinEventLoopUntil(() =>
-        aWindow.closed || handled !== undefined);
-
-    return handled || false;
+    return aEventDispatcher.sendRequestForResult(message).then((response) => response || false).catch(() => false);
   }
 };
--- a/netwerk/base/nsIChannelEventSink.idl
+++ b/netwerk/base/nsIChannelEventSink.idl
@@ -48,16 +48,23 @@ interface nsIChannelEventSink : nsISuppo
     /**
      * This is a special-cased redirect coming from hitting HSTS upgrade
      * redirect from http to https only.  In some cases this type of redirect
      * may be considered as safe despite not being the-same-origin redirect.
      */
     const unsigned long REDIRECT_STS_UPGRADE = 1 << 3;
 
     /**
+     * This redirect has already been presented to the nsILoadURIDelegate
+     * for possible handling; if this flag is set we may safely skip checking
+     * if the nsILoadURIDelegate will handle the redirect.
+     */
+    const unsigned long REDIRECT_DELEGATES_CHECKED = 1 << 4;
+
+    /**
      * Called when a redirect occurs. This may happen due to an HTTP 3xx status
      * code. The purpose of this method is to notify the sink that a redirect
      * is about to happen, but also to give the sink the right to veto the
      * redirect by throwing or passing a failure-code in the callback.
      *
      * Note that vetoing the redirect simply means that |newChannel| will not
      * be opened. It is important to understand that |oldChannel| will continue
      * loading as if it received a HTTP 200, which includes notifying observers
--- a/uriloader/base/nsDocLoader.cpp
+++ b/uriloader/base/nsDocLoader.cpp
@@ -1,16 +1,18 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nspr.h"
 #include "mozilla/Logging.h"
 #include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
 
 #include "nsDocLoader.h"
 #include "nsCURILoader.h"
 #include "nsNetUtil.h"
 #include "nsIHttpChannel.h"
 #include "nsIWebProgressListener2.h"
 
 #include "nsIServiceManager.h"
@@ -29,16 +31,18 @@
 #include "nsIScriptSecurityManager.h"
 
 #include "nsITransport.h"
 #include "nsISocketTransport.h"
 #include "nsIDocShell.h"
 #include "nsIDocument.h"
 #include "nsPresContext.h"
 #include "nsIAsyncVerifyRedirectCallback.h"
+#include "nsILoadURIDelegate.h"
+#include "nsIBrowserDOMWindow.h"
 
 using mozilla::DebugOnly;
 using mozilla::LogLevel;
 
 //
 // Log module for nsIDocumentLoader logging...
 //
 // To enable logging (see mozilla/Logging.h for full details):
@@ -1415,21 +1419,116 @@ int64_t nsDocLoader::CalculateMaxProgres
     if (info->mMaxProgress < info->mCurrentProgress) {
       return int64_t(-1);
     }
     max += info->mMaxProgress;
   }
   return max;
 }
 
+class LoadURIDelegateRedirectHandler final : public mozilla::dom::PromiseNativeHandler
+{
+public:
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_CLASS(LoadURIDelegateRedirectHandler)
+
+  LoadURIDelegateRedirectHandler(nsDocLoader* aDocLoader,
+                                 nsIChannel* aOldChannel,
+                                 nsIChannel* aNewChannel,
+                                 uint32_t aFlags,
+                                 nsIAsyncVerifyRedirectCallback* aCallback)
+  : mDocLoader(aDocLoader)
+  , mOldChannel(aOldChannel)
+  , mNewChannel(aNewChannel)
+  , mFlags(aFlags)
+  , mCallback(aCallback)
+  {}
+
+  void
+  ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override
+  {
+    if (aValue.isBoolean() && aValue.toBoolean()) {
+      // The app handled the redirect, notify the callback
+      mCallback->OnRedirectVerifyCallback(NS_ERROR_ABORT);
+    } else {
+      UnhandledCallback();
+    }
+  }
+
+  void
+  RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override
+  {
+    UnhandledCallback();
+  }
+
+private:
+  ~LoadURIDelegateRedirectHandler()
+  {}
+
+  void UnhandledCallback()
+  {
+    // If the redirect wasn't handled by the nsILoadURIDelegate, let Gecko
+    // handle it.
+    mFlags |= nsIChannelEventSink::REDIRECT_DELEGATES_CHECKED;
+    mDocLoader->AsyncOnChannelRedirect(mOldChannel, mNewChannel, mFlags,
+                                       mCallback);
+  }
+
+  RefPtr<nsDocLoader> mDocLoader;
+  nsCOMPtr<nsIChannel> mOldChannel;
+  nsCOMPtr<nsIChannel> mNewChannel;
+  uint32_t mFlags;
+  nsCOMPtr<nsIAsyncVerifyRedirectCallback> mCallback;
+};
+
+NS_IMPL_CYCLE_COLLECTION(LoadURIDelegateRedirectHandler, mDocLoader, 
+                         mOldChannel, mNewChannel, mCallback)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LoadURIDelegateRedirectHandler)
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(LoadURIDelegateRedirectHandler)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(LoadURIDelegateRedirectHandler)
+
 NS_IMETHODIMP nsDocLoader::AsyncOnChannelRedirect(nsIChannel *aOldChannel,
                                                   nsIChannel *aNewChannel,
                                                   uint32_t aFlags,
                                                   nsIAsyncVerifyRedirectCallback *cb)
 {
+  if ((aFlags &
+      (nsIChannelEventSink::REDIRECT_TEMPORARY |
+       nsIChannelEventSink::REDIRECT_PERMANENT)) &&
+      !(aFlags & nsIChannelEventSink::REDIRECT_DELEGATES_CHECKED)) {
+    nsCOMPtr<nsIDocShell> docShell =
+      do_QueryInterface(static_cast<nsIRequestObserver*>(this));
+
+    nsCOMPtr<nsILoadURIDelegate> delegate;
+    docShell->GetLoadURIDelegate(getter_AddRefs(delegate));
+
+    nsCOMPtr<nsIURI> newURI;
+    aNewChannel->GetURI(getter_AddRefs(newURI));
+
+    if (newURI && delegate) {
+      RefPtr<mozilla::dom::Promise> promise;
+      const int where = nsIBrowserDOMWindow::OPEN_CURRENTWINDOW;
+      nsresult rv = delegate->LoadURI(newURI, where, /* flags */ 0,
+                                      /* triggering principal */ nullptr,
+                                      getter_AddRefs(promise));
+      if (NS_SUCCEEDED(rv) && promise) {
+        RefPtr<LoadURIDelegateRedirectHandler> handler =
+          new LoadURIDelegateRedirectHandler(this, aOldChannel, aNewChannel,
+                                             aFlags, cb);
+
+        promise->AppendNativeHandler(handler);
+        return NS_OK;
+      }
+    }
+  }
+
   if (aOldChannel)
   {
     nsLoadFlags loadFlags = 0;
     int32_t stateFlags = nsIWebProgressListener::STATE_REDIRECTING |
                          nsIWebProgressListener::STATE_IS_REQUEST;
 
     aOldChannel->GetLoadFlags(&loadFlags);
     // If the document channel is being redirected, then indicate that the
--- a/xpcom/base/nsILoadURIDelegate.idl
+++ b/xpcom/base/nsILoadURIDelegate.idl
@@ -21,13 +21,16 @@ interface nsILoadURIDelegate : nsISuppor
 {
   /**
    * Delegates the URI load.
    *
    * @param aURI The URI to load.
    * @param aWhere See possible values described in nsIBrowserDOMWindow.
    * @param aFlags Flags which control the behavior of the load.
    * @param aTriggeringPrincipal The principal that triggered the load of aURI.
+   * @return A promise which can resolve to a boolean indicating whether or
+   *         not the app handled the load. Rejection should be treated the same
+   *         as a false resolution.
   */
-  boolean
+  Promise
   loadURI(in nsIURI aURI, in short aWhere, in long aFlags,
           in nsIPrincipal aTriggeringPrincipal);
 };