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