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.provider.Settings; 24 import android.telephony.PhoneStateListener; 25 import android.telephony.ServiceState; 26 import android.telephony.TelephonyManager; 27 import android.text.TextUtils; 28 import android.util.Slog; 29 30 import java.util.Locale; 31 import java.util.Timer; 32 import java.util.TimerTask; 33 34 /** 35 * This class is used to detect the country where the user is. The sources of 36 * country are queried in order of reliability, like 37 * <ul> 38 * <li>Mobile network</li> 39 * <li>Location</li> 40 * <li>SIM's country</li> 41 * <li>Phone's locale</li> 42 * </ul> 43 * <p> 44 * Call the {@link #detectCountry()} to get the available country immediately. 45 * <p> 46 * To be notified of the future country change, using the 47 * {@link #setCountryListener(CountryListener)} 48 * <p> 49 * Using the {@link #stop()} to stop listening to the country change. 50 * <p> 51 * The country information will be refreshed every 52 * {@link #LOCATION_REFRESH_INTERVAL} once the location based country is used. 53 * 54 * @hide 55 */ 56 public class ComprehensiveCountryDetector extends CountryDetectorBase { 57 58 private final static String TAG = "ComprehensiveCountryDetector"; 59 /* package */ static final boolean DEBUG = false; 60 61 /** 62 * The refresh interval when the location based country was used 63 */ 64 private final static long LOCATION_REFRESH_INTERVAL = 1000 * 60 * 60 * 24; // 1 day 65 66 protected CountryDetectorBase mLocationBasedCountryDetector; 67 protected Timer mLocationRefreshTimer; 68 69 private final int mPhoneType; 70 private Country mCountry; 71 private TelephonyManager mTelephonyManager; 72 private Country mCountryFromLocation; 73 private boolean mStopped = false; 74 private ServiceState mLastState; 75 76 private PhoneStateListener mPhoneStateListener = new PhoneStateListener() { 77 @Override 78 public void onServiceStateChanged(ServiceState serviceState) { 79 // TODO: Find out how often we will be notified, if this method is called too 80 // many times, let's consider querying the network. 81 Slog.d(TAG, "onServiceStateChanged"); 82 // We only care the state change 83 if (mLastState == null || mLastState.getState() != serviceState.getState()) { 84 detectCountry(true, true); 85 mLastState = new ServiceState(serviceState); 86 } 87 } 88 }; 89 90 /** 91 * The listener for receiving the notification from LocationBasedCountryDetector. 92 */ 93 private CountryListener mLocationBasedCountryDetectionListener = new CountryListener() { 94 @Override 95 public void onCountryDetected(Country country) { 96 if (DEBUG) Slog.d(TAG, "Country detected via LocationBasedCountryDetector"); 97 mCountryFromLocation = country; 98 // Don't start the LocationBasedCountryDetector. 99 detectCountry(true, false); 100 stopLocationBasedDetector(); 101 } 102 }; 103 104 public ComprehensiveCountryDetector(Context context) { 105 super(context); 106 mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 107 mPhoneType = mTelephonyManager.getPhoneType(); 108 } 109 110 @Override 111 public Country detectCountry() { 112 // Don't start the LocationBasedCountryDetector if we have been stopped. 113 return detectCountry(false, !mStopped); 114 } 115 116 @Override 117 public void stop() { 118 Slog.i(TAG, "Stop the detector."); 119 cancelLocationRefresh(); 120 removePhoneStateListener(); 121 stopLocationBasedDetector(); 122 mListener = null; 123 mStopped = true; 124 } 125 126 /** 127 * Get the country from different sources in order of the reliability. 128 */ 129 private Country getCountry() { 130 Country result = null; 131 result = getNetworkBasedCountry(); 132 if (result == null) { 133 result = getLastKnownLocationBasedCountry(); 134 } 135 if (result == null) { 136 result = getSimBasedCountry(); 137 } 138 if (result == null) { 139 result = getLocaleCountry(); 140 } 141 return result; 142 } 143 144 /** 145 * @return the country from the mobile network. 146 */ 147 protected Country getNetworkBasedCountry() { 148 String countryIso = null; 149 // TODO: The document says the result may be unreliable on CDMA networks. Shall we use 150 // it on CDMA phone? We may test the Android primarily used countries. 151 if (mPhoneType == TelephonyManager.PHONE_TYPE_GSM) { 152 countryIso = mTelephonyManager.getNetworkCountryIso(); 153 if (!TextUtils.isEmpty(countryIso)) { 154 return new Country(countryIso, Country.COUNTRY_SOURCE_NETWORK); 155 } 156 } 157 return null; 158 } 159 160 /** 161 * @return the cached location based country. 162 */ 163 protected Country getLastKnownLocationBasedCountry() { 164 return mCountryFromLocation; 165 } 166 167 /** 168 * @return the country from SIM card 169 */ 170 protected Country getSimBasedCountry() { 171 String countryIso = null; 172 countryIso = mTelephonyManager.getSimCountryIso(); 173 if (!TextUtils.isEmpty(countryIso)) { 174 return new Country(countryIso, Country.COUNTRY_SOURCE_SIM); 175 } 176 return null; 177 } 178 179 /** 180 * @return the country from the system's locale. 181 */ 182 protected Country getLocaleCountry() { 183 Locale defaultLocale = Locale.getDefault(); 184 if (defaultLocale != null) { 185 return new Country(defaultLocale.getCountry(), Country.COUNTRY_SOURCE_LOCALE); 186 } else { 187 return null; 188 } 189 } 190 191 /** 192 * @param notifyChange indicates whether the listener should be notified the change of the 193 * country 194 * @param startLocationBasedDetection indicates whether the LocationBasedCountryDetector could 195 * be started if the current country source is less reliable than the location. 196 * @return the current available UserCountry 197 */ 198 private Country detectCountry(boolean notifyChange, boolean startLocationBasedDetection) { 199 Country country = getCountry(); 200 runAfterDetectionAsync(mCountry != null ? new Country(mCountry) : mCountry, country, 201 notifyChange, startLocationBasedDetection); 202 mCountry = country; 203 return mCountry; 204 } 205 206 /** 207 * Run the tasks in the service's thread. 208 */ 209 protected void runAfterDetectionAsync(final Country country, final Country detectedCountry, 210 final boolean notifyChange, final boolean startLocationBasedDetection) { 211 mHandler.post(new Runnable() { 212 @Override 213 public void run() { 214 runAfterDetection( 215 country, detectedCountry, notifyChange, startLocationBasedDetection); 216 } 217 }); 218 } 219 220 @Override 221 public void setCountryListener(CountryListener listener) { 222 CountryListener prevListener = mListener; 223 mListener = listener; 224 if (mListener == null) { 225 // Stop listening all services 226 removePhoneStateListener(); 227 stopLocationBasedDetector(); 228 cancelLocationRefresh(); 229 } else if (prevListener == null) { 230 addPhoneStateListener(); 231 detectCountry(false, true); 232 } 233 } 234 235 void runAfterDetection(final Country country, final Country detectedCountry, 236 final boolean notifyChange, final boolean startLocationBasedDetection) { 237 if (notifyChange) { 238 notifyIfCountryChanged(country, detectedCountry); 239 } 240 if (DEBUG) { 241 Slog.d(TAG, "startLocationBasedDetection=" + startLocationBasedDetection 242 + " detectCountry=" + (detectedCountry == null ? null : 243 "(source: " + detectedCountry.getSource() 244 + ", countryISO: " + detectedCountry.getCountryIso() + ")") 245 + " isAirplaneModeOff()=" + isAirplaneModeOff() 246 + " mListener=" + mListener 247 + " isGeoCoderImplemnted()=" + isGeoCoderImplemented()); 248 } 249 250 if (startLocationBasedDetection && (detectedCountry == null 251 || detectedCountry.getSource() > Country.COUNTRY_SOURCE_LOCATION) 252 && isAirplaneModeOff() && mListener != null && isGeoCoderImplemented()) { 253 if (DEBUG) Slog.d(TAG, "run startLocationBasedDetector()"); 254 // Start finding location when the source is less reliable than the 255 // location and the airplane mode is off (as geocoder will not 256 // work). 257 // TODO : Shall we give up starting the detector within a 258 // period of time? 259 startLocationBasedDetector(mLocationBasedCountryDetectionListener); 260 } 261 if (detectedCountry == null 262 || detectedCountry.getSource() >= Country.COUNTRY_SOURCE_LOCATION) { 263 // Schedule the location refresh if the country source is 264 // not more reliable than the location or no country is 265 // found. 266 // TODO: Listen to the preference change of GPS, Wifi etc, 267 // and start detecting the country. 268 scheduleLocationRefresh(); 269 } else { 270 // Cancel the location refresh once the current source is 271 // more reliable than the location. 272 cancelLocationRefresh(); 273 stopLocationBasedDetector(); 274 } 275 } 276 277 /** 278 * Find the country from LocationProvider. 279 */ 280 private synchronized void startLocationBasedDetector(CountryListener listener) { 281 if (mLocationBasedCountryDetector != null) { 282 return; 283 } 284 if (DEBUG) { 285 Slog.d(TAG, "starts LocationBasedDetector to detect Country code via Location info " 286 + "(e.g. GPS)"); 287 } 288 mLocationBasedCountryDetector = createLocationBasedCountryDetector(); 289 mLocationBasedCountryDetector.setCountryListener(listener); 290 mLocationBasedCountryDetector.detectCountry(); 291 } 292 293 private synchronized void stopLocationBasedDetector() { 294 if (DEBUG) { 295 Slog.d(TAG, "tries to stop LocationBasedDetector " 296 + "(current detector: " + mLocationBasedCountryDetector + ")"); 297 } 298 if (mLocationBasedCountryDetector != null) { 299 mLocationBasedCountryDetector.stop(); 300 mLocationBasedCountryDetector = null; 301 } 302 } 303 304 protected CountryDetectorBase createLocationBasedCountryDetector() { 305 return new LocationBasedCountryDetector(mContext); 306 } 307 308 protected boolean isAirplaneModeOff() { 309 return Settings.System.getInt( 310 mContext.getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0) == 0; 311 } 312 313 /** 314 * Notify the country change. 315 */ 316 private void notifyIfCountryChanged(final Country country, final Country detectedCountry) { 317 if (detectedCountry != null && mListener != null 318 && (country == null || !country.equals(detectedCountry))) { 319 Slog.d(TAG, 320 "The country was changed from " + country != null ? country.getCountryIso() : 321 country + " to " + detectedCountry.getCountryIso()); 322 notifyListener(detectedCountry); 323 } 324 } 325 326 /** 327 * Schedule the next location refresh. We will do nothing if the scheduled task exists. 328 */ 329 private synchronized void scheduleLocationRefresh() { 330 if (mLocationRefreshTimer != null) return; 331 if (DEBUG) { 332 Slog.d(TAG, "start periodic location refresh timer. Interval: " 333 + LOCATION_REFRESH_INTERVAL); 334 } 335 mLocationRefreshTimer = new Timer(); 336 mLocationRefreshTimer.schedule(new TimerTask() { 337 @Override 338 public void run() { 339 if (DEBUG) { 340 Slog.d(TAG, "periodic location refresh event. Starts detecting Country code"); 341 } 342 mLocationRefreshTimer = null; 343 detectCountry(false, true); 344 } 345 }, LOCATION_REFRESH_INTERVAL); 346 } 347 348 /** 349 * Cancel the scheduled refresh task if it exists 350 */ 351 private synchronized void cancelLocationRefresh() { 352 if (mLocationRefreshTimer != null) { 353 mLocationRefreshTimer.cancel(); 354 mLocationRefreshTimer = null; 355 } 356 } 357 358 protected synchronized void addPhoneStateListener() { 359 if (mPhoneStateListener == null && mPhoneType == TelephonyManager.PHONE_TYPE_GSM) { 360 mLastState = null; 361 mPhoneStateListener = new PhoneStateListener() { 362 @Override 363 public void onServiceStateChanged(ServiceState serviceState) { 364 // TODO: Find out how often we will be notified, if this 365 // method is called too 366 // many times, let's consider querying the network. 367 Slog.d(TAG, "onServiceStateChanged"); 368 // We only care the state change 369 if (mLastState == null || mLastState.getState() != serviceState.getState()) { 370 detectCountry(true, true); 371 mLastState = new ServiceState(serviceState); 372 } 373 } 374 }; 375 mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE); 376 } 377 } 378 379 protected synchronized void removePhoneStateListener() { 380 if (mPhoneStateListener != null) { 381 mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); 382 mPhoneStateListener = null; 383 } 384 } 385 386 protected boolean isGeoCoderImplemented() { 387 return Geocoder.isPresent(); 388 } 389 } 390