Home | History | Annotate | Download | only in location
      1 /*
      2  * Copyright (C) 2010 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.location;
     18 
     19 import android.content.Context;
     20 import android.location.Country;
     21 import android.location.CountryListener;
     22 import android.location.Geocoder;
     23 import android.os.SystemClock;
     24 import android.provider.Settings;
     25 import android.telephony.PhoneStateListener;
     26 import android.telephony.ServiceState;
     27 import android.telephony.TelephonyManager;
     28 import android.text.TextUtils;
     29 import android.util.Log;
     30 import android.util.Slog;
     31 
     32 import java.util.Locale;
     33 import java.util.Timer;
     34 import java.util.TimerTask;
     35 import java.util.concurrent.ConcurrentLinkedQueue;
     36 
     37 /**
     38  * This class is used to detect the country where the user is. The sources of
     39  * country are queried in order of reliability, like
     40  * <ul>
     41  * <li>Mobile network</li>
     42  * <li>Location</li>
     43  * <li>SIM's country</li>
     44  * <li>Phone's locale</li>
     45  * </ul>
     46  * <p>
     47  * Call the {@link #detectCountry()} to get the available country immediately.
     48  * <p>
     49  * To be notified of the future country change, using the
     50  * {@link #setCountryListener(CountryListener)}
     51  * <p>
     52  * Using the {@link #stop()} to stop listening to the country change.
     53  * <p>
     54  * The country information will be refreshed every
     55  * {@link #LOCATION_REFRESH_INTERVAL} once the location based country is used.
     56  *
     57  * @hide
     58  */
     59 public class ComprehensiveCountryDetector extends CountryDetectorBase {
     60 
     61     private final static String TAG = "CountryDetector";
     62     /* package */ static final boolean DEBUG = false;
     63 
     64     /**
     65      * Max length of logs to maintain for debugging.
     66      */
     67     private static final int MAX_LENGTH_DEBUG_LOGS = 20;
     68 
     69     /**
     70      * The refresh interval when the location based country was used
     71      */
     72     private final static long LOCATION_REFRESH_INTERVAL = 1000 * 60 * 60 * 24; // 1 day
     73 
     74     protected CountryDetectorBase mLocationBasedCountryDetector;
     75     protected Timer mLocationRefreshTimer;
     76 
     77     private Country mCountry;
     78     private final TelephonyManager mTelephonyManager;
     79     private Country mCountryFromLocation;
     80     private boolean mStopped = false;
     81 
     82     private PhoneStateListener mPhoneStateListener;
     83 
     84     /**
     85      * List of the most recent country state changes for debugging. This should have
     86      * a max length of MAX_LENGTH_LOGS.
     87      */
     88     private final ConcurrentLinkedQueue<Country> mDebugLogs = new ConcurrentLinkedQueue<Country>();
     89 
     90     /**
     91      * Most recent {@link Country} result that was added to the debug logs {@link #mDebugLogs}.
     92      * We keep track of this value to help prevent adding many of the same {@link Country} objects
     93      * to the logs.
     94      */
     95     private Country mLastCountryAddedToLogs;
     96 
     97     /**
     98      * Object used to synchronize access to {@link #mLastCountryAddedToLogs}. Be careful if
     99      * using it to synchronize anything else in this file.
    100      */
    101     private final Object mObject = new Object();
    102 
    103     /**
    104      * Start time of the current session for which the detector has been active.
    105      */
    106     private long mStartTime;
    107 
    108     /**
    109      * Stop time of the most recent session for which the detector was active.
    110      */
    111     private long mStopTime;
    112 
    113     /**
    114      * The sum of all the time intervals in which the detector was active.
    115      */
    116     private long mTotalTime;
    117 
    118     /**
    119      * Number of {@link PhoneStateListener#onServiceStateChanged(ServiceState state)} events that
    120      * have occurred for the current session for which the detector has been active.
    121      */
    122     private int mCountServiceStateChanges;
    123 
    124     /**
    125      * Total number of {@link PhoneStateListener#onServiceStateChanged(ServiceState state)} events
    126      * that have occurred for all time intervals in which the detector has been active.
    127      */
    128     private int mTotalCountServiceStateChanges;
    129 
    130     /**
    131      * The listener for receiving the notification from LocationBasedCountryDetector.
    132      */
    133     private CountryListener mLocationBasedCountryDetectionListener = new CountryListener() {
    134         @Override
    135         public void onCountryDetected(Country country) {
    136             if (DEBUG) Slog.d(TAG, "Country detected via LocationBasedCountryDetector");
    137             mCountryFromLocation = country;
    138             // Don't start the LocationBasedCountryDetector.
    139             detectCountry(true, false);
    140             stopLocationBasedDetector();
    141         }
    142     };
    143 
    144     public ComprehensiveCountryDetector(Context context) {
    145         super(context);
    146         mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    147     }
    148 
    149     @Override
    150     public Country detectCountry() {
    151         // Don't start the LocationBasedCountryDetector if we have been stopped.
    152         return detectCountry(false, !mStopped);
    153     }
    154 
    155     @Override
    156     public void stop() {
    157         // Note: this method in this subclass called only by tests.
    158         Slog.i(TAG, "Stop the detector.");
    159         cancelLocationRefresh();
    160         removePhoneStateListener();
    161         stopLocationBasedDetector();
    162         mListener = null;
    163         mStopped = true;
    164     }
    165 
    166     /**
    167      * Get the country from different sources in order of the reliability.
    168      */
    169     private Country getCountry() {
    170         Country result = null;
    171         result = getNetworkBasedCountry();
    172         if (result == null) {
    173             result = getLastKnownLocationBasedCountry();
    174         }
    175         if (result == null) {
    176             result = getSimBasedCountry();
    177         }
    178         if (result == null) {
    179             result = getLocaleCountry();
    180         }
    181         addToLogs(result);
    182         return result;
    183     }
    184 
    185     /**
    186      * Attempt to add this {@link Country} to the debug logs.
    187      */
    188     private void addToLogs(Country country) {
    189         if (country == null) {
    190             return;
    191         }
    192         // If the country (ISO and source) are the same as before, then there is no
    193         // need to add this country as another entry in the logs. Synchronize access to this
    194         // variable since multiple threads could be calling this method.
    195         synchronized (mObject) {
    196             if (mLastCountryAddedToLogs != null && mLastCountryAddedToLogs.equals(country)) {
    197                 return;
    198             }
    199             mLastCountryAddedToLogs = country;
    200         }
    201         // Manually maintain a max limit for the list of logs
    202         if (mDebugLogs.size() >= MAX_LENGTH_DEBUG_LOGS) {
    203             mDebugLogs.poll();
    204         }
    205         if (DEBUG) {
    206             Slog.d(TAG, country.toString());
    207         }
    208         mDebugLogs.add(country);
    209     }
    210 
    211     private boolean isNetworkCountryCodeAvailable() {
    212         // On CDMA TelephonyManager.getNetworkCountryIso() just returns SIM country.  We don't want
    213         // to prioritize it over location based country, so ignore it.
    214         final int phoneType = mTelephonyManager.getPhoneType();
    215         if (DEBUG) Slog.v(TAG, "    phonetype=" + phoneType);
    216         return phoneType == TelephonyManager.PHONE_TYPE_GSM;
    217     }
    218 
    219     /**
    220      * @return the country from the mobile network.
    221      */
    222     protected Country getNetworkBasedCountry() {
    223         String countryIso = null;
    224         if (isNetworkCountryCodeAvailable()) {
    225             countryIso = mTelephonyManager.getNetworkCountryIso();
    226             if (!TextUtils.isEmpty(countryIso)) {
    227                 return new Country(countryIso, Country.COUNTRY_SOURCE_NETWORK);
    228             }
    229         }
    230         return null;
    231     }
    232 
    233     /**
    234      * @return the cached location based country.
    235      */
    236     protected Country getLastKnownLocationBasedCountry() {
    237         return mCountryFromLocation;
    238     }
    239 
    240     /**
    241      * @return the country from SIM card
    242      */
    243     protected Country getSimBasedCountry() {
    244         String countryIso = null;
    245         countryIso = mTelephonyManager.getSimCountryIso();
    246         if (!TextUtils.isEmpty(countryIso)) {
    247             return new Country(countryIso, Country.COUNTRY_SOURCE_SIM);
    248         }
    249         return null;
    250     }
    251 
    252     /**
    253      * @return the country from the system's locale.
    254      */
    255     protected Country getLocaleCountry() {
    256         Locale defaultLocale = Locale.getDefault();
    257         if (defaultLocale != null) {
    258             return new Country(defaultLocale.getCountry(), Country.COUNTRY_SOURCE_LOCALE);
    259         } else {
    260             return null;
    261         }
    262     }
    263 
    264     /**
    265      * @param notifyChange indicates whether the listener should be notified the change of the
    266      * country
    267      * @param startLocationBasedDetection indicates whether the LocationBasedCountryDetector could
    268      * be started if the current country source is less reliable than the location.
    269      * @return the current available UserCountry
    270      */
    271     private Country detectCountry(boolean notifyChange, boolean startLocationBasedDetection) {
    272         Country country = getCountry();
    273         runAfterDetectionAsync(mCountry != null ? new Country(mCountry) : mCountry, country,
    274                 notifyChange, startLocationBasedDetection);
    275         mCountry = country;
    276         return mCountry;
    277     }
    278 
    279     /**
    280      * Run the tasks in the service's thread.
    281      */
    282     protected void runAfterDetectionAsync(final Country country, final Country detectedCountry,
    283             final boolean notifyChange, final boolean startLocationBasedDetection) {
    284         mHandler.post(new Runnable() {
    285             @Override
    286             public void run() {
    287                 runAfterDetection(
    288                         country, detectedCountry, notifyChange, startLocationBasedDetection);
    289             }
    290         });
    291     }
    292 
    293     @Override
    294     public void setCountryListener(CountryListener listener) {
    295         CountryListener prevListener = mListener;
    296         mListener = listener;
    297         if (mListener == null) {
    298             // Stop listening all services
    299             removePhoneStateListener();
    300             stopLocationBasedDetector();
    301             cancelLocationRefresh();
    302             mStopTime = SystemClock.elapsedRealtime();
    303             mTotalTime += mStopTime;
    304         } else if (prevListener == null) {
    305             addPhoneStateListener();
    306             detectCountry(false, true);
    307             mStartTime = SystemClock.elapsedRealtime();
    308             mStopTime = 0;
    309             mCountServiceStateChanges = 0;
    310         }
    311     }
    312 
    313     void runAfterDetection(final Country country, final Country detectedCountry,
    314             final boolean notifyChange, final boolean startLocationBasedDetection) {
    315         if (notifyChange) {
    316             notifyIfCountryChanged(country, detectedCountry);
    317         }
    318         if (DEBUG) {
    319             Slog.d(TAG, "startLocationBasedDetection=" + startLocationBasedDetection
    320                     + " detectCountry=" + (detectedCountry == null ? null :
    321                         "(source: " + detectedCountry.getSource()
    322                         + ", countryISO: " + detectedCountry.getCountryIso() + ")")
    323                     + " isAirplaneModeOff()=" + isAirplaneModeOff()
    324                     + " mListener=" + mListener
    325                     + " isGeoCoderImplemnted()=" + isGeoCoderImplemented());
    326         }
    327 
    328         if (startLocationBasedDetection && (detectedCountry == null
    329                 || detectedCountry.getSource() > Country.COUNTRY_SOURCE_LOCATION)
    330                 && isAirplaneModeOff() && mListener != null && isGeoCoderImplemented()) {
    331             if (DEBUG) Slog.d(TAG, "run startLocationBasedDetector()");
    332             // Start finding location when the source is less reliable than the
    333             // location and the airplane mode is off (as geocoder will not
    334             // work).
    335             // TODO : Shall we give up starting the detector within a
    336             // period of time?
    337             startLocationBasedDetector(mLocationBasedCountryDetectionListener);
    338         }
    339         if (detectedCountry == null
    340                 || detectedCountry.getSource() >= Country.COUNTRY_SOURCE_LOCATION) {
    341             // Schedule the location refresh if the country source is
    342             // not more reliable than the location or no country is
    343             // found.
    344             // TODO: Listen to the preference change of GPS, Wifi etc,
    345             // and start detecting the country.
    346             scheduleLocationRefresh();
    347         } else {
    348             // Cancel the location refresh once the current source is
    349             // more reliable than the location.
    350             cancelLocationRefresh();
    351             stopLocationBasedDetector();
    352         }
    353     }
    354 
    355     /**
    356      * Find the country from LocationProvider.
    357      */
    358     private synchronized void startLocationBasedDetector(CountryListener listener) {
    359         if (mLocationBasedCountryDetector != null) {
    360             return;
    361         }
    362         if (DEBUG) {
    363             Slog.d(TAG, "starts LocationBasedDetector to detect Country code via Location info "
    364                     + "(e.g. GPS)");
    365         }
    366         mLocationBasedCountryDetector = createLocationBasedCountryDetector();
    367         mLocationBasedCountryDetector.setCountryListener(listener);
    368         mLocationBasedCountryDetector.detectCountry();
    369     }
    370 
    371     private synchronized void stopLocationBasedDetector() {
    372         if (DEBUG) {
    373             Slog.d(TAG, "tries to stop LocationBasedDetector "
    374                     + "(current detector: " + mLocationBasedCountryDetector + ")");
    375         }
    376         if (mLocationBasedCountryDetector != null) {
    377             mLocationBasedCountryDetector.stop();
    378             mLocationBasedCountryDetector = null;
    379         }
    380     }
    381 
    382     protected CountryDetectorBase createLocationBasedCountryDetector() {
    383         return new LocationBasedCountryDetector(mContext);
    384     }
    385 
    386     protected boolean isAirplaneModeOff() {
    387         return Settings.System.getInt(
    388                 mContext.getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0) == 0;
    389     }
    390 
    391     /**
    392      * Notify the country change.
    393      */
    394     private void notifyIfCountryChanged(final Country country, final Country detectedCountry) {
    395         if (detectedCountry != null && mListener != null
    396                 && (country == null || !country.equals(detectedCountry))) {
    397             if (DEBUG) {
    398                 Slog.d(TAG, "" + country + " --> " + detectedCountry);
    399             }
    400             notifyListener(detectedCountry);
    401         }
    402     }
    403 
    404     /**
    405      * Schedule the next location refresh. We will do nothing if the scheduled task exists.
    406      */
    407     private synchronized void scheduleLocationRefresh() {
    408         if (mLocationRefreshTimer != null) return;
    409         if (DEBUG) {
    410             Slog.d(TAG, "start periodic location refresh timer. Interval: "
    411                     + LOCATION_REFRESH_INTERVAL);
    412         }
    413         mLocationRefreshTimer = new Timer();
    414         mLocationRefreshTimer.schedule(new TimerTask() {
    415             @Override
    416             public void run() {
    417                 if (DEBUG) {
    418                     Slog.d(TAG, "periodic location refresh event. Starts detecting Country code");
    419                 }
    420                 mLocationRefreshTimer = null;
    421                 detectCountry(false, true);
    422             }
    423         }, LOCATION_REFRESH_INTERVAL);
    424     }
    425 
    426     /**
    427      * Cancel the scheduled refresh task if it exists
    428      */
    429     private synchronized void cancelLocationRefresh() {
    430         if (mLocationRefreshTimer != null) {
    431             mLocationRefreshTimer.cancel();
    432             mLocationRefreshTimer = null;
    433         }
    434     }
    435 
    436     protected synchronized void addPhoneStateListener() {
    437         if (mPhoneStateListener == null) {
    438             mPhoneStateListener = new PhoneStateListener() {
    439                 @Override
    440                 public void onServiceStateChanged(ServiceState serviceState) {
    441                     mCountServiceStateChanges++;
    442                     mTotalCountServiceStateChanges++;
    443 
    444                     if (!isNetworkCountryCodeAvailable()) {
    445                         return;
    446                     }
    447                     if (DEBUG) Slog.d(TAG, "onServiceStateChanged: " + serviceState.getState());
    448 
    449                     detectCountry(true, true);
    450                 }
    451             };
    452             mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE);
    453         }
    454     }
    455 
    456     protected synchronized void removePhoneStateListener() {
    457         if (mPhoneStateListener != null) {
    458             mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
    459             mPhoneStateListener = null;
    460         }
    461     }
    462 
    463     protected boolean isGeoCoderImplemented() {
    464         return Geocoder.isPresent();
    465     }
    466 
    467     @Override
    468     public String toString() {
    469         long currentTime = SystemClock.elapsedRealtime();
    470         long currentSessionLength = 0;
    471         StringBuilder sb = new StringBuilder();
    472         sb.append("ComprehensiveCountryDetector{");
    473         // The detector hasn't stopped yet --> still running
    474         if (mStopTime == 0) {
    475             currentSessionLength = currentTime - mStartTime;
    476             sb.append("timeRunning=" + currentSessionLength + ", ");
    477         } else {
    478             // Otherwise, it has already stopped, so take the last session
    479             sb.append("lastRunTimeLength=" + (mStopTime - mStartTime) + ", ");
    480         }
    481         sb.append("totalCountServiceStateChanges=" + mTotalCountServiceStateChanges + ", ");
    482         sb.append("currentCountServiceStateChanges=" + mCountServiceStateChanges + ", ");
    483         sb.append("totalTime=" + (mTotalTime + currentSessionLength) + ", ");
    484         sb.append("currentTime=" + currentTime + ", ");
    485         sb.append("countries=");
    486         for (Country country : mDebugLogs) {
    487             sb.append("\n   " + country.toString());
    488         }
    489         sb.append("}");
    490         return sb.toString();
    491     }
    492 }
    493