Bug 1352997 - Part 5 - Implement common behaviour for custom tabs/web apps and switch over the former. r?sebastian,walkingice draft
authorJan Henning <jh+bugzilla@buttercookie.de>
Sat, 08 Apr 2017 19:19:32 +0200
changeset 569643 89d681b6d97d36e032c57522b56f1d49a18ed848
parent 569642 9008f40418255ca88a2faf303bf60d10445f6d5d
child 569644 eec886a795e5cfc4c526dd77ec8ae9ef6bc0f5b7
push id56240
push usermozilla@buttercookie.de
push dateThu, 27 Apr 2017 18:44:39 +0000
reviewerssebastian, walkingice
bugs1352997
milestone55.0a1
Bug 1352997 - Part 5 - Implement common behaviour for custom tabs/web apps and switch over the former. r?sebastian,walkingice This implements the common behaviour for restoring the correct tab when switching to/from custom tab and web app activities. Unlike our normal UI, those activities are basically single tab activities, that is each activity is linked to a certain Gecko tab, with no facilities (bugs aside) for the user to directly load/select a different tab within that activity. Therefore, here we basically update the selected tab only when the activity is starting up and initially creating its new (or, especially once tab type switching will be implemented, taking over an existing) content tab. When subsequently restoring, we then check whether the tab is still available. If it is, we select it, if not, we fall back to opening a new tab based on the available intent data. This also means that we no longer have to finish() the activity on closing so the activity state (finished) matches the tab (closed), which means that we no longer have to prematurely kill Gecko as a side effect of that. MozReview-Commit-ID: KjFz1qrqWLy
mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
mobile/android/base/java/org/mozilla/gecko/SingleTabActivity.java
mobile/android/base/java/org/mozilla/gecko/Tabs.java
mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
mobile/android/base/moz.build
mobile/android/tests/background/junit4/src/org/mozilla/gecko/customtabs/TestCustomTabsActivity.java
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -1705,20 +1705,30 @@ public abstract class GeckoApp
      */
     protected void loadStartupTab(final String url, final SafeIntent intent, final int flags) {
         // Invalid url
         if (url == null) {
             loadStartupTab(flags, intent.getAction());
             return;
         }
 
-        Tabs.getInstance().loadUrlWithIntentExtras(url, intent, flags);
+        final Tab newTab = Tabs.getInstance().loadUrlWithIntentExtras(url, intent, flags);
+        if (ThreadUtils.isOnUiThread()) {
+            onTabOpenFromIntent(newTab);
+        } else {
+            ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    onTabOpenFromIntent(newTab);
+                }
+            });
+        }
     }
 
-    private String getIntentURI(SafeIntent intent) {
+    protected String getIntentURI(SafeIntent intent) {
         final String passedUri;
         final String uri = getURIFromIntent(intent);
 
         if (!TextUtils.isEmpty(uri)) {
             passedUri = uri;
         } else {
             passedUri = null;
         }
@@ -1848,16 +1858,20 @@ public abstract class GeckoApp
             }
 
             if (GeckoThread.isRunning()) {
                 geckoConnected();
             }
         }
     }
 
+    protected void onTabOpenFromIntent(Tab tab) { }
+
+    protected void onTabSelectFromIntent(Tab tab) { }
+
     @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
     @Override
     public void onGlobalLayout() {
         if (Versions.preJB) {
             mMainLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
         } else {
             mMainLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
         }
@@ -2315,24 +2329,42 @@ public abstract class GeckoApp
             // Copy extras.
             settingsIntent.putExtras(intent.getUnsafe());
             startActivity(settingsIntent);
         }
 
         recordStartupActionTelemetry(passedUri, action);
     }
 
