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

Commit b88709cc authored by Neil Fuller's avatar Neil Fuller
Browse files

A skeleton for geolocation time zone detection

A skeleton for geolocation time zone detection which will be filled out
in later commits. The
TimeZoneDetectorStrategyImpl.suggestGeolocationTimeZone() method is left
stubbed for now.

Bug: 149014708
Test: atest com.android.server.timezonedetector
Change-Id: I09f9025f1d1a0904f01797f305143e31112724f7
parent 9d053095
Loading
Loading
Loading
Loading
+38 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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 java.io.PrintWriter;

/** An interface for components that can write their internal state to dumpsys logs. */
public interface Dumpable {

    /** Dump internal state. */
    void dump(PrintWriter pw, String[] args);

    /**
     * An interface that can be used expose when one component allows another to be registered so
     * that it is dumped at the same time.
     */
    interface Dumpee {

        /**
         * Registers the supplied {@link Dumpable}. When the implementation is dumped
         * {@link Dumpable#dump(PrintWriter, String[])} should be called on the {@code dumpable}.
         */
        void addDumpable(Dumpable dumpable);
    }
}
+173 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.annotation.NonNull;
import android.annotation.Nullable;
import android.os.ShellCommand;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.StringTokenizer;

/**
 * A time zone suggestion from a geolocation source.
 *
 * <p> Geolocation-based suggestions have the following properties:
 *
 * <ul>
 *     <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.
 *     <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>
 *     <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
 *     in detection logic and is not considered in {@link #hashCode()} or {@link #equals(Object)}.
 *     </li>
 * </ul>
 *
 * @hide
 */
public final class GeolocationTimeZoneSuggestion {

    @NonNull private final List<String> mZoneIds;
    @Nullable private ArrayList<String> mDebugInfo;

    public GeolocationTimeZoneSuggestion(@Nullable List<String> zoneIds) {
        if (zoneIds == null) {
            // Unopinionated
            mZoneIds = null;
        } else {
            mZoneIds = Collections.unmodifiableList(new ArrayList<>(zoneIds));
        }
    }

    /**
     * Returns the zone Ids being suggested. See {@link GeolocationTimeZoneSuggestion} for details.
     */
    @Nullable
    public List<String> getZoneIds() {
        return mZoneIds;
    }

    /** Returns debug information. See {@link GeolocationTimeZoneSuggestion} for details. */
    @NonNull
    public List<String> getDebugInfo() {
        return mDebugInfo == null
                ? Collections.emptyList() : Collections.unmodifiableList(mDebugInfo);
    }

    /**
     * Associates information with the instance that can be useful for debugging / logging. The
     * information is present in {@link #toString()} but is not considered for
     * {@link #equals(Object)} and {@link #hashCode()}.
     */
    public void addDebugInfo(String... debugInfos) {
        if (mDebugInfo == null) {
            mDebugInfo = new ArrayList<>();
        }
        mDebugInfo.addAll(Arrays.asList(debugInfos));
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        GeolocationTimeZoneSuggestion
                that = (GeolocationTimeZoneSuggestion) o;
        return Objects.equals(mZoneIds, that.mZoneIds);
    }

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

    @Override
    public String toString() {
        return "GeolocationTimeZoneSuggestion{"
                + "mZoneIds=" + mZoneIds
                + ", mDebugInfo=" + mDebugInfo
                + '}';
    }

    /** @hide */
    public static GeolocationTimeZoneSuggestion parseCommandLineArg(@NonNull ShellCommand cmd) {
        String zoneIdsString = null;
        String opt;
        while ((opt = cmd.getNextArg()) != null) {
            switch (opt) {
                case "--zone_ids": {
                    zoneIdsString  = cmd.getNextArgRequired();
                    break;
                }
                default: {
                    throw new IllegalArgumentException("Unknown option: " + opt);
                }
            }
        }
        List<String> zoneIds = parseZoneIdsArg(zoneIdsString);
        GeolocationTimeZoneSuggestion suggestion = new GeolocationTimeZoneSuggestion(zoneIds);
        suggestion.addDebugInfo("Command line injection");
        return suggestion;
    }

    private static List<String> parseZoneIdsArg(String zoneIdsString) {
        if ("UNCERTAIN".equals(zoneIdsString)) {
            return null;
        } else if ("EMPTY".equals(zoneIdsString)) {
            return Collections.emptyList();
        } else {
            ArrayList<String> zoneIds = new ArrayList<>();
            StringTokenizer tokenizer = new StringTokenizer(zoneIdsString, ",");
            while (tokenizer.hasMoreTokens()) {
                zoneIds.add(tokenizer.nextToken());
            }
            return zoneIds;
        }
    }

    /** @hide */
    public static void printCommandLineOpts(@NonNull PrintWriter pw) {
        pw.println("Geolocation suggestion options:");
        pw.println("  --zone_ids {UNCERTAIN|EMPTY|<Olson ID>+}");
        pw.println();
        pw.println("See " + GeolocationTimeZoneSuggestion.class.getName()
                + " for more information");
    }
}
+35 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.annotation.NonNull;

/**
 * The internal (in-process) system server API for the {@link
 * com.android.server.timezonedetector.TimeZoneDetectorService}.
 *
 * @hide
 */
public interface TimeZoneDetectorInternal extends Dumpable.Dumpee {

