• 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 
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 {
getDefaultLocale()71         public Locale getDefaultLocale() {
72             return Locale.getDefault();
73         }
74     }
75 
CountryDetector(Context context)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 
CountryDetector(Context context, TelephonyManager telephonyManager, LocationManager locationManager, LocaleProvider localeProvider)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 
registerForLocationUpdates(Context context, LocationManager locationManager)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
getInstanceForTest(Context context, TelephonyManager telephonyManager, LocationManager locationManager, LocaleProvider localeProvider, Geocoder geocoder)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      */
getInstance(Context context)123     public synchronized static CountryDetector getInstance(Context context) {
124         if (sInstance == null) {
125             sInstance = new CountryDetector(context.getApplicationContext());
126         }
127         return sInstance;
128     }
129 
getCurrentCountryIso()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      */
getNetworkBasedCountryIso()153     private String getNetworkBasedCountryIso() {
154         return mTelephonyManager.getNetworkCountryIso();
155     }
156 
157     /**
158      * @return the geocoded country code detected by the {@link LocationManager}.
159      */
getLocationBasedCountryIso()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      */
getSimBasedCountryIso()172     private String getSimBasedCountryIso() {
173         return mTelephonyManager.getSimCountryIso();
174     }
175 
176     /**
177      * @return the country code of the user's currently selected locale.
178      */
getLocaleBasedCountryIso()179     private String getLocaleBasedCountryIso() {
180         Locale defaultLocale = mLocaleProvider.getDefaultLocale();
181         if (defaultLocale != null) {
182             return defaultLocale.getCountry();
183         }
184         return null;
185     }
186 
isNetworkCountryCodeAvailable()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
onReceive(final Context context, Intent intent)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