/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.dialer.location; import android.Manifest.permission; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.location.Address; import android.location.Geocoder; import android.location.Location; import android.location.LocationManager; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v4.os.UserManagerCompat; import android.telephony.TelephonyManager; import android.text.TextUtils; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.DialerExecutor.Worker; import com.android.dialer.common.concurrent.DialerExecutorComponent; import java.util.List; import java.util.Locale; /** * This class is used to detect the country where the user is. It is a simplified version of the * country detector service in the framework. The sources of country location are queried in the * following order of reliability: * * * * As far as possible this class tries to replicate the behavior of the system's country detector * service: 1) Order in priority of sources of country location 2) Mobile network information * provided by CDMA phones is ignored 3) Location information is updated every 12 hours (instead of * 24 hours in the system) 4) Location updates only uses the {@link * LocationManager#PASSIVE_PROVIDER} to avoid active use of the GPS 5) If a location is successfully * obtained and geocoded, we never fall back to use of the SIM's country (for the system, the * fallback never happens without a reboot) 6) Location is not used if the device does not implement * a {@link android.location.Geocoder} */ public class CountryDetector { private static final String KEY_PREFERENCE_TIME_UPDATED = "preference_time_updated"; static final String KEY_PREFERENCE_CURRENT_COUNTRY = "preference_current_country"; // Wait 12 hours between updates private static final long TIME_BETWEEN_UPDATES_MS = 1000L * 60 * 60 * 12; // Minimum distance before an update is triggered, in meters. We don't need this to be too // exact because all we care about is what country the user is in. private static final long DISTANCE_BETWEEN_UPDATES_METERS = 5000; // Used as a default country code when all the sources of country data have failed in the // exceedingly rare event that the device does not have a default locale set for some reason. private static final String DEFAULT_COUNTRY_ISO = "US"; @VisibleForTesting public static CountryDetector instance; private final TelephonyManager telephonyManager; private final LocaleProvider localeProvider; private final Geocoder geocoder; private final Context appContext; @VisibleForTesting public CountryDetector( Context appContext, TelephonyManager telephonyManager, LocationManager locationManager, LocaleProvider localeProvider, Geocoder geocoder) { this.telephonyManager = telephonyManager; this.localeProvider = localeProvider; this.appContext = appContext; this.geocoder = geocoder; // If the device does not implement Geocoder there is no point trying to get location updates // because we cannot retrieve the country based on the location anyway. if (Geocoder.isPresent()) { registerForLocationUpdates(appContext, locationManager); } } @SuppressWarnings("missingPermission") private static void registerForLocationUpdates(Context context, LocationManager locationManager) { if (!hasLocationPermissions(context)) { LogUtil.w( "CountryDetector.registerForLocationUpdates", "no location permissions, not registering for location updates"); return; } LogUtil.i("CountryDetector.registerForLocationUpdates", "registering for location updates"); final Intent activeIntent = new Intent(context, LocationChangedReceiver.class); final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, activeIntent, PendingIntent.FLAG_UPDATE_CURRENT); locationManager.requestLocationUpdates( LocationManager.PASSIVE_PROVIDER, TIME_BETWEEN_UPDATES_MS, DISTANCE_BETWEEN_UPDATES_METERS, pendingIntent); } /** @return the single instance of the {@link CountryDetector} */ public static synchronized CountryDetector getInstance(Context context) { if (instance == null) { Context appContext = context.getApplicationContext(); instance = new CountryDetector( appContext, (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE), (LocationManager) context.getSystemService(Context.LOCATION_SERVICE), Locale::getDefault, new Geocoder(appContext)); } return instance; } public String getCurrentCountryIso() { String result = null; if (isNetworkCountryCodeAvailable()) { result = getNetworkBasedCountryIso(); } if (TextUtils.isEmpty(result)) { result = getLocationBasedCountryIso(); } if (TextUtils.isEmpty(result)) { result = getSimBasedCountryIso(); } if (TextUtils.isEmpty(result)) { result = getLocaleBasedCountryIso(); } if (TextUtils.isEmpty(result)) { result = DEFAULT_COUNTRY_ISO; } return result.toUpperCase(Locale.US); } /** @return the country code of the current telephony network the user is connected to. */ private String getNetworkBasedCountryIso() { return telephonyManager.getNetworkCountryIso(); } /** @return the geocoded country code detected by the {@link LocationManager}. */ @Nullable private String getLocationBasedCountryIso() { if (!Geocoder.isPresent() || !hasLocationPermissions(appContext) || !UserManagerCompat.isUserUnlocked(appContext)) { return null; } return PreferenceManager.getDefaultSharedPreferences(appContext) .getString(KEY_PREFERENCE_CURRENT_COUNTRY, null); } /** @return the country code of the SIM card currently inserted in the device. */ private String getSimBasedCountryIso() { return telephonyManager.getSimCountryIso(); } /** @return the country code of the user's currently selected locale. */ private String getLocaleBasedCountryIso() { Locale defaultLocale = localeProvider.getLocale(); if (defaultLocale != null) { return defaultLocale.getCountry(); } return null; } private boolean isNetworkCountryCodeAvailable() { // On CDMA TelephonyManager.getNetworkCountryIso() just returns the SIM's country code. // In this case, we want to ignore the value returned and fallback to location instead. return telephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM; } /** Interface for accessing the current locale. */ public interface LocaleProvider { Locale getLocale(); } public static class LocationChangedReceiver extends BroadcastReceiver { @Override public void onReceive(final Context context, Intent intent) { if (!intent.hasExtra(LocationManager.KEY_LOCATION_CHANGED)) { return; } final Location location = (Location) intent.getExtras().get(LocationManager.KEY_LOCATION_CHANGED); // TODO: rething how we access the gecoder here, right now we have to set the static instance // of CountryDetector to make this work for tests which is weird // (see CountryDetectorTest.locationChangedBroadcast_GeocodesLocation) processLocationUpdate(context, CountryDetector.getInstance(context).geocoder, location); } } private static void processLocationUpdate( Context appContext, Geocoder geocoder, Location location) { DialerExecutorComponent.get(appContext) .dialerExecutorFactory() .createNonUiTaskBuilder(new GeocodeCountryWorker(geocoder)) .onSuccess( country -> { if (country == null) { return; } PreferenceManager.getDefaultSharedPreferences(appContext) .edit() .putLong(CountryDetector.KEY_PREFERENCE_TIME_UPDATED, System.currentTimeMillis()) .putString(CountryDetector.KEY_PREFERENCE_CURRENT_COUNTRY, country) .apply(); }) .onFailure( throwable -> LogUtil.w( "CountryDetector.processLocationUpdate", "exception occurred when getting geocoded country from location", throwable)) .build() .executeParallel(location); } /** Worker that given a {@link Location} returns an ISO 3166-1 two letter country code. */ private static class GeocodeCountryWorker implements Worker { @NonNull private final Geocoder geocoder; GeocodeCountryWorker(@NonNull Geocoder geocoder) { this.geocoder = Assert.isNotNull(geocoder); } /** @return the ISO 3166-1 two letter country code if geocoded, else null */ @Nullable @Override public String doInBackground(@Nullable Location location) throws Throwable { if (location == null) { return null; } List
addresses = geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1); if (addresses != null && !addresses.isEmpty()) { return addresses.get(0).getCountryCode(); } return null; } } private static boolean hasLocationPermissions(Context context) { return context.checkSelfPermission(permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; } }