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.internal.telephony;
  
  import android.app.Notification;
  import android.app.NotificationManager;
  import android.app.PendingIntent;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.database.ContentObserver;
  import android.os.Handler;
  import android.os.Message;
  import android.os.PersistableBundle;
  import android.provider.Settings;
  import android.telephony.CarrierConfigManager;
  import android.telephony.Rlog;
  import android.telephony.ServiceState;
  import android.telephony.SubscriptionManager;
  import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.telephony.util.NotificationChannelController;
  
  import java.util.HashMap;
  import java.util.Map;
  
  
  
  /**
   * This contains Carrier specific logic based on the states/events
   * managed in ServiceStateTracker.
   * {@hide}
   */
  public class CarrierServiceStateTracker extends Handler {
      private static final String LOG_TAG = "CSST";
      protected static final int CARRIER_EVENT_BASE = 100;
      protected static final int CARRIER_EVENT_VOICE_REGISTRATION = CARRIER_EVENT_BASE + 1;
      protected static final int CARRIER_EVENT_VOICE_DEREGISTRATION = CARRIER_EVENT_BASE + 2;
      protected static final int CARRIER_EVENT_DATA_REGISTRATION = CARRIER_EVENT_BASE + 3;
      protected static final int CARRIER_EVENT_DATA_DEREGISTRATION = CARRIER_EVENT_BASE + 4;
      protected static final int CARRIER_EVENT_IMS_CAPABILITIES_CHANGED = CARRIER_EVENT_BASE + 5;
  
      private static final int UNINITIALIZED_DELAY_VALUE = -1;
      private Phone mPhone;
      private ServiceStateTracker mSST;
      private final Map<Integer, NotificationType> mNotificationTypeMap = new HashMap<>();
      private int mPreviousSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
      public static final int NOTIFICATION_PREF_NETWORK = 1000;
      public static final int NOTIFICATION_EMERGENCY_NETWORK = 1001;
  
      public CarrierServiceStateTracker(Phone phone, ServiceStateTracker sst) {
          this.mPhone = phone;
          this.mSST = sst;
          phone.getContext().registerReceiver(mBroadcastReceiver, new IntentFilter(
                  CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
          // Listen for subscriber changes
          SubscriptionManager.from(mPhone.getContext()).addOnSubscriptionsChangedListener(
                  new OnSubscriptionsChangedListener(this.getLooper()) {
                      @Override
                      public void onSubscriptionsChanged() {
                          int subId = mPhone.getSubId();
                          if (mPreviousSubId != subId) {
                              mPreviousSubId = subId;
                              registerPrefNetworkModeObserver();
                          }
                      }
                  });
  
          registerNotificationTypes();
          registerPrefNetworkModeObserver();
      }
  
      private ContentObserver mPrefNetworkModeObserver = new ContentObserver(this) {
          @Override
          public void onChange(boolean selfChange) {
              handlePrefNetworkModeChanged();
          }
      };
  
      /**
       * Return preferred network mode observer
       */
      @VisibleForTesting
      public ContentObserver getContentObserver() {
          return mPrefNetworkModeObserver;
      }
  
      private void registerPrefNetworkModeObserver() {
          int subId = mPhone.getSubId();
          unregisterPrefNetworkModeObserver();
          if (SubscriptionManager.isValidSubscriptionId(subId)) {
              mPhone.getContext().getContentResolver().registerContentObserver(
                      Settings.Global.getUriFor(Settings.Global.PREFERRED_NETWORK_MODE + subId),
                      true,
                      mPrefNetworkModeObserver);
          }
      }
  
      private void unregisterPrefNetworkModeObserver() {
          mPhone.getContext().getContentResolver().unregisterContentObserver(
                  mPrefNetworkModeObserver);
      }
  
      /**
       * Returns mNotificationTypeMap
       */
      @VisibleForTesting
      public Map<Integer, NotificationType> getNotificationTypeMap() {
          return mNotificationTypeMap;
      }
  
      private void registerNotificationTypes() {
          mNotificationTypeMap.put(NOTIFICATION_PREF_NETWORK,
                  new PrefNetworkNotification(NOTIFICATION_PREF_NETWORK));
          mNotificationTypeMap.put(NOTIFICATION_EMERGENCY_NETWORK,
                  new EmergencyNetworkNotification(NOTIFICATION_EMERGENCY_NETWORK));
      }
  
      @Override
      public void handleMessage(Message msg) {
          switch (msg.what) {
              case CARRIER_EVENT_VOICE_REGISTRATION:
              case CARRIER_EVENT_DATA_REGISTRATION:
              case CARRIER_EVENT_VOICE_DEREGISTRATION:
              case CARRIER_EVENT_DATA_DEREGISTRATION:
                  handleConfigChanges();
                  break;
              case CARRIER_EVENT_IMS_CAPABILITIES_CHANGED:
                  handleImsCapabilitiesChanged();
                  break;
              case NOTIFICATION_EMERGENCY_NETWORK:
              case NOTIFICATION_PREF_NETWORK:
                  Rlog.d(LOG_TAG, "sending notification after delay: " + msg.what);
                  NotificationType notificationType = mNotificationTypeMap.get(msg.what);
                  if (notificationType != null) {
                      sendNotification(notificationType);
                  }
                  break;
          }
      }
  
      private boolean isPhoneStillRegistered() {
          if (mSST.mSS == null) {
              return true; //something has gone wrong, return true and not show the notification.
          }
          return (mSST.mSS.getVoiceRegState() == ServiceState.STATE_IN_SERVICE
                  || mSST.mSS.getDataRegState() == ServiceState.STATE_IN_SERVICE);
      }
  
      private boolean isPhoneVoiceRegistered() {
          if (mSST.mSS == null) {
              return true; //something has gone wrong, return true and not show the notification.
          }
          return (mSST.mSS.getVoiceRegState() == ServiceState.STATE_IN_SERVICE);
      }
  
      private boolean isPhoneRegisteredForWifiCalling() {
          Rlog.d(LOG_TAG, "isPhoneRegisteredForWifiCalling: " + mPhone.isWifiCallingEnabled());
          return mPhone.isWifiCallingEnabled();
      }
  
      /**
       * Returns true if the radio is off or in Airplane Mode else returns false.
       */
      @VisibleForTesting
      public boolean isRadioOffOrAirplaneMode() {
          Context context = mPhone.getContext();
          int airplaneMode = -1;
          try {
              airplaneMode = Settings.Global.getInt(context.getContentResolver(),
                      Settings.Global.AIRPLANE_MODE_ON, 0);
          } catch (Exception e) {
              Rlog.e(LOG_TAG, "Unable to get AIRPLACE_MODE_ON.");
              return true;
          }
          return (!mSST.isRadioOn() || (airplaneMode != 0));
      }
  
      /**
       * Returns true if the preferred network is set to 'Global'.
       */
      private boolean isGlobalMode() {
          Context context = mPhone.getContext();
          int preferredNetworkSetting = -1;
          try {
              preferredNetworkSetting =
                      android.provider.Settings.Global.getInt(context.getContentResolver(),
                              android.provider.Settings.Global.PREFERRED_NETWORK_MODE
                                      + mPhone.getSubId(), Phone.PREFERRED_NT_MODE);
          } catch (Exception e) {
              Rlog.e(LOG_TAG, "Unable to get PREFERRED_NETWORK_MODE.");
              return true;
          }
          return (preferredNetworkSetting == RILConstants.NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA);
      }
  
      private void handleConfigChanges() {
          for (Map.Entry<Integer, NotificationType> entry : mNotificationTypeMap.entrySet()) {
              NotificationType notificationType = entry.getValue();
              evaluateSendingMessageOrCancelNotification(notificationType);
          }
      }
  
      private void handlePrefNetworkModeChanged() {
          NotificationType notificationType = mNotificationTypeMap.get(NOTIFICATION_PREF_NETWORK);
          if (notificationType != null) {
              evaluateSendingMessageOrCancelNotification(notificationType);
          }
      }
  
      private void handleImsCapabilitiesChanged() {
          NotificationType notificationType = mNotificationTypeMap
                  .get(NOTIFICATION_EMERGENCY_NETWORK);
          if (notificationType != null) {
              evaluateSendingMessageOrCancelNotification(notificationType);
          }
      }
  
      private void evaluateSendingMessageOrCancelNotification(NotificationType notificationType) {
          if (evaluateSendingMessage(notificationType)) {
              Message notificationMsg = obtainMessage(notificationType.getTypeId(), null);
              Rlog.i(LOG_TAG, "starting timer for notifications." + notificationType.getTypeId());
              sendMessageDelayed(notificationMsg, getDelay(notificationType));
          } else {
              cancelNotification(notificationType.getTypeId());
              Rlog.i(LOG_TAG, "canceling notifications: " + notificationType.getTypeId());
          }
      }
  
      /**
       * This method adds a level of indirection, and was created so we can unit the class.
       **/
      @VisibleForTesting
      public boolean evaluateSendingMessage(NotificationType notificationType) {
          return notificationType.sendMessage();
      }
  
      /**
       * This method adds a level of indirection, and was created so we can unit the class.
       **/
      @VisibleForTesting
      public int getDelay(NotificationType notificationType) {
          return notificationType.getDelay();
      }
  
      /**
       * This method adds a level of indirection, and was created so we can unit the class.
       **/
      @VisibleForTesting
      public Notification.Builder getNotificationBuilder(NotificationType notificationType) {
          return notificationType.getNotificationBuilder();
      }
  
      /**
       * This method adds a level of indirection, and was created so we can unit the class.
       **/
      @VisibleForTesting
      public NotificationManager getNotificationManager(Context context) {
          return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
      }
  
      private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
              CarrierConfigManager carrierConfigManager = (CarrierConfigManager)
                      context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
              PersistableBundle b = carrierConfigManager.getConfigForSubId(mPhone.getSubId());
  
              for (Map.Entry<Integer, NotificationType> entry : mNotificationTypeMap.entrySet()) {
                  NotificationType notificationType = entry.getValue();
                  notificationType.setDelay(b);
              }
              handleConfigChanges();
          }
      };
  
      /**
       * Post a notification to the NotificationManager for changing network type.
       */
      @VisibleForTesting
      public void sendNotification(NotificationType notificationType) {
          if (!evaluateSendingMessage(notificationType)) {
              return;
          }
  
          Context context = mPhone.getContext();
          Notification.Builder builder = getNotificationBuilder(notificationType);
          // set some common attributes
          builder.setWhen(System.currentTimeMillis())
                  .setAutoCancel(true)
                  .setSmallIcon(com.android.internal.R.drawable.stat_sys_warning)
                  .setColor(context.getResources().getColor(
                         com.android.internal.R.color.system_notification_accent_color));
  
          getNotificationManager(context).notify(notificationType.getTypeId(), builder.build());
      }
  
      /**
       * Cancel notifications if a registration is pending or has been sent.
       **/
      public void cancelNotification(int notificationId) {
          Context context = mPhone.getContext();
          removeMessages(notificationId);
          getNotificationManager(context).cancel(notificationId);
      }
  
      /**
       * Dispose the CarrierServiceStateTracker.
       */
      public void dispose() {
          unregisterPrefNetworkModeObserver();
      }
  
      /**
       * Class that defines the different types of notifications.
       */
      public interface NotificationType {
  
          /**
           * decides if the message should be sent, Returns boolean
           **/
          boolean sendMessage();
  
          /**
           * returns the interval by which the message is delayed.
           **/
          int getDelay();
  
          /** sets the interval by which the message is delayed.
           * @param bundle PersistableBundle
          **/
          void setDelay(PersistableBundle bundle);
  
          /**
           * returns notification type id.
           **/
          int getTypeId();
  
          /**
           * returns the notification builder, for the notification to be displayed.
           **/
          Notification.Builder getNotificationBuilder();
      }
  
      /**
       * Class that defines the network notification, which is shown when the phone cannot camp on
       * a network, and has 'preferred mode' set to global.
       */
      public class PrefNetworkNotification implements NotificationType {
  
          private final int mTypeId;
          private int mDelay = UNINITIALIZED_DELAY_VALUE;
  
          PrefNetworkNotification(int typeId) {
              this.mTypeId = typeId;
          }
  
          /** sets the interval by which the message is delayed.
           * @param bundle PersistableBundle
           **/
          public void setDelay(PersistableBundle bundle) {
              if (bundle == null) {
                  Rlog.e(LOG_TAG, "bundle is null");
                  return;
              }
              this.mDelay = bundle.getInt(
                      CarrierConfigManager.KEY_PREF_NETWORK_NOTIFICATION_DELAY_INT);
              Rlog.i(LOG_TAG, "reading time to delay notification pref network: " + mDelay);
          }
  
          public int getDelay() {
              return mDelay;
          }
  
          public int getTypeId() {
              return mTypeId;
          }
  
          /**
           * Contains logic on sending notifications.
           */
          public boolean sendMessage() {
              Rlog.i(LOG_TAG, "PrefNetworkNotification: sendMessage() w/values: "
                      + "," + isPhoneStillRegistered() + "," + mDelay + "," + isGlobalMode()
                      + "," + mSST.isRadioOn());
              if (mDelay == UNINITIALIZED_DELAY_VALUE || isPhoneStillRegistered() || isGlobalMode()
                      || isRadioOffOrAirplaneMode()) {
                  return false;
              }
              return true;
          }
  
          /**
           * Builds a partial notificaiton builder, and returns it.
           */
          public Notification.Builder getNotificationBuilder() {
              Context context = mPhone.getContext();
              Intent notificationIntent = new Intent(Settings.ACTION_DATA_ROAMING_SETTINGS);
              notificationIntent.putExtra("expandable", true);
              PendingIntent settingsIntent = PendingIntent.getActivity(context, 0, notificationIntent,
                      PendingIntent.FLAG_ONE_SHOT);
              CharSequence title = context.getText(
                      com.android.internal.R.string.NetworkPreferenceSwitchTitle);
              CharSequence details = context.getText(
                      com.android.internal.R.string.NetworkPreferenceSwitchSummary);
              return new Notification.Builder(context)
                      .setContentTitle(title)
                      .setStyle(new Notification.BigTextStyle().bigText(details))
                      .setContentText(details)
                      .setChannel(NotificationChannelController.CHANNEL_ID_ALERT)
                      .setContentIntent(settingsIntent);
          }
      }
  
      /**
       * Class that defines the emergency notification, which is shown when the user is out of cell
       * connectivity, but has wifi enabled.
       */
      public class EmergencyNetworkNotification implements NotificationType {
  
          private final int mTypeId;
          private int mDelay = UNINITIALIZED_DELAY_VALUE;
  
          EmergencyNetworkNotification(int typeId) {
              this.mTypeId = typeId;
          }
  
          /** sets the interval by which the message is delayed.
           * @param bundle PersistableBundle
           **/
          public void setDelay(PersistableBundle bundle) {
              if (bundle == null) {
                  Rlog.e(LOG_TAG, "bundle is null");
                  return;
              }
              this.mDelay = bundle.getInt(
                      CarrierConfigManager.KEY_EMERGENCY_NOTIFICATION_DELAY_INT);
              Rlog.i(LOG_TAG, "reading time to delay notification emergency: " + mDelay);
          }
  
          public int getDelay() {
              return mDelay;
          }
  
          public int getTypeId() {
              return mTypeId;
          }
  
          /**
           * Contains logic on sending notifications,
           */
          public boolean sendMessage() {
              Rlog.i(LOG_TAG, "EmergencyNetworkNotification: sendMessage() w/values: "
                      + "," + isPhoneVoiceRegistered() + "," + mDelay + ","
                      + isPhoneRegisteredForWifiCalling() + "," + mSST.isRadioOn());
              if (mDelay == UNINITIALIZED_DELAY_VALUE || isPhoneVoiceRegistered()
                      || !isPhoneRegisteredForWifiCalling()) {
                  return false;
              }
              return true;
          }
  
          /**
           * Builds a partial notificaiton builder, and returns it.
           */
          public Notification.Builder getNotificationBuilder() {
              Context context = mPhone.getContext();
              CharSequence title = context.getText(
                      com.android.internal.R.string.EmergencyCallWarningTitle);
              CharSequence details = context.getText(
                      com.android.internal.R.string.EmergencyCallWarningSummary);
              return new Notification.Builder(context)
                      .setContentTitle(title)
                      .setStyle(new Notification.BigTextStyle().bigText(details))
                      .setContentText(details)
                      .setChannel(NotificationChannelController.CHANNEL_ID_WFC);
          }
      }
  }
  