276 lines
10 KiB
Java
276 lines
10 KiB
Java
|
/*
|
||
|
* 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.dialer.location;
|
||
|
|
||
|
import android.Manifest.permission;
|
||
|
import android.app.PendingIntent;
|
||
|
import android.content.BroadcastReceiver;
|
||
|
import android.content.Context;
|
||
|
import android.content.Intent;
|
||
|
import android.content.pm.PackageManager;
|
||
|
import android.location.Address;
|
||
|
import android.location.Geocoder;
|
||
|
import android.location.Location;
|
||
|
import android.location.LocationManager;
|
||
|
import android.preference.PreferenceManager;
|
||
|
import android.support.annotation.NonNull;
|
||
|
import android.support.annotation.Nullable;
|
||
|
import android.support.annotation.VisibleForTesting;
|
||
|
import android.support.v4.os.UserManagerCompat;
|
||
|
import android.telephony.TelephonyManager;
|
||
|
import android.text.TextUtils;
|
||
|
import com.android.dialer.common.Assert;
|
||
|
import com.android.dialer.common.LogUtil;
|
||
|
import com.android.dialer.common.concurrent.DialerExecutor.Worker;
|
||
|
import com.android.dialer.common.concurrent.DialerExecutorComponent;
|
||
|
import java.util.List;
|
||
|
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 {
|
||
|
private static final String KEY_PREFERENCE_TIME_UPDATED = "preference_time_updated";
|
||
|
static final String KEY_PREFERENCE_CURRENT_COUNTRY = "preference_current_country";
|
||
|
// 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;
|
||
|
// 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";
|
||
|
|
||
|
@VisibleForTesting public static CountryDetector instance;
|
||
|
|
||
|
private final TelephonyManager telephonyManager;
|
||
|
private final LocaleProvider localeProvider;
|
||
|
private final Geocoder geocoder;
|
||
|
private final Context appContext;
|
||
|
|
||
|
@VisibleForTesting
|
||
|
public CountryDetector(
|
||
|
Context appContext,
|
||
|
TelephonyManager telephonyManager,
|
||
|
LocationManager locationManager,
|
||
|
LocaleProvider localeProvider,
|
||
|
Geocoder geocoder) {
|
||
|
this.telephonyManager = telephonyManager;
|
||
|
this.localeProvider = localeProvider;
|
||
|
this.appContext = appContext;
|
||
|
this.geocoder = geocoder;
|
||
|
|
||
|
// If the device does not implement Geocoder there is no point trying to get location updates
|
||
|
// because we cannot retrieve the country based on the location anyway.
|
||
|
if (Geocoder.isPresent()) {
|
||
|
registerForLocationUpdates(appContext, locationManager);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@SuppressWarnings("missingPermission")
|
||
|
private static void registerForLocationUpdates(Context context, LocationManager locationManager) {
|
||
|
if (!hasLocationPermissions(context)) {
|
||
|
LogUtil.w(
|
||
|
"CountryDetector.registerForLocationUpdates",
|
||
|
"no location permissions, not registering for location updates");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
LogUtil.i("CountryDetector.registerForLocationUpdates", "registering for location updates");
|
||
|
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
/** @return the single instance of the {@link CountryDetector} */
|
||
|
public static synchronized CountryDetector getInstance(Context context) {
|
||
|
if (instance == null) {
|
||
|
Context appContext = context.getApplicationContext();
|
||
|
instance =
|
||
|
new CountryDetector(
|
||
|
appContext,
|
||
|
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE),
|
||
|
(LocationManager) context.getSystemService(Context.LOCATION_SERVICE),
|
||
|
Locale::getDefault,
|
||
|
new Geocoder(appContext));
|
||
|
}
|
||
|
return instance;
|
||
|
}
|
||
|
|
||
|
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 telephonyManager.getNetworkCountryIso();
|
||
|
}
|
||
|
|
||
|
/** @return the geocoded country code detected by the {@link LocationManager}. */
|
||
|
@Nullable
|
||
|
private String getLocationBasedCountryIso() {
|
||
|
if (!Geocoder.isPresent()
|
||
|
|| !hasLocationPermissions(appContext)
|
||
|
|| !UserManagerCompat.isUserUnlocked(appContext)) {
|
||
|
return null;
|
||
|
}
|
||
|
return PreferenceManager.getDefaultSharedPreferences(appContext)
|
||
|
.getString(KEY_PREFERENCE_CURRENT_COUNTRY, null);
|
||
|
}
|
||
|
|
||
|
/** @return the country code of the SIM card currently inserted in the device. */
|
||
|
private String getSimBasedCountryIso() {
|
||
|
return telephonyManager.getSimCountryIso();
|
||
|
}
|
||
|
|
||
|
/** @return the country code of the user's currently selected locale. */
|
||
|
private String getLocaleBasedCountryIso() {
|
||
|
Locale defaultLocale = localeProvider.getLocale();
|
||
|
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 telephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM;
|
||
|
}
|
||
|
|
||
|
/** Interface for accessing the current locale. */
|
||
|
public interface LocaleProvider {
|
||
|
Locale getLocale();
|
||
|
}
|
||
|
|
||
|
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);
|
||
|
|
||
|
// TODO: rething how we access the gecoder here, right now we have to set the static instance
|
||
|
// of CountryDetector to make this work for tests which is weird
|
||
|
// (see CountryDetectorTest.locationChangedBroadcast_GeocodesLocation)
|
||
|
processLocationUpdate(context, CountryDetector.getInstance(context).geocoder, location);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static void processLocationUpdate(
|
||
|
Context appContext, Geocoder geocoder, Location location) {
|
||
|
DialerExecutorComponent.get(appContext)
|
||
|
.dialerExecutorFactory()
|
||
|
.createNonUiTaskBuilder(new GeocodeCountryWorker(geocoder))
|
||
|
.onSuccess(
|
||
|
country -> {
|
||
|
if (country == null) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
PreferenceManager.getDefaultSharedPreferences(appContext)
|
||
|
.edit()
|
||
|
.putLong(CountryDetector.KEY_PREFERENCE_TIME_UPDATED, System.currentTimeMillis())
|
||
|
.putString(CountryDetector.KEY_PREFERENCE_CURRENT_COUNTRY, country)
|
||
|
.apply();
|
||
|
})
|
||
|
.onFailure(
|
||
|
throwable ->
|
||
|
LogUtil.w(
|
||
|
"CountryDetector.processLocationUpdate",
|
||
|
"exception occurred when getting geocoded country from location",
|
||
|
throwable))
|
||
|
.build()
|
||
|
.executeParallel(location);
|
||
|
}
|
||
|
|
||
|
/** Worker that given a {@link Location} returns an ISO 3166-1 two letter country code. */
|
||
|
private static class GeocodeCountryWorker implements Worker<Location, String> {
|
||
|
@NonNull private final Geocoder geocoder;
|
||
|
|
||
|
GeocodeCountryWorker(@NonNull Geocoder geocoder) {
|
||
|
this.geocoder = Assert.isNotNull(geocoder);
|
||
|
}
|
||
|
|
||
|
/** @return the ISO 3166-1 two letter country code if geocoded, else null */
|
||
|
@Nullable
|
||
|
@Override
|
||
|
public String doInBackground(@Nullable Location location) throws Throwable {
|
||
|
if (location == null) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
List<Address> addresses =
|
||
|
geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1);
|
||
|
if (addresses != null && !addresses.isEmpty()) {
|
||
|
return addresses.get(0).getCountryCode();
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static boolean hasLocationPermissions(Context context) {
|
||
|
return context.checkSelfPermission(permission.ACCESS_FINE_LOCATION)
|
||
|
== PackageManager.PERMISSION_GRANTED;
|
||
|
}
|
||
|
}
|