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.dialer.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.location.Address; 24 import android.location.Geocoder; 25 import android.location.Location; 26 import android.location.LocationManager; 27 import android.preference.PreferenceManager; 28 import android.support.annotation.NonNull; 29 import android.support.annotation.Nullable; 30 import android.support.annotation.VisibleForTesting; 31 import android.support.v4.os.UserManagerCompat; 32 import android.telephony.TelephonyManager; 33 import android.text.TextUtils; 34 import com.android.dialer.common.Assert; 35 import com.android.dialer.common.LogUtil; 36 import com.android.dialer.common.concurrent.DialerExecutor.Worker; 37 import com.android.dialer.common.concurrent.DialerExecutorComponent; 38 import com.android.dialer.util.PermissionsUtil; 39 import java.util.List; 40 import java.util.Locale; 41 42 /** 43 * This class is used to detect the country where the user is. It is a simplified version of the 44 * country detector service in the framework. The sources of country location are queried in the 45 * following order of reliability: 46 * 47 * <ul> 48 * <li>Mobile network 49 * <li>Location manager 50 * <li>SIM's country 51 * <li>User's default locale 52 * </ul> 53 * 54 * As far as possible this class tries to replicate the behavior of the system's country detector 55 * service: 1) Order in priority of sources of country location 2) Mobile network information 56 * provided by CDMA phones is ignored 3) Location information is updated every 12 hours (instead of 57 * 24 hours in the system) 4) Location updates only uses the {@link 58 * LocationManager#PASSIVE_PROVIDER} to avoid active use of the GPS 5) If a location is successfully 59 * obtained and geocoded, we never fall back to use of the SIM's country (for the system, the 60 * fallback never happens without a reboot) 6) Location is not used if the device does not implement 61 * a {@link android.location.Geocoder} 62 */ 63 public class CountryDetector { 64 private static final String KEY_PREFERENCE_TIME_UPDATED = "preference_time_updated"; 65 static final String KEY_PREFERENCE_CURRENT_COUNTRY = "preference_current_country"; 66 // Wait 12 hours between updates 67 private static final long TIME_BETWEEN_UPDATES_MS = 1000L * 60 * 60 * 12; 68 // Minimum distance before an update is triggered, in meters. We don't need this to be too 69 // exact because all we care about is what country the user is in. 70 private static final long DISTANCE_BETWEEN_UPDATES_METERS = 5000; 71 // Used as a default country code when all the sources of country data have failed in the 72 // exceedingly rare event that the device does not have a default locale set for some reason. 73 private static final String DEFAULT_COUNTRY_ISO = "US"; 74 75 @VisibleForTesting static CountryDetector sInstance; 76 77 private final TelephonyManager telephonyManager; 78 private final LocaleProvider localeProvider; 79 private final Geocoder geocoder; 80 private final Context appContext; 81 82 @VisibleForTesting 83 CountryDetector( 84 Context appContext, 85 TelephonyManager telephonyManager, 86 LocationManager locationManager, 87 LocaleProvider localeProvider, 88 Geocoder geocoder) { 89 this.telephonyManager = telephonyManager; 90 this.localeProvider = localeProvider; 91 this.appContext = appContext; 92 this.geocoder = geocoder; 93 94 // If the device does not implement Geocoder there is no point trying to get location updates 95 // because we cannot retrieve the country based on the location anyway. 96 if (Geocoder.isPresent()) { 97 registerForLocationUpdates(appContext, locationManager); 98 } 99 } 100 101 private static void registerForLocationUpdates(Context context, LocationManager locationManager) { 102 if (!PermissionsUtil.hasLocationPermissions(context)) { 103 LogUtil.w( 104 "CountryDetector.registerForLocationUpdates", 105 "no location permissions, not registering for location updates"); 106 return; 107 } 108 109 LogUtil.i("CountryDetector.registerForLocationUpdates", "registering for location updates"); 110 111 final Intent activeIntent = new Intent(context, LocationChangedReceiver.class); 112 final PendingIntent pendingIntent = 113 PendingIntent.getBroadcast(context, 0, activeIntent, PendingIntent.FLAG_UPDATE_CURRENT); 114 115 locationManager.requestLocationUpdates( 116 LocationManager.PASSIVE_PROVIDER, 117 TIME_BETWEEN_UPDATES_MS, 118 DISTANCE_BETWEEN_UPDATES_METERS, 119 pendingIntent); 120 } 121 122 /** @return the single instance of the {@link CountryDetector} */ 123 public static synchronized CountryDetector getInstance(Context context) { 124 if (sInstance == null) { 125 Context appContext = context.getApplicationContext(); 126 sInstance = 127 new CountryDetector( 128 appContext, 129 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE), 130 (LocationManager) context.getSystemService(Context.LOCATION_SERVICE), 131 Locale::getDefault, 132 new Geocoder(appContext)); 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 /** @return the country code of the current telephony network the user is connected to. */ 158 private String getNetworkBasedCountryIso() { 159 return telephonyManager.getNetworkCountryIso(); 160 } 161 162 /** @return the geocoded country code detected by the {@link LocationManager}. */ 163 @Nullable 164 private String getLocationBasedCountryIso() { 165 if (!Geocoder.isPresent() 166 || !PermissionsUtil.hasLocationPermissions(appContext) 167 || !UserManagerCompat.isUserUnlocked(appContext)) { 168 return null; 169 } 170 return PreferenceManager.getDefaultSharedPreferences(appContext) 171 .getString(KEY_PREFERENCE_CURRENT_COUNTRY, null); 172 } 173 174 /** @return the country code of the SIM card currently inserted in the device. */ 175 private String getSimBasedCountryIso() { 176 return telephonyManager.getSimCountryIso(); 177 } 178 179 /** @return the country code of the user's currently selected locale. */ 180 private String getLocaleBasedCountryIso() { 181 Locale defaultLocale = localeProvider.getLocale(); 182 if (defaultLocale != null) { 183 return defaultLocale.getCountry(); 184 } 185 return null; 186 } 187 188 private boolean isNetworkCountryCodeAvailable() { 189 // On CDMA TelephonyManager.getNetworkCountryIso() just returns the SIM's country code. 190 // In this case, we want to ignore the value returned and fallback to location instead. 191 return telephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM; 192 } 193 194 /** Interface for accessing the current locale. */ 195 interface LocaleProvider { 196 Locale getLocale(); 197 } 198 199 public static class LocationChangedReceiver extends BroadcastReceiver { 200 201 @Override 202 public void onReceive(final Context context, Intent intent) { 203 if (!intent.hasExtra(LocationManager.KEY_LOCATION_CHANGED)) { 204 return; 205 } 206 207 final Location location = 208 (Location) intent.getExtras().get(LocationManager.KEY_LOCATION_CHANGED); 209 210 // TODO: rething how we access the gecoder here, right now we have to set the static instance 211 // of CountryDetector to make this work for tests which is weird 212 // (see CountryDetectorTest.locationChangedBroadcast_GeocodesLocation) 213 processLocationUpdate(context, CountryDetector.getInstance(context).geocoder, location); 214 } 215 } 216 217 private static void processLocationUpdate( 218 Context appContext, Geocoder geocoder, Location location) { 219 DialerExecutorComponent.get(appContext) 220 .dialerExecutorFactory() 221 .createNonUiTaskBuilder(new GeocodeCountryWorker(geocoder)) 222 .onSuccess( 223 country -> { 224 if (country == null) { 225 return; 226 } 227 228 PreferenceManager.getDefaultSharedPreferences(appContext) 229 .edit() 230 .putLong(CountryDetector.KEY_PREFERENCE_TIME_UPDATED, System.currentTimeMillis()) 231 .putString(CountryDetector.KEY_PREFERENCE_CURRENT_COUNTRY, country) 232 .apply(); 233 }) 234 .onFailure( 235 throwable -> 236 LogUtil.w( 237 "CountryDetector.processLocationUpdate", 238 "exception occurred when getting geocoded country from location", 239 throwable)) 240 .build() 241 .executeParallel(location); 242 } 243 244 /** Worker that given a {@link Location} returns an ISO 3166-1 two letter country code. */ 245 private static class GeocodeCountryWorker implements Worker<Location, String> { 246 @NonNull private final Geocoder geocoder; 247 248 GeocodeCountryWorker(@NonNull Geocoder geocoder) { 249 this.geocoder = Assert.isNotNull(geocoder); 250 } 251 252 /** @return the ISO 3166-1 two letter country code if geocoded, else null */ 253 @Nullable 254 @Override 255 public String doInBackground(@Nullable Location location) throws Throwable { 256 if (location == null) { 257 return null; 258 } 259 260 List<Address> addresses = 261 geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1); 262 if (addresses != null && !addresses.isEmpty()) { 263 return addresses.get(0).getCountryCode(); 264 } 265 return null; 266 } 267 } 268 } 269