1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.contacts.common.location; 18 19 import android.app.PendingIntent; 20 import android.content.BroadcastReceiver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.SharedPreferences; 24 import android.location.Geocoder; 25 import android.location.Location; 26 import android.location.LocationManager; 27 import android.preference.PreferenceManager; 28 import android.telephony.TelephonyManager; 29 import android.text.TextUtils; 30 import android.util.Log; 31 import com.android.dialer.util.PermissionsUtil; 32 import java.util.Locale; 33 34 /** 35 * This class is used to detect the country where the user is. It is a simplified version of the 36 * country detector service in the framework. The sources of country location are queried in the 37 * following order of reliability: 38 * 39 * <ul> 40 * <li>Mobile network 41 * <li>Location manager 42 * <li>SIM's country 43 * <li>User's default locale 44 * </ul> 45 * 46 * As far as possible this class tries to replicate the behavior of the system's country detector 47 * service: 1) Order in priority of sources of country location 2) Mobile network information 48 * provided by CDMA phones is ignored 3) Location information is updated every 12 hours (instead of 49 * 24 hours in the system) 4) Location updates only uses the {@link 50 * LocationManager#PASSIVE_PROVIDER} to avoid active use of the GPS 5) If a location is successfully 51 * obtained and geocoded, we never fall back to use of the SIM's country (for the system, the 52 * fallback never happens without a reboot) 6) Location is not used if the device does not implement 53 * a {@link android.location.Geocoder} 54 */ 55 public class CountryDetector { 56 57 public static final String KEY_PREFERENCE_TIME_UPDATED = "preference_time_updated"; 58 public static final String KEY_PREFERENCE_CURRENT_COUNTRY = "preference_current_country"; 59 private static final String TAG = "CountryDetector"; 60 // Wait 12 hours between updates 61 private static final long TIME_BETWEEN_UPDATES_MS = 1000L * 60 * 60 * 12; 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 private static CountryDetector sInstance; 66 private final TelephonyManager mTelephonyManager; 67 private final LocationManager mLocationManager; 68 private final LocaleProvider mLocaleProvider; 69 // Used as a default country code when all the sources of country data have failed in the 70 // exceedingly rare event that the device does not have a default locale set for some reason. 71 private static final String DEFAULT_COUNTRY_ISO = "US"; 72 private final Context mContext; 73 CountryDetector(Context context)74 private CountryDetector(Context context) { 75 this( 76 context, 77 (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( 83 Context context, 84 TelephonyManager telephonyManager, 85 LocationManager locationManager, 86 LocaleProvider localeProvider) { 87 mTelephonyManager = telephonyManager; 88 mLocationManager = locationManager; 89 mLocaleProvider = localeProvider; 90 mContext = context; 91 92 registerForLocationUpdates(context, mLocationManager); 93 } 94 registerForLocationUpdates(Context context, LocationManager locationManager)95 public static void registerForLocationUpdates(Context context, 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 = 109 PendingIntent.getBroadcast(context, 0, activeIntent, PendingIntent.FLAG_UPDATE_CURRENT); 110 111 locationManager.requestLocationUpdates( 112 LocationManager.PASSIVE_PROVIDER, 113 TIME_BETWEEN_UPDATES_MS, 114 DISTANCE_BETWEEN_UPDATES_METERS, 115 pendingIntent); 116 } 117 118 /** 119 * Returns the instance of the country detector. {@link #initialize(Context)} must have been 120 * called previously. 121 * 122 * @return the initialized country detector. 123 */ getInstance(Context context)124 public static synchronized CountryDetector getInstance(Context context) { 125 if (sInstance == null) { 126 sInstance = new CountryDetector(context.getApplicationContext()); 127 } 128 return sInstance; 129 } 130 131 /** Factory method for {@link CountryDetector} that allows the caller to provide mock objects. */ getInstanceForTest( Context context, TelephonyManager telephonyManager, LocationManager locationManager, LocaleProvider localeProvider, Geocoder geocoder)132 public CountryDetector getInstanceForTest( 133 Context context, 134 TelephonyManager telephonyManager, 135 LocationManager locationManager, 136 LocaleProvider localeProvider, 137 Geocoder geocoder) { 138 return new CountryDetector(context, telephonyManager, locationManager, localeProvider); 139 } 140 getCurrentCountryIso()141 public String getCurrentCountryIso() { 142 String result = null; 143 if (isNetworkCountryCodeAvailable()) { 144 result = getNetworkBasedCountryIso(); 145 } 146 if (TextUtils.isEmpty(result)) { 147 result = getLocationBasedCountryIso(); 148 } 149 if (TextUtils.isEmpty(result)) { 150 result = getSimBasedCountryIso(); 151 } 152 if (TextUtils.isEmpty(result)) { 153 result = getLocaleBasedCountryIso(); 154 } 155 if (TextUtils.isEmpty(result)) { 156 result = DEFAULT_COUNTRY_ISO; 157 } 158 return result.toUpperCase(Locale.US); 159 } 160 161 /** @return the country code of the current telephony network the user is connected to. */ getNetworkBasedCountryIso()162 private String getNetworkBasedCountryIso() { 163 return mTelephonyManager.getNetworkCountryIso(); 164 } 165 166 /** @return the geocoded country code detected by the {@link LocationManager}. */ 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 /** @return the country code of the SIM card currently inserted in the device. */ getSimBasedCountryIso()177 private String getSimBasedCountryIso() { 178 return mTelephonyManager.getSimCountryIso(); 179 } 180 181 /** @return the country code of the user's currently selected locale. */ getLocaleBasedCountryIso()182 private String getLocaleBasedCountryIso() { 183 Locale defaultLocale = mLocaleProvider.getDefaultLocale(); 184 if (defaultLocale != null) { 185 return defaultLocale.getCountry(); 186 } 187 return null; 188 } 189 isNetworkCountryCodeAvailable()190 private boolean isNetworkCountryCodeAvailable() { 191 // On CDMA TelephonyManager.getNetworkCountryIso() just returns the SIM's country code. 192 // In this case, we want to ignore the value returned and fallback to location instead. 193 return mTelephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM; 194 } 195 196 /** 197 * Class that can be used to return the user's default locale. This is in its own class so that it 198 * can be mocked out. 199 */ 200 public static class LocaleProvider { 201 getDefaultLocale()202 public Locale getDefaultLocale() { 203 return Locale.getDefault(); 204 } 205 } 206 207 public static class LocationChangedReceiver extends BroadcastReceiver { 208 209 @Override onReceive(final Context context, Intent intent)210 public void onReceive(final Context context, Intent intent) { 211 if (!intent.hasExtra(LocationManager.KEY_LOCATION_CHANGED)) { 212 return; 213 } 214 215 final Location location = 216 (Location) intent.getExtras().get(LocationManager.KEY_LOCATION_CHANGED); 217 218 UpdateCountryService.updateCountry(context, location); 219 } 220 } 221 } 222