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