Home | History | Annotate | Download | only in location
      1 package com.android.contacts.common.location;
      2 
      3 import android.app.PendingIntent;
      4 import android.content.BroadcastReceiver;
      5 import android.content.Context;
      6 import android.content.Intent;
      7 import android.content.SharedPreferences;
      8 import android.location.Geocoder;
      9 import android.location.Location;
     10 import android.location.LocationManager;
     11 import android.preference.PreferenceManager;
     12 import android.telephony.TelephonyManager;
     13 import android.text.TextUtils;
     14 
     15 import com.android.contacts.common.testing.NeededForTesting;
     16 
     17 import java.util.Locale;
     18 
     19 /**
     20  * This class is used to detect the country where the user is. It is a simplified version of the
     21  * country detector service in the framework. The sources of country location are queried in the
     22  * following order of reliability:
     23  * <ul>
     24  * <li>Mobile network</li>
     25  * <li>Location manager</li>
     26  * <li>SIM's country</li>
     27  * <li>User's default locale</li>
     28  * </ul>
     29  *
     30  * As far as possible this class tries to replicate the behavior of the system's country detector
     31  * service:
     32  * 1) Order in priority of sources of country location
     33  * 2) Mobile network information provided by CDMA phones is ignored
     34  * 3) Location information is updated every 12 hours (instead of 24 hours in the system)
     35  * 4) Location updates only uses the {@link LocationManager#PASSIVE_PROVIDER} to avoid active use
     36  *    of the GPS
     37  * 5) If a location is successfully obtained and geocoded, we never fall back to use of the
     38  *    SIM's country (for the system, the fallback never happens without a reboot)
     39  * 6) Location is not used if the device does not implement a {@link android.location.Geocoder}
     40 */
     41 public class CountryDetector {
     42     private static final String TAG = "CountryDetector";
     43 
     44     public static final String KEY_PREFERENCE_TIME_UPDATED = "preference_time_updated";
     45     public static final String KEY_PREFERENCE_CURRENT_COUNTRY = "preference_current_country";
     46 
     47     private static CountryDetector sInstance;
     48 
     49     private final TelephonyManager mTelephonyManager;
     50     private final LocationManager mLocationManager;
     51     private final LocaleProvider mLocaleProvider;
     52 
     53     // Used as a default country code when all the sources of country data have failed in the
     54     // exceedingly rare event that the device does not have a default locale set for some reason.
     55     private final String DEFAULT_COUNTRY_ISO = "US";
     56 
     57     // Wait 12 hours between updates
     58     private static final long TIME_BETWEEN_UPDATES_MS = 1000L * 60 * 60 * 12;
     59 
     60     // Minimum distance before an update is triggered, in meters. We don't need this to be too
     61     // exact because all we care about is what country the user is in.
     62     private static final long DISTANCE_BETWEEN_UPDATES_METERS = 5000;
     63 
     64     private final Context mContext;
     65 
     66     /**
     67      * Class that can be used to return the user's default locale. This is in its own class so that
     68      * it can be mocked out.
     69      */
     70     public static class LocaleProvider {
     71         public Locale getDefaultLocale() {
     72             return Locale.getDefault();
     73         }
     74     }
     75 
     76     private CountryDetector(Context context) {
     77         this (context, (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE),
     78                 (LocationManager) context.getSystemService(Context.LOCATION_SERVICE),
     79                 new LocaleProvider());
     80     }
     81 
     82     private CountryDetector(Context context, TelephonyManager telephonyManager,
     83             LocationManager locationManager, LocaleProvider localeProvider) {
     84         mTelephonyManager = telephonyManager;
     85         mLocationManager = locationManager;
     86         mLocaleProvider = localeProvider;
     87         mContext = context;
     88 
     89         registerForLocationUpdates(context, mLocationManager);
     90     }
     91 
     92     public static void registerForLocationUpdates(Context context,
     93             LocationManager locationManager) {
     94         if (!Geocoder.isPresent()) {
     95             // Certain devices do not have an implementation of a geocoder - in that case there is
     96             // no point trying to get location updates because we cannot retrieve the country based
     97             // on the location anyway.
     98             return;
     99         }
    100         final Intent activeIntent = new Intent(context, LocationChangedReceiver.class);
    101         final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, activeIntent,
    102                 PendingIntent.FLAG_UPDATE_CURRENT);
    103 
    104         locationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER,
    105                 TIME_BETWEEN_UPDATES_MS, DISTANCE_BETWEEN_UPDATES_METERS, pendingIntent);
    106     }
    107 
    108     /**
    109      * Factory method for {@link CountryDetector} that allows the caller to provide mock objects.
    110      */
    111     @NeededForTesting
    112     public CountryDetector getInstanceForTest(Context context, TelephonyManager telephonyManager,
    113             LocationManager locationManager, LocaleProvider localeProvider, Geocoder geocoder) {
    114         return new CountryDetector(context, telephonyManager, locationManager, localeProvider);
    115     }
    116 
    117     /**
    118      * Returns the instance of the country detector. {@link #initialize(Context)} must have been
    119      * called previously.
    120      *
    121      * @return the initialized country detector.
    122      */
    123     public synchronized static CountryDetector getInstance(Context context) {
    124         if (sInstance == null) {
    125             sInstance = new CountryDetector(context.getApplicationContext());
    126         }
    127         return sInstance;
    128     }
    129 
    130     public String getCurrentCountryIso() {
    131         String result = null;
    132         if (isNetworkCountryCodeAvailable()) {
    133             result = getNetworkBasedCountryIso();
    134         }
    135         if (TextUtils.isEmpty(result)) {
    136             result = getLocationBasedCountryIso();
    137         }
    138         if (TextUtils.isEmpty(result)) {
    139             result = getSimBasedCountryIso();
    140         }
    141         if (TextUtils.isEmpty(result)) {
    142             result = getLocaleBasedCountryIso();
    143         }
    144         if (TextUtils.isEmpty(result)) {
    145             result = DEFAULT_COUNTRY_ISO;
    146         }
    147         return result.toUpperCase(Locale.US);
    148     }
    149 
    150     /**
    151      * @return the country code of the current telephony network the user is connected to.
    152      */
    153     private String getNetworkBasedCountryIso() {
    154         return mTelephonyManager.getNetworkCountryIso();
    155     }
    156 
    157     /**
    158      * @return the geocoded country code detected by the {@link LocationManager}.
    159      */
    160     private String getLocationBasedCountryIso() {
    161         if (!Geocoder.isPresent()) {
    162             return null;
    163         }
    164         final SharedPreferences sharedPreferences =
    165                 PreferenceManager.getDefaultSharedPreferences(mContext);
    166         return sharedPreferences.getString(KEY_PREFERENCE_CURRENT_COUNTRY, null);
    167     }
    168 
    169     /**
    170      * @return the country code of the SIM card currently inserted in the device.
    171      */
    172     private String getSimBasedCountryIso() {
    173         return mTelephonyManager.getSimCountryIso();
    174     }
    175 
    176     /**
    177      * @return the country code of the user's currently selected locale.
    178      */
    179     private String getLocaleBasedCountryIso() {
    180         Locale defaultLocale = mLocaleProvider.getDefaultLocale();
    181         if (defaultLocale != null) {
    182             return defaultLocale.getCountry();
    183         }
    184         return null;
    185     }
    186 
    187     private boolean isNetworkCountryCodeAvailable() {
    188         // On CDMA TelephonyManager.getNetworkCountryIso() just returns the SIM's country code.
    189         // In this case, we want to ignore the value returned and fallback to location instead.
    190         return mTelephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM;
    191     }
    192 
    193     public static class LocationChangedReceiver extends BroadcastReceiver {
    194 
    195         @Override
    196         public void onReceive(final Context context, Intent intent) {
    197             if (!intent.hasExtra(LocationManager.KEY_LOCATION_CHANGED)) {
    198                 return;
    199             }
    200 
    201             final Location location = (Location)intent.getExtras().get(
    202                     LocationManager.KEY_LOCATION_CHANGED);
    203 
    204             UpdateCountryService.updateCountry(context, location);
    205         }
    206     }
    207 
    208 }
    209