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