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.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 
     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 
     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 
     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.
     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 
     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 
    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      */
    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 
    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      */
    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 
    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
    268     static String tagFor(int id) {
    269         return String.format("ConnectivityNotification:%d", id);
    270     }
    271 
    272     @VisibleForTesting
    273     static String nameOf(int eventId) {
    274         NotificationType t = NotificationType.getFromId(eventId);
    275         return (t != null) ? t.name() : "UNKNOWN";
    276     }
    277 }
    278