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