From ab9321d13de29cf314e8a1774b592d1f1d7e94d8 Mon Sep 17 00:00:00 2001 From: Wink Saville Date: Sat, 29 Jun 2013 21:10:57 -0700 Subject: Add checkMobileProvisioning to ConnectivityService. Bug: 9279964 Change-Id: I42c326a21e05aa301e9d974ed9ac1d59472780ec --- .../com/android/server/ConnectivityService.java | 512 ++++++++++++++++++++- 1 file changed, 509 insertions(+), 3 deletions(-) (limited to 'services') diff --git a/services/java/com/android/server/ConnectivityService.java b/services/java/com/android/server/ConnectivityService.java index 37a8cb8..9e9253a 100644 --- a/services/java/com/android/server/ConnectivityService.java +++ b/services/java/com/android/server/ConnectivityService.java @@ -31,6 +31,9 @@ import static android.net.ConnectivityManager.isNetworkTypeValid; import static android.net.NetworkPolicyManager.RULE_ALLOW_ALL; import static android.net.NetworkPolicyManager.RULE_REJECT_METERED; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.bluetooth.BluetoothTetheringDataTracker; import android.content.BroadcastReceiver; import android.content.ContentResolver; @@ -52,11 +55,13 @@ import android.net.INetworkPolicyManager; import android.net.INetworkStatsService; import android.net.LinkAddress; import android.net.LinkProperties; +import android.net.Uri; import android.net.LinkProperties.CompareResult; import android.net.MobileDataStateTracker; import android.net.NetworkConfig; import android.net.NetworkInfo; import android.net.NetworkInfo.DetailedState; +import android.net.NetworkInfo.State; import android.net.NetworkQuotaInfo; import android.net.NetworkState; import android.net.NetworkStateTracker; @@ -66,6 +71,7 @@ import android.net.ProxyProperties; import android.net.RouteInfo; import android.net.wifi.WifiStateTracker; import android.net.wimax.WimaxManagerConstants; +import android.os.AsyncTask; import android.os.Binder; import android.os.FileUtils; import android.os.Handler; @@ -79,6 +85,7 @@ import android.os.ParcelFileDescriptor; import android.os.PowerManager; import android.os.Process; import android.os.RemoteException; +import android.os.ResultReceiver; import android.os.ServiceManager; import android.os.SystemClock; import android.os.SystemProperties; @@ -86,13 +93,16 @@ import android.os.UserHandle; import android.provider.Settings; import android.security.Credentials; import android.security.KeyStore; +import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Slog; import android.util.SparseIntArray; +import com.android.internal.R; import com.android.internal.net.LegacyVpnInfo; import com.android.internal.net.VpnConfig; import com.android.internal.net.VpnProfile; +import com.android.internal.telephony.DctConstants; import com.android.internal.telephony.Phone; import com.android.internal.telephony.PhoneConstants; import com.android.internal.util.IndentingPrintWriter; @@ -111,9 +121,11 @@ import java.io.FileDescriptor; import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Constructor; +import java.net.HttpURLConnection; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; +import java.net.URL; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; @@ -121,6 +133,8 @@ import java.util.Collection; import java.util.GregorianCalendar; import java.util.HashSet; import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; /** * @hide @@ -141,6 +155,12 @@ public class ConnectivityService extends IConnectivityManager.Stub { private static final String NETWORK_RESTORE_DELAY_PROP_NAME = "android.telephony.apn-restore"; + // Default value if FAIL_FAST_TIME_MS is not set + private static final int DEFAULT_FAIL_FAST_TIME_MS = 1 * 60 * 1000; + // system property that can override DEFAULT_FAIL_FAST_TIME_MS + private static final String FAIL_FAST_TIME_MS = + "persist.radio.fail_fast_time_ms"; + // used in recursive route setting to add gateways for the host for which // a host route was requested. private static final int MAX_HOSTROUTE_CYCLE_COUNT = 10; @@ -292,6 +312,11 @@ public class ConnectivityService extends IConnectivityManager.Stub { private static final int EVENT_VPN_STATE_CHANGED = 14; + /** + * Used internally to disable fail fast of mobile data + */ + private static final int EVENT_ENABLE_FAIL_FAST_MOBILE_DATA = 15; + /** Handler used for internal events. */ private InternalHandler mHandler; /** Handler used for incoming {@link NetworkStateTracker} events. */ @@ -346,6 +371,10 @@ public class ConnectivityService extends IConnectivityManager.Stub { // the set of network types that can only be enabled by system/sig apps List mProtectedNetworks; + private AtomicInteger mEnableFailFastMobileDataTag = new AtomicInteger(0); + + TelephonyManager mTelephonyManager; + public ConnectivityService(Context context, INetworkManagementService netd, INetworkStatsService statsService, INetworkPolicyManager policyManager) { // Currently, omitting a NetworkFactory will create one internally @@ -394,6 +423,7 @@ public class ConnectivityService extends IConnectivityManager.Stub { mNetd = checkNotNull(netManager, "missing INetworkManagementService"); mPolicyManager = checkNotNull(policyManager, "missing INetworkPolicyManager"); mKeyStore = KeyStore.getInstance(); + mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); try { mPolicyManager.registerListener(mPolicyListener); @@ -1408,8 +1438,12 @@ public class ConnectivityService extends IConnectivityManager.Stub { netState != DetailedState.CAPTIVE_PORTAL_CHECK) || tracker.isTeardownRequested()) { if (VDBG) { - log("requestRouteToHostAddress on down network " + - "(" + networkType + ") - dropped"); + log("requestRouteToHostAddress on down network " + + "(" + networkType + ") - dropped" + + " tracker=" + tracker + + " netState=" + netState + + " isTeardownRequested=" + + ((tracker != null) ? tracker.isTeardownRequested() : "tracker:null")); } return false; } @@ -1417,12 +1451,15 @@ public class ConnectivityService extends IConnectivityManager.Stub { try { InetAddress addr = InetAddress.getByAddress(hostAddress); LinkProperties lp = tracker.getLinkProperties(); - return addRouteToAddress(lp, addr); + boolean ok = addRouteToAddress(lp, addr); + if (DBG) log("requestRouteToHostAddress ok=" + ok); + return ok; } catch (UnknownHostException e) { if (DBG) log("requestRouteToHostAddress got " + e.toString()); } finally { Binder.restoreCallingIdentity(token); } + if (DBG) log("requestRouteToHostAddress X bottom return false"); return false; } @@ -2824,6 +2861,19 @@ public class ConnectivityService extends IConnectivityManager.Stub { } break; } + case EVENT_ENABLE_FAIL_FAST_MOBILE_DATA: { + int tag = mEnableFailFastMobileDataTag.get(); + if (msg.arg1 == tag) { + MobileDataStateTracker mobileDst = + (MobileDataStateTracker) mNetTrackers[ConnectivityManager.TYPE_MOBILE]; + if (mobileDst != null) { + mobileDst.setEnableFailFastMobileData(msg.arg2); + } + } else { + log("EVENT_ENABLE_FAIL_FAST_MOBILE_DATA: stale arg1:" + msg.arg1 + + " != tag:" + tag); + } + } } } } @@ -3472,4 +3522,460 @@ public class ConnectivityService extends IConnectivityManager.Stub { } return ConnectivityManager.TYPE_NONE; } + + /** + * Have mobile data fail fast if enabled. + * + * @param enabled DctConstants.ENABLED/DISABLED + */ + private void setEnableFailFastMobileData(int enabled) { + int tag; + + if (enabled == DctConstants.ENABLED) { + tag = mEnableFailFastMobileDataTag.incrementAndGet(); + } else { + tag = mEnableFailFastMobileDataTag.get(); + } + mHandler.sendMessage(mHandler.obtainMessage(EVENT_ENABLE_FAIL_FAST_MOBILE_DATA, tag, + enabled)); + } + + @Override + public int checkMobileProvisioning(boolean sendNotification, int suggestedTimeOutMs, + final ResultReceiver resultReceiver) { + log("checkMobileProvisioning: E sendNotification=" + sendNotification + + " suggestedTimeOutMs=" + suggestedTimeOutMs + + " resultReceiver=" + resultReceiver); + enforceChangePermission(); + + int timeOutMs = suggestedTimeOutMs; + if (suggestedTimeOutMs > CheckMp.MAX_TIMEOUT_MS) { + timeOutMs = CheckMp.MAX_TIMEOUT_MS; + } + + final long token = Binder.clearCallingIdentity(); + try { + CheckMp checkMp = new CheckMp(mContext, this); + CheckMp.CallBack cb = new CheckMp.CallBack() { + @Override + void onComplete(Integer result) { + log("CheckMp.onComplete: result=" + result); + if (resultReceiver != null) { + log("CheckMp.onComplete: send result"); + resultReceiver.send(result, null); + } + NetworkInfo ni = + mNetTrackers[ConnectivityManager.TYPE_MOBILE_HIPRI].getNetworkInfo(); + switch(result) { + case ConnectivityManager.CMP_RESULT_CODE_CONNECTABLE: + case ConnectivityManager.CMP_RESULT_CODE_NO_CONNECTION: { + log("CheckMp.onComplete: ignore, connected or no connection"); + break; + } + case ConnectivityManager.CMP_RESULT_CODE_REDIRECTED: { + log("CheckMp.onComplete: warm sim"); + String url = getProvisioningUrl(); + if (TextUtils.isEmpty(url)) { + url = mContext.getResources() + .getString(R.string.mobile_redirected_provisioning_url); + } + if (TextUtils.isEmpty(url) == false) { + log("CheckMp.onComplete: warm sim (redirected), url=" + url); + setNotificationVisible(true, ni, url); + } else { + log("CheckMp.onComplete: warm sim (redirected), no url"); + } + break; + } + case ConnectivityManager.CMP_RESULT_CODE_NO_DNS: + case ConnectivityManager.CMP_RESULT_CODE_NO_TCP_CONNECTION: { + String url = getProvisioningUrl(); + if (TextUtils.isEmpty(url) == false) { + log("CheckMp.onComplete: warm sim (no dns/tcp), url=" + url); + setNotificationVisible(true, ni, url); + } else { + log("CheckMp.onComplete: warm sim (no dns/tcp), no url"); + } + break; + } + default: { + loge("CheckMp.onComplete: ignore unexpected result=" + result); + break; + } + } + } + }; + CheckMp.Params params = + new CheckMp.Params(checkMp.getDefaultUrl(), timeOutMs, cb); + log("checkMobileProvisioning: params=" + params); + setNotificationVisible(false, null, null); + checkMp.execute(params); + } finally { + Binder.restoreCallingIdentity(token); + log("checkMobileProvisioning: X"); + } + return timeOutMs; + } + + static class CheckMp extends + AsyncTask { + private static final String CHECKMP_TAG = "CheckMp"; + public static final int MAX_TIMEOUT_MS = 60000; + private static final int SOCKET_TIMEOUT_MS = 5000; + private Context mContext; + private ConnectivityService mCs; + private TelephonyManager mTm; + private Params mParams; + + /** + * Parameters for AsyncTask.execute + */ + static class Params { + private String mUrl; + private long mTimeOutMs; + private CallBack mCb; + + Params(String url, long timeOutMs, CallBack cb) { + mUrl = url; + mTimeOutMs = timeOutMs; + mCb = cb; + } + + @Override + public String toString() { + return "{" + " url=" + mUrl + " mTimeOutMs=" + mTimeOutMs + " mCb=" + mCb + "}"; + } + } + + /** + * The call back object passed in Params. onComplete will be called + * on the main thread. + */ + abstract static class CallBack { + // Called on the main thread. + abstract void onComplete(Integer result); + } + + public CheckMp(Context context, ConnectivityService cs) { + mContext = context; + mCs = cs; + + // Setup access to TelephonyService we'll be using. + mTm = (TelephonyManager) mContext.getSystemService( + Context.TELEPHONY_SERVICE); + } + + /** + * Get the default url to use for the test. + */ + public String getDefaultUrl() { + // See http://go/clientsdns for usage approval + String server = Settings.Global.getString(mContext.getContentResolver(), + Settings.Global.CAPTIVE_PORTAL_SERVER); + if (server == null) { + server = "clients3.google.com"; + } + return "http://" + server + "/generate_204"; + } + + /** + * Detect if its possible to connect to the http url. DNS based detection techniques + * do not work at all hotspots. The best way to check is to perform a request to + * a known address that fetches the data we expect. + */ + private synchronized Integer isMobileOk(Params params) { + Integer result = ConnectivityManager.CMP_RESULT_CODE_NO_CONNECTION; + Uri orgUri = Uri.parse(params.mUrl); + Random rand = new Random(); + mParams = params; + + try { + if (mCs.isNetworkSupported(ConnectivityManager.TYPE_MOBILE) == false) { + log("isMobileOk: not mobile capable"); + result = ConnectivityManager.CMP_RESULT_CODE_NO_CONNECTION; + return result; + } + + // Enable fail fast as we'll do retries here and use a + // hipri connection so the default connection stays active. + log("isMobileOk: start hipri url=" + params.mUrl); + mCs.setEnableFailFastMobileData(DctConstants.ENABLED); + mCs.startUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE, + Phone.FEATURE_ENABLE_HIPRI, new Binder()); + + // Continue trying to connect until time has run out + long endTime = SystemClock.elapsedRealtime() + params.mTimeOutMs; + while(SystemClock.elapsedRealtime() < endTime) { + try { + // Wait for hipri to connect. + // TODO: Don't poll and handle situation where hipri fails + // because default is retrying. See b/9569540 + NetworkInfo.State state = mCs + .getNetworkInfo(ConnectivityManager.TYPE_MOBILE_HIPRI).getState(); + if (state != NetworkInfo.State.CONNECTED) { + log("isMobileOk: not connected ni=" + + mCs.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_HIPRI)); + sleep(1); + result = ConnectivityManager.CMP_RESULT_CODE_NO_CONNECTION; + continue; + } + + // Get of the addresses associated with the url host. We need to use the + // address otherwise HttpURLConnection object will use the name to get + // the addresses and is will try every address but that will bypass the + // route to host we setup and the connection could succeed as the default + // interface might be connected to the internet via wifi or other interface. + InetAddress[] addresses; + try { + addresses = InetAddress.getAllByName(orgUri.getHost()); + } catch (UnknownHostException e) { + log("isMobileOk: UnknownHostException"); + result = ConnectivityManager.CMP_RESULT_CODE_NO_DNS; + return result; + } + log("isMobileOk: addresses=" + inetAddressesToString(addresses)); + + // Get the type of addresses supported by this link + LinkProperties lp = mCs.getLinkProperties( + ConnectivityManager.TYPE_MOBILE_HIPRI); + boolean linkHasIpv4 = hasIPv4Address(lp); + boolean linkHasIpv6 = hasIPv6Address(lp); + log("isMobileOk: linkHasIpv4=" + linkHasIpv4 + + " linkHasIpv6=" + linkHasIpv6); + + // Loop through at most 3 valid addresses or all of the address or until + // we run out of time + int loops = Math.min(3, addresses.length); + for(int validAddr=0, addrTried=0; + (validAddr < loops) && (addrTried < addresses.length) + && (SystemClock.elapsedRealtime() < endTime); + addrTried ++) { + + // Choose the address at random but make sure its type is supported + InetAddress hostAddr = addresses[rand.nextInt(addresses.length)]; + if (((hostAddr instanceof Inet4Address) && linkHasIpv4) + || ((hostAddr instanceof Inet6Address) && linkHasIpv6)) { + // Valid address, so use it + validAddr += 1; + } else { + // Invalid address so try next address + continue; + } + + // Make a route to host so we check the specific interface. + if (mCs.requestRouteToHostAddress(ConnectivityManager.TYPE_MOBILE_HIPRI, + hostAddr.getAddress())) { + // Wait a short time to be sure the route is established ?? + log("isMobileOk:" + + " wait to establish route to hostAddr=" + hostAddr); + sleep(3); + } else { + log("isMobileOk:" + + " could not establish route to hostAddr=" + hostAddr); + continue; + } + + // Rewrite the url to have numeric address to use the specific route. + // I also set the "Connection" to "Close" as by default "Keep-Alive" + // is used which is useless in this case. + URL newUrl = new URL(orgUri.getScheme() + "://" + + hostAddr.getHostAddress() + orgUri.getPath()); + log("isMobileOk: newUrl=" + newUrl); + + HttpURLConnection urlConn = null; + try { + // Open the connection set the request header and get the response + urlConn = (HttpURLConnection) newUrl.openConnection( + java.net.Proxy.NO_PROXY); + urlConn.setInstanceFollowRedirects(false); + urlConn.setConnectTimeout(SOCKET_TIMEOUT_MS); + urlConn.setReadTimeout(SOCKET_TIMEOUT_MS); + urlConn.setUseCaches(false); + urlConn.setAllowUserInteraction(false); + urlConn.setRequestProperty("Connection", "close"); + int responseCode = urlConn.getResponseCode(); + if (responseCode == 204) { + result = ConnectivityManager.CMP_RESULT_CODE_CONNECTABLE; + } else { + result = ConnectivityManager.CMP_RESULT_CODE_REDIRECTED; + } + log("isMobileOk: connected responseCode=" + responseCode); + urlConn.disconnect(); + urlConn = null; + return result; + } catch (Exception e) { + log("isMobileOk: HttpURLConnection Exception e=" + e); + if (urlConn != null) { + urlConn.disconnect(); + urlConn = null; + } + } + } + result = ConnectivityManager.CMP_RESULT_CODE_NO_TCP_CONNECTION; + log("isMobileOk: loops|timed out"); + return result; + } catch (Exception e) { + log("isMobileOk: Exception e=" + e); + continue; + } + } + log("isMobileOk: timed out"); + } finally { + log("isMobileOk: F stop hipri"); + mCs.setEnableFailFastMobileData(DctConstants.DISABLED); + mCs.stopUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE, + Phone.FEATURE_ENABLE_HIPRI); + log("isMobileOk: X result=" + result); + } + return result; + } + + @Override + protected Integer doInBackground(Params... params) { + return isMobileOk(params[0]); + } + + @Override + protected void onPostExecute(Integer result) { + log("onPostExecute: result=" + result); + if ((mParams != null) && (mParams.mCb != null)) { + mParams.mCb.onComplete(result); + } + } + + private String inetAddressesToString(InetAddress[] addresses) { + StringBuffer sb = new StringBuffer(); + boolean firstTime = true; + for(InetAddress addr : addresses) { + if (firstTime) { + firstTime = false; + } else { + sb.append(","); + } + sb.append(addr); + } + return sb.toString(); + } + + private void printNetworkInfo() { + boolean hasIccCard = mTm.hasIccCard(); + int simState = mTm.getSimState(); + log("hasIccCard=" + hasIccCard + + " simState=" + simState); + NetworkInfo[] ni = mCs.getAllNetworkInfo(); + if (ni != null) { + log("ni.length=" + ni.length); + for (NetworkInfo netInfo: ni) { + log("netInfo=" + netInfo.toString()); + } + } else { + log("no network info ni=null"); + } + } + + /** + * Sleep for a few seconds then return. + * @param seconds + */ + private static void sleep(int seconds) { + try { + Thread.sleep(seconds * 1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public boolean hasIPv4Address(LinkProperties lp) { + return lp.hasIPv4Address(); + } + + // Not implemented in LinkProperties, do it here. + public boolean hasIPv6Address(LinkProperties lp) { + for (LinkAddress address : lp.getLinkAddresses()) { + if (address.getAddress() instanceof Inet6Address) { + return true; + } + } + return false; + } + + private void log(String s) { + Slog.d(ConnectivityService.TAG, "[" + CHECKMP_TAG + "] " + s); + } + } + + private static final String NOTIFICATION_ID = "CaptivePortal.Notification"; + + private void setNotificationVisible(boolean visible, NetworkInfo networkInfo, String url) { + log("setNotificationVisible: E visible=" + visible + " ni=" + networkInfo + " url=" + url); + + Resources r = Resources.getSystem(); + NotificationManager notificationManager = (NotificationManager) mContext + .getSystemService(Context.NOTIFICATION_SERVICE); + + if (visible) { + CharSequence title; + CharSequence details; + int icon; + switch (networkInfo.getType()) { + case ConnectivityManager.TYPE_WIFI: + log("setNotificationVisible: TYPE_WIFI"); + title = r.getString(R.string.wifi_available_sign_in, 0); + details = r.getString(R.string.network_available_sign_in_detailed, + networkInfo.getExtraInfo()); + icon = R.drawable.stat_notify_wifi_in_range; + break; + case ConnectivityManager.TYPE_MOBILE: + case ConnectivityManager.TYPE_MOBILE_HIPRI: + log("setNotificationVisible: TYPE_MOBILE|HIPRI"); + title = r.getString(R.string.network_available_sign_in, 0); + // TODO: Change this to pull from NetworkInfo once a printable + // name has been added to it + details = mTelephonyManager.getNetworkOperatorName(); + icon = R.drawable.stat_notify_rssi_in_range; + break; + default: + log("setNotificationVisible: other type=" + networkInfo.getType()); + title = r.getString(R.string.network_available_sign_in, 0); + details = r.getString(R.string.network_available_sign_in_detailed, + networkInfo.getExtraInfo()); + icon = R.drawable.stat_notify_rssi_in_range; + break; + } + + Notification notification = new Notification(); + notification.when = 0; + notification.icon = icon; + notification.flags = Notification.FLAG_AUTO_CANCEL; + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | + Intent.FLAG_ACTIVITY_NEW_TASK); + notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0); + notification.tickerText = title; + notification.setLatestEventInfo(mContext, title, details, notification.contentIntent); + + log("setNotificaitionVisible: notify notificaiton=" + notification); + notificationManager.notify(NOTIFICATION_ID, 1, notification); + } else { + log("setNotificaitionVisible: cancel"); + notificationManager.cancel(NOTIFICATION_ID, 1); + } + log("setNotificationVisible: X visible=" + visible + " ni=" + networkInfo + " url=" + url); + } + + private String getProvisioningUrl() { + String url = mContext.getResources().getString(R.string.mobile_provisioning_url); + log("getProvisioningUrl: resource url=" + url); + + // populate the iccid and imei in the provisioning url. + if (!TextUtils.isEmpty(url)) { + url = String.format(url, + mTelephonyManager.getSimSerialNumber() /* ICCID */, + mTelephonyManager.getDeviceId() /* IMEI */, + mTelephonyManager.getLine1Number() /* Phone numer */); + } + + log("getProvisioningUrl: url=" + url); + return url; + } } -- cgit v1.1