+    /**
+     * Check whether an intent with tab switch extras refers to a tab that
+     * is actually existing at the moment.
+     *
+     * @param intent The intent to be checked.
+     * @return True if the tab specified in the intent is existing in our Tabs list.
+     */
     protected boolean hasGeckoTab(SafeIntent intent) {
         final int tabId = intent.getIntExtra(Tabs.INTENT_EXTRA_TAB_ID, INVALID_TAB_ID);
-        return Tabs.getInstance().getTab(tabId) != null;
+        final String intentSessionUUID = intent.getStringExtra(Tabs.INTENT_EXTRA_SESSION_UUID);
+        final Tab tabToCheck = Tabs.getInstance().getTab(tabId);
+
+        // We only care about comparing session UUIDs if one was specified in the intent.
+        // Otherwise, we just try matching the tab ID with one of our open tabs.
+        return tabToCheck != null && (!intent.hasExtra(Tabs.INTENT_EXTRA_SESSION_UUID) ||
+                GeckoApplication.getSessionUUID().equals(intentSessionUUID));
     }
 
     protected void handleSelectTabIntent(SafeIntent intent) {
         final int tabId = intent.getIntExtra(Tabs.INTENT_EXTRA_TAB_ID, INVALID_TAB_ID);
-        Tabs.getInstance().selectTab(tabId);
+        final Tab selectedTab = Tabs.getInstance().selectTab(tabId);
+        // If the tab selection has been redirected to a different activity,
+        // the selectedTab within Tabs will not have been updated yet.
+        if (selectedTab == Tabs.getInstance().getSelectedTab()) {
+            onTabSelectFromIntent(selectedTab);
+        }
     }
 
     /**
      * Handles getting a URI from an intent in a way that is backwards-
      * compatible with our previous implementations.
      */
     protected String getURIFromIntent(SafeIntent intent) {
         final String action = intent.getAction();
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/SingleTabActivity.java
@@ -0,0 +1,179 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import org.mozilla.gecko.mozglue.SafeIntent;
+
+import static org.mozilla.gecko.Tabs.INTENT_EXTRA_SESSION_UUID;
+import static org.mozilla.gecko.Tabs.INTENT_EXTRA_TAB_ID;
+import static org.mozilla.gecko.Tabs.INVALID_TAB_ID;
+
+public abstract class SingleTabActivity extends GeckoApp {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        final Intent externalIntent = getIntent();
+        // We need the current activity to already be up-to-date before
+        // calling into the superclass.
+        GeckoActivityMonitor.getInstance().setCurrentActivity(this);
+
+        decideTabAction(new SafeIntent(externalIntent), savedInstanceState);
+
+        super.onCreate(savedInstanceState);
+        // GeckoApp's default behaviour is to reset the intent if we've got any
+        // savedInstanceState, which we don't want here.
+        setIntent(externalIntent);
+    }
+
+    @Override
+    protected void onNewIntent(Intent externalIntent) {
+        final SafeIntent intent = new SafeIntent(externalIntent);
+        // We need the current activity to already be up-to-date before
+        // calling into the superclass.
+        GeckoActivityMonitor.getInstance().setCurrentActivity(this);
+
+        if (decideTabAction(intent, null)) {
+            // GeckoApp will handle tab selection.
+            super.onNewIntent(intent.getUnsafe());
+        } else {
+            // We're not calling the superclass in this code path, so we'll
+            // have to notify the activity monitor ourselves.
+            GeckoActivityMonitor.getInstance().onActivityNewIntent(this);
+            loadTabFromIntent(intent);
+        }
+        // Again, unlike GeckoApp's default behaviour we want to keep the intent around
+        // because we might still require its data (e.g. to get custom tab customisations).
+        setIntent(intent.getUnsafe());
+    }
+
+    @Override
+    protected boolean saveSelectedStartupTab() {
+        // We ignore the tab selection made by session restoring in order to display our own tab,
+        // so we should save that tab's ID in case the user starts up our normal browsing UI later
+        // during the session.
+        return true;
+    }
+
+    @Override
+    protected void restoreLastSelectedTab() {
+        if (!mInitialized) {
+            // During startup from onCreate(), initialize() will handle selecting the startup tab.
+            // If this here is called afterwards, it's a no-op anyway. If for some reason
+            // (e.g. debugging) initialize() takes longer than usual and hasn't finished by the time
+            // onResume() runs and calls us, we just exit early so as not to interfere.
+            return;
+        }
+
+        final Tabs tabs = Tabs.getInstance();
+        final Tab tabToSelect = tabs.getTab(mLastSelectedTabId);
+
+        // If the tab we've stored is still existing and valid select it...
+        if (tabToSelect != null && GeckoApplication.getSessionUUID().equals(mLastSessionUUID) &&
+                tabs.currentActivityMatchesTab(tabToSelect)) {
+            tabs.selectTab(mLastSelectedTabId);
+        } else {
+            // ... otherwise fall back to the intent data and open a new tab.
+            loadTabFromIntent(new SafeIntent(getIntent()));
+        }
+    }
+
+    private void loadTabFromIntent(final SafeIntent intent) {
+        final int flags = getNewTabFlags();
+        loadStartupTab(getIntentURI(intent), intent, flags);
+    }
+
+    /**
+     * @return True if we're going to select an existing tab, false if we want to load a new tab.
+     */
+    private boolean decideTabAction(@NonNull final SafeIntent intent,
+                                    @Nullable final Bundle savedInstanceState) {
+        final Tabs tabs = Tabs.getInstance();
+
+        if (hasGeckoTab(intent)) {
+            final Tab tabToSelect = tabs.getTab(intent.getIntExtra(INTENT_EXTRA_TAB_ID, INVALID_TAB_ID));
+            if (tabs.currentActivityMatchesTab(tabToSelect)) {
+                // Nothing further to do here, GeckoApp will select the correct
+                // tab from the intent.
+                return true;
+            }
+        }
+        // The intent doesn't refer to a valid tab, so don't pass that data on.
+        intent.getUnsafe().removeExtra(INTENT_EXTRA_TAB_ID);
+        intent.getUnsafe().removeExtra(INTENT_EXTRA_SESSION_UUID);
+        // The tab data in the intent can become stale if we've been killed, or have
+        // closed the tab/changed its type since the original intent.
+        // We therefore attempt to fall back to the last selected tab. In onNewIntent,
+        // we can directly use the stored data, otherwise we'll look for it in the
+        // savedInstanceState.
+        final int lastSelectedTabId;
+        final String lastSessionUUID;
+
+        if (savedInstanceState != null) {
+            lastSelectedTabId = savedInstanceState.getInt(LAST_SELECTED_TAB);
+            lastSessionUUID = savedInstanceState.getString(LAST_SESSION_UUID);
+        } else {
+            lastSelectedTabId = mLastSelectedTabId;
+            lastSessionUUID = mLastSessionUUID;
+        }
+
+        final Tab tabToSelect = tabs.getTab(lastSelectedTabId);
+        if (tabToSelect != null && GeckoApplication.getSessionUUID().equals(lastSessionUUID) &&
+                tabs.currentActivityMatchesTab(tabToSelect)) {
+            intent.getUnsafe().putExtra(INTENT_EXTRA_TAB_ID, lastSelectedTabId);
+            intent.getUnsafe().putExtra(INTENT_EXTRA_SESSION_UUID, lastSessionUUID);
+            return true;
+        }
+
+        // If we end up here, this means that there's no suitable tab we can take over.
+        // Instead, we'll just open a new tab from the data specified in the intent.
+        return false;
+    }
+
+    @Override
+    protected void onDone() {
+        // Our startup logic should be robust enough to cope with it's tab having been closed even
+        // though the activity might survive, so we don't have to call finish() just to make sure
+        // that a new tab is opened in that case. This also has the advantage that we'll remain in
+        // memory as long as the low-memory killer permits, so we can potentially avoid a costly
+        // re-startup of Gecko if the user returns to us soon.
+        moveTaskToBack(true);
+    }
+
+    /**
+     * For us here, mLastSelectedTabId/Hash will hold the tab that will be selected when the
+     * activity is resumed/recreated, unless
+     * - it has been explicitly overridden through an intent
+     * - the tab cannot be found, in which case the URI passed as intent data will instead be
+     *   opened in a new tab.
+     * Therefore, we only update the stored tab data from those two locations.
+     */
+
+    /**
+     * Called when an intent or onResume() has caused us to load and select a new tab.
+     *
+     * @param tab The new tab that has been opened and selected.
+     */
+    @Override
+    protected void onTabOpenFromIntent(Tab tab) {
+        mLastSelectedTabId = tab.getId();
+        mLastSessionUUID = GeckoApplication.getSessionUUID();
+    }
+
+    /**
+     * Called when an intent has caused us to select an already existing tab.
+     *
+     * @param tab The already existing tab that has been selected for this activity.
+     */
+    @Override
+    protected void onTabSelectFromIntent(Tab tab) {
+        mLastSelectedTabId = tab.getId();
+        mLastSessionUUID = GeckoApplication.getSessionUUID();
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/Tabs.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tabs.java
@@ -59,16 +59,17 @@ import android.text.TextUtils;
 import android.util.Log;
 
 import static org.mozilla.gecko.Tab.TabType;
 
 public class Tabs implements BundleEventListener {
     private static final String LOGTAG = "GeckoTabs";
 
     public static final String INTENT_EXTRA_TAB_ID = "TabId";
+    public static final String INTENT_EXTRA_SESSION_UUID = "SessionUUID";
     private static final String PRIVATE_TAB_INTENT_EXTRA = "private_tab";
 
     // mOrder and mTabs are always of the same cardinality, and contain the same values.
     private volatile CopyOnWriteArrayList<Tab> mOrder = new CopyOnWriteArrayList<Tab>();
 
     // A cache that maps a tab ID to an mOrder tab position.  All access should be synchronized.
     private final TabPositionCache tabPositionCache = new TabPositionCache();
 
@@ -348,17 +349,17 @@ public class Tabs implements BundleEvent
         data.putInt("id", tab.getId());
         EventDispatcher.getInstance().dispatch("Tab:Selected", data);
         return tab;
     }
 
     /**
      * Check whether the currently active activity matches the tab type of the passed tab.
      */
-    private boolean currentActivityMatchesTab(Tab tab) {
+    public boolean currentActivityMatchesTab(Tab tab) {
         final Activity currentActivity = GeckoActivityMonitor.getInstance().getCurrentActivity();
 
         if (currentActivity == null) {
             return false;
         }
         String currentActivityName = currentActivity.getClass().getName();
         return currentActivityName.equals(getClassNameForTab(tab));
     }
@@ -389,16 +390,17 @@ public class Tabs implements BundleEvent
                 intent = new Intent(GeckoApp.ACTION_SWITCH_TAB);
                 break;
         }
 
         intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, getClassNameForTab(tab));
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         intent.putExtra(BrowserContract.SKIP_TAB_QUEUE_FLAG, true);
         intent.putExtra(INTENT_EXTRA_TAB_ID, tab.getId());
+        intent.putExtra(INTENT_EXTRA_SESSION_UUID, GeckoApplication.getSessionUUID());
         mAppContext.startActivity(intent);
     }
 
     // TODO: When things have settled down a bit, we should split this and everything similar
     // TODO: in the WebAppActivity into a dedicated WebAppManifest class (bug 1353868).
     private Uri getStartUriFromManifest(String manifestPath) throws IOException, JSONException {
         File manifestFile = new File(manifestPath);
         final JSONObject manifest = FileUtils.readJSONObjectFromFile(manifestFile);
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
@@ -27,18 +27,18 @@ import android.text.TextUtils;
 import android.util.Log;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
 import android.widget.ProgressBar;
 
 import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.SingleTabActivity;
 import org.mozilla.gecko.SnackbarBuilder;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
 import org.mozilla.gecko.menu.GeckoMenu;
@@ -50,129 +50,135 @@ import org.mozilla.gecko.util.GeckoBundl
 import org.mozilla.gecko.util.IntentUtils;
 import org.mozilla.gecko.widget.ActionModePresenter;
 import org.mozilla.gecko.widget.GeckoPopupMenu;
 
 import java.util.List;
 
 import static org.mozilla.gecko.Tabs.TabEvents;
 
-public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedListener {
+public class CustomTabsActivity extends SingleTabActivity implements Tabs.OnTabsChangedListener {
+
     private static final String LOGTAG = "CustomTabsActivity";
-    private static final String SAVED_START_INTENT = "saved_intent_which_started_this_activity";
 
     private final SparseArrayCompat<PendingIntent> menuItemsIntent = new SparseArrayCompat<>();
     private GeckoPopupMenu popupMenu;
     private View doorhangerOverlay;
     private ActionBarPresenter actionBarPresenter;
     private ProgressBar mProgressView;
     // A state to indicate whether this activity is finishing with customize animation
     private boolean usingCustomAnimation = false;
 
-    // Bug 1351605 - getIntent() not always returns the intent which started this activity.
-    // Therefore we make a copy in case of this Activity is re-created.
-    private SafeIntent startIntent;
-
     private MenuItem menuItemControl;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        if (savedInstanceState != null) {
-            final Intent restoredIntent = savedInstanceState.getParcelable(SAVED_START_INTENT);
-            startIntent = new SafeIntent(restoredIntent);
-        } else {
-            startIntent = new SafeIntent(getIntent());
-            final String host = getReferrerHost();
-            recordCustomTabUsage(host);
-        }
-
-        if (!mIsRestoringActivity || !hasGeckoTab(startIntent)) {
-            sendTelemetry();
-        }
+        final SafeIntent intent = new SafeIntent(getIntent());
 
         setThemeFromToolbarColor();
 
         doorhangerOverlay = findViewById(R.id.custom_tabs_doorhanger_overlay);
 
         mProgressView = (ProgressBar) findViewById(R.id.page_progress);
         final Toolbar toolbar = (Toolbar) findViewById(R.id.actionbar);
         setSupportActionBar(toolbar);
         final ActionBar actionBar = getSupportActionBar();
         bindNavigationCallback(toolbar);
 
         actionBarPresenter = new ActionBarPresenter(actionBar);
-        actionBarPresenter.displayUrlOnly(startIntent.getDataString());
-        actionBarPresenter.setBackgroundColor(IntentUtil.getToolbarColor(startIntent), getWindow());
+        actionBarPresenter.displayUrlOnly(intent.getDataString());
+        actionBarPresenter.setBackgroundColor(IntentUtil.getToolbarColor(intent), getWindow());
         actionBarPresenter.setTextLongClickListener(new UrlCopyListener());
 
         Tabs.registerOnTabsChangedListener(this);
     }
 
+    @Override
+    protected void onTabOpenFromIntent(Tab tab) {
+        super.onTabOpenFromIntent(tab);
+
+        final String host = getReferrerHost();
+        recordCustomTabUsage(host);
+        sendTelemetry();
+    }
+
+    @Override
+    protected void onTabSelectFromIntent(Tab tab) {
+        super.onTabSelectFromIntent(tab);
+
+        // We already listen for SELECTED events, but if the activity has been destroyed and
+        // subsequently recreated without a different tab having been selected in Gecko in the
+        // meantime, our startup won't trigger a SELECTED event because the selected tab in Gecko
+        // doesn't actually change.
+        actionBarPresenter.update(tab);
+    }
+
     private void sendTelemetry() {
+        final SafeIntent startIntent = new SafeIntent(getIntent());
+
         Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "customtab");
         if (IntentUtil.hasToolbarColor(startIntent)) {
             Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "customtab-hasToolbarColor");
         }
         if (IntentUtil.hasActionButton(startIntent)) {
             Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "customtab-hasActionButton");
         }
         if (IntentUtil.isActionButtonTinted(startIntent)) {
             Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "customtab-isActionButtonTinted");
         }
         if (IntentUtil.hasShareItem(startIntent)) {
             Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "customtab-hasShareItem");
         }
