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