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

Commit 87655fd9 authored by Neil Fuller's avatar Neil Fuller
Browse files

Add a time field to GeolocationTimeZoneSuggestion

Add a time field to GeolocationTimeZoneSuggestion for use in upcoming
fallback logic. This is should help establish ordering across
different time zone detection mechanisms.

Bug: 197624972
Test: treehugger
Test: frameworks/base: atest services/tests/servicestests/src/com/android/server/timezonedetector/
Test: cts: atest hostsidetests/time/host/
Change-Id: I824679f3eca39a69d5413d365889ad8566d3e557
parent 135703fe
Loading
Loading
Loading
Loading
+69 −18
Original line number Diff line number Diff line
@@ -16,9 +16,11 @@

package com.android.server.timezonedetector;

import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.ShellCommand;
import android.os.SystemClock;

import java.io.PrintWriter;
import java.util.ArrayList;
@@ -29,27 +31,39 @@ import java.util.Objects;
import java.util.StringTokenizer;

/**
 * A time zone suggestion from a geolocation source.
 * A time zone suggestion from the location_time_zone_manager service to the time_zone_detector
 * service.
 *
 * <p>Geolocation-based suggestions have the following properties:
 *
 * <ul>
 *     <li>{@code effectiveFromElapsedMillis}: The time according to the elapsed realtime clock
 *     after which the suggestion should be considered in effect. For example, when a location fix
 *     used to establish the time zone is old, then the suggestion
 *     {@code effectiveFromElapsedMillis} should reflect this and indicates the time zone that was
 *     detected / correct at that time. The time_zone_detector is only expected to use the latest
 *     suggestion it has received, and so later suggestions always counteract previous suggestions.
 *     The inclusion of this information means that the time_zone_detector can take into account
 *     ordering when comparing suggestions from different sources.
 *     <br />Note: Because the times can be back-dated, time_zone_detector can be sent a sequence of
 *     suggestions where the {@code effectiveFromElapsedMillis} of later suggestions is before
 *     the {@code effectiveFromElapsedMillis} of an earlier one.</li>
 *     <li>{@code zoneIds}. When not {@code null}, {@code zoneIds} contains a list of suggested time
 *     zone IDs, e.g. ["America/Phoenix", "America/Denver"]. Usually there will be a single zoneId.
 *     When there are multiple, this indicates multiple answers are possible for the current
 *     location / accuracy, i.e. if there is a nearby time zone border. The detection logic
 *     receiving the suggestion is expected to use the first element in the absence of other
 *     information, but one of the others may be used if there is supporting evidence / preferences
 *     such as a device setting or corroborating signals from another source.
 *     location / accuracy, e.g. if there is a nearby time zone border. The time_zone_detector is
 *     expected to use the first element in the absence of other information, but one of the other
 *     zone IDs may be used if there is supporting evidence / preferences such as a device setting
 *     or corroborating signals from another source.
 *     <br />{@code zoneIds} can be empty if the current location has been determined to have no
 *     time zone. For example, oceans or disputed areas. This is considered a strong signal and the
 *     received need not look for time zone from other sources.
 *     <br />{@code zoneIds} can be {@code null} to indicate that the geolocation source has entered
 *     an "un-opinionated" state and any previous suggestion is being withdrawn. This indicates the
 *     source cannot provide a valid suggestion due to technical limitations. For example, a
 *     geolocation source may become un-opinionated if the device's location is no longer known with
 *     sufficient accuracy, or if the location is known but no time zone can be determined because
 *     no time zone mapping information is available.</li>
 *     time_zone_detector need not look for time zone from other sources.
 *     <br />{@code zoneIds} can be {@code null} to indicate that the location_time_zone_manager has
 *     entered an "uncertain" state and any previous suggestion is being withdrawn. This indicates
 *     the location_time_zone_manager cannot provide a valid suggestion. For example, the
 *     location_time_zone_manager may become uncertain if components further downstream cannot
 *     determine the device's location with sufficient accuracy, or if the location is known but no
 *     time zone can be determined because no time zone mapping information is available.</li>
 *     <li>{@code debugInfo} contains debugging metadata associated with the suggestion. This is
 *     used to record why the suggestion exists and how it was obtained. This information exists
 *     only to aid in debugging and therefore is used by {@link #toString()}, but it is not for use
@@ -61,10 +75,14 @@ import java.util.StringTokenizer;
 */
public final class GeolocationTimeZoneSuggestion {

