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