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