Bug 1289242 - Implement ASOpenURLDelegate to handle URL loading from the detail activity r?sebastian draft
authorAndrzej Hunt <ahunt@mozilla.com>
Mon, 01 Aug 2016 10:29:04 -0700
changeset 399216 12a4de303876348478f7dc0fbff0482a6a24a421
parent 398725 47550e79a9c51b0cf97f55ab9edd7b9b7f302cb3
child 399217 5352c8aadc736de1cfdb71dc1f611f2b310e6fd3
push id25764
push userahunt@mozilla.com
push dateWed, 10 Aug 2016 17:18:33 +0000
reviewerssebastian
bugs1289242
milestone51.0a1
Bug 1289242 - Implement ASOpenURLDelegate to handle URL loading from the detail activity r?sebastian MozReview-Commit-ID: 8oMkkBDLLNA
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/home/activitystream/ASOpenURLDelegate.java
mobile/android/base/moz.build
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -10,17 +10,16 @@ import android.app.DownloadManager;
 import android.os.Environment;
 import android.support.annotation.CheckResult;
 import android.support.annotation.NonNull;
 
 import android.widget.VideoView;
 import android.graphics.Rect;
 
 import org.json.JSONArray;
-import org.mozilla.gecko.activitystream.ActivityStream;
 import org.mozilla.gecko.adjust.AdjustHelperInterface;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
 import org.mozilla.gecko.Tabs.TabEvents;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.cleanup.FileCleanupController;
@@ -50,16 +49,17 @@ import org.mozilla.gecko.home.HomeConfig
 import org.mozilla.gecko.home.HomeConfig.PanelType;
 import org.mozilla.gecko.home.HomeConfigPrefsBackend;
 import org.mozilla.gecko.home.HomeFragment;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.HomePanelsManager;
 import org.mozilla.gecko.home.HomeScreen;
 import org.mozilla.gecko.home.SearchEngine;
+import org.mozilla.gecko.home.activitystream.ASOpenURLDelegate;
 import org.mozilla.gecko.javaaddons.JavaAddonManager;
 import org.mozilla.gecko.media.AudioFocusAgent;
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuItem;
 import org.mozilla.gecko.mozglue.SafeIntent;
 import org.mozilla.gecko.notifications.NotificationClient;
 import org.mozilla.gecko.notifications.ServiceNotificationClient;
 import org.mozilla.gecko.overlays.ui.ShareDialog;
@@ -217,16 +217,18 @@ public class BrowserApp extends GeckoApp
     private static final int ACTIVITY_REQUEST_PREFERENCES = 1001;
     private static final int ACTIVITY_REQUEST_TAB_QUEUE = 2001;
     public static final int ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK = 3001;
     public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS = 3002;
     public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE = 3003;
     public static final int ACTIVITY_REQUEST_TRIPLE_READERVIEW = 4001;
     public static final int ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK = 4002;
     public static final int ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE = 4003;
+    public static final int ACTIVITY_REQUEST_AS_DETAIL = 5001;
+    public static final int ACTIVITY_RESULT_AS_DETAIL_OPEN = 5002;
 
     public static final String ACTION_VIEW_MULTIPLE = AppConstants.ANDROID_PACKAGE_NAME + ".action.VIEW_MULTIPLE";
 
     @RobocopTarget
     public static final String EXTRA_SKIP_STARTPANE = "skipstartpane";
     private static final String EOL_NOTIFIED = "eol_notified";
 
     private BrowserSearch mBrowserSearch;
@@ -316,17 +318,18 @@ public class BrowserApp extends GeckoApp
     private final List<BrowserAppDelegate> delegates = Collections.unmodifiableList(Arrays.asList(
             (BrowserAppDelegate) new AddToHomeScreenPromotion(),
             (BrowserAppDelegate) new ScreenshotDelegate(),
             (BrowserAppDelegate) new BookmarkStateChangeDelegate(),
             (BrowserAppDelegate) new ReaderViewBookmarkPromotion(),
             (BrowserAppDelegate) new ContentNotificationsDelegate(),
             (BrowserAppDelegate) new PostUpdateHandler(),
             new TelemetryCorePingDelegate(),
-            new OfflineTabStatusDelegate()
+            new OfflineTabStatusDelegate(),
+            new ASOpenURLDelegate()
     ));
 
     @NonNull
     private SearchEngineManager mSearchEngineManager; // Contains reference to Context - DO NOT LEAK!
 
     private boolean mHasResumed;
 
     @Override
