blob: 27b50d89214416811c564884b93c21aa5a083db8 [file] [log] [blame]
/*
* 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) {
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)
+ '}';
}
}
}