Home | History | Annotate | Download | only in location
      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 public static CountryDetector instance;
     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   public 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 (instance == null) {
    125       Context appContext = context.getApplicationContext();
    126       instance =
    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 instance;
    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   public 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