diff options
18 files changed, 2106 insertions, 1 deletions
diff --git a/Android.mk b/Android.mk index 3afbfd7a0460..696b1176cf8d 100644 --- a/Android.mk +++ b/Android.mk @@ -163,6 +163,8 @@ LOCAL_SRC_FILES += \ core/java/com/android/internal/view/IInputMethodManager.aidl \ core/java/com/android/internal/view/IInputMethodSession.aidl \ core/java/com/android/internal/widget/IRemoteViewsFactory.aidl \ + location/java/android/location/ICountryDetector.aidl \ + location/java/android/location/ICountryListener.aidl \ location/java/android/location/IGeocodeProvider.aidl \ location/java/android/location/IGpsStatusListener.aidl \ location/java/android/location/IGpsStatusProvider.aidl \ diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 8eded3d9117a..10ebd607ec4d 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -62,6 +62,8 @@ import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.hardware.SensorManager; +import android.location.CountryDetector; +import android.location.ICountryDetector; import android.location.ILocationManager; import android.location.LocationManager; import android.media.AudioManager; @@ -168,6 +170,7 @@ class ContextImpl extends Context { private static ThrottleManager sThrottleManager; private static WifiManager sWifiManager; private static LocationManager sLocationManager; + private static CountryDetector sCountryDetector; private static final HashMap<File, SharedPreferencesImpl> sSharedPrefs = new HashMap<File, SharedPreferencesImpl>(); @@ -954,6 +957,8 @@ class ContextImpl extends Context { return AccessibilityManager.getInstance(this); } else if (LOCATION_SERVICE.equals(name)) { return getLocationManager(); + } else if (COUNTRY_DETECTOR.equals(name)) { + return getCountryDetector(); } else if (SEARCH_SERVICE.equals(name)) { return getSearchManager(); } else if (SENSOR_SERVICE.equals(name)) { @@ -1120,6 +1125,17 @@ class ContextImpl extends Context { return sLocationManager; } + private CountryDetector getCountryDetector() { + synchronized (sSync) { + if (sCountryDetector == null) { + IBinder b = ServiceManager.getService(COUNTRY_DETECTOR); + ICountryDetector service = ICountryDetector.Stub.asInterface(b); + sCountryDetector = new CountryDetector(service); + } + } + return sCountryDetector; + } + private SearchManager getSearchManager() { synchronized (mSync) { if (mSearchManager == null) { diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index b5ec633ce276..879103ee1168 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -1380,7 +1380,16 @@ public abstract class Context { * @see android.location.LocationManager */ public static final String LOCATION_SERVICE = "location"; - + + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.location.CountryDetector} for detecting the country that + * the user is in. + * + * @hide + */ + public static final String COUNTRY_DETECTOR = "country_detector"; + /** * Use with {@link #getSystemService} to retrieve a {@link * android.app.SearchManager} for handling searches. diff --git a/location/java/android/location/Country.aidl b/location/java/android/location/Country.aidl new file mode 100644 index 000000000000..c83d645aa869 --- /dev/null +++ b/location/java/android/location/Country.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2010, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.location; + +parcelable Country;
\ No newline at end of file diff --git a/location/java/android/location/Country.java b/location/java/android/location/Country.java new file mode 100755 index 000000000000..3c05403a6bc2 --- /dev/null +++ b/location/java/android/location/Country.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.location; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * This class wraps the country information. + * + * @hide + */ +public class Country implements Parcelable { + /** + * The country code came from the mobile network + */ + public static final int COUNTRY_SOURCE_NETWORK = 0; + + /** + * The country code came from the location service + */ + public static final int COUNTRY_SOURCE_LOCATION = 1; + + /** + * The country code was read from the SIM card + */ + public static final int COUNTRY_SOURCE_SIM = 2; + + /** + * The country code came from the system locale setting + */ + public static final int COUNTRY_SOURCE_LOCALE = 3; + + /** + * The ISO 3166-1 two letters country code. + */ + private final String mCountryIso; + + /** + * Where the country code came from. + */ + private final int mSource; + + private int mHashCode; + /** + * + * @param countryIso the ISO 3166-1 two letters country code. + * @param source where the countryIso came from, could be one of below + * values + * <p> + * <ul> + * <li>{@link #COUNTRY_SOURCE_NETWORK}</li> + * <li>{@link #COUNTRY_SOURCE_LOCATION}</li> + * <li>{@link #COUNTRY_SOURCE_SIM}</li> + * <li>{@link #COUNTRY_SOURCE_LOCALE}</li> + * </ul> + */ + public Country(final String countryIso, final int source) { + if (countryIso == null || source < COUNTRY_SOURCE_NETWORK + || source > COUNTRY_SOURCE_LOCALE) { + throw new IllegalArgumentException(); + } + mCountryIso = countryIso.toLowerCase(); + mSource = source; + } + + public Country(Country country) { + mCountryIso = country.mCountryIso; + mSource = country.mSource; + } + + /** + * @return the ISO 3166-1 two letters country code + */ + public final String getCountryIso() { + return mCountryIso; + } + + /** + * @return where the country code came from, could be one of below values + * <p> + * <ul> + * <li>{@link #COUNTRY_SOURCE_NETWORK}</li> + * <li>{@link #COUNTRY_SOURCE_LOCATION}</li> + * <li>{@link #COUNTRY_SOURCE_SIM}</li> + * <li>{@link #COUNTRY_SOURCE_LOCALE}</li> + * </ul> + */ + public final int getSource() { + return mSource; + } + + public static final Parcelable.Creator<Country> CREATOR = new Parcelable.Creator<Country>() { + public Country createFromParcel(Parcel in) { + return new Country(in.readString(), in.readInt()); + } + + public Country[] newArray(int size) { + return new Country[size]; + } + }; + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeString(mCountryIso); + parcel.writeInt(mSource); + } + + @Override + public boolean equals(Object object) { + if (object == this) { + return true; + } + if (object instanceof Country) { + Country c = (Country) object; + return mCountryIso.equals(c.getCountryIso()) && mSource == c.getSource(); + } + return false; + } + + @Override + public int hashCode() { + int hash = mHashCode; + if (hash == 0) { + hash = 17; + hash = hash * 13 + mCountryIso.hashCode(); + hash = hash * 13 + mSource; + mHashCode = hash; + } + return mHashCode; + } + + /** + * Compare the specified country to this country object ignoring the mSource + * field, return true if the countryIso fields are equal + * + * @param country the country to compare + * @return true if the specified country's countryIso field is equal to this + * country's, false otherwise. + */ + public boolean equalsIgnoreSource(Country country) { + return country != null && mCountryIso.equals(country.getCountryIso()); + } +} diff --git a/location/java/android/location/CountryDetector.java b/location/java/android/location/CountryDetector.java new file mode 100644 index 000000000000..0b780cea8407 --- /dev/null +++ b/location/java/android/location/CountryDetector.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.location; + +import java.util.HashMap; + +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.util.Log; + +/** + * This class provides access to the system country detector service. This + * service allows applications to obtain the country that the user is in. + * <p> + * The country will be detected in order of reliability, like + * <ul> + * <li>Mobile network</li> + * <li>Location</li> + * <li>SIM's country</li> + * <li>Phone's locale</li> + * </ul> + * <p> + * Call the {@link #detectCountry()} to get the available country immediately. + * <p> + * To be notified of the future country change, use the + * {@link #addCountryListener} + * <p> + * <p> + * You do not instantiate this class directly; instead, retrieve it through + * {@link android.content.Context#getSystemService + * Context.getSystemService(Context.COUNTRY_DETECTOR)}. + * <p> + * Both ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION permissions are needed. + * + * @hide + */ +public class CountryDetector { + + /** + * The class to wrap the ICountryListener.Stub and CountryListener objects + * together. The CountryListener will be notified through the specific + * looper once the country changed and detected. + */ + private final static class ListenerTransport extends ICountryListener.Stub { + + private final CountryListener mListener; + + private final Handler mHandler; + + public ListenerTransport(CountryListener listener, Looper looper) { + mListener = listener; + if (looper != null) { + mHandler = new Handler(looper); + } else { + mHandler = new Handler(); + } + } + + public void onCountryDetected(final Country country) { + mHandler.post(new Runnable() { + public void run() { + mListener.onCountryDetected(country); + } + }); + } + } + + private final static String TAG = "CountryDetector"; + private final ICountryDetector mService; + private final HashMap<CountryListener, ListenerTransport> mListeners; + + /** + * @hide - hide this constructor because it has a parameter of type + * ICountryDetector, which is a system private class. The right way to + * create an instance of this class is using the factory + * Context.getSystemService. + */ + public CountryDetector(ICountryDetector service) { + mService = service; + mListeners = new HashMap<CountryListener, ListenerTransport>(); + } + + /** + * Start detecting the country that the user is in. + * + * @return the country if it is available immediately, otherwise null will + * be returned. + */ + public Country detectCountry() { + try { + return mService.detectCountry(); + } catch (RemoteException e) { + Log.e(TAG, "detectCountry: RemoteException", e); + return null; + } + } + + /** + * Add a listener to receive the notification when the country is detected + * or changed. + * + * @param listener will be called when the country is detected or changed. + * @param looper a Looper object whose message queue will be used to + * implement the callback mechanism. If looper is null then the + * callbacks will be called on the main thread. + */ + public void addCountryListener(CountryListener listener, Looper looper) { + synchronized (mListeners) { + if (!mListeners.containsKey(listener)) { + ListenerTransport transport = new ListenerTransport(listener, looper); + try { + mService.addCountryListener(transport); + mListeners.put(listener, transport); + } catch (RemoteException e) { + Log.e(TAG, "addCountryListener: RemoteException", e); + } + } + } + } + + /** + * Remove the listener + */ + public void removeCountryListener(CountryListener listener) { + synchronized (mListeners) { + ListenerTransport transport = mListeners.get(listener); + if (transport != null) { + try { + mListeners.remove(listener); + mService.removeCountryListener(transport); + } catch (RemoteException e) { + Log.e(TAG, "removeCountryListener: RemoteException", e); + } + } + } + } +} diff --git a/location/java/android/location/CountryListener.java b/location/java/android/location/CountryListener.java new file mode 100644 index 000000000000..e36db412eaec --- /dev/null +++ b/location/java/android/location/CountryListener.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.location; + +/** + * The listener for receiving the notification when the country is detected or + * changed + * + * @hide + */ +public interface CountryListener { + /** + * @param country the changed or detected country. + */ + void onCountryDetected(Country country); +} diff --git a/location/java/android/location/ICountryDetector.aidl b/location/java/android/location/ICountryDetector.aidl new file mode 100644 index 000000000000..6eaf07ca330d --- /dev/null +++ b/location/java/android/location/ICountryDetector.aidl @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2010, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.location; + +import android.location.Country; +import android.location.ICountryListener; + +/** + * The API for detecting the country where the user is. + * + * {@hide} + */ +interface ICountryDetector +{ + /** + * Start detecting the country that the user is in. + * @return the country if it is available immediately, otherwise null will be returned. + */ + Country detectCountry(); + + /** + * Add a listener to receive the notification when the country is detected or changed. + */ + void addCountryListener(in ICountryListener listener); + + /** + * Remove the listener + */ + void removeCountryListener(in ICountryListener listener); +}
\ No newline at end of file diff --git a/location/java/android/location/ICountryListener.aidl b/location/java/android/location/ICountryListener.aidl new file mode 100644 index 000000000000..76ecb134246f --- /dev/null +++ b/location/java/android/location/ICountryListener.aidl @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2010, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.location; + +import android.location.Country; +/** + * {@hide} + */ +oneway interface ICountryListener +{ + void onCountryDetected(in Country country); +} diff --git a/location/tests/locationtests/src/android/location/CountryTester.java b/location/tests/locationtests/src/android/location/CountryTester.java new file mode 100644 index 000000000000..9802d5a6226d --- /dev/null +++ b/location/tests/locationtests/src/android/location/CountryTester.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.location; + +import android.test.AndroidTestCase; + +public class CountryTester extends AndroidTestCase { + public void testCountryEquals() { + Country countryA = new Country("US", Country.COUNTRY_SOURCE_NETWORK); + Country countryB = new Country("US", Country.COUNTRY_SOURCE_LOCALE); + Country countryC = new Country("CN", Country.COUNTRY_SOURCE_LOCALE); + Country countryD = new Country("us", Country.COUNTRY_SOURCE_NETWORK); + assertTrue(countryA.equalsIgnoreSource(countryB)); + assertFalse(countryA.equalsIgnoreSource(countryC)); + assertFalse(countryA.equals(countryC)); + assertTrue(countryA.equals(countryD)); + assertTrue(countryA.hashCode() == countryD.hashCode()); + } +} diff --git a/services/java/com/android/server/CountryDetectorService.java b/services/java/com/android/server/CountryDetectorService.java new file mode 100644 index 000000000000..3081ebefd05a --- /dev/null +++ b/services/java/com/android/server/CountryDetectorService.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.server; + +import java.util.HashMap; + +import com.android.server.location.ComprehensiveCountryDetector; + +import android.content.Context; +import android.location.Country; +import android.location.CountryListener; +import android.location.ICountryDetector; +import android.location.ICountryListener; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Process; +import android.os.RemoteException; +import android.util.Slog; + +/** + * This class detects the country that the user is in through + * {@link ComprehensiveCountryDetector}. + * + * @hide + */ +public class CountryDetectorService extends ICountryDetector.Stub implements Runnable { + + /** + * The class represents the remote listener, it will also removes itself + * from listener list when the remote process was died. + */ + private final class Receiver implements IBinder.DeathRecipient { + private final ICountryListener mListener; + private final IBinder mKey; + + public Receiver(ICountryListener listener) { + mListener = listener; + mKey = listener.asBinder(); + } + + public void binderDied() { + removeListener(mKey); + } + + @Override + public boolean equals(Object otherObj) { + if (otherObj instanceof Receiver) { + return mKey.equals(((Receiver) otherObj).mKey); + } + return false; + } + + @Override + public int hashCode() { + return mKey.hashCode(); + } + + public ICountryListener getListener() { + return mListener; + } + } + + private final static String TAG = "CountryDetectorService"; + + private final HashMap<IBinder, Receiver> mReceivers; + private final Context mContext; + private ComprehensiveCountryDetector mCountryDetector; + private boolean mSystemReady; + private Handler mHandler; + private CountryListener mLocationBasedDetectorListener; + + public CountryDetectorService(Context context) { + super(); + mReceivers = new HashMap<IBinder, Receiver>(); + mContext = context; + } + + @Override + public Country detectCountry() throws RemoteException { + if (!mSystemReady) { + throw new RemoteException(); + } + return mCountryDetector.detectCountry(); + } + + /** + * Add the ICountryListener into the listener list. + */ + @Override + public void addCountryListener(ICountryListener listener) throws RemoteException { + if (!mSystemReady) { + throw new RemoteException(); + } + addListener(listener); + } + + /** + * Remove the ICountryListener from the listener list. + */ + @Override + public void removeCountryListener(ICountryListener listener) throws RemoteException { + if (!mSystemReady) { + throw new RemoteException(); + } + removeListener(listener.asBinder()); + } + + private void addListener(ICountryListener listener) { + synchronized (mReceivers) { + Receiver r = new Receiver(listener); + try { + listener.asBinder().linkToDeath(r, 0); + mReceivers.put(listener.asBinder(), r); + if (mReceivers.size() == 1) { + Slog.d(TAG, "The first listener is added"); + setCountryListener(mLocationBasedDetectorListener); + } + } catch (RemoteException e) { + Slog.e(TAG, "linkToDeath failed:", e); + } + } + } + + private void removeListener(IBinder key) { + synchronized (mReceivers) { + mReceivers.remove(key); + if (mReceivers.isEmpty()) { + setCountryListener(null); + Slog.d(TAG, "No listener is left"); + } + } + } + + + protected void notifyReceivers(Country country) { + synchronized(mReceivers) { + for (Receiver receiver : mReceivers.values()) { + try { + receiver.getListener().onCountryDetected(country); + } catch (RemoteException e) { + // TODO: Shall we remove the receiver? + Slog.e(TAG, "notifyReceivers failed:", e); + } + } + } + } + + void systemReady() { + // Shall we wait for the initialization finish. + Thread thread = new Thread(this, "CountryDetectorService"); + thread.start(); + } + + private void initialize() { + mCountryDetector = new ComprehensiveCountryDetector(mContext); + mLocationBasedDetectorListener = new CountryListener() { + public void onCountryDetected(final Country country) { + mHandler.post(new Runnable() { + public void run() { + notifyReceivers(country); + } + }); + } + }; + } + + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + Looper.prepare(); + mHandler = new Handler(); + initialize(); + mSystemReady = true; + Looper.loop(); + } + + protected void setCountryListener(final CountryListener listener) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCountryDetector.setCountryListener(listener); + } + }); + } + + // For testing + boolean isSystemReady() { + return mSystemReady; + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 3586d21df75e..2412b7d860a0 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -206,6 +206,7 @@ class ServerThread extends Thread { NotificationManagerService notification = null; WallpaperManagerService wallpaper = null; LocationManagerService location = null; + CountryDetectorService countryDetector = null; if (factoryTest != SystemServer.FACTORY_TEST_LOW_LEVEL) { try { @@ -316,6 +317,14 @@ class ServerThread extends Thread { } try { + Slog.i(TAG, "Country Detector"); + countryDetector = new CountryDetectorService(context); + ServiceManager.addService(Context.COUNTRY_DETECTOR, countryDetector); + } catch (Throwable e) { + Slog.e(TAG, "Failure starting Country Detector", e); + } + + try { Slog.i(TAG, "Search Service"); ServiceManager.addService(Context.SEARCH_SERVICE, new SearchManagerService(context)); @@ -479,6 +488,7 @@ class ServerThread extends Thread { final InputMethodManagerService immF = imm; final RecognitionManagerService recognitionF = recognition; final LocationManagerService locationF = location; + final CountryDetectorService countryDetectorF = countryDetector; // We now tell the activity manager it is okay to run third party // code. It will call back into us once it has gotten to the state @@ -506,6 +516,7 @@ class ServerThread extends Thread { if (wallpaperF != null) wallpaperF.systemReady(); if (immF != null) immF.systemReady(); if (locationF != null) locationF.systemReady(); + if (countryDetectorF != null) countryDetectorF.systemReady(); if (throttleF != null) throttleF.systemReady(); } }); diff --git a/services/java/com/android/server/location/ComprehensiveCountryDetector.java b/services/java/com/android/server/location/ComprehensiveCountryDetector.java new file mode 100755 index 000000000000..e692f8d94bfd --- /dev/null +++ b/services/java/com/android/server/location/ComprehensiveCountryDetector.java @@ -0,0 +1,359 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.location; + +import java.util.Locale; +import java.util.Timer; +import java.util.TimerTask; + +import android.content.Context; +import android.location.Country; +import android.location.CountryListener; +import android.location.Geocoder; +import android.provider.Settings; +import android.telephony.PhoneStateListener; +import android.telephony.ServiceState; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Slog; + +/** + * This class is used to detect the country where the user is. The sources of + * country are queried in order of reliability, like + * <ul> + * <li>Mobile network</li> + * <li>Location</li> + * <li>SIM's country</li> + * <li>Phone's locale</li> + * </ul> + * <p> + * Call the {@link #detectCountry()} to get the available country immediately. + * <p> + * To be notified of the future country change, using the + * {@link #setCountryListener(CountryListener)} + * <p> + * Using the {@link #stop()} to stop listening to the country change. + * <p> + * The country information will be refreshed every + * {@link #LOCATION_REFRESH_INTERVAL} once the location based country is used. + * + * @hide + */ +public class ComprehensiveCountryDetector extends CountryDetectorBase { + + private final static String TAG = "ComprehensiveCountryDetector"; + + /** + * The refresh interval when the location based country was used + */ + private final static long LOCATION_REFRESH_INTERVAL = 1000 * 60 * 60 * 24; // 1 day + + protected CountryDetectorBase mLocationBasedCountryDetector; + protected Timer mLocationRefreshTimer; + + private final int mPhoneType; + private Country mCountry; + private TelephonyManager mTelephonyManager; + private Country mCountryFromLocation; + private boolean mStopped = false; + private ServiceState mLastState; + + private PhoneStateListener mPhoneStateListener = new PhoneStateListener() { + @Override + public void onServiceStateChanged(ServiceState serviceState) { + // TODO: Find out how often we will be notified, if this method is called too + // many times, let's consider querying the network. + Slog.d(TAG, "onServiceStateChanged"); + // We only care the state change + if (mLastState == null || mLastState.getState() != serviceState.getState()) { + detectCountry(true, true); + mLastState = new ServiceState(serviceState); + } + } + }; + + /** + * The listener for receiving the notification from LocationBasedCountryDetector. + */ + private CountryListener mLocationBasedCountryDetectionListener = new CountryListener() { + public void onCountryDetected(Country country) { + mCountryFromLocation = country; + // Don't start the LocationBasedCountryDetector. + detectCountry(true, false); + stopLocationBasedDetector(); + } + }; + + public ComprehensiveCountryDetector(Context context) { + super(context); + mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + mPhoneType = mTelephonyManager.getPhoneType(); + } + + @Override + public Country detectCountry() { + // Don't start the LocationBasedCountryDetector if we have been stopped. + return detectCountry(false, !mStopped); + } + + @Override + public void stop() { + Slog.i(TAG, "Stop the detector."); + cancelLocationRefresh(); + removePhoneStateListener(); + stopLocationBasedDetector(); + mListener = null; + mStopped = true; + } + + /** + * Get the country from different sources in order of the reliability. + */ + private Country getCountry() { + Country result = null; + result = getNetworkBasedCountry(); + if (result == null) { + result = getLastKnownLocationBasedCountry(); + } + if (result == null) { + result = getSimBasedCountry(); + } + if (result == null) { + result = getLocaleCountry(); + } + return result; + } + + /** + * @return the country from the mobile network. + */ + protected Country getNetworkBasedCountry() { + String countryIso = null; + // TODO: The document says the result may be unreliable on CDMA networks. Shall we use + // it on CDMA phone? We may test the Android primarily used countries. + if (mPhoneType == TelephonyManager.PHONE_TYPE_GSM) { + countryIso = mTelephonyManager.getNetworkCountryIso(); + if (!TextUtils.isEmpty(countryIso)) { + return new Country(countryIso, Country.COUNTRY_SOURCE_NETWORK); + } + } + return null; + } + + /** + * @return the cached location based country. + */ + protected Country getLastKnownLocationBasedCountry() { + return mCountryFromLocation; + } + + /** + * @return the country from SIM card + */ + protected Country getSimBasedCountry() { + String countryIso = null; + countryIso = mTelephonyManager.getSimCountryIso(); + if (!TextUtils.isEmpty(countryIso)) { + return new Country(countryIso, Country.COUNTRY_SOURCE_SIM); + } + return null; + } + + /** + * @return the country from the system's locale. + */ + protected Country getLocaleCountry() { + Locale defaultLocale = Locale.getDefault(); + if (defaultLocale != null) { + return new Country(defaultLocale.getCountry(), Country.COUNTRY_SOURCE_LOCALE); + } else { + return null; + } + } + + /** + * @param notifyChange indicates whether the listener should be notified the change of the + * country + * @param startLocationBasedDetection indicates whether the LocationBasedCountryDetector could + * be started if the current country source is less reliable than the location. + * @return the current available UserCountry + */ + private Country detectCountry(boolean notifyChange, boolean startLocationBasedDetection) { + Country country = getCountry(); + runAfterDetectionAsync(mCountry != null ? new Country(mCountry) : mCountry, country, + notifyChange, startLocationBasedDetection); + mCountry = country; + return mCountry; + } + + /** + * Run the tasks in the service's thread. + */ + protected void runAfterDetectionAsync(final Country country, final Country detectedCountry, + final boolean notifyChange, final boolean startLocationBasedDetection) { + mHandler.post(new Runnable() { + public void run() { + runAfterDetection( + country, detectedCountry, notifyChange, startLocationBasedDetection); + } + }); + } + + @Override + public void setCountryListener(CountryListener listener) { + CountryListener prevListener = mListener; + mListener = listener; + if (mListener == null) { + // Stop listening all services + removePhoneStateListener(); + stopLocationBasedDetector(); + cancelLocationRefresh(); + } else if (prevListener == null) { + addPhoneStateListener(); + detectCountry(false, true); + } + } + + void runAfterDetection(final Country country, final Country detectedCountry, + final boolean notifyChange, final boolean startLocationBasedDetection) { + if (notifyChange) { + notifyIfCountryChanged(country, detectedCountry); + } + if (startLocationBasedDetection && (detectedCountry == null + || detectedCountry.getSource() > Country.COUNTRY_SOURCE_LOCATION) + && isAirplaneModeOff() && mListener != null && isGeoCoderImplemented()) { + // Start finding location when the source is less reliable than the + // location and the airplane mode is off (as geocoder will not + // work). + // TODO : Shall we give up starting the detector within a + // period of time? + startLocationBasedDetector(mLocationBasedCountryDetectionListener); + } + if (detectedCountry == null + || detectedCountry.getSource() >= Country.COUNTRY_SOURCE_LOCATION) { + // Schedule the location refresh if the country source is + // not more reliable than the location or no country is + // found. + // TODO: Listen to the preference change of GPS, Wifi etc, + // and start detecting the country. + scheduleLocationRefresh(); + } else { + // Cancel the location refresh once the current source is + // more reliable than the location. + cancelLocationRefresh(); + stopLocationBasedDetector(); + } + } + + /** + * Find the country from LocationProvider. + */ + private synchronized void startLocationBasedDetector(CountryListener listener) { + if (mLocationBasedCountryDetector != null) { + return; + } + mLocationBasedCountryDetector = createLocationBasedCountryDetector(); + mLocationBasedCountryDetector.setCountryListener(listener); + mLocationBasedCountryDetector.detectCountry(); + } + + private synchronized void stopLocationBasedDetector() { + if (mLocationBasedCountryDetector != null) { + mLocationBasedCountryDetector.stop(); + mLocationBasedCountryDetector = null; + } + } + + protected CountryDetectorBase createLocationBasedCountryDetector() { + return new LocationBasedCountryDetector(mContext); + } + + protected boolean isAirplaneModeOff() { + return Settings.System.getInt( + mContext.getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0) == 0; + } + + /** + * Notify the country change. + */ + private void notifyIfCountryChanged(final Country country, final Country detectedCountry) { + if (detectedCountry != null && mListener != null + && (country == null || !country.equals(detectedCountry))) { + Slog.d(TAG, + "The country was changed from " + country != null ? country.getCountryIso() : + country + " to " + detectedCountry.getCountryIso()); + notifyListener(detectedCountry); + } + } + + /** + * Schedule the next location refresh. We will do nothing if the scheduled task exists. + */ + private synchronized void scheduleLocationRefresh() { + if (mLocationRefreshTimer != null) return; + mLocationRefreshTimer = new Timer(); + mLocationRefreshTimer.schedule(new TimerTask() { + @Override + public void run() { + mLocationRefreshTimer = null; + detectCountry(false, true); + } + }, LOCATION_REFRESH_INTERVAL); + } + + /** + * Cancel the scheduled refresh task if it exists + */ + private synchronized void cancelLocationRefresh() { + if (mLocationRefreshTimer != null) { + mLocationRefreshTimer.cancel(); + mLocationRefreshTimer = null; + } + } + + protected synchronized void addPhoneStateListener() { + if (mPhoneStateListener == null && mPhoneType == TelephonyManager.PHONE_TYPE_GSM) { + mLastState = null; + mPhoneStateListener = new PhoneStateListener() { + @Override + public void onServiceStateChanged(ServiceState serviceState) { + // TODO: Find out how often we will be notified, if this + // method is called too + // many times, let's consider querying the network. + Slog.d(TAG, "onServiceStateChanged"); + // We only care the state change + if (mLastState == null || mLastState.getState() != serviceState.getState()) { + detectCountry(true, true); + mLastState = new ServiceState(serviceState); + } + } + }; + mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE); + } + } + + protected synchronized void removePhoneStateListener() { + if (mPhoneStateListener != null) { + mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); + mPhoneStateListener = null; + } + } + + protected boolean isGeoCoderImplemented() { + return Geocoder.isImplemented(); + } +} diff --git a/services/java/com/android/server/location/CountryDetectorBase.java b/services/java/com/android/server/location/CountryDetectorBase.java new file mode 100644 index 000000000000..8326ef949858 --- /dev/null +++ b/services/java/com/android/server/location/CountryDetectorBase.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.location; + +import android.content.Context; +import android.location.Country; +import android.location.CountryListener; +import android.os.Handler; + +/** + * This class defines the methods need to be implemented by the country + * detector. + * <p> + * Calling {@link #detectCountry} to start detecting the country. The country + * could be returned immediately if it is available. + * + * @hide + */ +public abstract class CountryDetectorBase { + protected final Handler mHandler; + protected final Context mContext; + protected CountryListener mListener; + protected Country mDetectedCountry; + + public CountryDetectorBase(Context ctx) { + mContext = ctx; + mHandler = new Handler(); + } + + /** + * Start detecting the country that the user is in. + * + * @return the country if it is available immediately, otherwise null should + * be returned. + */ + public abstract Country detectCountry(); + + /** + * Register a listener to receive the notification when the country is detected or changed. + * <p> + * The previous listener will be replaced if it exists. + */ + public void setCountryListener(CountryListener listener) { + mListener = listener; + } + + /** + * Stop detecting the country. The detector should release all system services and be ready to + * be freed + */ + public abstract void stop(); + + protected void notifyListener(Country country) { + if (mListener != null) { + mListener.onCountryDetected(country); + } + } +} diff --git a/services/java/com/android/server/location/LocationBasedCountryDetector.java b/services/java/com/android/server/location/LocationBasedCountryDetector.java new file mode 100755 index 000000000000..139f05d211fd --- /dev/null +++ b/services/java/com/android/server/location/LocationBasedCountryDetector.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.location; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; + +import android.content.Context; +import android.location.Address; +import android.location.Country; +import android.location.Geocoder; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Bundle; +import android.util.Slog; + +/** + * This class detects which country the user currently is in through the enabled + * location providers and the GeoCoder + * <p> + * Use {@link #detectCountry} to start querying. If the location can not be + * resolved within the given time, the last known location will be used to get + * the user country through the GeoCoder. The IllegalStateException will be + * thrown if there is a ongoing query. + * <p> + * The current query can be stopped by {@link #stop()} + * + * @hide + */ +public class LocationBasedCountryDetector extends CountryDetectorBase { + private final static String TAG = "LocationBasedCountryDetector"; + private final static long QUERY_LOCATION_TIMEOUT = 1000 * 60 * 5; // 5 mins + + /** + * Used for canceling location query + */ + protected Timer mTimer; + + /** + * The thread to query the country from the GeoCoder. + */ + protected Thread mQueryThread; + protected List<LocationListener> mLocationListeners; + + private LocationManager mLocationManager; + private List<String> mEnabledProviders; + + public LocationBasedCountryDetector(Context ctx) { + super(ctx); + mLocationManager = (LocationManager) ctx.getSystemService(Context.LOCATION_SERVICE); + } + + /** + * @return the ISO 3166-1 two letters country code from the location + */ + protected String getCountryFromLocation(Location location) { + String country = null; + Geocoder geoCoder = new Geocoder(mContext); + try { + List<Address> addresses = geoCoder.getFromLocation( + location.getLatitude(), location.getLongitude(), 1); + if (addresses != null && addresses.size() > 0) { + country = addresses.get(0).getCountryCode(); + } + } catch (IOException e) { + Slog.w(TAG, "Exception occurs when getting country from location"); + } + return country; + } + + /** + * Register the listeners with the location providers + */ + protected void registerEnabledProviders(List<LocationListener> listeners) { + int total = listeners.size(); + for (int i = 0; i< total; i++) { + mLocationManager.requestLocationUpdates( + mEnabledProviders.get(i), 0, 0, listeners.get(i)); + } + } + + /** + * Unregister the listeners with the location providers + */ + protected void unregisterProviders(List<LocationListener> listeners) { + for (LocationListener listener : listeners) { + mLocationManager.removeUpdates(listener); + } + } + + /** + * @return the last known location from all providers + */ + protected Location getLastKnownLocation() { + List<String> providers = mLocationManager.getAllProviders(); + Location bestLocation = null; + for (String provider : providers) { + Location lastKnownLocation = mLocationManager.getLastKnownLocation(provider); + if (lastKnownLocation != null) { + if (bestLocation == null || bestLocation.getTime() < lastKnownLocation.getTime()) { + bestLocation = lastKnownLocation; + } + } + } + return bestLocation; + } + + /** + * @return the timeout for querying the location. + */ + protected long getQueryLocationTimeout() { + return QUERY_LOCATION_TIMEOUT; + } + + /** + * @return the total number of enabled location providers + */ + protected int getTotalEnabledProviders() { + if (mEnabledProviders == null) { + mEnabledProviders = mLocationManager.getProviders(true); + } + return mEnabledProviders.size(); + } + + /** + * Start detecting the country. + * <p> + * Queries the location from all location providers, then starts a thread to query the + * country from GeoCoder. + */ + @Override + public synchronized Country detectCountry() { + if (mLocationListeners != null) { + throw new IllegalStateException(); + } + // Request the location from all enabled providers. + int totalProviders = getTotalEnabledProviders(); + if (totalProviders > 0) { + mLocationListeners = new ArrayList<LocationListener>(totalProviders); + for (int i = 0; i < totalProviders; i++) { + LocationListener listener = new LocationListener () { + public void onLocationChanged(Location location) { + if (location != null) { + LocationBasedCountryDetector.this.stop(); + queryCountryCode(location); + } + } + public void onProviderDisabled(String provider) { + } + public void onProviderEnabled(String provider) { + } + public void onStatusChanged(String provider, int status, Bundle extras) { + } + }; + mLocationListeners.add(listener); + } + registerEnabledProviders(mLocationListeners); + mTimer = new Timer(); + mTimer.schedule(new TimerTask() { + @Override + public void run() { + mTimer = null; + LocationBasedCountryDetector.this.stop(); + // Looks like no provider could provide the location, let's try the last + // known location. + queryCountryCode(getLastKnownLocation()); + } + }, getQueryLocationTimeout()); + } else { + // There is no provider enabled. + queryCountryCode(getLastKnownLocation()); + } + return mDetectedCountry; + } + + /** + * Stop the current query without notifying the listener. + */ + @Override + public synchronized void stop() { + if (mLocationListeners != null) { + unregisterProviders(mLocationListeners); + mLocationListeners = null; + } + if (mTimer != null) { + mTimer.cancel(); + mTimer = null; + } + } + + /** + * Start a new thread to query the country from Geocoder. + */ + private synchronized void queryCountryCode(final Location location) { + if (location == null) { + notifyListener(null); + return; + } + if (mQueryThread != null) return; + mQueryThread = new Thread(new Runnable() { + public void run() { + String countryIso = null; + if (location != null) { + countryIso = getCountryFromLocation(location); + } + if (countryIso != null) { + mDetectedCountry = new Country(countryIso, Country.COUNTRY_SOURCE_LOCATION); + } else { + mDetectedCountry = null; + } + notifyListener(mDetectedCountry); + mQueryThread = null; + } + }); + mQueryThread.start(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/CountryDetectorServiceTest.java b/services/tests/servicestests/src/com/android/server/CountryDetectorServiceTest.java new file mode 100644 index 000000000000..17a1585614b1 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/CountryDetectorServiceTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server; + +import android.content.Context; +import android.location.Country; +import android.location.CountryListener; +import android.location.ICountryListener; +import android.os.RemoteException; +import android.test.AndroidTestCase; + +public class CountryDetectorServiceTest extends AndroidTestCase { + private class CountryListenerTester extends ICountryListener.Stub { + private Country mCountry; + + @Override + public void onCountryDetected(Country country) throws RemoteException { + mCountry = country; + } + + public Country getCountry() { + return mCountry; + } + + public boolean isNotified() { + return mCountry != null; + } + } + + private class CountryDetectorServiceTester extends CountryDetectorService { + + private CountryListener mListener; + + public CountryDetectorServiceTester(Context context) { + super(context); + } + + @Override + public void notifyReceivers(Country country) { + super.notifyReceivers(country); + } + + @Override + protected void setCountryListener(final CountryListener listener) { + mListener = listener; + } + + public boolean isListenerSet() { + return mListener != null; + } + } + + public void testAddRemoveListener() throws RemoteException { + CountryDetectorServiceTester serviceTester = new CountryDetectorServiceTester(getContext()); + serviceTester.systemReady(); + waitForSystemReady(serviceTester); + CountryListenerTester listenerTester = new CountryListenerTester(); + serviceTester.addCountryListener(listenerTester); + assertTrue(serviceTester.isListenerSet()); + serviceTester.removeCountryListener(listenerTester); + assertFalse(serviceTester.isListenerSet()); + } + + public void testNotifyListeners() throws RemoteException { + CountryDetectorServiceTester serviceTester = new CountryDetectorServiceTester(getContext()); + CountryListenerTester listenerTesterA = new CountryListenerTester(); + CountryListenerTester listenerTesterB = new CountryListenerTester(); + Country country = new Country("US", Country.COUNTRY_SOURCE_NETWORK); + serviceTester.systemReady(); + waitForSystemReady(serviceTester); + serviceTester.addCountryListener(listenerTesterA); + serviceTester.addCountryListener(listenerTesterB); + serviceTester.notifyReceivers(country); + assertTrue(serviceTester.isListenerSet()); + assertTrue(listenerTesterA.isNotified()); + assertTrue(listenerTesterB.isNotified()); + serviceTester.removeCountryListener(listenerTesterA); + serviceTester.removeCountryListener(listenerTesterB); + assertFalse(serviceTester.isListenerSet()); + } + + private void waitForSystemReady(CountryDetectorService service) { + int count = 5; + while (count-- > 0) { + try { + Thread.sleep(500); + } catch (Exception e) { + } + if (service.isSystemReady()) { + return; + } + } + throw new RuntimeException("Wait System Ready timeout"); + } +} diff --git a/services/tests/servicestests/src/com/android/server/location/ComprehensiveCountryDetectorTest.java b/services/tests/servicestests/src/com/android/server/location/ComprehensiveCountryDetectorTest.java new file mode 100644 index 000000000000..98966c032d14 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/location/ComprehensiveCountryDetectorTest.java @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.location; + +import android.location.Country; +import android.location.CountryListener; +import android.test.AndroidTestCase; + +public class ComprehensiveCountryDetectorTest extends AndroidTestCase { + private class TestCountryDetector extends ComprehensiveCountryDetector { + public final static String COUNTRY_ISO = "us"; + private boolean mLocationBasedDetectorStarted; + private boolean mLocationBasedDetectorStopped; + protected boolean mNotified; + private boolean listenerAdded = false; + + private Country mNotifiedCountry; + public TestCountryDetector() { + super(getContext()); + } + + public void notifyLocationBasedListener(Country country) { + mNotified = true; + mNotifiedCountry = country; + mLocationBasedCountryDetector.notifyListener(country); + } + + public boolean locationBasedDetectorStarted() { + return mLocationBasedCountryDetector != null && mLocationBasedDetectorStarted; + } + + public boolean locationBasedDetectorStopped() { + return mLocationBasedCountryDetector == null && mLocationBasedDetectorStopped; + } + + public boolean locationRefreshStarted() { + return mLocationRefreshTimer != null; + } + + public boolean locationRefreshCancelled() { + return mLocationRefreshTimer == null; + } + + @Override + protected CountryDetectorBase createLocationBasedCountryDetector() { + return new CountryDetectorBase(mContext) { + @Override + public Country detectCountry() { + mLocationBasedDetectorStarted = true; + return null; + } + + @Override + public void stop() { + mLocationBasedDetectorStopped = true; + } + }; + } + + @Override + protected Country getNetworkBasedCountry() { + return null; + } + + @Override + protected Country getLastKnownLocationBasedCountry() { + return mNotifiedCountry; + } + + @Override + protected Country getSimBasedCountry() { + return null; + } + + @Override + protected Country getLocaleCountry() { + return null; + } + + @Override + protected void runAfterDetectionAsync(final Country country, final Country detectedCountry, + final boolean notifyChange, final boolean startLocationBasedDetection) { + runAfterDetection(country, detectedCountry, notifyChange, startLocationBasedDetection); + }; + + @Override + protected boolean isAirplaneModeOff() { + return true; + } + + @Override + protected synchronized void addPhoneStateListener() { + listenerAdded = true; + } + + @Override + protected synchronized void removePhoneStateListener() { + listenerAdded = false; + } + + @Override + protected boolean isGeoCoderImplemented() { + return true; + } + + public boolean isPhoneStateListenerAdded() { + return listenerAdded; + } + } + + private class CountryListenerImpl implements CountryListener { + private boolean mNotified; + private Country mCountry; + + public void onCountryDetected(Country country) { + mNotified = true; + mCountry = country; + } + + public boolean notified() { + return mNotified; + } + + public Country getCountry() { + return mCountry; + } + } + + public void testDetectNetworkBasedCountry() { + final Country resultCountry = new Country( + TestCountryDetector.COUNTRY_ISO, Country.COUNTRY_SOURCE_NETWORK); + TestCountryDetector countryDetector = new TestCountryDetector() { + @Override + protected Country getNetworkBasedCountry() { + return resultCountry; + } + }; + CountryListenerImpl listener = new CountryListenerImpl(); + countryDetector.setCountryListener(listener); + Country country = countryDetector.detectCountry(); + assertTrue(sameCountry(country, resultCountry)); + assertFalse(listener.notified()); + assertFalse(countryDetector.locationBasedDetectorStarted()); + assertFalse(countryDetector.locationRefreshStarted()); + countryDetector.stop(); + } + + public void testDetectLocationBasedCountry() { + final Country resultCountry = new Country( + TestCountryDetector.COUNTRY_ISO, Country.COUNTRY_SOURCE_SIM); + final Country locationBasedCountry = new Country( + TestCountryDetector.COUNTRY_ISO, Country.COUNTRY_SOURCE_LOCATION); + TestCountryDetector countryDetector = new TestCountryDetector() { + @Override + protected Country getSimBasedCountry() { + return resultCountry; + } + }; + CountryListenerImpl listener = new CountryListenerImpl(); + countryDetector.setCountryListener(listener); + Country country = countryDetector.detectCountry(); + assertTrue(sameCountry(country, resultCountry)); + assertTrue(countryDetector.locationBasedDetectorStarted()); + countryDetector.notifyLocationBasedListener(locationBasedCountry); + assertTrue(listener.notified()); + assertTrue(sameCountry(listener.getCountry(), locationBasedCountry)); + assertTrue(countryDetector.locationBasedDetectorStopped()); + assertTrue(countryDetector.locationRefreshStarted()); + countryDetector.stop(); + assertTrue(countryDetector.locationRefreshCancelled()); + } + + public void testLocaleBasedCountry() { + final Country resultCountry = new Country( + TestCountryDetector.COUNTRY_ISO, Country.COUNTRY_SOURCE_LOCALE); + TestCountryDetector countryDetector = new TestCountryDetector() { + @Override + protected Country getLocaleCountry() { + return resultCountry; + } + }; + CountryListenerImpl listener = new CountryListenerImpl(); + countryDetector.setCountryListener(listener); + Country country = countryDetector.detectCountry(); + assertTrue(sameCountry(country, resultCountry)); + assertFalse(listener.notified()); + assertTrue(countryDetector.locationBasedDetectorStarted()); + assertTrue(countryDetector.locationRefreshStarted()); + countryDetector.stop(); + assertTrue(countryDetector.locationRefreshCancelled()); + } + + public void testStoppingDetector() { + // Test stopping detector when LocationBasedCountryDetector was started + final Country resultCountry = new Country( + TestCountryDetector.COUNTRY_ISO, Country.COUNTRY_SOURCE_SIM); + TestCountryDetector countryDetector = new TestCountryDetector() { + @Override + protected Country getSimBasedCountry() { + return resultCountry; + } + }; + CountryListenerImpl listener = new CountryListenerImpl(); + countryDetector.setCountryListener(listener); + Country country = countryDetector.detectCountry(); + assertTrue(sameCountry(country, resultCountry)); + assertTrue(countryDetector.locationBasedDetectorStarted()); + countryDetector.stop(); + // The LocationBasedDetector should be stopped. + assertTrue(countryDetector.locationBasedDetectorStopped()); + // The location refresh should not running. + assertTrue(countryDetector.locationRefreshCancelled()); + } + + public void testLocationBasedCountryNotFound() { + final Country resultCountry = new Country( + TestCountryDetector.COUNTRY_ISO, Country.COUNTRY_SOURCE_SIM); + TestCountryDetector countryDetector = new TestCountryDetector() { + @Override + protected Country getSimBasedCountry() { + return resultCountry; + } + }; + CountryListenerImpl listener = new CountryListenerImpl(); + countryDetector.setCountryListener(listener); + Country country = countryDetector.detectCountry(); + assertTrue(sameCountry(country, resultCountry)); + assertTrue(countryDetector.locationBasedDetectorStarted()); + countryDetector.notifyLocationBasedListener(null); + assertFalse(listener.notified()); + assertTrue(sameCountry(listener.getCountry(), null)); + assertTrue(countryDetector.locationBasedDetectorStopped()); + assertTrue(countryDetector.locationRefreshStarted()); + countryDetector.stop(); + assertTrue(countryDetector.locationRefreshCancelled()); + } + + public void testNoCountryFound() { + TestCountryDetector countryDetector = new TestCountryDetector(); + CountryListenerImpl listener = new CountryListenerImpl(); + countryDetector.setCountryListener(listener); + Country country = countryDetector.detectCountry(); + assertTrue(sameCountry(country, null)); + assertTrue(countryDetector.locationBasedDetectorStarted()); + countryDetector.notifyLocationBasedListener(null); + assertFalse(listener.notified()); + assertTrue(sameCountry(listener.getCountry(), null)); + assertTrue(countryDetector.locationBasedDetectorStopped()); + assertTrue(countryDetector.locationRefreshStarted()); + countryDetector.stop(); + assertTrue(countryDetector.locationRefreshCancelled()); + } + + public void testAddRemoveListener() { + TestCountryDetector countryDetector = new TestCountryDetector(); + CountryListenerImpl listener = new CountryListenerImpl(); + countryDetector.setCountryListener(listener); + assertTrue(countryDetector.isPhoneStateListenerAdded()); + assertTrue(countryDetector.locationBasedDetectorStarted()); + countryDetector.setCountryListener(null); + assertFalse(countryDetector.isPhoneStateListenerAdded()); + assertTrue(countryDetector.locationBasedDetectorStopped()); + } + + public void testGeocoderNotImplemented() { + TestCountryDetector countryDetector = new TestCountryDetector() { + @Override + protected boolean isGeoCoderImplemented() { + return false; + } + }; + CountryListenerImpl listener = new CountryListenerImpl(); + countryDetector.setCountryListener(listener); + assertTrue(countryDetector.isPhoneStateListenerAdded()); + assertFalse(countryDetector.locationBasedDetectorStarted()); + countryDetector.setCountryListener(null); + assertFalse(countryDetector.isPhoneStateListenerAdded()); + } + + private boolean sameCountry(Country country1, Country country2) { + return country1 == null && country2 == null || country1 != null && country2 != null && + country1.getCountryIso().equalsIgnoreCase(country2.getCountryIso()) && + country1.getSource() == country2.getSource(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/location/LocationBasedCountryDetectorTest.java b/services/tests/servicestests/src/com/android/server/location/LocationBasedCountryDetectorTest.java new file mode 100755 index 000000000000..71e8e2a62623 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/location/LocationBasedCountryDetectorTest.java @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.server.location; + +import java.util.ArrayList; +import java.util.List; +import java.util.Timer; + +import android.location.Country; +import android.location.CountryListener; +import android.location.Location; +import android.location.LocationListener; +import android.test.AndroidTestCase; + +public class LocationBasedCountryDetectorTest extends AndroidTestCase { + private class TestCountryDetector extends LocationBasedCountryDetector { + public static final int TOTAL_PROVIDERS = 2; + protected Object countryFoundLocker = new Object(); + protected boolean notifyCountry = false; + private final Location mLocation; + private final String mCountry; + private final long mQueryLocationTimeout; + private List<LocationListener> mListeners; + + public TestCountryDetector(String country, String provider) { + this(country, provider, 1000 * 60 * 5); + } + + public TestCountryDetector(String country, String provider, long queryLocationTimeout) { + super(getContext()); + mCountry = country; + mLocation = new Location(provider); + mQueryLocationTimeout = queryLocationTimeout; + mListeners = new ArrayList<LocationListener>(); + } + + @Override + protected String getCountryFromLocation(Location location) { + synchronized (countryFoundLocker) { + if (!notifyCountry) { + try { + countryFoundLocker.wait(); + } catch (InterruptedException e) { + } + } + } + if (mLocation.getProvider().endsWith(location.getProvider())) { + return mCountry; + } else { + return null; + } + } + + @Override + protected Location getLastKnownLocation() { + return mLocation; + } + + @Override + protected void registerEnabledProviders(List<LocationListener> listeners) { + mListeners.addAll(listeners); + } + + @Override + protected void unregisterProviders(List<LocationListener> listeners) { + for (LocationListener listener : mLocationListeners) { + assertTrue(mListeners.remove(listener)); + } + } + + @Override + protected long getQueryLocationTimeout() { + return mQueryLocationTimeout; + } + + @Override + protected int getTotalEnabledProviders() { + return TOTAL_PROVIDERS; + } + + public void notifyLocationFound() { + // Listener could be removed in the notification. + LocationListener[] listeners = new LocationListener[mListeners.size()]; + mLocationListeners.toArray(listeners); + for (LocationListener listener :listeners) { + listener.onLocationChanged(mLocation); + } + } + + public int getListenersCount() { + return mListeners.size(); + } + + public void notifyCountryFound() { + synchronized (countryFoundLocker) { + notifyCountry = true; + countryFoundLocker.notify(); + } + } + + public Timer getTimer() { + return mTimer; + } + + public Thread getQueryThread() { + return mQueryThread; + } + } + + private class CountryListenerImpl implements CountryListener { + private boolean mNotified; + private String mCountryCode; + public void onCountryDetected(Country country) { + mNotified = true; + if (country != null) { + mCountryCode = country.getCountryIso(); + } + } + + public boolean notified() { + return mNotified; + } + + public String getCountry() { + return mCountryCode; + } + } + + public void testFindingCountry() { + final String country = "us"; + final String provider = "Good"; + CountryListenerImpl countryListener = new CountryListenerImpl(); + TestCountryDetector detector = new TestCountryDetector(country, provider); + detector.setCountryListener(countryListener); + detector.detectCountry(); + assertEquals(detector.getListenersCount(), TestCountryDetector.TOTAL_PROVIDERS); + detector.notifyLocationFound(); + // All listeners should be unregistered + assertEquals(detector.getListenersCount(), 0); + assertNull(detector.getTimer()); + Thread queryThread = waitForQueryThreadLaunched(detector); + detector.notifyCountryFound(); + // Wait for query thread ending + waitForThreadEnding(queryThread); + // QueryThread should be set to NULL + assertNull(detector.getQueryThread()); + assertTrue(countryListener.notified()); + assertEquals(countryListener.getCountry(), country); + } + + public void testFindingCountryCancelled() { + final String country = "us"; + final String provider = "Good"; + CountryListenerImpl countryListener = new CountryListenerImpl(); + TestCountryDetector detector = new TestCountryDetector(country, provider); + detector.setCountryListener(countryListener); + detector.detectCountry(); + assertEquals(detector.getListenersCount(), TestCountryDetector.TOTAL_PROVIDERS); + detector.notifyLocationFound(); + // All listeners should be unregistered + assertEquals(detector.getListenersCount(), 0); + // The time should be stopped + assertNull(detector.getTimer()); + Thread queryThread = waitForQueryThreadLaunched(detector); + detector.stop(); + // There is no way to stop the thread, let's test it could be stopped, after get country + detector.notifyCountryFound(); + // Wait for query thread ending + waitForThreadEnding(queryThread); + // QueryThread should be set to NULL + assertNull(detector.getQueryThread()); + assertTrue(countryListener.notified()); + assertEquals(countryListener.getCountry(), country); + } + + public void testFindingLocationCancelled() { + final String country = "us"; + final String provider = "Good"; + CountryListenerImpl countryListener = new CountryListenerImpl(); + TestCountryDetector detector = new TestCountryDetector(country, provider); + detector.setCountryListener(countryListener); + detector.detectCountry(); + assertEquals(detector.getListenersCount(), TestCountryDetector.TOTAL_PROVIDERS); + detector.stop(); + // All listeners should be unregistered + assertEquals(detector.getListenersCount(), 0); + // The time should be stopped + assertNull(detector.getTimer()); + // QueryThread should still be NULL + assertNull(detector.getQueryThread()); + assertFalse(countryListener.notified()); + } + + public void testFindingLocationFailed() { + final String country = "us"; + final String provider = "Good"; + long timeout = 1000; + TestCountryDetector detector = new TestCountryDetector(country, provider, timeout) { + @Override + protected Location getLastKnownLocation() { + return null; + } + }; + CountryListenerImpl countryListener = new CountryListenerImpl(); + detector.setCountryListener(countryListener); + detector.detectCountry(); + assertEquals(detector.getListenersCount(), TestCountryDetector.TOTAL_PROVIDERS); + waitForTimerReset(detector); + // All listeners should be unregistered + assertEquals(detector.getListenersCount(), 0); + // QueryThread should still be NULL + assertNull(detector.getQueryThread()); + assertTrue(countryListener.notified()); + assertNull(countryListener.getCountry()); + } + + public void testFindingCountryFailed() { + final String country = "us"; + final String provider = "Good"; + TestCountryDetector detector = new TestCountryDetector(country, provider) { + @Override + protected String getCountryFromLocation(Location location) { + synchronized (countryFoundLocker) { + if (! notifyCountry) { + try { + countryFoundLocker.wait(); + } catch (InterruptedException e) { + } + } + } + // We didn't find country. + return null; + } + }; + CountryListenerImpl countryListener = new CountryListenerImpl(); + detector.setCountryListener(countryListener); + detector.detectCountry(); + assertEquals(detector.getListenersCount(), TestCountryDetector.TOTAL_PROVIDERS); + detector.notifyLocationFound(); + // All listeners should be unregistered + assertEquals(detector.getListenersCount(), 0); + assertNull(detector.getTimer()); + Thread queryThread = waitForQueryThreadLaunched(detector); + detector.notifyCountryFound(); + // Wait for query thread ending + waitForThreadEnding(queryThread); + // QueryThread should be set to NULL + assertNull(detector.getQueryThread()); + // CountryListener should be notified + assertTrue(countryListener.notified()); + assertNull(countryListener.getCountry()); + } + + public void testFindingCountryWithLastKnownLocation() { + final String country = "us"; + final String provider = "Good"; + long timeout = 1000; + TestCountryDetector detector = new TestCountryDetector(country, provider, timeout); + CountryListenerImpl countryListener = new CountryListenerImpl(); + detector.setCountryListener(countryListener); + detector.detectCountry(); + assertEquals(detector.getListenersCount(), TestCountryDetector.TOTAL_PROVIDERS); + waitForTimerReset(detector); + // All listeners should be unregistered + assertEquals(detector.getListenersCount(), 0); + Thread queryThread = waitForQueryThreadLaunched(detector); + detector.notifyCountryFound(); + // Wait for query thread ending + waitForThreadEnding(queryThread); + // QueryThread should be set to NULL + assertNull(detector.getQueryThread()); + // CountryListener should be notified + assertTrue(countryListener.notified()); + assertEquals(countryListener.getCountry(), country); + } + + private void waitForTimerReset(TestCountryDetector detector) { + int count = 5; + long interval = 1000; + try { + while (count-- > 0 && detector.getTimer() != null) { + Thread.sleep(interval); + } + } catch (InterruptedException e) { + } + Timer timer = detector.getTimer(); + assertTrue(timer == null); + } + + private void waitForThreadEnding(Thread thread) { + try { + thread.join(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + private Thread waitForQueryThreadLaunched(TestCountryDetector detector) { + int count = 5; + long interval = 1000; + try { + while (count-- > 0 && detector.getQueryThread() == null) { + Thread.sleep(interval); + } + } catch (InterruptedException e) { + } + Thread thread = detector.getQueryThread(); + assertTrue(thread != null); + return thread; + } +} |