/* * 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.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 * *

* Call the {@link #detectCountry()} to get the available country immediately. *

* To be notified of the future country change, using the * {@link #setCountryListener(CountryListener)} *

* Using the {@link #stop()} to stop listening to the country change. *

* 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 mDebugLogs = new ConcurrentLinkedQueue(); /** * 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(); } }