// 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.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
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: "[email protected]"
}
contacts {
email: "[email protected]"
}
}
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(
@JniType("Profile*") Profile profile,
String url,
long timeoutMs,
ConnectivityCheckerCallback callback,
int annotationHashCode);
boolean isUrlValid(String url);
}
}