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