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

Commit 90f6d7bf authored by Neil Fuller's avatar Neil Fuller Committed by Android (Google) Code Review
Browse files

Merge "A skeleton for geolocation time zone detection"

parents 7d5c3e4c b88709cc
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