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