• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.countrydetector;
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.Slog;
30 
31 import java.util.Locale;
32 import java.util.Timer;
33 import java.util.TimerTask;
34 import 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  */
58 public 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 
ComprehensiveCountryDetector(Context context)143     public ComprehensiveCountryDetector(Context context) {
144         super(context);
145         mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
146     }
147 
148     @Override
detectCountry()149     public Country detectCountry() {
150         // Don't start the LocationBasedCountryDetector if we have been stopped.
151         return detectCountry(false, !mStopped);
152     }
153 
154     @Override
stop()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      */
getCountry()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      */
addToLogs(Country country)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 
isNetworkCountryCodeAvailable()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      */
getNetworkBasedCountry()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      */
getLastKnownLocationBasedCountry()235     protected Country getLastKnownLocationBasedCountry() {
236         return mCountryFromLocation;
237     }
238 
239     /**
240      * @return the country from SIM card
241      */
getSimBasedCountry()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      */
getLocaleCountry()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      */
detectCountry(boolean notifyChange, boolean startLocationBasedDetection)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      */
runAfterDetectionAsync(final Country country, final Country detectedCountry, final boolean notifyChange, final boolean startLocationBasedDetection)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
setCountryListener(CountryListener listener)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 
runAfterDetection(final Country country, final Country detectedCountry, final boolean notifyChange, final boolean startLocationBasedDetection)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                     + " isWifiOn()=" + isWifiOn()
324                     + " mListener=" + mListener
325                     + " isGeoCoderImplemnted()=" + isGeoCoderImplemented());
326         }
327 
328         if (startLocationBasedDetection && (detectedCountry == null
329                 || detectedCountry.getSource() > Country.COUNTRY_SOURCE_LOCATION)
330                 && (isAirplaneModeOff() || isWifiOn()) && mListener != null
331                 && isGeoCoderImplemented()) {
332             if (DEBUG) Slog.d(TAG, "run startLocationBasedDetector()");
333             // Start finding location when the source is less reliable than the
334             // location and the airplane mode is off (as geocoder will not
335             // work).
336             // TODO : Shall we give up starting the detector within a
337             // period of time?
338             startLocationBasedDetector(mLocationBasedCountryDetectionListener);
339         }
340         if (detectedCountry == null
341                 || detectedCountry.getSource() >= Country.COUNTRY_SOURCE_LOCATION) {
342             // Schedule the location refresh if the country source is
343             // not more reliable than the location or no country is
344             // found.
345             // TODO: Listen to the preference change of GPS, Wifi etc,
346             // and start detecting the country.
347             scheduleLocationRefresh();
348         } else {
349             // Cancel the location refresh once the current source is
350             // more reliable than the location.
351             cancelLocationRefresh();
352             stopLocationBasedDetector();
353         }
354     }
355 
356     /**
357      * Find the country from LocationProvider.
358      */
startLocationBasedDetector(CountryListener listener)359     private synchronized void startLocationBasedDetector(CountryListener listener) {
360         if (mLocationBasedCountryDetector != null) {
361             return;
362         }
363         if (DEBUG) {
364             Slog.d(TAG, "starts LocationBasedDetector to detect Country code via Location info "
365                     + "(e.g. GPS)");
366         }
367         mLocationBasedCountryDetector = createLocationBasedCountryDetector();
368         mLocationBasedCountryDetector.setCountryListener(listener);
369         mLocationBasedCountryDetector.detectCountry();
370     }
371 
stopLocationBasedDetector()372     private synchronized void stopLocationBasedDetector() {
373         if (DEBUG) {
374             Slog.d(TAG, "tries to stop LocationBasedDetector "
375                     + "(current detector: " + mLocationBasedCountryDetector + ")");
376         }
377         if (mLocationBasedCountryDetector != null) {
378             mLocationBasedCountryDetector.stop();
379             mLocationBasedCountryDetector = null;
380         }
381     }
382 
createLocationBasedCountryDetector()383     protected CountryDetectorBase createLocationBasedCountryDetector() {
384         return new LocationBasedCountryDetector(mContext);
385     }
386 
isAirplaneModeOff()387     protected boolean isAirplaneModeOff() {
388         return Settings.Global.getInt(
389                 mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0) == 0;
390     }
391 
isWifiOn()392     protected boolean isWifiOn() {
393         return Settings.Global.getInt(
394                 mContext.getContentResolver(), Settings.Global.WIFI_ON, 0) != 0;
395     }
396 
397     /**
398      * Notify the country change.
399      */
notifyIfCountryChanged(final Country country, final Country detectedCountry)400     private void notifyIfCountryChanged(final Country country, final Country detectedCountry) {
401         if (detectedCountry != null && mListener != null
402                 && (country == null || !country.equals(detectedCountry))) {
403             if (DEBUG) {
404                 Slog.d(TAG, "" + country + " --> " + detectedCountry);
405             }
406             notifyListener(detectedCountry);
407         }
408     }
409 
410     /**
411      * Schedule the next location refresh. We will do nothing if the scheduled task exists.
412      */
scheduleLocationRefresh()413     private synchronized void scheduleLocationRefresh() {
414         if (mLocationRefreshTimer != null) return;
415         if (DEBUG) {
416             Slog.d(TAG, "start periodic location refresh timer. Interval: "
417                     + LOCATION_REFRESH_INTERVAL);
418         }
419         mLocationRefreshTimer = new Timer();
420         mLocationRefreshTimer.schedule(new TimerTask() {
421             @Override
422             public void run() {
423                 if (DEBUG) {
424                     Slog.d(TAG, "periodic location refresh event. Starts detecting Country code");
425                 }
426                 mLocationRefreshTimer = null;
427                 detectCountry(false, true);
428             }
429         }, LOCATION_REFRESH_INTERVAL);
430     }
431 
432     /**
433      * Cancel the scheduled refresh task if it exists
434      */
cancelLocationRefresh()435     private synchronized void cancelLocationRefresh() {
436         if (mLocationRefreshTimer != null) {
437             mLocationRefreshTimer.cancel();
438             mLocationRefreshTimer = null;
439         }
440     }
441 
addPhoneStateListener()442     protected synchronized void addPhoneStateListener() {
443         if (mPhoneStateListener == null) {
444             mPhoneStateListener = new PhoneStateListener() {
445                 @Override
446                 public void onServiceStateChanged(ServiceState serviceState) {
447                     mCountServiceStateChanges++;
448                     mTotalCountServiceStateChanges++;
449 
450                     if (!isNetworkCountryCodeAvailable()) {
451                         return;
452                     }
453                     if (DEBUG) Slog.d(TAG, "onServiceStateChanged: " + serviceState.getState());
454 
455                     detectCountry(true, true);
456                 }
457             };
458             mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE);
459         }
460     }
461 
removePhoneStateListener()462     protected synchronized void removePhoneStateListener() {
463         if (mPhoneStateListener != null) {
464             mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
465             mPhoneStateListener = null;
466         }
467     }
468 
isGeoCoderImplemented()469     protected boolean isGeoCoderImplemented() {
470         return Geocoder.isPresent();
471     }
472 
473     @Override
toString()474     public String toString() {
475         long currentTime = SystemClock.elapsedRealtime();
476         long currentSessionLength = 0;
477         StringBuilder sb = new StringBuilder();
478         sb.append("ComprehensiveCountryDetector{");
479         // The detector hasn't stopped yet --> still running
480         if (mStopTime == 0) {
481             currentSessionLength = currentTime - mStartTime;
482             sb.append("timeRunning=" + currentSessionLength + ", ");
483         } else {
484             // Otherwise, it has already stopped, so take the last session
485             sb.append("lastRunTimeLength=" + (mStopTime - mStartTime) + ", ");
486         }
487         sb.append("totalCountServiceStateChanges=" + mTotalCountServiceStateChanges + ", ");
488         sb.append("currentCountServiceStateChanges=" + mCountServiceStateChanges + ", ");
489         sb.append("totalTime=" + (mTotalTime + currentSessionLength) + ", ");
490         sb.append("currentTime=" + currentTime + ", ");
491         sb.append("countries=");
492         for (Country country : mDebugLogs) {
493             sb.append("\n   " + country.toString());
494         }
495         sb.append("}");
496         return sb.toString();
497     }
498 }
499