blob: 2d6a148012deef0b2e4179fd7ab25f6979b43626 [file] [log] [blame]
Bai Taoa58a8752010-07-13 15:32:16 +08001/*
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
17package com.android.server.location;
18
Bai Taoa58a8752010-07-13 15:32:16 +080019import android.content.Context;
20import android.location.Country;
21import android.location.CountryListener;
22import android.location.Geocoder;
Katherine Kuanda123492011-12-12 12:56:32 -080023import android.os.SystemClock;
Bai Taoa58a8752010-07-13 15:32:16 +080024import android.provider.Settings;
25import android.telephony.PhoneStateListener;
26import android.telephony.ServiceState;
27import android.telephony.TelephonyManager;
28import android.text.TextUtils;
Katherine Kuanda123492011-12-12 12:56:32 -080029import android.util.Log;
Bai Taoa58a8752010-07-13 15:32:16 +080030import android.util.Slog;
31
Marc Blank141e00c2010-09-20 20:35:09 -070032import java.util.Locale;
33import java.util.Timer;
34import java.util.TimerTask;
Katherine Kuanda123492011-12-12 12:56:32 -080035import java.util.concurrent.ConcurrentLinkedQueue;
Marc Blank141e00c2010-09-20 20:35:09 -070036
Bai Taoa58a8752010-07-13 15:32:16 +080037/**
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 */
59public class ComprehensiveCountryDetector extends CountryDetectorBase {
60
Katherine Kuanda123492011-12-12 12:56:32 -080061 private final static String TAG = "CountryDetector";
Daisuke Miyakawaa550bdc2011-04-27 08:26:33 -070062 /* package */ static final boolean DEBUG = false;
Bai Taoa58a8752010-07-13 15:32:16 +080063
64 /**
Katherine Kuanda123492011-12-12 12:56:32 -080065 * Max length of logs to maintain for debugging.
66 */
67 private static final int MAX_LENGTH_DEBUG_LOGS = 20;
68
69 /**
Bai Taoa58a8752010-07-13 15:32:16 +080070 * 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
Bai Taoa58a8752010-07-13 15:32:16 +080077 private Country mCountry;
Makoto Onukif9165b72011-12-09 17:43:35 -080078 private final TelephonyManager mTelephonyManager;
Bai Taoa58a8752010-07-13 15:32:16 +080079 private Country mCountryFromLocation;
80 private boolean mStopped = false;
Bai Taoa58a8752010-07-13 15:32:16 +080081
Makoto Onukif9165b72011-12-09 17:43:35 -080082 private PhoneStateListener mPhoneStateListener;
Bai Taoa58a8752010-07-13 15:32:16 +080083
84 /**
Katherine Kuanda123492011-12-12 12:56:32 -080085 * 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 /**
Bai Taoa58a8752010-07-13 15:32:16 +0800131 * The listener for receiving the notification from LocationBasedCountryDetector.
132 */
133 private CountryListener mLocationBasedCountryDetectionListener = new CountryListener() {
Daisuke Miyakawaa550bdc2011-04-27 08:26:33 -0700134 @Override
Bai Taoa58a8752010-07-13 15:32:16 +0800135 public void onCountryDetected(Country country) {
Daisuke Miyakawaa550bdc2011-04-27 08:26:33 -0700136 if (DEBUG) Slog.d(TAG, "Country detected via LocationBasedCountryDetector");
Bai Taoa58a8752010-07-13 15:32:16 +0800137 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);
Bai Taoa58a8752010-07-13 15:32:16 +0800147 }
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() {
Makoto Onukif9165b72011-12-09 17:43:35 -0800157 // Note: this method in this subclass called only by tests.
Bai Taoa58a8752010-07-13 15:32:16 +0800158 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 }
Katherine Kuanda123492011-12-12 12:56:32 -0800181 addToLogs(result);
Bai Taoa58a8752010-07-13 15:32:16 +0800182 return result;
183 }
184
Katherine Kuanda123492011-12-12 12:56:32 -0800185 /**
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 (Log.isLoggable(TAG, Log.DEBUG)) {
206 Slog.d(TAG, country.toString());
207 }
208 mDebugLogs.add(country);
209 }
210
Makoto Onukif9165b72011-12-09 17:43:35 -0800211 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
Bai Taoa58a8752010-07-13 15:32:16 +0800219 /**
220 * @return the country from the mobile network.
221 */
222 protected Country getNetworkBasedCountry() {
223 String countryIso = null;
Makoto Onukif9165b72011-12-09 17:43:35 -0800224 if (isNetworkCountryCodeAvailable()) {
Bai Taoa58a8752010-07-13 15:32:16 +0800225 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() {
Daisuke Miyakawaa550bdc2011-04-27 08:26:33 -0700285 @Override
Bai Taoa58a8752010-07-13 15:32:16 +0800286 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();
Katherine Kuanda123492011-12-12 12:56:32 -0800302 mStopTime = SystemClock.elapsedRealtime();
303 mTotalTime += mStopTime;
Bai Taoa58a8752010-07-13 15:32:16 +0800304 } else if (prevListener == null) {
305 addPhoneStateListener();
306 detectCountry(false, true);
Katherine Kuanda123492011-12-12 12:56:32 -0800307 mStartTime = SystemClock.elapsedRealtime();
308 mStopTime = 0;
309 mCountServiceStateChanges = 0;
Bai Taoa58a8752010-07-13 15:32:16 +0800310 }
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 }
Daisuke Miyakawaa550bdc2011-04-27 08:26:33 -0700318 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
Bai Taoa58a8752010-07-13 15:32:16 +0800328 if (startLocationBasedDetection && (detectedCountry == null
329 || detectedCountry.getSource() > Country.COUNTRY_SOURCE_LOCATION)
330 && isAirplaneModeOff() && mListener != null && isGeoCoderImplemented()) {
Daisuke Miyakawaa550bdc2011-04-27 08:26:33 -0700331 if (DEBUG) Slog.d(TAG, "run startLocationBasedDetector()");
Bai Taoa58a8752010-07-13 15:32:16 +0800332 // 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 }
Daisuke Miyakawaa550bdc2011-04-27 08:26:33 -0700362 if (DEBUG) {
363 Slog.d(TAG, "starts LocationBasedDetector to detect Country code via Location info "
364 + "(e.g. GPS)");
365 }
Bai Taoa58a8752010-07-13 15:32:16 +0800366 mLocationBasedCountryDetector = createLocationBasedCountryDetector();
367 mLocationBasedCountryDetector.setCountryListener(listener);
368 mLocationBasedCountryDetector.detectCountry();
369 }
370
371 private synchronized void stopLocationBasedDetector() {
Daisuke Miyakawaa550bdc2011-04-27 08:26:33 -0700372 if (DEBUG) {
373 Slog.d(TAG, "tries to stop LocationBasedDetector "
374 + "(current detector: " + mLocationBasedCountryDetector + ")");
375 }
Bai Taoa58a8752010-07-13 15:32:16 +0800376 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.System.getInt(
388 mContext.getContentResolver(), Settings.System.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))) {
Katherine Kuanda123492011-12-12 12:56:32 -0800397 if (Log.isLoggable(TAG, Log.DEBUG)) {
398 Slog.d(TAG, "" + country + " --> " + detectedCountry);
399 }
Bai Taoa58a8752010-07-13 15:32:16 +0800400 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;
Daisuke Miyakawaa550bdc2011-04-27 08:26:33 -0700409 if (DEBUG) {
410 Slog.d(TAG, "start periodic location refresh timer. Interval: "
411 + LOCATION_REFRESH_INTERVAL);
412 }
Bai Taoa58a8752010-07-13 15:32:16 +0800413 mLocationRefreshTimer = new Timer();
414 mLocationRefreshTimer.schedule(new TimerTask() {
415 @Override
416 public void run() {
Daisuke Miyakawaa550bdc2011-04-27 08:26:33 -0700417 if (DEBUG) {
418 Slog.d(TAG, "periodic location refresh event. Starts detecting Country code");
419 }
Bai Taoa58a8752010-07-13 15:32:16 +0800420 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() {
Makoto Onukif9165b72011-12-09 17:43:35 -0800437 if (mPhoneStateListener == null) {
Bai Taoa58a8752010-07-13 15:32:16 +0800438 mPhoneStateListener = new PhoneStateListener() {
439 @Override
440 public void onServiceStateChanged(ServiceState serviceState) {
Katherine Kuanda123492011-12-12 12:56:32 -0800441 mCountServiceStateChanges++;
442 mTotalCountServiceStateChanges++;
443
Makoto Onukif9165b72011-12-09 17:43:35 -0800444 if (!isNetworkCountryCodeAvailable()) {
445 return;
Bai Taoa58a8752010-07-13 15:32:16 +0800446 }
Makoto Onukif9165b72011-12-09 17:43:35 -0800447 if (DEBUG) Slog.d(TAG, "onServiceStateChanged: " + serviceState.getState());
448
449 detectCountry(true, true);
Bai Taoa58a8752010-07-13 15:32:16 +0800450 }
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() {
Marc Blank141e00c2010-09-20 20:35:09 -0700464 return Geocoder.isPresent();
Bai Taoa58a8752010-07-13 15:32:16 +0800465 }
Katherine Kuanda123492011-12-12 12:56:32 -0800466
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 }
Bai Taoa58a8752010-07-13 15:32:16 +0800492}