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

--HG--
extra : rebase_source : 188fd2275083ddb982af806d4660c02caab85bee
This commit is contained in:
Jan Henning 2017-04-08 19:19:32 +02:00
Родитель 6dcea3ee36
Коммит a31794fbba
6 изменённых файлов: 269 добавлений и 58 удалений

Просмотреть файл

@ -1710,10 +1710,20 @@ public abstract class GeckoApp
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);
@ -1853,6 +1863,10 @@ public abstract class GeckoApp
}
}
protected void onTabOpenFromIntent(Tab tab) { }
protected void onTabSelectFromIntent(Tab tab) { }
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
public void onGlobalLayout() {
@ -2320,14 +2334,32 @@ public abstract class GeckoApp
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);
}
}
/**

Просмотреть файл

@ -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();
}
}

Просмотреть файл

@ -64,6 +64,7 @@ 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.
@ -353,7 +354,7 @@ public class Tabs implements BundleEventListener {
/**
* 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) {
@ -394,6 +395,7 @@ public class Tabs implements BundleEventListener {
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);
}

Просмотреть файл

@ -32,8 +32,8 @@ 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;
@ -55,9 +55,9 @@ 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;
@ -67,28 +67,13 @@ public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedLi
// 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();
@ -101,14 +86,36 @@ public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedLi
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");
@ -122,8 +129,6 @@ public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedLi
if (IntentUtil.hasShareItem(startIntent)) {
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "customtab-hasShareItem");
}
}
private void recordCustomTabUsage(final String host) {
@ -138,7 +143,7 @@ public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedLi
}
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;
@ -153,7 +158,7 @@ public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedLi
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();
}
@ -163,11 +168,12 @@ public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedLi
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;
}
}
@ -193,11 +199,6 @@ public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedLi
return doorhangerOverlay;
}
@Override
protected void onDone() {
finish();
}
@Override
public void onTabChanged(Tab tab, TabEvents msg, String data) {
if (!Tabs.getInstance().isSelectedTab(tab) ||
@ -227,12 +228,6 @@ public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedLi
updateMenuItemForward();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(SAVED_START_INTENT, startIntent.getUnsafe());
}
@Override
public void onResume() {
super.onResume();
@ -253,10 +248,11 @@ public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedLi
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
@ -349,10 +345,10 @@ public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedLi
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();
}
});
}
@ -378,6 +374,7 @@ public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedLi
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() {
@ -388,8 +385,8 @@ public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedLi
});
// 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;
@ -398,7 +395,7 @@ public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedLi
}
// 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));
}
@ -499,7 +496,7 @@ public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedLi
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);
}

Просмотреть файл

@ -821,6 +821,7 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'search/SearchEngineManager.java',
'SessionParser.java',
'SharedPreferencesHelper.java',
'SingleTabActivity.java',
'SiteIdentity.java',
'SnackbarBuilder.java',
'SuggestClient.java',

Просмотреть файл

@ -64,9 +64,9 @@ public class TestCustomTabsActivity {
@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());
@ -79,9 +79,9 @@ public class TestCustomTabsActivity {
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));
@ -94,10 +94,10 @@ public class TestCustomTabsActivity {
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());
}