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