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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Jan Henning 2019-01-14 19:21:49 +00:00
parent a1c718855a
commit f74e1879b8
2 changed files with 63 additions and 1 deletions

View File

@ -63,6 +63,7 @@ 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;
@ -71,6 +72,7 @@ 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;
@ -136,6 +138,7 @@ public abstract class GeckoApp extends GeckoActivity
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";
@ -658,6 +661,16 @@ public abstract class GeckoApp extends GeckoActivity
}
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) { }
@ -696,7 +709,7 @@ public abstract class GeckoApp extends GeckoActivity
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() {
@ -976,6 +989,11 @@ public abstract class GeckoApp extends GeckoActivity
**/
@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();
@ -1106,6 +1124,9 @@ public abstract class GeckoApp extends GeckoActivity
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()
@ -1119,6 +1140,9 @@ public abstract class GeckoApp extends GeckoActivity
}
mLayerView.setSession(session, GeckoApplication.getRuntime());
mLayerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
if (mIsRestoringActivity && !receivedSavedInstanceState) {
restoreGeckoViewState(getGeckoApplication().getSavedState());
}
getAppEventDispatcher().registerGeckoThreadListener(this,
"Locale:Set",
@ -1313,6 +1337,26 @@ public abstract class GeckoApp extends GeckoActivity
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();
@ -2534,6 +2578,10 @@ public abstract class GeckoApp extends GeckoActivity
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

View File

@ -16,6 +16,7 @@ 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;
@ -26,6 +27,7 @@ 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;
@ -81,6 +83,10 @@ public class GeckoApplication extends Application
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();
@ -641,6 +647,14 @@ public class GeckoApplication extends Application
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) {