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.provider.Settings;
     24 import android.telephony.PhoneStateListener;
     25 import android.telephony.ServiceState;
     26 import android.telephony.TelephonyManager;
     27 import android.text.TextUtils;
     28 import android.util.Slog;
     29 
     30 import java.util.Locale;
     31 import java.util.Timer;
     32 import java.util.TimerTask;
     33 
     34 /**
     35  * This class is used to detect the country where the user is. The sources of
     36  * country are queried in order of reliability, like
     37  * <ul>
     38  * <li>Mobile network</li>
     39  * <li>Location</li>
     40  * <li>SIM's country</li>
     41  * <li>Phone's locale</li>
     42  * </ul>
     43  * <p>
     44  * Call the {@link #detectCountry()} to get the available country immediately.
     45  * <p>
     46  * To be notified of the future country change, using the
     47  * {@link #setCountryListener(CountryListener)}
     48  * <p>
     49  * Using the {@link #stop()} to stop listening to the country change.
     50  * <p>
     51  * The country information will be refreshed every
     52  * {@link #LOCATION_REFRESH_INTERVAL} once the location based country is used.
     53  *
     54  * @hide
     55  */
     56 public class ComprehensiveCountryDetector extends CountryDetectorBase {
     57 
     58     private final static String TAG = "ComprehensiveCountryDetector";
     59     /* package */ static final boolean DEBUG = false;
     60 
     61     /**
     62      * The refresh interval when the location based country was used
     63      */
     64     private final static long LOCATION_REFRESH_INTERVAL = 1000 * 60 * 60 * 24; // 1 day
     65 
     66     protected CountryDetectorBase mLocationBasedCountryDetector;
     67     protected Timer mLocationRefreshTimer;
     68 
     69     private final int mPhoneType;
     70     private Country mCountry;
     71     private TelephonyManager mTelephonyManager;
     72     private Country mCountryFromLocation;
     73     private boolean mStopped = false;
     74     private ServiceState mLastState;
     75 
     76     private PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
     77         @Override
     78         public void onServiceStateChanged(ServiceState serviceState) {
     79             // TODO: Find out how often we will be notified, if this method is called too
     80             // many times, let's consider querying the network.
     81             Slog.d(TAG, "onServiceStateChanged");
     82             // We only care the state change
     83             if (mLastState == null || mLastState.getState() != serviceState.getState()) {
     84                 detectCountry(true, true);
     85                 mLastState = new ServiceState(serviceState);
     86             }
     87         }
     88     };
     89 
     90     /**
     91      * The listener for receiving the notification from LocationBasedCountryDetector.
     92      */
     93     private CountryListener mLocationBasedCountryDetectionListener = new CountryListener() {
     94         @Override
     95         public void onCountryDetected(Country country) {
     96             if (DEBUG) Slog.d(TAG, "Country detected via LocationBasedCountryDetector");
     97             mCountryFromLocation = country;
     98             // Don't start the LocationBasedCountryDetector.
     99             detectCountry(true, false);
    100             stopLocationBasedDetector();
    101         }
    102     };
    103 
    104     public ComprehensiveCountryDetector(Context context) {
    105         super(context);
    106         mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    107         mPhoneType = mTelephonyManager.getPhoneType();
    108     }
    109 
    110     @Override
    111     public Country detectCountry() {
    112         // Don't start the LocationBasedCountryDetector if we have been stopped.
    113         return detectCountry(false, !mStopped);
    114     }
    115 
    116     @Override
    117     public void stop() {
    118         Slog.i(TAG, "Stop the detector.");
    119         cancelLocationRefresh();
    120         removePhoneStateListener();
    121         stopLocationBasedDetector();
    122         mListener = null;
    123         mStopped = true;
    124     }
    125 
    126     /**
    127      * Get the country from different sources in order of the reliability.
    128      */
    129     private Country getCountry() {
    130         Country result = null;
    131         result = getNetworkBasedCountry();
    132         if (result == null) {
    133             result = getLastKnownLocationBasedCountry();
    134         }
    135         if (result == null) {
    136             result = getSimBasedCountry();
    137         }
    138         if (result == null) {
    139             result = getLocaleCountry();
    140         }
    141         return result;
    142     }
    143 
    144     /**
    145      * @return the country from the mobile network.
    146      */
    147     protected Country getNetworkBasedCountry() {
    148         String countryIso = null;
    149         // TODO: The document says the result may be unreliable on CDMA networks. Shall we use
    150         // it on CDMA phone? We may test the Android primarily used countries.
    151         if (mPhoneType == TelephonyManager.PHONE_TYPE_GSM) {
    152             countryIso = mTelephonyManager.getNetworkCountryIso();
    153             if (!TextUtils.isEmpty(countryIso)) {
    154                 return new Country(countryIso, Country.COUNTRY_SOURCE_NETWORK);
    155             }
    156         }
    157         return null;
    158     }
    159 
    160     /**
    161      * @return the cached location based country.
    162      */
    163     protected Country getLastKnownLocationBasedCountry() {
    164         return mCountryFromLocation;
    165     }
    166 
    167     /**
    168      * @return the country from SIM card
    169      */
    170     protected Country getSimBasedCountry() {
    171         String countryIso = null;
    172         countryIso = mTelephonyManager.getSimCountryIso();
    173         if (!TextUtils.isEmpty(countryIso)) {
    174             return new Country(countryIso, Country.COUNTRY_SOURCE_SIM);
    175         }
    176         return null;
    177     }
    178 
    179     /**
    180      * @return the country from the system's locale.
    181      */
    182     protected Country getLocaleCountry() {
    183         Locale defaultLocale = Locale.getDefault();
    184         if (defaultLocale != null) {
    185             return new Country(defaultLocale.getCountry(), Country.COUNTRY_SOURCE_LOCALE);
    186         } else {
    187             return null;
    188         }
    189     }
    190 
    191     /**
    192      * @param notifyChange indicates whether the listener should be notified the change of the
    193      * country
    194      * @param startLocationBasedDetection indicates whether the LocationBasedCountryDetector could
    195      * be started if the current country source is less reliable than the location.
    196      * @return the current available UserCountry
    197      */
    198     private Country detectCountry(boolean notifyChange, boolean startLocationBasedDetection) {
    199         Country country = getCountry();
    200         runAfterDetectionAsync(mCountry != null ? new Country(mCountry) : mCountry, country,
    201                 notifyChange, startLocationBasedDetection);
    202         mCountry = country;
    203         return mCountry;
    204     }
    205 
    206     /**
    207      * Run the tasks in the service's thread.
    208      */
    209     protected void runAfterDetectionAsync(final Country country, final Country detectedCountry,
    210             final boolean notifyChange, final boolean startLocationBasedDetection) {
    211         mHandler.post(new Runnable() {
    212             @Override
    213             public void run() {
    214                 runAfterDetection(
    215                         country, detectedCountry, notifyChange, startLocationBasedDetection);
    216             }
    217         });
    218     }
    219 
    220     @Override
    221     public void setCountryListener(CountryListener listener) {
    222         CountryListener prevListener = mListener;
    223         mListener = listener;
    224         if (mListener == null) {
    225             // Stop listening all services
    226             removePhoneStateListener();
    227             stopLocationBasedDetector();
    228             cancelLocationRefresh();
    229         } else if (prevListener == null) {
    230             addPhoneStateListener();
    231             detectCountry(false, true);
    232         }
    233     }
    234 
    235     void runAfterDetection(final Country country, final Country detectedCountry,
    236             final boolean notifyChange, final boolean startLocationBasedDetection) {
    237         if (notifyChange) {
    238             notifyIfCountryChanged(country, detectedCountry);
    239         }
    240         if (DEBUG) {
    241             Slog.d(TAG, "startLocationBasedDetection=" + startLocationBasedDetection
    242                     + " detectCountry=" + (detectedCountry == null ? null :
    243                         "(source: " + detectedCountry.getSource()
    244                         + ", countryISO: " + detectedCountry.getCountryIso() + ")")
    245                     + " isAirplaneModeOff()=" + isAirplaneModeOff()
    246                     + " mListener=" + mListener
    247                     + " isGeoCoderImplemnted()=" + isGeoCoderImplemented());
    248         }
    249 
    250         if (startLocationBasedDetection && (detectedCountry == null
    251                 || detectedCountry.getSource() > Country.COUNTRY_SOURCE_LOCATION)
    252                 && isAirplaneModeOff() && mListener != null && isGeoCoderImplemented()) {
    253             if (DEBUG) Slog.d(TAG, "run startLocationBasedDetector()");
    254             // Start finding location when the source is less reliable than the
    255             // location and the airplane mode is off (as geocoder will not
    256             // work).
    257             // TODO : Shall we give up starting the detector within a
    258             // period of time?
    259             startLocationBasedDetector(mLocationBasedCountryDetectionListener);
    260         }
    261         if (detectedCountry == null
    262                 || detectedCountry.getSource() >= Country.COUNTRY_SOURCE_LOCATION) {
    263             // Schedule the location refresh if the country source is
    264             // not more reliable than the location or no country is
    265             // found.
    266             // TODO: Listen to the preference change of GPS, Wifi etc,
    267             // and start detecting the country.
    268             scheduleLocationRefresh();
    269         } else {
    270             // Cancel the location refresh once the current source is
    271             // more reliable than the location.
    272             cancelLocationRefresh();
    273             stopLocationBasedDetector();
    274         }
    275     }
    276 
    277     /**
    278      * Find the country from LocationProvider.
    279      */
    280     private synchronized void startLocationBasedDetector(CountryListener listener) {
    281         if (mLocationBasedCountryDetector != null) {
    282             return;
    283         }
    284         if (DEBUG) {
    285             Slog.d(TAG, "starts LocationBasedDetector to detect Country code via Location info "
    286                     + "(e.g. GPS)");
    287         }
    288         mLocationBasedCountryDetector = createLocationBasedCountryDetector();
    289         mLocationBasedCountryDetector.setCountryListener(listener);
    290         mLocationBasedCountryDetector.detectCountry();
    291     }
    292 
    293     private synchronized void stopLocationBasedDetector() {
    294         if (DEBUG) {
    295             Slog.d(TAG, "tries to stop LocationBasedDetector "
    296                     + "(current detector: " + mLocationBasedCountryDetector + ")");
    297         }
    298         if (mLocationBasedCountryDetector != null) {
    299             mLocationBasedCountryDetector.stop();
    300             mLocationBasedCountryDetector = null;
    301         }
    302     }
    303 
    304     protected CountryDetectorBase createLocationBasedCountryDetector() {
    305         return new LocationBasedCountryDetector(mContext);
    306     }
    307 
    308     protected boolean isAirplaneModeOff() {
    309         return Settings.System.getInt(
    310                 mContext.getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0) == 0;
    311     }
    312 
    313     /**
    314      * Notify the country change.
    315      */
    316     private void notifyIfCountryChanged(final Country country, final Country detectedCountry) {
    317         if (detectedCountry != null && mListener != null
    318                 && (country == null || !country.equals(detectedCountry))) {
    319             Slog.d(TAG,
    320                     "The country was changed from " + country != null ? country.getCountryIso() :
    321                         country + " to " + detectedCountry.getCountryIso());
    322             notifyListener(detectedCountry);
    323         }
    324     }
    325 
    326     /**
    327      * Schedule the next location refresh. We will do nothing if the scheduled task exists.
    328      */
    329     private synchronized void scheduleLocationRefresh() {
    330         if (mLocationRefreshTimer != null) return;
    331         if (DEBUG) {
    332             Slog.d(TAG, "start periodic location refresh timer. Interval: "
    333                     + LOCATION_REFRESH_INTERVAL);
    334         }
    335         mLocationRefreshTimer = new Timer();
    336         mLocationRefreshTimer.schedule(new TimerTask() {
    337             @Override
    338             public void run() {
    339                 if (DEBUG) {
    340                     Slog.d(TAG, "periodic location refresh event. Starts detecting Country code");
    341                 }
    342                 mLocationRefreshTimer = null;
    343                 detectCountry(false, true);
    344             }
    345         }, LOCATION_REFRESH_INTERVAL);
    346     }
    347 
    348     /**
    349      * Cancel the scheduled refresh task if it exists
    350      */
    351     private synchronized void cancelLocationRefresh() {
    352         if (mLocationRefreshTimer != null) {
    353             mLocationRefreshTimer.cancel();
    354             mLocationRefreshTimer = null;
    355         }
    356     }
    357 
    358     protected synchronized void addPhoneStateListener() {
    359         if (mPhoneStateListener == null && mPhoneType == TelephonyManager.PHONE_TYPE_GSM) {
    360             mLastState = null;
    361             mPhoneStateListener = new PhoneStateListener() {
    362                 @Override
    363                 public void onServiceStateChanged(ServiceState serviceState) {
    364                     // TODO: Find out how often we will be notified, if this
    365                     // method is called too
    366                     // many times, let's consider querying the network.
    367                     Slog.d(TAG, "onServiceStateChanged");
    368                     // We only care the state change
    369                     if (mLastState == null || mLastState.getState() != serviceState.getState()) {
    370                         detectCountry(true, true);
    371                         mLastState = new ServiceState(serviceState);
    372                     }
    373                 }
    374             };
    375             mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE);
    376         }
    377     }
    378 
    379     protected synchronized void removePhoneStateListener() {
    380         if (mPhoneStateListener != null) {
    381             mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
    382             mPhoneStateListener = null;
    383         }
    384     }
    385 
    386     protected boolean isGeoCoderImplemented() {
    387         return Geocoder.isPresent();
    388     }
    389 }
    390