1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.connectivity; 18 19 import static android.net.ConnectivityManager.NETID_UNSET; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.app.PendingIntent; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.net.NetworkCapabilities; 28 import android.os.SystemClock; 29 import android.os.UserHandle; 30 import android.text.TextUtils; 31 import android.text.format.DateUtils; 32 import android.util.Log; 33 import android.util.SparseArray; 34 import android.util.SparseBooleanArray; 35 import android.util.SparseIntArray; 36 37 import com.android.internal.R; 38 import com.android.internal.annotations.VisibleForTesting; 39 import com.android.internal.util.MessageUtils; 40 import com.android.server.connectivity.NetworkNotificationManager.NotificationType; 41 42 import java.util.Arrays; 43 import java.util.HashMap; 44 45 /** 46 * Class that monitors default network linger events and possibly notifies the user of network 47 * switches. 48 * 49 * This class is not thread-safe and all its methods must be called on the ConnectivityService 50 * handler thread. 51 */ 52 public class LingerMonitor { 53 54 private static final boolean DBG = true; 55 private static final boolean VDBG = false; 56 private static final String TAG = LingerMonitor.class.getSimpleName(); 57 58 public static final int DEFAULT_NOTIFICATION_DAILY_LIMIT = 3; 59 public static final long DEFAULT_NOTIFICATION_RATE_LIMIT_MILLIS = DateUtils.MINUTE_IN_MILLIS; 60 61 private static final HashMap<String, Integer> TRANSPORT_NAMES = makeTransportToNameMap(); 62 @VisibleForTesting 63 public static final Intent CELLULAR_SETTINGS = new Intent().setComponent(new ComponentName( 64 "com.android.settings", "com.android.settings.Settings$DataUsageSummaryActivity")); 65 66 @VisibleForTesting 67 public static final int NOTIFY_TYPE_NONE = 0; 68 public static final int NOTIFY_TYPE_NOTIFICATION = 1; 69 public static final int NOTIFY_TYPE_TOAST = 2; 70 71 private static SparseArray<String> sNotifyTypeNames = MessageUtils.findMessageNames( 72 new Class[] { LingerMonitor.class }, new String[]{ "NOTIFY_TYPE_" }); 73 74 private final Context mContext; 75 private final NetworkNotificationManager mNotifier; 76 private final int mDailyLimit; 77 private final long mRateLimitMillis; 78 79 private long mFirstNotificationMillis; 80 private long mLastNotificationMillis; 81 private int mNotificationCounter; 82 83 /** Current notifications. Maps the netId we switched away from to the netId we switched to. */ 84 private final SparseIntArray mNotifications = new SparseIntArray(); 85 86 /** Whether we ever notified that we switched away from a particular network. */ 87 private final SparseBooleanArray mEverNotified = new SparseBooleanArray(); 88 LingerMonitor(Context context, NetworkNotificationManager notifier, int dailyLimit, long rateLimitMillis)89 public LingerMonitor(Context context, NetworkNotificationManager notifier, 90 int dailyLimit, long rateLimitMillis) { 91 mContext = context; 92 mNotifier = notifier; 93 mDailyLimit = dailyLimit; 94 mRateLimitMillis = rateLimitMillis; 95 // Ensure that (now - mLastNotificationMillis) >= rateLimitMillis at first 96 mLastNotificationMillis = -rateLimitMillis; 97 } 98 makeTransportToNameMap()99 private static HashMap<String, Integer> makeTransportToNameMap() { 100 SparseArray<String> numberToName = MessageUtils.findMessageNames( 101 new Class[] { NetworkCapabilities.class }, new String[]{ "TRANSPORT_" }); 102 HashMap<String, Integer> nameToNumber = new HashMap<>(); 103 for (int i = 0; i < numberToName.size(); i++) { 104 // MessageUtils will fail to initialize if there are duplicate constant values, so there 105 // are no duplicates here. 106 nameToNumber.put(numberToName.valueAt(i), numberToName.keyAt(i)); 107 } 108 return nameToNumber; 109 } 110 hasTransport(NetworkAgentInfo nai, int transport)111 private static boolean hasTransport(NetworkAgentInfo nai, int transport) { 112 return nai.networkCapabilities.hasTransport(transport); 113 } 114 getNotificationSource(NetworkAgentInfo toNai)115 private int getNotificationSource(NetworkAgentInfo toNai) { 116 for (int i = 0; i < mNotifications.size(); i++) { 117 if (mNotifications.valueAt(i) == toNai.network.netId) { 118 return mNotifications.keyAt(i); 119 } 120 } 121 return NETID_UNSET; 122 } 123 everNotified(NetworkAgentInfo nai)124 private boolean everNotified(NetworkAgentInfo nai) { 125 return mEverNotified.get(nai.network.netId, false); 126 } 127 128 @VisibleForTesting isNotificationEnabled(NetworkAgentInfo fromNai, NetworkAgentInfo toNai)129 public boolean isNotificationEnabled(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) { 130 // TODO: Evaluate moving to CarrierConfigManager. 131 String[] notifySwitches = 132 mContext.getResources().getStringArray(R.array.config_networkNotifySwitches); 133 134 if (VDBG) { 135 Log.d(TAG, "Notify on network switches: " + Arrays.toString(notifySwitches)); 136 } 137 138 for (String notifySwitch : notifySwitches) { 139 if (TextUtils.isEmpty(notifySwitch)) continue; 140 String[] transports = notifySwitch.split("-", 2); 141 if (transports.length != 2) { 142 Log.e(TAG, "Invalid network switch notification configuration: " + notifySwitch); 143 continue; 144 } 145 int fromTransport = TRANSPORT_NAMES.get("TRANSPORT_" + transports[0]); 146 int toTransport = TRANSPORT_NAMES.get("TRANSPORT_" + transports[1]); 147 if (hasTransport(fromNai, fromTransport) && hasTransport(toNai, toTransport)) { 148 return true; 149 } 150 } 151 152 return false; 153 } 154 showNotification(NetworkAgentInfo fromNai, NetworkAgentInfo toNai)155 private void showNotification(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) { 156 mNotifier.showNotification(fromNai.network.netId, NotificationType.NETWORK_SWITCH, 157 fromNai, toNai, createNotificationIntent(), true); 158 } 159 160 @VisibleForTesting createNotificationIntent()161 protected PendingIntent createNotificationIntent() { 162 return PendingIntent.getActivityAsUser(mContext, 0, CELLULAR_SETTINGS, 163 PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); 164 } 165 166 // Removes any notification that was put up as a result of switching to nai. maybeStopNotifying(NetworkAgentInfo nai)167 private void maybeStopNotifying(NetworkAgentInfo nai) { 168 int fromNetId = getNotificationSource(nai); 169 if (fromNetId != NETID_UNSET) { 170 mNotifications.delete(fromNetId); 171 mNotifier.clearNotification(fromNetId); 172 // Toasts can't be deleted. 173 } 174 } 175 176 // Notify the user of a network switch using a notification or a toast. notify(NetworkAgentInfo fromNai, NetworkAgentInfo toNai, boolean forceToast)177 private void notify(NetworkAgentInfo fromNai, NetworkAgentInfo toNai, boolean forceToast) { 178 int notifyType = 179 mContext.getResources().getInteger(R.integer.config_networkNotifySwitchType); 180 if (notifyType == NOTIFY_TYPE_NOTIFICATION && forceToast) { 181 notifyType = NOTIFY_TYPE_TOAST; 182 } 183 184 if (VDBG) { 185 Log.d(TAG, "Notify type: " + sNotifyTypeNames.get(notifyType, "" + notifyType)); 186 } 187 188 switch (notifyType) { 189 case NOTIFY_TYPE_NONE: 190 return; 191 case NOTIFY_TYPE_NOTIFICATION: 192 showNotification(fromNai, toNai); 193 break; 194 case NOTIFY_TYPE_TOAST: 195 mNotifier.showToast(fromNai, toNai); 196 break; 197 default: 198 Log.e(TAG, "Unknown notify type " + notifyType); 199 return; 200 } 201 202 if (DBG) { 203 Log.d(TAG, "Notifying switch from=" + fromNai.toShortString() 204 + " to=" + toNai.toShortString() 205 + " type=" + sNotifyTypeNames.get(notifyType, "unknown(" + notifyType + ")")); 206 } 207 208 mNotifications.put(fromNai.network.netId, toNai.network.netId); 209 mEverNotified.put(fromNai.network.netId, true); 210 } 211 212 /** 213 * Put up or dismiss a notification or toast for of a change in the default network if needed. 214 * 215 * Putting up a notification when switching from no network to some network is not supported 216 * and as such this method can't be called with a null |fromNai|. It can be called with a 217 * null |toNai| if there isn't a default network any more. 218 * 219 * @param fromNai switching from this NAI 220 * @param toNai switching to this NAI 221 */ 222 // The default network changed from fromNai to toNai due to a change in score. noteLingerDefaultNetwork(@onNull final NetworkAgentInfo fromNai, @Nullable final NetworkAgentInfo toNai)223 public void noteLingerDefaultNetwork(@NonNull final NetworkAgentInfo fromNai, 224 @Nullable final NetworkAgentInfo toNai) { 225 if (VDBG) { 226 Log.d(TAG, "noteLingerDefaultNetwork from=" + fromNai.toShortString() 227 + " everValidated=" + fromNai.everValidated 228 + " lastValidated=" + fromNai.lastValidated 229 + " to=" + toNai.toShortString()); 230 } 231 232 // If we are currently notifying the user because the device switched to fromNai, now that 233 // we are switching away from it we should remove the notification. This includes the case 234 // where we switch back to toNai because its score improved again (e.g., because it regained 235 // Internet access). 236 maybeStopNotifying(fromNai); 237 238 // If the network was simply lost (either because it disconnected or because it stopped 239 // being the default with no replacement), then don't show a notification. 240 if (null == toNai) return; 241 242 // If this network never validated, don't notify. Otherwise, we could do things like: 243 // 244 // 1. Unvalidated wifi connects. 245 // 2. Unvalidated mobile data connects. 246 // 3. Cell validates, and we show a notification. 247 // or: 248 // 1. User connects to wireless printer. 249 // 2. User turns on cellular data. 250 // 3. We show a notification. 251 if (!fromNai.everValidated) return; 252 253 // If this network is a captive portal, don't notify. This cannot happen on initial connect 254 // to a captive portal, because the everValidated check above will fail. However, it can 255 // happen if the captive portal reasserts itself (e.g., because its timeout fires). In that 256 // case, as soon as the captive portal reasserts itself, we'll show a sign-in notification. 257 // We don't want to overwrite that notification with this one; the user has already been 258 // notified, and of the two, the captive portal notification is the more useful one because 259 // it allows the user to sign in to the captive portal. In this case, display a toast 260 // in addition to the captive portal notification. 261 // 262 // Note that if the network we switch to is already up when the captive portal reappears, 263 // this won't work because NetworkMonitor tells ConnectivityService that the network is 264 // unvalidated (causing a switch) before asking it to show the sign in notification. In this 265 // case, the toast won't show and we'll only display the sign in notification. This is the 266 // best we can do at this time. 267 boolean forceToast = fromNai.networkCapabilities.hasCapability( 268 NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL); 269 270 // Only show the notification once, in order to avoid irritating the user every time. 271 // TODO: should we do this? 272 if (everNotified(fromNai)) { 273 if (VDBG) { 274 Log.d(TAG, "Not notifying handover from " + fromNai.toShortString() 275 + ", already notified"); 276 } 277 return; 278 } 279 280 // Only show the notification if we switched away because a network became unvalidated, not 281 // because its score changed. 282 // TODO: instead of just skipping notification, keep a note of it, and show it if it becomes 283 // unvalidated. 284 if (fromNai.lastValidated) return; 285 286 if (!isNotificationEnabled(fromNai, toNai)) return; 287 288 final long now = SystemClock.elapsedRealtime(); 289 if (isRateLimited(now) || isAboveDailyLimit(now)) return; 290 291 notify(fromNai, toNai, forceToast); 292 } 293 noteDisconnect(NetworkAgentInfo nai)294 public void noteDisconnect(NetworkAgentInfo nai) { 295 mNotifications.delete(nai.network.netId); 296 mEverNotified.delete(nai.network.netId); 297 maybeStopNotifying(nai); 298 // No need to cancel notifications on nai: NetworkMonitor does that on disconnect. 299 } 300 isRateLimited(long now)301 private boolean isRateLimited(long now) { 302 final long millisSinceLast = now - mLastNotificationMillis; 303 if (millisSinceLast < mRateLimitMillis) { 304 return true; 305 } 306 mLastNotificationMillis = now; 307 return false; 308 } 309 isAboveDailyLimit(long now)310 private boolean isAboveDailyLimit(long now) { 311 if (mFirstNotificationMillis == 0) { 312 mFirstNotificationMillis = now; 313 } 314 final long millisSinceFirst = now - mFirstNotificationMillis; 315 if (millisSinceFirst > DateUtils.DAY_IN_MILLIS) { 316 mNotificationCounter = 0; 317 mFirstNotificationMillis = 0; 318 } 319 if (mNotificationCounter >= mDailyLimit) { 320 return true; 321 } 322 mNotificationCounter++; 323 return false; 324 } 325 } 326