Bug 1494748 - Ensure GeckoView saved state lives at least as long as the app process. r=snorp
authorJan Henning <jh+bugzilla@buttercookie.de>
Mon, 14 Jan 2019 19:21:49 +0000
changeset 513768 025a428fbec4bf0a2e770f908a5c4ad5e4718d32
parent 513767 f69c74d69b46bb866b2431dca9267557767e5749
child 513769 71273ff7fc0c74c23abafbb343ae21f823c7d832
push id1953
push userffxbld-merge
push dateMon, 11 Mar 2019 12:10:20 +0000
treeherdermozilla-release@9c35dcbaa899 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp
bugs1494748
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1494748 - Ensure GeckoView saved state lives at least as long as the app process. r=snorp The arguably most interesting bit of state of BrowserApp/GeckoApp, namely the currently open tabs, are living partly in Gecko and partly in the Tabs manager singleton, the lifetimes of both of which are tied to the lifetime of the app process. If the whole process has been killed, things are simple: Neither the Tabs manager nor Gecko know anything about any tabs and we simply restore them through the session store if enabled. If GeckoApp is however being restored into an app process in which it had already executed earlier on, meaning that we have some open tabs, it relies on the savedInstanceState in order to correctly reconnect its GeckoView instance with the correct previous GeckoSession. We can however end up in a state where we don't have a savedInstanceState (e.g. because the user swiped away the BrowserApp activity in the task switcher), but the app process keeps running throughout (if another activity of ours is still present in the task switcher, e.g. a custom tab, or else if a service is active, then standard Android keeps the process running even if the user swipes away an activity). In that case, if GeckoApp is subsequently recreated, the Android UI sees all the Android-side tabs in the Tabs manager, and Gecko in fact still has the Window open that is containing all those tabs, but without the savedInstanceState GeckoApp doesn't know anything about that Window and proceeds to open a fresh session instead. This means that all previous tabs will appear white and unresponsive, while freshly opened tabs will load, but they won't be correctly saved in the session store, their context menu isn't working, etc., because we're not really expecting to handle multiple Gecko-side Windows. To fix this, we disable automatic state-saving for GeckoApp's GeckoView instance and instead do it manually, so we can keep another reference to the saved state in GeckoApplication, and therefore are able to retrieve it from there for as long as the app process keeps running. Differential Revision: https://phabricator.services.mozilla.com/D16393
mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -58,24 +58,26 @@ import android.content.SharedPreferences
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Configuration;
 import android.graphics.BitmapFactory;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.Parcelable;
 import android.os.StrictMode;
 import android.provider.ContactsContract;
 import android.support.annotation.NonNull;
 import android.support.annotation.WorkerThread;
 import android.support.design.widget.Snackbar;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
 import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.View;
@@ -131,16 +133,17 @@ public abstract class GeckoApp extends G
     public static final String ACTION_LAUNCH_SETTINGS      = "org.mozilla.gecko.SETTINGS";
     public static final String ACTION_LOAD                 = "org.mozilla.gecko.LOAD";
     public static final String ACTION_INIT_PW              = "org.mozilla.gecko.INIT_PW";
     public static final String ACTION_SWITCH_TAB           = "org.mozilla.gecko.SWITCH_TAB";
     public static final String ACTION_SHUTDOWN             = "org.mozilla.gecko.SHUTDOWN";
 
     public static final String INTENT_REGISTER_STUMBLER_LISTENER = "org.mozilla.gecko.STUMBLER_REGISTER_LOCAL_LISTENER";
 
+    private static final String GECKOVIEW_STATE_BUNDLE     = "geckoViewState";
     public static final String EXTRA_STATE_BUNDLE          = "stateBundle";
 
     public static final String PREFS_ALLOW_STATE_BUNDLE    = "allowStateBundle";
     public static final String PREFS_FLASH_USAGE           = "playFlashCount";
     public static final String PREFS_VERSION_CODE          = "versionCode";
     public static final String PREFS_WAS_STOPPED           = "wasStopped";
     public static final String PREFS_CRASHED_COUNT         = "crashedCount";
     public static final String PREFS_CLEANUP_TEMP_FILES    = "cleanupTempFiles";