    @ElapsedRealtimeLong private final long mEffectiveFromElapsedMillis;
    @Nullable private final List<String> mZoneIds;
    @Nullable private ArrayList<String> mDebugInfo;

    public GeolocationTimeZoneSuggestion(@Nullable List<String> zoneIds) {
    private GeolocationTimeZoneSuggestion(
            @ElapsedRealtimeLong long effectiveFromElapsedMillis,
            @Nullable List<String> zoneIds) {
        mEffectiveFromElapsedMillis = effectiveFromElapsedMillis;
        if (zoneIds == null) {
            // Unopinionated
            mZoneIds = null;
@@ -73,6 +91,34 @@ public final class GeolocationTimeZoneSuggestion {
        }
    }

    /**
     * Creates a "uncertain" suggestion instance.
     */
    @NonNull
    public static GeolocationTimeZoneSuggestion createUncertainSuggestion(
            @ElapsedRealtimeLong long effectiveFromElapsedMillis) {
        return new GeolocationTimeZoneSuggestion(effectiveFromElapsedMillis, null);
    }

    /**
     * Creates a "certain" suggestion instance.
     */
    @NonNull
    public static GeolocationTimeZoneSuggestion createCertainSuggestion(
            @ElapsedRealtimeLong long effectiveFromElapsedMillis,
            @NonNull List<String> zoneIds) {
        return new GeolocationTimeZoneSuggestion(effectiveFromElapsedMillis, zoneIds);
    }

    /**
     * Returns the "effective from" time associated with the suggestion. See {@link
     * GeolocationTimeZoneSuggestion} for details.
     */
    @ElapsedRealtimeLong
    public long getEffectiveFromElapsedMillis() {
        return mEffectiveFromElapsedMillis;
    }

    /**
     * Returns the zone Ids being suggested. See {@link GeolocationTimeZoneSuggestion} for details.
     */
@@ -110,18 +156,20 @@ public final class GeolocationTimeZoneSuggestion {
        }
        GeolocationTimeZoneSuggestion
                that = (GeolocationTimeZoneSuggestion) o;
        return Objects.equals(mZoneIds, that.mZoneIds);
        return mEffectiveFromElapsedMillis == that.mEffectiveFromElapsedMillis
                && Objects.equals(mZoneIds, that.mZoneIds);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mZoneIds);
        return Objects.hash(mEffectiveFromElapsedMillis, mZoneIds);
    }

    @Override
    public String toString() {
        return "GeolocationTimeZoneSuggestion{"
                + "mZoneIds=" + mZoneIds
                + "mEffectiveFromElapsedMillis=" + mEffectiveFromElapsedMillis
                + ", mZoneIds=" + mZoneIds
                + ", mDebugInfo=" + mDebugInfo
                + '}';
    }
@@ -141,8 +189,11 @@ public final class GeolocationTimeZoneSuggestion {
                }
            }
        }

        long elapsedRealtimeMillis = SystemClock.elapsedRealtime();
        List<String> zoneIds = parseZoneIdsArg(zoneIdsString);
        GeolocationTimeZoneSuggestion suggestion = new GeolocationTimeZoneSuggestion(zoneIds);
        GeolocationTimeZoneSuggestion suggestion =
                new GeolocationTimeZoneSuggestion(elapsedRealtimeMillis, zoneIds);
        suggestion.addDebugInfo("Command line injection");
        return suggestion;
    }
+7 −0
Original line number Diff line number Diff line
@@ -16,7 +16,9 @@

package com.android.server.timezonedetector.location;

import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.os.SystemClock;

import com.android.server.LocalServices;
import com.android.server.timezonedetector.ConfigurationChangeListener;
@@ -83,4 +85,9 @@ class ControllerEnvironmentImpl extends LocationTimeZoneProviderController.Envir
    Duration getProviderEventFilteringAgeThreshold() {
        return mServiceConfigAccessor.getLocationTimeZoneProviderEventFilteringAgeThreshold();
    }

    @Override
    @ElapsedRealtimeLong long elapsedRealtimeMillis() {
        return SystemClock.elapsedRealtime();
    }
}
+70 −40
Original line number Diff line number Diff line
@@ -31,10 +31,11 @@ import static com.android.server.timezonedetector.location.LocationTimeZoneProvi
import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STOPPED;

import android.annotation.DurationMillisLong;
import android.annotation.IntRange;
import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.service.timezone.TimeZoneProviderEvent;
import android.service.timezone.TimeZoneProviderSuggestion;
import android.util.IndentingPrintWriter;

