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