• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
getDefaultLocale()73         public Locale getDefaultLocale() {
74             return Locale.getDefault();
75         }
76     }
77 
CountryDetector(Context context)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 
CountryDetector(Context context, TelephonyManager telephonyManager, LocationManager locationManager, LocaleProvider localeProvider)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 
registerForLocationUpdates(Context context, LocationManager locationManager)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
getInstanceForTest(Context context, TelephonyManager telephonyManager, LocationManager locationManager, LocaleProvider localeProvider, Geocoder geocoder)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      */
getInstance(Context context)130     public synchronized static CountryDetector getInstance(Context context) {
131         if (sInstance == null) {
132             sInstance = new CountryDetector(context.getApplicationContext());
133         }
134         return sInstance;
135     }
136 
getCurrentCountryIso()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      */
getNetworkBasedCountryIso()160     private String getNetworkBasedCountryIso() {
161         return mTelephonyManager.getNetworkCountryIso();
162     }
163 
164     /**
165      * @return the geocoded country code detected by the {@link LocationManager}.
166      */
getLocationBasedCountryIso()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      */
getSimBasedCountryIso()179     private String getSimBasedCountryIso() {
180         return mTelephonyManager.getSimCountryIso();
181     }
182 
183     /**
184      * @return the country code of the user's currently selected locale.
185      */
getLocaleBasedCountryIso()186     private String getLocaleBasedCountryIso() {
187         Locale defaultLocale = mLocaleProvider.getDefaultLocale();
188         if (defaultLocale != null) {
189             return defaultLocale.getCountry();
190         }
191         return null;
192     }
193 
isNetworkCountryCodeAvailable()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
onReceive(final Context context, Intent intent)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