Bug 1351739 - Part 5 - Implement activity switching for web apps. r?sebastian,walkingice draft
authorJan Henning <jh+bugzilla@buttercookie.de>
Sun, 02 Apr 2017 18:26:45 +0200
changeset 569637 6c49e349f10443a8657c0e9834cf4ce7fa13d6a0
parent 569636 78baa9118e7e3d35fd0b85ace07dd79ac5a6cfb5
child 569638 566b6149b704c83f7042183bb923d1e7cd266694
push id56240
push usermozilla@buttercookie.de
push dateThu, 27 Apr 2017 18:44:39 +0000
reviewerssebastian, walkingice
bugs1351739, 1352997, 1351605
milestone55.0a1
Bug 1351739 - Part 5 - Implement activity switching for web apps. r?sebastian,walkingice Differences to custom tabs: - We don't have to store the full intent, just storing the manifest path is enough. - Akin to the LauncherActivity we have to route the request to the correct WebAppActivity instance depending on the manifest path. We also have to modify the intent handling when GeckoApp is starting up - the intent handling of the GeckoApp + BrowserApp combo requires "nulling" out (by setting it to ACTION_MAIN) the current intent if it's not a fresh intent (e.g. the activity is recreated after having been destroyed or relaunched from the task switcher). For web apps on the other hand we want to keep the intent around even in those cases, as it contains state we need even later on. Additionally, we want to make use of GeckoApp's startup code for either selecting the tab from the intent or loading a new tab. Therefore we save the launch intent and restore it once GeckoApp's onCreate() has run. Note that this solution is not entirely correct either, because with this each onCreate() call will open a new tab, even when this is not necessary when only the activity (but not Firefox and Gecko as a whole) had been destroyed. This behaviour will be fixed as part of bug 1352997. This approach is also a bit different than the one chosen in bug 1351605 for custom tabs, which was independently developed in parallel. Bug 1352997 will unify this, too. MozReview-Commit-ID: 94uZ3c8CUVD
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/webapps/WebAppActivity.java
--- a/mobile/android/base/java/org/mozilla/gecko/Tab.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tab.java
@@ -56,16 +56,17 @@ public class Tab {
     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 String mManifestPath;
     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).
     private final boolean mExternal;
@@ -302,16 +303,24 @@ public class Tab {
     public SafeIntent getCustomTabIntent() {
         return mCustomTabIntent;
     }
 
     public String getManifestUrl() {
         return mManifestUrl;
     }
 
+    /**
+     * @return If not empty, the path to a locally installed copy of the Progressive Web App
+     *         manifest file for this tab.
+     */
+    public String getManifestPath() {
+        return mManifestPath;
+    }
+
     public boolean hasOpenSearch() {
         return mHasOpenSearch;
     }
 
     public boolean hasLoadedFromCache() {
         return mLoadedFromCache;
     }
 
@@ -479,16 +488,20 @@ public class Tab {
     public void setCustomTabIntent(SafeIntent intent) {
         mCustomTabIntent = intent;
     }
 
     public void setManifestUrl(String manifestUrl) {
         mManifestUrl = manifestUrl;
     }
 
+    public void setManifestPath(String manifestPath) {
+        mManifestPath = manifestPath;
+    }
+
     public void setHasOpenSearch(boolean hasOpenSearch) {
         mHasOpenSearch = hasOpenSearch;
     }
 
     public void setLoadedFromCache(boolean loadedFromCache) {
         mLoadedFromCache = loadedFromCache;
     }
 
--- a/mobile/android/base/java/org/mozilla/gecko/Tabs.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tabs.java
@@ -1,44 +1,51 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 java.io.File;
+import java.io.IOException;
 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.json.JSONException;
+import org.json.JSONObject;
 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;
 import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.FileUtils;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.JavaUtil;
 import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.webapps.WebAppActivity;
+import org.mozilla.gecko.webapps.WebAppIndexer;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.OnAccountsUpdateListener;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.ContentObserver;
 import android.database.sqlite.SQLiteException;
@@ -362,37 +369,62 @@ public class Tabs implements BundleEvent
             case CUSTOMTAB:
                 if (tab.getCustomTabIntent() != null) {
                     intent = tab.getCustomTabIntent().getUnsafe();
                 } else {
                     intent = new Intent(Intent.ACTION_VIEW);
                     intent.setData(Uri.parse(tab.getURL()));
                 }
                 break;
+            case WEBAPP:
+                intent = new Intent(GeckoApp.ACTION_WEBAPP);
+                final String manifestPath = tab.getManifestPath();
+                try {
+                    intent.setData(getStartUriFromManifest(manifestPath));
+                } catch (IOException | JSONException e) {
+                    Log.e(LOGTAG, "Failed to get start URI from manifest", e);
+                    intent.setData(Uri.parse(tab.getURL()));
+                }
+                intent.putExtra(WebAppActivity.MANIFEST_PATH, manifestPath);
+                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);
     }
 
