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