Bug 1277467 - Add OfflineTabStatusDelegate for displaying tab-is-offline notifiations when appropriate r=sebastian draft
authorGrigory Kruglov <gkruglov@mozilla.com>
Fri, 24 Jun 2016 16:56:04 -0700
changeset 381238 457a42ebba5cfc32ab7dabce0a8a11d6511b9c08
parent 380622 9c19cd72e0dc19d5c80bb1bf7fd2177c14862056
child 523932 786615874d65d487c7a634778edc5bd9f3b10468
push id21444
push usergkruglov@mozilla.com
push dateFri, 24 Jun 2016 23:56:43 +0000
reviewerssebastian
bugs1277467
milestone50.0a1
Bug 1277467 - Add OfflineTabStatusDelegate for displaying tab-is-offline notifiations when appropriate r=sebastian - Use Content:PageShow event to inform Tabs that they were actively loaded from cache - Move offline notification logic away from browser.js and into a delegate, which displays notifications when tab in question is user-visible MozReview-Commit-ID: 2qCACHyWOlp
mobile/android/base/java/org/mozilla/gecko/BrowserApp.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/TelemetryContract.java
mobile/android/base/java/org/mozilla/gecko/delegates/OfflineTabStatusDelegate.java
mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/moz.build
mobile/android/base/strings.xml.in
mobile/android/chrome/content/browser.js
mobile/android/locales/en-US/chrome/browser.properties
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -19,16 +19,17 @@ import org.mozilla.gecko.DynamicToolbar.
 import org.mozilla.gecko.Tabs.TabEvents;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.cleanup.FileCleanupController;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.SuggestedSites;
 import org.mozilla.gecko.delegates.BrowserAppDelegate;
+import org.mozilla.gecko.delegates.OfflineTabStatusDelegate;
 import org.mozilla.gecko.delegates.ScreenshotDelegate;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.distribution.DistributionStoreCallback;
 import org.mozilla.gecko.dlc.DownloadContentService;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
 import org.mozilla.gecko.feeds.ContentNotificationsDelegate;
@@ -305,17 +306,18 @@ public class BrowserApp extends GeckoApp
     private final DynamicToolbar mDynamicToolbar = new DynamicToolbar();
 
     private final List<BrowserAppDelegate> delegates = Collections.unmodifiableList(Arrays.asList(
             (BrowserAppDelegate) new AddToHomeScreenPromotion(),
             (BrowserAppDelegate) new ScreenshotDelegate(),
             (BrowserAppDelegate) new BookmarkStateChangeDelegate(),
             (BrowserAppDelegate) new ReaderViewBookmarkPromotion(),
             (BrowserAppDelegate) new ContentNotificationsDelegate(),
-            new TelemetryCorePingDelegate()
+            new TelemetryCorePingDelegate(),
+            new OfflineTabStatusDelegate()
     ));
 
     @NonNull
     private SearchEngineManager mSearchEngineManager; // Contains reference to Context - DO NOT LEAK!
 
     private boolean mHasResumed;
 
     @Override
