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.NetworkCapabilities.NET_CAPABILITY_INTERNET; 20 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; 21 import static android.net.NetworkCapabilities.TRANSPORT_VPN; 22 import static android.net.NetworkCapabilities.TRANSPORT_WIFI; 23 24 import android.annotation.NonNull; 25 import android.app.ActivityOptions; 26 import android.app.Notification; 27 import android.app.NotificationManager; 28 import android.app.PendingIntent; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.res.Resources; 32 import android.graphics.drawable.Icon; 33 import android.net.NetworkSpecifier; 34 import android.net.TelephonyNetworkSpecifier; 35 import android.net.wifi.WifiInfo; 36 import android.os.Build; 37 import android.os.Bundle; 38 import android.os.UserHandle; 39 import android.telephony.SubscriptionManager; 40 import android.telephony.TelephonyManager; 41 import android.text.TextUtils; 42 import android.util.Log; 43 import android.util.SparseArray; 44 import android.util.SparseIntArray; 45 import android.widget.Toast; 46 47 import com.android.connectivity.resources.R; 48 import com.android.internal.annotations.VisibleForTesting; 49 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; 50 import com.android.modules.utils.build.SdkLevel; 51 52 public class NetworkNotificationManager { 53 54 55 public static enum NotificationType { 56 LOST_INTERNET(SystemMessage.NOTE_NETWORK_LOST_INTERNET), 57 NETWORK_SWITCH(SystemMessage.NOTE_NETWORK_SWITCH), 58 NO_INTERNET(SystemMessage.NOTE_NETWORK_NO_INTERNET), 59 PARTIAL_CONNECTIVITY(SystemMessage.NOTE_NETWORK_PARTIAL_CONNECTIVITY), 60 SIGN_IN(SystemMessage.NOTE_NETWORK_SIGN_IN), 61 PRIVATE_DNS_BROKEN(SystemMessage.NOTE_NETWORK_PRIVATE_DNS_BROKEN); 62 63 public final int eventId; 64 NotificationType(int eventId)65 NotificationType(int eventId) { 66 this.eventId = eventId; 67 Holder.sIdToTypeMap.put(eventId, this); 68 } 69 70 private static class Holder { 71 private static SparseArray<NotificationType> sIdToTypeMap = new SparseArray<>(); 72 } 73 getFromId(int id)74 public static NotificationType getFromId(int id) { 75 return Holder.sIdToTypeMap.get(id); 76 } 77 }; 78 79 private static final String TAG = NetworkNotificationManager.class.getSimpleName(); 80 private static final boolean DBG = true; 81 82 // Notification channels used by ConnectivityService mainline module, it should be aligned with 83 // SystemNotificationChannels so the channels are the same as the ones used as the system 84 // server. 85 public static final String NOTIFICATION_CHANNEL_NETWORK_STATUS = "NETWORK_STATUS"; 86 public static final String NOTIFICATION_CHANNEL_NETWORK_ALERTS = "NETWORK_ALERTS"; 87 88 // The context is for the current user (system server) 89 private final Context mContext; 90 private final ConnectivityResources mResources; 91 private final TelephonyManager mTelephonyManager; 92 // The notification manager is created from a context for User.ALL, so notifications 93 // will be sent to all users. 94 private final NotificationManager mNotificationManager; 95 // Tracks the types of notifications managed by this instance, from creation to cancellation. 96 private final SparseIntArray mNotificationTypeMap; 97 NetworkNotificationManager(@onNull final Context c, @NonNull final TelephonyManager t)98 public NetworkNotificationManager(@NonNull final Context c, @NonNull final TelephonyManager t) { 99 mContext = c; 100 mTelephonyManager = t; 101 mNotificationManager = 102 (NotificationManager) c.createContextAsUser(UserHandle.ALL, 0 /* flags */) 103 .getSystemService(Context.NOTIFICATION_SERVICE); 104 mNotificationTypeMap = new SparseIntArray(); 105 mResources = new ConnectivityResources(mContext); 106 } 107 108 @VisibleForTesting approximateTransportType(NetworkAgentInfo nai)109 protected static int approximateTransportType(NetworkAgentInfo nai) { 110 return nai.isVPN() ? TRANSPORT_VPN : getFirstTransportType(nai); 111 } 112 113 // TODO: deal more gracefully with multi-transport networks. getFirstTransportType(NetworkAgentInfo nai)114 private static int getFirstTransportType(NetworkAgentInfo nai) { 115 // TODO: The range is wrong, the safer and correct way is to change the range from 116 // MIN_TRANSPORT to MAX_TRANSPORT. 117 for (int i = 0; i < 64; i++) { 118 if (nai.networkCapabilities.hasTransport(i)) return i; 119 } 120 return -1; 121 } 122 getTransportName(final int transportType)123 private String getTransportName(final int transportType) { 124 String[] networkTypes = mResources.get().getStringArray(R.array.network_switch_type_name); 125 try { 126 return networkTypes[transportType]; 127 } catch (IndexOutOfBoundsException e) { 128 return mResources.get().getString(R.string.network_switch_type_name_unknown); 129 } 130 } 131 getIcon(int transportType)132 private static int getIcon(int transportType) { 133 return (transportType == TRANSPORT_WIFI) 134 ? R.drawable.stat_notify_wifi_in_range // TODO: Distinguish ! from ?. 135 : R.drawable.stat_notify_rssi_in_range; 136 } 137 138 /** 139 * Show or hide network provisioning notifications. 140 * 141 * We use notifications for two purposes: to notify that a network requires sign in 142 * (NotificationType.SIGN_IN), or to notify that a network does not have Internet access 143 * (NotificationType.NO_INTERNET). We display at most one notification per ID, so on a 144 * particular network we can display the notification type that was most recently requested. 145 * So for example if a captive portal fails to reply within a few seconds of connecting, we 146 * might first display NO_INTERNET, and then when the captive portal check completes, display 147 * SIGN_IN. 148 * 149 * @param id an identifier that uniquely identifies this notification. This must match 150 * between show and hide calls. We use the NetID value but for legacy callers 151 * we concatenate the range of types with the range of NetIDs. 152 * @param notifyType the type of the notification. 153 * @param nai the network with which the notification is associated. For a SIGN_IN, NO_INTERNET, 154 * or LOST_INTERNET notification, this is the network we're connecting to. For a 155 * NETWORK_SWITCH notification it's the network that we switched from. When this network 156 * disconnects the notification is removed. 157 * @param switchToNai for a NETWORK_SWITCH notification, the network we are switching to. Null 158 * in all other cases. Only used to determine the text of the notification. 159 */ showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai, NetworkAgentInfo switchToNai, PendingIntent intent, boolean highPriority)160 public void showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai, 161 NetworkAgentInfo switchToNai, PendingIntent intent, boolean highPriority) { 162 final String tag = tagFor(id); 163 final int eventId = notifyType.eventId; 164 final int transportType; 165 final CharSequence name; 166 if (nai != null) { 167 transportType = approximateTransportType(nai); 168 final String extraInfo = nai.networkInfo.getExtraInfo(); 169 if (nai.linkProperties != null && nai.linkProperties.getCaptivePortalData() != null 170 && !TextUtils.isEmpty(nai.linkProperties.getCaptivePortalData() 171 .getVenueFriendlyName())) { 172 name = nai.linkProperties.getCaptivePortalData().getVenueFriendlyName(); 173 } else { 174 name = TextUtils.isEmpty(extraInfo) 175 ? WifiInfo.sanitizeSsid(nai.networkCapabilities.getSsid()) : extraInfo; 176 } 177 // Only notify for Internet-capable networks. 178 if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET)) return; 179 } else { 180 // Legacy notifications. 181 transportType = TRANSPORT_CELLULAR; 182 name = ""; 183 } 184 185 // Clear any previous notification with lower priority, otherwise return. http://b/63676954. 186 // A new SIGN_IN notification with a new intent should override any existing one. 187 final int previousEventId = mNotificationTypeMap.get(id); 188 final NotificationType previousNotifyType = NotificationType.getFromId(previousEventId); 189 if (priority(previousNotifyType) > priority(notifyType)) { 190 Log.d(TAG, String.format( 191 "ignoring notification %s for network %s with existing notification %s", 192 notifyType, id, previousNotifyType)); 193 return; 194 } 195 clearNotification(id); 196 197 if (DBG) { 198 Log.d(TAG, String.format( 199 "showNotification tag=%s event=%s transport=%s name=%s highPriority=%s", 200 tag, nameOf(eventId), getTransportName(transportType), name, highPriority)); 201 } 202 203 final Resources r = mResources.get(); 204 if (highPriority && maybeNotifyViaDialog(r, notifyType, intent)) { 205 Log.d(TAG, "Notified via dialog for event " + nameOf(eventId)); 206 return; 207 } 208 209 final CharSequence title; 210 final CharSequence details; 211 Icon icon = Icon.createWithResource( 212 mResources.getResourcesContext(), getIcon(transportType)); 213 final boolean showAsNoInternet = notifyType == NotificationType.PARTIAL_CONNECTIVITY 214 && r.getBoolean(R.bool.config_partialConnectivityNotifiedAsNoInternet); 215 if (showAsNoInternet) { 216 Log.d(TAG, "Showing partial connectivity as NO_INTERNET"); 217 } 218 if ((notifyType == NotificationType.NO_INTERNET || showAsNoInternet) 219 && transportType == TRANSPORT_WIFI) { 220 title = r.getString(R.string.wifi_no_internet, name); 221 details = r.getString(R.string.wifi_no_internet_detailed); 222 } else if (notifyType == NotificationType.PRIVATE_DNS_BROKEN) { 223 if (transportType == TRANSPORT_CELLULAR) { 224 title = r.getString(R.string.mobile_no_internet); 225 } else if (transportType == TRANSPORT_WIFI) { 226 title = r.getString(R.string.wifi_no_internet, name); 227 } else { 228 title = r.getString(R.string.other_networks_no_internet); 229 } 230 details = r.getString(R.string.private_dns_broken_detailed); 231 } else if (notifyType == NotificationType.PARTIAL_CONNECTIVITY 232 && transportType == TRANSPORT_WIFI) { 233 title = r.getString(R.string.network_partial_connectivity, name); 234 details = r.getString(R.string.network_partial_connectivity_detailed); 235 } else if (notifyType == NotificationType.LOST_INTERNET && 236 transportType == TRANSPORT_WIFI) { 237 title = r.getString(R.string.wifi_no_internet, name); 238 details = r.getString(R.string.wifi_no_internet_detailed); 239 } else if (notifyType == NotificationType.SIGN_IN) { 240 switch (transportType) { 241 case TRANSPORT_WIFI: 242 title = r.getString(R.string.wifi_available_sign_in, 0); 243 details = r.getString(R.string.network_available_sign_in_detailed, name); 244 break; 245 case TRANSPORT_CELLULAR: 246 title = r.getString(R.string.network_available_sign_in, 0); 247 // TODO: Change this to pull from NetworkInfo once a printable 248 // name has been added to it 249 NetworkSpecifier specifier = nai.networkCapabilities.getNetworkSpecifier(); 250 int subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID; 251 if (specifier instanceof TelephonyNetworkSpecifier) { 252 subId = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId(); 253 } 254 255 details = mTelephonyManager.createForSubscriptionId(subId) 256 .getNetworkOperatorName(); 257 break; 258 default: 259 title = r.getString(R.string.network_available_sign_in, 0); 260 details = r.getString(R.string.network_available_sign_in_detailed, name); 261 break; 262 } 263 } else if (notifyType == NotificationType.NETWORK_SWITCH) { 264 String fromTransport = getTransportName(transportType); 265 String toTransport = getTransportName(approximateTransportType(switchToNai)); 266 title = r.getString(R.string.network_switch_metered, toTransport); 267 details = r.getString(R.string.network_switch_metered_detail, toTransport, 268 fromTransport); 269 } else if (notifyType == NotificationType.NO_INTERNET 270 || notifyType == NotificationType.PARTIAL_CONNECTIVITY) { 271 // NO_INTERNET and PARTIAL_CONNECTIVITY notification for non-WiFi networks 272 // are sent, but they are not implemented yet. 273 return; 274 } else { 275 Log.wtf(TAG, "Unknown notification type " + notifyType + " on network transport " 276 + getTransportName(transportType)); 277 return; 278 } 279 // When replacing an existing notification for a given network, don't alert, just silently 280 // update the existing notification. Note that setOnlyAlertOnce() will only work for the 281 // same id, and the id used here is the NotificationType which is different in every type of 282 // notification. This is required because the notification metrics only track the ID but not 283 // the tag. 284 final boolean hasPreviousNotification = previousNotifyType != null; 285 final String channelId = (highPriority && !hasPreviousNotification) 286 ? NOTIFICATION_CHANNEL_NETWORK_ALERTS : NOTIFICATION_CHANNEL_NETWORK_STATUS; 287 Notification.Builder builder = new Notification.Builder(mContext, channelId) 288 .setWhen(System.currentTimeMillis()) 289 .setShowWhen(notifyType == NotificationType.NETWORK_SWITCH) 290 .setSmallIcon(icon) 291 .setAutoCancel(r.getBoolean(R.bool.config_autoCancelNetworkNotifications)) 292 .setTicker(title) 293 .setColor(mContext.getColor(android.R.color.system_notification_accent_color)) 294 .setContentTitle(title) 295 .setContentIntent(intent) 296 .setLocalOnly(true) 297 .setOnlyAlertOnce(true) 298 // TODO: consider having action buttons to disconnect on the sign-in notification 299 // especially if it is ongoing 300 .setOngoing(notifyType == NotificationType.SIGN_IN 301 && r.getBoolean(R.bool.config_ongoingSignInNotification)); 302 303 if (notifyType == NotificationType.NETWORK_SWITCH) { 304 builder.setStyle(new Notification.BigTextStyle().bigText(details)); 305 } else { 306 builder.setContentText(details); 307 } 308 309 if (notifyType == NotificationType.SIGN_IN) { 310 builder.extend(new Notification.TvExtender().setChannelId(channelId)); 311 } 312 313 Notification notification = builder.build(); 314 315 mNotificationTypeMap.put(id, eventId); 316 try { 317 mNotificationManager.notify(tag, eventId, notification); 318 } catch (NullPointerException npe) { 319 Log.d(TAG, "setNotificationVisible: visible notificationManager error", npe); 320 } 321 } 322 maybeNotifyViaDialog(Resources res, NotificationType notifyType, PendingIntent intent)323 private boolean maybeNotifyViaDialog(Resources res, NotificationType notifyType, 324 PendingIntent intent) { 325 final boolean activelyPreferBadWifi = SdkLevel.isAtLeastU() 326 || (SdkLevel.isAtLeastT() 327 && res.getInteger(R.integer.config_activelyPreferBadWifi) == 1); 328 if ((notifyType != NotificationType.LOST_INTERNET || !activelyPreferBadWifi) 329 && notifyType != NotificationType.NO_INTERNET 330 && notifyType != NotificationType.PARTIAL_CONNECTIVITY) { 331 return false; 332 } 333 if (!res.getBoolean(R.bool.config_notifyNoInternetAsDialogWhenHighPriority)) { 334 return false; 335 } 336 337 try { 338 Bundle options = null; 339 340 if (SdkLevel.isAtLeastU() && intent.isActivity()) { 341 // Also check SDK_INT >= T separately, as the linter in some T-based branches does 342 // not recognize "isAtLeastU && something" as an SDK check for T+ APIs. 343 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 344 // Android U requires pending intent background start mode to be specified: 345 // See #background-activity-restrictions in 346 // https://developer.android.com/about/versions/14/behavior-changes-14 347 // But setPendingIntentBackgroundActivityStartMode is U+, and replaces 348 // setPendingIntentBackgroundActivityLaunchAllowed which is T+ but deprecated. 349 // Use setPendingIntentBackgroundActivityLaunchAllowed as the U+ version is not 350 // yet available in all branches. 351 final ActivityOptions activityOptions = ActivityOptions.makeBasic(); 352 activityOptions.setPendingIntentBackgroundActivityLaunchAllowed(true); 353 options = activityOptions.toBundle(); 354 } 355 } 356 357 intent.send(null, 0, null, null, null, null, options); 358 } catch (PendingIntent.CanceledException e) { 359 Log.e(TAG, "Error sending dialog PendingIntent", e); 360 } 361 return true; 362 } 363 364 /** 365 * Clear the notification with the given id, only if it matches the given type. 366 */ clearNotification(int id, NotificationType notifyType)367 public void clearNotification(int id, NotificationType notifyType) { 368 final int previousEventId = mNotificationTypeMap.get(id); 369 final NotificationType previousNotifyType = NotificationType.getFromId(previousEventId); 370 if (notifyType != previousNotifyType) { 371 return; 372 } 373 clearNotification(id); 374 } 375 clearNotification(int id)376 public void clearNotification(int id) { 377 if (mNotificationTypeMap.indexOfKey(id) < 0) { 378 return; 379 } 380 final String tag = tagFor(id); 381 final int eventId = mNotificationTypeMap.get(id); 382 if (DBG) { 383 Log.d(TAG, String.format("clearing notification tag=%s event=%s", tag, 384 nameOf(eventId))); 385 } 386 try { 387 mNotificationManager.cancel(tag, eventId); 388 } catch (NullPointerException npe) { 389 Log.d(TAG, String.format( 390 "failed to clear notification tag=%s event=%s", tag, nameOf(eventId)), npe); 391 } 392 mNotificationTypeMap.delete(id); 393 } 394 395 /** 396 * Legacy provisioning notifications coming directly from DcTracker. 397 */ setProvNotificationVisible(boolean visible, int id, String action)398 public void setProvNotificationVisible(boolean visible, int id, String action) { 399 if (visible) { 400 // For legacy purposes, action is sent as the action + the phone ID from DcTracker. 401 // Split the string here and send the phone ID as an extra instead. 402 String[] splitAction = action.split(":"); 403 Intent intent = new Intent(splitAction[0]); 404 try { 405 intent.putExtra("provision.phone.id", Integer.parseInt(splitAction[1])); 406 } catch (NumberFormatException ignored) { } 407 PendingIntent pendingIntent = PendingIntent.getBroadcast( 408 mContext, 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE); 409 showNotification(id, NotificationType.SIGN_IN, null, null, pendingIntent, false); 410 } else { 411 clearNotification(id); 412 } 413 } 414 showToast(NetworkAgentInfo fromNai, NetworkAgentInfo toNai)415 public void showToast(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) { 416 String fromTransport = getTransportName(approximateTransportType(fromNai)); 417 String toTransport = getTransportName(approximateTransportType(toNai)); 418 String text = mResources.get().getString( 419 R.string.network_switch_metered_toast, fromTransport, toTransport); 420 Toast.makeText(mContext, text, Toast.LENGTH_LONG).show(); 421 } 422 423 /** Get the logging tag for a notification ID */ 424 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) tagFor(int id)425 public static String tagFor(int id) { 426 return String.format("ConnectivityNotification:%d", id); 427 } 428 429 @VisibleForTesting nameOf(int eventId)430 static String nameOf(int eventId) { 431 NotificationType t = NotificationType.getFromId(eventId); 432 return (t != null) ? t.name() : "UNKNOWN"; 433 } 434 435 /** 436 * A notification with a higher number will take priority over a notification with a lower 437 * number. 438 */ 439 @VisibleForTesting priority(NotificationType t)440 public static int priority(NotificationType t) { 441 if (t == null) { 442 return 0; 443 } 444 switch (t) { 445 case SIGN_IN: 446 return 6; 447 case PARTIAL_CONNECTIVITY: 448 return 5; 449 case PRIVATE_DNS_BROKEN: 450 return 4; 451 case NO_INTERNET: 452 return 3; 453 case NETWORK_SWITCH: 454 return 2; 455 case LOST_INTERNET: 456 return 1; 457 default: 458 return 0; 459 } 460 } 461 } 462