// Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.offlinepages; import android.content.Context; import org.chromium.base.Log; import org.chromium.base.VisibleForTesting; import org.chromium.chrome.browser.snackbar.SnackbarManager; import org.chromium.chrome.browser.snackbar.SnackbarManager.SnackbarController; import org.chromium.chrome.browser.tab.EmptyTabObserver; import org.chromium.chrome.browser.tab.Tab; import org.chromium.net.NetworkChangeNotifier; import java.util.HashMap; import java.util.Map; /** * A class that observes events for a tab which has an associated offline page. It is created when * the first offline page is loaded in any tab. When there is more offline pages opened, the are all * watched by the same observer. This observer will decide when to show a reload snackbar for such * tabs. The following conditions need to be met to show the snackbar: * <ul> * <li>Tab has to be shown,</li> * <li>Offline page has to be loaded,</li> * <li>Chrome is connected to the web,</li> * <li>Unless triggering condition is change in network, snackbar hasn't been shown for that * tab.</li> * </ul> * When the last tab with offline page is closed or navigated away from, this observer stops * listening to network changes. */ public class OfflinePageTabObserver extends EmptyTabObserver implements NetworkChangeNotifier.ConnectionTypeObserver { private static final String TAG = "OfflinePageTO"; /** Class for keeping the state of observed tabs. */ private static class TabState { /** Whether content in a tab finished loading. */ public boolean isLoaded; /** Whether a snackbar was shown for the tab. */ public boolean wasSnackbarSeen; public TabState(boolean isLoaded) { this.isLoaded = isLoaded; this.wasSnackbarSeen = false; } } private Context mContext; private SnackbarManager mSnackbarManager; private SnackbarController mSnackbarController; /** Map of observed tabs. */ private final Map<Integer, TabState> mObservedTabs = new HashMap<>(); private boolean mIsObservingNetworkChanges; /** Current tab, kept track of for the network change notification. */ private Tab mCurrentTab; private static OfflinePageTabObserver sInstance; static void init(Context context, SnackbarManager manager, SnackbarController controller) { sInstance = new OfflinePageTabObserver(context, manager, controller); } static OfflinePageTabObserver getInstance() { return sInstance; } @VisibleForTesting static void setInstanceForTesting(OfflinePageTabObserver instance) { sInstance = instance; } /** * Create and attach a tab observer if we don't already have one, otherwise update it. * @param tab The tab we are adding an observer for. */ public static void addObserverForTab(Tab tab) { assert getInstance() != null; getInstance().startObservingTab(tab); getInstance().maybeShowReloadSnackbar(tab, false); } /** * Builds a new OfflinePageTabObserver. * @param context Android context. * @param snackbarManager The snackbar manager to show and dismiss snackbars. * @param snackbarController Controller to use to build the snackbar. */ OfflinePageTabObserver(Context context, SnackbarManager snackbarManager, SnackbarController snackbarController) { mContext = context; mSnackbarManager = snackbarManager; mSnackbarController = snackbarController; // The first time observer is created snackbar has net yet been shown. mIsObservingNetworkChanges = false; } // Methods from EmptyTabObserver @Override public void onPageLoadFinished(Tab tab) { Log.d(TAG, "onPageLoadFinished"); if (isObservingTab(tab)) { mObservedTabs.get(tab.getId()).isLoaded = true; maybeShowReloadSnackbar(tab, false); } } @Override public void onShown(Tab tab) { Log.d(TAG, "onShow"); maybeShowReloadSnackbar(tab, false); mCurrentTab = tab; } @Override public void onHidden(Tab hiddenTab) { Log.d(TAG, "onHidden"); mCurrentTab = null; mSnackbarManager.dismissSnackbars(mSnackbarController); } @Override public void onDestroyed(Tab tab) { Log.d(TAG, "onDestroyed"); stopObservingTab(tab); mSnackbarManager.dismissSnackbars(mSnackbarController); } @Override public void onUrlUpdated(Tab tab) { Log.d(TAG, "onUrlUpdated"); if (!tab.isOfflinePage()) { stopObservingTab(tab); } else { if (isObservingTab(tab)) { mObservedTabs.get(tab.getId()).isLoaded = false; mObservedTabs.get(tab.getId()).wasSnackbarSeen = false; } } // In case any snackbars are showing, dismiss them before we navigate away. mSnackbarManager.dismissSnackbars(mSnackbarController); } void startObservingTab(Tab tab) { if (!tab.isOfflinePage()) return; mCurrentTab = tab; // If we are not observing the tab yet, let's. if (!isObservingTab(tab)) { // Adding a tab happens from inside of onPageLoadFinished, therefore if this is the time // we start observing the tab, the page inside of it is already loaded. mObservedTabs.put(tab.getId(), new TabState(true)); tab.addObserver(this); } // If we are not observing network changes yet, let's. if (!isObservingNetworkChanges()) { startObservingNetworkChanges(); mIsObservingNetworkChanges = true; } } /** * Removes the observer for a tab with the specified tabId. * @param tab tab that was observed. */ void stopObservingTab(Tab tab) { // If we are observing the tab, stop. if (isObservingTab(tab)) { mObservedTabs.remove(tab.getId()); tab.removeObserver(this); } // If there are not longer any tabs being observed, stop listening for network changes. if (mObservedTabs.isEmpty() && isObservingNetworkChanges()) { stopObservingNetworkChanges(); mIsObservingNetworkChanges = false; } } // Methods from ConnectionTypeObserver. @Override public void onConnectionTypeChanged(int connectionType) { Log.d(TAG, "Got connectivity event, connectionType: " + connectionType + ", is connected: " + isConnected() + ", controller: " + mSnackbarController); maybeShowReloadSnackbar(mCurrentTab, true); } @VisibleForTesting boolean isObservingTab(Tab tab) { return mObservedTabs.containsKey(tab.getId()); } @VisibleForTesting boolean isLoadedTab(Tab tab) { return isObservingTab(tab) && mObservedTabs.get(tab.getId()).isLoaded; } @VisibleForTesting boolean wasSnackbarSeen(Tab tab) { return isObservingTab(tab) && mObservedTabs.get(tab.getId()).wasSnackbarSeen; } @VisibleForTesting boolean isObservingNetworkChanges() { return mIsObservingNetworkChanges; } @VisibleForTesting boolean isConnected() { return OfflinePageUtils.isConnected(); } void maybeShowReloadSnackbar(Tab tab, boolean isNetworkEvent) { if (tab == null || tab.isFrozen() || tab.isHidden() || !tab.isOfflinePage() || !isConnected() || !isLoadedTab(tab) || (wasSnackbarSeen(tab) && !isNetworkEvent)) { // Conditions to show a snackbar are not met. return; } showReloadSnackbar(tab); mObservedTabs.get(tab.getId()).wasSnackbarSeen = true; } @VisibleForTesting void showReloadSnackbar(Tab tab) { OfflinePageUtils.showReloadSnackbar( mContext, mSnackbarManager, mSnackbarController, tab.getId()); } @VisibleForTesting void startObservingNetworkChanges() { NetworkChangeNotifier.addConnectionTypeObserver(this); } @VisibleForTesting void stopObservingNetworkChanges() { NetworkChangeNotifier.removeConnectionTypeObserver(this); } }