• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.server.location;
18 
19 import android.content.Context;
20 import android.location.Country;
21 import android.location.CountryListener;
22 import android.location.Geocoder;
23 import android.os.SystemClock;
24 import android.provider.Settings;
25 import android.telephony.PhoneStateListener;
26 import android.telephony.ServiceState;
27 import android.telephony.TelephonyManager;
28 import android.text.TextUtils;
29 import android.util.Log;
30 import android.util.Slog;
31 
32 import java.util.Locale;
33 import java.util.Timer;
34 import java.util.TimerTask;
35 import 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  */
59 public 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 
ComprehensiveCountryDetector(Context context)144     public ComprehensiveCountryDetector(Context context) {
145         super(context);
146         mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
147     }
148 
149     @Override
detectCountry()150     public Country detectCountry() {
151         // Don't start the LocationBasedCountryDetector if we have been stopped.
152         return detectCountry(false, !mStopped);
153     }
154 
155     @Override
stop()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      */
getCountry()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      */
addToLogs(Country country)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 
isNetworkCountryCodeAvailable()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      */
getNetworkBasedCountry()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      */
getLastKnownLocationBasedCountry()236     protected Country getLastKnownLocationBasedCountry() {
237         return mCountryFromLocation;
238     }
239 
240     /**
241      * @return the country from SIM card
242      */
getSimBasedCountry()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      */
getLocaleCountry()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      */
detectCountry(boolean notifyChange, boolean startLocationBasedDetection)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      */
runAfterDetectionAsync(final Country country, final Country detectedCountry, final boolean notifyChange, final boolean startLocationBasedDetection)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
setCountryListener(CountryListener listener)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 
runAfterDetection(final Country country, final Country detectedCountry, final boolean notifyChange, final boolean startLocationBasedDetection)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      */
startLocationBasedDetector(CountryListener listener)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 
stopLocationBasedDetector()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 
createLocationBasedCountryDetector()382     protected CountryDetectorBase createLocationBasedCountryDetector() {
383         return new LocationBasedCountryDetector(mContext);
384     }
385 
isAirplaneModeOff()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      */
notifyIfCountryChanged(final Country country, final Country detectedCountry)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      */
scheduleLocationRefresh()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      */
cancelLocationRefresh()429     private synchronized void cancelLocationRefresh() {
430         if (mLocationRefreshTimer != null) {
431             mLocationRefreshTimer.cancel();
432             mLocationRefreshTimer = null;
433         }
434     }
435 
addPhoneStateListener()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 
removePhoneStateListener()456     protected synchronized void removePhoneStateListener() {
457         if (mPhoneStateListener != null) {
458             mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
459             mPhoneStateListener = null;
460         }
461     }
462 
isGeoCoderImplemented()463     protected boolean isGeoCoderImplemented() {
464         return Geocoder.isPresent();
465     }
466 
467     @Override
toString()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