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 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