diff options
author | Neil Fuller <nfuller@google.com> | 2020-12-14 20:59:15 +0000 |
---|---|---|
committer | Neil Fuller <nfuller@google.com> | 2020-12-15 22:19:39 +0000 |
commit | 37d8e591073bac94199cec3ca97e86e79f82fe2b (patch) | |
tree | 788765416680c7c1291e84d570e3f55c067428fa | |
parent | 74199467ae8a5858b14e58e8d49306fb67746326 (diff) | |
download | GeoTZ-37d8e591073bac94199cec3ca97e86e79f82fe2b.tar.gz |
Switch to android.service APIs
Switch the OfflineTimeZoneProvider to new, cleaner System APIs.
Bug: 175633818
Test: build / treehugger
Change-Id: If4a772e6063a464bd7166e6cd08653ea7485c330
10 files changed, 381 insertions, 308 deletions
diff --git a/apex/com.android.geotz/Android.bp b/apex/com.android.geotz/Android.bp index aec1a84..6fad0fa 100644 --- a/apex/com.android.geotz/Android.bp +++ b/apex/com.android.geotz/Android.bp @@ -62,7 +62,7 @@ java_library { static_libs: [ "offlinelocationtimezoneprovider", ], - sdk_version: "current", + sdk_version: "system_current", apex_available: [ "com.android.geotz", ], diff --git a/locationtzprovider/Android.bp b/locationtzprovider/Android.bp index c25e887..9c3c4b5 100644 --- a/locationtzprovider/Android.bp +++ b/locationtzprovider/Android.bp @@ -17,10 +17,9 @@ java_library { name: "offlinelocationtimezoneprovider", srcs: ["src/main/java/**/*.java"], - sdk_version: "current", + sdk_version: "system_current", libs: [ "androidx.annotation_annotation", - "com.android.location.provider", ], static_libs: [ "geotz_lookup", @@ -35,10 +34,7 @@ android_test { name: "OfflineLocationTimeZoneProviderTests", srcs: ["src/test/java/**/*.java"], manifest: "src/test/AndroidManifest.xml", - sdk_version: "current", - libs: [ - "com.android.location.provider", - ], + sdk_version: "system_current", static_libs: [ "androidx.test.runner", "junit", diff --git a/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/EnvironmentImpl.java b/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/EnvironmentImpl.java index 60fc294..a29078d 100644 --- a/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/EnvironmentImpl.java +++ b/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/EnvironmentImpl.java @@ -32,11 +32,11 @@ import android.os.SystemClock; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.location.timezone.provider.LocationTimeZoneEventUnbundled; import com.android.timezone.geotz.lookup.GeoTimeZonesFinder; import com.android.timezone.geotz.provider.core.Cancellable; import com.android.timezone.geotz.provider.core.OfflineLocationTimeZoneDelegate; import com.android.timezone.geotz.provider.core.OfflineLocationTimeZoneDelegate.ListenModeEnum; +import com.android.timezone.geotz.provider.core.TimeZoneProviderResult; import java.io.File; import java.io.IOException; @@ -63,15 +63,14 @@ class EnvironmentImpl implements OfflineLocationTimeZoneDelegate.Environment { @NonNull private final Handler mHandler; @NonNull - private final Consumer<LocationTimeZoneEventUnbundled> mEventConsumer; + private final Consumer<TimeZoneProviderResult> mResultConsumer; @NonNull private final File mGeoDataFile; - EnvironmentImpl( - @NonNull Context context, - @NonNull Consumer<LocationTimeZoneEventUnbundled> eventConsumer) { + EnvironmentImpl(@NonNull Context context, + @NonNull Consumer<TimeZoneProviderResult> resultConsumer) { mLocationManager = context.getSystemService(LocationManager.class); - mEventConsumer = Objects.requireNonNull(eventConsumer); + mResultConsumer = Objects.requireNonNull(resultConsumer); mHandler = new Handler(Looper.getMainLooper()); Properties configProperties = loadConfigProperties(getClass().getClassLoader()); @@ -97,7 +96,7 @@ class EnvironmentImpl implements OfflineLocationTimeZoneDelegate.Environment { @Override @NonNull public <T> Cancellable startTimeout(@Nullable Consumer<T> callback, @NonNull T callbackToken, - @NonNull long delayMillis) { + long delayMillis) { // Deliberate use of an anonymous class as the equality of lambdas is not well defined but // instance equality is required for the remove call. @@ -213,8 +212,8 @@ class EnvironmentImpl implements OfflineLocationTimeZoneDelegate.Environment { } @Override - public void reportLocationTimeZoneEvent(@NonNull LocationTimeZoneEventUnbundled event) { - mEventConsumer.accept(event); + public void reportTimeZoneProviderResult(@NonNull TimeZoneProviderResult result) { + mResultConsumer.accept(result); } @Override diff --git a/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/OfflineLocationTimeZoneProvider.java b/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/OfflineLocationTimeZoneProvider.java deleted file mode 100644 index a11e667..0000000 --- a/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/OfflineLocationTimeZoneProvider.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2020 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.timezone.geotz.provider; - -import android.content.Context; - -import androidx.annotation.NonNull; - -import com.android.location.timezone.provider.LocationTimeZoneProviderBase; -import com.android.location.timezone.provider.LocationTimeZoneProviderRequestUnbundled; -import com.android.timezone.geotz.provider.core.LogUtils; -import com.android.timezone.geotz.provider.core.OfflineLocationTimeZoneDelegate; -import com.android.timezone.geotz.provider.core.OfflineLocationTimeZoneDelegate.Environment; - -import java.io.PrintWriter; - -/** - * A Location Time Zone Provider implementation that uses only Android public SDK APIs and on-device - * data to determine the current time zone(s) for a location. - * - * <p>This implementation can be deployed to run as either the current user (e.g. it could run in a - * user-specific process like GMS core), or always as the system user (e.g. it can run in a single - * user process like system server). It relies on being disabled by the system server when the user - * changes. No location state is retained when the provider is disabled. - * - * <p>See {@link OfflineLocationTimeZoneDelegate} for implementation details. - * - * <p>The provider is configured via a "offlineltzprovider.properties" resource file. See - * {@link EnvironmentImpl} for details. - */ -final class OfflineLocationTimeZoneProvider extends LocationTimeZoneProviderBase { - - private static final String ATTRIBUTION_TAG = "OfflineLocationTimeZoneProvider"; - - @NonNull - private final OfflineLocationTimeZoneDelegate mDelegate; - - OfflineLocationTimeZoneProvider(@NonNull Context context) { - super(context, LogUtils.LOG_TAG); - Context attributionContext = context.createAttributionContext(ATTRIBUTION_TAG); - Environment environment = new EnvironmentImpl( - attributionContext, this::reportLocationTimeZoneEvent); - mDelegate = new OfflineLocationTimeZoneDelegate(environment); - } - - public void onBind() { - mDelegate.onBind(); - } - - public void onDestroy() { - mDelegate.onDestroy(); - } - - @Override - protected void onSetRequest(@NonNull LocationTimeZoneProviderRequestUnbundled request) { - if (request.getReportLocationTimeZone()) { - mDelegate.onEnable(request.getInitializationTimeoutMillis()); - } else { - mDelegate.onDisable(); - } - } - - public void dump(@NonNull PrintWriter pw) { - mDelegate.dump(pw); - } - -}
\ No newline at end of file diff --git a/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/OfflineLocationTimeZoneProviderService.java b/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/OfflineLocationTimeZoneProviderService.java new file mode 100644 index 0000000..d108f3c --- /dev/null +++ b/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/OfflineLocationTimeZoneProviderService.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2020 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.timezone.geotz.provider; + +import android.content.Context; +import android.service.timezone.TimeZoneProviderService; + +import androidx.annotation.NonNull; + +import com.android.timezone.geotz.provider.core.OfflineLocationTimeZoneDelegate; +import com.android.timezone.geotz.provider.core.OfflineLocationTimeZoneDelegate.Environment; +import com.android.timezone.geotz.provider.core.TimeZoneProviderResult; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * A Location Time Zone Provider implementation that uses only Android public SDK APIs and on-device + * data to determine the current time zone(s) for a location. + * + * <p>This implementation can be deployed to run as either the current user (e.g. it could run in a + * user-specific process like GMS core), or always as the system user (e.g. it can run in a single + * user process like system server). It relies on being stopped by the system server when the user + * changes. No location state is retained when the provider is stopped. + * + * <p>See {@link OfflineLocationTimeZoneDelegate} for implementation details. + * + * <p>The provider is configured via a "offlineltzprovider.properties" resource file. See + * {@link EnvironmentImpl} for details. + */ +public final class OfflineLocationTimeZoneProviderService extends TimeZoneProviderService { + + private static final String ATTRIBUTION_TAG = "OfflineLocationTimeZoneProviderService"; + + // NonNull after onCreate() + private OfflineLocationTimeZoneDelegate mDelegate; + + @Override + public void onCreate() { + Context attributionContext = createAttributionContext(ATTRIBUTION_TAG); + Environment environment = new EnvironmentImpl( + attributionContext, this::reportTimeZoneProviderEvent); + mDelegate = new OfflineLocationTimeZoneDelegate(environment); + } + + @Override + public void onStartUpdates(long initializationTimeoutMillis) { + mDelegate.onStartUpdates(initializationTimeoutMillis); + } + + private void reportTimeZoneProviderEvent( + @NonNull TimeZoneProviderResult timeZoneProviderResult) { + switch (timeZoneProviderResult.getType()) { + case TimeZoneProviderResult.RESULT_TYPE_SUGGESTION: { + reportSuggestion(timeZoneProviderResult.getSuggestion()); + break; + } + case TimeZoneProviderResult.RESULT_TYPE_UNCERTAIN: { + reportUncertain(); + break; + } + case TimeZoneProviderResult.RESULT_TYPE_PERMANENT_FAILURE: { + reportPermanentFailure(timeZoneProviderResult.getFailureCause()); + break; + } + default: { + throw new IllegalArgumentException("Unknown result type=" + timeZoneProviderResult); + } + } + } + + @Override + public void onStopUpdates() { + mDelegate.onStopUpdates(); + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + mDelegate.dump(writer); + } +}
\ No newline at end of file diff --git a/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/OfflineLocationTimeZoneService.java b/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/OfflineLocationTimeZoneService.java deleted file mode 100644 index fe4f1e6..0000000 --- a/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/OfflineLocationTimeZoneService.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 2020 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.timezone.geotz.provider; - -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; - -import androidx.annotation.Nullable; - -import java.io.FileDescriptor; -import java.io.PrintWriter; - -/** - * The service that provides the {@link OfflineLocationTimeZoneProvider}. An instance of this - * service is discovered via configuration in an {@code AndroidManifest.xml}. - * - * <p>See {@link com.android.server.location.timezone.LocationTimeZoneManagerService} for the server - * component that binds to it and how it is discovered. - * - * <p>See {@link com.android.server.ServiceWatcher} for how to control how the service is treated - * and how the server resolves to a single service if there are multiple available. - */ -public final class OfflineLocationTimeZoneService extends Service { - - private final Object mLock = new Object(); - @Nullable - private OfflineLocationTimeZoneProvider mProvider; - - @Override - public IBinder onBind(Intent intent) { - synchronized (mLock) { - if (mProvider == null) { - mProvider = new OfflineLocationTimeZoneProvider(this); - mProvider.onBind(); - } - return mProvider.getBinder(); - } - } - - @Override - public void onDestroy() { - synchronized (mLock) { - if (mProvider != null) { - mProvider.onDestroy(); - mProvider = null; - } - } - } - - @Override - protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { - synchronized (mLock) { - if (mProvider != null) { - mProvider.dump(writer); - } - } - } -} diff --git a/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/core/Mode.java b/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/core/Mode.java index ad0f02b..dcf4621 100644 --- a/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/core/Mode.java +++ b/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/core/Mode.java @@ -42,31 +42,31 @@ import java.util.Objects; * <p>See the docs for each {@code MODE_} constant for an explanation of each mode. * * <pre> - * The initial mode is {@link #MODE_DISABLED}. + * The initial mode is {@link #MODE_STOPPED}. * * Valid transitions: * - * {@link #MODE_DISABLED} - * -> {@link #MODE_ENABLED}(1) - * - when the LTZP first receives an enabled request it starts listening for the current + * {@link #MODE_STOPPED} + * -> {@link #MODE_STARTED}(1) + * - when the LTZP first receives an started request it starts listening for the current * location with {@link OfflineLocationTimeZoneDelegate#LOCATION_LISTEN_MODE_HIGH}. - * {@link #MODE_ENABLED}(1) - * -> {@link #MODE_ENABLED}(2) + * {@link #MODE_STARTED}(1) + * -> {@link #MODE_STARTED}(2) * - when the LTZP receives a valid location, or if it is unable to determine the current * location within the mode timeout, it moves to {@link * OfflineLocationTimeZoneDelegate#LOCATION_LISTEN_MODE_LOW} - * -> {@link #MODE_DISABLED} - * - when the system server sends a "disabled" request the LTZP is disabled. - * {@link #MODE_ENABLED}(2) - * -> {@link #MODE_ENABLED}(1) + * -> {@link #MODE_STOPPED} + * - when the system server sends a "stopped" request the LTZP is stopped. + * {@link #MODE_STARTED}(2) + * -> {@link #MODE_STARTED}(1) * - when no valid location has been received within the mode timeout, the LTZP will start * listening for the current location using {@link * OfflineLocationTimeZoneDelegate#LOCATION_LISTEN_MODE_HIGH}. - * -> {@link #MODE_ENABLED}(2) + * -> {@link #MODE_STARTED}(2) * - when the LTZP receives a valid location, it stays in {@link * OfflineLocationTimeZoneDelegate#LOCATION_LISTEN_MODE_LOW} - * -> {@link #MODE_DISABLED} - * - when the system server sends a "disabled" request the LTZP is disabled. + * -> {@link #MODE_STOPPED} + * - when the system server sends a "stopped" request the LTZP is stopped. * * {All states} * -> {@link #MODE_FAILED} (terminal state) @@ -78,21 +78,21 @@ import java.util.Objects; */ class Mode { - @IntDef({ MODE_DISABLED, MODE_ENABLED, MODE_FAILED, MODE_DESTROYED }) + @IntDef({ MODE_STOPPED, MODE_STARTED, MODE_FAILED, MODE_DESTROYED }) @interface ModeEnum {} /** * An inactive state. The LTZP may not have received a request yet, or it has and the LTZP has - * been explicitly disabled. + * been explicitly stopped. */ @ModeEnum - static final int MODE_DISABLED = 1; + static final int MODE_STOPPED = 1; /** - * The LTZP has been enabled by the system server, and is listening for the current location. + * The LTZP has been started by the system server, and is listening for the current location. */ @ModeEnum - static final int MODE_ENABLED = 2; + static final int MODE_STARTED = 2; /** * The LTZP's service has been destroyed. @@ -111,7 +111,7 @@ class Mode { final int mModeEnum; /** - * The current location listen mode. Only used when mModeEnum == {@link #MODE_ENABLED}. + * The current location listen mode. Only used when mModeEnum == {@link #MODE_STARTED}. */ final @ListenModeEnum int mListenMode; @@ -134,21 +134,21 @@ class Mode { private final String mEntryCause; /** - * Used when mModeEnum == {@link #MODE_ENABLED}. The {@link Cancellable} that can be + * Used when mModeEnum == {@link #MODE_STARTED}. The {@link Cancellable} that can be * used to stop listening for the current location. */ @Nullable private Cancellable mLocationListenerCancellable; /** - * Used when mModeEnum == {@link #MODE_ENABLED}. The {@link Cancellable} that can be + * Used when mModeEnum == {@link #MODE_STARTED}. The {@link Cancellable} that can be * used to stop listening for the current location. */ @Nullable private Cancellable mTimeoutCancellable; /** - * Used when mModeEnum == {@link #MODE_ENABLED} to record the token associated with the + * Used when mModeEnum == {@link #MODE_STARTED} to record the token associated with the * mode timeout. */ @Nullable @@ -169,15 +169,15 @@ class Mode { mEntryCause = entryCause; } - /** Returns the disabled mode which is the starting state for a provider. */ + /** Returns the stopped mode which is the starting state for a provider. */ @NonNull - static Mode createDisabledMode() { - return new Mode(MODE_DISABLED, "init" /* entryCause */); + static Mode createStoppedMode() { + return new Mode(MODE_STOPPED, "init" /* entryCause */); } /** * Associates the supplied {@link Cancellable} with the mode to enable location listening to - * be cancelled. Used when mModeEnum == {@link #MODE_ENABLED}. See + * be cancelled. Used when mModeEnum == {@link #MODE_STARTED}. See * {@link #cancelLocationListening()}. */ void setLocationListenerCancellable(@NonNull Cancellable locationListenerCancellable) { @@ -206,7 +206,7 @@ class Mode { /** * Associates the {@code timeoutToken} with the mode for later retrieval. Used for - * {@link #MODE_ENABLED}. + * {@link #MODE_STARTED}. */ void setTimeoutInfo(@NonNull Cancellable timeoutCancellable, @NonNull String timeoutToken) { @@ -252,10 +252,10 @@ class Mode { /** Returns a string representation of the {@link ModeEnum} value provided. */ static String prettyPrintModeEnum(@ModeEnum int modeEnum) { switch (modeEnum) { - case MODE_DISABLED: - return "MODE_DISABLED"; - case MODE_ENABLED: - return "MODE_ENABLED"; + case MODE_STOPPED: + return "MODE_STOPPED"; + case MODE_STARTED: + return "MODE_STARTED"; case MODE_DESTROYED: return "MODE_DESTROYED"; case MODE_FAILED: @@ -280,7 +280,7 @@ class Mode { } private static @ModeEnum int validateModeEnum(@ModeEnum int modeEnum) { - if (modeEnum < MODE_DISABLED || modeEnum > MODE_FAILED) { + if (modeEnum < MODE_STOPPED || modeEnum > MODE_FAILED) { throw new IllegalArgumentException("modeEnum=" + modeEnum); } return modeEnum; @@ -288,7 +288,7 @@ class Mode { private static @ListenModeEnum int validateListenModeEnum( @ModeEnum int modeEnum, @ListenModeEnum int listenMode) { - if (modeEnum == MODE_ENABLED) { + if (modeEnum == MODE_STARTED) { if (listenMode != LOCATION_LISTEN_MODE_HIGH && listenMode != LOCATION_LISTEN_MODE_LOW) { throw new IllegalArgumentException(); } diff --git a/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/core/OfflineLocationTimeZoneDelegate.java b/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/core/OfflineLocationTimeZoneDelegate.java index fef030a..f127343 100644 --- a/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/core/OfflineLocationTimeZoneDelegate.java +++ b/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/core/OfflineLocationTimeZoneDelegate.java @@ -22,19 +22,19 @@ import static com.android.timezone.geotz.provider.core.LogUtils.logDebug; import static com.android.timezone.geotz.provider.core.LogUtils.logWarn; import static com.android.timezone.geotz.provider.core.Mode.MODE_DESTROYED; import static com.android.timezone.geotz.provider.core.Mode.MODE_FAILED; -import static com.android.timezone.geotz.provider.core.Mode.MODE_DISABLED; -import static com.android.timezone.geotz.provider.core.Mode.MODE_ENABLED; +import static com.android.timezone.geotz.provider.core.Mode.MODE_STARTED; +import static com.android.timezone.geotz.provider.core.Mode.MODE_STOPPED; import static com.android.timezone.geotz.provider.core.Mode.prettyPrintListenModeEnum; import android.location.Location; import android.os.SystemClock; +import android.service.timezone.TimeZoneProviderSuggestion; import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.location.timezone.provider.LocationTimeZoneEventUnbundled; import com.android.timezone.geotz.lookup.GeoTimeZonesFinder; import com.android.timezone.geotz.lookup.GeoTimeZonesFinder.LocationToken; @@ -46,9 +46,9 @@ import java.util.Objects; import java.util.function.Consumer; /** - * A class encapsulating the time zone detection logic for an Offline LocationTimeZoneProvider. - * It has been decoupled from the Android environment and many API via the {@link Environment} - * interface to enable easier unit testing. + * A class encapsulating the time zone detection logic for an Offline location-based + * {@link android.service.timezone.TimeZoneProviderService}. It has been decoupled from the Android + * environment and many API via the {@link Environment} interface to enable easier unit testing. * * <p>The overall goal of this class is to balance power consumption with responsiveness. * @@ -57,19 +57,19 @@ import java.util.function.Consumer; * <p>The instance interacts with multiple threads, but state changes occur in a single-threaded * manner through the use of a lock object, {@link #mLock}. * - * <p>When first enabled, the current location is requested using {@link + * <p>When first started, the current location is requested using {@link * Environment#startLocationListening(int, Consumer)} with {@link #LOCATION_LISTEN_MODE_HIGH} and * a timeout is requested using {@link Environment#startTimeout(Consumer, Object, long)}. * * <p>If a valid location is found within the timeout, the time zones for the location are looked up - * and an {@link LocationTimeZoneEventUnbundled event} is sent via {@link - * Environment#reportLocationTimeZoneEvent(LocationTimeZoneEventUnbundled)}. + * and an {@link TimeZoneProviderResult result} is recorded via {@link + * Environment#reportTimeZoneProviderResult(TimeZoneProviderResult)}. * * <p>If a valid location cannot be found within the timeout, a {@link - * LocationTimeZoneEventUnbundled#EVENT_TYPE_UNCERTAIN} {@link - * LocationTimeZoneEventUnbundled event} is sent to the system server. + * TimeZoneProviderResult#RESULT_TYPE_UNCERTAIN} {@link TimeZoneProviderResult result} is recorded to the + * system server. * - * <p>After an {@link LocationTimeZoneEventUnbundled event} has been sent, the provider restarts + * <p>After an {@link TimeZoneProviderResult result} has been sent, the provider restarts * location listening with a new timeout but in {@link #LOCATION_LISTEN_MODE_LOW}. If the current * location continues to be available it will stay in this mode, extending the timeout, otherwise it * will switch to {@link #LOCATION_LISTEN_MODE_HIGH} with a shorter timeout. @@ -113,7 +113,7 @@ public final class OfflineLocationTimeZoneDelegate { */ @NonNull <T> Cancellable startTimeout(@NonNull Consumer<T> callback, - @Nullable T callbackToken, @NonNull long delayMillis); + @Nullable T callbackToken, long delayMillis); /** * Starts an async location lookup. The location passed to {@code locationListener} will not @@ -134,7 +134,7 @@ public final class OfflineLocationTimeZoneDelegate { /** * Used to report location time zone information. */ - void reportLocationTimeZoneEvent(@NonNull LocationTimeZoneEventUnbundled event); + void reportTimeZoneProviderResult(@NonNull TimeZoneProviderResult result); /** See {@link SystemClock#elapsedRealtime()}. */ long elapsedRealtimeMillis(); @@ -157,16 +157,17 @@ public final class OfflineLocationTimeZoneDelegate { private LocationToken mLastLocationToken; /** - * The last location time zone event sent by the provider. Currently used for debugging only. + * The last time zone provider result determined by the provider. Currently used for debugging + * only. */ @GuardedBy("mLock") - private final ReferenceWithHistory<LocationTimeZoneEventUnbundled> mLastLocationTimeZoneEvent = + private final ReferenceWithHistory<TimeZoneProviderResult> mLastTimeZoneProviderResult = new ReferenceWithHistory<>(10); public OfflineLocationTimeZoneDelegate(@NonNull Environment environment) { mEnvironment = Objects.requireNonNull(environment); - mCurrentMode.set(Mode.createDisabledMode()); + mCurrentMode.set(Mode.createStoppedMode()); } public void onBind() { @@ -175,13 +176,13 @@ public final class OfflineLocationTimeZoneDelegate { synchronized (mLock) { Mode currentMode = mCurrentMode.get(); - if (currentMode.mModeEnum != MODE_DISABLED) { + if (currentMode.mModeEnum != MODE_STOPPED) { handleUnexpectedStateTransition( "onBind() called when in unexpected mode=" + currentMode); return; } - Mode newMode = new Mode(MODE_DISABLED, entryCause); + Mode newMode = new Mode(MODE_STOPPED, entryCause); mCurrentMode.set(newMode); } } @@ -194,35 +195,35 @@ public final class OfflineLocationTimeZoneDelegate { cancelTimeoutAndLocationCallbacks(); Mode currentMode = mCurrentMode.get(); - if (currentMode.mModeEnum == MODE_ENABLED) { - sendTimeZoneUncertainEventIfNeeded(); + if (currentMode.mModeEnum == MODE_STARTED) { + sendTimeZoneUncertainResultIfNeeded(); } Mode newMode = new Mode(MODE_DESTROYED, entryCause); mCurrentMode.set(newMode); } } - public void onDisable() { - String debugInfo = "onDisable()"; + public void onStopUpdates() { + String debugInfo = "onStopUpdates()"; logDebug(debugInfo); synchronized (mLock) { Mode currentMode = mCurrentMode.get(); switch (currentMode.mModeEnum) { - case MODE_DISABLED: { - // No-op - the provider is already disabled. - logWarn("Unexpected onDisable() when currentMode=" + currentMode); + case MODE_STOPPED: { + // No-op - the provider is already stopped. + logWarn("Unexpected onStopUpdates() when currentMode=" + currentMode); break; } - case MODE_ENABLED: { - enterDisabledMode(debugInfo); + case MODE_STARTED: { + enterStoppedMode(debugInfo); break; } case MODE_FAILED: case MODE_DESTROYED: default: { handleUnexpectedStateTransition( - "Unexpected onDisable() when currentMode=" + currentMode); + "Unexpected onStopUpdates() when currentMode=" + currentMode); break; } } @@ -230,31 +231,32 @@ public final class OfflineLocationTimeZoneDelegate { } - public void onEnable(@NonNull long initializationTimeoutMillis) { - String debugInfo = "onEnable(), initializationTimeoutMillis=" + initializationTimeoutMillis; + public void onStartUpdates(long initializationTimeoutMillis) { + String debugInfo = "onStartUpdates()," + + " initializationTimeoutMillis=" + initializationTimeoutMillis; logDebug(debugInfo); synchronized (mLock) { Mode currentMode = mCurrentMode.get(); switch (currentMode.mModeEnum) { - case MODE_DISABLED: { + case MODE_STOPPED: { // Always start in the most aggressive location listening mode. The request // contains the time in which the LTZP is given to provide the first - // event, so this is used for the first timeout. + // result, so this is used for the first timeout. enterLocationListeningMode(LOCATION_LISTEN_MODE_HIGH, debugInfo, initializationTimeoutMillis); break; } - case MODE_ENABLED: { - // No-op - the provider is already enabled. - logWarn("Unexpected onEnabled() received when in currentMode=" + currentMode); + case MODE_STARTED: { + // No-op - the provider is already started. + logWarn("Unexpected onStarted() received when in currentMode=" + currentMode); break; } case MODE_FAILED: case MODE_DESTROYED: default: { handleUnexpectedStateTransition( - "Unexpected onEnabled() received when in currentMode=" + currentMode); + "Unexpected onStarted() received when in currentMode=" + currentMode); break; } } @@ -268,17 +270,13 @@ public final class OfflineLocationTimeZoneDelegate { + formatElapsedRealtimeMillis(mEnvironment.elapsedRealtimeMillis())); pw.println("mCurrentMode=" + mCurrentMode); pw.println("mLastLocationToken=" + mLastLocationToken); - pw.println("mLastLocationTimeZoneEvent=" + mLastLocationTimeZoneEvent); + pw.println("mLastTimeZoneProviderResult=" + mLastTimeZoneProviderResult); pw.println(); pw.println("Mode history:"); - // pw.increaseIndent(); mCurrentMode.dump(pw); - // pw.decreaseIndent(); pw.println(); - pw.println("LocationTimeZoneEvent history:"); - // pw.increaseIndent(); - mLastLocationTimeZoneEvent.dump(pw); - // pw.decreaseIndent(); + pw.println("TimeZoneProviderResult history:"); + mLastTimeZoneProviderResult.dump(pw); } } @@ -289,7 +287,7 @@ public final class OfflineLocationTimeZoneDelegate { } /** - * Accepts the current location when in {@link Mode#MODE_ENABLED}. + * Accepts the current location when in {@link Mode#MODE_STARTED}. */ private void onLocationReceived(@NonNull Location location) { Objects.requireNonNull(location); @@ -297,7 +295,7 @@ public final class OfflineLocationTimeZoneDelegate { synchronized (mLock) { Mode currentMode = mCurrentMode.get(); - if (currentMode.mModeEnum != MODE_ENABLED) { + if (currentMode.mModeEnum != MODE_STARTED) { // This is not expected to happen. String unexpectedStateDebugInfo = "Unexpected call to onLocationReceived()," + " location=" + location @@ -313,7 +311,7 @@ public final class OfflineLocationTimeZoneDelegate { // A good location has been received. try { - sendTimeZoneCertainEventIfNeeded(location); + sendTimeZoneCertainResultIfNeeded(location); // Move to the least aggressive location listening mode. enterLocationListeningMode(LOCATION_LISTEN_MODE_LOW, debugInfo, @@ -324,7 +322,7 @@ public final class OfflineLocationTimeZoneDelegate { + " previous debugInfo=" + debugInfo; logWarn(lookupFailureDebugInfo, e); - enterFailedMode(lookupFailureDebugInfo); + enterFailedMode(new IOException(lookupFailureDebugInfo, e)); } } } @@ -339,7 +337,7 @@ public final class OfflineLocationTimeZoneDelegate { synchronized (mLock) { Mode currentMode = mCurrentMode.get(); - if (currentMode.mModeEnum != MODE_ENABLED) { + if (currentMode.mModeEnum != MODE_STARTED) { handleUnexpectedCallback("Unexpected timeout for mode=" + currentMode); return; } @@ -350,7 +348,7 @@ public final class OfflineLocationTimeZoneDelegate { } if (currentMode.mListenMode == LOCATION_LISTEN_MODE_HIGH) { - sendTimeZoneUncertainEventIfNeeded(); + sendTimeZoneUncertainResultIfNeeded(); enterLocationListeningMode(LOCATION_LISTEN_MODE_LOW, debugInfo, LOCATION_LISTEN_MODE_LOW_TIMEOUT_MILLIS); } else { @@ -361,7 +359,7 @@ public final class OfflineLocationTimeZoneDelegate { } @GuardedBy("mLock") - private void sendTimeZoneCertainEventIfNeeded(@NonNull Location location) + private void sendTimeZoneCertainResultIfNeeded(@NonNull Location location) throws IOException { try (GeoTimeZonesFinder geoTimeZonesFinder = mEnvironment.createGeoTimeZoneFinder()) { // Convert the location to a LocationToken. @@ -369,61 +367,59 @@ public final class OfflineLocationTimeZoneDelegate { location.getLatitude(), location.getLongitude()); // If the location token is the same as the last lookup, there is no need to do the - // lookup / send another event. + // lookup / send another suggestion. if (locationToken.equals(mLastLocationToken)) { logDebug("Location token=" + locationToken + " has not changed."); } else { List<String> tzIds = geoTimeZonesFinder.findTimeZonesForLocationToken(locationToken); logDebug("tzIds found for location=" + location + ", tzIds=" + tzIds); - LocationTimeZoneEventUnbundled event = - new LocationTimeZoneEventUnbundled.Builder() - .setEventType(LocationTimeZoneEventUnbundled.EVENT_TYPE_SUCCESS) - .setTimeZoneIds(tzIds) - .build(); - reportLocationTimeZoneEventInternal(event, locationToken); + TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder() + .setTimeZoneIds(tzIds) + .setElapsedRealtimeMillis(mEnvironment.elapsedRealtimeMillis()) + .build(); + + TimeZoneProviderResult result = + TimeZoneProviderResult.createSuggestion(suggestion); + reportTimeZoneProviderResultInternal(result, locationToken); } } } @GuardedBy("mLock") - private void sendTimeZoneUncertainEventIfNeeded() { - LocationTimeZoneEventUnbundled lastEvent = mLastLocationTimeZoneEvent.get(); - - // If the last event was uncertain, there is no need to send another. - if (lastEvent == null || - lastEvent.getEventType() != LocationTimeZoneEventUnbundled.EVENT_TYPE_UNCERTAIN) { - LocationTimeZoneEventUnbundled event = new LocationTimeZoneEventUnbundled.Builder() - .setEventType(LocationTimeZoneEventUnbundled.EVENT_TYPE_UNCERTAIN) - .build(); - reportLocationTimeZoneEventInternal(event, null /* locationToken */); + private void sendTimeZoneUncertainResultIfNeeded() { + TimeZoneProviderResult lastResult = mLastTimeZoneProviderResult.get(); + + // If the last result was uncertain, there is no need to send another. + if (lastResult == null || + lastResult.getType() != TimeZoneProviderResult.RESULT_TYPE_UNCERTAIN) { + TimeZoneProviderResult result = TimeZoneProviderResult.createUncertain(); + reportTimeZoneProviderResultInternal(result, null /* locationToken */); } else { - logDebug("sendTimeZoneUncertainEventIfNeeded(): Last event=" + lastEvent + logDebug("sendTimeZoneUncertainResultIfNeeded(): Last result=" + lastResult + ", no need to sent another."); } } @GuardedBy("mLock") - private void sendPermanentFailureEvent() { - LocationTimeZoneEventUnbundled event = new LocationTimeZoneEventUnbundled.Builder() - .setEventType(LocationTimeZoneEventUnbundled.EVENT_TYPE_PERMANENT_FAILURE) - .build(); - reportLocationTimeZoneEventInternal(event, null /* locationToken */); + private void sendPermanentFailureResult(@NonNull Throwable cause) { + TimeZoneProviderResult result = TimeZoneProviderResult.createPermanentFailure(cause); + reportTimeZoneProviderResultInternal(result, null /* locationToken */); } @GuardedBy("mLock") - private void reportLocationTimeZoneEventInternal( - @NonNull LocationTimeZoneEventUnbundled event, + private void reportTimeZoneProviderResultInternal( + @NonNull TimeZoneProviderResult result, @Nullable LocationToken locationToken) { - mLastLocationTimeZoneEvent.set(event); + mLastTimeZoneProviderResult.set(result); mLastLocationToken = locationToken; - mEnvironment.reportLocationTimeZoneEvent(event); + mEnvironment.reportTimeZoneProviderResult(result); } @GuardedBy("mLock") private void clearLocationState() { mLastLocationToken = null; - mLastLocationTimeZoneEvent.set(null); + mLastTimeZoneProviderResult.set(null); } /** Called when leaving the current mode to cancel all pending asynchronous operations. */ @@ -449,35 +445,36 @@ public final class OfflineLocationTimeZoneDelegate { } @GuardedBy("mLock") - private void enterFailedMode(@NonNull String entryCause) { + private void enterFailedMode(@NonNull Throwable entryCause) { logDebug("Provider entering failed mode, entryCause=" + entryCause); cancelTimeoutAndLocationCallbacks(); - sendPermanentFailureEvent(); + sendPermanentFailureResult(entryCause); - Mode newMode = new Mode(MODE_FAILED, entryCause); + String failureReason = entryCause.getMessage(); + Mode newMode = new Mode(MODE_FAILED, failureReason); mCurrentMode.set(newMode); } @GuardedBy("mLock") - private void enterDisabledMode(@NonNull String entryCause) { - logDebug("Provider entering disabled mode, entryCause=" + entryCause); + private void enterStoppedMode(@NonNull String entryCause) { + logDebug("Provider entering stopped mode, entryCause=" + entryCause); cancelTimeoutAndLocationCallbacks(); - // Clear all location-derived state. The provider may be disabled due to the current user + // Clear all location-derived state. The provider may be stopped due to the current user // changing. clearLocationState(); - Mode newMode = new Mode(MODE_DISABLED, entryCause); + Mode newMode = new Mode(MODE_STOPPED, entryCause); mCurrentMode.set(newMode); } @GuardedBy("mLock") private void enterLocationListeningMode( @ListenModeEnum int listenMode, - @NonNull String entryCause, @NonNull long timeoutMillis) { + @NonNull String entryCause, long timeoutMillis) { logDebug("Provider entering location listening mode" + ", listenMode=" + prettyPrintListenModeEnum(listenMode) + ", entryCause=" + entryCause); @@ -485,8 +482,8 @@ public final class OfflineLocationTimeZoneDelegate { Mode currentMode = mCurrentMode.get(); currentMode.cancelTimeout(); - Mode newMode = new Mode(MODE_ENABLED, entryCause, listenMode); - if (currentMode.mModeEnum != MODE_ENABLED + Mode newMode = new Mode(MODE_STARTED, entryCause, listenMode); + if (currentMode.mModeEnum != MODE_STARTED || currentMode.mListenMode != listenMode) { currentMode.cancelLocationListening(); Cancellable locationListenerCancellable = diff --git a/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/core/TimeZoneProviderResult.java b/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/core/TimeZoneProviderResult.java new file mode 100644 index 0000000..7633412 --- /dev/null +++ b/locationtzprovider/src/main/java/com/android/timezone/geotz/provider/core/TimeZoneProviderResult.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2020 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.timezone.geotz.provider.core; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import android.service.timezone.TimeZoneProviderService; +import android.service.timezone.TimeZoneProviderSuggestion; + +import java.util.Objects; + +/** + * A result of a period of time zone detection. + */ +public final class TimeZoneProviderResult { + + @IntDef({ RESULT_TYPE_PERMANENT_FAILURE, RESULT_TYPE_SUGGESTION, RESULT_TYPE_UNCERTAIN }) + public @interface ResultType {} + + /** + * The provider failed permanently. See {@link + * TimeZoneProviderService#reportPermanentFailure(Throwable)} + */ + public static final int RESULT_TYPE_PERMANENT_FAILURE = 1; + + /** + * The provider made a suggestion. See {@link + * TimeZoneProviderService#reportSuggestion(TimeZoneProviderSuggestion)} + */ + public static final int RESULT_TYPE_SUGGESTION = 2; + + /** + * The provider was uncertain about the time zone. See {@link + * TimeZoneProviderService#reportUncertain()} + */ + public static final int RESULT_TYPE_UNCERTAIN = 3; + + private static final TimeZoneProviderResult UNCERTAIN_RESULT = + new TimeZoneProviderResult(RESULT_TYPE_UNCERTAIN, null, null); + + private static final int RESULT_TYPE_MIN = RESULT_TYPE_PERMANENT_FAILURE; + private static final int RESULT_TYPE_MAX = RESULT_TYPE_UNCERTAIN; + + @ResultType + private final int mType; + + @Nullable + private final TimeZoneProviderSuggestion mSuggestion; + + @Nullable + private final Throwable mFailureCause; + + private TimeZoneProviderResult(@ResultType int type, + @Nullable TimeZoneProviderSuggestion suggestion, + @Nullable Throwable failureCause) { + mType = type; + mSuggestion = suggestion; + mFailureCause = failureCause; + } + + /** Returns a result of type {@link #RESULT_TYPE_SUGGESTION}. */ + public static TimeZoneProviderResult createSuggestion( + @NonNull TimeZoneProviderSuggestion suggestion) { + return new TimeZoneProviderResult(RESULT_TYPE_SUGGESTION, + Objects.requireNonNull(suggestion), null); + } + + /** Returns a result of type {@link #RESULT_TYPE_UNCERTAIN}. */ + public static TimeZoneProviderResult createUncertain() { + return UNCERTAIN_RESULT; + } + + /** Returns a result of type {@link #RESULT_TYPE_PERMANENT_FAILURE}. */ + public static TimeZoneProviderResult createPermanentFailure(@NonNull Throwable cause) { + return new TimeZoneProviderResult(RESULT_TYPE_PERMANENT_FAILURE, null, + Objects.requireNonNull(cause)); + } + + /** + * Returns the result type. + */ + public @ResultType int getType() { + return mType; + } + + /** + * Returns the suggestion. Populated for {@link #RESULT_TYPE_SUGGESTION}. + */ + @NonNull + public TimeZoneProviderSuggestion getSuggestion() { + return mSuggestion; + } + + /** + * Returns the failure cause. Populated for {@link #RESULT_TYPE_PERMANENT_FAILURE}. + */ + @Nullable + public Throwable getFailureCause() { + return mFailureCause; + } + + @Override + public String toString() { + return "TimeZoneProviderResult{" + + "mResultType=" + mType + + ", mSuggestion=" + mSuggestion; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TimeZoneProviderResult that = (TimeZoneProviderResult) o; + return mType == that.mType + && Objects.equals(mSuggestion, that.mSuggestion); + } + + @Override + public int hashCode() { + return Objects.hash(mType, mSuggestion); + } +} diff --git a/locationtzprovider/src/test/java/com/android/timezone/geotz/provider/core/OfflineLocationTimeZoneDelegateTest.java b/locationtzprovider/src/test/java/com/android/timezone/geotz/provider/core/OfflineLocationTimeZoneDelegateTest.java index 602eeb9..17aad97 100644 --- a/locationtzprovider/src/test/java/com/android/timezone/geotz/provider/core/OfflineLocationTimeZoneDelegateTest.java +++ b/locationtzprovider/src/test/java/com/android/timezone/geotz/provider/core/OfflineLocationTimeZoneDelegateTest.java @@ -26,7 +26,6 @@ import android.location.Location; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.location.timezone.provider.LocationTimeZoneEventUnbundled; import com.android.timezone.geotz.lookup.GeoTimeZonesFinder; import org.junit.Before; @@ -62,18 +61,18 @@ public class OfflineLocationTimeZoneDelegateTest { List<String> timeZoneIds = Arrays.asList("Europe/London"); mTestGeoTimeZoneFinder.setTimeZonesForLocation(latDegrees, lngDegrees, timeZoneIds); - assertEquals(Mode.MODE_DISABLED, mDelegate.getCurrentModeEnumForTests()); + assertEquals(Mode.MODE_STOPPED, mDelegate.getCurrentModeEnumForTests()); mTestEnvironment.assertIsNotListening(); mTestEnvironment.assertNoTimeoutSet(); mDelegate.onBind(); - assertEquals(Mode.MODE_DISABLED, mDelegate.getCurrentModeEnumForTests()); + assertEquals(Mode.MODE_STOPPED, mDelegate.getCurrentModeEnumForTests()); mTestEnvironment.assertIsNotListening(); mTestEnvironment.assertNoTimeoutSet(); final int initializationTimeoutMillis = 20000; - mDelegate.onEnable(initializationTimeoutMillis); - assertEquals(Mode.MODE_ENABLED, mDelegate.getCurrentModeEnumForTests()); + mDelegate.onStartUpdates(initializationTimeoutMillis); + assertEquals(Mode.MODE_STARTED, mDelegate.getCurrentModeEnumForTests()); mTestEnvironment.assertIsListening( OfflineLocationTimeZoneDelegate.LOCATION_LISTEN_MODE_HIGH); mTestEnvironment.assertTimeoutSet(initializationTimeoutMillis); @@ -83,8 +82,8 @@ public class OfflineLocationTimeZoneDelegateTest { location.setLongitude(1.0); mTestEnvironment.simulateCurrentLocationDetected(location); - mTestEnvironment.assertLocationEventReported(timeZoneIds); - assertEquals(Mode.MODE_ENABLED, mDelegate.getCurrentModeEnumForTests()); + mTestEnvironment.assertSuggestionResult(timeZoneIds); + assertEquals(Mode.MODE_STARTED, mDelegate.getCurrentModeEnumForTests()); mTestEnvironment.assertIsListening( OfflineLocationTimeZoneDelegate.LOCATION_LISTEN_MODE_LOW); mTestEnvironment.assertTimeoutSet( @@ -97,7 +96,7 @@ public class OfflineLocationTimeZoneDelegateTest { private long mElapsedRealtimeMillis; private TestLocationListenerState mLocationListeningState; private TestTimeoutState<?> mTimeoutState; - private LocationTimeZoneEventUnbundled mLastEvent; + private TimeZoneProviderResult mLastResult; @NonNull @Override @@ -124,9 +123,9 @@ public class OfflineLocationTimeZoneDelegateTest { } @Override - public void reportLocationTimeZoneEvent(@NonNull LocationTimeZoneEventUnbundled event) { - assertNotNull(event); - mLastEvent = event; + public void reportTimeZoneProviderResult(@NonNull TimeZoneProviderResult result) { + assertNotNull(result); + mLastResult = result; } @Override @@ -157,11 +156,10 @@ public class OfflineLocationTimeZoneDelegateTest { assertEquals(expectedTimeoutMillis, mTimeoutState.mDelayMillis); } - public void assertLocationEventReported(List<String> expectedTimeZoneIds) { - assertNotNull(mLastEvent); - assertEquals(LocationTimeZoneEventUnbundled.EVENT_TYPE_SUCCESS, - mLastEvent.getEventType()); - assertEquals(expectedTimeZoneIds, mLastEvent.getTimeZoneIds()); + public void assertSuggestionResult(List<String> expectedTimeZoneIds) { + assertNotNull(mLastResult); + assertEquals(TimeZoneProviderResult.RESULT_TYPE_SUGGESTION, mLastResult.getType()); + assertEquals(expectedTimeZoneIds, mLastResult.getSuggestion().getTimeZoneIds()); } private class TestLocationListenerState extends TestCancellable { |