// Copyright 2015 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.feedback; import android.os.SystemClock; import org.chromium.base.Log; import org.chromium.base.ThreadUtils; import org.chromium.base.VisibleForTesting; import org.chromium.chrome.browser.profiles.Profile; import org.chromium.net.ConnectionType; import org.chromium.net.NetworkChangeNotifier; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; /** * A utility class for checking if the device is currently connected to the Internet by using * both available network stacks, and checking over both HTTP and HTTPS. */ public class ConnectivityTask { private static final String TAG = "feedback"; /** * The key for the data describing how long time from the connection check was started, * until the data was collected. This is to better understand the connection data. * This string is user visible. */ @VisibleForTesting static final String CONNECTION_CHECK_ELAPSED_KEY = "Connection check elapsed (ms)"; /** * The key for the data describing the current connection type. * This string is user visible. */ @VisibleForTesting static final String CONNECTION_TYPE_KEY = "Connection type"; /** * The key for the data describing whether Chrome was able to successfully connect to the * HTTP connection check URL using the Chrome network stack. * This string is user visible. */ @VisibleForTesting static final String CHROME_HTTP_KEY = "HTTP connection check (Chrome network stack)"; /** * The key for the data describing whether Chrome was able to successfully connect to the * HTTPS connection check URL using the Chrome network stack. * This string is user visible. */ @VisibleForTesting static final String CHROME_HTTPS_KEY = "HTTPS connection check (Chrome network stack)"; /** * The key for the data describing whether Chrome was able to successfully connect to the * HTTP connection check URL using the Android network stack. * This string is user visible. */ @VisibleForTesting static final String SYSTEM_HTTP_KEY = "HTTP connection check (Android network stack)"; /** * The key for the data describing whether Chrome was able to successfully connect to the * HTTPS connection check URL using the Android network stack. * This string is user visible. */ @VisibleForTesting static final String SYSTEM_HTTPS_KEY = "HTTPS connection check (Android network stack)"; private static String getHumanReadableType(Type type) { switch (type) { case CHROME_HTTP: return CHROME_HTTP_KEY; case CHROME_HTTPS: return CHROME_HTTPS_KEY; case SYSTEM_HTTP: return SYSTEM_HTTP_KEY; case SYSTEM_HTTPS: return SYSTEM_HTTPS_KEY; default: throw new IllegalArgumentException("Unknown connection type: " + type); } } static String getHumanReadableResult(int result) { switch (result) { case ConnectivityCheckResult.UNKNOWN: return "UNKNOWN"; case ConnectivityCheckResult.CONNECTED: return "CONNECTED"; case ConnectivityCheckResult.NOT_CONNECTED: return "NOT_CONNECTED"; case ConnectivityCheckResult.TIMEOUT: return "TIMEOUT"; case ConnectivityCheckResult.ERROR: return "ERROR"; default: throw new IllegalArgumentException("Unknown result value: " + result); } } static String getHumanReadableConnectionType(int connectionType) { switch (connectionType) { case ConnectionType.CONNECTION_UNKNOWN: return "Unknown"; case ConnectionType.CONNECTION_ETHERNET: return "Ethernet"; case ConnectionType.CONNECTION_WIFI: return "WiFi"; case ConnectionType.CONNECTION_2G: return "2G"; case ConnectionType.CONNECTION_3G: return "3G"; case ConnectionType.CONNECTION_4G: return "4G"; case ConnectionType.CONNECTION_NONE: return "NONE"; case ConnectionType.CONNECTION_BLUETOOTH: return "Bluetooth"; default: return "Unknown connection type " + connectionType; } } /** * ConnectivityResult is the callback for when the result of a connectivity check is ready. */ interface ConnectivityResult { /** * Called when the FeedbackData is ready. */ void onResult(FeedbackData feedbackData); } /** * FeedbackData contains the set of information that is to be included in a feedback report. */ static final class FeedbackData { private final Map<Type, Integer> mConnections; private final int mTimeoutMs; private final long mElapsedTimeMs; private final int mConnectionType; FeedbackData(Map<Type, Integer> connections, int timeoutMs, long elapsedTimeMs, int connectionType) { mConnections = connections; mTimeoutMs = timeoutMs; mElapsedTimeMs = elapsedTimeMs; mConnectionType = connectionType; } /** * @return a {@link Map} with information about connection status for different connection * types. */ @VisibleForTesting Map<Type, Integer> getConnections() { return Collections.unmodifiableMap(mConnections); } /** * @return the timeout that was used for data collection. */ @VisibleForTesting int getTimeoutMs() { return mTimeoutMs; } /** * @return the time that was used from starting the check until data was gathered. */ @VisibleForTesting long getElapsedTimeMs() { return mElapsedTimeMs; } /** * @return a {@link Map} with all the data fields for this feedback. */ Map<String, String> toMap() { Map<String, String> map = new HashMap<>(); for (Map.Entry<Type, Integer> entry : mConnections.entrySet()) { map.put(getHumanReadableType(entry.getKey()), getHumanReadableResult(entry.getValue())); } map.put(CONNECTION_CHECK_ELAPSED_KEY, String.valueOf(mElapsedTimeMs)); map.put(CONNECTION_TYPE_KEY, getHumanReadableConnectionType(mConnectionType)); return map; } } /** * The type of network stack and connectivity check this result is about. */ public enum Type { CHROME_HTTP, CHROME_HTTPS, SYSTEM_HTTP, SYSTEM_HTTPS } private class SingleTypeTask implements ConnectivityChecker.ConnectivityCheckerCallback { private final Type mType; public SingleTypeTask(Type type) { mType = type; } /** * Starts the current task by calling the appropriate method on the * {@link ConnectivityChecker}. * The result will be put in {@link #mResult} when it comes back from the network stack. */ public void start(Profile profile, int timeoutMs) { Log.v(TAG, "Starting task for " + mType); switch (mType) { case CHROME_HTTP: ConnectivityChecker.checkConnectivityChromeNetworkStack( profile, false, timeoutMs, this); break; case CHROME_HTTPS: ConnectivityChecker.checkConnectivityChromeNetworkStack( profile, true, timeoutMs, this); break; case SYSTEM_HTTP: ConnectivityChecker.checkConnectivitySystemNetworkStack(false, timeoutMs, this); break; case SYSTEM_HTTPS: ConnectivityChecker.checkConnectivitySystemNetworkStack(true, timeoutMs, this); break; default: Log.e(TAG, "Failed to recognize type " + mType); } } @Override public void onResult(int result) { ThreadUtils.assertOnUiThread(); Log.v(TAG, "Got result for " + getHumanReadableType(mType) + ": result = " + getHumanReadableResult(result)); mResult.put(mType, result); if (isDone()) postCallbackResult(); } private void postCallbackResult() { if (mCallback == null) return; ThreadUtils.postOnUiThread(new Runnable() { @Override public void run() { mCallback.onResult(get()); } }); } } private final Map<Type, Integer> mResult = new EnumMap<Type, Integer>(Type.class); private final int mTimeoutMs; private final ConnectivityResult mCallback; private final long mStartCheckTimeMs; @VisibleForTesting ConnectivityTask(Profile profile, int timeoutMs, ConnectivityResult callback) { mTimeoutMs = timeoutMs; mCallback = callback; mStartCheckTimeMs = SystemClock.elapsedRealtime(); init(profile, timeoutMs); } @VisibleForTesting void init(Profile profile, int timeoutMs) { for (Type t : Type.values()) { SingleTypeTask task = new SingleTypeTask(t); task.start(profile, timeoutMs); } } /** * @return whether all connectivity type results have been collected. */ public boolean isDone() { ThreadUtils.assertOnUiThread(); return mResult.size() == Type.values().length; } /** * Retrieves the connectivity that has been collected up until this call. This method fills in * {@link ConnectivityCheckResult#UNKNOWN} for results that have not been retrieved yet. * * @return the {@link FeedbackData}. */ public FeedbackData get() { ThreadUtils.assertOnUiThread(); Map<Type, Integer> result = new EnumMap<Type, Integer>(Type.class); // Ensure the map is filled with a result for all {@link Type}s. for (Type type : Type.values()) { if (mResult.containsKey(type)) { result.put(type, mResult.get(type)); } else { result.put(type, ConnectivityCheckResult.UNKNOWN); } } long elapsedTimeMs = SystemClock.elapsedRealtime() - mStartCheckTimeMs; int connectionType = NetworkChangeNotifier.getInstance().getCurrentConnectionType(); return new FeedbackData(result, mTimeoutMs, elapsedTimeMs, connectionType); } /** * Starts an asynchronous request for checking whether the device is currently connected to the * Internet using both the Chrome and the Android system network stack. * * The result will be given back in the {@link ConnectivityResult} callback that is passed in, * either when all results have been gathered successfully or if a timeout happened. The result * can also be retrieved by calling {@link #get}, and this call must happen from the main * thread. {@link #isDone} can be used to see if all requests have been completed. It is OK to * get the result before {@link #isDone()} returns true. * * @param profile the context to do the check in. * @param timeoutMs number of milliseconds to wait before giving up waiting for a connection. * @param callback the callback for the result. May be null. * @return a ConnectivityTask to retrieve the results. */ public static ConnectivityTask create( Profile profile, int timeoutMs, @Nullable ConnectivityResult callback) { ThreadUtils.assertOnUiThread(); return new ConnectivityTask(profile, timeoutMs, callback); } }