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