content: /*
   * Copyright (C) 2010 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.location;
  
  import android.content.Context;
  import android.location.Country;
  import android.location.CountryListener;
  import android.location.Geocoder;
  import android.os.SystemClock;
  import android.provider.Settings;
  import android.telephony.PhoneStateListener;
  import android.telephony.ServiceState;
  import android.telephony.TelephonyManager;
  import android.text.TextUtils;
  import android.util.Slog;
  
  import java.util.Locale;
  import java.util.Timer;
  import java.util.TimerTask;
  import java.util.concurrent.ConcurrentLinkedQueue;
  
  /**
   * This class is used to detect the country where the user is. The sources of
   * country are queried in order of reliability, like
   * <ul>
   * <li>Mobile network</li>
   * <li>Location</li>
   * <li>SIM's country</li>
   * <li>Phone's locale</li>
   * </ul>
   * <p>
   * Call the {@link #detectCountry()} to get the available country immediately.
   * <p>
   * To be notified of the future country change, using the
   * {@link #setCountryListener(CountryListener)}
   * <p>
   * Using the {@link #stop()} to stop listening to the country change.
   * <p>
   * The country information will be refreshed every
   * {@link #LOCATION_REFRESH_INTERVAL} once the location based country is used.
   *
   * @hide
   */
  public class ComprehensiveCountryDetector extends CountryDetectorBase {
  
      private final static String TAG = "CountryDetector";
      /* package */ static final boolean DEBUG = false;
  
      /**
       * Max length of logs to maintain for debugging.
       */
      private static final int MAX_LENGTH_DEBUG_LOGS = 20;
  
      /**
       * The refresh interval when the location based country was used
       */
      private final static long LOCATION_REFRESH_INTERVAL = 1000 * 60 * 60 * 24; // 1 day
  
      protected CountryDetectorBase mLocationBasedCountryDetector;
      protected Timer mLocationRefreshTimer;
  
      private Country mCountry;
      private final TelephonyManager mTelephonyManager;
      private Country mCountryFromLocation;
      private boolean mStopped = false;
  
      private PhoneStateListener mPhoneStateListener;
  
      /**
       * List of the most recent country state changes for debugging. This should have
       * a max length of MAX_LENGTH_LOGS.
       */
      private final ConcurrentLinkedQueue<Country> mDebugLogs = new ConcurrentLinkedQueue<Country>();
  
      /**
       * Most recent {@link Country} result that was added to the debug logs {@link #mDebugLogs}.
       * We keep track of this value to help prevent adding many of the same {@link Country} objects
       * to the logs.
       */
      private Country mLastCountryAddedToLogs;
  
      /**
       * Object used to synchronize access to {@link #mLastCountryAddedToLogs}. Be careful if
       * using it to synchronize anything else in this file.
       */
      private final Object mObject = new Object();
  
      /**
       * Start time of the current session for which the detector has been active.
       */
      private long mStartTime;
  
      /**
       * Stop time of the most recent session for which the detector was active.
       */
      private long mStopTime;
  
      /**
       * The sum of all the time intervals in which the detector was active.
       */
      private long mTotalTime;
  
      /**
       * Number of {@link PhoneStateListener#onServiceStateChanged(ServiceState state)} events that
       * have occurred for the current session for which the detector has been active.
       */
      private int mCountServiceStateChanges;
  
      /**
       * Total number of {@link PhoneStateListener#onServiceStateChanged(ServiceState state)} events
       * that have occurred for all time intervals in which the detector has been active.
       */
      private int mTotalCountServiceStateChanges;
  
      /**
       * The listener for receiving the notification from LocationBasedCountryDetector.
       */
      private CountryListener mLocationBasedCountryDetectionListener = new CountryListener() {
          @Override
          public void onCountryDetected(Country country) {
              if (DEBUG) Slog.d(TAG, "Country detected via LocationBasedCountryDetector");
              mCountryFromLocation = country;
              // Don't start the LocationBasedCountryDetector.
              detectCountry(true, false);
              stopLocationBasedDetector();
          }
      };
  
      public ComprehensiveCountryDetector(Context context) {
          super(context);
          mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
      }
  
      @Override
      public Country detectCountry() {
          // Don't start the LocationBasedCountryDetector if we have been stopped.
          return detectCountry(false, !mStopped);
      }
  
      @Override
      public void stop() {
          // Note: this method in this subclass called only by tests.
          Slog.i(TAG, "Stop the detector.");
          cancelLocationRefresh();
          removePhoneStateListener();
          stopLocationBasedDetector();
          mListener = null;
          mStopped = true;
      }
  
      /**
       * Get the country from different sources in order of the reliability.
       */
      private Country getCountry() {
          Country result = null;
          result = getNetworkBasedCountry();
          if (result == null) {
              result = getLastKnownLocationBasedCountry();
          }
          if (result == null) {
              result = getSimBasedCountry();
          }
          if (result == null) {
              result = getLocaleCountry();
          }
          addToLogs(result);
          return result;
      }
  
      /**
       * Attempt to add this {@link Country} to the debug logs.
       */
      private void addToLogs(Country country) {
          if (country == null) {
              return;
          }
          // If the country (ISO and source) are the same as before, then there is no
          // need to add this country as another entry in the logs. Synchronize access to this
          // variable since multiple threads could be calling this method.
          synchronized (mObject) {
              if (mLastCountryAddedToLogs != null && mLastCountryAddedToLogs.equals(country)) {
                  return;
              }
              mLastCountryAddedToLogs = country;
          }
          // Manually maintain a max limit for the list of logs
          if (mDebugLogs.size() >= MAX_LENGTH_DEBUG_LOGS) {
              mDebugLogs.poll();
          }
          if (DEBUG) {
              Slog.d(TAG, country.toString());
          }
          mDebugLogs.add(country);
      }
  
      private boolean isNetworkCountryCodeAvailable() {
          // On CDMA TelephonyManager.getNetworkCountryIso() just returns SIM country.  We don't want
          // to prioritize it over location based country, so ignore it.
          final int phoneType = mTelephonyManager.getPhoneType();
          if (DEBUG) Slog.v(TAG, "    phonetype=" + phoneType);
          return phoneType == TelephonyManager.PHONE_TYPE_GSM;
      }
  
      /**
       * @return the country from the mobile network.
       */
      protected Country getNetworkBasedCountry() {
          String countryIso = null;
          if (isNetworkCountryCodeAvailable()) {
              countryIso = mTelephonyManager.getNetworkCountryIso();
              if (!TextUtils.isEmpty(countryIso)) {
                  return new Country(countryIso, Country.COUNTRY_SOURCE_NETWORK);
              }
          }
          return null;
      }
  
      /**
       * @return the cached location based country.
       */
      protected Country getLastKnownLocationBasedCountry() {
          return mCountryFromLocation;
      }
  
      /**
       * @return the country from SIM card
       */
      protected Country getSimBasedCountry() {
          String countryIso = null;
          countryIso = mTelephonyManager.getSimCountryIso();
          if (!TextUtils.isEmpty(countryIso)) {
              return new Country(countryIso, Country.COUNTRY_SOURCE_SIM);
          }
          return null;
      }
  
      /**
       * @return the country from the system's locale.
       */
      protected Country getLocaleCountry() {
          Locale defaultLocale = Locale.getDefault();
          if (defaultLocale != null) {
              return new Country(defaultLocale.getCountry(), Country.COUNTRY_SOURCE_LOCALE);
          } else {
              return null;
          }
      }
  
      /**
       * @param notifyChange indicates whether the listener should be notified the change of the
       * country
       * @param startLocationBasedDetection indicates whether the LocationBasedCountryDetector could
       * be started if the current country source is less reliable than the location.
       * @return the current available UserCountry
       */
      private Country detectCountry(boolean notifyChange, boolean startLocationBasedDetection) {
          Country country = getCountry();
          runAfterDetectionAsync(mCountry != null ? new Country(mCountry) : mCountry, country,
                  notifyChange, startLocationBasedDetection);
          mCountry = country;
          return mCountry;
      }
  
      /**
       * Run the tasks in the service's thread.
       */
      protected void runAfterDetectionAsync(final Country country, final Country detectedCountry,
              final boolean notifyChange, final boolean startLocationBasedDetection) {
          mHandler.post(new Runnable() {
              @Override
              public void run() {
                  runAfterDetection(
                          country, detectedCountry, notifyChange, startLocationBasedDetection);
              }
          });
      }
  
      @Override
      public void setCountryListener(CountryListener listener) {
          CountryListener prevListener = mListener;
          mListener = listener;
          if (mListener == null) {
              // Stop listening all services
              removePhoneStateListener();
              stopLocationBasedDetector();
              cancelLocationRefresh();
              mStopTime = SystemClock.elapsedRealtime();
              mTotalTime += mStopTime;
          } else if (prevListener == null) {
              addPhoneStateListener();
              detectCountry(false, true);
              mStartTime = SystemClock.elapsedRealtime();
              mStopTime = 0;
              mCountServiceStateChanges = 0;
          }
      }
  
      void runAfterDetection(final Country country, final Country detectedCountry,
              final boolean notifyChange, final boolean startLocationBasedDetection) {
          if (notifyChange) {
              notifyIfCountryChanged(country, detectedCountry);
          }
          if (DEBUG) {
              Slog.d(TAG, "startLocationBasedDetection=" + startLocationBasedDetection
                      + " detectCountry=" + (detectedCountry == null ? null :
                          "(source: " + detectedCountry.getSource()
                          + ", countryISO: " + detectedCountry.getCountryIso() + ")")
                      + " isAirplaneModeOff()=" + isAirplaneModeOff()
                      + " mListener=" + mListener
                      + " isGeoCoderImplemnted()=" + isGeoCoderImplemented());
          }
  
          if (startLocationBasedDetection && (detectedCountry == null
                  || detectedCountry.getSource() > Country.COUNTRY_SOURCE_LOCATION)
                  && isAirplaneModeOff() && mListener != null && isGeoCoderImplemented()) {
              if (DEBUG) Slog.d(TAG, "run startLocationBasedDetector()");
              // Start finding location when the source is less reliable than the
              // location and the airplane mode is off (as geocoder will not
              // work).
              // TODO : Shall we give up starting the detector within a
              // period of time?
              startLocationBasedDetector(mLocationBasedCountryDetectionListener);
          }
          if (detectedCountry == null
                  || detectedCountry.getSource() >= Country.COUNTRY_SOURCE_LOCATION) {
              // Schedule the location refresh if the country source is
              // not more reliable than the location or no country is
              // found.
              // TODO: Listen to the preference change of GPS, Wifi etc,
              // and start detecting the country.
              scheduleLocationRefresh();
          } else {
              // Cancel the location refresh once the current source is
              // more reliable than the location.
              cancelLocationRefresh();
              stopLocationBasedDetector();
          }
      }
  
      /**
       * Find the country from LocationProvider.
       */
      private synchronized void startLocationBasedDetector(CountryListener listener) {
          if (mLocationBasedCountryDetector != null) {
              return;
          }
          if (DEBUG) {
              Slog.d(TAG, "starts LocationBasedDetector to detect Country code via Location info "
                      + "(e.g. GPS)");
          }
          mLocationBasedCountryDetector = createLocationBasedCountryDetector();
          mLocationBasedCountryDetector.setCountryListener(listener);
          mLocationBasedCountryDetector.detectCountry();
      }
  
      private synchronized void stopLocationBasedDetector() {
          if (DEBUG) {
              Slog.d(TAG, "tries to stop LocationBasedDetector "
                      + "(current detector: " + mLocationBasedCountryDetector + ")");
          }
          if (mLocationBasedCountryDetector != null) {
              mLocationBasedCountryDetector.stop();
              mLocationBasedCountryDetector = null;
          }
      }
  
      protected CountryDetectorBase createLocationBasedCountryDetector() {
          return new LocationBasedCountryDetector(mContext);
      }
  
      protected boolean isAirplaneModeOff() {
          return Settings.Global.getInt(
                  mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0) == 0;
      }
  
      /**
       * Notify the country change.
       */
      private void notifyIfCountryChanged(final Country country, final Country detectedCountry) {
          if (detectedCountry != null && mListener != null
                  && (country == null || !country.equals(detectedCountry))) {
              if (DEBUG) {
                  Slog.d(TAG, "" + country + " --> " + detectedCountry);
              }
              notifyListener(detectedCountry);
          }
      }
  
      /**
       * Schedule the next location refresh. We will do nothing if the scheduled task exists.
       */
      private synchronized void scheduleLocationRefresh() {
          if (mLocationRefreshTimer != null) return;
          if (DEBUG) {
              Slog.d(TAG, "start periodic location refresh timer. Interval: "
                      + LOCATION_REFRESH_INTERVAL);
          }
          mLocationRefreshTimer = new Timer();
          mLocationRefreshTimer.schedule(new TimerTask() {
              @Override
              public void run() {
                  if (DEBUG) {
                      Slog.d(TAG, "periodic location refresh event. Starts detecting Country code");
                  }
                  mLocationRefreshTimer = null;
                  detectCountry(false, true);
              }
          }, LOCATION_REFRESH_INTERVAL);
      }
  
      /**
       * Cancel the scheduled refresh task if it exists
       */
      private synchronized void cancelLocationRefresh() {
          if (mLocationRefreshTimer != null) {
              mLocationRefreshTimer.cancel();
              mLocationRefreshTimer = null;
          }
      }
  
      protected synchronized void addPhoneStateListener() {
          if (mPhoneStateListener == null) {
              mPhoneStateListener = new PhoneStateListener() {
                  @Override
                  public void onServiceStateChanged(ServiceState serviceState) {
                      mCountServiceStateChanges++;
                      mTotalCountServiceStateChanges++;
  
                      if (!isNetworkCountryCodeAvailable()) {
                          return;
                      }
                      if (DEBUG) Slog.d(TAG, "onServiceStateChanged: " + serviceState.getState());
  
                      detectCountry(true, true);
                  }
              };
              mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE);
          }
      }
  
      protected synchronized void removePhoneStateListener() {
          if (mPhoneStateListener != null) {
              mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
              mPhoneStateListener = null;
          }
      }
  
      protected boolean isGeoCoderImplemented() {
          return Geocoder.isPresent();
      }
  
      @Override
      public String toString() {
          long currentTime = SystemClock.elapsedRealtime();
          long currentSessionLength = 0;
          StringBuilder sb = new StringBuilder();
          sb.append("ComprehensiveCountryDetector{");
          // The detector hasn't stopped yet --> still running
          if (mStopTime == 0) {
              currentSessionLength = currentTime - mStartTime;
              sb.append("timeRunning=" + currentSessionLength + ", ");
          } else {
              // Otherwise, it has already stopped, so take the last session
              sb.append("lastRunTimeLength=" + (mStopTime - mStartTime) + ", ");
          }
          sb.append("totalCountServiceStateChanges=" + mTotalCountServiceStateChanges + ", ");
          sb.append("currentCountServiceStateChanges=" + mCountServiceStateChanges + ", ");
          sb.append("totalTime=" + (mTotalTime + currentSessionLength) + ", ");
          sb.append("currentTime=" + currentTime + ", ");
          sb.append("countries=");
          for (Country country : mDebugLogs) {
              sb.append("\n   " + country.toString());
          }
          sb.append("}");
          return sb.toString();
      }
  }
  