chromium/chrome/android/java/src/org/chromium/chrome/browser/feedback/ConnectivityTask.java

// Copyright 2015 The Chromium Authors
// 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 androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.net.ConnectionType;
import org.chromium.net.NetworkChangeNotifier;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * 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 int type) {
        switch (type) {
            case Type.CHROME_HTTP:
                return CHROME_HTTP_KEY;
            case Type.CHROME_HTTPS:
                return CHROME_HTTPS_KEY;
            case Type.SYSTEM_HTTP:
                return SYSTEM_HTTP_KEY;
            case Type.SYSTEM_HTTPS:
                return SYSTEM_HTTPS_KEY;
            default:
                throw new IllegalArgumentException("Unknown connection type: " + type);
        }
    }

    static String getHumanReadableResult(@ConnectivityCheckResult 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(@ConnectionType 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_5G:
                return "5G";
            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<Integer, Integer> mConnections;
        private final int mTimeoutMs;
        private final long mElapsedTimeMs;
        private final int mConnectionType;

        FeedbackData(
                Map<Integer, 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<Integer, 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<Integer, 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. */
    @IntDef({Type.CHROME_HTTP, Type.CHROME_HTTPS, Type.SYSTEM_HTTP, Type.SYSTEM_HTTPS})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Type {
        int CHROME_HTTP = 0;
        int CHROME_HTTPS = 1;
        int SYSTEM_HTTP = 2;
        int SYSTEM_HTTPS = 3;
        int NUM_ENTRIES = 4;
    }

    private class SingleTypeTask implements ConnectivityChecker.ConnectivityCheckerCallback {
        private final @Type int mType;

        public SingleTypeTask(@Type int 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 Type.CHROME_HTTP:
                    ConnectivityChecker.checkConnectivityChromeNetworkStack(
                            profile, false, timeoutMs, this);
                    break;
                case Type.CHROME_HTTPS:
                    ConnectivityChecker.checkConnectivityChromeNetworkStack(
                            profile, true, timeoutMs, this);
                    break;
                case Type.SYSTEM_HTTP:
                    ConnectivityChecker.checkConnectivitySystemNetworkStack(false, timeoutMs, this);
                    break;
                case Type.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;
            PostTask.postTask(
                    TaskTraits.UI_DEFAULT,
                    new Runnable() {
                        @Override
                        public void run() {
                            mCallback.onResult(get());
                        }
                    });
        }
    }

    private final Map<Integer, Integer> mResult = new HashMap<>();
    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) {
        assert Type.CHROME_HTTP == 0;
        for (@Type int t = Type.CHROME_HTTP; t < Type.NUM_ENTRIES; t++) {
            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.NUM_ENTRIES;
    }

    /**
     * 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<Integer, Integer> result = new HashMap<>();
        assert Type.CHROME_HTTP == 0;
        // Ensure the map is filled with a result for all {@link Type}s.
        for (@Type int type = Type.CHROME_HTTP; type < Type.NUM_ENTRIES; type++) {
            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);
    }
}