• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.DialerExecutors;
37 import com.android.dialer.common.concurrent.DialerExecutor.Worker;
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
CountryDetector( Context appContext, TelephonyManager telephonyManager, LocationManager locationManager, LocaleProvider localeProvider, Geocoder geocoder)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 
registerForLocationUpdates(Context context, LocationManager locationManager)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} */
getInstance(Context context)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 
getCurrentCountryIso()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. */
getNetworkBasedCountryIso()158   private String getNetworkBasedCountryIso() {
159     return telephonyManager.getNetworkCountryIso();
160   }
161 
162   /** @return the geocoded country code detected by the {@link LocationManager}. */
163   @Nullable
getLocationBasedCountryIso()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. */
getSimBasedCountryIso()175   private String getSimBasedCountryIso() {
176     return telephonyManager.getSimCountryIso();
177   }
178 
179   /** @return the country code of the user's currently selected locale. */
getLocaleBasedCountryIso()180   private String getLocaleBasedCountryIso() {
181     Locale defaultLocale = localeProvider.getLocale();
182     if (defaultLocale != null) {
183       return defaultLocale.getCountry();
184     }
185     return null;
186   }
187 
isNetworkCountryCodeAvailable()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 {
getLocale()196     Locale getLocale();
197   }
198 
199   public static class LocationChangedReceiver extends BroadcastReceiver {
200 
201     @Override
onReceive(final Context context, Intent intent)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 
processLocationUpdate( Context appContext, Geocoder geocoder, Location location)217   private static void processLocationUpdate(
218       Context appContext, Geocoder geocoder, Location location) {
219     DialerExecutors.createNonUiTaskBuilder(new GeocodeCountryWorker(geocoder))
220         .onSuccess(
221             country -> {
222               if (country == null) {
223                 return;
224               }
225 
226               PreferenceManager.getDefaultSharedPreferences(appContext)
227                   .edit()
228                   .putLong(CountryDetector.KEY_PREFERENCE_TIME_UPDATED, System.currentTimeMillis())
229                   .putString(CountryDetector.KEY_PREFERENCE_CURRENT_COUNTRY, country)
230                   .apply();
231             })
232         .onFailure(
233             throwable ->
234                 LogUtil.w(
235                     "CountryDetector.processLocationUpdate",
236                     "exception occurred when getting geocoded country from location",
237                     throwable))
238         .build()
239         .executeParallel(location);
240   }
241 
242   /** Worker that given a {@link Location} returns an ISO 3166-1 two letter country code. */
243   private static class GeocodeCountryWorker implements Worker<Location, String> {
244     @NonNull private final Geocoder geocoder;
245 
GeocodeCountryWorker(@onNull Geocoder geocoder)246     GeocodeCountryWorker(@NonNull Geocoder geocoder) {
247       this.geocoder = Assert.isNotNull(geocoder);
248     }
249 
250     /** @return the ISO 3166-1 two letter country code if geocoded, else null */
251     @Nullable
252     @Override
doInBackground(@ullable Location location)253     public String doInBackground(@Nullable Location location) throws Throwable {
254       if (location == null) {
255         return null;
256       }
257 
258       List<Address> addresses =
259           geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1);
260       if (addresses != null && !addresses.isEmpty()) {
261         return addresses.get(0).getCountryCode();
262       }
263       return null;
264     }
265   }
266 }
267