import com.android.internal.annotations.GuardedBy;
@@ -43,7 +44,6 @@ import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion;
import com.android.server.timezonedetector.location.ThreadingDomain.SingleRunnableQueue;

import java.time.Duration;
import java.util.List;
import java.util.Objects;

/**
@@ -184,7 +184,7 @@ class ControllerImpl extends LocationTimeZoneProviderController {
        // re-started).
        if (mLastSuggestion != null && mLastSuggestion.getZoneIds() != null) {
            GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion(
                    "Providers are stopping");
                    mEnvironment.elapsedRealtimeMillis(), "Providers are stopping");
            makeSuggestion(suggestion);
        }
    }
@@ -272,6 +272,7 @@ class ControllerImpl extends LocationTimeZoneProviderController {
                    // If both providers are {perm failed} then the controller immediately
                    // becomes uncertain.
                    GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion(
                            mEnvironment.elapsedRealtimeMillis(),
                            "Providers are failed:"
                                    + " primary=" + mPrimaryProvider.getCurrentState()
                                    + " secondary=" + mPrimaryProvider.getCurrentState());
@@ -410,6 +411,7 @@ class ControllerImpl extends LocationTimeZoneProviderController {
            // If both providers are now terminated, then a suggestion must be sent informing the
            // time zone detector that there are no further updates coming in future.
            GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion(
                    mEnvironment.elapsedRealtimeMillis(),
                    "Both providers are terminated:"
                            + " primary=" + primaryCurrentState.provider
                            + ", secondary=" + secondaryCurrentState.provider);
@@ -432,8 +434,9 @@ class ControllerImpl extends LocationTimeZoneProviderController {
            // the loss of a binder-based provider, or initialization took too long. This is treated
            // the same as explicit uncertainty, i.e. where the provider has explicitly told this
            // process it is uncertain.
            handleProviderUncertainty(provider, "provider=" + provider
                    + ", implicit uncertainty, event=null");
            long uncertaintyStartedElapsedMillis = mEnvironment.elapsedRealtimeMillis();
            handleProviderUncertainty(provider, uncertaintyStartedElapsedMillis,
                    "provider=" + provider + ", implicit uncertainty, event=null");
            return;
        }

@@ -448,18 +451,17 @@ class ControllerImpl extends LocationTimeZoneProviderController {
        switch (event.getType()) {
            case EVENT_TYPE_PERMANENT_FAILURE: {
                // This shouldn't happen. A provider cannot be started and have this event type.
                warnLog("Provider=" + provider
                        + " is started, but event suggests it shouldn't be");
                warnLog("Provider=" + provider + " is started, but event suggests it shouldn't be");
                break;
            }
            case EVENT_TYPE_UNCERTAIN: {
                handleProviderUncertainty(provider, "provider=" + provider
                        + ", explicit uncertainty. event=" + event);
                long uncertaintyStartedElapsedMillis = event.getCreationElapsedMillis();
                handleProviderUncertainty(provider, uncertaintyStartedElapsedMillis,
                        "provider=" + provider + ", explicit uncertainty. event=" + event);
                break;
            }
            case EVENT_TYPE_SUGGESTION: {
                handleProviderSuggestion(provider, event.getSuggestion().getTimeZoneIds(),
                        "Event received provider=" + provider + ", event=" + event);
                handleProviderSuggestion(provider, event);
                break;
            }
            default: {
@@ -475,8 +477,8 @@ class ControllerImpl extends LocationTimeZoneProviderController {
    @GuardedBy("mSharedLock")
    private void handleProviderSuggestion(
            @NonNull LocationTimeZoneProvider provider,
            @Nullable List<String> timeZoneIds,
            @NonNull String reason) {
            @NonNull TimeZoneProviderEvent providerEvent) {

        // By definition, the controller is now certain.
        cancelUncertaintyTimeout();

@@ -484,10 +486,25 @@ class ControllerImpl extends LocationTimeZoneProviderController {
            stopProviderIfStarted(mSecondaryProvider);
        }

        GeolocationTimeZoneSuggestion suggestion = new GeolocationTimeZoneSuggestion(timeZoneIds);
        suggestion.addDebugInfo(reason);
        // Rely on the receiver to dedupe suggestions. It is better to over-communicate.
        makeSuggestion(suggestion);
        TimeZoneProviderSuggestion providerSuggestion = providerEvent.getSuggestion();

        // For the suggestion's effectiveFromElapsedMillis, use the time embedded in the provider's
        // suggestion (which indicates the time when the provider detected the location used to
        // establish the time zone).
        //
        // An alternative would be to use the current time or the providerEvent creation time, but
        // this would hinder the ability for the time_zone_detector to judge which suggestions are
        // based on newer information when comparing suggestions between different sources.
        long effectiveFromElapsedMillis = providerSuggestion.getElapsedRealtimeMillis();
        GeolocationTimeZoneSuggestion geoSuggestion =
                GeolocationTimeZoneSuggestion.createCertainSuggestion(
                        effectiveFromElapsedMillis, providerSuggestion.getTimeZoneIds());

        String debugInfo = "Event received provider=" + provider
                + ", providerEvent=" + providerEvent
                + ", suggestionCreationTime=" + mEnvironment.elapsedRealtimeMillis();
        geoSuggestion.addDebugInfo(debugInfo);
        makeSuggestion(geoSuggestion);
    }

    @Override
@@ -551,7 +568,9 @@ class ControllerImpl extends LocationTimeZoneProviderController {
     */
    @GuardedBy("mSharedLock")
    void handleProviderUncertainty(
            @NonNull LocationTimeZoneProvider provider, @NonNull String reason) {
            @NonNull LocationTimeZoneProvider provider,
            @ElapsedRealtimeLong long uncertaintyStartedElapsedMillis,
            @NonNull String reason) {
        Objects.requireNonNull(provider);

        // Start the uncertainty timeout if needed to ensure the controller will eventually make an
@@ -559,9 +578,11 @@ class ControllerImpl extends LocationTimeZoneProviderController {
        if (!mUncertaintyTimeoutQueue.hasQueued()) {
            debugLog("Starting uncertainty timeout: reason=" + reason);

            Duration delay = mEnvironment.getUncertaintyDelay();
            mUncertaintyTimeoutQueue.runDelayed(() -> onProviderUncertaintyTimeout(provider),
                    delay.toMillis());
            Duration uncertaintyDelay = mEnvironment.getUncertaintyDelay();
            mUncertaintyTimeoutQueue.runDelayed(
                    () -> onProviderUncertaintyTimeout(
                            provider, uncertaintyStartedElapsedMillis, uncertaintyDelay),
                    uncertaintyDelay.toMillis());
        }

        if (provider == mPrimaryProvider) {
@@ -573,21 +594,45 @@ class ControllerImpl extends LocationTimeZoneProviderController {
        }
    }

    private void onProviderUncertaintyTimeout(@NonNull LocationTimeZoneProvider provider) {
    private void onProviderUncertaintyTimeout(
            @NonNull LocationTimeZoneProvider provider,
            @ElapsedRealtimeLong long uncertaintyStartedElapsedMillis,
            @NonNull Duration uncertaintyDelay) {
        mThreadingDomain.assertCurrentThread();

        synchronized (mSharedLock) {
            long afterUncertaintyTimeoutElapsedMillis = mEnvironment.elapsedRealtimeMillis();

            // For the effectiveFromElapsedMillis suggestion property, use the
            // uncertaintyStartedElapsedMillis. This is the time when the provider first reported
            // uncertainty, i.e. before the uncertainty timeout.
            //
            // afterUncertaintyTimeoutElapsedMillis could be used instead, which is the time when
            // the location_time_zone_manager finally confirms that the time zone was uncertain,
            // but the suggestion property allows the information to be back-dated, which should
            // help when comparing suggestions from different sources.
            GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion(
                    uncertaintyStartedElapsedMillis,
                    "Uncertainty timeout triggered for " + provider.getName() + ":"
                            + " primary=" + mPrimaryProvider
                            + ", secondary=" + mSecondaryProvider);
                            + ", secondary=" + mSecondaryProvider
                            + ", uncertaintyStarted="
                            + Duration.ofMillis(uncertaintyStartedElapsedMillis)
                            + ", afterUncertaintyTimeout="
                            + Duration.ofMillis(afterUncertaintyTimeoutElapsedMillis)
                            + ", uncertaintyDelay=" + uncertaintyDelay
            );
            makeSuggestion(suggestion);
        }
    }

    @NonNull
    private static GeolocationTimeZoneSuggestion createUncertainSuggestion(@NonNull String reason) {
        GeolocationTimeZoneSuggestion suggestion = new GeolocationTimeZoneSuggestion(null);
    private static GeolocationTimeZoneSuggestion createUncertainSuggestion(
            @ElapsedRealtimeLong long effectiveFromElapsedMillis,
            @NonNull String reason) {
        GeolocationTimeZoneSuggestion suggestion =
                GeolocationTimeZoneSuggestion.createUncertainSuggestion(
                        effectiveFromElapsedMillis);
        suggestion.addDebugInfo(reason);
        return suggestion;
    }
@@ -622,19 +667,4 @@ class ControllerImpl extends LocationTimeZoneProviderController {
            return builder.build();
        }
    }

    @Nullable
    private LocationTimeZoneProvider getLocationTimeZoneProvider(
            @IntRange(from = 0, to = 1) int providerIndex) {
        LocationTimeZoneProvider targetProvider;
        if (providerIndex == 0) {
            targetProvider = mPrimaryProvider;
        } else if (providerIndex == 1) {
            targetProvider = mSecondaryProvider;
        } else {
            warnLog("Bad providerIndex=" + providerIndex);
            targetProvider = null;
        }
        return targetProvider;
    }
}
+7 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.server.timezonedetector.location;

import android.annotation.DurationMillisLong;
import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.os.Handler;

@@ -140,6 +141,12 @@ abstract class LocationTimeZoneProviderController implements Dumpable {
         * passed on.
         */
        abstract Duration getUncertaintyDelay();

        /**
         * Returns the elapsed realtime as millis, the same as {@link
         * android.os.SystemClock#elapsedRealtime()}.
         */
        abstract @ElapsedRealtimeLong long elapsedRealtimeMillis();
    }

    /**
+27 −17
Original line number Diff line number Diff line
@@ -34,26 +34,36 @@ public class GeolocationTimeZoneSuggestionTest {

    @Test
    public void testEquals() {
        GeolocationTimeZoneSuggestion one = new GeolocationTimeZoneSuggestion(ARBITRARY_ZONE_IDS1);
        assertEquals(one, one);
        long time1 = 1111L;
        GeolocationTimeZoneSuggestion certain1v1 =
                GeolocationTimeZoneSuggestion.createCertainSuggestion(time1, ARBITRARY_ZONE_IDS1);
        assertEquals(certain1v1, certain1v1);

        GeolocationTimeZoneSuggestion two = new GeolocationTimeZoneSuggestion(ARBITRARY_ZONE_IDS1);
        assertEquals(one, two);
        assertEquals(two, one);
        GeolocationTimeZoneSuggestion certain1v2 =
                GeolocationTimeZoneSuggestion.createCertainSuggestion(time1, ARBITRARY_ZONE_IDS1);
        assertEquals(certain1v1, certain1v2);
        assertEquals(certain1v2, certain1v1);

        GeolocationTimeZoneSuggestion nullZone = new GeolocationTimeZoneSuggestion(null);
        assertNotEquals(one, nullZone);
        assertNotEquals(nullZone, one);
        assertEquals(nullZone, nullZone);
        // DebugInfo must not be considered in equals().
        certain1v1.addDebugInfo("Debug info 1");
        certain1v2.addDebugInfo("Debug info 2");
        assertEquals(certain1v1, certain1v2);

        GeolocationTimeZoneSuggestion three =
                new GeolocationTimeZoneSuggestion(ARBITRARY_ZONE_IDS2);
        assertNotEquals(one, three);
        assertNotEquals(three, one);
        long time2 = 2222L;
        GeolocationTimeZoneSuggestion certain2 =
                GeolocationTimeZoneSuggestion.createCertainSuggestion(time2, ARBITRARY_ZONE_IDS1);
        assertNotEquals(certain1v1, certain2);
        assertNotEquals(certain2, certain1v1);

        // DebugInfo must not be considered in equals().
        one.addDebugInfo("Debug info 1");
        two.addDebugInfo("Debug info 2");
        assertEquals(one, two);
        GeolocationTimeZoneSuggestion uncertain =
                GeolocationTimeZoneSuggestion.createUncertainSuggestion(time1);
        assertNotEquals(certain1v1, uncertain);
        assertNotEquals(uncertain, certain1v1);
        assertEquals(uncertain, uncertain);

        GeolocationTimeZoneSuggestion certain3 =
                GeolocationTimeZoneSuggestion.createCertainSuggestion(time1, ARBITRARY_ZONE_IDS2);
        assertNotEquals(certain1v1, certain3);
        assertNotEquals(certain3, certain1v1);
    }
}
Loading