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