    /**
     * Suggests the current time zone, determined using geolocation, to the detector. The
     * detector may ignore the signal based on system settings, whether better information is
     * available, and so on.
     */
    void suggestGeolocationTimeZone(@NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion);
}
+64 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.annotation.NonNull;
import android.content.Context;
import android.os.Handler;

import com.android.internal.annotations.VisibleForTesting;

import java.util.Objects;

/**
 * The real {@link TimeZoneDetectorInternal} local service implementation.
 *
 * @hide
 */
public final class TimeZoneDetectorInternalImpl implements TimeZoneDetectorInternal {

    @NonNull private final Context mContext;
    @NonNull private final Handler mHandler;
    @NonNull private final TimeZoneDetectorStrategy mTimeZoneDetectorStrategy;

    static TimeZoneDetectorInternalImpl create(@NonNull Context context, @NonNull Handler handler,
            @NonNull TimeZoneDetectorStrategy timeZoneDetectorStrategy) {
        return new TimeZoneDetectorInternalImpl(context, handler, timeZoneDetectorStrategy);
    }

    @VisibleForTesting
    public TimeZoneDetectorInternalImpl(@NonNull Context context, @NonNull Handler handler,
            @NonNull TimeZoneDetectorStrategy timeZoneDetectorStrategy) {
        mContext = Objects.requireNonNull(context);
        mHandler = Objects.requireNonNull(handler);
        mTimeZoneDetectorStrategy = Objects.requireNonNull(timeZoneDetectorStrategy);
    }

    @Override
    public void addDumpable(Dumpable dumpable) {
        mTimeZoneDetectorStrategy.addDumpable(dumpable);
    }

    @Override
    public void suggestGeolocationTimeZone(
            @NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion) {
        Objects.requireNonNull(timeZoneSuggestion);

        mHandler.post(
                () -> mTimeZoneDetectorStrategy.suggestGeolocationTimeZone(timeZoneSuggestion));
    }
}
+36 −7
Original line number Diff line number Diff line
@@ -63,9 +63,10 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub
    private static final String TAG = "TimeZoneDetectorService";

    /**
     * Handles the lifecycle for {@link TimeZoneDetectorService}.
     * Handles the service lifecycle for {@link TimeZoneDetectorService} and
     * {@link TimeZoneDetectorInternalImpl}.
     */
    public static class Lifecycle extends SystemService {
    public static final class Lifecycle extends SystemService {

        public Lifecycle(@NonNull Context context) {
            super(context);
@@ -73,10 +74,21 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub

        @Override
        public void onStart() {
            TimeZoneDetectorService service = TimeZoneDetectorService.create(getContext());
            // Obtain / create the shared dependencies.
            Context context = getContext();
            Handler handler = FgThread.getHandler();
            TimeZoneDetectorStrategy timeZoneDetectorStrategy =
                    TimeZoneDetectorStrategyImpl.create(context);

            // Create and publish the local service for use by internal callers.
            TimeZoneDetectorInternal internal =
                    TimeZoneDetectorInternalImpl.create(context, handler, timeZoneDetectorStrategy);
            publishLocalService(TimeZoneDetectorInternal.class, internal);

            // Publish the binder service so it can be accessed from other (appropriately
            // permissioned) processes.
            TimeZoneDetectorService service =
                    TimeZoneDetectorService.create(context, handler, timeZoneDetectorStrategy);
            publishBinderService(Context.TIME_ZONE_DETECTOR_SERVICE, service);
        }
    }
@@ -94,11 +106,10 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub
    @NonNull
    private final ArrayList<ConfigListenerInfo> mConfigurationListeners = new ArrayList<>();

    private static TimeZoneDetectorService create(@NonNull Context context) {
        final TimeZoneDetectorStrategy timeZoneDetectorStrategy =
                TimeZoneDetectorStrategyImpl.create(context);
    private static TimeZoneDetectorService create(
            @NonNull Context context, @NonNull Handler handler,
            @NonNull TimeZoneDetectorStrategy timeZoneDetectorStrategy) {

        Handler handler = FgThread.getHandler();
        TimeZoneDetectorService service =
                new TimeZoneDetectorService(context, handler, timeZoneDetectorStrategy);

@@ -224,6 +235,16 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub
        }
    }

    /** Provided for command-line access. This is not exposed as a binder API. */
    void suggestGeolocationTimeZone(
            @NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion) {
        enforceSuggestGeolocationTimeZonePermission();
        Objects.requireNonNull(timeZoneSuggestion);

        mHandler.post(
                () -> mTimeZoneDetectorStrategy.suggestGeolocationTimeZone(timeZoneSuggestion));
    }

    @Override
    public boolean suggestManualTimeZone(@NonNull ManualTimeZoneSuggestion timeZoneSuggestion) {
        enforceSuggestManualTimeZonePermission();
@@ -267,6 +288,14 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub
                "manage time and time zone configuration");
    }

    private void enforceSuggestGeolocationTimeZonePermission() {
        // The associated method is only used for the shell command interface, it's not possible to
        // call it via Binder, and Shell currently can set the time zone directly anyway.
        mContext.enforceCallingOrSelfPermission(
                android.Manifest.permission.SET_TIME_ZONE,
                "suggest geolocation time zone");
    }

    private void enforceSuggestTelephonyTimeZonePermission() {
        mContext.enforceCallingPermission(
                android.Manifest.permission.SUGGEST_TELEPHONY_TIME_AND_ZONE,
Loading