Track latency of captive portal checks
When captive portal check occurs, track its latency, whether or not
we received a response, and whether or not the response was a captive
portal. Pair with information identifying the access point / base
station, and broadcast it (with a system|signature-protected
permission).
Broadcast only occurs if user has consented to
Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE.
Change-Id: I6fd59954a7ee2cc7acedf064a1465882653b2173
diff --git a/core/java/android/net/CaptivePortalTracker.java b/core/java/android/net/CaptivePortalTracker.java
index 21995c0..19d74ed 100644
--- a/core/java/android/net/CaptivePortalTracker.java
+++ b/core/java/android/net/CaptivePortalTracker.java
@@ -28,11 +28,23 @@
import android.database.ContentObserver;
import android.net.ConnectivityManager;
import android.net.IConnectivityManager;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
import android.os.Handler;
import android.os.UserHandle;
import android.os.Message;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.provider.Settings;
+import android.telephony.CellIdentityCdma;
+import android.telephony.CellIdentityGsm;
+import android.telephony.CellIdentityLte;
+import android.telephony.CellIdentityWcdma;
+import android.telephony.CellInfo;
+import android.telephony.CellInfoCdma;
+import android.telephony.CellInfoGsm;
+import android.telephony.CellInfoLte;
+import android.telephony.CellInfoWcdma;
import android.telephony.TelephonyManager;
import com.android.internal.util.State;
@@ -44,6 +56,7 @@
import java.net.Inet4Address;
import java.net.URL;
import java.net.UnknownHostException;
+import java.util.List;
import com.android.internal.R;
@@ -60,12 +73,29 @@
private static final int SOCKET_TIMEOUT_MS = 10000;
+ public static final String ACTION_NETWORK_CONDITIONS_MEASURED =
+ "android.net.conn.NETWORK_CONDITIONS_MEASURED";
+ public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type";
+ public static final String EXTRA_NETWORK_TYPE = "extra_network_type";
+ public static final String EXTRA_RESPONSE_RECEIVED = "extra_response_received";
+ public static final String EXTRA_IS_CAPTIVE_PORTAL = "extra_is_captive_portal";
+ public static final String EXTRA_CELL_ID = "extra_cellid";
+ public static final String EXTRA_SSID = "extra_ssid";
+ public static final String EXTRA_BSSID = "extra_bssid";
+ /** real time since boot */
+ public static final String EXTRA_REQUEST_TIMESTAMP_MS = "extra_request_timestamp_ms";
+ public static final String EXTRA_RESPONSE_TIMESTAMP_MS = "extra_response_timestamp_ms";
+
+ private static final String PERMISSION_ACCESS_NETWORK_CONDITIONS =
+ "android.permission.ACCESS_NETWORK_CONDITIONS";
+
private String mServer;
private String mUrl;
private boolean mNotificationShown = false;
private boolean mIsCaptivePortalCheckEnabled = false;
private IConnectivityManager mConnService;
private TelephonyManager mTelephonyManager;
+ private WifiManager mWifiManager;
private Context mContext;
private NetworkInfo mNetworkInfo;
@@ -92,6 +122,7 @@
mContext = context;
mConnService = cs;
mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
mProvisioningObserver = new ProvisioningObserver();
IntentFilter filter = new IntentFilter();
@@ -319,7 +350,8 @@
}
/**
- * Do a URL fetch on a known server to see if we get the data we expect
+ * Do a URL fetch on a known server to see if we get the data we expect.
+ * Measure the response time and broadcast that.
*/
private boolean isCaptivePortal(InetAddress server) {
HttpURLConnection urlConnection = null;
@@ -327,6 +359,7 @@
mUrl = "http://" + server.getHostAddress() + "/generate_204";
if (DBG) log("Checking " + mUrl);
+ long requestTimestamp = -1;
try {
URL url = new URL(mUrl);
urlConnection = (HttpURLConnection) url.openConnection();
@@ -334,11 +367,26 @@
urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
urlConnection.setUseCaches(false);
+
+ // Time how long it takes to get a response to our request
+ requestTimestamp = SystemClock.elapsedRealtime();
+
urlConnection.getInputStream();
+
+ // Time how long it takes to get a response to our request
+ long responseTimestamp = SystemClock.elapsedRealtime();
+
// we got a valid response, but not from the real google
- return urlConnection.getResponseCode() != 204;
+ boolean isCaptivePortal = urlConnection.getResponseCode() != 204;
+
+ sendNetworkConditionsBroadcast(true /* response received */, isCaptivePortal,
+ requestTimestamp, responseTimestamp);
+ return isCaptivePortal;
} catch (IOException e) {
if (DBG) log("Probably not a portal: exception " + e);
+ if (requestTimestamp != -1) {
+ sendFailedCaptivePortalCheckBroadcast(requestTimestamp);
+ } // else something went wrong with setting up the urlConnection
return false;
} finally {
if (urlConnection != null) {
@@ -352,12 +400,15 @@
try {
inetAddress = InetAddress.getAllByName(hostname);
} catch (UnknownHostException e) {
+ sendFailedCaptivePortalCheckBroadcast(SystemClock.elapsedRealtime());
return null;
}
for (InetAddress a : inetAddress) {
if (a instanceof Inet4Address) return a;
}
+
+ sendFailedCaptivePortalCheckBroadcast(SystemClock.elapsedRealtime());
return null;
}
@@ -414,4 +465,80 @@
}
mNotificationShown = visible;
}
+
+ private void sendFailedCaptivePortalCheckBroadcast(long requestTimestampMs) {
+ sendNetworkConditionsBroadcast(false /* response received */, false /* ignored */,
+ requestTimestampMs, 0 /* ignored */);
+ }
+
+ /**
+ * @param responseReceived - whether or not we received a valid HTTP response to our request.
+ * If false, isCaptivePortal and responseTimestampMs are ignored
+ */
+ private void sendNetworkConditionsBroadcast(boolean responseReceived, boolean isCaptivePortal,
+ long requestTimestampMs, long responseTimestampMs) {
+ if (Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 0) {
+ if (DBG) log("Don't send network conditions - lacking user consent.");
+ return;
+ }
+
+ Intent latencyBroadcast = new Intent(ACTION_NETWORK_CONDITIONS_MEASURED);
+ switch (mNetworkInfo.getType()) {
+ case ConnectivityManager.TYPE_WIFI:
+ WifiInfo currentWifiInfo = mWifiManager.getConnectionInfo();
+ if (currentWifiInfo != null) {
+ latencyBroadcast.putExtra(EXTRA_SSID, currentWifiInfo.getSSID());
+ latencyBroadcast.putExtra(EXTRA_BSSID, currentWifiInfo.getBSSID());
+ } else {
+ if (DBG) logw("network info is TYPE_WIFI but no ConnectionInfo found");
+ return;
+ }
+ break;
+ case ConnectivityManager.TYPE_MOBILE:
+ latencyBroadcast.putExtra(EXTRA_NETWORK_TYPE, mTelephonyManager.getNetworkType());
+ List<CellInfo> info = mTelephonyManager.getAllCellInfo();
+ if (info == null) return;
+ StringBuffer uniqueCellId = new StringBuffer();
+ int numRegisteredCellInfo = 0;
+ for (CellInfo cellInfo : info) {
+ if (cellInfo.isRegistered()) {
+ numRegisteredCellInfo++;
+ if (numRegisteredCellInfo > 1) {
+ if (DBG) log("more than one registered CellInfo. Can't " +
+ "tell which is active. Bailing.");
+ return;
+ }
+ if (cellInfo instanceof CellInfoCdma) {
+ CellIdentityCdma cellId = ((CellInfoCdma) cellInfo).getCellIdentity();
+ latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
+ } else if (cellInfo instanceof CellInfoGsm) {
+ CellIdentityGsm cellId = ((CellInfoGsm) cellInfo).getCellIdentity();
+ latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
+ } else if (cellInfo instanceof CellInfoLte) {
+ CellIdentityLte cellId = ((CellInfoLte) cellInfo).getCellIdentity();
+ latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
+ } else if (cellInfo instanceof CellInfoWcdma) {
+ CellIdentityWcdma cellId = ((CellInfoWcdma) cellInfo).getCellIdentity();
+ latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
+ } else {
+ if (DBG) logw("Registered cellinfo is unrecognized");
+ return;
+ }
+ }
+ }
+ break;
+ default:
+ return;
+ }
+ latencyBroadcast.putExtra(EXTRA_CONNECTIVITY_TYPE, mNetworkInfo.getType());
+ latencyBroadcast.putExtra(EXTRA_RESPONSE_RECEIVED, responseReceived);
+ latencyBroadcast.putExtra(EXTRA_REQUEST_TIMESTAMP_MS, requestTimestampMs);
+
+ if (responseReceived) {
+ latencyBroadcast.putExtra(EXTRA_IS_CAPTIVE_PORTAL, isCaptivePortal);
+ latencyBroadcast.putExtra(EXTRA_RESPONSE_TIMESTAMP_MS, responseTimestampMs);
+ }
+ mContext.sendBroadcast(latencyBroadcast, PERMISSION_ACCESS_NETWORK_CONDITIONS);
+ }
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index a9a14ad..d106cf2 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -201,6 +201,7 @@
<protected-broadcast android:name="android.net.wifi.p2p.PERSISTENT_GROUPS_CHANGED" />
<protected-broadcast android:name="android.net.conn.TETHER_STATE_CHANGED" />
<protected-broadcast android:name="android.net.conn.INET_CONDITION_ACTION" />
+ <protected-broadcast android:name="android.net.conn.NETWORK_CONDITIONS_MEASURED" />
<protected-broadcast android:name="android.intent.action.EXTERNAL_APPLICATIONS_AVAILABLE" />
<protected-broadcast android:name="android.intent.action.EXTERNAL_APPLICATIONS_UNAVAILABLE" />
<protected-broadcast android:name="android.intent.action.AIRPLANE_MODE" />
@@ -2399,6 +2400,13 @@
android:description="@string/permdesc_invokeCarrierSetup"
android:protectionLevel="signature|system" />
+ <!-- Allows an application to listen for network condition observations.
+ @hide This is not a third-party API (intended for system apps). -->
+ <permission android:name="android.permission.ACCESS_NETWORK_CONDITIONS"
+ android:label="@string/permlab_accessNetworkConditions"
+ android:description="@string/permdesc_accessNetworkConditions"
+ android:protectionLevel="signature|system" />
+
<!-- The system process is explicitly the only one allowed to launch the
confirmation UI for full backup/restore -->
<uses-permission android:name="android.permission.CONFIRM_FULL_BACKUP"/>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index d3dbb2a..1b5ee68 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1905,6 +1905,11 @@
<!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
<string name="permdesc_invokeCarrierSetup">Allows the holder to invoke the carrier-provided configuration app. Should never be needed for normal apps.</string>
+ <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permlab_accessNetworkConditions">listen for observations on network conditions</string>
+ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permdesc_accessNetworkConditions">Allows an application to listen for observations on network conditions. Should never be needed for normal apps.</string>
+
<!-- Policy administration -->
<!-- Title of policy access to limiting the user's password choices -->