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 android.app.Notification;
20 import android.app.NotificationManager;
21 import android.app.PendingIntent;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.res.Resources;
25 import android.net.NetworkCapabilities;
26 import android.os.UserHandle;
27 import android.telephony.TelephonyManager;
28 import android.util.Slog;
29 import android.util.SparseArray;
30 import android.util.SparseIntArray;
31 import android.widget.Toast;
32 import com.android.internal.R;
33 import com.android.internal.annotations.VisibleForTesting;
34 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
35 import com.android.internal.notification.SystemNotificationChannels;
36 
37 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
38 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
39 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
40 
41 public class NetworkNotificationManager {
42 
43 
44     public static enum NotificationType {
45         LOST_INTERNET(SystemMessage.NOTE_NETWORK_LOST_INTERNET),
46         NETWORK_SWITCH(SystemMessage.NOTE_NETWORK_SWITCH),
47         NO_INTERNET(SystemMessage.NOTE_NETWORK_NO_INTERNET),
48         SIGN_IN(SystemMessage.NOTE_NETWORK_SIGN_IN);
49 
50         public final int eventId;
51 
NotificationType(int eventId)52         NotificationType(int eventId) {
53             this.eventId = eventId;
54             Holder.sIdToTypeMap.put(eventId, this);
55         }
56 
57         private static class Holder {
58             private static SparseArray<NotificationType> sIdToTypeMap = new SparseArray<>();
59         }
60 
getFromId(int id)61         public static NotificationType getFromId(int id) {
62             return Holder.sIdToTypeMap.get(id);
63         }
64     };
65 
66     private static final String TAG = NetworkNotificationManager.class.getSimpleName();
67     private static final boolean DBG = true;
68     private static final boolean VDBG = false;
69 
70     private final Context mContext;
71     private final TelephonyManager mTelephonyManager;
72     private final NotificationManager mNotificationManager;
73     // Tracks the types of notifications managed by this instance, from creation to cancellation.
74     private final SparseIntArray mNotificationTypeMap;
75 
NetworkNotificationManager(Context c, TelephonyManager t, NotificationManager n)76     public NetworkNotificationManager(Context c, TelephonyManager t, NotificationManager n) {
77         mContext = c;
78         mTelephonyManager = t;
79         mNotificationManager = n;
80         mNotificationTypeMap = new SparseIntArray();
81     }
82 
83     // TODO: deal more gracefully with multi-transport networks.
getFirstTransportType(NetworkAgentInfo nai)84     private static int getFirstTransportType(NetworkAgentInfo nai) {
85         for (int i = 0; i < 64; i++) {
86             if (nai.networkCapabilities.hasTransport(i)) return i;
87         }
88         return -1;
89     }
90 
getTransportName(int transportType)91     private static String getTransportName(int transportType) {
92         Resources r = Resources.getSystem();
93         String[] networkTypes = r.getStringArray(R.array.network_switch_type_name);
94         try {
95             return networkTypes[transportType];
96         } catch (IndexOutOfBoundsException e) {
97             return r.getString(R.string.network_switch_type_name_unknown);
98         }
99     }
100 
getIcon(int transportType)101     private static int getIcon(int transportType) {
102         return (transportType == TRANSPORT_WIFI) ?
103                 R.drawable.stat_notify_wifi_in_range :  // TODO: Distinguish ! from ?.
104                 R.drawable.stat_notify_rssi_in_range;
105     }
106 
107     /**
108      * Show or hide network provisioning notifications.
109      *
110      * We use notifications for two purposes: to notify that a network requires sign in
111      * (NotificationType.SIGN_IN), or to notify that a network does not have Internet access
112      * (NotificationType.NO_INTERNET). We display at most one notification per ID, so on a
113      * particular network we can display the notification type that was most recently requested.
114      * So for example if a captive portal fails to reply within a few seconds of connecting, we
115      * might first display NO_INTERNET, and then when the captive portal check completes, display
116      * SIGN_IN.
117      *
118      * @param id an identifier that uniquely identifies this notification.  This must match
119      *         between show and hide calls.  We use the NetID value but for legacy callers
120      *         we concatenate the range of types with the range of NetIDs.
121      * @param nai the network with which the notification is associated. For a SIGN_IN, NO_INTERNET,
122      *         or LOST_INTERNET notification, this is the network we're connecting to. For a
123      *         NETWORK_SWITCH notification it's the network that we switched from. When this network
124      *         disconnects the notification is removed.
125      * @param switchToNai for a NETWORK_SWITCH notification, the network we are switching to. Null
126      *         in all other cases. Only used to determine the text of the notification.
127      */
showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai, NetworkAgentInfo switchToNai, PendingIntent intent, boolean highPriority)128     public void showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai,
129             NetworkAgentInfo switchToNai, PendingIntent intent, boolean highPriority) {
130         final String tag = tagFor(id);
131         final int eventId = notifyType.eventId;
132         final int transportType;
133         final String extraInfo;
134         if (nai != null) {
135             transportType = getFirstTransportType(nai);
136             extraInfo = nai.networkInfo.getExtraInfo();
137             // Only notify for Internet-capable networks.
138             if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET)) return;
139         } else {
140             // Legacy notifications.
141             transportType = TRANSPORT_CELLULAR;
142             extraInfo = null;
143         }
144 
145         if (DBG) {
146             Slog.d(TAG, String.format(
147                     "showNotification tag=%s event=%s transport=%s extraInfo=%s highPrioriy=%s",
148                     tag, nameOf(eventId), getTransportName(transportType), extraInfo,
149                     highPriority));
150         }
151 
152         Resources r = Resources.getSystem();
153         CharSequence title;
154         CharSequence details;
155         int icon = getIcon(transportType);
156         if (notifyType == NotificationType.NO_INTERNET && transportType == TRANSPORT_WIFI) {
157             title = r.getString(R.string.wifi_no_internet, 0);
158             details = r.getString(R.string.wifi_no_internet_detailed);
159         } else if (notifyType == NotificationType.LOST_INTERNET &&
160                 transportType == TRANSPORT_WIFI) {
161             title = r.getString(R.string.wifi_no_internet, 0);
162             details = r.getString(R.string.wifi_no_internet_detailed);
163         } else if (notifyType == NotificationType.SIGN_IN) {
164             switch (transportType) {
165                 case TRANSPORT_WIFI:
166                     title = r.getString(R.string.wifi_available_sign_in, 0);
167                     details = r.getString(R.string.network_available_sign_in_detailed, extraInfo);
168                     break;
169                 case TRANSPORT_CELLULAR:
170                     title = r.getString(R.string.network_available_sign_in, 0);
171                     // TODO: Change this to pull from NetworkInfo once a printable
172                     // name has been added to it
173                     details = mTelephonyManager.getNetworkOperatorName();
174                     break;
175                 default:
176                     title = r.getString(R.string.network_available_sign_in, 0);
177                     details = r.getString(R.string.network_available_sign_in_detailed, extraInfo);
178                     break;
179             }
180         } else if (notifyType == NotificationType.NETWORK_SWITCH) {
181             String fromTransport = getTransportName(transportType);
182             String toTransport = getTransportName(getFirstTransportType(switchToNai));
183             title = r.getString(R.string.network_switch_metered, toTransport);
184             details = r.getString(R.string.network_switch_metered_detail, toTransport,
185                     fromTransport);
186         } else {
187             Slog.wtf(TAG, "Unknown notification type " + notifyType + " on network transport "
188                     + getTransportName(transportType));
189             return;
190         }
191 
192         final String channelId = highPriority ? SystemNotificationChannels.NETWORK_ALERTS :
193                 SystemNotificationChannels.NETWORK_STATUS;
194         Notification.Builder builder = new Notification.Builder(mContext, channelId)
195                 .setWhen(System.currentTimeMillis())
196                 .setShowWhen(notifyType == NotificationType.NETWORK_SWITCH)
197                 .setSmallIcon(icon)
198                 .setAutoCancel(true)
199                 .setTicker(title)
200                 .setColor(mContext.getColor(
201                         com.android.internal.R.color.system_notification_accent_color))
202                 .setContentTitle(title)
203                 .setContentIntent(intent)
204                 .setLocalOnly(true)
205                 .setOnlyAlertOnce(true);
206 
207         if (notifyType == NotificationType.NETWORK_SWITCH) {
208             builder.setStyle(new Notification.BigTextStyle().bigText(details));
209         } else {
210             builder.setContentText(details);
211         }
212 
213         if (notifyType == NotificationType.SIGN_IN) {
214             builder.extend(new Notification.TvExtender().setChannelId(channelId));
215         }
216 
217         Notification notification = builder.build();
218 
219         mNotificationTypeMap.put(id, eventId);
220         try {
221             mNotificationManager.notifyAsUser(tag, eventId, notification, UserHandle.ALL);
222         } catch (NullPointerException npe) {
223             Slog.d(TAG, "setNotificationVisible: visible notificationManager error", npe);
224         }
225     }
226 
clearNotification(int id)227     public void clearNotification(int id) {
228         if (mNotificationTypeMap.indexOfKey(id) < 0) {
229             return;
230         }
231         final String tag = tagFor(id);
232         final int eventId = mNotificationTypeMap.get(id);
233         if (DBG) {
234             Slog.d(TAG, String.format("clearing notification tag=%s event=%s", tag,
235                    nameOf(eventId)));
236         }
237         try {
238             mNotificationManager.cancelAsUser(tag, eventId, UserHandle.ALL);
239         } catch (NullPointerException npe) {
240             Slog.d(TAG, String.format(
241                     "failed to clear notification tag=%s event=%s", tag, nameOf(eventId)), npe);
242         }
243         mNotificationTypeMap.delete(id);
244     }
245 
246     /**
247      * Legacy provisioning notifications coming directly from DcTracker.
248      */
setProvNotificationVisible(boolean visible, int id, String action)249     public void setProvNotificationVisible(boolean visible, int id, String action) {
250         if (visible) {
251             Intent intent = new Intent(action);
252             PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
253             showNotification(id, NotificationType.SIGN_IN, null, null, pendingIntent, false);
254         } else {
255             clearNotification(id);
256         }
257     }
258 
showToast(NetworkAgentInfo fromNai, NetworkAgentInfo toNai)259     public void showToast(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
260         String fromTransport = getTransportName(getFirstTransportType(fromNai));
261         String toTransport = getTransportName(getFirstTransportType(toNai));
262         String text = mContext.getResources().getString(
263                 R.string.network_switch_metered_toast, fromTransport, toTransport);
264         Toast.makeText(mContext, text, Toast.LENGTH_LONG).show();
265     }
266 
267     @VisibleForTesting
tagFor(int id)268     static String tagFor(int id) {
269         return String.format("ConnectivityNotification:%d", id);
270     }
271 
272     @VisibleForTesting
nameOf(int eventId)273     static String nameOf(int eventId) {
274         NotificationType t = NotificationType.getFromId(eventId);
275         return (t != null) ? t.name() : "UNKNOWN";
276     }
277 }
278