blob: b38af80548d1fe9d6a1e93da36cf912e2893da98 [file] [log] [blame]
// Copyright 2018 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.indicator;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.os.Build;
import android.support.v7.content.res.AppCompatResources;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ContextUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.download.DownloadActivity;
import org.chromium.chrome.browser.download.DownloadUtils;
import org.chromium.chrome.browser.snackbar.Snackbar;
import org.chromium.chrome.browser.snackbar.SnackbarManager;
import org.chromium.chrome.browser.snackbar.SnackbarManager.SnackbarController;
import org.chromium.chrome.browser.snackbar.SnackbarManager.SnackbarManageable;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.net.ConnectionType;
import org.chromium.net.NetworkChangeNotifier;
/**
* Class that controls when to show the offline indicator.
*/
public class OfflineIndicatorController
implements NetworkChangeNotifier.ConnectionTypeObserver, SnackbarController {
// OfflineIndicatorCTREvent defined in tools/metrics/histograms/enums.xml.
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
public static final int OFFLINE_INDICATOR_CTR_DISPLAYED = 0;
public static final int OFFLINE_INDICATOR_CTR_CLICKED = 1;
public static final int OFFLINE_INDICATOR_CTR_COUNT = 2;
private static final int DURATION_MS = 10000;
private static boolean sSkipSystemCheckForTesting = false;
@SuppressLint("StaticFieldLeak")
private static OfflineIndicatorController sInstance;
private boolean mIsOffline = false;
private boolean mIsShowingOfflineIndicator = false;
private OfflineIndicatorController() {
NetworkChangeNotifier.addConnectionTypeObserver(this);
updateConnectionType();
}
/**
* Initializes the singleton once.
*/
public static void initialize() {
// No need to create the singleton if the feature is not enabled.
if (!ChromeFeatureList.isEnabled(ChromeFeatureList.OFFLINE_INDICATOR)) return;
if (sInstance == null) {
sInstance = new OfflineIndicatorController();
}
}
/**
* Returns the singleton instance.
*/
public static OfflineIndicatorController getInstance() {
assert sInstance != null;
return sInstance;
}
/**
* Called to update the offline indicator per current connection state when the activity is
* running in foreground now or the tab is being shown.
*/
public static void onUpdate() {
if (sInstance == null) return;
sInstance.updateConnectionType();
}
@Override
public void onConnectionTypeChanged(int connectionType) {
if (connectionType == ConnectionType.CONNECTION_NONE) {
mIsOffline = true;
} else {
if (!performSystemCheckForValidatedNetwork()) {
mIsOffline = false;
}
}
updateOfflineIndicator();
}
@Override
public void onAction(Object actionData) {
mIsShowingOfflineIndicator = false;
DownloadUtils.showDownloadManager(null, null, true /*showPrefetchedContent*/);
RecordHistogram.recordEnumeratedHistogram(
"OfflineIndicator.CTR", OFFLINE_INDICATOR_CTR_CLICKED, OFFLINE_INDICATOR_CTR_COUNT);
}
@Override
public void onDismissNoAction(Object actionData) {
mIsShowingOfflineIndicator = false;
}
/**
* Consults with the Android connection manager to find out if there is a validated network.
* Returns true if the check is performed. A validated network is one of the following:
* 1) a functioning network providing Internet access
* 2) a captive portal and the user decided to use it as is
*/
@TargetApi(Build.VERSION_CODES.M)
private boolean performSystemCheckForValidatedNetwork() {
if (sSkipSystemCheckForTesting) return false;
// NetworkCapabilities.NET_CAPABILITY_VALIDATED is only available on Marshmallow and
// later versions.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return false;
ConnectivityManager connectivityManager =
(ConnectivityManager) ContextUtils.getApplicationContext().getSystemService(
Context.CONNECTIVITY_SERVICE);
if (connectivityManager == null) return false;
Network[] networks = connectivityManager.getAllNetworks();
for (Network network : networks) {
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
if (capabilities != null
&& capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
mIsOffline = false;
return true;
}
}
mIsOffline = true;
return true;
}
private void updateConnectionType() {
onConnectionTypeChanged(NetworkChangeNotifier.getInstance().getCurrentConnectionType());
}
private void updateOfflineIndicator() {
Activity activity = ApplicationStatus.getLastTrackedFocusedActivity();
if (!(activity instanceof SnackbarManageable)) return;
SnackbarManager snackbarManager = ((SnackbarManageable) activity).getSnackbarManager();
if (snackbarManager == null) return;
if (mIsOffline) {
showOfflineIndicator(activity, snackbarManager);
} else {
hideOfflineIndicator(snackbarManager);
}
}
private boolean canShowOfflineIndicator(Activity activity) {
// No need to show offline indicator since Downloads home is already home of the offline
// content.
if (activity instanceof DownloadActivity) return false;
if (activity instanceof ChromeActivity) {
ChromeActivity chromeActivity = (ChromeActivity) activity;
Tab tab = chromeActivity.getActivityTab();
if (tab == null) return false;
if (tab.isShowingErrorPage()) return false;
}
return true;
}
private void showOfflineIndicator(Activity activity, SnackbarManager snackbarManager) {
if (mIsShowingOfflineIndicator || !canShowOfflineIndicator(activity)) return;
Drawable icon =
AppCompatResources.getDrawable(activity, R.drawable.ic_offline_pin_blue_white);
Snackbar snackbar =
Snackbar.make(activity.getString(R.string.offline_indicator_offline_title), this,
Snackbar.TYPE_ACTION, Snackbar.UMA_OFFLINE_INDICATOR)
.setSingleLine(true)
.setProfileImage(icon)
.setBackgroundColor(Color.BLACK)
.setTextAppearance(R.style.WhiteBody)
.setAction(
activity.getString(R.string.offline_indicator_view_offline_content),
null);
snackbar.setDuration(DURATION_MS);
snackbarManager.showSnackbar(snackbar);
RecordHistogram.recordEnumeratedHistogram("OfflineIndicator.CTR",
OFFLINE_INDICATOR_CTR_DISPLAYED, OFFLINE_INDICATOR_CTR_COUNT);
mIsShowingOfflineIndicator = true;
}
private void hideOfflineIndicator(SnackbarManager snackbarManager) {
if (!mIsShowingOfflineIndicator) return;
snackbarManager.dismissSnackbars(this);
}
@VisibleForTesting
static void skipSystemCheckForTesting() {
sSkipSystemCheckForTesting = true;
}
}