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