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()); + } } }