@@ -2706,17 +2709,17 @@ public class BrowserApp extends GeckoApp
 
         // Show the toolbar before hiding about:home so the
         // onMetricsChanged callback still works.
         if (mDynamicToolbar.isEnabled()) {
             mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
         }
 
         if (mHomeScreen == null) {
-            if (ActivityStream.isEnabled(this)) {
+            if (org.mozilla.gecko.activitystream.ActivityStream.isEnabled(this)) {
                 final ViewStub asStub = (ViewStub) findViewById(R.id.activity_stream_stub);
                 mHomeScreen = (HomeScreen) asStub.inflate();
             } else {
                 final ViewStub homePagerStub = (ViewStub) findViewById(R.id.home_pager_stub);
                 mHomeScreen = (HomeScreen) homePagerStub.inflate();
 
                 // For now these listeners are HomePager specific. In future we might want
                 // to have a more abstracted data storage, with one Bundle containing all
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ASOpenURLDelegate.java
@@ -0,0 +1,165 @@
+/* -*- 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.home.activitystream;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.support.annotation.UiThread;
+import android.util.Log;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.delegates.BrowserAppDelegate;
+import org.mozilla.gecko.home.HomePager;
+import org.mozilla.gecko.util.StringUtils;
+
+import java.util.EnumSet;
+
+/**
+ * Delegate handling url-opening for the Activity Stream Detail activity.
+ *
+ * Since the Activity Stream detail screens are running in a separate activity, we cannot
+ * open URLs in BrowserApp directly. Instead we either open them when the activity returns (in the case
+ * of directly clicking on a URL) via onActivityResult, or by storing them in a queue (in the case
+ * of URLs opened in the background).
+ *
+ * Persisting background URLs is important because it's possible for the app to be killed between
+ * "opening" the URL (e.g. "open in new tab" from the context menu, after which we remain in
+ * the detail activity) and resuming BrowserApp (the app could be killed in between use of the context
+ * menu and resuming BrowserApp).
+ */
+public class ASOpenURLDelegate extends BrowserAppDelegate {
+    private static final String LOGTAG = StringUtils.safeSubstring(ASOpenURLDelegate.class.getSimpleName(), 0, 23);
+
+    // Hardcode the name so that we can survive future refactorings. (This is highly unlikely, but
+    // it's still possible that URLs are queued, the app is killed, updated to a new version, and
+    // then reopened. Using a fixed name here avoids us having to cope with a class name/package change
+    // if we were to use the classname instead.)
+    private static final String PREFS_FILE_BACKGROUND = "org.mozilla.gecko.home.activitystream.ASOpenURLDelegate";
+
+    private static final String PREFS_KEY_BACKGROUND_LIST = "key_background_list";
+
+    // This implementation assumes that background flags only contain the PRIVATE flag. We use an assert in onCreate
+    // to protect against changes in this assumption. We could potentially serialise all the flags
+    // for this case, but that is brittle and not ideal (see the same reasoning as for hardcoding
+    // PREFS_FILE_BACKGROUND).
+    private static final String KEY_URL = "url";
+    private static final String KEY_PRIVATE = "private";
+    // We are however able to serialise the flags for immediate opening since we are explicitly
+    // imediately open BrowserApp (and we don't persist the url/flags in that case).
+    private static final String KEY_FLAGS = "flags";
+
+    public static void openURL(Activity activity, String url, EnumSet<HomePager.OnUrlOpenListener.Flags> flags) {
+        Intent data = new Intent();
+        data.putExtra(KEY_URL, url);
+
+        Bundle flagsBundle = new Bundle();
+        flagsBundle.putSerializable(KEY_FLAGS, flags);
+        data.putExtra(KEY_FLAGS, flagsBundle);
+
+        activity.setResult(BrowserApp.ACTIVITY_RESULT_AS_DETAIL_OPEN, data);
+        activity.finish();
+    }
+
+    @Override
+    public void onActivityResult(BrowserApp browserApp, int requestCode, int resultCode,
+                                 Intent data) {
+        if (requestCode == BrowserApp.ACTIVITY_REQUEST_AS_DETAIL &&
+            resultCode == BrowserApp.ACTIVITY_RESULT_AS_DETAIL_OPEN) {
+            final String url = data.getStringExtra(KEY_URL);
+            final Bundle flagsBundle = data.getBundleExtra(KEY_FLAGS);
+            final EnumSet<HomePager.OnUrlOpenListener.Flags> flags = (EnumSet<HomePager.OnUrlOpenListener.Flags>) flagsBundle.getSerializable(KEY_FLAGS);
+
+            browserApp.onUrlOpen(url, flags);
+        }
+    }
+
+    @Override
+    public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
+        // Early return: do nothing if AS is disabled
+        if (!org.mozilla.gecko.activitystream.ActivityStream.isEnabled(browserApp)) {
+            return;
+        }
+
+        super.onCreate(browserApp, savedInstanceState);
+
+        // Assert that no new flags have been added to OnUrlOpenInBackgroundListener: we currently
+        // assume there is only the PRIVATE flag (to open new background tabs as a private tab).
+        // If/when new flags are added, these need to be explicitly handled in  in openInBackground()
+        // and onResume.
+        HomePager.OnUrlOpenInBackgroundListener.Flags[] flags = HomePager.OnUrlOpenInBackgroundListener.Flags.values();
+        if (flags.length != 1 ||
+                flags[0] != HomePager.OnUrlOpenInBackgroundListener.Flags.PRIVATE) {
+            throw new IllegalStateException("ASOpenURLDelegate must be updated to handle new flags in OnUrlOpenInBackgroundListener");
+        }
+    }
+
+    /**
+     * Queue a URL to be opened when BrowserApp resumes.
+     *
+     * Matches the API of onURLOpenedInBackground. Must be run on the UI thread.
+     */
+    @UiThread
+    public static void openInBackground(final Context context, final String url, EnumSet<HomePager.OnUrlOpenInBackgroundListener.Flags> flags) {
+        final SharedPreferences prefs = context.getSharedPreferences(PREFS_FILE_BACKGROUND, Context.MODE_PRIVATE);
+
+        try {
+            final JSONArray list;
+
+            if (prefs.contains(PREFS_KEY_BACKGROUND_LIST)) {
+                final String listString = prefs.getString(PREFS_KEY_BACKGROUND_LIST, "");
+                list = new JSONArray(listString);
+            } else {
+                list = new JSONArray();
+            }
+
+            JSONObject item = new JSONObject();
+            item.put(KEY_URL, url);
+            item.put(KEY_PRIVATE, flags.contains(HomePager.OnUrlOpenInBackgroundListener.Flags.PRIVATE));
+
+            list.put(item);
+
+            prefs.edit().putString(PREFS_KEY_BACKGROUND_LIST, list.toString()).apply();
+        } catch (JSONException e) {
+            Log.e(LOGTAG, "Unable to persist URL to open in background", e);
+        }
+    }
+
+
+    @Override
+    public void onResume(BrowserApp browserApp) {
+        // Early return: do nothing if AS is disabled
+        if (!org.mozilla.gecko.activitystream.ActivityStream.isEnabled(browserApp)) {
+            return;
+        }
+
+        final SharedPreferences prefs = browserApp.getSharedPreferences(PREFS_FILE_BACKGROUND, Context.MODE_PRIVATE);
+
+        try {
+            if (prefs.contains(PREFS_KEY_BACKGROUND_LIST)) {
+                final String listString = prefs.getString(PREFS_KEY_BACKGROUND_LIST, "");
+                JSONArray list = new JSONArray(listString);
+
+                final int n = list.length();
+                for (int i = 0; i < n; i++) {
+                    JSONObject item = list.getJSONObject(i);
+                    boolean isPrivate = item.getBoolean(KEY_PRIVATE);
+                    EnumSet<HomePager.OnUrlOpenInBackgroundListener.Flags> flags = isPrivate ? EnumSet.of(HomePager.OnUrlOpenInBackgroundListener.Flags.PRIVATE) :
+                            EnumSet.noneOf(HomePager.OnUrlOpenInBackgroundListener.Flags.class);
+                    browserApp.onUrlOpenInBackground(item.getString("url"), flags);
+                }
+
+                prefs.edit().remove(PREFS_KEY_BACKGROUND_LIST).apply();
+            }
+        } catch (JSONException e) {
+            Log.e(LOGTAG, "Unable to open persisted background tabs", e);
+        }
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -434,16 +434,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'GeckoProfilesProvider.java',
     'GeckoUpdateReceiver.java',
     'GlobalHistory.java',
     'GuestSession.java',
     'health/HealthRecorder.java',
     'health/SessionInformation.java',
     'health/StubbedHealthRecorder.java',
     'home/activitystream/ActivityStream.java',
+    'home/activitystream/ASOpenURLDelegate.java',
     'home/BookmarkFolderView.java',
     'home/BookmarkScreenshotRow.java',
     'home/BookmarksListAdapter.java',
     'home/BookmarksListView.java',
     'home/BookmarksPanel.java',
     'home/BrowserSearch.java',
     'home/ClientsAdapter.java',
     'home/CombinedHistoryAdapter.java',