diff options
Diffstat (limited to 'services/core/java/com/android/server/location/ComprehensiveCountryDetector.java')
-rw-r--r-- | services/core/java/com/android/server/location/ComprehensiveCountryDetector.java | 492 |
1 files changed, 492 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/location/ComprehensiveCountryDetector.java b/services/core/java/com/android/server/location/ComprehensiveCountryDetector.java new file mode 100644 index 0000000..354858b --- /dev/null +++ b/services/core/java/com/android/server/location/ComprehensiveCountryDetector.java @@ -0,0 +1,492 @@ +/* + * Copyright (C) 2010 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.server.location; + +import android.content.Context; +import android.location.Country; +import android.location.CountryListener; +import android.location.Geocoder; +import android.os.SystemClock; +import android.provider.Settings; +import android.telephony.PhoneStateListener; +import android.telephony.ServiceState; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; +import android.util.Slog; + +import java.util.Locale; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * This class is used to detect the country where the user is. The sources of + * country are queried in order of reliability, like + * <ul> + * <li>Mobile network</li> + * <li>Location</li> + * <li>SIM's country</li> + * <li>Phone's locale</li> + * </ul> + * <p> + * Call the {@link #detectCountry()} to get the available country immediately. + * <p> + * To be notified of the future country change, using the + * {@link #setCountryListener(CountryListener)} + * <p> + * Using the {@link #stop()} to stop listening to the country change. + * <p> + * The country information will be refreshed every + * {@link #LOCATION_REFRESH_INTERVAL} once the location based country is used. + * + * @hide + */ +public class ComprehensiveCountryDetector extends CountryDetectorBase { + + private final static String TAG = "CountryDetector"; + /* package */ static final boolean DEBUG = false; + + /** + * Max length of logs to maintain for debugging. + */ + private static final int MAX_LENGTH_DEBUG_LOGS = 20; + + /** + * The refresh interval when the location based country was used + */ + private final static long LOCATION_REFRESH_INTERVAL = 1000 * 60 * 60 * 24; // 1 day + + protected CountryDetectorBase mLocationBasedCountryDetector; + protected Timer mLocationRefreshTimer; + + private Country mCountry; + private final TelephonyManager mTelephonyManager; + private Country mCountryFromLocation; + private boolean mStopped = false; + + private PhoneStateListener mPhoneStateListener; + + /** + * List of the most recent country state changes for debugging. This should have + * a max length of MAX_LENGTH_LOGS. + */ + private final ConcurrentLinkedQueue<Country> mDebugLogs = new ConcurrentLinkedQueue<Country>(); + + /** + * Most recent {@link Country} result that was added to the debug logs {@link #mDebugLogs}. + * We keep track of this value to help prevent adding many of the same {@link Country} objects + * to the logs. + */ + private Country mLastCountryAddedToLogs; + + /** + * Object used to synchronize access to {@link #mLastCountryAddedToLogs}. Be careful if + * using it to synchronize anything else in this file. + */ + private final Object mObject = new Object(); + + /** + * Start time of the current session for which the detector has been active. + */ + private long mStartTime; + + /** + * Stop time of the most recent session for which the detector was active. + */ + private long mStopTime; + + /** + * The sum of all the time intervals in which the detector was active. + */ + private long mTotalTime; + + /** + * Number of {@link PhoneStateListener#onServiceStateChanged(ServiceState state)} events that + * have occurred for the current session for which the detector has been active. + */ + private int mCountServiceStateChanges; + + /** + * Total number of {@link PhoneStateListener#onServiceStateChanged(ServiceState state)} events + * that have occurred for all time intervals in which the detector has been active. + */ + private int mTotalCountServiceStateChanges; + + /** + * The listener for receiving the notification from LocationBasedCountryDetector. + */ + private CountryListener mLocationBasedCountryDetectionListener = new CountryListener() { + @Override + public void onCountryDetected(Country country) { + if (DEBUG) Slog.d(TAG, "Country detected via LocationBasedCountryDetector"); + mCountryFromLocation = country; + // Don't start the LocationBasedCountryDetector. + detectCountry(true, false); + stopLocationBasedDetector(); + } + }; + + public ComprehensiveCountryDetector(Context context) { + super(context); + mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + } + + @Override + public Country detectCountry() { + // Don't start the LocationBasedCountryDetector if we have been stopped. + return detectCountry(false, !mStopped); + } + + @Override + public void stop() { + // Note: this method in this subclass called only by tests. + Slog.i(TAG, "Stop the detector."); + cancelLocationRefresh(); + removePhoneStateListener(); + stopLocationBasedDetector(); + mListener = null; + mStopped = true; + } + + /** + * Get the country from different sources in order of the reliability. + */ + private Country getCountry() { + Country result = null; + result = getNetworkBasedCountry(); + if (result == null) { + result = getLastKnownLocationBasedCountry(); + } + if (result == null) { + result = getSimBasedCountry(); + } + if (result == null) { + result = getLocaleCountry(); + } + addToLogs(result); + return result; + } + + /** + * Attempt to add this {@link Country} to the debug logs. + */ + private void addToLogs(Country country) { + if (country == null) { + return; + } + // If the country (ISO and source) are the same as before, then there is no + // need to add this country as another entry in the logs. Synchronize access to this + // variable since multiple threads could be calling this method. + synchronized (mObject) { + if (mLastCountryAddedToLogs != null && mLastCountryAddedToLogs.equals(country)) { + return; + } + mLastCountryAddedToLogs = country; + } + // Manually maintain a max limit for the list of logs + if (mDebugLogs.size() >= MAX_LENGTH_DEBUG_LOGS) { + mDebugLogs.poll(); + } + if (DEBUG) { + Slog.d(TAG, country.toString()); + } + mDebugLogs.add(country); + } + + private boolean isNetworkCountryCodeAvailable() { + // On CDMA TelephonyManager.getNetworkCountryIso() just returns SIM country. We don't want + // to prioritize it over location based country, so ignore it. + final int phoneType = mTelephonyManager.getPhoneType(); + if (DEBUG) Slog.v(TAG, " phonetype=" + phoneType); + return phoneType == TelephonyManager.PHONE_TYPE_GSM; + } + + /** + * @return the country from the mobile network. + */ + protected Country getNetworkBasedCountry() { + String countryIso = null; + if (isNetworkCountryCodeAvailable()) { + countryIso = mTelephonyManager.getNetworkCountryIso(); + if (!TextUtils.isEmpty(countryIso)) { + return new Country(countryIso, Country.COUNTRY_SOURCE_NETWORK); + } + } + return null; + } + + /** + * @return the cached location based country. + */ + protected Country getLastKnownLocationBasedCountry() { + return mCountryFromLocation; + } + + /** + * @return the country from SIM card + */ + protected Country getSimBasedCountry() { + String countryIso = null; + countryIso = mTelephonyManager.getSimCountryIso(); + if (!TextUtils.isEmpty(countryIso)) { + return new Country(countryIso, Country.COUNTRY_SOURCE_SIM); + } + return null; + } + + /** + * @return the country from the system's locale. + */ + protected Country getLocaleCountry() { + Locale defaultLocale = Locale.getDefault(); + if (defaultLocale != null) { + return new Country(defaultLocale.getCountry(), Country.COUNTRY_SOURCE_LOCALE); + } else { + return null; + } + } + + /** + * @param notifyChange indicates whether the listener should be notified the change of the + * country + * @param startLocationBasedDetection indicates whether the LocationBasedCountryDetector could + * be started if the current country source is less reliable than the location. + * @return the current available UserCountry + */ + private Country detectCountry(boolean notifyChange, boolean startLocationBasedDetection) { + Country country = getCountry(); + runAfterDetectionAsync(mCountry != null ? new Country(mCountry) : mCountry, country, + notifyChange, startLocationBasedDetection); + mCountry = country; + return mCountry; + } + + /** + * Run the tasks in the service's thread. + */ + protected void runAfterDetectionAsync(final Country country, final Country detectedCountry, + final boolean notifyChange, final boolean startLocationBasedDetection) { + mHandler.post(new Runnable() { + @Override + public void run() { + runAfterDetection( + country, detectedCountry, notifyChange, startLocationBasedDetection); + } + }); + } + + @Override + public void setCountryListener(CountryListener listener) { + CountryListener prevListener = mListener; + mListener = listener; + if (mListener == null) { + // Stop listening all services + removePhoneStateListener(); + stopLocationBasedDetector(); + cancelLocationRefresh(); + mStopTime = SystemClock.elapsedRealtime(); + mTotalTime += mStopTime; + } else if (prevListener == null) { + addPhoneStateListener(); + detectCountry(false, true); + mStartTime = SystemClock.elapsedRealtime(); + mStopTime = 0; + mCountServiceStateChanges = 0; + } + } + + void runAfterDetection(final Country country, final Country detectedCountry, + final boolean notifyChange, final boolean startLocationBasedDetection) { + if (notifyChange) { + notifyIfCountryChanged(country, detectedCountry); + } + if (DEBUG) { + Slog.d(TAG, "startLocationBasedDetection=" + startLocationBasedDetection + + " detectCountry=" + (detectedCountry == null ? null : + "(source: " + detectedCountry.getSource() + + ", countryISO: " + detectedCountry.getCountryIso() + ")") + + " isAirplaneModeOff()=" + isAirplaneModeOff() + + " mListener=" + mListener + + " isGeoCoderImplemnted()=" + isGeoCoderImplemented()); + } + + if (startLocationBasedDetection && (detectedCountry == null + || detectedCountry.getSource() > Country.COUNTRY_SOURCE_LOCATION) + && isAirplaneModeOff() && mListener != null && isGeoCoderImplemented()) { + if (DEBUG) Slog.d(TAG, "run startLocationBasedDetector()"); + // Start finding location when the source is less reliable than the + // location and the airplane mode is off (as geocoder will not + // work). + // TODO : Shall we give up starting the detector within a + // period of time? + startLocationBasedDetector(mLocationBasedCountryDetectionListener); + } + if (detectedCountry == null + || detectedCountry.getSource() >= Country.COUNTRY_SOURCE_LOCATION) { + // Schedule the location refresh if the country source is + // not more reliable than the location or no country is + // found. + // TODO: Listen to the preference change of GPS, Wifi etc, + // and start detecting the country. + scheduleLocationRefresh(); + } else { + // Cancel the location refresh once the current source is + // more reliable than the location. + cancelLocationRefresh(); + stopLocationBasedDetector(); + } + } + + /** + * Find the country from LocationProvider. + */ + private synchronized void startLocationBasedDetector(CountryListener listener) { + if (mLocationBasedCountryDetector != null) { + return; + } + if (DEBUG) { + Slog.d(TAG, "starts LocationBasedDetector to detect Country code via Location info " + + "(e.g. GPS)"); + } + mLocationBasedCountryDetector = createLocationBasedCountryDetector(); + mLocationBasedCountryDetector.setCountryListener(listener); + mLocationBasedCountryDetector.detectCountry(); + } + + private synchronized void stopLocationBasedDetector() { + if (DEBUG) { + Slog.d(TAG, "tries to stop LocationBasedDetector " + + "(current detector: " + mLocationBasedCountryDetector + ")"); + } + if (mLocationBasedCountryDetector != null) { + mLocationBasedCountryDetector.stop(); + mLocationBasedCountryDetector = null; + } + } + + protected CountryDetectorBase createLocationBasedCountryDetector() { + return new LocationBasedCountryDetector(mContext); + } + + protected boolean isAirplaneModeOff() { + return Settings.Global.getInt( + mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0) == 0; + } + + /** + * Notify the country change. + */ + private void notifyIfCountryChanged(final Country country, final Country detectedCountry) { + if (detectedCountry != null && mListener != null + && (country == null || !country.equals(detectedCountry))) { + if (DEBUG) { + Slog.d(TAG, "" + country + " --> " + detectedCountry); + } + notifyListener(detectedCountry); + } + } + + /** + * Schedule the next location refresh. We will do nothing if the scheduled task exists. + */ + private synchronized void scheduleLocationRefresh() { + if (mLocationRefreshTimer != null) return; + if (DEBUG) { + Slog.d(TAG, "start periodic location refresh timer. Interval: " + + LOCATION_REFRESH_INTERVAL); + } + mLocationRefreshTimer = new Timer(); + mLocationRefreshTimer.schedule(new TimerTask() { + @Override + public void run() { + if (DEBUG) { + Slog.d(TAG, "periodic location refresh event. Starts detecting Country code"); + } + mLocationRefreshTimer = null; + detectCountry(false, true); + } + }, LOCATION_REFRESH_INTERVAL); + } + + /** + * Cancel the scheduled refresh task if it exists + */ + private synchronized void cancelLocationRefresh() { + if (mLocationRefreshTimer != null) { + mLocationRefreshTimer.cancel(); + mLocationRefreshTimer = null; + } + } + + protected synchronized void addPhoneStateListener() { + if (mPhoneStateListener == null) { + mPhoneStateListener = new PhoneStateListener() { + @Override + public void onServiceStateChanged(ServiceState serviceState) { + mCountServiceStateChanges++; + mTotalCountServiceStateChanges++; + + if (!isNetworkCountryCodeAvailable()) { + return; + } + if (DEBUG) Slog.d(TAG, "onServiceStateChanged: " + serviceState.getState()); + + detectCountry(true, true); + } + }; + mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE); + } + } + + protected synchronized void removePhoneStateListener() { + if (mPhoneStateListener != null) { + mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); + mPhoneStateListener = null; + } + } + + protected boolean isGeoCoderImplemented() { + return Geocoder.isPresent(); + } + + @Override + public String toString() { + long currentTime = SystemClock.elapsedRealtime(); + long currentSessionLength = 0; + StringBuilder sb = new StringBuilder(); + sb.append("ComprehensiveCountryDetector{"); + // The detector hasn't stopped yet --> still running + if (mStopTime == 0) { + currentSessionLength = currentTime - mStartTime; + sb.append("timeRunning=" + currentSessionLength + ", "); + } else { + // Otherwise, it has already stopped, so take the last session + sb.append("lastRunTimeLength=" + (mStopTime - mStartTime) + ", "); + } + sb.append("totalCountServiceStateChanges=" + mTotalCountServiceStateChanges + ", "); + sb.append("currentCountServiceStateChanges=" + mCountServiceStateChanges + ", "); + sb.append("totalTime=" + (mTotalTime + currentSessionLength) + ", "); + sb.append("currentTime=" + currentTime + ", "); + sb.append("countries="); + for (Country country : mDebugLogs) { + sb.append("\n " + country.toString()); + } + sb.append("}"); + return sb.toString(); + } +} |