+    // 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);
+        final JSONObject manifestField = manifest.getJSONObject("manifest");
+
+        return Uri.parse(manifestField.getString("start_url"));
+    }
+
     /**
      * 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();
+            case WEBAPP:
+                final int index =  WebAppIndexer.getInstance().getIndexForManifest(
+                        tab.getManifestPath(), mAppContext);
+                return WebAppIndexer.WEBAPP_CLASS + index;
             default:
                 return AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS;
         }
     }
 
     public synchronized boolean selectLastTab() {
         if (mOrder.isEmpty()) {
             return false;
@@ -1106,16 +1138,19 @@ public class Tabs implements BundleEvent
             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 (intent.hasExtra(WebAppActivity.MANIFEST_PATH)) {
+                    tabToSelect.setManifestPath(intent.getStringExtra(WebAppActivity.MANIFEST_PATH));
+                }
             }
             if (isFirstShownAfterActivityUnhidden) {
                 // We just opened Firefox so we want to show
                 // the toolbar but not animate it to avoid jank.
                 tabToSelect.setShouldShowToolbarWithoutAnimationOnFirstSelection(true);
             }
         }
 
--- a/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
@@ -38,39 +38,47 @@ import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.util.ColorUtil;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.FileUtils;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.widget.AnchoredPopup;
 
+import static org.mozilla.gecko.Tabs.TabEvents;
+
 public class WebAppActivity extends GeckoApp {
+    private static final String LOGTAG = "WebAppActivity";
+
     public static final String MANIFEST_PATH = "MANIFEST_PATH";
 
-    private static final String LOGTAG = "WebAppActivity";
-
     private TextView mUrlView;
     private View doorhangerOverlay;
 
     private String mManifestPath;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
+        Intent savedIntent = getIntent();
         super.onCreate(savedInstanceState);
-
-        Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "webapp");
+        // GeckoApp's default behaviour is to reset the intent if we've got any
+        // savedInstanceState, which we don't want here.
+        setIntent(savedIntent);
 
         if (savedInstanceState != null) {
             mManifestPath = savedInstanceState.getString(WebAppActivity.MANIFEST_PATH, null);
         } else {
             mManifestPath = getIntent().getStringExtra(WebAppActivity.MANIFEST_PATH);
         }
         loadManifest(mManifestPath);
 
+        if (!mIsRestoringActivity || !hasGeckoTab(new SafeIntent(getIntent()))) {
+            Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "customtab");
+        }
+
         final Toolbar toolbar = (Toolbar) findViewById(R.id.actionbar);
         setSupportActionBar(toolbar);
 
         final ProgressBar progressBar = (ProgressBar) findViewById(R.id.page_progress);
         progressBar.setVisibility(View.GONE);
 
         final ActionBar actionBar = getSupportActionBar();
         actionBar.setCustomView(R.layout.webapps_action_bar_custom_view);
@@ -112,21 +120,23 @@ public class WebAppActivity extends Geck
             case "Website:AppLeft":
                 getSupportActionBar().show();
                 break;
         }
     }
 
     @Override
     public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
-        if (!Tabs.getInstance().isSelectedTab(tab)) {
+        if (tab == null || !Tabs.getInstance().isSelectedTab(tab) ||
+                tab.getType() != Tab.TabType.WEBAPP) {
             return;
         }
 
-        if (msg == Tabs.TabEvents.LOCATION_CHANGE) {
+        if (msg == TabEvents.LOCATION_CHANGE ||
+                msg == TabEvents.SELECTED) {
             mUrlView.setText(tab.getURL());
         }
     }
 
     @Override
     protected void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
 
@@ -150,30 +160,36 @@ public class WebAppActivity extends Geck
 
     /**
      * In case this activity is reused (the user has opened > 10 current web apps)
      * we check that app launched is still within the same host as the
      * shortcut has set, if not we reload the homescreens url
      */
     @Override
     protected void onNewIntent(Intent externalIntent) {
-
-        restoreLastSelectedTab();
+        super.onNewIntent(externalIntent);
 
         final SafeIntent intent = new SafeIntent(externalIntent);
-        final String launchUrl = intent.getDataString();
-        final String currentUrl = Tabs.getInstance().getSelectedTab().getURL();
-        final boolean isSameDomain = Uri.parse(currentUrl).getHost()
-                .equals(Uri.parse(launchUrl).getHost());
+
+        if (hasGeckoTab(intent)) {
+            loadManifest(intent.getStringExtra(WebAppActivity.MANIFEST_PATH));
+        } else {
+            restoreLastSelectedTab();
 
-        if (!isSameDomain) {
-            Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "webapp");
-            mManifestPath = externalIntent.getStringExtra(WebAppActivity.MANIFEST_PATH);
-            loadManifest(mManifestPath);
-            Tabs.getInstance().loadUrl(launchUrl);
+            final String launchUrl = intent.getDataString();
+            final String currentUrl = Tabs.getInstance().getSelectedTab().getURL();
+            final boolean isSameDomain = Uri.parse(currentUrl).getHost()
+                    .equals(Uri.parse(launchUrl).getHost());
+
+            if (!isSameDomain) {
+                Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "webapp");
+                mManifestPath = externalIntent.getStringExtra(WebAppActivity.MANIFEST_PATH);
+                loadManifest(mManifestPath);
+                Tabs.getInstance().loadUrl(launchUrl);
+            }
         }
     }
 
     private void loadManifest(String manifestPath) {
         if (manifestPath == null) {
             Log.e(LOGTAG, "Missing manifest");
             return;
         }