--- a/mobile/android/base/java/org/mozilla/gecko/Tab.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tab.java
@@ -99,16 +99,19 @@ public class Tab {
     private int mHistoryIndex;
     private int mHistorySize;
     private boolean mCanDoBack;
     private boolean mCanDoForward;
 
     private boolean mIsEditing;
     private final TabEditingState mEditingState = new TabEditingState();
 
+    // Will be true when tab is loaded from cache while device was offline.
+    private boolean mLoadedFromCache;
+
     public static final int STATE_DELAYED = 0;
     public static final int STATE_LOADING = 1;
     public static final int STATE_SUCCESS = 2;
     public static final int STATE_ERROR = 3;
 
     public static final int LOAD_PROGRESS_INIT = 10;
     public static final int LOAD_PROGRESS_START = 20;
     public static final int LOAD_PROGRESS_LOCATION_CHANGE = 60;
@@ -296,16 +299,20 @@ public class Tab {
     public boolean hasFeeds() {
         return mHasFeeds;
     }
 
     public boolean hasOpenSearch() {
         return mHasOpenSearch;
     }
 
+    public boolean hasLoadedFromCache() {
+        return mLoadedFromCache;
+    }
+
     public SiteIdentity getSiteIdentity() {
         return mSiteIdentity;
     }
 
     public SiteLogins getSiteLogins() {
         return mSiteLogins;
     }
 
@@ -524,16 +531,20 @@ public class Tab {
     public void setHasFeeds(boolean hasFeeds) {
         mHasFeeds = hasFeeds;
     }
 
     public void setHasOpenSearch(boolean hasOpenSearch) {
         mHasOpenSearch = hasOpenSearch;
     }
 
+    public void setLoadedFromCache(boolean loadedFromCache) {
+        mLoadedFromCache = loadedFromCache;
+    }
+
     public void updateIdentityData(JSONObject identityData) {
         mSiteIdentity.update(identityData);
     }
 
     public void setLoginInsecure(boolean isInsecure) {
         mSiteIdentity.setLoginInsecure(isInsecure);
     }
 
--- a/mobile/android/base/java/org/mozilla/gecko/Tabs.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tabs.java
@@ -100,16 +100,17 @@ public class Tabs implements GeckoEventL
         }
     };
 
     private Tabs() {
         EventDispatcher.getInstance().registerGeckoThreadListener(this,
             "Tab:Added",
             "Tab:Close",
             "Tab:Select",
+            "Tab:LoadedFromCache",
             "Content:LocationChange",
             "Content:LoginInsecure",
             "Content:SecurityChange",
             "Content:StateChange",
             "Content:LoadError",
             "Content:PageShow",
             "DOMContentLoaded",
             "DOMTitleChanged",
@@ -504,18 +505,19 @@ public class Tabs implements GeckoEventL
                         tab.handleDocumentStop(message.getBoolean("success"));
                         notifyListeners(tab, Tabs.TabEvents.STOP);
                     }
                 }
             } else if (event.equals("Content:LoadError")) {
                 tab.handleContentLoaded();
                 notifyListeners(tab, Tabs.TabEvents.LOAD_ERROR);
             } else if (event.equals("Content:PageShow")) {
+                tab.setLoadedFromCache(message.getBoolean("fromCache"));
+                tab.updateUserRequested(message.getString("userRequested"));
                 notifyListeners(tab, TabEvents.PAGE_SHOW);
-                tab.updateUserRequested(message.getString("userRequested"));
             } else if (event.equals("DOMContentLoaded")) {
                 tab.handleContentLoaded();
                 String backgroundColor = message.getString("bgColor");
                 if (backgroundColor != null) {
                     tab.setBackgroundColor(backgroundColor);
                 } else {
                     // Default to white if no color is given
                     tab.setBackgroundColor(Color.WHITE);
--- a/mobile/android/base/java/org/mozilla/gecko/TelemetryContract.java
+++ b/mobile/android/base/java/org/mozilla/gecko/TelemetryContract.java
@@ -99,16 +99,19 @@ public interface TelemetryContract {
         UNDO("undo.1"),
 
         // Unpinning an item.
         UNPIN("unpin.1"),
 
         // Stop holding a resource (reader, bookmark, etc) for viewing later.
         UNSAVE("unsave.1"),
 
+        // When the user performs actions on the in-content network error page.
+        NETERROR("neterror.1"),
+
         // VALUES BELOW THIS LINE ARE EXCLUSIVE TO TESTING.
         _TEST1("_test_event_1.1"),
         _TEST2("_test_event_2.1"),
         _TEST3("_test_event_3.1"),
         _TEST4("_test_event_4.1"),
         ;
 
         private final String string;
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/delegates/OfflineTabStatusDelegate.java
@@ -0,0 +1,110 @@
+/* -*- 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.delegates;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.annotation.CallSuper;
+import android.support.design.widget.Snackbar;
+import android.support.v4.content.ContextCompat;
+
+import org.mozilla.gecko.AboutPages;
+import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.SnackbarHelper;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.WeakHashMap;
+
+/**
+ * Displays "Showing offline version" message when tabs are loaded from cache while offline.
+ */
+public class OfflineTabStatusDelegate extends TabsTrayVisibilityAwareDelegate implements Tabs.OnTabsChangedListener {
+    private WeakReference<Activity> activityReference;
+    private WeakHashMap<Tab, Void> tabsQueuedForOfflineSnackbar = new WeakHashMap<>();
+
+    @CallSuper
+    @Override
+    public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
+        super.onCreate(browserApp, savedInstanceState);
+        activityReference = new WeakReference<Activity>(browserApp);
+    }
+
+    @Override
+    public void onResume(BrowserApp browserApp) {
+        Tabs.registerOnTabsChangedListener(this);
+    }
+
+    @Override
+    public void onPause(BrowserApp browserApp) {
+        Tabs.unregisterOnTabsChangedListener(this);
+    }
+
+    public void onTabChanged(final Tab tab, Tabs.TabEvents event, String data) {
+        if (tab == null) {
+            return;
+        }
+
+        // Ignore tabs loaded regularly.
+        if (!tab.hasLoadedFromCache()) {
+            return;
+        }
+
+        // Ignore tabs displaying about pages
+        if (AboutPages.isAboutPage(tab.getURL())) {
+            return;
+        }
+
+        switch (event) {
+            // Show offline notification if tab is visible, or queue it for display later.
+            case PAGE_SHOW:
+                if (!isTabsTrayVisible() && Tabs.getInstance().isSelectedTab(tab)) {
+                    showLoadedOfflineSnackbar(activityReference.get());
+                } else {
+                    tabsQueuedForOfflineSnackbar.put(tab, null);
+                }
+                break;
+            // When tab is selected and offline notification was queued, display it if possible.
+            // SELECTED event might also fire when we're on a TabStrip, so check first.
+            case SELECTED:
+                if (isTabsTrayVisible()) {
+                    break;
+                }
+                if (tabsQueuedForOfflineSnackbar.containsKey(tab)) {
+                    showLoadedOfflineSnackbar(activityReference.get());
+                    tabsQueuedForOfflineSnackbar.remove(tab);
+                }
+                break;
+        }
+    }
+
+    /**
+     * Displays the notification snackbar and logs a telemetry event.
+     *
+     * @param activity which will be used for displaying the snackbar.
+     */
+    private static void showLoadedOfflineSnackbar(final Activity activity) {
+        if (activity == null) {
+            return;
+        }
+
+        Telemetry.sendUIEvent(TelemetryContract.Event.NETERROR, TelemetryContract.Method.TOAST, "usecache");
+
+        SnackbarHelper.showSnackbarWithActionAndColors(
+                activity,
+                activity.getResources().getString(R.string.tab_offline_version),
+                Snackbar.LENGTH_INDEFINITE,
+                null, null, null,
+                ContextCompat.getColor(activity, R.color.link_blue),
+                null
+        );
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java
+++ b/mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java
@@ -4,42 +4,43 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.promotion;
 
 import android.app.Activity;
 import android.content.Context;
 import android.database.Cursor;
 import android.os.Bundle;
+import android.support.annotation.CallSuper;
 import android.util.Log;
 
 import com.keepsafe.switchboard.SwitchBoard;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.AboutPages;
 import org.mozilla.gecko.BrowserApp;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.UrlAnnotations;
-import org.mozilla.gecko.delegates.ForegroundAwareDelegate;
+import org.mozilla.gecko.delegates.TabsTrayVisibilityAwareDelegate;
 import org.mozilla.gecko.util.Experiments;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import java.lang.ref.WeakReference;
 
 import ch.boye.httpclientandroidlib.util.TextUtils;
 
 /**
  * Promote "Add to home screen" if user visits website often.
  */
-public class AddToHomeScreenPromotion extends ForegroundAwareDelegate implements Tabs.OnTabsChangedListener {
+public class AddToHomeScreenPromotion extends TabsTrayVisibilityAwareDelegate implements Tabs.OnTabsChangedListener {
     private static class URLHistory {
         public final long visits;
         public final long lastVisit;
 
         private URLHistory(long visits, long lastVisit) {
             this.visits = visits;
             this.lastVisit = lastVisit;
         }
@@ -52,16 +53,17 @@ public class AddToHomeScreenPromotion ex
     private static final String EXPERIMENT_LAST_VISIT_MAXIMUM_AGE = "lastVisitMaximumAgeMs";
 
     private WeakReference<Activity> activityReference;
     private boolean isEnabled;
     private int minimumVisits;
     private int lastVisitMinimumAgeMs;
     private int lastVisitMaximumAgeMs;
 
+    @CallSuper
     @Override
     public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
         super.onCreate(browserApp, savedInstanceState);
         activityReference = new WeakReference<Activity>(browserApp);
 
         initializeExperiment(browserApp);
     }
 
@@ -122,17 +124,17 @@ public class AddToHomeScreenPromotion ex
             return;
         }
 
         if (tab.isPrivate()) {
             // Never show the prompt for private browsing tabs.
             return;
         }
 
-        if (!isInForeground) {
+        if (isTabsTrayVisible()) {
             // We only want to show this prompt if this tab is in the foreground and not on top
             // of the tabs tray.
             return;
         }
 
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -42,16 +42,19 @@
 
 <!-- Localization note: These are used as the titles of different pages on the home screen.
      They are automatically converted to all caps by the Android platform. -->
 <!ENTITY  bookmarks_title "Bookmarks">
 <!ENTITY  history_title "History">
 
 <!ENTITY  switch_to_tab "Switch to tab">
 
+<!-- Localization note: Shown in a snackbar when tab is loaded from cache while device was offline. -->
+<!ENTITY  tab_offline_version "Showing offline version">
+
 <!ENTITY  crash_reporter_title "&brandShortName; Crash Reporter">
 <!ENTITY  crash_message2 "&brandShortName; had a problem and crashed. Your tabs should be listed on the &brandShortName; Start page when you restart.">
 <!ENTITY  crash_send_report_message3 "Tell &vendorShortName; about this crash so they can fix it">
 <!ENTITY  crash_include_url2 "Include the address of the page I was on">
 <!ENTITY  crash_sorry "We\'re sorry">
 <!ENTITY  crash_comment "Add a comment (comments are publicly visible)">
 <!ENTITY  crash_allow_contact2 "Allow &vendorShortName; to contact me about this report">
 <!ENTITY  crash_email "Your email">
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -251,16 +251,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'db/TabsAccessor.java',
     'db/TabsProvider.java',
     'db/UrlAnnotations.java',
     'db/URLMetadata.java',
     'db/URLMetadataTable.java',
     'delegates/BookmarkStateChangeDelegate.java',
     'delegates/BrowserAppDelegate.java',
     'delegates/BrowserAppDelegateWithReference.java',
+    'delegates/OfflineTabStatusDelegate.java',
     'delegates/ScreenshotDelegate.java',
     'delegates/TabsTrayVisibilityAwareDelegate.java',
     'DevToolsAuthHelper.java',
     'distribution/Distribution.java',
     'distribution/DistributionStoreCallback.java',
     'distribution/ReferrerDescriptor.java',
     'distribution/ReferrerReceiver.java',
     'dlc/BaseAction.java',
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -63,16 +63,18 @@
 
   <string name="firstrun_welcome_restricted">&onboard_start_restricted1;</string>
 
   <string name="bookmarks_title">&bookmarks_title;</string>
   <string name="history_title">&history_title;</string>
 
   <string name="switch_to_tab">&switch_to_tab;</string>
 
+  <string name="tab_offline_version">&tab_offline_version;</string>
+
   <string name="crash_reporter_title">&crash_reporter_title;</string>
   <string name="crash_message2">&crash_message2;</string>
   <string name="crash_send_report_message3">&crash_send_report_message3;</string>
   <string name="crash_include_url2">&crash_include_url2;</string>
   <string name="crash_sorry">&crash_sorry;</string>
   <string name="crash_comment">&crash_comment;</string>
   <string name="crash_allow_contact2">&crash_allow_contact2;</string>
   <string name="crash_email">&crash_email;</string>
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -4292,17 +4292,18 @@ Tab.prototype = {
         if (!docURI.startsWith("about:neterror") && !this.isSearch) {
           // If this wasn't an error page and the user isn't search, don't retain the typed entry
           this.userRequested = "";
         }
 
         Messaging.sendRequest({
           type: "Content:PageShow",
           tabID: this.id,
-          userRequested: this.userRequested
+          userRequested: this.userRequested,
+          fromCache: Tabs.useCache
         });
 
         this.isSearch = false;
 
         if (!aEvent.persisted && Services.prefs.getBoolPref("browser.ui.linkify.phone")) {
           if (!this._linkifier)
             this._linkifier = new Linkifier();
           this._linkifier.linkifyNumbers(this.browser.contentWindow.document);
@@ -7387,35 +7388,16 @@ var Tabs = {
   },
 
   handleEvent: function(aEvent) {
     switch (aEvent.type) {
       case "pageshow":
         // Clear the domain cache whenever a page is loaded into any browser.
         this._domains.clear();
 
-        // Notify if we are loading a page from cache.
-        if (this._useCache) {
-          let targetDoc = aEvent.originalTarget;
-          let isTopLevel = (targetDoc.defaultView.parent === targetDoc.defaultView);
-
-          // Ignore any about: pages, especially about:neterror since it means we failed to find the page in cache.
-          let targetURI = targetDoc.documentURI;
-          if (isTopLevel && !targetURI.startsWith("about:")) {
-            UITelemetry.addEvent("neterror.1", "toast", null, "usecache");
-            Snackbars.show(
-              Strings.browser.GetStringFromName("networkOffline.message2"),
-              Snackbars.LENGTH_INDEFINITE,
-              {
-                // link_blue
-                backgroundColor: "#0096DD"
-              }
-            );
-          }
-        }
         break;
       case "TabOpen":
         // Use opening a new tab as a trigger to expire the most stale tab.
         this.expireLruTab();
         break;
     }
   },
 
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -417,19 +417,16 @@ getUserMedia.blockedCameraAndMicrophoneA
 # Tip shown to users the first time we hide the reader mode toolbar.
 readerMode.toolbarTip=Tap the screen to show reader options
 
 #Open in App
 openInApp.pageAction = Open in App
 openInApp.ok = OK
 openInApp.cancel = Cancel
 
-#Network Offline
-networkOffline.message2 = Showing offline version
-
 #Tab sharing
 tabshare.title = "Choose a tab to stream"
 #Tabs in context menus
 browser.menu.context.default = Link
 browser.menu.context.img = Image
 browser.menu.context.video = Video
 browser.menu.context.audio = Audio
 browser.menu.context.tel = Phone