@@ -653,16 +656,26 @@ public abstract class GeckoApp extends G
 
             // Make sure we are not bloating the Bundle which can result in TransactionTooLargeException
             if (getBundleSizeInBytes(outState) > MAX_BUNDLE_SIZE_BYTES) {
                 outState.remove(SAVED_STATE_PRIVATE_SESSION);
             }
         }
 
         outState.putBoolean(SAVED_STATE_IN_BACKGROUND, isApplicationInBackground());
+
+        // There are situations where the saved instance state will be cleared (e.g. user swipes
+        // away activity in the task switcher), but Gecko will actually remain alive (because
+        // another activity or service of ours is still running in this process). The saved state is
+        // the only way we can reconnect to our previous GeckoView session and all the user's open
+        // tabs, so we need to keep a copy of the state ourselves.
+        SparseArray<Parcelable> geckoViewState = new SparseArray<>();
+        mLayerView.saveHierarchyState(geckoViewState);
+        outState.putSparseParcelableArray(GECKOVIEW_STATE_BUNDLE, geckoViewState);
+        getGeckoApplication().setSavedState(geckoViewState);
     }
 
     public void addTab(int flags) { }
 
     public void addTab() { }
 
     public void addPrivateTab() { }
 
@@ -691,17 +704,17 @@ public abstract class GeckoApp extends G
             // know that mHealthRecorder will exist. That doesn't stop us being
             // paranoid.
             // This method is cheap, so don't spawn a new runnable.
             final HealthRecorder rec = mHealthRecorder;
             if (rec != null) {
               rec.recordGeckoStartupTime(mGeckoReadyStartupTimer.getElapsed());
             }
 
-            ((GeckoApplication) getApplicationContext()).onDelayedStartup();
+            getGeckoApplication().onDelayedStartup();
 
             // Reset the crash loop counter if we remain alive for at least half a minute.
             ThreadUtils.postDelayedToBackgroundThread(new Runnable() {
                 @Override
                 public void run() {
                     getSharedPreferences().edit().putInt(PREFS_CRASHED_COUNT, 0).apply();
                 }
             }, STARTUP_PHASE_DURATION_MS);
