Merge "Add infrastructure for metrics" into sc-dev

This commit is contained in:
Neil Fuller
2021-03-12 17:23:34 +00:00
committed by Android (Google) Code Review
15 changed files with 747 additions and 47 deletions

View File

@@ -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.
*
* <p>
* Regarding time zone ID ordinals:
* <p>
* 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<String> 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<String> 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<String> 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<String> zoneIdOrdinalGenerator,
@Nullable GeolocationTimeZoneSuggestion geolocationSuggestion) {
if (geolocationSuggestion == null) {
return null;
}
List<String> 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)
+ '}';
}
}
}

View File

@@ -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<T> {
private final ArraySet<T> 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<T> objects) {
int[] ordinals = new int[objects.size()];
for (int i = 0; i < ordinals.length; i++) {
ordinals[i] = ordinal(objects.get(i));
}
return ordinals;
}
}

View File

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

View File

@@ -90,4 +90,10 @@ public final class TimeZoneDetectorInternalImpl implements TimeZoneDetectorInter
mHandler.post(
() -> mTimeZoneDetectorStrategy.suggestGeolocationTimeZone(timeZoneSuggestion));
}
@Override
@NonNull
public MetricsTimeZoneDetectorState generateMetricsState() {
return mTimeZoneDetectorStrategy.generateMetricsState();
}
}

View File

@@ -66,9 +66,10 @@ import android.util.IndentingPrintWriter;
* <p>Threading:
*
* <p>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();
}

View File

@@ -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<String> 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) {

View File

@@ -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);
}

View File

@@ -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. */

View File

@@ -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);
}

View File

@@ -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.
}
}

View File

@@ -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);

View File

@@ -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<String> 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<String> 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"));
}
}

View File

@@ -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<String> 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);
}

View File

@@ -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) {

View File

@@ -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<Bundle> 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<Integer> 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());
}
}
}