// 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 androidx.annotation.VisibleForTesting;

import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.base.task.AsyncTask;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.net.ChromiumNetworkAdapter;
import org.chromium.net.NetworkTrafficAnnotationTag;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.SocketTimeoutException;
import java.net.URL;

/**
 * A utility class for checking if the device is currently connected to the Internet.
 */
@JNINamespace("chrome::android")
public final class ConnectivityChecker {
    private static final String TAG = "feedback";

    private static final String DEFAULT_HTTP_NO_CONTENT_URL =
            "http://clients4.google.com/generate_204";
    private static final String DEFAULT_HTTPS_NO_CONTENT_URL =
            "https://clients4.google.com/generate_204";

    private static String sHttpNoContentUrl = DEFAULT_HTTP_NO_CONTENT_URL;
    private static String sHttpsNoContentUrl = DEFAULT_HTTPS_NO_CONTENT_URL;

    private static final NetworkTrafficAnnotationTag TRAFFIC_ANNOTATION =
            NetworkTrafficAnnotationTag.createComplete("android_feedback_connectivity_checker",
                    "semantics {"
                            + "  sender: 'Feedback Connectivity Checker'"
                            + "  description:"
                            + "    'When sending user initiated feedback about Chrome to '"
                            + "    'Google, this request checks the user\\'s connectivity. It '"
                            + "    'does this by attempting to connect to Google servers and '"
                            + "    'records whether the browser was able to directly connect '"
                            + "    'the servers or not. A redirected request is not considered '"
                            + "    'a success.'"
                            + "  trigger: 'User triggers the application feedback flow.'"
                            + "  data: 'No additional data.'"
                            + "  destination: GOOGLE_OWNED_SERVICE"
                            + "  internal {"
                            + "    contacts {"
                            + "      email: 'clank-dev@google.com'"
                            + "    }"
                            + "    contacts {"
                            + "      email: 'nyquist@chromium.org'"
                            + "    }"
                            + "  }"
                            + "  user_data {"
                            + "    type: NONE"
                            + "  }"
                            + "  last_reviewed: '2023-01-13'"
                            + "}"
                            + "policy {"
                            + "  cookies_allowed: NO"
                            + "  setting: 'This feature can not be disabled.'"
                            + "  policy_exception_justification:"
                            + "    'A policy for this is not considered necessary as this '"
                            + "    'request is manually initiated by the user and does '"
                            + "    'not contain any additional data.'"
                            + "}");

    /**
     * A callback for whether the device is currently connected to the Internet.
     */
    public interface ConnectivityCheckerCallback {
        /**
         * Called when the result of the connectivity check is ready.
         */
        void onResult(int result);
    }

    static void overrideUrlsForTest(String httpUrl, String httpsUrl) {
        ThreadUtils.assertOnUiThread();
        sHttpNoContentUrl = httpUrl;
        sHttpsNoContentUrl = httpsUrl;
    }

    private static void postResult(final ConnectivityCheckerCallback callback, final int result) {
        PostTask.postTask(TaskTraits.UI_DEFAULT, new Runnable() {
            @Override
            public void run() {
                callback.onResult(result);
            }
        });
    }

    /**
     * Starts an asynchronous request for checking whether the device is currently connected to the
     * Internet using the Android system network stack. The result passed to the callback denotes
     * whether the attempt to connect to the server was successful.
     *
     * If the profile or URL is invalid, the callback will be called with false.
     * The server reply for the URL must respond with HTTP 204 without any redirects for the
     * connectivity check to be successful.
     *
     * This method takes ownership of the callback object until the callback has happened.
     * This method must be called on the main thread.
     * @param timeoutMs number of milliseconds to wait before giving up waiting for a connection.
     * @param callback the callback which will get the result.
     */
    public static void checkConnectivitySystemNetworkStack(
            boolean useHttps, int timeoutMs, ConnectivityCheckerCallback callback) {
        String url = useHttps ? sHttpsNoContentUrl : sHttpNoContentUrl;
        checkConnectivitySystemNetworkStack(url, timeoutMs, callback);
    }

    static void checkConnectivitySystemNetworkStack(
            String urlStr, final int timeoutMs, final ConnectivityCheckerCallback callback) {
        if (!ConnectivityCheckerJni.get().isUrlValid(urlStr)) {
            Log.w(TAG, "Predefined URL invalid.");
            postResult(callback, ConnectivityCheckResult.ERROR);
            return;
        }
        final URL url;
        try {
            url = new URL(urlStr);
        } catch (MalformedURLException e) {
            Log.w(TAG, "Failed to parse predefined URL: " + e);
            postResult(callback, ConnectivityCheckResult.ERROR);
            return;
        }
        new AsyncTask<Integer>() {
            @Override
            protected Integer doInBackground() {
                try {
                    HttpURLConnection conn =
                            (HttpURLConnection) ChromiumNetworkAdapter.openConnection(
                                    url, TRAFFIC_ANNOTATION);
                    conn.setInstanceFollowRedirects(false);
                    conn.setRequestMethod("GET");
                    conn.setDoInput(false);
                    conn.setDoOutput(false);
                    conn.setConnectTimeout(timeoutMs);
                    conn.setReadTimeout(timeoutMs);

                    conn.connect();
                    int responseCode = conn.getResponseCode();
                    if (responseCode == HttpURLConnection.HTTP_NO_CONTENT) {
                        return ConnectivityCheckResult.CONNECTED;
                    } else {
                        return ConnectivityCheckResult.NOT_CONNECTED;
                    }
                } catch (SocketTimeoutException e) {
                    return ConnectivityCheckResult.TIMEOUT;
                } catch (ProtocolException e) {
                    return ConnectivityCheckResult.ERROR;
                } catch (IOException e) {
                    return ConnectivityCheckResult.NOT_CONNECTED;
                }
            }

            @Override
            protected void onPostExecute(Integer result) {
                callback.onResult(result);
            }
        }
                .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    /**
     * Starts an asynchronous request for checking whether the device is currently connected to the
     * Internet using the Chrome network stack. The result passed to the callback denotes whether
     *the
     * attempt to connect to the server was successful.
     *
     * If the profile or URL is invalid, the callback will be called with false.
     * The server reply for the URL must respond with HTTP 204 without any redirects for the
     * connectivity check to be successful.
     *
     * This method takes ownership of the callback object until the callback has happened.
     * This method must be called on the main thread.
     * @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 which will get the result.
     */
    public static void checkConnectivityChromeNetworkStack(Profile profile, boolean useHttps,
            int timeoutMs, ConnectivityCheckerCallback callback) {
        String url = useHttps ? sHttpsNoContentUrl : sHttpNoContentUrl;
        checkConnectivityChromeNetworkStack(profile, url, timeoutMs, callback);
    }

    @VisibleForTesting
    static void checkConnectivityChromeNetworkStack(
            Profile profile, String url, long timeoutMs, ConnectivityCheckerCallback callback) {
        ThreadUtils.assertOnUiThread();
        ConnectivityCheckerJni.get().checkConnectivity(
                profile, url, timeoutMs, callback, TRAFFIC_ANNOTATION.getHashCode());
    }

    @CalledByNative
    private static void executeCallback(Object callback, int result) {
        ((ConnectivityCheckerCallback) callback).onResult(result);
    }

    private ConnectivityChecker() {}

    @NativeMethods
    interface Natives {
        void checkConnectivity(Profile profile, String url, long timeoutMs,
                ConnectivityCheckerCallback callback, int annotationHashCode);
        boolean isUrlValid(String url);
    }
}
