Bug 1351739 - Part 3 - Switch activities when a custom tab is selected/unselected. r?sebastian,walkingice draft
authorJan Henning <jh+bugzilla@buttercookie.de>
Sat, 08 Apr 2017 23:10:51 +0200
changeset 569635 15f98966a64ddd86700fc193f17fd0898786a463
parent 569634 29ed3048a66572623d5c7c9af7b3718d043f0803
child 569636 78baa9118e7e3d35fd0b85ace07dd79ac5a6cfb5
push id56240
push usermozilla@buttercookie.de
push dateThu, 27 Apr 2017 18:44:39 +0000
reviewerssebastian, walkingice
bugs1351739
milestone55.0a1
Bug 1351739 - Part 3 - Switch activities when a custom tab is selected/unselected. r?sebastian,walkingice On tab selection, the Tabs instance now checks whether the type of the tab to be selected matches the currently running activity. If it doesn't, the tab switching is aborted and instead, an intent for the correct activity is sent. When the new activity launches, it finds that the intent also includes a tab ID, which means that instead of opening a new tab we retry the tab selection, which will then succeed now that we're in the correct activity. Because for custom tabs the launch intent can contain all sorts of customisations, we now have to save the intent when a custom tab is opened for the first time, so that later on, when switching e.g. from BrowserApp back to a custom tab we can use the correct intent to launch the custom tab activity. MozReview-Commit-ID: KWdkweKBocz
mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
mobile/android/base/java/org/mozilla/gecko/Tab.java
mobile/android/base/java/org/mozilla/gecko/Tabs.java
mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -405,16 +405,18 @@ public abstract class GeckoApp
     public void removeAppStateListener(GeckoAppShell.AppStateListener listener) {
         mAppStateListeners.remove(listener);
     }
 
     @Override
     public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
         // When a tab is closed, it is always unselected first.
         // When a tab is unselected, another tab is always selected first.
+        // When we're switching activities because of differing tab types,
+        // the first statement is not true.
         switch (msg) {
             case UNSELECTED:
                 break;
 
             case LOCATION_CHANGE:
                 // We only care about location change for the selected tab.
                 if (!Tabs.getInstance().isSelectedTab(tab))
                     break;
@@ -1237,16 +1239,18 @@ public abstract class GeckoApp
             Log.i(LOGTAG, "System locale changed. Restarting.");
             doRestart();
             return;
         }
 
         if (sAlreadyLoaded) {
             // This happens when the GeckoApp activity is destroyed by Android
             // without killing the entire application (see Bug 769269).
+            // Now that we've got multiple GeckoApp-based activities, this can
+            // also happen if we're not the first activity to run within a session.
             mIsRestoringActivity = true;
             Telemetry.addToHistogram("FENNEC_RESTORING_ACTIVITY", 1);
 
         } else {
             final String action = intent.getAction();
             final String args = intent.getStringExtra("args");
 
             sAlreadyLoaded = true;
@@ -1711,19 +1715,22 @@ public abstract class GeckoApp
                 try {
                     wait();
                 } catch (final InterruptedException e) {
                     // Ignore and wait again.
                 }
             }
         }
 
+        if (mIsRestoringActivity && hasGeckoTab(intent)) {
+            Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED);
+            handleSelectTabIntent(intent);
         // External URLs should always be loaded regardless of whether Gecko is
         // already running.
