From ddc5fd88fe36cfdb2e216bcf51d334c167d004cb Mon Sep 17 00:00:00 2001 From: Lorenzo Colitti Date: Mon, 22 Aug 2016 16:46:40 +0900 Subject: [PATCH] Support notifying network switches via notifications and toasts. Bug: 31025214 Change-Id: If1578f422f38a1dcfaec529882aed01ae5e8d53c --- .../android/server/ConnectivityService.java | 17 +- .../server/connectivity/LingerMonitor.java | 256 ++++++++++++++++++ .../server/connectivity/NetworkAgentInfo.java | 5 +- .../NetworkNotificationManager.java | 48 +++- 4 files changed, 312 insertions(+), 14 deletions(-) create mode 100644 services/core/java/com/android/server/connectivity/LingerMonitor.java diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index 079383201a..14243c540f 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -131,6 +131,7 @@ import com.android.server.am.BatteryStatsService; import com.android.server.connectivity.DataConnectionStats; import com.android.server.connectivity.KeepaliveTracker; import com.android.server.connectivity.Nat464Xlat; +import com.android.server.connectivity.LingerMonitor; import com.android.server.connectivity.NetworkAgentInfo; import com.android.server.connectivity.NetworkDiagnostics; import com.android.server.connectivity.NetworkMonitor; @@ -438,6 +439,7 @@ public class ConnectivityService extends IConnectivityManager.Stub private KeepaliveTracker mKeepaliveTracker; private NetworkNotificationManager mNotifier; + private LingerMonitor mLingerMonitor; // sequence number for Networks; keep in sync with system/netd/NetworkController.cpp private final static int MIN_NET_ID = 100; // some reserved marks @@ -836,6 +838,7 @@ public class ConnectivityService extends IConnectivityManager.Stub mKeepaliveTracker = new KeepaliveTracker(mHandler); mNotifier = new NetworkNotificationManager(mContext, mTelephonyManager, mContext.getSystemService(NotificationManager.class)); + mLingerMonitor = new LingerMonitor(mContext, mNotifier); } private NetworkRequest createInternetRequestForTransport(int transportType) { @@ -2241,7 +2244,7 @@ public class ConnectivityService extends IConnectivityManager.Stub break; } if (!nai.networkMisc.provisioningNotificationDisabled) { - mNotifier.showNotification(netId, NotificationType.SIGN_IN, nai, + mNotifier.showNotification(netId, NotificationType.SIGN_IN, nai, null, (PendingIntent) msg.obj, nai.networkMisc.explicitlySelected); } } @@ -2392,6 +2395,7 @@ public class ConnectivityService extends IConnectivityManager.Stub } mLegacyTypeTracker.remove(nai, wasDefault); rematchAllNetworksAndRequests(null, 0); + mLingerMonitor.noteDisconnect(nai); if (nai.created) { // Tell netd to clean up the configuration for this network // (routing rules, DNS, etc). @@ -2713,7 +2717,7 @@ public class ConnectivityService extends IConnectivityManager.Stub PendingIntent pendingIntent = PendingIntent.getActivityAsUser( mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); - mNotifier.showNotification(nai.network.netId, NotificationType.NO_INTERNET, nai, + mNotifier.showNotification(nai.network.netId, NotificationType.NO_INTERNET, nai, null, pendingIntent, true); } @@ -4249,6 +4253,10 @@ public class ConnectivityService extends IConnectivityManager.Stub return nai == getDefaultNetwork(); } + private boolean isDefaultRequest(NetworkRequestInfo nri) { + return nri.request.requestId == mDefaultRequest.requestId; + } + public int registerNetworkAgent(Messenger messenger, NetworkInfo networkInfo, LinkProperties linkProperties, NetworkCapabilities networkCapabilities, int currentScore, NetworkMisc networkMisc) { @@ -4691,6 +4699,9 @@ public class ConnectivityService extends IConnectivityManager.Stub if (VDBG) log(" accepting network in place of " + currentNetwork.name()); currentNetwork.removeRequest(nri.request.requestId); currentNetwork.lingerRequest(nri.request, now, mLingerDelayMs); + if (isDefaultRequest(nri)) { + mLingerMonitor.noteLingerDefaultNetwork(currentNetwork, newNetwork); + } affectedNetworks.add(currentNetwork); } else { if (VDBG) log(" accepting network in place of null"); @@ -4708,7 +4719,7 @@ public class ConnectivityService extends IConnectivityManager.Stub // network. Think about if there is a way to reduce this. Push // netid->request mapping to each factory? sendUpdatedScoreToFactories(nri.request, newNetwork.getCurrentScore()); - if (mDefaultRequest.requestId == nri.request.requestId) { + if (isDefaultRequest(nri)) { isNewDefault = true; oldDefaultNetwork = currentNetwork; } diff --git a/services/core/java/com/android/server/connectivity/LingerMonitor.java b/services/core/java/com/android/server/connectivity/LingerMonitor.java new file mode 100644 index 0000000000..4034877023 --- /dev/null +++ b/services/core/java/com/android/server/connectivity/LingerMonitor.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import android.app.PendingIntent; +import android.net.ConnectivityManager; +import android.net.NetworkCapabilities; +import android.net.Uri; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.util.SparseBooleanArray; +import java.util.Arrays; +import java.util.HashMap; + +import com.android.internal.util.MessageUtils; +import com.android.server.connectivity.NetworkNotificationManager; +import com.android.server.connectivity.NetworkNotificationManager.NotificationType; + +import static android.net.ConnectivityManager.NETID_UNSET; + +/** + * Class that monitors default network linger events and possibly notifies the user of network + * switches. + * + * This class is not thread-safe and all its methods must be called on the ConnectivityService + * handler thread. + */ +public class LingerMonitor { + + private static final boolean DBG = true; + private static final boolean VDBG = false; + private static final String TAG = LingerMonitor.class.getSimpleName(); + + private static final HashMap sTransportNames = makeTransportToNameMap(); + private static final Intent CELLULAR_SETTINGS = new Intent().setComponent(new ComponentName( + "com.android.settings", "com.android.settings.Settings$DataUsageSummaryActivity")); + + private static final int NOTIFY_TYPE_NONE = 0; + private static final int NOTIFY_TYPE_NOTIFICATION = 1; + private static final int NOTIFY_TYPE_TOAST = 2; + + private static SparseArray sNotifyTypeNames = MessageUtils.findMessageNames( + new Class[] { LingerMonitor.class }, new String[]{ "NOTIFY_TYPE_" }); + + private final Context mContext; + private final NetworkNotificationManager mNotifier; + + /** Current notifications. Maps the netId we switched away from to the netId we switched to. */ + private final SparseIntArray mNotifications = new SparseIntArray(); + + /** Whether we ever notified that we switched away from a particular network. */ + private final SparseBooleanArray mEverNotified = new SparseBooleanArray(); + + public LingerMonitor(Context context, NetworkNotificationManager notifier) { + mContext = context; + mNotifier = notifier; + } + + private static HashMap makeTransportToNameMap() { + SparseArray numberToName = MessageUtils.findMessageNames( + new Class[] { NetworkCapabilities.class }, new String[]{ "TRANSPORT_" }); + HashMap nameToNumber = new HashMap<>(); + for (int i = 0; i < numberToName.size(); i++) { + // MessageUtils will fail to initialize if there are duplicate constant values, so there + // are no duplicates here. + nameToNumber.put(numberToName.valueAt(i), numberToName.keyAt(i)); + } + return nameToNumber; + } + + private static boolean hasTransport(NetworkAgentInfo nai, int transport) { + return nai.networkCapabilities.hasTransport(transport); + } + + private int getNotificationSource(NetworkAgentInfo toNai) { + for (int i = 0; i < mNotifications.size(); i++) { + if (mNotifications.valueAt(i) == toNai.network.netId) { + return mNotifications.keyAt(i); + } + } + return NETID_UNSET; + } + + private boolean everNotified(NetworkAgentInfo nai) { + return mEverNotified.get(nai.network.netId, false); + } + + private boolean isNotificationEnabled(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) { + // TODO: Evaluate moving to CarrierConfigManager. + String[] notifySwitches = mContext.getResources().getStringArray( + com.android.internal.R.array.config_networkNotifySwitches); + + if (VDBG) { + Log.d(TAG, "Notify on network switches: " + Arrays.toString(notifySwitches)); + } + + for (String notifySwitch : notifySwitches) { + if (TextUtils.isEmpty(notifySwitch)) continue; + String[] transports = notifySwitch.split("-", 2); + if (transports.length != 2) { + Log.e(TAG, "Invalid network switch notification configuration: " + notifySwitch); + continue; + } + int fromTransport = sTransportNames.get("TRANSPORT_" + transports[0]); + int toTransport = sTransportNames.get("TRANSPORT_" + transports[1]); + if (hasTransport(fromNai, fromTransport) && hasTransport(toNai, toTransport)) { + return true; + } + } + + return false; + } + + private void showNotification(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) { + PendingIntent pendingIntent = PendingIntent.getActivityAsUser( + mContext, 0, CELLULAR_SETTINGS, PendingIntent.FLAG_CANCEL_CURRENT, null, + UserHandle.CURRENT); + + mNotifier.showNotification(fromNai.network.netId, NotificationType.NETWORK_SWITCH, + fromNai, toNai, pendingIntent, true); + } + + // Removes any notification that was put up as a result of switching to nai. + private void maybeStopNotifying(NetworkAgentInfo nai) { + int fromNetId = getNotificationSource(nai); + if (fromNetId != NETID_UNSET) { + mNotifications.delete(fromNetId); + mNotifier.clearNotification(fromNetId); + // Toasts can't be deleted. + } + } + + // Notify the user of a network switch using a notification or a toast. + private void notify(NetworkAgentInfo fromNai, NetworkAgentInfo toNai, boolean forceToast) { + boolean notify = false; + int notifyType = mContext.getResources().getInteger( + com.android.internal.R.integer.config_networkNotifySwitchType); + + if (notifyType == NOTIFY_TYPE_NOTIFICATION && forceToast) { + notifyType = NOTIFY_TYPE_TOAST; + } + + switch (notifyType) { + case NOTIFY_TYPE_NONE: + break; + case NOTIFY_TYPE_NOTIFICATION: + showNotification(fromNai, toNai); + notify = true; + break; + case NOTIFY_TYPE_TOAST: + mNotifier.showToast(fromNai, toNai); + notify = true; + break; + default: + Log.e(TAG, "Unknown notify type " + notifyType); + } + + if (VDBG) { + Log.d(TAG, "Notify type: " + sNotifyTypeNames.get(notifyType, "" + notifyType)); + } + + if (notify) { + if (DBG) { + Log.d(TAG, "Notifying switch from=" + fromNai.name() + " to=" + toNai.name() + + " type=" + sNotifyTypeNames.get(notifyType, "unknown(" + notifyType + ")")); + } + mNotifications.put(fromNai.network.netId, toNai.network.netId); + mEverNotified.put(fromNai.network.netId, true); + } + } + + // The default network changed from fromNai to toNai due to a change in score. + public void noteLingerDefaultNetwork(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) { + if (VDBG) { + Log.d(TAG, "noteLingerDefaultNetwork from=" + fromNai.name() + + " everValidated=" + fromNai.everValidated + + " lastValidated=" + fromNai.lastValidated + + " to=" + toNai.name()); + } + + // If we are currently notifying the user because the device switched to fromNai, now that + // we are switching away from it we should remove the notification. This includes the case + // where we switch back to toNai because its score improved again (e.g., because it regained + // Internet access). + maybeStopNotifying(fromNai); + + // If this network never validated, don't notify. Otherwise, we could do things like: + // + // 1. Unvalidated wifi connects. + // 2. Unvalidated mobile data connects. + // 3. Cell validates, and we show a notification. + // or: + // 1. User connects to wireless printer. + // 2. User turns on cellular data. + // 3. We show a notification. + if (!fromNai.everValidated) return; + + // If this network is a captive portal, don't notify. This cannot happen on initial connect + // to a captive portal, because the everValidated check above will fail. However, it can + // happen if the captive portal reasserts itself (e.g., because its timeout fires). In that + // case, as soon as the captive portal reasserts itself, we'll show a sign-in notification. + // We don't want to overwrite that notification with this one; the user has already been + // notified, and of the two, the captive portal notification is the more useful one because + // it allows the user to sign in to the captive portal. In this case, display a toast + // in addition to the captive portal notification. + // + // Note that if the network we switch to is already up when the captive portal reappears, + // this won't work because NetworkMonitor tells ConnectivityService that the network is + // unvalidated (causing a switch) before asking it to show the sign in notification. In this + // case, the toast won't show and we'll only display the sign in notification. This is the + // best we can do at this time. + boolean forceToast = fromNai.networkCapabilities.hasCapability( + NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL); + + // Only show the notification once, in order to avoid irritating the user every time. + // TODO: should we do this? + if (everNotified(fromNai)) { + if (VDBG) { + Log.d(TAG, "Not notifying handover from " + fromNai.name() + ", already notified"); + } + return; + } + + if (isNotificationEnabled(fromNai, toNai)) { + notify(fromNai, toNai, forceToast); + } + } + + public void noteDisconnect(NetworkAgentInfo nai) { + mNotifications.delete(nai.network.netId); + mEverNotified.delete(nai.network.netId); + maybeStopNotifying(nai); + // No need to cancel notifications on nai: NetworkMonitor does that on disconnect. + } +} diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java index 7a25df6859..b0330b94a6 100644 --- a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java +++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java @@ -411,14 +411,17 @@ public class NetworkAgentInfo implements Comparable { /** * Cancel lingering. Called by ConnectivityService when a request is added to this network. + * Returns true if the given request was lingering on this network, false otherwise. */ - public void unlingerRequest(NetworkRequest request) { + public boolean unlingerRequest(NetworkRequest request) { LingerTimer timer = mLingerTimerForRequest.get(request.requestId); if (timer != null) { if (VDBG) Log.d(TAG, "Removing LingerTimer " + timer + " from " + this.name()); mLingerTimers.remove(timer); mLingerTimerForRequest.remove(request.requestId); + return true; } + return false; } public long getLingerExpiry() { diff --git a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java index 4680a8a105..99926a971b 100644 --- a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java +++ b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java @@ -19,6 +19,7 @@ package com.android.server.connectivity; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; +import android.widget.Toast; import android.content.Context; import android.content.Intent; import android.content.res.Resources; @@ -34,9 +35,9 @@ import static android.net.NetworkCapabilities.*; public class NetworkNotificationManager { - public static enum NotificationType { SIGN_IN, NO_INTERNET; }; + public static enum NotificationType { SIGN_IN, NO_INTERNET, NETWORK_SWITCH }; - private static final String NOTIFICATION_ID = "CaptivePortal.Notification"; + private static final String NOTIFICATION_ID = "Connectivity.Notification"; private static final String TAG = NetworkNotificationManager.class.getSimpleName(); private static final boolean DBG = true; @@ -90,9 +91,15 @@ public class NetworkNotificationManager { * @param id an identifier that uniquely identifies this notification. This must match * between show and hide calls. We use the NetID value but for legacy callers * we concatenate the range of types with the range of NetIDs. + * @param nai the network with which the notification is associated. For a SIGN_IN or + * NO_INTERNET notification, this is the network we're connecting to. For a + * NETWORK_SWITCH notification it's the network that we switched from. When this network + * disconnects the notification is removed. + * @param switchToNai for a NETWORK_SWITCH notification, the network we are switching to. Null + * in all other cases. Only used to determine the text of the notification. */ - public void showNotification(int id, NotificationType notifyType, - NetworkAgentInfo nai, PendingIntent intent, boolean highPriority) { + public void showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai, + NetworkAgentInfo switchToNai, PendingIntent intent, boolean highPriority) { int transportType; String extraInfo; if (nai != null) { @@ -136,29 +143,42 @@ public class NetworkNotificationManager { details = r.getString(R.string.network_available_sign_in_detailed, extraInfo); break; } + } else if (notifyType == NotificationType.NETWORK_SWITCH) { + String fromTransport = getTransportName(transportType); + String toTransport = getTransportName(getFirstTransportType(switchToNai)); + title = r.getString(R.string.network_switch_metered, toTransport); + details = r.getString(R.string.network_switch_metered_detail, toTransport, + fromTransport); } else { Slog.wtf(TAG, "Unknown notification type " + notifyType + "on network transport " + getTransportName(transportType)); return; } - Notification notification = new Notification.Builder(mContext) - .setWhen(0) + Notification.Builder builder = new Notification.Builder(mContext) + .setWhen(System.currentTimeMillis()) + .setShowWhen(notifyType == NotificationType.NETWORK_SWITCH) .setSmallIcon(icon) .setAutoCancel(true) .setTicker(title) .setColor(mContext.getColor( com.android.internal.R.color.system_notification_accent_color)) .setContentTitle(title) - .setContentText(details) .setContentIntent(intent) .setLocalOnly(true) .setPriority(highPriority ? Notification.PRIORITY_HIGH : Notification.PRIORITY_DEFAULT) .setDefaults(highPriority ? Notification.DEFAULT_ALL : 0) - .setOnlyAlertOnce(true) - .build(); + .setOnlyAlertOnce(true); + + if (notifyType == NotificationType.NETWORK_SWITCH) { + builder.setStyle(new Notification.BigTextStyle().bigText(details)); + } else { + builder.setContentText(details); + } + + Notification notification = builder.build(); try { mNotificationManager.notifyAsUser(NOTIFICATION_ID, id, notification, UserHandle.ALL); @@ -185,9 +205,17 @@ public class NetworkNotificationManager { if (visible) { Intent intent = new Intent(action); PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); - showNotification(id, NotificationType.SIGN_IN, null, pendingIntent, false); + showNotification(id, NotificationType.SIGN_IN, null, null, pendingIntent, false); } else { clearNotification(id); } } + + public void showToast(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) { + String fromTransport = getTransportName(getFirstTransportType(fromNai)); + String toTransport = getTransportName(getFirstTransportType(toNai)); + String text = mContext.getResources().getString( + R.string.network_switch_metered_toast, fromTransport, toTransport); + Toast.makeText(mContext, text, Toast.LENGTH_LONG).show(); + } }