content: /*
   * Copyright 2018 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 static android.text.format.DateUtils.MINUTE_IN_MILLIS;
  import static android.text.format.DateUtils.SECOND_IN_MILLIS;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.net.wifi.WifiManager;
  import android.os.AsyncResult;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.Message;
  import android.telephony.CellInfo;
  import android.telephony.CellInfoGsm;
  import android.telephony.CellInfoLte;
  import android.telephony.CellInfoWcdma;
  import android.telephony.Rlog;
  import android.telephony.ServiceState;
  import android.telephony.SubscriptionManager;
  import android.telephony.TelephonyManager;
  import android.text.TextUtils;
  import android.util.LocalLog;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.IndentingPrintWriter;
  
  import java.io.FileDescriptor;
  import java.io.PrintWriter;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  
  /**
   * The locale tracker keeps tracking the current locale of the phone.
   */
  public class LocaleTracker extends Handler {
      private static final boolean DBG = true;
      private static final String TAG = LocaleTracker.class.getSimpleName();
  
      /** Event for getting cell info from the modem */
      private static final int EVENT_REQUEST_CELL_INFO = 1;
  
      /** Event for service state changed */
      private static final int EVENT_SERVICE_STATE_CHANGED = 2;
  
      /** Event for sim state changed */
      private static final int EVENT_SIM_STATE_CHANGED = 3;
  
      /** Event for incoming unsolicited cell info */
      private static final int EVENT_UNSOL_CELL_INFO = 4;
  
      /** Event for incoming cell info */
      private static final int EVENT_RESPONSE_CELL_INFO = 5;
  
      /** Event to fire if the operator from ServiceState is considered truly lost */
      private static final int EVENT_OPERATOR_LOST = 6;
  
      // Todo: Read this from Settings.
      /** The minimum delay to get cell info from the modem */
      private static final long CELL_INFO_MIN_DELAY_MS = 2 * SECOND_IN_MILLIS;
  
      // Todo: Read this from Settings.
      /** The maximum delay to get cell info from the modem */
      private static final long CELL_INFO_MAX_DELAY_MS = 10 * MINUTE_IN_MILLIS;
  
      // Todo: Read this from Settings.
      /** The delay for periodically getting cell info from the modem */
      private static final long CELL_INFO_PERIODIC_POLLING_DELAY_MS = 10 * MINUTE_IN_MILLIS;
  
      /**
       * The delay after the last time the device camped on a cell before declaring that the
       * ServiceState's MCC information can no longer be used (and thus kicking in the CellInfo
       * based tracking.
       */
      private static final long SERVICE_OPERATOR_LOST_DELAY_MS = 10 * MINUTE_IN_MILLIS;
  
      /** The maximum fail count to prevent delay time overflow */
      private static final int MAX_FAIL_COUNT = 30;
  
      private final Phone mPhone;
  
      private final NitzStateMachine mNitzStateMachine;
  
      /** SIM card state. Must be one of TelephonyManager.SIM_STATE_XXX */
      private int mSimState;
  
      /** Current serving PLMN's MCC/MNC */
      @Nullable
      private String mOperatorNumeric;
  
      /** Current cell tower information */
      @Nullable
      private List<CellInfo> mCellInfoList;
  
      /** Count of invalid cell info we've got so far. Will reset once we get a successful one */
      private int mFailCellInfoCount;
  
      /** The ISO-3166 two-letter code of device's current country */
      @Nullable
      private String mCurrentCountryIso;
  
      /** Current service state. Must be one of ServiceState.STATE_XXX. */
      private int mLastServiceState = ServiceState.STATE_POWER_OFF;
  
      private boolean mIsTracking = false;
  
      private final LocalLog mLocalLog = new LocalLog(50);
  
      /** Broadcast receiver to get SIM card state changed event */
      private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
              if (TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED.equals(intent.getAction())) {
                  int phoneId = intent.getIntExtra(PhoneConstants.PHONE_KEY, 0);
                  if (phoneId == mPhone.getPhoneId()) {
                      obtainMessage(EVENT_SIM_STATE_CHANGED,
                              intent.getIntExtra(TelephonyManager.EXTRA_SIM_STATE,
                                      TelephonyManager.SIM_STATE_UNKNOWN), 0).sendToTarget();
                  }
              }
          }
      };
  
      /**
       * Message handler
       *
       * @param msg The message
       */
      @Override
      public void handleMessage(Message msg) {
          switch (msg.what) {
              case EVENT_REQUEST_CELL_INFO:
                  mPhone.requestCellInfoUpdate(null, obtainMessage(EVENT_RESPONSE_CELL_INFO));
                  break;
  
              case EVENT_UNSOL_CELL_INFO:
                  processCellInfo((AsyncResult) msg.obj);
                  // If the unsol happened to be useful, use it; otherwise, pretend it didn't happen.
                  if (mCellInfoList != null && mCellInfoList.size() > 0) requestNextCellInfo(true);
                  break;
  
              case EVENT_RESPONSE_CELL_INFO:
                  processCellInfo((AsyncResult) msg.obj);
                  // If the cellInfo was non-empty then it's business as usual. Either way, this
                  // cell info was requested by us, so it's our trigger to schedule another one.
                  requestNextCellInfo(mCellInfoList != null && mCellInfoList.size() > 0);
                  break;
  
              case EVENT_SERVICE_STATE_CHANGED:
                  AsyncResult ar = (AsyncResult) msg.obj;
                  onServiceStateChanged((ServiceState) ar.result);
                  break;
  
              case EVENT_SIM_STATE_CHANGED:
                  onSimCardStateChanged(msg.arg1);
                  break;
  
              case EVENT_OPERATOR_LOST:
                  updateOperatorNumericImmediate("");
                  updateTrackingStatus();
                  break;
  
              default:
                  throw new IllegalStateException("Unexpected message arrives. msg = " + msg.what);
          }
      }
  
      /**
       * Constructor
       *
       * @param phone The phone object
       * @param nitzStateMachine NITZ state machine
       * @param looper The looper message handler
       */
      public LocaleTracker(Phone phone, NitzStateMachine nitzStateMachine, Looper looper)  {
          super(looper);
          mPhone = phone;
          mNitzStateMachine = nitzStateMachine;
          mSimState = TelephonyManager.SIM_STATE_UNKNOWN;
  
          final IntentFilter filter = new IntentFilter();
          filter.addAction(TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED);
          mPhone.getContext().registerReceiver(mBroadcastReceiver, filter);
  
          mPhone.registerForServiceStateChanged(this, EVENT_SERVICE_STATE_CHANGED, null);
          mPhone.registerForCellInfo(this, EVENT_UNSOL_CELL_INFO, null);
      }
  
      /**
       * Get the device's current country.
       *
       * @return The device's current country. Empty string if the information is not available.
       */
      @NonNull
      public String getCurrentCountry() {
          return (mCurrentCountryIso != null) ? mCurrentCountryIso : "";
      }
  
      /**
       * Get the MCC from cell tower information.
       *
       * @return MCC in string format. Null if the information is not available.
       */
      @Nullable
      private String getMccFromCellInfo() {
          String selectedMcc = null;
          if (mCellInfoList != null) {
              Map<String, Integer> countryCodeMap = new HashMap<>();
              int maxCount = 0;
              for (CellInfo cellInfo : mCellInfoList) {
                  String mcc = null;
                  if (cellInfo instanceof CellInfoGsm) {
                      mcc = ((CellInfoGsm) cellInfo).getCellIdentity().getMccString();
                  } else if (cellInfo instanceof CellInfoLte) {
                      mcc = ((CellInfoLte) cellInfo).getCellIdentity().getMccString();
                  } else if (cellInfo instanceof CellInfoWcdma) {
                      mcc = ((CellInfoWcdma) cellInfo).getCellIdentity().getMccString();
                  }
                  if (mcc != null) {
                      int count = 1;
                      if (countryCodeMap.containsKey(mcc)) {
                          count = countryCodeMap.get(mcc) + 1;
                      }
                      countryCodeMap.put(mcc, count);
                      // This is unlikely, but if MCC from cell info looks different, we choose the
                      // MCC that occurs most.
                      if (count > maxCount) {
                          maxCount = count;
                          selectedMcc = mcc;
                      }
                  }
              }
          }
          return selectedMcc;
      }
  
      /**
       * Called when SIM card state changed. Only when we absolutely know the SIM is absent, we get
       * cell info from the network. Other SIM states like NOT_READY might be just a transitioning
       * state.
       *
       * @param state SIM card state. Must be one of TelephonyManager.SIM_STATE_XXX.
       */
      private void onSimCardStateChanged(int state) {
          mSimState = state;
          updateLocale();
          updateTrackingStatus();
      }
  
      /**
       * Called when service state changed.
       *
       * @param serviceState Service state
       */
      private void onServiceStateChanged(ServiceState serviceState) {
          mLastServiceState = serviceState.getState();
          updateLocale();
          updateTrackingStatus();
      }
  
      /**
       * Update MCC/MNC from network service state.
       *
       * @param operatorNumeric MCC/MNC of the operator
       */
      public void updateOperatorNumeric(String operatorNumeric) {
          if (TextUtils.isEmpty(operatorNumeric)) {
              sendMessageDelayed(obtainMessage(EVENT_OPERATOR_LOST), SERVICE_OPERATOR_LOST_DELAY_MS);
          } else {
              removeMessages(EVENT_OPERATOR_LOST);
              updateOperatorNumericImmediate(operatorNumeric);
          }
      }
  
      private void updateOperatorNumericImmediate(String operatorNumeric) {
          // Check if the operator numeric changes.
          if (!operatorNumeric.equals(mOperatorNumeric)) {
              String msg = "Operator numeric changes to \"" + operatorNumeric + "\"";
              if (DBG) log(msg);
              mLocalLog.log(msg);
              mOperatorNumeric = operatorNumeric;
              updateLocale();
          }
      }
  
      private void processCellInfo(AsyncResult ar) {
          if (ar == null || ar.exception != null) {
              mCellInfoList = null;
              return;
          }
          List<CellInfo> cellInfoList = (List<CellInfo>) ar.result;
          String msg = "processCellInfo: cell info=" + cellInfoList;
          if (DBG) log(msg);
          mCellInfoList = cellInfoList;
          updateLocale();
      }
  
      private void requestNextCellInfo(boolean succeeded) {
          if (!mIsTracking) return;
  
          removeMessages(EVENT_REQUEST_CELL_INFO);
          if (succeeded) {
              resetCellInfoRetry();
              // Now we need to get the cell info from the modem periodically
              // even if we already got the cell info because the user can move.
              removeMessages(EVENT_UNSOL_CELL_INFO);
              removeMessages(EVENT_RESPONSE_CELL_INFO);
              sendMessageDelayed(obtainMessage(EVENT_REQUEST_CELL_INFO),
                      CELL_INFO_PERIODIC_POLLING_DELAY_MS);
          } else {
              // If we can't get a valid cell info. Try it again later.
              long delay = getCellInfoDelayTime(++mFailCellInfoCount);
              if (DBG) log("Can't get cell info. Try again in " + delay / 1000 + " secs.");
              sendMessageDelayed(obtainMessage(EVENT_REQUEST_CELL_INFO), delay);
          }
      }
  
      /**
       * Get the delay time to get cell info from modem. The delay time grows exponentially to prevent
       * battery draining.
       *
       * @param failCount Count of invalid cell info we've got so far.
       * @return The delay time for next get cell info
       */
      @VisibleForTesting
      public static long getCellInfoDelayTime(int failCount) {
          // Exponentially grow the delay time. Note we limit the fail count to MAX_FAIL_COUNT to
          // prevent overflow in Math.pow().
          long delay = CELL_INFO_MIN_DELAY_MS
                  * (long) Math.pow(2, Math.min(failCount, MAX_FAIL_COUNT) - 1);
          return Math.min(Math.max(delay, CELL_INFO_MIN_DELAY_MS), CELL_INFO_MAX_DELAY_MS);
      }
  
      /**
       * Stop retrying getting cell info from the modem. It cancels any scheduled cell info retrieving
       * request.
       */
      private void resetCellInfoRetry() {
          mFailCellInfoCount = 0;
          removeMessages(EVENT_REQUEST_CELL_INFO);
      }
  
      private void updateTrackingStatus() {
          boolean shouldTrackLocale =
                  (mSimState == TelephonyManager.SIM_STATE_ABSENT
                          || TextUtils.isEmpty(mOperatorNumeric))
                  && (mLastServiceState == ServiceState.STATE_OUT_OF_SERVICE
                          || mLastServiceState == ServiceState.STATE_EMERGENCY_ONLY);
          if (shouldTrackLocale) {
              startTracking();
          } else {
              stopTracking();
          }
      }
  
      private void stopTracking() {
          if (!mIsTracking) return;
          mIsTracking = false;
          String msg = "Stopping LocaleTracker";
          if (DBG) log(msg);
          mLocalLog.log(msg);
          mCellInfoList = null;
          resetCellInfoRetry();
      }
  
      private void startTracking() {
          if (mIsTracking) return;
          String msg = "Starting LocaleTracker";
          mLocalLog.log(msg);
          if (DBG) log(msg);
          mIsTracking = true;
          sendMessage(obtainMessage(EVENT_REQUEST_CELL_INFO));
      }
  
      /**
       * Update the device's current locale
       */
      private synchronized void updateLocale() {
          // If MCC is available from network service state, use it first.
          String mcc = null;
          String countryIso = "";
          if (!TextUtils.isEmpty(mOperatorNumeric)) {
              try {
                  mcc = mOperatorNumeric.substring(0, 3);
                  countryIso = MccTable.countryCodeForMcc(mcc);
              } catch (StringIndexOutOfBoundsException ex) {
                  loge("updateLocale: Can't get country from operator numeric. mcc = "
                          + mcc + ". ex=" + ex);
              }
          }
  
          // If for any reason we can't get country from operator numeric, try to get it from cell
          // info.
          if (TextUtils.isEmpty(countryIso)) {
              mcc = getMccFromCellInfo();
              countryIso = MccTable.countryCodeForMcc(mcc);
          }
  
          log("updateLocale: mcc = " + mcc + ", country = " + countryIso);
          boolean countryChanged = false;
          if (!Objects.equals(countryIso, mCurrentCountryIso)) {
              String msg = "updateLocale: Change the current country to \"" + countryIso
                      + "\", mcc = " + mcc + ", mCellInfoList = " + mCellInfoList;
              log(msg);
              mLocalLog.log(msg);
              mCurrentCountryIso = countryIso;
  
              TelephonyManager.setTelephonyProperty(mPhone.getPhoneId(),
                      TelephonyProperties.PROPERTY_OPERATOR_ISO_COUNTRY, mCurrentCountryIso);
  
              // Set the country code for wifi. This sets allowed wifi channels based on the
              // country of the carrier we see. If we can't see any, reset to 0 so we don't
              // broadcast on forbidden channels.
              WifiManager wifiManager = (WifiManager) mPhone.getContext()
                      .getSystemService(Context.WIFI_SERVICE);
              if (wifiManager != null) {
                  wifiManager.setCountryCode(countryIso);
              } else {
                  msg = "Wifi manager is not available.";
                  log(msg);
                  mLocalLog.log(msg);
              }
  
  
              Intent intent = new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED);
              intent.putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, countryIso);
              SubscriptionManager.putPhoneIdAndSubIdExtra(intent, mPhone.getPhoneId());
              mPhone.getContext().sendBroadcast(intent);
  
              countryChanged = true;
          }
  
          if (TextUtils.isEmpty(countryIso)) {
              mNitzStateMachine.handleNetworkCountryCodeUnavailable();
          } else {
              mNitzStateMachine.handleNetworkCountryCodeSet(countryChanged);
          }
      }
  
      /** Exposed for testing purposes */
      public boolean isTracking() {
          return mIsTracking;
      }
  
      private void log(String msg) {
          Rlog.d(TAG, msg);
      }
  
      private void loge(String msg) {
          Rlog.e(TAG, msg);
      }
  
      /**
       * Print the DeviceStateMonitor into the given stream.
       *
       * @param fd The raw file descriptor that the dump is being sent to.
       * @param pw A PrintWriter to which the dump is to be set.
       * @param args Additional arguments to the dump request.
       */
      public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
          final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
          pw.println("LocaleTracker:");
          ipw.increaseIndent();
          ipw.println("mIsTracking = " + mIsTracking);
          ipw.println("mOperatorNumeric = " + mOperatorNumeric);
          ipw.println("mSimState = " + mSimState);
          ipw.println("mCellInfoList = " + mCellInfoList);
          ipw.println("mCurrentCountryIso = " + mCurrentCountryIso);
          ipw.println("mFailCellInfoCount = " + mFailCellInfoCount);
          ipw.println("Local logs:");
          ipw.increaseIndent();
          mLocalLog.dump(fd, ipw, args);
          ipw.decreaseIndent();
          ipw.decreaseIndent();
          ipw.flush();
      }
  }
  