-        if (isExternalURL) {
+        } else if (isExternalURL) {
             // Restore tabs before opening an external URL so that the new tab
             // is animated properly.
             Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED);
             processActionViewIntent(new Runnable() {
                 @Override
                 public void run() {
                     final int flags = getNewTabFlags();
                     loadStartupTab(passedUri, intent, flags);
@@ -2199,17 +2206,20 @@ public abstract class GeckoApp
         final String uri = getURIFromIntent(intent);
         final String passedUri;
         if (!TextUtils.isEmpty(uri)) {
             passedUri = uri;
         } else {
             passedUri = null;
         }
 
-        if (ACTION_LOAD.equals(action)) {
+        if (hasGeckoTab(intent)) {
+            // This also covers ACTION_SWITCH_TAB.
+            handleSelectTabIntent(intent);
+        } else if (ACTION_LOAD.equals(action)) {
             Tabs.getInstance().loadUrl(intent.getDataString());
             lastSelectedTabId = INVALID_TAB_ID;
         } else if (Intent.ACTION_VIEW.equals(action)) {
             processActionViewIntent(new Runnable() {
                 @Override
                 public void run() {
                     final String url = intent.getDataString();
                     int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_USER_ENTERED | Tabs.LOADURL_EXTERNAL;
@@ -2227,25 +2237,32 @@ public abstract class GeckoApp
         } else if (NotificationHelper.HELPER_BROADCAST_ACTION.equals(action)) {
             NotificationHelper.getInstance(getApplicationContext()).handleNotificationIntent(intent);
         } else if (ACTION_LAUNCH_SETTINGS.equals(action)) {
             // Check if launched from data reporting notification.
             Intent settingsIntent = new Intent(GeckoApp.this, GeckoPreferences.class);
             // Copy extras.
             settingsIntent.putExtras(intent.getUnsafe());
             startActivity(settingsIntent);
-        } else if (ACTION_SWITCH_TAB.equals(action)) {
-            final int tabId = intent.getIntExtra("TabId", INVALID_TAB_ID);
-            Tabs.getInstance().selectTab(tabId);
-            lastSelectedTabId = INVALID_TAB_ID;
         }
 
         recordStartupActionTelemetry(passedUri, action);
     }
 
+    protected boolean hasGeckoTab(SafeIntent intent) {
+        final int tabId = intent.getIntExtra(Tabs.INTENT_EXTRA_TAB_ID, INVALID_TAB_ID);
+        return Tabs.getInstance().getTab(tabId) != null;
+    }
+
+    protected void handleSelectTabIntent(SafeIntent intent) {
+        final int tabId = intent.getIntExtra(Tabs.INTENT_EXTRA_TAB_ID, INVALID_TAB_ID);
+        Tabs.getInstance().selectTab(tabId);
+        lastSelectedTabId = INVALID_TAB_ID;
+    }
+
     /**
      * 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();
         if (ACTION_ALERT_CALLBACK.equals(action) ||
                 NotificationHelper.HELPER_BROADCAST_ACTION.equals(action)) {
@@ -2721,17 +2738,19 @@ public abstract class GeckoApp
 
                 if (tab.doBack()) {
                     return;
                 }
 
                 if (tab.isExternal()) {
                     onDone();
                     Tab nextSelectedTab = Tabs.getInstance().getNextTab(tab);
-                    if (nextSelectedTab != null) {
+                    // Closing the tab will select the next tab. There's no need to unzombify it
+                    // if we're really exiting - switching activities is a different matter, though.
+                    if (nextSelectedTab != null && nextSelectedTab.getType() == tab.getType()) {
                         final GeckoBundle data = new GeckoBundle(1);
                         data.putInt("nextSelectedTabId", nextSelectedTab.getId());
                         EventDispatcher.getInstance().dispatch("Tab:KeepZombified", data);
                     }
                     tabs.closeTab(tab);
                     return;
                 }
 
--- a/mobile/android/base/java/org/mozilla/gecko/Tab.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tab.java
@@ -15,16 +15,17 @@ import org.mozilla.gecko.annotation.Robo
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.URLMetadata;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.icons.IconCallback;
 import org.mozilla.gecko.icons.IconDescriptor;
 import org.mozilla.gecko.icons.IconRequestBuilder;
 import org.mozilla.gecko.icons.IconResponse;
 import org.mozilla.gecko.icons.Icons;
+import org.mozilla.gecko.mozglue.SafeIntent;
 import org.mozilla.gecko.reader.ReaderModeUtils;
 import org.mozilla.gecko.reader.ReadingListHelper;
 import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.widget.SiteLogins;
 
 import android.content.ContentResolver;
@@ -53,16 +54,17 @@ public class Tab {
     private Bitmap mFavicon;
     private String mFaviconUrl;
     private String mApplicationId; // Intended to be null after explicit user action.
 
     private IconRequestBuilder mIconRequestBuilder;
     private Future<IconResponse> mRunningIconRequest;
 
     private boolean mHasFeeds;
+    private SafeIntent mCustomTabIntent;
     private String mManifestUrl;
     private boolean mHasOpenSearch;
     private final SiteIdentity mSiteIdentity;
     private SiteLogins mSiteLogins;
     private BitmapDrawable mThumbnail;
     private volatile int mParentId;
     // Indicates the url was loaded from a source external to the app. This will be cleared
     // when the user explicitly loads a new url (e.g. clicking a link is not explicit).
@@ -292,16 +294,20 @@ public class Tab {
     public synchronized String getFaviconURL() {
         return mFaviconUrl;
     }
 
     public boolean hasFeeds() {
         return mHasFeeds;
     }
 
+    public SafeIntent getCustomTabIntent() {
+        return mCustomTabIntent;
+    }
+
     public String getManifestUrl() {
         return mManifestUrl;
     }
 
     public boolean hasOpenSearch() {
         return mHasOpenSearch;
     }
 
@@ -465,16 +471,20 @@ public class Tab {
         mFavicon = null;
         mFaviconUrl = null;
     }
 
     public void setHasFeeds(boolean hasFeeds) {
         mHasFeeds = hasFeeds;
     }
 
+    public void setCustomTabIntent(SafeIntent intent) {
+        mCustomTabIntent = intent;
+    }
+
     public void setManifestUrl(String manifestUrl) {
         mManifestUrl = manifestUrl;
     }
 
     public void setHasOpenSearch(boolean hasOpenSearch) {
         mHasOpenSearch = hasOpenSearch;
     }
 
--- a/mobile/android/base/java/org/mozilla/gecko/Tabs.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tabs.java
@@ -7,22 +7,26 @@ package org.mozilla.gecko;
 
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import android.app.Activity;
+import android.content.Intent;
 import android.content.SharedPreferences;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 
 import org.mozilla.gecko.annotation.JNITarget;
 import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.customtabs.CustomTabsActivity;
+import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.distribution.PartnerBrowserCustomizationsClient;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.mozglue.SafeIntent;
 import org.mozilla.gecko.notifications.WhatsNewReceiver;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.reader.ReaderModeUtils;
 import org.mozilla.gecko.util.BundleEventListener;
@@ -47,17 +51,19 @@ import android.support.v4.content.Contex
 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";
     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();
 
     // All writes to mSelectedTab must be synchronized on the Tabs instance.
     // In general, it's preferred to always use selectTab()).
@@ -306,16 +312,24 @@ public class Tabs implements BundleEvent
         final Tab tab = mTabs.get(id);
 
         // This avoids a NPE below, but callers need to be careful to
         // handle this case.
         if (tab == null || oldTab == tab) {
             return tab;
         }
 
+        if (oldTab != null && oldTab.getType() != tab.getType() &&
+                !currentActivityMatchesTab(tab)) {
+            // We're in the wrong activity for this kind of tab, so launch the correct one
+            // and then try again.
+            launchActivityForTab(tab);
+            return tab;
+        }
+
         mSelectedTab = tab;
         notifyListeners(tab, TabEvents.SELECTED);
 
         if (mLayerView != null) {
             mLayerView.setClearColor(getTabColor(tab));
         }
 
         if (oldTab != null) {
@@ -324,16 +338,66 @@ public class Tabs implements BundleEvent
 
         // Pass a message to Gecko to update tab state in BrowserApp.
         final GeckoBundle data = new GeckoBundle(1);
         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) {
+        final Activity currentActivity = GeckoActivityMonitor.getInstance().getCurrentActivity();
+
+        if (currentActivity == null) {
+            return false;
+        }
+        String currentActivityName = currentActivity.getClass().getName();
+        return currentActivityName.equals(getClassNameForTab(tab));
+    }
+
+    private void launchActivityForTab(Tab tab) {
+        final Intent intent;
+        switch (tab.getType()) {
+            case CUSTOMTAB:
+                if (tab.getCustomTabIntent() != null) {
+                    intent = tab.getCustomTabIntent().getUnsafe();
+                } else {
+                    intent = new Intent(Intent.ACTION_VIEW);
+                    intent.setData(Uri.parse(tab.getURL()));
+                }
+                break;
+            default:
+                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());
+        mAppContext.startActivity(intent);
+    }
+
+    /**
+     * Get the class name of the activity that should be displaying this tab.
+     */
+    private String getClassNameForTab(Tab tab) {
+        TabType type = tab.getType();
+
+        switch (type) {
+            case CUSTOMTAB:
+                return CustomTabsActivity.class.getName();
+            default:
+                return AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS;
+        }
+    }
+
     public synchronized boolean selectLastTab() {
         if (mOrder.isEmpty()) {
             return false;
         }
 
         selectTab(mOrder.get(mOrder.size() - 1).getId());
         return true;
     }
@@ -1036,16 +1100,23 @@ public class Tabs implements BundleEvent
             String tabUrl = (url != null && Uri.parse(url).getScheme() != null) ? url : null;
 
             // Add the new tab to the end of the tab order.
             final int tabIndex = NEW_LAST_INDEX;
 
             tabToSelect = addTab(tabId, tabUrl, external, parentId, url, isPrivate, tabIndex, type);
             tabToSelect.setDesktopMode(desktopMode);
             tabToSelect.setApplicationId(applicationId);
+            if (intent != null) {
+                if (customTab) {
+                    // The intent can contain all sorts of customisations, so we save it in case
+                    // we need to launch a new custom tab activity for this tab.
+                    tabToSelect.setCustomTabIntent(intent);
+                }
+            }
             if (isFirstShownAfterActivityUnhidden) {
                 // We just opened Firefox so we want to show
                 // the toolbar but not animate it to avoid jank.
                 tabToSelect.setShouldShowToolbarWithoutAnimationOnFirstSelection(true);
             }
         }
 
         EventDispatcher.getInstance().dispatch("Tab:Load", data);
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
@@ -76,22 +76,25 @@ public class CustomTabsActivity extends 
     @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 {
-            sendTelemetry();
             startIntent = new SafeIntent(getIntent());
             final String host = getReferrerHost();
             recordCustomTabUsage(host);
         }
 
+        if (!mIsRestoringActivity || !hasGeckoTab(startIntent)) {
+            sendTelemetry();
+        }
+
         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();
@@ -192,34 +195,37 @@ public class CustomTabsActivity extends 
 
     @Override
     protected void onDone() {
         finish();
     }
 
     @Override
     public void onTabChanged(Tab tab, TabEvents msg, String data) {
-        if (!Tabs.getInstance().isSelectedTab(tab)) {
+        if (!Tabs.getInstance().isSelectedTab(tab) ||
+                tab.getType() != Tab.TabType.CUSTOMTAB) {
             return;
         }
 
         if (msg == TabEvents.START
                 || msg == TabEvents.STOP
                 || msg == TabEvents.ADDED
                 || msg == TabEvents.LOAD_ERROR
                 || msg == TabEvents.LOADED
-                || msg == TabEvents.LOCATION_CHANGE) {
+                || msg == TabEvents.LOCATION_CHANGE
+                || msg == TabEvents.SELECTED) {
 
             updateProgress((tab.getState() == Tab.STATE_LOADING),
                     tab.getLoadProgress());
         }
 
         if (msg == TabEvents.LOCATION_CHANGE
                 || msg == TabEvents.SECURITY_CHANGE
-                || msg == TabEvents.TITLE) {
+                || msg == TabEvents.TITLE
+                || msg == TabEvents.SELECTED) {
             actionBarPresenter.update(tab);
         }
 
         updateMenuItemForward();
     }
 
     @Override
     protected void onSaveInstanceState(Bundle outState) {
--- a/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
@@ -437,17 +437,17 @@ public class MediaControlService extends
         final Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
         intent.setAction(action);
         return intent;
     }
 
     private PendingIntent createContentIntent(int tabId) {
         Intent intent = new Intent(getApplicationContext(), BrowserApp.class);
         intent.setAction(GeckoApp.ACTION_SWITCH_TAB);
-        intent.putExtra("TabId", tabId);
+        intent.putExtra(Tabs.INTENT_EXTRA_TAB_ID, tabId);
         return PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
     }
 
     private PendingIntent createDeleteIntent() {
         Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
         intent.setAction(ACTION_STOP);
         return  PendingIntent.getService(getApplicationContext(), 1, intent, 0);
     }