-
-
     }
 
     private void recordCustomTabUsage(final String host) {
         final GeckoBundle data = new GeckoBundle(1);
         if (host != null) {
             data.putString("client", host);
         } else {
             data.putString("client", "unknown");
         }
         // Pass a message to Gecko to send Telemetry data
         EventDispatcher.getInstance().dispatch("Telemetry:CustomTabsPing", data);
     }
 
     private void setThemeFromToolbarColor() {
-        final int color = ColorUtil.getReadableTextColor(IntentUtil.getToolbarColor(startIntent));
+        final int color = ColorUtil.getReadableTextColor(IntentUtil.getToolbarColor(new SafeIntent(getIntent())));
         @StyleRes final int styleRes = (color == Color.BLACK)
                 ? R.style.GeckoCustomTabs_Light
                 : R.style.GeckoCustomTabs;
 
         setTheme(styleRes);
     }
 
     // Bug 1329145: 3rd party app could specify customized exit-animation to this activity.
     // Activity.overridePendingTransition will invoke getPackageName to retrieve that animation resource.
     // In that case, to return different package name to get customized animation resource.
     @Override
     public String getPackageName() {
         if (usingCustomAnimation) {
             // Use its package name to retrieve animation resource
-            return IntentUtil.getAnimationPackageName(startIntent);
+            return IntentUtil.getAnimationPackageName(new SafeIntent(getIntent()));
         } else {
             return super.getPackageName();
         }
     }
 
     @Override
     public void finish() {
         super.finish();
 
+        final SafeIntent intent = new SafeIntent(getIntent());
         // When 3rd party app launch this Activity, it could also specify custom exit-animation.
-        if (IntentUtil.hasExitAnimation(startIntent)) {
+        if (IntentUtil.hasExitAnimation(intent)) {
             usingCustomAnimation = true;
-            overridePendingTransition(IntentUtil.getEnterAnimationRes(startIntent),
-                    IntentUtil.getExitAnimationRes(startIntent));
+            overridePendingTransition(IntentUtil.getEnterAnimationRes(intent),
+                    IntentUtil.getExitAnimationRes(intent));
             usingCustomAnimation = false;
         }
     }
 
     @Override
     protected int getNewTabFlags() {
         return Tabs.LOADURL_CUSTOMTAB | super.getNewTabFlags();
     }
@@ -189,21 +195,16 @@ public class CustomTabsActivity extends 
     }
 
     @Override
     public View getDoorhangerOverlay() {
         return doorhangerOverlay;
     }
 
     @Override
