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