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
--- 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