• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.annotation.NonNull;
25 import android.app.Notification;
26 import android.app.NotificationManager;
27 import android.app.PendingIntent;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.res.Resources;
31 import android.graphics.drawable.Icon;
32 import android.net.ConnectivityResources;
33 import android.net.NetworkSpecifier;
34 import android.net.TelephonyNetworkSpecifier;
35 import android.net.wifi.WifiInfo;
36 import android.os.UserHandle;
37 import android.telephony.SubscriptionManager;
38 import android.telephony.TelephonyManager;
39 import android.text.TextUtils;
40 import android.util.Log;
41 import android.util.SparseArray;
42 import android.util.SparseIntArray;
43 import android.widget.Toast;
44 
45 import com.android.connectivity.resources.R;
46 import com.android.internal.annotations.VisibleForTesting;
47 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
48 
49 public class NetworkNotificationManager {
50 
51 
52     public static enum NotificationType {
53         LOST_INTERNET(SystemMessage.NOTE_NETWORK_LOST_INTERNET),
54         NETWORK_SWITCH(SystemMessage.NOTE_NETWORK_SWITCH),
55         NO_INTERNET(SystemMessage.NOTE_NETWORK_NO_INTERNET),
56         PARTIAL_CONNECTIVITY(SystemMessage.NOTE_NETWORK_PARTIAL_CONNECTIVITY),
57         SIGN_IN(SystemMessage.NOTE_NETWORK_SIGN_IN),
58         PRIVATE_DNS_BROKEN(SystemMessage.NOTE_NETWORK_PRIVATE_DNS_BROKEN);
59 
60         public final int eventId;
61 
NotificationType(int eventId)62         NotificationType(int eventId) {
63             this.eventId = eventId;
64             Holder.sIdToTypeMap.put(eventId, this);
65         }
66 
67         private static class Holder {
68             private static SparseArray<NotificationType> sIdToTypeMap = new SparseArray<>();
69         }
70 
getFromId(int id)71         public static NotificationType getFromId(int id) {
72             return Holder.sIdToTypeMap.get(id);
73         }
74     };
75 
76     private static final String TAG = NetworkNotificationManager.class.getSimpleName();
77     private static final boolean DBG = true;
78 
79     // Notification channels used by ConnectivityService mainline module, it should be aligned with
80     // SystemNotificationChannels so the channels are the same as the ones used as the system
81     // server.
82     public static final String NOTIFICATION_CHANNEL_NETWORK_STATUS = "NETWORK_STATUS";
83     public static final String NOTIFICATION_CHANNEL_NETWORK_ALERTS = "NETWORK_ALERTS";
84 
85     // The context is for the current user (system server)
86     private final Context mContext;
87     private final ConnectivityResources mResources;
88     private final TelephonyManager mTelephonyManager;
89     // The notification manager is created from a context for User.ALL, so notifications
90     // will be sent to all users.
91     private final NotificationManager mNotificationManager;
92     // Tracks the types of notifications managed by this instance, from creation to cancellation.
93     private final SparseIntArray mNotificationTypeMap;
94 
NetworkNotificationManager(@onNull final Context c, @NonNull final TelephonyManager t)95     public NetworkNotificationManager(@NonNull final Context c, @NonNull final TelephonyManager t) {
96         mContext = c;
97         mTelephonyManager = t;
98         mNotificationManager =
99                 (NotificationManager) c.createContextAsUser(UserHandle.ALL, 0 /* flags */)
100                         .getSystemService(Context.NOTIFICATION_SERVICE);
101         mNotificationTypeMap = new SparseIntArray();
102         mResources = new ConnectivityResources(mContext);
103     }
104 
105     @VisibleForTesting
approximateTransportType(NetworkAgentInfo nai)106     protected static int approximateTransportType(NetworkAgentInfo nai) {
107         return nai.isVPN() ? TRANSPORT_VPN : getFirstTransportType(nai);
108     }
109 
110     // TODO: deal more gracefully with multi-transport networks.
getFirstTransportType(NetworkAgentInfo nai)111     private static int getFirstTransportType(NetworkAgentInfo nai) {
112         // TODO: The range is wrong, the safer and correct way is to change the range from
113         // MIN_TRANSPORT to MAX_TRANSPORT.
114         for (int i = 0; i < 64; i++) {
115             if (nai.networkCapabilities.hasTransport(i)) return i;
116         }
117         return -1;
118     }
119 
getTransportName(final int transportType)120     private String getTransportName(final int transportType) {
121         String[] networkTypes = mResources.get().getStringArray(R.array.network_switch_type_name);
122         try {
123             return networkTypes[transportType];
124         } catch (IndexOutOfBoundsException e) {
125             return mResources.get().getString(R.string.network_switch_type_name_unknown);
126         }
127     }
128 
getIcon(int transportType)129     private static int getIcon(int transportType) {
130         return (transportType == TRANSPORT_WIFI)
131                 ? R.drawable.stat_notify_wifi_in_range  // TODO: Distinguish ! from ?.
132                 : R.drawable.stat_notify_rssi_in_range;
133     }
134 
135     /**
136      * Show or hide network provisioning notifications.
137      *
138      * We use notifications for two purposes: to notify that a network requires sign in
139      * (NotificationType.SIGN_IN), or to notify that a network does not have Internet access
140      * (NotificationType.NO_INTERNET). We display at most one notification per ID, so on a
141      * particular network we can display the notification type that was most recently requested.
142      * So for example if a captive portal fails to reply within a few seconds of connecting, we
143      * might first display NO_INTERNET, and then when the captive portal check completes, display
144      * SIGN_IN.
145      *
146      * @param id an identifier that uniquely identifies this notification.  This must match
147      *         between show and hide calls.  We use the NetID value but for legacy callers
148      *         we concatenate the range of types with the range of NetIDs.
149      * @param notifyType the type of the notification.
150      * @param nai the network with which the notification is associated. For a SIGN_IN, NO_INTERNET,
151      *         or LOST_INTERNET notification, this is the network we're connecting to. For a
152      *         NETWORK_SWITCH notification it's the network that we switched from. When this network
153      *         disconnects the notification is removed.
154      * @param switchToNai for a NETWORK_SWITCH notification, the network we are switching to. Null
155      *         in all other cases. Only used to determine the text of the notification.
156      */
showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai, NetworkAgentInfo switchToNai, PendingIntent intent, boolean highPriority)157     public void showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai,
158             NetworkAgentInfo switchToNai, PendingIntent intent, boolean highPriority) {
159         final String tag = tagFor(id);
160         final int eventId = notifyType.eventId;
161         final int transportType;
162         final CharSequence name;
163         if (nai != null) {
164             transportType = approximateTransportType(nai);
165             final String extraInfo = nai.networkInfo.getExtraInfo();
166             if (nai.linkProperties != null && nai.linkProperties.getCaptivePortalData() != null
167                     && !TextUtils.isEmpty(nai.linkProperties.getCaptivePortalData()
168                     .getVenueFriendlyName())) {
169                 name = nai.linkProperties.getCaptivePortalData().getVenueFriendlyName();
170             } else {
171                 name = TextUtils.isEmpty(extraInfo)
172                         ? WifiInfo.sanitizeSsid(nai.networkCapabilities.getSsid()) : extraInfo;
173             }
174             // Only notify for Internet-capable networks.
175             if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET)) return;
176         } else {
177             // Legacy notifications.
178             transportType = TRANSPORT_CELLULAR;
179             name = "";
180         }
181 
182         // Clear any previous notification with lower priority, otherwise return. http://b/63676954.
183         // A new SIGN_IN notification with a new intent should override any existing one.
184         final int previousEventId = mNotificationTypeMap.get(id);
185         final NotificationType previousNotifyType = NotificationType.getFromId(previousEventId);
186         if (priority(previousNotifyType) > priority(notifyType)) {
187             Log.d(TAG, String.format(
188                     "ignoring notification %s for network %s with existing notification %s",
189                     notifyType, id, previousNotifyType));
190             return;
191         }
192         clearNotification(id);
193 
194         if (DBG) {
195             Log.d(TAG, String.format(
196                     "showNotification tag=%s event=%s transport=%s name=%s highPriority=%s",
197                     tag, nameOf(eventId), getTransportName(transportType), name, highPriority));
198         }
199 
200         final Resources r = mResources.get();
201         if (highPriority && maybeNotifyViaDialog(r, notifyType, intent)) {
202             Log.d(TAG, "Notified via dialog for event " + nameOf(eventId));
203             return;
204         }
205 
206         final CharSequence title;
207         final CharSequence details;
208         Icon icon = Icon.createWithResource(
209                 mResources.getResourcesContext(), getIcon(transportType));
210         final boolean showAsNoInternet = notifyType == NotificationType.PARTIAL_CONNECTIVITY
211                 && r.getBoolean(R.bool.config_partialConnectivityNotifiedAsNoInternet);
212         if (showAsNoInternet) {
213             Log.d(TAG, "Showing partial connectivity as NO_INTERNET");
214         }
215         if ((notifyType == NotificationType.NO_INTERNET || showAsNoInternet)
216                 && transportType == TRANSPORT_WIFI) {
217             title = r.getString(R.string.wifi_no_internet, name);
218             details = r.getString(R.string.wifi_no_internet_detailed);
219         } else if (notifyType == NotificationType.PRIVATE_DNS_BROKEN) {
220             if (transportType == TRANSPORT_CELLULAR) {
221                 title = r.getString(R.string.mobile_no_internet);
222             } else if (transportType == TRANSPORT_WIFI) {
223                 title = r.getString(R.string.wifi_no_internet, name);
224             } else {
225                 title = r.getString(R.string.other_networks_no_internet);
226             }
227             details = r.getString(R.string.private_dns_broken_detailed);
228         } else if (notifyType == NotificationType.PARTIAL_CONNECTIVITY
229                 && transportType == TRANSPORT_WIFI) {
230             title = r.getString(R.string.network_partial_connectivity, name);
231             details = r.getString(R.string.network_partial_connectivity_detailed);
232         } else if (notifyType == NotificationType.LOST_INTERNET &&
233                 transportType == TRANSPORT_WIFI) {
234             title = r.getString(R.string.wifi_no_internet, name);
235             details = r.getString(R.string.wifi_no_internet_detailed);
236         } else if (notifyType == NotificationType.SIGN_IN) {
237             switch (transportType) {
238                 case TRANSPORT_WIFI:
239                     title = r.getString(R.string.wifi_available_sign_in, 0);
240                     details = r.getString(R.string.network_available_sign_in_detailed, name);
241                     break;
242                 case TRANSPORT_CELLULAR:
243                     title = r.getString(R.string.network_available_sign_in, 0);
244                     // TODO: Change this to pull from NetworkInfo once a printable
245                     // name has been added to it
246                     NetworkSpecifier specifier = nai.networkCapabilities.getNetworkSpecifier();
247                     int subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID;
248                     if (specifier instanceof TelephonyNetworkSpecifier) {
249                         subId = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
250                     }
251 
252                     details = mTelephonyManager.createForSubscriptionId(subId)
253                             .getNetworkOperatorName();
254                     break;
255                 default:
256                     title = r.getString(R.string.network_available_sign_in, 0);
257                     details = r.getString(R.string.network_available_sign_in_detailed, name);
258                     break;
259             }
260         } else if (notifyType == NotificationType.NETWORK_SWITCH) {
261             String fromTransport = getTransportName(transportType);
262             String toTransport = getTransportName(approximateTransportType(switchToNai));
263             title = r.getString(R.string.network_switch_metered, toTransport);
264             details = r.getString(R.string.network_switch_metered_detail, toTransport,
265                     fromTransport);
266         } else if (notifyType == NotificationType.NO_INTERNET
267                     || notifyType == NotificationType.PARTIAL_CONNECTIVITY) {
268             // NO_INTERNET and PARTIAL_CONNECTIVITY notification for non-WiFi networks
269             // are sent, but they are not implemented yet.
270             return;
271         } else {
272             Log.wtf(TAG, "Unknown notification type " + notifyType + " on network transport "
273                     + getTransportName(transportType));
274             return;
275         }
276         // When replacing an existing notification for a given network, don't alert, just silently
277         // update the existing notification. Note that setOnlyAlertOnce() will only work for the
278         // same id, and the id used here is the NotificationType which is different in every type of
279         // notification. This is required because the notification metrics only track the ID but not
280         // the tag.
281         final boolean hasPreviousNotification = previousNotifyType != null;
282         final String channelId = (highPriority && !hasPreviousNotification)
283                 ? NOTIFICATION_CHANNEL_NETWORK_ALERTS : NOTIFICATION_CHANNEL_NETWORK_STATUS;
284         Notification.Builder builder = new Notification.Builder(mContext, channelId)
285                 .setWhen(System.currentTimeMillis())
286                 .setShowWhen(notifyType == NotificationType.NETWORK_SWITCH)
287                 .setSmallIcon(icon)
288                 .setAutoCancel(r.getBoolean(R.bool.config_autoCancelNetworkNotifications))
289                 .setTicker(title)
290                 .setColor(mContext.getColor(android.R.color.system_notification_accent_color))
291                 .setContentTitle(title)
292                 .setContentIntent(intent)
293                 .setLocalOnly(true)
294                 .setOnlyAlertOnce(true)
295                 // TODO: consider having action buttons to disconnect on the sign-in notification
296                 // especially if it is ongoing
297                 .setOngoing(notifyType == NotificationType.SIGN_IN
298                         && r.getBoolean(R.bool.config_ongoingSignInNotification));
299 
300         if (notifyType == NotificationType.NETWORK_SWITCH) {
301             builder.setStyle(new Notification.BigTextStyle().bigText(details));
302         } else {
303             builder.setContentText(details);
304         }
305 
306         if (notifyType == NotificationType.SIGN_IN) {
307             builder.extend(new Notification.TvExtender().setChannelId(channelId));
308         }
309 
310         Notification notification = builder.build();
311 
312         mNotificationTypeMap.put(id, eventId);
313         try {
314             mNotificationManager.notify(tag, eventId, notification);
315         } catch (NullPointerException npe) {
316             Log.d(TAG, "setNotificationVisible: visible notificationManager error", npe);
317         }
318     }
319 
maybeNotifyViaDialog(Resources res, NotificationType notifyType, PendingIntent intent)320     private boolean maybeNotifyViaDialog(Resources res, NotificationType notifyType,
321             PendingIntent intent) {
322         if (notifyType != NotificationType.NO_INTERNET
323                 && notifyType != NotificationType.PARTIAL_CONNECTIVITY) {
324             return false;
325         }
326         if (!res.getBoolean(R.bool.config_notifyNoInternetAsDialogWhenHighPriority)) {
327             return false;
328         }
329 
330         try {
331             intent.send();
332         } catch (PendingIntent.CanceledException e) {
333             Log.e(TAG, "Error sending dialog PendingIntent", e);
334         }
335         return true;
336     }
337 
338     /**
339      * Clear the notification with the given id, only if it matches the given type.
340      */
clearNotification(int id, NotificationType notifyType)341     public void clearNotification(int id, NotificationType notifyType) {
342         final int previousEventId = mNotificationTypeMap.get(id);
343         final NotificationType previousNotifyType = NotificationType.getFromId(previousEventId);
344         if (notifyType != previousNotifyType) {
345             return;
346         }
347         clearNotification(id);
348     }
349 
clearNotification(int id)350     public void clearNotification(int id) {
351         if (mNotificationTypeMap.indexOfKey(id) < 0) {
352             return;
353         }
354         final String tag = tagFor(id);
355         final int eventId = mNotificationTypeMap.get(id);
356         if (DBG) {
357             Log.d(TAG, String.format("clearing notification tag=%s event=%s", tag,
358                    nameOf(eventId)));
359         }
360         try {
361             mNotificationManager.cancel(tag, eventId);
362         } catch (NullPointerException npe) {
363             Log.d(TAG, String.format(
364                     "failed to clear notification tag=%s event=%s", tag, nameOf(eventId)), npe);
365         }
366         mNotificationTypeMap.delete(id);
367     }
368 
369     /**
370      * Legacy provisioning notifications coming directly from DcTracker.
371      */
setProvNotificationVisible(boolean visible, int id, String action)372     public void setProvNotificationVisible(boolean visible, int id, String action) {
373         if (visible) {
374             // For legacy purposes, action is sent as the action + the phone ID from DcTracker.
375             // Split the string here and send the phone ID as an extra instead.
376             String[] splitAction = action.split(":");
377             Intent intent = new Intent(splitAction[0]);
378             try {
379                 intent.putExtra("provision.phone.id", Integer.parseInt(splitAction[1]));
380             } catch (NumberFormatException ignored) { }
381             PendingIntent pendingIntent = PendingIntent.getBroadcast(
382                     mContext, 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE);
383             showNotification(id, NotificationType.SIGN_IN, null, null, pendingIntent, false);
384         } else {
385             clearNotification(id);
386         }
387     }
388 
showToast(NetworkAgentInfo fromNai, NetworkAgentInfo toNai)389     public void showToast(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
390         String fromTransport = getTransportName(approximateTransportType(fromNai));
391         String toTransport = getTransportName(approximateTransportType(toNai));
392         String text = mResources.get().getString(
393                 R.string.network_switch_metered_toast, fromTransport, toTransport);
394         Toast.makeText(mContext, text, Toast.LENGTH_LONG).show();
395     }
396 
397     @VisibleForTesting
tagFor(int id)398     static String tagFor(int id) {
399         return String.format("ConnectivityNotification:%d", id);
400     }
401 
402     @VisibleForTesting
nameOf(int eventId)403     static String nameOf(int eventId) {
404         NotificationType t = NotificationType.getFromId(eventId);
405         return (t != null) ? t.name() : "UNKNOWN";
406     }
407 
408     /**
409      * A notification with a higher number will take priority over a notification with a lower
410      * number.
411      */
priority(NotificationType t)412     private static int priority(NotificationType t) {
413         if (t == null) {
414             return 0;
415         }
416         switch (t) {
417             case SIGN_IN:
418                 return 6;
419             case PARTIAL_CONNECTIVITY:
420                 return 5;
421             case PRIVATE_DNS_BROKEN:
422                 return 4;
423             case NO_INTERNET:
424                 return 3;
425             case NETWORK_SWITCH:
426                 return 2;
427             case LOST_INTERNET:
428                 return 1;
429             default:
430                 return 0;
431         }
432     }
433 }
434