diff options
author | Yorke Lee <yorkelee@google.com> | 2014-06-13 16:54:50 -0700 |
---|---|---|
committer | Yorke Lee <yorkelee@google.com> | 2014-07-07 12:21:48 -0700 |
commit | f5366543ba27cc1102479b918607f04f52071be7 (patch) | |
tree | 750b0268226db5ad0e90d8496d3f76d7983be772 | |
parent | 4ab0cc8065d7080d8ea899814b0efd98908cb1b3 (diff) | |
download | packages_apps_ContactsCommon-f5366543ba27cc1102479b918607f04f52071be7.tar.gz packages_apps_ContactsCommon-f5366543ba27cc1102479b918607f04f52071be7.tar.bz2 packages_apps_ContactsCommon-f5366543ba27cc1102479b918607f04f52071be7.zip |
Add in-app CountryDetector (1/4)
Replace use of the hidden CountryDetector system service with one that
runs in-app.
Instead of using a LocationManager listener to listen to location updates,
the in-app CountryDetector uses pending intents to passively listen to
any location updates.
This change also introduces an IntentService that is used to simplify the
asynchronous geocoding of the location that is provided to the CountryDetector's
BroadcastReceiver by the LocationManager
Bug: 15593973
Change-Id: I2915aad176963a04c7c577addb659984f0db56f2
3 files changed, 295 insertions, 12 deletions
diff --git a/src/com/android/contacts/common/GeoUtil.java b/src/com/android/contacts/common/GeoUtil.java index 6c41d24a..cd0139b7 100644 --- a/src/com/android/contacts/common/GeoUtil.java +++ b/src/com/android/contacts/common/GeoUtil.java @@ -16,9 +16,10 @@ package com.android.contacts.common; +import android.app.Application; import android.content.Context; -import android.location.Country; -import android.location.CountryDetector; + +import com.android.contacts.common.location.CountryDetector; import com.google.i18n.phonenumbers.geocoding.PhoneNumberOfflineGeocoder; import com.google.i18n.phonenumbers.NumberParseException; @@ -33,20 +34,15 @@ import java.util.Locale; public class GeoUtil { /** + * Returns the country code of the country the user is currently in. Before calling this + * method, make sure that {@link CountryDetector#initialize(Context)} has already been called + * in {@link Application#onCreate()}. * @return The ISO 3166-1 two letters country code of the country the user * is in. */ public static String getCurrentCountryIso(Context context) { - final CountryDetector detector = - (CountryDetector) context.getSystemService(Context.COUNTRY_DETECTOR); - if (detector != null) { - final Country country = detector.detectCountry(); - if (country != null) { - return country.getCountryIso(); - } - } - // Fallback to Locale if have issues with CountryDetector - return Locale.getDefault().getCountry(); + // The {@link CountryDetector} should never return null so this is safe to return as-is. + return CountryDetector.getInstance(context).getCurrentCountryIso(); } public static String getGeocodedLocationFor(Context context, String phoneNumber) { diff --git a/src/com/android/contacts/common/location/CountryDetector.java b/src/com/android/contacts/common/location/CountryDetector.java new file mode 100644 index 00000000..7ad57d2e --- /dev/null +++ b/src/com/android/contacts/common/location/CountryDetector.java @@ -0,0 +1,208 @@ +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 com.android.contacts.common.testing.NeededForTesting; + +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> + * <li>Location manager</li> + * <li>SIM's country</li> + * <li>User's default locale</li> + * </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 TAG = "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 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 final String DEFAULT_COUNTRY_ISO = "US"; + + // 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 final Context mContext; + + /** + * 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(); + } + } + + 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 (!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); + } + + /** + * Factory method for {@link CountryDetector} that allows the caller to provide mock objects. + */ + @NeededForTesting + public CountryDetector getInstanceForTest(Context context, TelephonyManager telephonyManager, + LocationManager locationManager, LocaleProvider localeProvider, Geocoder geocoder) { + return new CountryDetector(context, telephonyManager, locationManager, localeProvider); + } + + /** + * Returns the instance of the country detector. {@link #initialize(Context)} must have been + * called previously. + * + * @return the initialized country detector. + */ + public synchronized static CountryDetector getInstance(Context context) { + if (sInstance == null) { + sInstance = new CountryDetector(context.getApplicationContext()); + } + return sInstance; + } + + 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()) { + 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; + } + + 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); + } + } + +} diff --git a/src/com/android/contacts/common/location/UpdateCountryService.java b/src/com/android/contacts/common/location/UpdateCountryService.java new file mode 100644 index 00000000..e339306f --- /dev/null +++ b/src/com/android/contacts/common/location/UpdateCountryService.java @@ -0,0 +1,79 @@ +package com.android.contacts.common.location; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.location.Address; +import android.location.Geocoder; +import android.location.Location; +import android.preference.PreferenceManager; +import android.util.Log; + +import java.io.IOException; +import java.util.List; + +/** + * Service used to perform asynchronous geocoding from within a broadcast receiver. Given a + * {@link Location}, convert it into a country code, and save it in shared preferences. + */ +public class UpdateCountryService extends IntentService { + private static final String TAG = UpdateCountryService.class.getSimpleName(); + + private static final String ACTION_UPDATE_COUNTRY = "saveCountry"; + + private static final String KEY_INTENT_LOCATION = "location"; + + public UpdateCountryService() { + super(TAG); + } + + public static void updateCountry(Context context, Location location) { + final Intent serviceIntent = new Intent(context, UpdateCountryService.class); + serviceIntent.setAction(ACTION_UPDATE_COUNTRY); + serviceIntent.putExtra(UpdateCountryService.KEY_INTENT_LOCATION, location); + context.startService(serviceIntent); + } + + @Override + protected void onHandleIntent(Intent intent) { + if (ACTION_UPDATE_COUNTRY.equals(intent.getAction())) { + final Location location = (Location) intent.getParcelableExtra(KEY_INTENT_LOCATION); + final String country = getCountryFromLocation(getApplicationContext(), location); + + if (country == null) { + return; + } + + final SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + + final Editor editor = prefs.edit(); + editor.putLong(CountryDetector.KEY_PREFERENCE_TIME_UPDATED, + System.currentTimeMillis()); + editor.putString(CountryDetector.KEY_PREFERENCE_CURRENT_COUNTRY, country); + editor.commit(); + } + } + + /** + * Given a {@link Location}, return a country code. + * + * @return the ISO 3166-1 two letter country code + */ + private String getCountryFromLocation(Context context, Location location) { + final Geocoder geocoder = new Geocoder(context); + String country = null; + try { + final List<Address> addresses = geocoder.getFromLocation( + location.getLatitude(), location.getLongitude(), 1); + if (addresses != null && addresses.size() > 0) { + country = addresses.get(0).getCountryCode(); + } + } catch (IOException e) { + Log.w(TAG, "Exception occurred when getting geocoded country from location"); + } + return country; + } +} |