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