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