diff --git a/services/core/java/com/android/server/timezonedetector/MetricsTimeZoneDetectorState.java b/services/core/java/com/android/server/timezonedetector/MetricsTimeZoneDetectorState.java
new file mode 100644
index 0000000000000..c8c828f10ad3a
--- /dev/null
+++ b/services/core/java/com/android/server/timezonedetector/MetricsTimeZoneDetectorState.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2021 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.timezonedetector;
+
+import static libcore.io.IoUtils.closeQuietly;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.timezonedetector.ManualTimeZoneSuggestion;
+import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
+import android.util.proto.ProtoOutputStream;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A class that provides time zone detector state information for metrics.
+ *
+ *
+ * Regarding time zone ID ordinals:
+ *
+ * We don't want to leak user location information by reporting time zone IDs. Instead, time zone
+ * IDs are consistently identified within a given instance of this class by a numeric ID. This
+ * allows comparison of IDs without revealing what those IDs are.
+ */
+public final class MetricsTimeZoneDetectorState {
+
+ @IntDef(prefix = "DETECTION_MODE_",
+ value = { DETECTION_MODE_MANUAL, DETECTION_MODE_GEO, DETECTION_MODE_TELEPHONY})
+ @interface DetectionMode {};
+
+ @DetectionMode
+ public static final int DETECTION_MODE_MANUAL = 0;
+ @DetectionMode
+ public static final int DETECTION_MODE_GEO = 1;
+ @DetectionMode
+ public static final int DETECTION_MODE_TELEPHONY = 2;
+
+ @NonNull
+ private final ConfigurationInternal mConfigurationInternal;
+ @NonNull
+ private final int mDeviceTimeZoneIdOrdinal;
+ @Nullable
+ private final MetricsTimeZoneSuggestion mLatestManualSuggestion;
+ @Nullable
+ private final MetricsTimeZoneSuggestion mLatestTelephonySuggestion;
+ @Nullable
+ private final MetricsTimeZoneSuggestion mLatestGeolocationSuggestion;
+
+ private MetricsTimeZoneDetectorState(
+ @NonNull ConfigurationInternal configurationInternal,
+ int deviceTimeZoneIdOrdinal,
+ @Nullable MetricsTimeZoneSuggestion latestManualSuggestion,
+ @Nullable MetricsTimeZoneSuggestion latestTelephonySuggestion,
+ @Nullable MetricsTimeZoneSuggestion latestGeolocationSuggestion) {
+ mConfigurationInternal = Objects.requireNonNull(configurationInternal);
+ mDeviceTimeZoneIdOrdinal = deviceTimeZoneIdOrdinal;
+ mLatestManualSuggestion = latestManualSuggestion;
+ mLatestTelephonySuggestion = latestTelephonySuggestion;
+ mLatestGeolocationSuggestion = latestGeolocationSuggestion;
+ }
+
+ /**
+ * Creates {@link MetricsTimeZoneDetectorState} from the supplied parameters, using the {@link
+ * OrdinalGenerator} to generate time zone ID ordinals.
+ */
+ public static MetricsTimeZoneDetectorState create(
+ @NonNull OrdinalGenerator tzIdOrdinalGenerator,
+ @NonNull ConfigurationInternal configurationInternal,
+ @NonNull String deviceTimeZoneId,
+ @Nullable ManualTimeZoneSuggestion latestManualSuggestion,
+ @Nullable TelephonyTimeZoneSuggestion latestTelephonySuggestion,
+ @Nullable GeolocationTimeZoneSuggestion latestGeolocationSuggestion) {
+
+ // TODO(b/172934905) Add logic to canonicalize the time zone IDs to Android's preferred IDs
+ // so that the ordinals will match even when the ID is not identical, just equivalent.
+ int deviceTimeZoneIdOrdinal =
+ tzIdOrdinalGenerator.ordinal(Objects.requireNonNull(deviceTimeZoneId));
+ MetricsTimeZoneSuggestion latestObfuscatedManualSuggestion =
+ createMetricsTimeZoneSuggestion(tzIdOrdinalGenerator, latestManualSuggestion);
+ MetricsTimeZoneSuggestion latestObfuscatedTelephonySuggestion =
+ createMetricsTimeZoneSuggestion(tzIdOrdinalGenerator, latestTelephonySuggestion);
+ MetricsTimeZoneSuggestion latestObfuscatedGeolocationSuggestion =
+ createMetricsTimeZoneSuggestion(tzIdOrdinalGenerator, latestGeolocationSuggestion);
+
+ return new MetricsTimeZoneDetectorState(
+ configurationInternal, deviceTimeZoneIdOrdinal, latestObfuscatedManualSuggestion,
+ latestObfuscatedTelephonySuggestion, latestObfuscatedGeolocationSuggestion);
+ }
+
+ /** Returns true if the device supports telephony time zone detection. */
+ public boolean isTelephonyDetectionSupported() {
+ return mConfigurationInternal.isTelephonyDetectionSupported();
+ }
+
+ /** Returns true if the device supports geolocation time zone detection. */
+ public boolean isGeoDetectionSupported() {
+ return mConfigurationInternal.isGeoDetectionSupported();
+ }
+
+ /** Returns true if user's location can be used generally. */
+ public boolean isUserLocationEnabled() {
+ return mConfigurationInternal.isLocationEnabled();
+ }
+
+ /** Returns the value of the geolocation time zone detection enabled setting. */
+ public boolean getGeoDetectionEnabledSetting() {
+ return mConfigurationInternal.getGeoDetectionEnabledSetting();
+ }
+
+ /** Returns the value of the auto time zone detection enabled setting. */
+ public boolean getAutoDetectionEnabledSetting() {
+ return mConfigurationInternal.getAutoDetectionEnabledSetting();
+ }
+
+ /**
+ * Returns the detection mode the device is currently using, which can be influenced by various
+ * things besides the user's setting.
+ */
+ @DetectionMode
+ public int getDetectionMode() {
+ if (!mConfigurationInternal.getAutoDetectionEnabledBehavior()) {
+ return DETECTION_MODE_MANUAL;
+ } else if (mConfigurationInternal.getGeoDetectionEnabledBehavior()) {
+ return DETECTION_MODE_GEO;
+ } else {
+ return DETECTION_MODE_TELEPHONY;
+ }
+ }
+
+ /**
+ * Returns the ordinal for the device's currently set time zone ID.
+ * See {@link MetricsTimeZoneDetectorState} for information about ordinals.
+ */
+ @NonNull
+ public int getDeviceTimeZoneIdOrdinal() {
+ return mDeviceTimeZoneIdOrdinal;
+ }
+
+ /**
+ * Returns bytes[] for a {@link MetricsTimeZoneSuggestion} for the last manual
+ * suggestion received.
+ */
+ @Nullable
+ public byte[] getLatestManualSuggestionProtoBytes() {
+ return suggestionProtoBytes(mLatestManualSuggestion);
+ }
+
+ /**
+ * Returns bytes[] for a {@link MetricsTimeZoneSuggestion} for the last, best
+ * telephony suggestion received.
+ */
+ @Nullable
+ public byte[] getLatestTelephonySuggestionProtoBytes() {
+ return suggestionProtoBytes(mLatestTelephonySuggestion);
+ }
+
+ /**
+ * Returns bytes[] for a {@link MetricsTimeZoneSuggestion} for the last geolocation
+ * suggestion received.
+ */
+ @Nullable
+ public byte[] getLatestGeolocationSuggestionProtoBytes() {
+ return suggestionProtoBytes(mLatestGeolocationSuggestion);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ MetricsTimeZoneDetectorState that = (MetricsTimeZoneDetectorState) o;
+ return mDeviceTimeZoneIdOrdinal == that.mDeviceTimeZoneIdOrdinal
+ && mConfigurationInternal.equals(that.mConfigurationInternal)
+ && Objects.equals(mLatestManualSuggestion, that.mLatestManualSuggestion)
+ && Objects.equals(mLatestTelephonySuggestion, that.mLatestTelephonySuggestion)
+ && Objects.equals(mLatestGeolocationSuggestion, that.mLatestGeolocationSuggestion);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mConfigurationInternal, mDeviceTimeZoneIdOrdinal,
+ mLatestManualSuggestion, mLatestTelephonySuggestion, mLatestGeolocationSuggestion);
+ }
+
+ @Override
+ public String toString() {
+ return "MetricsTimeZoneDetectorState{"
+ + "mConfigurationInternal=" + mConfigurationInternal
+ + ", mDeviceTimeZoneIdOrdinal=" + mDeviceTimeZoneIdOrdinal
+ + ", mLatestManualSuggestion=" + mLatestManualSuggestion
+ + ", mLatestTelephonySuggestion=" + mLatestTelephonySuggestion
+ + ", mLatestGeolocationSuggestion=" + mLatestGeolocationSuggestion
+ + '}';
+ }
+
+ private static byte[] suggestionProtoBytes(
+ @Nullable MetricsTimeZoneSuggestion suggestion) {
+ if (suggestion == null) {
+ return null;
+ }
+ return suggestion.toBytes();
+ }
+
+ @Nullable
+ private static MetricsTimeZoneSuggestion createMetricsTimeZoneSuggestion(
+ @NonNull OrdinalGenerator zoneIdOrdinalGenerator,
+ @NonNull ManualTimeZoneSuggestion manualSuggestion) {
+ if (manualSuggestion == null) {
+ return null;
+ }
+
+ int zoneIdOrdinal = zoneIdOrdinalGenerator.ordinal(manualSuggestion.getZoneId());
+ return MetricsTimeZoneSuggestion.createCertain(
+ new int[] { zoneIdOrdinal });
+ }
+
+ @Nullable
+ private static MetricsTimeZoneSuggestion createMetricsTimeZoneSuggestion(
+ @NonNull OrdinalGenerator zoneIdOrdinalGenerator,
+ @NonNull TelephonyTimeZoneSuggestion telephonySuggestion) {
+ if (telephonySuggestion == null) {
+ return null;
+ }
+ if (telephonySuggestion.getZoneId() == null) {
+ return MetricsTimeZoneSuggestion.createUncertain();
+ }
+ int zoneIdOrdinal = zoneIdOrdinalGenerator.ordinal(telephonySuggestion.getZoneId());
+ return MetricsTimeZoneSuggestion.createCertain(new int[] { zoneIdOrdinal });
+ }
+
+ @Nullable
+ private static MetricsTimeZoneSuggestion createMetricsTimeZoneSuggestion(
+ @NonNull OrdinalGenerator zoneIdOrdinalGenerator,
+ @Nullable GeolocationTimeZoneSuggestion geolocationSuggestion) {
+ if (geolocationSuggestion == null) {
+ return null;
+ }
+
+ List zoneIds = geolocationSuggestion.getZoneIds();
+ if (zoneIds == null) {
+ return MetricsTimeZoneSuggestion.createUncertain();
+ }
+ return MetricsTimeZoneSuggestion.createCertain(zoneIdOrdinalGenerator.ordinals(zoneIds));
+ }
+
+ /**
+ * A Java class that closely matches the android.app.time.MetricsTimeZoneSuggestion
+ * proto definition.
+ */
+ private static final class MetricsTimeZoneSuggestion {
+ @Nullable
+ private final int[] mZoneIdOrdinals;
+
+ MetricsTimeZoneSuggestion(@Nullable int[] zoneIdOrdinals) {
+ mZoneIdOrdinals = zoneIdOrdinals;
+ }
+
+ @NonNull
+ static MetricsTimeZoneSuggestion createUncertain() {
+ return new MetricsTimeZoneSuggestion(null);
+ }
+
+ public static MetricsTimeZoneSuggestion createCertain(
+ @NonNull int[] zoneIdOrdinals) {
+ return new MetricsTimeZoneSuggestion(zoneIdOrdinals);
+ }
+
+ boolean isCertain() {
+ return mZoneIdOrdinals != null;
+ }
+
+ @Nullable
+ int[] getZoneIdOrdinals() {
+ return mZoneIdOrdinals;
+ }
+
+ byte[] toBytes() {
+ // We don't get access to the atoms.proto definition for nested proto fields, so we use
+ // an identically specified proto.
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ ProtoOutputStream protoOutputStream = new ProtoOutputStream(byteArrayOutputStream);
+ int typeProtoValue = isCertain()
+ ? android.app.time.MetricsTimeZoneSuggestion.CERTAIN
+ : android.app.time.MetricsTimeZoneSuggestion.UNCERTAIN;
+ protoOutputStream.write(android.app.time.MetricsTimeZoneSuggestion.TYPE,
+ typeProtoValue);
+ if (isCertain()) {
+ for (int zoneIdOrdinal : getZoneIdOrdinals()) {
+ protoOutputStream.write(
+ android.app.time.MetricsTimeZoneSuggestion.TIME_ZONE_ORDINALS,
+ zoneIdOrdinal);
+ }
+ }
+ protoOutputStream.flush();
+ closeQuietly(byteArrayOutputStream);
+ return byteArrayOutputStream.toByteArray();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ MetricsTimeZoneSuggestion that = (MetricsTimeZoneSuggestion) o;
+ return Arrays.equals(mZoneIdOrdinals, that.mZoneIdOrdinals);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(mZoneIdOrdinals);
+ }
+
+ @Override
+ public String toString() {
+ return "MetricsTimeZoneSuggestion{"
+ + "mZoneIdOrdinals=" + Arrays.toString(mZoneIdOrdinals)
+ + '}';
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/timezonedetector/OrdinalGenerator.java b/services/core/java/com/android/server/timezonedetector/OrdinalGenerator.java
new file mode 100644
index 0000000000000..a448773c40d55
--- /dev/null
+++ b/services/core/java/com/android/server/timezonedetector/OrdinalGenerator.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2021 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.timezonedetector;
+
+import android.util.ArraySet;
+
+import java.util.List;
+
+/**
+ * A helper class that turns a set of objects into ordinal values, i.e. each object is offered
+ * up via {@link #ordinal(Object)} or similar method, and a number will be returned. If the
+ * object has been seen before by the instance then the same number will be returned. Intended
+ * for situations where it is useful to know if values from some finite set are the same or
+ * different, but the value is either large or may reveal PII. This class relies on {@link
+ * Object#equals(Object)} and {@link Object#hashCode()}.
+ */
+class OrdinalGenerator {
+ private final ArraySet mKnownIds = new ArraySet<>();
+
+ int ordinal(T object) {
+ int ordinal = mKnownIds.indexOf(object);
+ if (ordinal < 0) {
+ ordinal = mKnownIds.size();
+ mKnownIds.add(object);
+ }
+ return ordinal;
+ }
+
+ int[] ordinals(List objects) {
+ int[] ordinals = new int[objects.size()];
+ for (int i = 0; i < ordinals.length; i++) {
+ ordinals[i] = ordinal(objects.get(i));
+ }
+ return ordinals;
+ }
+}
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java
index cd220b164851f..d429b8762a7ca 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java
@@ -50,4 +50,8 @@ public interface TimeZoneDetectorInternal extends Dumpable.Container {
* available, and so on. This method may be implemented asynchronously.
*/
void suggestGeolocationTimeZone(@NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion);
+
+ /** Generates a state snapshot for metrics. */
+ @NonNull
+ MetricsTimeZoneDetectorState generateMetricsState();
}
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java
index 7ba20eee03926..4e78f5aa444c3 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java
@@ -90,4 +90,10 @@ public final class TimeZoneDetectorInternalImpl implements TimeZoneDetectorInter
mHandler.post(
() -> mTimeZoneDetectorStrategy.suggestGeolocationTimeZone(timeZoneSuggestion));
}
+
+ @Override
+ @NonNull
+ public MetricsTimeZoneDetectorState generateMetricsState() {
+ return mTimeZoneDetectorStrategy.generateMetricsState();
+ }
}
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java
index 8266f121822ec..e3f31b6aa3265 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java
@@ -66,9 +66,10 @@ import android.util.IndentingPrintWriter;
* Threading:
*
*
Suggestion calls with a void return type may be handed off to a separate thread and handled
- * asynchronously. Synchronous calls like {@link #getCurrentUserConfigurationInternal()}, and debug
- * calls like {@link #dump(IndentingPrintWriter, String[])}, may be called on a different thread
- * concurrently with other operations.
+ * asynchronously. Synchronous calls like {@link #getCurrentUserConfigurationInternal()},
+ * {@link #generateMetricsState()} and debug calls like {@link
+ * #dump(IndentingPrintWriter, String[])}, may be called on a different thread concurrently with
+ * other operations.
*
* @hide
*/
@@ -123,4 +124,8 @@ public interface TimeZoneDetectorStrategy extends Dumpable, Dumpable.Container {
* suggestion.
*/
void suggestTelephonyTimeZone(@NonNull TelephonyTimeZoneSuggestion suggestion);
+
+ /** Generates a state snapshot for metrics. */
+ @NonNull
+ MetricsTimeZoneDetectorState generateMetricsState();
}
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java
index 2e96a1065af4c..5d34dd7daffb6 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java
@@ -371,6 +371,28 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat
}
}
+ @Override
+ @NonNull
+ public synchronized MetricsTimeZoneDetectorState generateMetricsState() {
+ int currentUserId = mEnvironment.getCurrentUserId();
+ // Just capture one telephony suggestion: the one that would be used right now if telephony
+ // detection is in use.
+ QualifiedTelephonyTimeZoneSuggestion bestQualifiedTelephonySuggestion =
+ findBestTelephonySuggestion();
+ TelephonyTimeZoneSuggestion telephonySuggestion =
+ bestQualifiedTelephonySuggestion == null
+ ? null : bestQualifiedTelephonySuggestion.suggestion;
+ // A new generator is created each time: we don't want / require consistency.
+ OrdinalGenerator tzIdOrdinalGenerator = new OrdinalGenerator<>();
+ return MetricsTimeZoneDetectorState.create(
+ tzIdOrdinalGenerator,
+ getConfigurationInternal(currentUserId),
+ mEnvironment.getDeviceTimeZone(),
+ getLatestManualSuggestion(),
+ telephonySuggestion,
+ getLatestGeolocationSuggestion());
+ }
+
private static int scoreTelephonySuggestion(@NonNull TelephonyTimeZoneSuggestion suggestion) {
int score;
if (suggestion.getZoneId() == null) {
diff --git a/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java b/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java
index 417a6368be4cb..4fa920e5b7d25 100644
--- a/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java
+++ b/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java
@@ -42,10 +42,12 @@ class BinderLocationTimeZoneProvider extends LocationTimeZoneProvider {
@NonNull private final LocationTimeZoneProviderProxy mProxy;
BinderLocationTimeZoneProvider(
+ @NonNull ProviderMetricsLogger providerMetricsLogger,
@NonNull ThreadingDomain threadingDomain,
@NonNull String providerName,
@NonNull LocationTimeZoneProviderProxy proxy) {
- super(threadingDomain, providerName, new ZoneInfoDbTimeZoneIdValidator());
+ super(providerMetricsLogger, threadingDomain, providerName,
+ new ZoneInfoDbTimeZoneIdValidator());
mProxy = Objects.requireNonNull(proxy);
}
diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java
index 588382158bc9f..ca4a6408cfbb5 100644
--- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java
+++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java
@@ -45,6 +45,7 @@ import com.android.server.FgThread;
import com.android.server.SystemService;
import com.android.server.timezonedetector.ServiceConfigAccessor;
import com.android.server.timezonedetector.TimeZoneDetectorInternal;
+import com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderMetricsLogger;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -297,7 +298,9 @@ public class LocationTimeZoneManagerService extends Binder {
R.string.config_primaryLocationTimeZoneProviderPackageName
);
}
- return new BinderLocationTimeZoneProvider(mThreadingDomain, PRIMARY_PROVIDER_NAME, proxy);
+ ProviderMetricsLogger providerMetricsLogger = new RealProviderMetricsLogger(0);
+ return new BinderLocationTimeZoneProvider(
+ providerMetricsLogger, mThreadingDomain, PRIMARY_PROVIDER_NAME, proxy);
}
@NonNull
@@ -317,7 +320,9 @@ public class LocationTimeZoneManagerService extends Binder {
R.string.config_secondaryLocationTimeZoneProviderPackageName
);
}
- return new BinderLocationTimeZoneProvider(mThreadingDomain, SECONDARY_PROVIDER_NAME, proxy);
+ ProviderMetricsLogger providerMetricsLogger = new RealProviderMetricsLogger(1);
+ return new BinderLocationTimeZoneProvider(
+ providerMetricsLogger, mThreadingDomain, SECONDARY_PROVIDER_NAME, proxy);
}
/** Used for bug triage and in tests to simulate provider events. */
diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java
index b97c838017eb5..cc815dc618868 100644
--- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java
+++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java
@@ -97,6 +97,14 @@ abstract class LocationTimeZoneProvider implements Dumpable {
boolean isValid(@NonNull String timeZoneId);
}
+ /**
+ * Listener interface used to log provider events for metrics.
+ */
+ interface ProviderMetricsLogger {
+ /** Logs that a provider changed state. */
+ void onProviderStateChanged(@ProviderStateEnum int stateEnum);
+ }
+
/**
* Information about the provider's current state.
*/
@@ -349,6 +357,7 @@ abstract class LocationTimeZoneProvider implements Dumpable {
}
}
+ @NonNull private final ProviderMetricsLogger mProviderMetricsLogger;
@NonNull final ThreadingDomain mThreadingDomain;
@NonNull final Object mSharedLock;
@NonNull final String mProviderName;
@@ -380,10 +389,12 @@ abstract class LocationTimeZoneProvider implements Dumpable {
@NonNull private TimeZoneIdValidator mTimeZoneIdValidator;
/** Creates the instance. */
- LocationTimeZoneProvider(@NonNull ThreadingDomain threadingDomain,
+ LocationTimeZoneProvider(@NonNull ProviderMetricsLogger providerMetricsLogger,
+ @NonNull ThreadingDomain threadingDomain,
@NonNull String providerName,
@NonNull TimeZoneIdValidator timeZoneIdValidator) {
mThreadingDomain = Objects.requireNonNull(threadingDomain);
+ mProviderMetricsLogger = Objects.requireNonNull(providerMetricsLogger);
mInitializationTimeoutQueue = threadingDomain.createSingleRunnableQueue();
mSharedLock = threadingDomain.getLockObject();
mProviderName = Objects.requireNonNull(providerName);
@@ -485,6 +496,7 @@ abstract class LocationTimeZoneProvider implements Dumpable {
mCurrentState.set(newState);
onSetCurrentState(newState);
if (!Objects.equals(newState, oldState)) {
+ mProviderMetricsLogger.onProviderStateChanged(newState.stateEnum);
if (mStateChangeRecording) {
mRecordedStates.add(newState);
}
diff --git a/services/core/java/com/android/server/timezonedetector/location/RealProviderMetricsLogger.java b/services/core/java/com/android/server/timezonedetector/location/RealProviderMetricsLogger.java
new file mode 100644
index 0000000000000..dfff6f2dd5ae6
--- /dev/null
+++ b/services/core/java/com/android/server/timezonedetector/location/RealProviderMetricsLogger.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2021 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.timezonedetector.location;
+
+import android.annotation.IntRange;
+
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderMetricsLogger;
+import com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.ProviderStateEnum;
+
+/**
+ * The real implementation of {@link ProviderMetricsLogger} which logs using
+ * {@link FrameworkStatsLog}.
+ */
+public class RealProviderMetricsLogger implements ProviderMetricsLogger {
+
+ @IntRange(from = 0, to = 1)
+ private final int mProviderIndex;
+
+ public RealProviderMetricsLogger(@IntRange(from = 0, to = 1) int providerIndex) {
+ mProviderIndex = providerIndex;
+ }
+
+ @Override
+ public void onProviderStateChanged(@ProviderStateEnum int stateEnum) {
+ // TODO(b/172934905): Implement once the atom has landed.
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/FakeTimeZoneDetectorStrategy.java b/services/tests/servicestests/src/com/android/server/timezonedetector/FakeTimeZoneDetectorStrategy.java
index bad380acf4b39..51f627ab415c8 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/FakeTimeZoneDetectorStrategy.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/FakeTimeZoneDetectorStrategy.java
@@ -117,6 +117,11 @@ class FakeTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy {
mLastTelephonySuggestion = timeZoneSuggestion;
}
+ @Override
+ public MetricsTimeZoneDetectorState generateMetricsState() {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public void addDumpable(Dumpable dumpable) {
mDumpables.add(dumpable);
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/OrdinalGeneratorTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/OrdinalGeneratorTest.java
new file mode 100644
index 0000000000000..af954d599334e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/OrdinalGeneratorTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 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.timezonedetector;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+public class OrdinalGeneratorTest {
+
+ @Test
+ public void testOrdinal() {
+ OrdinalGenerator ordinalGenerator = new OrdinalGenerator<>();
+ int oneOrd = ordinalGenerator.ordinal("One");
+ int twoOrd = ordinalGenerator.ordinal("Two");
+ assertNotEquals(oneOrd, twoOrd);
+
+ assertEquals(oneOrd, ordinalGenerator.ordinal("One"));
+ assertEquals(twoOrd, ordinalGenerator.ordinal("Two"));
+
+ int threeOrd = ordinalGenerator.ordinal("Three");
+ assertNotEquals(oneOrd, threeOrd);
+ assertNotEquals(twoOrd, threeOrd);
+ }
+
+ @Test
+ public void testOrdinals() {
+ OrdinalGenerator ordinalGenerator = new OrdinalGenerator<>();
+ int[] oneTwoOrds = ordinalGenerator.ordinals(Arrays.asList("One", "Two"));
+ int[] twoThreeOrds = ordinalGenerator.ordinals(Arrays.asList("Two", "Three"));
+ assertEquals(oneTwoOrds[0], ordinalGenerator.ordinal("One"));
+ assertEquals(oneTwoOrds[1], ordinalGenerator.ordinal("Two"));
+ assertEquals(twoThreeOrds[0], ordinalGenerator.ordinal("Two"));
+ assertEquals(twoThreeOrds[1], ordinalGenerator.ordinal("Three"));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
index b0341d7d67d54..f91ce87e8f08b 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
@@ -925,6 +925,106 @@ public class TimeZoneDetectorStrategyImplTest {
assertTrue(dumpCalled.get());
}
+ @Test
+ public void testGenerateMetricsState() {
+ ConfigurationInternal expectedInternalConfig = CONFIG_INT_AUTO_DISABLED_GEO_DISABLED;
+ String expectedDeviceTimeZoneId = "InitialZoneId";
+
+ Script script = new Script()
+ .initializeConfig(expectedInternalConfig)
+ .initializeTimeZoneSetting(expectedDeviceTimeZoneId);
+
+ assertMetricsState(expectedInternalConfig, expectedDeviceTimeZoneId, null, null,
+ null, MetricsTimeZoneDetectorState.DETECTION_MODE_MANUAL);
+
+ // Make sure the manual suggestion is recorded.
+ ManualTimeZoneSuggestion manualSuggestion = createManualSuggestion("Zone1");
+ script.simulateManualTimeZoneSuggestion(USER_ID, manualSuggestion,
+ true /* expectedResult */)
+ .verifyTimeZoneChangedAndReset(manualSuggestion);
+ expectedDeviceTimeZoneId = manualSuggestion.getZoneId();
+ assertMetricsState(expectedInternalConfig, expectedDeviceTimeZoneId,
+ manualSuggestion, null, null,
+ MetricsTimeZoneDetectorState.DETECTION_MODE_MANUAL);
+
+ // With time zone auto detection off, telephony suggestions will be recorded, but geo
+ // suggestions won't out of an abundance of caution around respecting user privacy when
+ // geo detection is off.
+ TelephonyTimeZoneSuggestion telephonySuggestion =
+ createTelephonySuggestion(0 /* slotIndex */, MATCH_TYPE_NETWORK_COUNTRY_ONLY,
+ QUALITY_SINGLE_ZONE, "Zone2");
+ GeolocationTimeZoneSuggestion geolocationTimeZoneSuggestion =
+ createGeoLocationSuggestion(Arrays.asList("Zone3", "Zone2"));
+ script.simulateTelephonyTimeZoneSuggestion(telephonySuggestion)
+ .verifyTimeZoneNotChanged()
+ .simulateGeolocationTimeZoneSuggestion(geolocationTimeZoneSuggestion)
+ .verifyTimeZoneNotChanged();
+
+ assertMetricsState(expectedInternalConfig, expectedDeviceTimeZoneId,
+ manualSuggestion, telephonySuggestion, null /* expectedGeoSuggestion */,
+ MetricsTimeZoneDetectorState.DETECTION_MODE_MANUAL);
+
+ // Update the config and confirm that the config metrics state updates also.
+ TimeZoneConfiguration configUpdate =
+ createConfig(true /* autoDetection */, true /* geoDetection */);
+ expectedInternalConfig = new ConfigurationInternal.Builder(expectedInternalConfig)
+ .setAutoDetectionEnabled(true)
+ .setGeoDetectionEnabled(true)
+ .build();
+ script.simulateUpdateConfiguration(USER_ID, configUpdate, true /* expectedResult */)
+ .verifyConfigurationChangedAndReset(expectedInternalConfig)
+ .verifyTimeZoneNotChanged();
+ assertMetricsState(expectedInternalConfig, expectedDeviceTimeZoneId,
+ manualSuggestion, telephonySuggestion, null /* expectedGeoSuggestion */,
+ MetricsTimeZoneDetectorState.DETECTION_MODE_GEO);
+
+ // Now simulate a geo suggestion and confirm it is used and reported in the metrics too.
+ expectedDeviceTimeZoneId = geolocationTimeZoneSuggestion.getZoneIds().get(0);
+ script.simulateGeolocationTimeZoneSuggestion(geolocationTimeZoneSuggestion)
+ .verifyTimeZoneChangedAndReset(expectedDeviceTimeZoneId);
+ assertMetricsState(expectedInternalConfig, expectedDeviceTimeZoneId,
+ manualSuggestion, telephonySuggestion, geolocationTimeZoneSuggestion,
+ MetricsTimeZoneDetectorState.DETECTION_MODE_GEO);
+ }
+
+ /**
+ * Asserts that the information returned by {@link
+ * TimeZoneDetectorStrategy#generateMetricsState()} matches expectations.
+ */
+ private void assertMetricsState(
+ ConfigurationInternal expectedInternalConfig,
+ String expectedDeviceTimeZoneId, ManualTimeZoneSuggestion expectedManualSuggestion,
+ TelephonyTimeZoneSuggestion expectedTelephonySuggestion,
+ GeolocationTimeZoneSuggestion expectedGeolocationTimeZoneSuggestion,
+ int expectedDetectionMode) {
+
+ MetricsTimeZoneDetectorState actualState = mTimeZoneDetectorStrategy.generateMetricsState();
+
+ // Check the various feature state values are what we expect.
+ assertFeatureStateMatchesConfig(expectedInternalConfig, actualState, expectedDetectionMode);
+
+ OrdinalGenerator tzIdOrdinalGenerator = new OrdinalGenerator<>();
+ MetricsTimeZoneDetectorState expectedState =
+ MetricsTimeZoneDetectorState.create(
+ tzIdOrdinalGenerator, expectedInternalConfig, expectedDeviceTimeZoneId,
+ expectedManualSuggestion, expectedTelephonySuggestion,
+ expectedGeolocationTimeZoneSuggestion);
+ // Rely on MetricsTimeZoneDetectorState.equals() for time zone ID ordinal comparisons.
+ assertEquals(expectedState, actualState);
+ }
+
+ private static void assertFeatureStateMatchesConfig(ConfigurationInternal config,
+ MetricsTimeZoneDetectorState actualState, int expectedDetectionMode) {
+ assertEquals(config.isTelephonyDetectionSupported(),
+ actualState.isTelephonyDetectionSupported());
+ assertEquals(config.isGeoDetectionSupported(), actualState.isGeoDetectionSupported());
+ assertEquals(config.getAutoDetectionEnabledSetting(),
+ actualState.getAutoDetectionEnabledSetting());
+ assertEquals(config.getGeoDetectionEnabledSetting(),
+ actualState.getGeoDetectionEnabledSetting());
+ assertEquals(expectedDetectionMode, actualState.getDetectionMode());
+ }
+
private static ManualTimeZoneSuggestion createManualSuggestion(String zoneId) {
return new ManualTimeZoneSuggestion(zoneId);
}
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/ControllerImplTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/ControllerImplTest.java
index 3daa7f0483c67..5a100a297cfc3 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/ControllerImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/ControllerImplTest.java
@@ -79,15 +79,18 @@ public class ControllerImplTest {
// For simplicity, the TestThreadingDomain uses the test's main thread. To execute posted
// runnables, the test must call methods on mTestThreadingDomain otherwise those runnables
// will never get a chance to execute.
+ LocationTimeZoneProvider.ProviderMetricsLogger stubbedProviderMetricsLogger = stateEnum -> {
+ // Stubbed.
+ };
mTestThreadingDomain = new TestThreadingDomain();
mTestCallback = new TestCallback(mTestThreadingDomain);
mTimeZoneAvailabilityChecker = new FakeTimeZoneIdValidator();
- mTestPrimaryLocationTimeZoneProvider =
- new TestLocationTimeZoneProvider(
- mTestThreadingDomain, "primary", mTimeZoneAvailabilityChecker);
- mTestSecondaryLocationTimeZoneProvider =
- new TestLocationTimeZoneProvider(
- mTestThreadingDomain, "secondary", mTimeZoneAvailabilityChecker);
+ mTestPrimaryLocationTimeZoneProvider = new TestLocationTimeZoneProvider(
+ stubbedProviderMetricsLogger, mTestThreadingDomain, "primary",
+ mTimeZoneAvailabilityChecker);
+ mTestSecondaryLocationTimeZoneProvider = new TestLocationTimeZoneProvider(
+ stubbedProviderMetricsLogger, mTestThreadingDomain, "secondary",
+ mTimeZoneAvailabilityChecker);
}
@Test
@@ -1181,10 +1184,11 @@ public class ControllerImplTest {
/**
* Creates the instance.
*/
- TestLocationTimeZoneProvider(ThreadingDomain threadingDomain,
- String providerName,
+ TestLocationTimeZoneProvider(ProviderMetricsLogger providerMetricsLogger,
+ ThreadingDomain threadingDomain, String providerName,
TimeZoneIdValidator timeZoneIdValidator) {
- super(threadingDomain, providerName, timeZoneIdValidator);
+ super(providerMetricsLogger, threadingDomain, providerName,
+ timeZoneIdValidator);
}
public void setFailDuringInitialization(boolean failInitialization) {
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java
index 278fdaff260fe..d13a04e13406d 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java
@@ -53,6 +53,7 @@ import org.junit.Test;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashSet;
+import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
@@ -79,19 +80,18 @@ public class LocationTimeZoneProviderTest {
@Test
public void lifecycle() {
String providerName = "arbitrary";
- TestLocationTimeZoneProvider provider =
- new TestLocationTimeZoneProvider(
- mTestThreadingDomain,
- providerName,
- mTimeZoneAvailabilityChecker);
+ RecordingProviderMetricsLogger providerMetricsLogger = new RecordingProviderMetricsLogger();
+ TestLocationTimeZoneProvider provider = new TestLocationTimeZoneProvider(
+ providerMetricsLogger, mTestThreadingDomain, providerName,
+ mTimeZoneAvailabilityChecker);
mTimeZoneAvailabilityChecker.validIds("Europe/London");
// initialize()
provider.initialize(mProviderListener);
provider.assertOnInitializeCalled();
- ProviderState currentState = provider.getCurrentState();
- assertEquals(PROVIDER_STATE_STOPPED, currentState.stateEnum);
+ ProviderState currentState = assertAndReturnProviderState(
+ provider, providerMetricsLogger, PROVIDER_STATE_STOPPED);
assertNull(currentState.currentUserConfiguration);
assertSame(provider, currentState.provider);
mTestThreadingDomain.assertQueueEmpty();
@@ -105,9 +105,9 @@ public class LocationTimeZoneProviderTest {
provider.assertOnStartCalled(arbitraryInitializationTimeout);
- currentState = provider.getCurrentState();
+ currentState = assertAndReturnProviderState(
+ provider, providerMetricsLogger, PROVIDER_STATE_STARTED_INITIALIZING);
assertSame(provider, currentState.provider);
- assertEquals(PROVIDER_STATE_STARTED_INITIALIZING, currentState.stateEnum);
assertEquals(config, currentState.currentUserConfiguration);
assertNull(currentState.event);
// The initialization timeout should be queued.
@@ -129,9 +129,9 @@ public class LocationTimeZoneProviderTest {
TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(suggestion);
provider.simulateProviderEventReceived(event);
- currentState = provider.getCurrentState();
+ currentState = assertAndReturnProviderState(
+ provider, providerMetricsLogger, PROVIDER_STATE_STARTED_CERTAIN);
assertSame(provider, currentState.provider);
- assertEquals(PROVIDER_STATE_STARTED_CERTAIN, currentState.stateEnum);
assertEquals(event, currentState.event);
assertEquals(config, currentState.currentUserConfiguration);
mTestThreadingDomain.assertQueueEmpty();
@@ -141,9 +141,9 @@ public class LocationTimeZoneProviderTest {
event = TimeZoneProviderEvent.createUncertainEvent();
provider.simulateProviderEventReceived(event);
- currentState = provider.getCurrentState();
+ currentState = assertAndReturnProviderState(
+ provider, providerMetricsLogger, PROVIDER_STATE_STARTED_UNCERTAIN);
assertSame(provider, currentState.provider);
- assertEquals(PROVIDER_STATE_STARTED_UNCERTAIN, currentState.stateEnum);
assertEquals(event, currentState.event);
assertEquals(config, currentState.currentUserConfiguration);
mTestThreadingDomain.assertQueueEmpty();
@@ -153,7 +153,8 @@ public class LocationTimeZoneProviderTest {
provider.stopUpdates();
provider.assertOnStopUpdatesCalled();
- currentState = provider.getCurrentState();
+ currentState = assertAndReturnProviderState(
+ provider, providerMetricsLogger, PROVIDER_STATE_STOPPED);
assertSame(provider, currentState.provider);
assertEquals(PROVIDER_STATE_STOPPED, currentState.stateEnum);
assertNull(currentState.event);
@@ -171,11 +172,10 @@ public class LocationTimeZoneProviderTest {
@Test
public void defaultHandleTestCommandImpl() {
String providerName = "primary";
- TestLocationTimeZoneProvider provider =
- new TestLocationTimeZoneProvider(
- mTestThreadingDomain,
- providerName,
- mTimeZoneAvailabilityChecker);
+ StubbedProviderMetricsLogger providerMetricsLogger = new StubbedProviderMetricsLogger();
+ TestLocationTimeZoneProvider provider = new TestLocationTimeZoneProvider(
+ providerMetricsLogger, mTestThreadingDomain, providerName,
+ mTimeZoneAvailabilityChecker);
TestCommand testCommand = TestCommand.createForTests("test", new Bundle());
AtomicReference resultReference = new AtomicReference<>();
@@ -191,11 +191,10 @@ public class LocationTimeZoneProviderTest {
@Test
public void stateRecording() {
String providerName = "primary";
- TestLocationTimeZoneProvider provider =
- new TestLocationTimeZoneProvider(
- mTestThreadingDomain,
- providerName,
- mTimeZoneAvailabilityChecker);
+ StubbedProviderMetricsLogger providerMetricsLogger = new StubbedProviderMetricsLogger();
+ TestLocationTimeZoneProvider provider = new TestLocationTimeZoneProvider(
+ providerMetricsLogger, mTestThreadingDomain, providerName,
+ mTimeZoneAvailabilityChecker);
provider.setStateChangeRecordingEnabled(true);
mTimeZoneAvailabilityChecker.validIds("Europe/London");
@@ -237,11 +236,10 @@ public class LocationTimeZoneProviderTest {
@Test
public void considerSuggestionWithInvalidTimeZoneIdsAsUncertain() {
String providerName = "primary";
- TestLocationTimeZoneProvider provider =
- new TestLocationTimeZoneProvider(
- mTestThreadingDomain,
- providerName,
- mTimeZoneAvailabilityChecker);
+ StubbedProviderMetricsLogger providerMetricsLogger = new StubbedProviderMetricsLogger();
+ TestLocationTimeZoneProvider provider = new TestLocationTimeZoneProvider(
+ providerMetricsLogger, mTestThreadingDomain, providerName,
+ mTimeZoneAvailabilityChecker);
provider.setStateChangeRecordingEnabled(true);
provider.initialize(mProviderListener);
@@ -285,6 +283,20 @@ public class LocationTimeZoneProviderTest {
}
}
+ /**
+ * Returns the provider's state after asserting that the current state matches what is expected.
+ * This also asserts that the metrics logger was informed of the state change.
+ */
+ private static ProviderState assertAndReturnProviderState(
+ TestLocationTimeZoneProvider provider,
+ RecordingProviderMetricsLogger providerMetricsLogger, int expectedStateEnum) {
+ ProviderState currentState = provider.getCurrentState();
+ assertEquals(expectedStateEnum, currentState.stateEnum);
+ providerMetricsLogger.assertChangeLoggedAndRemove(expectedStateEnum);
+ providerMetricsLogger.assertNoMoreLogEntries();
+ return currentState;
+ }
+
private static class TestLocationTimeZoneProvider extends LocationTimeZoneProvider {
private boolean mOnInitializeCalled;
@@ -294,10 +306,11 @@ public class LocationTimeZoneProviderTest {
private boolean mOnStopUpdatesCalled;
/** Creates the instance. */
- TestLocationTimeZoneProvider(@NonNull ThreadingDomain threadingDomain,
+ TestLocationTimeZoneProvider(@NonNull ProviderMetricsLogger providerMetricsLogger,
+ @NonNull ThreadingDomain threadingDomain,
@NonNull String providerName,
@NonNull TimeZoneIdValidator timeZoneIdValidator) {
- super(threadingDomain, providerName, timeZoneIdValidator);
+ super(providerMetricsLogger, threadingDomain, providerName, timeZoneIdValidator);
}
@Override
@@ -366,6 +379,36 @@ public class LocationTimeZoneProviderTest {
public void validIds(String... timeZoneIdss) {
mValidTimeZoneIds.addAll(asList(timeZoneIdss));
}
+ }
+ private static class StubbedProviderMetricsLogger implements
+ LocationTimeZoneProvider.ProviderMetricsLogger {
+
+ @Override
+ public void onProviderStateChanged(int stateEnum) {
+ // Stubbed
+ }
+ }
+
+ private static class RecordingProviderMetricsLogger implements
+ LocationTimeZoneProvider.ProviderMetricsLogger {
+
+ private LinkedList mStates = new LinkedList<>();
+
+ @Override
+ public void onProviderStateChanged(int stateEnum) {
+ mStates.add(stateEnum);
+ }
+
+ public void assertChangeLoggedAndRemove(int expectedLoggedState) {
+ assertEquals("expected loggedState=" + expectedLoggedState
+ + " but states logged were=" + mStates,
+ (Integer) expectedLoggedState, mStates.peekFirst());
+ mStates.removeFirst();
+ }
+
+ public void assertNoMoreLogEntries() {
+ assertTrue(mStates.isEmpty());
+ }
}
}