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