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