blob: 7d9e42b38e6b2743099c63504439a04b8ba0cee4 [file] [log] [blame]
/*
* 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.contacts.common.location;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationManager;
import android.preference.PreferenceManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import com.android.dialer.util.PermissionsUtil;
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:
*
* <ul>
* <li>Mobile network
* <li>Location manager
* <li>SIM's country
* <li>User's default locale
* </ul>
*
* 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 {
public static final String KEY_PREFERENCE_TIME_UPDATED = "preference_time_updated";
public static final String KEY_PREFERENCE_CURRENT_COUNTRY = "preference_current_country";
private static final String TAG = "CountryDetector";
// 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;
private static CountryDetector sInstance;
private final TelephonyManager mTelephonyManager;
private final LocationManager mLocationManager;
private final LocaleProvider mLocaleProvider;
// 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";
private final Context mContext;
private CountryDetector(Context context) {
this(
context,
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE),
(LocationManager) context.getSystemService(Context.LOCATION_SERVICE),
new LocaleProvider());
}
private CountryDetector(
Context context,
TelephonyManager telephonyManager,
LocationManager locationManager,
LocaleProvider localeProvider) {
mTelephonyManager = telephonyManager;
mLocationManager = locationManager;
mLocaleProvider = localeProvider;
mContext = context;
registerForLocationUpdates(context, mLocationManager);
}
public static void registerForLocationUpdates(Context context, LocationManager locationManager) {
if (!PermissionsUtil.hasLocationPermissions(context)) {
Log.w(TAG, "No location permissions, not registering for location updates.");
return;
}
if (!Geocoder.isPresent()) {
// Certain devices do not have an implementation of a geocoder - in that case there is
// no point trying to get location updates because we cannot retrieve the country based
// on the location anyway.
return;
}
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);
}
/**
* Returns the instance of the country detector. {@link #initialize(Context)} must have been
* called previously.
*
* @return the initialized country detector.
*/
public static synchronized CountryDetector getInstance(Context context) {
if (sInstance == null) {
sInstance = new CountryDetector(context.getApplicationContext());
}
return sInstance;
}
/** Factory method for {@link CountryDetector} that allows the caller to provide mock objects. */
public CountryDetector getInstanceForTest(
Context context,
TelephonyManager telephonyManager,
LocationManager locationManager,
LocaleProvider localeProvider,
Geocoder geocoder) {
return new CountryDetector(context, telephonyManager, locationManager, localeProvider);
}
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 mTelephonyManager.getNetworkCountryIso();
}
/** @return the geocoded country code detected by the {@link LocationManager}. */
private String getLocationBasedCountryIso() {
if (!Geocoder.isPresent() || !PermissionsUtil.hasLocationPermissions(mContext)) {
return null;
}
final SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(mContext);
return sharedPreferences.getString(KEY_PREFERENCE_CURRENT_COUNTRY, null);
}
/** @return the country code of the SIM card currently inserted in the device. */
private String getSimBasedCountryIso() {
return mTelephonyManager.getSimCountryIso();
}
/** @return the country code of the user's currently selected locale. */
private String getLocaleBasedCountryIso() {
Locale defaultLocale = mLocaleProvider.getDefaultLocale();
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 mTelephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM;
}
/**
* Class that can be used to return the user's default locale. This is in its own class so that it
* can be mocked out.
*/
public static class LocaleProvider {
public Locale getDefaultLocale() {
return Locale.getDefault();
}
}
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);
UpdateCountryService.updateCountry(context, location);
}
}
}