-    protected void onDone() {
-        finish();
-    }
-
-    @Override
     public void onTabChanged(Tab tab, TabEvents msg, String data) {
         if (!Tabs.getInstance().isSelectedTab(tab) ||
                 tab.getType() != Tab.TabType.CUSTOMTAB) {
             return;
         }
 
         if (msg == TabEvents.START
                 || msg == TabEvents.STOP
@@ -223,22 +224,16 @@ public class CustomTabsActivity extends 
                 || msg == TabEvents.SELECTED) {
             actionBarPresenter.update(tab);
         }
 
         updateMenuItemForward();
     }
 
     @Override
-    protected void onSaveInstanceState(Bundle outState) {
-        super.onSaveInstanceState(outState);
-        outState.putParcelable(SAVED_START_INTENT, startIntent.getUnsafe());
-    }
-
-    @Override
     public void onResume() {
         super.onResume();
         mLayerView.getDynamicToolbarAnimator().setPinned(true, PinReason.CUSTOM_TAB);
     }
 
     @Override
     public void onPause() {
         super.onPause();
@@ -248,20 +243,21 @@ public class CustomTabsActivity extends 
     // Usually should use onCreateOptionsMenu() to initialize menu items. But GeckoApp overwrite
     // it to support custom menu(Bug 739412). Then the parameter *menu* in this.onCreateOptionsMenu()
     // and this.onPrepareOptionsMenu() are different instances - GeckoApp.onCreatePanelMenu() changed it.
     // CustomTabsActivity only use standard menu in ActionBar, so initialize menu here.
     @Override
     public boolean onCreatePanelMenu(final int id, final Menu menu) {
 
         // if 3rd-party app asks to add an action button
-        if (IntentUtil.hasActionButton(startIntent)) {
-            final Bitmap bitmap = IntentUtil.getActionButtonIcon(startIntent);
+        SafeIntent intent = new SafeIntent(getIntent());
+        if (IntentUtil.hasActionButton(intent)) {
+            final Bitmap bitmap = IntentUtil.getActionButtonIcon(intent);
             final Drawable icon = new BitmapDrawable(getResources(), bitmap);
-            final boolean shouldTint = IntentUtil.isActionButtonTinted(startIntent);
+            final boolean shouldTint = IntentUtil.isActionButtonTinted(intent);
             actionBarPresenter.addActionButton(menu, icon, shouldTint)
                     .setOnClickListener(new View.OnClickListener() {
                         @Override
                         public void onClick(View v) {
                             onActionButtonClicked();
                         }
                     });
         }
@@ -344,20 +340,20 @@ public class CustomTabsActivity extends 
             }
         };
     }
 
     private void bindNavigationCallback(@NonNull final Toolbar toolbar) {
         toolbar.setNavigationOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
+                onDone();
                 final Tabs tabs = Tabs.getInstance();
                 final Tab tab = tabs.getSelectedTab();
                 tabs.closeTab(tab);
-                finish();
             }
         });
     }
 
     private void performPendingIntent(@NonNull PendingIntent pendingIntent) {
         // bug 1337771: If intent-creator haven't set data url, call send() directly won't work.
         final Intent additional = new Intent();
         final Tab tab = Tabs.getInstance().getSelectedTab();
@@ -373,37 +369,38 @@ public class CustomTabsActivity extends 
      * To generate a popup menu which looks like an ordinary option menu, but have extra elements
      * such as footer.
      *
      * @return a GeckoPopupMenu which can be placed on any view.
      */
     private GeckoPopupMenu createCustomPopupMenu() {
         final GeckoPopupMenu popupMenu = new GeckoPopupMenu(this);
         final GeckoMenu geckoMenu = popupMenu.getMenu();
+        final SafeIntent intent = new SafeIntent(getIntent());
 
         // pass to to Activity.onMenuItemClick for consistency.
         popupMenu.setOnMenuItemClickListener(new GeckoPopupMenu.OnMenuItemClickListener() {
             @Override
             public boolean onMenuItemClick(MenuItem item) {
                 return CustomTabsActivity.this.onMenuItemClick(item);
             }
         });
 
         // to add custom menu items
-        final List<String> titles = IntentUtil.getMenuItemsTitle(startIntent);
-        final List<PendingIntent> intents = IntentUtil.getMenuItemsPendingIntent(startIntent);
+        final List<String> titles = IntentUtil.getMenuItemsTitle(intent);
+        final List<PendingIntent> intents = IntentUtil.getMenuItemsPendingIntent(intent);
         menuItemsIntent.clear();
         for (int i = 0; i < titles.size(); i++) {
             final int menuId = Menu.FIRST + i;
             geckoMenu.add(Menu.NONE, menuId, Menu.NONE, titles.get(i));
             menuItemsIntent.put(menuId, intents.get(i));
         }
 
         // to add share menu item, if necessary
-        if (IntentUtil.hasShareItem(startIntent) && !TextUtils.isEmpty(startIntent.getDataString())) {
+        if (IntentUtil.hasShareItem(intent) && !TextUtils.isEmpty(intent.getDataString())) {
             geckoMenu.add(Menu.NONE, R.id.share, Menu.NONE, getString(R.string.share));
         }
 
         final MenuInflater inflater = new GeckoMenuInflater(this);
         inflater.inflate(R.menu.customtabs_menu, geckoMenu);
 
         // insert default browser name to title of menu-item-Open-In
         final MenuItem openItem = geckoMenu.findItem(R.id.custom_tabs_menu_open_in);
@@ -494,17 +491,17 @@ public class CustomTabsActivity extends 
             intent.setAction(Intent.ACTION_VIEW);
             startActivity(intent);
             finish();
         }
     }
 
     private void onActionButtonClicked() {
         Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "customtab-action-button");
-        PendingIntent pendingIntent = IntentUtil.getActionButtonPendingIntent(startIntent);
+        PendingIntent pendingIntent = IntentUtil.getActionButtonPendingIntent(new SafeIntent(getIntent()));
         performPendingIntent(pendingIntent);
     }
 
 
     /**
      * Callback for Share menu item.
      */
     private void onShareClicked() {
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -816,16 +816,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'restrictions/RestrictionConfiguration.java',
     'restrictions/RestrictionProvider.java',
     'restrictions/Restrictions.java',
     'ScreenshotObserver.java',
     'search/SearchEngine.java',
     'search/SearchEngineManager.java',
     'SessionParser.java',
     'SharedPreferencesHelper.java',
+    'SingleTabActivity.java',
     'SiteIdentity.java',
     'SnackbarBuilder.java',
     'SuggestClient.java',
     'switchboard/AsyncConfigLoader.java',
     'switchboard/DeviceUuidFactory.java',
     'switchboard/Preferences.java',
     'switchboard/Switch.java',
     'switchboard/SwitchBoard.java',
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/customtabs/TestCustomTabsActivity.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/customtabs/TestCustomTabsActivity.java
@@ -59,46 +59,46 @@ public class TestCustomTabsActivity {
     }
 
     /**
      * Activity should not call overridePendingTransition if custom animation does not exist.
      */
     @Test
     public void testFinishWithoutCustomAnimation() {
         final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
-        final SafeIntent i = new SafeIntent(builder.build().intent);
+        final Intent i = builder.build().intent;
 
-        Whitebox.setInternalState(spyActivity, "startIntent", i);
+        doReturn(i).when(spyActivity).getIntent();
 
         spyActivity.finish();
         verify(spyActivity, times(0)).overridePendingTransition(anyInt(), anyInt());
     }
 
     /**
      * Activity should call overridePendingTransition if custom animation exists.
      */
     @Test
     public void testFinish() {
         final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
         builder.setExitAnimations(spyContext, enterRes, exitRes);
-        final SafeIntent i = new SafeIntent(builder.build().intent);
+        final Intent i = builder.build().intent;
 
-        Whitebox.setInternalState(spyActivity, "startIntent", i);
+        doReturn(i).when(spyActivity).getIntent();
 
         spyActivity.finish();
         verify(spyActivity, times(1)).overridePendingTransition(eq(enterRes), eq(exitRes));
     }
 
     /**
      * To get 3rd party app's package name, if custom animation exists.
      */
     @Test
     public void testGetPackageName() {
         final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
         builder.setExitAnimations(spyContext, enterRes, exitRes);
-        final SafeIntent i = new SafeIntent(builder.build().intent);
+        final Intent i = builder.build().intent;
 
+        doReturn(i).when(spyActivity).getIntent();
         Whitebox.setInternalState(spyActivity, "usingCustomAnimation", true);
-        Whitebox.setInternalState(spyActivity, "startIntent", i);
 
         Assert.assertEquals(THIRD_PARTY_PACKAGE_NAME, spyActivity.getPackageName());
     }
 }
\ No newline at end of file