content: /*
   * Copyright (C) 2016 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.connectivity;
  
  import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
  import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
  import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
  
  import android.app.Notification;
  import android.app.NotificationManager;
  import android.app.PendingIntent;
  import android.content.Context;
  import android.content.Intent;
  import android.content.res.Resources;
  import android.net.NetworkSpecifier;
  import android.net.StringNetworkSpecifier;
  import android.net.wifi.WifiInfo;
  import android.os.UserHandle;
  import android.telephony.AccessNetworkConstants.TransportType;
  import android.telephony.SubscriptionManager;
  import android.telephony.TelephonyManager;
  import android.text.TextUtils;
  import android.util.Slog;
  import android.util.SparseArray;
  import android.util.SparseIntArray;
  import android.widget.Toast;
  
  import com.android.internal.R;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
  import com.android.internal.notification.SystemNotificationChannels;
  
  public class NetworkNotificationManager {
  
  
      public static enum NotificationType {
          LOST_INTERNET(SystemMessage.NOTE_NETWORK_LOST_INTERNET),
          NETWORK_SWITCH(SystemMessage.NOTE_NETWORK_SWITCH),
          NO_INTERNET(SystemMessage.NOTE_NETWORK_NO_INTERNET),
          LOGGED_IN(SystemMessage.NOTE_NETWORK_LOGGED_IN),
          PARTIAL_CONNECTIVITY(SystemMessage.NOTE_NETWORK_PARTIAL_CONNECTIVITY),
          SIGN_IN(SystemMessage.NOTE_NETWORK_SIGN_IN);
  
          public final int eventId;
  
          NotificationType(int eventId) {
              this.eventId = eventId;
              Holder.sIdToTypeMap.put(eventId, this);
          }
  
          private static class Holder {
              private static SparseArray<NotificationType> sIdToTypeMap = new SparseArray<>();
          }
  
          public static NotificationType getFromId(int id) {
              return Holder.sIdToTypeMap.get(id);
          }
      };
  
      private static final String TAG = NetworkNotificationManager.class.getSimpleName();
      private static final boolean DBG = true;
      private static final boolean VDBG = false;
  
      private final Context mContext;
      private final TelephonyManager mTelephonyManager;
      private final NotificationManager mNotificationManager;
      // Tracks the types of notifications managed by this instance, from creation to cancellation.
      private final SparseIntArray mNotificationTypeMap;
  
      public NetworkNotificationManager(Context c, TelephonyManager t, NotificationManager n) {
          mContext = c;
          mTelephonyManager = t;
          mNotificationManager = n;
          mNotificationTypeMap = new SparseIntArray();
      }
  
      // TODO: deal more gracefully with multi-transport networks.
      private static int getFirstTransportType(NetworkAgentInfo nai) {
          for (int i = 0; i < 64; i++) {
              if (nai.networkCapabilities.hasTransport(i)) return i;
          }
          return -1;
      }
  
      private static String getTransportName(@TransportType int transportType) {
          Resources r = Resources.getSystem();
          String[] networkTypes = r.getStringArray(R.array.network_switch_type_name);
          try {
              return networkTypes[transportType];
          } catch (IndexOutOfBoundsException e) {
              return r.getString(R.string.network_switch_type_name_unknown);
          }
      }
  
      private static int getIcon(int transportType, NotificationType notifyType) {
          if (transportType != TRANSPORT_WIFI) {
              return R.drawable.stat_notify_rssi_in_range;
          }
  
          return notifyType == NotificationType.LOGGED_IN
              ? R.drawable.ic_wifi_signal_4
              : R.drawable.stat_notify_wifi_in_range;  // TODO: Distinguish ! from ?.
      }
  
      /**
       * Show or hide network provisioning notifications.
       *
       * We use notifications for two purposes: to notify that a network requires sign in
       * (NotificationType.SIGN_IN), or to notify that a network does not have Internet access
       * (NotificationType.NO_INTERNET). We display at most one notification per ID, so on a
       * particular network we can display the notification type that was most recently requested.
       * So for example if a captive portal fails to reply within a few seconds of connecting, we
       * might first display NO_INTERNET, and then when the captive portal check completes, display
       * SIGN_IN.
       *
       * @param id an identifier that uniquely identifies this notification.  This must match
       *         between show and hide calls.  We use the NetID value but for legacy callers
       *         we concatenate the range of types with the range of NetIDs.
       * @param notifyType the type of the notification.
       * @param nai the network with which the notification is associated. For a SIGN_IN, NO_INTERNET,
       *         or LOST_INTERNET notification, this is the network we're connecting to. For a
       *         NETWORK_SWITCH notification it's the network that we switched from. When this network
       *         disconnects the notification is removed.
       * @param switchToNai for a NETWORK_SWITCH notification, the network we are switching to. Null
       *         in all other cases. Only used to determine the text of the notification.
       */
      public void showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai,
              NetworkAgentInfo switchToNai, PendingIntent intent, boolean highPriority) {
          final String tag = tagFor(id);
          final int eventId = notifyType.eventId;
          final int transportType;
          final String name;
          if (nai != null) {
              transportType = getFirstTransportType(nai);
              final String extraInfo = nai.networkInfo.getExtraInfo();
              name = TextUtils.isEmpty(extraInfo) ? nai.networkCapabilities.getSSID() : extraInfo;
              // Only notify for Internet-capable networks.
              if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET)) return;
          } else {
              // Legacy notifications.
              transportType = TRANSPORT_CELLULAR;
              name = null;
          }
  
          // Clear any previous notification with lower priority, otherwise return. http://b/63676954.
          // A new SIGN_IN notification with a new intent should override any existing one.
          final int previousEventId = mNotificationTypeMap.get(id);
          final NotificationType previousNotifyType = NotificationType.getFromId(previousEventId);
          if (priority(previousNotifyType) > priority(notifyType)) {
              Slog.d(TAG, String.format(
                      "ignoring notification %s for network %s with existing notification %s",
                      notifyType, id, previousNotifyType));
              return;
          }
          clearNotification(id);
  
          if (DBG) {
              Slog.d(TAG, String.format(
                      "showNotification tag=%s event=%s transport=%s name=%s highPriority=%s",
                      tag, nameOf(eventId), getTransportName(transportType), name, highPriority));
          }
  
          Resources r = Resources.getSystem();
          CharSequence title;
          CharSequence details;
          int icon = getIcon(transportType, notifyType);
          if (notifyType == NotificationType.NO_INTERNET && transportType == TRANSPORT_WIFI) {
              title = r.getString(R.string.wifi_no_internet,
                      WifiInfo.removeDoubleQuotes(nai.networkCapabilities.getSSID()));
              details = r.getString(R.string.wifi_no_internet_detailed);
          } else if (notifyType == NotificationType.PARTIAL_CONNECTIVITY
                  && transportType == TRANSPORT_WIFI) {
              title = r.getString(R.string.network_partial_connectivity,
                      WifiInfo.removeDoubleQuotes(nai.networkCapabilities.getSSID()));
              details = r.getString(R.string.network_partial_connectivity_detailed);
          } else if (notifyType == NotificationType.LOST_INTERNET &&
                  transportType == TRANSPORT_WIFI) {
              title = r.getString(R.string.wifi_no_internet,
                      WifiInfo.removeDoubleQuotes(nai.networkCapabilities.getSSID()));
              details = r.getString(R.string.wifi_no_internet_detailed);
          } else if (notifyType == NotificationType.SIGN_IN) {
              switch (transportType) {
                  case TRANSPORT_WIFI:
                      title = r.getString(R.string.wifi_available_sign_in, 0);
                      details = r.getString(R.string.network_available_sign_in_detailed,
                              WifiInfo.removeDoubleQuotes(nai.networkCapabilities.getSSID()));
                      break;
                  case TRANSPORT_CELLULAR:
                      title = r.getString(R.string.network_available_sign_in, 0);
                      // TODO: Change this to pull from NetworkInfo once a printable
                      // name has been added to it
                      NetworkSpecifier specifier = nai.networkCapabilities.getNetworkSpecifier();
                      int subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID;
                      if (specifier instanceof StringNetworkSpecifier) {
                          try {
                              subId = Integer.parseInt(
                                      ((StringNetworkSpecifier) specifier).specifier);
                          } catch (NumberFormatException e) {
                              Slog.e(TAG, "NumberFormatException on "
                                      + ((StringNetworkSpecifier) specifier).specifier);
                          }
                      }
  
                      details = mTelephonyManager.createForSubscriptionId(subId)
                              .getNetworkOperatorName();
                      break;
                  default:
                      title = r.getString(R.string.network_available_sign_in, 0);
                      details = r.getString(R.string.network_available_sign_in_detailed, name);
                      break;
              }
          } else if (notifyType == NotificationType.LOGGED_IN) {
              title = WifiInfo.removeDoubleQuotes(nai.networkCapabilities.getSSID());
              details = r.getString(R.string.captive_portal_logged_in_detailed);
          } else if (notifyType == NotificationType.NETWORK_SWITCH) {
              String fromTransport = getTransportName(transportType);
              String toTransport = getTransportName(getFirstTransportType(switchToNai));
              title = r.getString(R.string.network_switch_metered, toTransport);
              details = r.getString(R.string.network_switch_metered_detail, toTransport,
                      fromTransport);
          } else if (notifyType == NotificationType.NO_INTERNET
                      || notifyType == NotificationType.PARTIAL_CONNECTIVITY) {
              // NO_INTERNET and PARTIAL_CONNECTIVITY notification for non-WiFi networks
              // are sent, but they are not implemented yet.
              return;
          } else {
              Slog.wtf(TAG, "Unknown notification type " + notifyType + " on network transport "
                      + getTransportName(transportType));
              return;
          }
          // When replacing an existing notification for a given network, don't alert, just silently
          // update the existing notification. Note that setOnlyAlertOnce() will only work for the
          // same id, and the id used here is the NotificationType which is different in every type of
          // notification. This is required because the notification metrics only track the ID but not
          // the tag.
          final boolean hasPreviousNotification = previousNotifyType != null;
          final String channelId = (highPriority && !hasPreviousNotification)
                  ? SystemNotificationChannels.NETWORK_ALERTS
                  : SystemNotificationChannels.NETWORK_STATUS;
          Notification.Builder builder = new Notification.Builder(mContext, channelId)
                  .setWhen(System.currentTimeMillis())
                  .setShowWhen(notifyType == NotificationType.NETWORK_SWITCH)
                  .setSmallIcon(icon)
                  .setAutoCancel(true)
                  .setTicker(title)
                  .setColor(mContext.getColor(
                          com.android.internal.R.color.system_notification_accent_color))
                  .setContentTitle(title)
                  .setContentIntent(intent)
                  .setLocalOnly(true)
                  .setOnlyAlertOnce(true);
  
          if (notifyType == NotificationType.NETWORK_SWITCH) {
              builder.setStyle(new Notification.BigTextStyle().bigText(details));
          } else {
              builder.setContentText(details);
          }
  
          if (notifyType == NotificationType.SIGN_IN) {
              builder.extend(new Notification.TvExtender().setChannelId(channelId));
          }
  
          Notification notification = builder.build();
  
          mNotificationTypeMap.put(id, eventId);
          try {
              mNotificationManager.notifyAsUser(tag, eventId, notification, UserHandle.ALL);
          } catch (NullPointerException npe) {
              Slog.d(TAG, "setNotificationVisible: visible notificationManager error", npe);
          }
      }
  
      /**
       * Clear the notification with the given id, only if it matches the given type.
       */
      public void clearNotification(int id, NotificationType notifyType) {
          final int previousEventId = mNotificationTypeMap.get(id);
          final NotificationType previousNotifyType = NotificationType.getFromId(previousEventId);
          if (notifyType != previousNotifyType) {
              return;
          }
          clearNotification(id);
      }
  
      public void clearNotification(int id) {
          if (mNotificationTypeMap.indexOfKey(id) < 0) {
              return;
          }
          final String tag = tagFor(id);
          final int eventId = mNotificationTypeMap.get(id);
          if (DBG) {
              Slog.d(TAG, String.format("clearing notification tag=%s event=%s", tag,
                     nameOf(eventId)));
          }
          try {
              mNotificationManager.cancelAsUser(tag, eventId, UserHandle.ALL);
          } catch (NullPointerException npe) {
              Slog.d(TAG, String.format(
                      "failed to clear notification tag=%s event=%s", tag, nameOf(eventId)), npe);
          }
          mNotificationTypeMap.delete(id);
      }
  
      /**
       * Legacy provisioning notifications coming directly from DcTracker.
       */
      public void setProvNotificationVisible(boolean visible, int id, String action) {
          if (visible) {
              Intent intent = new Intent(action);
              PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
              showNotification(id, NotificationType.SIGN_IN, null, null, pendingIntent, false);
          } else {
              clearNotification(id);
          }
      }
  
      public void showToast(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
          String fromTransport = getTransportName(getFirstTransportType(fromNai));
          String toTransport = getTransportName(getFirstTransportType(toNai));
          String text = mContext.getResources().getString(
                  R.string.network_switch_metered_toast, fromTransport, toTransport);
          Toast.makeText(mContext, text, Toast.LENGTH_LONG).show();
      }
  
      @VisibleForTesting
      static String tagFor(int id) {
          return String.format("ConnectivityNotification:%d", id);
      }
  
      @VisibleForTesting
      static String nameOf(int eventId) {
          NotificationType t = NotificationType.getFromId(eventId);
          return (t != null) ? t.name() : "UNKNOWN";
      }
  
      /**
       * A notification with a higher number will take priority over a notification with a lower
       * number.
       */
      private static int priority(NotificationType t) {
          if (t == null) {
              return 0;
          }
          switch (t) {
              case SIGN_IN:
                  return 5;
              case PARTIAL_CONNECTIVITY:
                  return 4;
              case NO_INTERNET:
                  return 3;
              case NETWORK_SWITCH:
                  return 2;
              case LOST_INTERNET:
              case LOGGED_IN:
                  return 1;
              default:
                  return 0;
          }
      }
  }
  