Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 27ddc8f4 authored by Neil Fuller's avatar Neil Fuller
Browse files

Add infrastructure for metrics

This contains refactoring and plumbing for adding metrics to the
time_zone_detector and location_time_zone_manager services.

Bug: 172934905
Test: build / boot / treehugger
Change-Id: I5517c78da10bcc5ddef78987ec5aedab59f3b73e
parent a455e386
Loading
Loading
Loading
Loading
+344 −0
Original line number Diff line number Diff line
/*
 * 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)
                    + '}';
        }
    }
}
+49 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+4 −0
Original line number Diff line number Diff line
@@ -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();
}
+6 −0
Original line number Diff line number Diff line
@@ -90,4 +90,10 @@ public final class TimeZoneDetectorInternalImpl implements TimeZoneDetectorInter
        mHandler.post(
                () -> mTimeZoneDetectorStrategy.suggestGeolocationTimeZone(timeZoneSuggestion));
    }

    @Override
    @NonNull
    public MetricsTimeZoneDetectorState generateMetricsState() {
        return mTimeZoneDetectorStrategy.generateMetricsState();
    }
}
+8 −3
Original line number Diff line number Diff line
@@ -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();
}
Loading