@@ -971,16 +984,21 @@ public abstract class GeckoApp extends G
     /**
      * Called when the activity is first created.
      *
      * Here we initialize all of our profile settings, Firefox Health Report,
      * and other one-shot constructions.
      **/
     @Override
     public void onCreate(Bundle savedInstanceState) {
+        // Within onCreate(), we might inject a different savedInstanceState for testing, but this
+        // won't influence what the OS will do with regards to calling onSaveInstanceState().
+        // Therefore, record whether we were passed some data or not.
+        final boolean receivedSavedInstanceState = (savedInstanceState != null);
+
         // Enable Android Strict Mode for developers' local builds (the "default" channel).
         if ("default".equals(AppConstants.MOZ_UPDATE_CHANNEL)) {
             enableStrictMode();
         }
 
         final boolean corruptAPK = GeckoThread.isState(GeckoThread.State.CORRUPT_APK);
         boolean supported = HardwareUtils.isSupportedSystem();
         if (supported) {
@@ -1101,29 +1119,35 @@ public abstract class GeckoApp extends G
 
         setContentView(getLayout());
 
         // Set up Gecko layout.
         mRootLayout = (RelativeLayout) findViewById(R.id.root_layout);
         mGeckoLayout = (RelativeLayout) findViewById(R.id.gecko_layout);
         mMainLayout = (RelativeLayout) findViewById(R.id.main_layout);
         mLayerView = (GeckoView) findViewById(R.id.layer_view);
+        // Disable automatic state staving - we require some special handling that we need to do
+        // ourselves.
+        mLayerView.setSaveFromParentEnabled(false);
 
         final GeckoSession session = new GeckoSession(
                 new GeckoSessionSettings.Builder()
                         .chromeUri("chrome://browser/content/browser.xul")
                         .build());
         session.setContentDelegate(this);
 
         // If the view already has a session, we need to ensure it is closed.
         if (mLayerView.getSession() != null) {
             mLayerView.getSession().close();
         }
         mLayerView.setSession(session, GeckoApplication.getRuntime());
         mLayerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
+        if (mIsRestoringActivity && !receivedSavedInstanceState) {
+            restoreGeckoViewState(getGeckoApplication().getSavedState());
+        }
 
         getAppEventDispatcher().registerGeckoThreadListener(this,
             "Locale:Set",
             "PrivateBrowsing:Data",
             null);
 
         getAppEventDispatcher().registerUiThreadListener(this,
             "Contact:Add",
@@ -1309,16 +1333,36 @@ public abstract class GeckoApp extends G
         if (mIsAbortingAppLaunch) {
             return;
         }
 
         mWasFirstTabShownAfterActivityUnhidden = false; // onStart indicates we were hidden.
     }
 
     @Override
+    protected void onRestoreInstanceState(Bundle savedInstanceState) {
+        super.onRestoreInstanceState(savedInstanceState);
+
+        final SparseArray<Parcelable> stateToRestore =
+                savedInstanceState.getSparseParcelableArray(GECKOVIEW_STATE_BUNDLE);
+        restoreGeckoViewState(stateToRestore);
+    }
+
+    /**
+     * Restores the given state into our GeckoView and clears any state we might have kept locally
+     * within our process, as it has now become obsolete.
+     */
+    private void restoreGeckoViewState(final SparseArray<Parcelable> state) {
+        if (state != null) {
+            mLayerView.restoreHierarchyState(state);
+        }
+        getGeckoApplication().setSavedState(null);
+    }
+
+    @Override
     protected void onStop() {
         super.onStop();
         // Overriding here is not necessary, but we do this so we don't
         // forget to add the abort if we override this method later.
         if (mIsAbortingAppLaunch) {
             return;
         }
     }
@@ -2529,16 +2573,20 @@ public abstract class GeckoApp extends G
 
     protected void recordStartupActionTelemetry(final String passedURL, final String action) {
     }
 
     public GeckoView getGeckoView() {
         return mLayerView;
     }
 
+    protected GeckoApplication getGeckoApplication() {
+        return (GeckoApplication) getApplicationContext();
+    }
+
     @Override
     public boolean setRequestedOrientationForCurrentActivity(int requestedActivityInfoOrientation) {
         // We want to support the Screen Orientation API, and it always makes sense to lock the
         // orientation of a browser Activity, so we support locking.
         if (getRequestedOrientation() == requestedActivityInfoOrientation) {
             return false;
         }
         setRequestedOrientation(requestedActivityInfoOrientation);
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
@@ -11,26 +11,28 @@ import android.app.Service;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Environment;
+import android.os.Parcelable;
 import android.os.Process;
 import android.os.SystemClock;
 import android.provider.MediaStore;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.design.widget.Snackbar;
 import android.support.multidex.MultiDex;
 import android.text.TextUtils;
 import android.util.Base64;
 import android.util.Log;
+import android.util.SparseArray;
 
 import com.squareup.leakcanary.LeakCanary;
 import com.squareup.leakcanary.RefWatcher;
 
 import org.mozilla.gecko.annotation.WrapForJNI;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.LocalBrowserDB;
@@ -76,16 +78,20 @@ public class GeckoApplication extends Ap
     private static final String MEDIA_DECODING_PROCESS_CRASH = "MEDIA_DECODING_PROCESS_CRASH";
 
     private boolean mInBackground;
     private boolean mPausedGecko;
     private boolean mIsInitialResume;
 
     private LightweightTheme mLightweightTheme;
 
+    // GeckoApp *must* keep its GeckoView state around for as long as our app process (and
+    // therefore Gecko) keeps running, even if Android clears the normal savedInstanceState.
+    private SparseArray<Parcelable> mSavedState;
+
     private RefWatcher mRefWatcher;
 
     private final EventListener mListener = new EventListener();
 
     private static String sSessionUUID = null;
 
     public GeckoApplication() {
         super();
@@ -636,16 +642,24 @@ public class GeckoApplication extends Ap
     public LightweightTheme getLightweightTheme() {
         return mLightweightTheme;
     }
 
     public void prepareLightweightTheme() {
         mLightweightTheme = new LightweightTheme(this);
     }
 
+    /* package */ void setSavedState(SparseArray<Parcelable> savedState) {
+        mSavedState = savedState;
+    }
+
+    /* package */ SparseArray<Parcelable> getSavedState() {
+        return mSavedState;
+    }
+
     public static void createShortcut() {
         final Tab selectedTab = Tabs.getInstance().getSelectedTab();
         if (selectedTab != null) {
             createShortcut(selectedTab.getTitle(), selectedTab.getURL());
         }
     }
 
     // Creates a homescreen shortcut for a web page.