Home | History | Annotate | Download | only in connectivity
      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 
     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 
     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 
     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.
     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 
     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 
    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      */
    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 
    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      */
    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 
    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
    283     static String tagFor(int id) {
    284         return String.format("ConnectivityNotification:%d", id);
    285     }
    286 
    287     @VisibleForTesting
    288     static String nameOf(int eventId) {
    289         NotificationType t = NotificationType.getFromId(eventId);
    290         return (t != null) ? t.name() : "UNKNOWN";
    291     }
    292 
    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