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