1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.server.location;
18
19import android.content.Context;
20import android.location.Country;
21import android.location.CountryListener;
22import android.location.Geocoder;
23import android.os.SystemClock;
24import android.provider.Settings;
25import android.telephony.PhoneStateListener;
26import android.telephony.ServiceState;
27import android.telephony.TelephonyManager;
28import android.text.TextUtils;
29import android.util.Slog;
30
31import java.util.Locale;
32import java.util.Timer;
33import java.util.TimerTask;
34import java.util.concurrent.ConcurrentLinkedQueue;
35
36/**
37 * This class is used to detect the country where the user is. The sources of
38 * country are queried in order of reliability, like
39 * <ul>
40 * <li>Mobile network</li>
41 * <li>Location</li>
42 * <li>SIM's country</li>
43 * <li>Phone's locale</li>
44 * </ul>
45 * <p>
46 * Call the {@link #detectCountry()} to get the available country immediately.
47 * <p>
48 * To be notified of the future country change, using the
49 * {@link #setCountryListener(CountryListener)}
50 * <p>
51 * Using the {@link #stop()} to stop listening to the country change.
52 * <p>
53 * The country information will be refreshed every
54 * {@link #LOCATION_REFRESH_INTERVAL} once the location based country is used.
55 *
56 * @hide
57 */
58public class ComprehensiveCountryDetector extends CountryDetectorBase {
59
60    private final static String TAG = "CountryDetector";
61    /* package */ static final boolean DEBUG = false;
62
63    /**
64     * Max length of logs to maintain for debugging.
65     */
66    private static final int MAX_LENGTH_DEBUG_LOGS = 20;
67
68    /**
69     * The refresh interval when the location based country was used
70     */
71    private final static long LOCATION_REFRESH_INTERVAL = 1000 * 60 * 60 * 24; // 1 day
72
73    protected CountryDetectorBase mLocationBasedCountryDetector;
74    protected Timer mLocationRefreshTimer;
75
76    private Country mCountry;
77    private final TelephonyManager mTelephonyManager;
78    private Country mCountryFromLocation;
79    private boolean mStopped = false;
80
81    private PhoneStateListener mPhoneStateListener;
82
83    /**
84     * List of the most recent country state changes for debugging. This should have
85     * a max length of MAX_LENGTH_LOGS.
86     */
87    private final ConcurrentLinkedQueue<Country> mDebugLogs = new ConcurrentLinkedQueue<Country>();
88
89    /**
90     * Most recent {@link Country} result that was added to the debug logs {@link #mDebugLogs}.
91     * We keep track of this value to help prevent adding many of the same {@link Country} objects
92     * to the logs.
93     */
94    private Country mLastCountryAddedToLogs;
95
96    /**
97     * Object used to synchronize access to {@link #mLastCountryAddedToLogs}. Be careful if
98     * using it to synchronize anything else in this file.
99     */
100    private final Object mObject = new Object();
101
102    /**
103     * Start time of the current session for which the detector has been active.
104     */
105    private long mStartTime;
106
107    /**
108     * Stop time of the most recent session for which the detector was active.
109     */
110    private long mStopTime;
111
112    /**
113     * The sum of all the time intervals in which the detector was active.
114     */
115    private long mTotalTime;
116
117    /**
118     * Number of {@link PhoneStateListener#onServiceStateChanged(ServiceState state)} events that
119     * have occurred for the current session for which the detector has been active.
120     */
121    private int mCountServiceStateChanges;
122
123    /**
124     * Total number of {@link PhoneStateListener#onServiceStateChanged(ServiceState state)} events
125     * that have occurred for all time intervals in which the detector has been active.
126     */
127    private int mTotalCountServiceStateChanges;
128
129    /**
130     * The listener for receiving the notification from LocationBasedCountryDetector.
131     */
132    private CountryListener mLocationBasedCountryDetectionListener = new CountryListener() {
133        @Override
134        public void onCountryDetected(Country country) {
135            if (DEBUG) Slog.d(TAG, "Country detected via LocationBasedCountryDetector");
136            mCountryFromLocation = country;
137            // Don't start the LocationBasedCountryDetector.
138            detectCountry(true, false);
139            stopLocationBasedDetector();
140        }
141    };
142
143    public ComprehensiveCountryDetector(Context context) {
144        super(context);
145        mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
146    }
147
148    @Override
149    public Country detectCountry() {
150        // Don't start the LocationBasedCountryDetector if we have been stopped.
151        return detectCountry(false, !mStopped);
152    }
153
154    @Override
155    public void stop() {
156        // Note: this method in this subclass called only by tests.
157        Slog.i(TAG, "Stop the detector.");
158        cancelLocationRefresh();
159        removePhoneStateListener();
160        stopLocationBasedDetector();
161        mListener = null;
162        mStopped = true;
163    }
164
165    /**
166     * Get the country from different sources in order of the reliability.
167     */
168    private Country getCountry() {
169        Country result = null;
170        result = getNetworkBasedCountry();
171        if (result == null) {
172            result = getLastKnownLocationBasedCountry();
173        }
174        if (result == null) {
175            result = getSimBasedCountry();
176        }
177        if (result == null) {
178            result = getLocaleCountry();
179        }
180        addToLogs(result);
181        return result;
182    }
183
184    /**
185     * Attempt to add this {@link Country} to the debug logs.
186     */
187    private void addToLogs(Country country) {
188        if (country == null) {
189            return;
190        }
191        // If the country (ISO and source) are the same as before, then there is no
192        // need to add this country as another entry in the logs. Synchronize access to this
193        // variable since multiple threads could be calling this method.
194        synchronized (mObject) {
195            if (mLastCountryAddedToLogs != null && mLastCountryAddedToLogs.equals(country)) {
196                return;
197            }
198            mLastCountryAddedToLogs = country;
199        }
200        // Manually maintain a max limit for the list of logs
201        if (mDebugLogs.size() >= MAX_LENGTH_DEBUG_LOGS) {
202            mDebugLogs.poll();
203        }
204        if (DEBUG) {
205            Slog.d(TAG, country.toString());
206        }
207        mDebugLogs.add(country);
208    }
209
210    private boolean isNetworkCountryCodeAvailable() {
211        // On CDMA TelephonyManager.getNetworkCountryIso() just returns SIM country.  We don't want
212        // to prioritize it over location based country, so ignore it.
213        final int phoneType = mTelephonyManager.getPhoneType();
214        if (DEBUG) Slog.v(TAG, "    phonetype=" + phoneType);
215        return phoneType == TelephonyManager.PHONE_TYPE_GSM;
216    }
217
218    /**
219     * @return the country from the mobile network.
220     */
221    protected Country getNetworkBasedCountry() {
222        String countryIso = null;
223        if (isNetworkCountryCodeAvailable()) {
224            countryIso = mTelephonyManager.getNetworkCountryIso();
225            if (!TextUtils.isEmpty(countryIso)) {
226                return new Country(countryIso, Country.COUNTRY_SOURCE_NETWORK);
227            }
228        }
229        return null;
230    }
231
232    /**
233     * @return the cached location based country.
234     */
235    protected Country getLastKnownLocationBasedCountry() {
236        return mCountryFromLocation;
237    }
238
239    /**
240     * @return the country from SIM card
241     */
242    protected Country getSimBasedCountry() {
243        String countryIso = null;
244        countryIso = mTelephonyManager.getSimCountryIso();
245        if (!TextUtils.isEmpty(countryIso)) {
246            return new Country(countryIso, Country.COUNTRY_SOURCE_SIM);
247        }
248        return null;
249    }
250
251    /**
252     * @return the country from the system's locale.
253     */
254    protected Country getLocaleCountry() {
255        Locale defaultLocale = Locale.getDefault();
256        if (defaultLocale != null) {
257            return new Country(defaultLocale.getCountry(), Country.COUNTRY_SOURCE_LOCALE);
258        } else {
259            return null;
260        }
261    }
262
263    /**
264     * @param notifyChange indicates whether the listener should be notified the change of the
265     * country
266     * @param startLocationBasedDetection indicates whether the LocationBasedCountryDetector could
267     * be started if the current country source is less reliable than the location.
268     * @return the current available UserCountry
269     */
270    private Country detectCountry(boolean notifyChange, boolean startLocationBasedDetection) {
271        Country country = getCountry();
272        runAfterDetectionAsync(mCountry != null ? new Country(mCountry) : mCountry, country,
273                notifyChange, startLocationBasedDetection);
274        mCountry = country;
275        return mCountry;
276    }
277
278    /**
279     * Run the tasks in the service's thread.
280     */
281    protected void runAfterDetectionAsync(final Country country, final Country detectedCountry,
282            final boolean notifyChange, final boolean startLocationBasedDetection) {
283        mHandler.post(new Runnable() {
284            @Override
285            public void run() {
286                runAfterDetection(
287                        country, detectedCountry, notifyChange, startLocationBasedDetection);
288            }
289        });
290    }
291
292    @Override
293    public void setCountryListener(CountryListener listener) {
294        CountryListener prevListener = mListener;
295        mListener = listener;
296        if (mListener == null) {
297            // Stop listening all services
298            removePhoneStateListener();
299            stopLocationBasedDetector();
300            cancelLocationRefresh();
301            mStopTime = SystemClock.elapsedRealtime();
302            mTotalTime += mStopTime;
303        } else if (prevListener == null) {
304            addPhoneStateListener();
305            detectCountry(false, true);
306            mStartTime = SystemClock.elapsedRealtime();
307            mStopTime = 0;
308            mCountServiceStateChanges = 0;
309        }
310    }
311
312    void runAfterDetection(final Country country, final Country detectedCountry,
313            final boolean notifyChange, final boolean startLocationBasedDetection) {
314        if (notifyChange) {
315            notifyIfCountryChanged(country, detectedCountry);
316        }
317        if (DEBUG) {
318            Slog.d(TAG, "startLocationBasedDetection=" + startLocationBasedDetection
319                    + " detectCountry=" + (detectedCountry == null ? null :
320                        "(source: " + detectedCountry.getSource()
321                        + ", countryISO: " + detectedCountry.getCountryIso() + ")")
322                    + " isAirplaneModeOff()=" + isAirplaneModeOff()
323                    + " mListener=" + mListener
324                    + " isGeoCoderImplemnted()=" + isGeoCoderImplemented());
325        }
326
327        if (startLocationBasedDetection && (detectedCountry == null
328                || detectedCountry.getSource() > Country.COUNTRY_SOURCE_LOCATION)
329                && isAirplaneModeOff() && mListener != null && isGeoCoderImplemented()) {
330            if (DEBUG) Slog.d(TAG, "run startLocationBasedDetector()");
331            // Start finding location when the source is less reliable than the
332            // location and the airplane mode is off (as geocoder will not
333            // work).
334            // TODO : Shall we give up starting the detector within a
335            // period of time?
336            startLocationBasedDetector(mLocationBasedCountryDetectionListener);
337        }
338        if (detectedCountry == null
339                || detectedCountry.getSource() >= Country.COUNTRY_SOURCE_LOCATION) {
340            // Schedule the location refresh if the country source is
341            // not more reliable than the location or no country is
342            // found.
343            // TODO: Listen to the preference change of GPS, Wifi etc,
344            // and start detecting the country.
345            scheduleLocationRefresh();
346        } else {
347            // Cancel the location refresh once the current source is
348            // more reliable than the location.
349            cancelLocationRefresh();
350            stopLocationBasedDetector();
351        }
352    }
353
354    /**
355     * Find the country from LocationProvider.
356     */
357    private synchronized void startLocationBasedDetector(CountryListener listener) {
358        if (mLocationBasedCountryDetector != null) {
359            return;
360        }
361        if (DEBUG) {
362            Slog.d(TAG, "starts LocationBasedDetector to detect Country code via Location info "
363                    + "(e.g. GPS)");
364        }
365        mLocationBasedCountryDetector = createLocationBasedCountryDetector();
366        mLocationBasedCountryDetector.setCountryListener(listener);
367        mLocationBasedCountryDetector.detectCountry();
368    }
369
370    private synchronized void stopLocationBasedDetector() {
371        if (DEBUG) {
372            Slog.d(TAG, "tries to stop LocationBasedDetector "
373                    + "(current detector: " + mLocationBasedCountryDetector + ")");
374        }
375        if (mLocationBasedCountryDetector != null) {
376            mLocationBasedCountryDetector.stop();
377            mLocationBasedCountryDetector = null;
378        }
379    }
380
381    protected CountryDetectorBase createLocationBasedCountryDetector() {
382        return new LocationBasedCountryDetector(mContext);
383    }
384
385    protected boolean isAirplaneModeOff() {
386        return Settings.Global.getInt(
387                mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0) == 0;
388    }
389
390    /**
391     * Notify the country change.
392     */
393    private void notifyIfCountryChanged(final Country country, final Country detectedCountry) {
394        if (detectedCountry != null && mListener != null
395                && (country == null || !country.equals(detectedCountry))) {
396            if (DEBUG) {
397                Slog.d(TAG, "" + country + " --> " + detectedCountry);
398            }
399            notifyListener(detectedCountry);
400        }
401    }
402
403    /**
404     * Schedule the next location refresh. We will do nothing if the scheduled task exists.
405     */
406    private synchronized void scheduleLocationRefresh() {
407        if (mLocationRefreshTimer != null) return;
408        if (DEBUG) {
409            Slog.d(TAG, "start periodic location refresh timer. Interval: "
410                    + LOCATION_REFRESH_INTERVAL);
411        }
412        mLocationRefreshTimer = new Timer();
413        mLocationRefreshTimer.schedule(new TimerTask() {
414            @Override
415            public void run() {
416                if (DEBUG) {
417                    Slog.d(TAG, "periodic location refresh event. Starts detecting Country code");
418                }
419                mLocationRefreshTimer = null;
420                detectCountry(false, true);
421            }
422        }, LOCATION_REFRESH_INTERVAL);
423    }
424
425    /**
426     * Cancel the scheduled refresh task if it exists
427     */
428    private synchronized void cancelLocationRefresh() {
429        if (mLocationRefreshTimer != null) {
430            mLocationRefreshTimer.cancel();
431            mLocationRefreshTimer = null;
432        }
433    }
434
435    protected synchronized void addPhoneStateListener() {
436        if (mPhoneStateListener == null) {
437            mPhoneStateListener = new PhoneStateListener() {
438                @Override
439                public void onServiceStateChanged(ServiceState serviceState) {
440                    mCountServiceStateChanges++;
441                    mTotalCountServiceStateChanges++;
442
443                    if (!isNetworkCountryCodeAvailable()) {
444                        return;
445                    }
446                    if (DEBUG) Slog.d(TAG, "onServiceStateChanged: " + serviceState.getState());
447
448                    detectCountry(true, true);
449                }
450            };
451            mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE);
452        }
453    }
454
455    protected synchronized void removePhoneStateListener() {
456        if (mPhoneStateListener != null) {
457            mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
458            mPhoneStateListener = null;
459        }
460    }
461
462    protected boolean isGeoCoderImplemented() {
463        return Geocoder.isPresent();
464    }
465
466    @Override
467    public String toString() {
468        long currentTime = SystemClock.elapsedRealtime();
469        long currentSessionLength = 0;
470        StringBuilder sb = new StringBuilder();
471        sb.append("ComprehensiveCountryDetector{");
472        // The detector hasn't stopped yet --> still running
473        if (mStopTime == 0) {
474            currentSessionLength = currentTime - mStartTime;
475            sb.append("timeRunning=" + currentSessionLength + ", ");
476        } else {
477            // Otherwise, it has already stopped, so take the last session
478            sb.append("lastRunTimeLength=" + (mStopTime - mStartTime) + ", ");
479        }
480        sb.append("totalCountServiceStateChanges=" + mTotalCountServiceStateChanges + ", ");
481        sb.append("currentCountServiceStateChanges=" + mCountServiceStateChanges + ", ");
482        sb.append("totalTime=" + (mTotalTime + currentSessionLength) + ", ");
483        sb.append("currentTime=" + currentTime + ", ");
484        sb.append("countries=");
485        for (Country country : mDebugLogs) {
486            sb.append("\n   " + country.toString());
487        }
488        sb.append("}");
489        return sb.toString();
490    }
491}
492