Bug 1352997 - Part 5 - Remember the correct tab for CustomTab/WebAppActivities. r?sebastian draft
authorJan Henning <jh+bugzilla@buttercookie.de>
Sat, 08 Apr 2017 19:19:32 +0200
changeset 559309 ec7a7bcb41dd1b5b6d748793f0b8b11e708af9c9
parent 559308 c4cebf4e7451a3d262ffa474d6d912f672924971
child 559310 d1ecda7ca51eaafccdf962b82cc1586e922dcfaa
push id53051
push usermozilla@buttercookie.de
push dateSun, 09 Apr 2017 17:35:56 +0000
reviewerssebastian
bugs1352997
milestone55.0a1
Bug 1352997 - Part 5 - Remember the correct tab for CustomTab/WebAppActivities. r?sebastian 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. 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
@@ -1655,20 +1655,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;
         }
@@ -1801,16 +1811,20 @@ public abstract class GeckoApp
                 geckoConnected();
                 if (mLayerView != null) {
                     mLayerView.setPaintState(LayerView.PAINT_BEFORE_FIRST);
                 }
             }
         }
     }
 
+    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);
         }
@@ -2269,24 +2283,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;
+        Tab tabToCheck = Tabs.getInstance().getTab(tabId);
+
+        if (tabToCheck == null) {
+            return false;
+        }
+        return !intent.hasExtra(Tabs.INTENT_EXTRA_TAB_HASH) ||
+                tabToCheck.hashCode() == intent.getIntExtra(Tabs.INTENT_EXTRA_TAB_HASH, 0);
     }
 
     protected void handleSelectTabIntent(SafeIntent intent) {
         final int tabId = intent.getIntExtra(Tabs.INTENT_EXTRA_TAB_ID, INVALID_TAB_ID);
-        Tabs.getInstance().selectTab(tabId);
+        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,177 @@
+/* -*- 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.Nullable;
+
+import org.mozilla.gecko.mozglue.SafeIntent;
+
+import static org.mozilla.gecko.Tabs.INTENT_EXTRA_TAB_HASH;
+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) {
+        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) {
+        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 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;
+        }
+
+        Tabs tabs = Tabs.getInstance();
+        Tab tabToSelect = tabs.getTab(mLastSelectedTabId);
+
+        // If the tab we've stored is still existing and valid select it...
+        if (tabToSelect != null && tabToSelect.hashCode() == mLastSelectedTabHash &&
+                tabs.currentActivityMatchesTab(tabToSelect)) {
+            tabs.selectTab(mLastSelectedTabId);
+        // ...otherwise fall back to the intent data and open a new tab.
+        } else {
+            loadTabFromIntent(new SafeIntent(getIntent()));
+        }
+    }
+
+    private void loadTabFromIntent(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(SafeIntent intent, @Nullable Bundle savedInstanceState) {
+        Tabs tabs = Tabs.getInstance();
+
+        if (hasGeckoTab(intent)) {
+            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_TAB_HASH);
+        // 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.
+        int lastSelectedTabId;
+        int lastSelectedTabHash;
+
+        if (savedInstanceState != null) {
+            lastSelectedTabId = savedInstanceState.getInt(LAST_SELECTED_TAB);
+            lastSelectedTabHash = savedInstanceState.getInt(LAST_SELECTED_TAB_HASH);
+        } else {
+            lastSelectedTabId = mLastSelectedTabId;
+            lastSelectedTabHash = mLastSelectedTabHash;
+        }
+
+        Tab tabToSelect = tabs.getTab(lastSelectedTabId);
+        if (tabToSelect != null && tabToSelect.hashCode() == lastSelectedTabHash &&
+                tabs.currentActivityMatchesTab(tabToSelect)) {
+            intent.getUnsafe().putExtra(INTENT_EXTRA_TAB_ID, lastSelectedTabId);
+            intent.getUnsafe().putExtra(INTENT_EXTRA_TAB_HASH, lastSelectedTabHash);
+            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 onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+
+        outState.getParcelable("test");
+    }
+
+    @Override
+    protected void onDone() {
+        // When we're opening a new tab, it's always done as an externally opened tab, which means
+        // that reaching the beginning of history while pressing back will close it.
+        // Therefore, we might as well finish the activity, too.
+        // If we manage to find a solution for bug 1354834, we might want to reconsider this - if we
+        // don't get OOM-killed, we can avoid to reload Gecko on the next launch and our code should
+        // in principle be able to cope with its tab having been closed and requiring a replacement.
+        finish();
+    }
+
+    /**
+     * 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();
+        mLastSelectedTabHash = tab.hashCode();
+    }
+
+    /**
+     * 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();
+        mLastSelectedTabHash = tab.hashCode();
+    }
+}
--- 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_TAB_HASH = "TabHash";
 
     // 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();
 
     // All writes to mSelectedTab must be synchronized on the Tabs instance.
@@ -347,17 +348,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) {
         Activity currentActivity = GeckoActivityMonitor.getInstance().getCurrentActivity();
 
         if (currentActivity == null) {
             return false;
         }
         String currentActivityName = currentActivity.getClass().getName();
         return currentActivityName.equals(getClassNameForTab(tab));
     }
@@ -388,16 +389,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_TAB_HASH, tab.hashCode());
         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
@@ -32,125 +32,130 @@ import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup.LayoutParams;
 import android.widget.ImageButton;
 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.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuInflater;
 import org.mozilla.gecko.mozglue.SafeIntent;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.ColorUtil;
 import org.mozilla.gecko.util.IntentUtils;
 import org.mozilla.gecko.widget.GeckoPopupMenu;
 import org.mozilla.gecko.util.GeckoBundle;
 
 import java.util.List;
 
-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 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;
-
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        if (savedInstanceState != null) {
-            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)) {
-            Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "customtab");
-        }
+        final SafeIntent intent = new SafeIntent(getIntent());
 
         setThemeFromToolbarColor();
 
         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());
         actionBar.setDisplayHomeAsUpEnabled(true);
 
         Tabs.registerOnTabsChangedListener(this);
     }
 
+    @Override
+    protected void onTabOpenFromIntent(Tab tab) {
+        super.onTabOpenFromIntent(tab);
+
+        final String host = getReferrerHost();
+        recordCustomTabUsage(host);
+        Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "customtab");
+    }
+
+    @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 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();
     }
@@ -162,21 +167,16 @@ public class CustomTabsActivity extends 
     }
 
     @Override
     public int getLayout() {
         return R.layout.customtabs_activity;
     }
 
     @Override
-    protected void onDone() {
-        finish();
-    }
-
-    @Override
     public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
         if (!Tabs.getInstance().isSelectedTab(tab) ||
                 tab.getType() != Tab.TabType.CUSTOMTAB) {
             return;
         }
 
         if (msg == Tabs.TabEvents.START
                 || msg == Tabs.TabEvents.STOP
@@ -194,29 +194,23 @@ public class CustomTabsActivity extends 
                 || msg == Tabs.TabEvents.TITLE
                 || msg == Tabs.TabEvents.SELECTED) {
             actionBarPresenter.update(tab);
         }
 
         updateMenuItemForward();
     }
 
-    @Override
-    protected void onSaveInstanceState(Bundle outState) {
-        super.onSaveInstanceState(outState);
-        outState.putParcelable(SAVED_START_INTENT, startIntent.getUnsafe());
-    }
-
     // 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) {
-        insertActionButton(menu, startIntent, actionBarPresenter.getTextPrimaryColor());
+        insertActionButton(menu, new SafeIntent(getIntent()), actionBarPresenter.getTextPrimaryColor());
 
         popupMenu = createCustomPopupMenu();
 
         // Create a ImageButton manually, and use it as an anchor for PopupMenu.
         final ImageButton btn = new ImageButton(getContext(),
                 null, 0, R.style.Widget_MenuButtonCustomTabs);
         btn.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
         btn.setOnClickListener(new View.OnClickListener() {
@@ -327,37 +321,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);
@@ -432,17 +427,17 @@ public class CustomTabsActivity extends 
             final Intent intent = new Intent();
             intent.setData(Uri.parse(tab.getURL()));
             intent.setAction(Intent.ACTION_VIEW);
             startActivity(intent);
         }
     }
 
     private void onActionButtonClicked() {
-        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
@@ -700,16 +700,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,50 +59,50 @@ 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());
     }
 
     @Test
     public void testInsertActionButton() {
         // create properties for CustomTabsIntent
         final String description = "Description";