• 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.contacts.common.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.content.SharedPreferences;
24 import android.location.Geocoder;
25 import android.location.Location;
26 import android.location.LocationManager;
27 import android.preference.PreferenceManager;
28 import android.telephony.TelephonyManager;
29 import android.text.TextUtils;
30 import android.util.Log;
31 import com.android.dialer.util.PermissionsUtil;
32 import java.util.Locale;
33 
34 /**
35  * This class is used to detect the country where the user is. It is a simplified version of the
36  * country detector service in the framework. The sources of country location are queried in the
37  * following order of reliability:
38  *
39  * <ul>
40  * <li>Mobile network
41  * <li>Location manager
42  * <li>SIM's country
43  * <li>User's default locale
44  * </ul>
45  *
46  * As far as possible this class tries to replicate the behavior of the system's country detector
47  * service: 1) Order in priority of sources of country location 2) Mobile network information
48  * provided by CDMA phones is ignored 3) Location information is updated every 12 hours (instead of
49  * 24 hours in the system) 4) Location updates only uses the {@link
50  * LocationManager#PASSIVE_PROVIDER} to avoid active use of the GPS 5) If a location is successfully
51  * obtained and geocoded, we never fall back to use of the SIM's country (for the system, the
52  * fallback never happens without a reboot) 6) Location is not used if the device does not implement
53  * a {@link android.location.Geocoder}
54  */
55 public class CountryDetector {
56 
57   public static final String KEY_PREFERENCE_TIME_UPDATED = "preference_time_updated";
58   public static final String KEY_PREFERENCE_CURRENT_COUNTRY = "preference_current_country";
59   private static final String TAG = "CountryDetector";
60   // Wait 12 hours between updates
61   private static final long TIME_BETWEEN_UPDATES_MS = 1000L * 60 * 60 * 12;
62   // Minimum distance before an update is triggered, in meters. We don't need this to be too
63   // exact because all we care about is what country the user is in.
64   private static final long DISTANCE_BETWEEN_UPDATES_METERS = 5000;
65   private static CountryDetector sInstance;
66   private final TelephonyManager mTelephonyManager;
67   private final LocationManager mLocationManager;
68   private final LocaleProvider mLocaleProvider;
69   // Used as a default country code when all the sources of country data have failed in the
70   // exceedingly rare event that the device does not have a default locale set for some reason.
71   private static final String DEFAULT_COUNTRY_ISO = "US";
72   private final Context mContext;
73 
CountryDetector(Context context)74   private CountryDetector(Context context) {
75     this(
76         context,
77         (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE),
78         (LocationManager) context.getSystemService(Context.LOCATION_SERVICE),
79         new LocaleProvider());
80   }
81 
CountryDetector( Context context, TelephonyManager telephonyManager, LocationManager locationManager, LocaleProvider localeProvider)82   private CountryDetector(
83       Context context,
84       TelephonyManager telephonyManager,
85       LocationManager locationManager,
86       LocaleProvider localeProvider) {
87     mTelephonyManager = telephonyManager;
88     mLocationManager = locationManager;
89     mLocaleProvider = localeProvider;
90     mContext = context;
91 
92     registerForLocationUpdates(context, mLocationManager);
93   }
94 
registerForLocationUpdates(Context context, LocationManager locationManager)95   public static void registerForLocationUpdates(Context context, LocationManager locationManager) {
96     if (!PermissionsUtil.hasLocationPermissions(context)) {
97       Log.w(TAG, "No location permissions, not registering for location updates.");
98       return;
99     }
100 
101     if (!Geocoder.isPresent()) {
102       // Certain devices do not have an implementation of a geocoder - in that case there is
103       // no point trying to get location updates because we cannot retrieve the country based
104       // on the location anyway.
105       return;
106     }
107     final Intent activeIntent = new Intent(context, LocationChangedReceiver.class);
108     final PendingIntent pendingIntent =
109         PendingIntent.getBroadcast(context, 0, activeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
110 
111     locationManager.requestLocationUpdates(
112         LocationManager.PASSIVE_PROVIDER,
113         TIME_BETWEEN_UPDATES_MS,
114         DISTANCE_BETWEEN_UPDATES_METERS,
115         pendingIntent);
116   }
117 
118   /**
119    * Returns the instance of the country detector. {@link #initialize(Context)} must have been
120    * called previously.
121    *
122    * @return the initialized country detector.
123    */
getInstance(Context context)124   public static synchronized CountryDetector getInstance(Context context) {
125     if (sInstance == null) {
126       sInstance = new CountryDetector(context.getApplicationContext());
127     }
128     return sInstance;
129   }
130 
131   /** Factory method for {@link CountryDetector} that allows the caller to provide mock objects. */
getInstanceForTest( Context context, TelephonyManager telephonyManager, LocationManager locationManager, LocaleProvider localeProvider, Geocoder geocoder)132   public CountryDetector getInstanceForTest(
133       Context context,
134       TelephonyManager telephonyManager,
135       LocationManager locationManager,
136       LocaleProvider localeProvider,
137       Geocoder geocoder) {
138     return new CountryDetector(context, telephonyManager, locationManager, localeProvider);
139   }
140 
getCurrentCountryIso()141   public String getCurrentCountryIso() {
142     String result = null;
143     if (isNetworkCountryCodeAvailable()) {
144       result = getNetworkBasedCountryIso();
145     }
146     if (TextUtils.isEmpty(result)) {
147       result = getLocationBasedCountryIso();
148     }
149     if (TextUtils.isEmpty(result)) {
150       result = getSimBasedCountryIso();
151     }
152     if (TextUtils.isEmpty(result)) {
153       result = getLocaleBasedCountryIso();
154     }
155     if (TextUtils.isEmpty(result)) {
156       result = DEFAULT_COUNTRY_ISO;
157     }
158     return result.toUpperCase(Locale.US);
159   }
160 
161   /** @return the country code of the current telephony network the user is connected to. */
getNetworkBasedCountryIso()162   private String getNetworkBasedCountryIso() {
163     return mTelephonyManager.getNetworkCountryIso();
164   }
165 
166   /** @return the geocoded country code detected by the {@link LocationManager}. */
getLocationBasedCountryIso()167   private String getLocationBasedCountryIso() {
168     if (!Geocoder.isPresent() || !PermissionsUtil.hasLocationPermissions(mContext)) {
169       return null;
170     }
171     final SharedPreferences sharedPreferences =
172         PreferenceManager.getDefaultSharedPreferences(mContext);
173     return sharedPreferences.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 mTelephonyManager.getSimCountryIso();
179   }
180 
181   /** @return the country code of the user's currently selected locale. */
getLocaleBasedCountryIso()182   private String getLocaleBasedCountryIso() {
183     Locale defaultLocale = mLocaleProvider.getDefaultLocale();
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 mTelephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM;
194   }
195 
196   /**
197    * Class that can be used to return the user's default locale. This is in its own class so that it
198    * can be mocked out.
199    */
200   public static class LocaleProvider {
201 
getDefaultLocale()202     public Locale getDefaultLocale() {
203       return Locale.getDefault();
204     }
205   }
206 
207   public static class LocationChangedReceiver extends BroadcastReceiver {
208 
209     @Override
onReceive(final Context context, Intent intent)210     public void onReceive(final Context context, Intent intent) {
211       if (!intent.hasExtra(LocationManager.KEY_LOCATION_CHANGED)) {
212         return;
213       }
214 
215       final Location location =
216           (Location) intent.getExtras().get(LocationManager.KEY_LOCATION_CHANGED);
217 
218       UpdateCountryService.updateCountry(context, location);
219     }
220   }
221 }
222