diff --git a/core/java/android/app/time/DetectorStatusTypes.java b/core/java/android/app/time/DetectorStatusTypes.java new file mode 100644 index 0000000000000..3643fc9a7d86e --- /dev/null +++ b/core/java/android/app/time/DetectorStatusTypes.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2022 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 android.app.time; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.text.TextUtils; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A set of constants that can relate to time or time zone detector status. + * + * + * + * @hide + */ +public final class DetectorStatusTypes { + + /** A status code for a detector. */ + @IntDef(prefix = "DETECTOR_STATUS_", value = { + DETECTOR_STATUS_UNKNOWN, + DETECTOR_STATUS_NOT_SUPPORTED, + DETECTOR_STATUS_NOT_RUNNING, + DETECTOR_STATUS_RUNNING, + }) + @Target(ElementType.TYPE_USE) + @Retention(RetentionPolicy.SOURCE) + public @interface DetectorStatus {} + + /** + * The detector status is unknown. Expected only for use as a placeholder before the actual + * status is known. + */ + public static final @DetectorStatus int DETECTOR_STATUS_UNKNOWN = 0; + + /** The detector is not supported on this device. */ + public static final @DetectorStatus int DETECTOR_STATUS_NOT_SUPPORTED = 1; + + /** The detector is supported but is not running. */ + public static final @DetectorStatus int DETECTOR_STATUS_NOT_RUNNING = 2; + + /** The detector is supported and is running. */ + public static final @DetectorStatus int DETECTOR_STATUS_RUNNING = 3; + + private DetectorStatusTypes() {} + + /** + * A status code for a detection algorithm. + */ + @IntDef(prefix = "DETECTION_ALGORITHM_STATUS_", value = { + DETECTION_ALGORITHM_STATUS_UNKNOWN, + DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED, + DETECTION_ALGORITHM_STATUS_NOT_RUNNING, + DETECTION_ALGORITHM_STATUS_RUNNING, + }) + @Target(ElementType.TYPE_USE) + @Retention(RetentionPolicy.SOURCE) + public @interface DetectionAlgorithmStatus {} + + /** + * The detection algorithm status is unknown. Expected only for use as a placeholder before the + * actual status is known. + */ + public static final @DetectionAlgorithmStatus int DETECTION_ALGORITHM_STATUS_UNKNOWN = 0; + + /** The detection algorithm is not supported on this device. */ + public static final @DetectionAlgorithmStatus int DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED = 1; + + /** The detection algorithm supported but is not running. */ + public static final @DetectionAlgorithmStatus int DETECTION_ALGORITHM_STATUS_NOT_RUNNING = 2; + + /** The detection algorithm supported and is running. */ + public static final @DetectionAlgorithmStatus int DETECTION_ALGORITHM_STATUS_RUNNING = 3; + + /** + * Validates the supplied value is one of the known {@code DETECTOR_STATUS_} constants and + * returns it if it is valid. {@link #DETECTOR_STATUS_UNKNOWN} is considered valid. + * + * @throws IllegalArgumentException if the value is not recognized + */ + public static @DetectorStatus int requireValidDetectorStatus( + @DetectorStatus int detectorStatus) { + if (detectorStatus < DETECTOR_STATUS_UNKNOWN || detectorStatus > DETECTOR_STATUS_RUNNING) { + throw new IllegalArgumentException("Invalid detector status: " + detectorStatus); + } + return detectorStatus; + } + + /** + * Returns a string for each {@code DETECTOR_STATUS_} constant. See also + * {@link #detectorStatusFromString(String)}. + * + * @throws IllegalArgumentException if the value is not recognized + */ + @NonNull + public static String detectorStatusToString(@DetectorStatus int detectorStatus) { + switch (detectorStatus) { + case DETECTOR_STATUS_UNKNOWN: + return "UNKNOWN"; + case DETECTOR_STATUS_NOT_SUPPORTED: + return "NOT_SUPPORTED"; + case DETECTOR_STATUS_NOT_RUNNING: + return "NOT_RUNNING"; + case DETECTOR_STATUS_RUNNING: + return "RUNNING"; + default: + throw new IllegalArgumentException("Unknown status: " + detectorStatus); + } + } + + /** + * Returns {@code DETECTOR_STATUS_} constant value from a string. See also + * {@link #detectorStatusToString(int)}. + * + * @throws IllegalArgumentException if the value is not recognized or is invalid + */ + public static @DetectorStatus int detectorStatusFromString( + @Nullable String detectorStatusString) { + if (TextUtils.isEmpty(detectorStatusString)) { + throw new IllegalArgumentException("Empty status: " + detectorStatusString); + } + + switch (detectorStatusString) { + case "UNKNOWN": + return DETECTOR_STATUS_UNKNOWN; + case "NOT_SUPPORTED": + return DETECTOR_STATUS_NOT_SUPPORTED; + case "NOT_RUNNING": + return DETECTOR_STATUS_NOT_RUNNING; + case "RUNNING": + return DETECTOR_STATUS_RUNNING; + default: + throw new IllegalArgumentException("Unknown status: " + detectorStatusString); + } + } + + /** + * Validates the supplied value is one of the known {@code DETECTION_ALGORITHM_} constants and + * returns it if it is valid. {@link #DETECTION_ALGORITHM_STATUS_UNKNOWN} is considered valid. + * + * @throws IllegalArgumentException if the value is not recognized + */ + public static @DetectionAlgorithmStatus int requireValidDetectionAlgorithmStatus( + @DetectionAlgorithmStatus int detectionAlgorithmStatus) { + if (detectionAlgorithmStatus < DETECTION_ALGORITHM_STATUS_UNKNOWN + || detectionAlgorithmStatus > DETECTION_ALGORITHM_STATUS_RUNNING) { + throw new IllegalArgumentException( + "Invalid detection algorithm: " + detectionAlgorithmStatus); + } + return detectionAlgorithmStatus; + } + + /** + * Returns a string for each {@code DETECTION_ALGORITHM_} constant. See also + * {@link #detectionAlgorithmStatusFromString(String)} + * + * @throws IllegalArgumentException if the value is not recognized + */ + @NonNull + public static String detectionAlgorithmStatusToString( + @DetectionAlgorithmStatus int detectorAlgorithmStatus) { + switch (detectorAlgorithmStatus) { + case DETECTION_ALGORITHM_STATUS_UNKNOWN: + return "UNKNOWN"; + case DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED: + return "NOT_SUPPORTED"; + case DETECTION_ALGORITHM_STATUS_NOT_RUNNING: + return "NOT_RUNNING"; + case DETECTION_ALGORITHM_STATUS_RUNNING: + return "RUNNING"; + default: + throw new IllegalArgumentException("Unknown status: " + detectorAlgorithmStatus); + } + } + + /** + * Returns {@code DETECTION_ALGORITHM_} constant value from a string. See also + * {@link #detectionAlgorithmStatusToString(int)} (String)} + * + * @throws IllegalArgumentException if the value is not recognized or is invalid + */ + public static @DetectionAlgorithmStatus int detectionAlgorithmStatusFromString( + @Nullable String detectorAlgorithmStatusString) { + + if (TextUtils.isEmpty(detectorAlgorithmStatusString)) { + throw new IllegalArgumentException("Empty status: " + detectorAlgorithmStatusString); + } + + switch (detectorAlgorithmStatusString) { + case "UNKNOWN": + return DETECTION_ALGORITHM_STATUS_UNKNOWN; + case "NOT_SUPPORTED": + return DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED; + case "NOT_RUNNING": + return DETECTION_ALGORITHM_STATUS_NOT_RUNNING; + case "RUNNING": + return DETECTION_ALGORITHM_STATUS_RUNNING; + default: + throw new IllegalArgumentException( + "Unknown status: " + detectorAlgorithmStatusString); + } + } +} diff --git a/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.aidl b/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.aidl new file mode 100644 index 0000000000000..7184b123af1ce --- /dev/null +++ b/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2022 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 android.app.time; + +parcelable LocationTimeZoneAlgorithmStatus; diff --git a/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.java b/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.java new file mode 100644 index 0000000000000..710b8c40cefe8 --- /dev/null +++ b/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.java @@ -0,0 +1,363 @@ +/* + * Copyright (C) 2022 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 android.app.time; + +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_UNKNOWN; +import static android.app.time.DetectorStatusTypes.detectionAlgorithmStatusFromString; +import static android.app.time.DetectorStatusTypes.detectionAlgorithmStatusToString; +import static android.app.time.DetectorStatusTypes.requireValidDetectionAlgorithmStatus; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.time.DetectorStatusTypes.DetectionAlgorithmStatus; +import android.os.Parcel; +import android.os.Parcelable; +import android.service.timezone.TimeZoneProviderStatus; +import android.text.TextUtils; + +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Information about the status of the location-based time zone detection algorithm. + * + * @hide + */ +public final class LocationTimeZoneAlgorithmStatus implements Parcelable { + + /** + * An enum that describes a location time zone provider's status. + * + * @hide + */ + @IntDef(prefix = "PROVIDER_STATUS_", value = { + PROVIDER_STATUS_NOT_PRESENT, + PROVIDER_STATUS_NOT_READY, + PROVIDER_STATUS_IS_CERTAIN, + PROVIDER_STATUS_IS_UNCERTAIN, + }) + @Target(ElementType.TYPE_USE) + @Retention(RetentionPolicy.SOURCE) + public @interface ProviderStatus {} + + /** + * Indicates a provider is not present because it has not been configured, the configuration + * is bad, or the provider has reported a permanent failure. + */ + public static final @ProviderStatus int PROVIDER_STATUS_NOT_PRESENT = 1; + + /** + * Indicates a provider has not reported it is certain or uncertain. This may be because it has + * just started running, or it has been stopped. + */ + public static final @ProviderStatus int PROVIDER_STATUS_NOT_READY = 2; + + /** + * Indicates a provider last reported it is certain. + */ + public static final @ProviderStatus int PROVIDER_STATUS_IS_CERTAIN = 3; + + /** + * Indicates a provider last reported it is uncertain. + */ + public static final @ProviderStatus int PROVIDER_STATUS_IS_UNCERTAIN = 4; + + /** + * An instance that provides no information about algorithm status because the algorithm has not + * yet reported. Effectively a "null" status placeholder. + */ + @NonNull + public static final LocationTimeZoneAlgorithmStatus UNKNOWN = + new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_UNKNOWN, + PROVIDER_STATUS_NOT_READY, null, PROVIDER_STATUS_NOT_READY, null); + + private final @DetectionAlgorithmStatus int mStatus; + private final @ProviderStatus int mPrimaryProviderStatus; + // May be populated when mPrimaryProviderReportedStatus == PROVIDER_STATUS_IS_CERTAIN + // or PROVIDER_STATUS_IS_UNCERTAIN + @Nullable private final TimeZoneProviderStatus mPrimaryProviderReportedStatus; + + private final @ProviderStatus int mSecondaryProviderStatus; + // May be populated when mSecondaryProviderReportedStatus == PROVIDER_STATUS_IS_CERTAIN + // or PROVIDER_STATUS_IS_UNCERTAIN + @Nullable private final TimeZoneProviderStatus mSecondaryProviderReportedStatus; + + public LocationTimeZoneAlgorithmStatus( + @DetectionAlgorithmStatus int status, + @ProviderStatus int primaryProviderStatus, + @Nullable TimeZoneProviderStatus primaryProviderReportedStatus, + @ProviderStatus int secondaryProviderStatus, + @Nullable TimeZoneProviderStatus secondaryProviderReportedStatus) { + + mStatus = requireValidDetectionAlgorithmStatus(status); + mPrimaryProviderStatus = requireValidProviderStatus(primaryProviderStatus); + mPrimaryProviderReportedStatus = primaryProviderReportedStatus; + mSecondaryProviderStatus = requireValidProviderStatus(secondaryProviderStatus); + mSecondaryProviderReportedStatus = secondaryProviderReportedStatus; + + boolean primaryProviderHasReported = hasProviderReported(primaryProviderStatus); + boolean primaryProviderReportedStatusPresent = primaryProviderReportedStatus != null; + if (!primaryProviderHasReported && primaryProviderReportedStatusPresent) { + throw new IllegalArgumentException( + "primaryProviderReportedStatus=" + primaryProviderReportedStatus + + ", primaryProviderStatus=" + + providerStatusToString(primaryProviderStatus)); + } + + boolean secondaryProviderHasReported = hasProviderReported(secondaryProviderStatus); + boolean secondaryProviderReportedStatusPresent = secondaryProviderReportedStatus != null; + if (!secondaryProviderHasReported && secondaryProviderReportedStatusPresent) { + throw new IllegalArgumentException( + "secondaryProviderReportedStatus=" + secondaryProviderReportedStatus + + ", secondaryProviderStatus=" + + providerStatusToString(secondaryProviderStatus)); + } + + // If the algorithm isn't running, providers can't report. + if (status != DETECTION_ALGORITHM_STATUS_RUNNING + && (primaryProviderHasReported || secondaryProviderHasReported)) { + throw new IllegalArgumentException( + "algorithmStatus=" + detectionAlgorithmStatusToString(status) + + ", primaryProviderReportedStatus=" + primaryProviderReportedStatus + + ", secondaryProviderReportedStatus=" + + secondaryProviderReportedStatus); + } + } + + /** + * Returns the status value of the detection algorithm. + */ + public @DetectionAlgorithmStatus int getStatus() { + return mStatus; + } + + /** + * Returns the status of the primary location time zone provider as categorized by the detection + * algorithm. + */ + public @ProviderStatus int getPrimaryProviderStatus() { + return mPrimaryProviderStatus; + } + + /** + * Returns the status of the primary location time zone provider as reported by the provider + * itself. Can be {@code null} when the provider hasn't reported, or omitted when it has. + */ + @Nullable + public TimeZoneProviderStatus getPrimaryProviderReportedStatus() { + return mPrimaryProviderReportedStatus; + } + + /** + * Returns the status of the secondary location time zone provider as categorized by the + * detection algorithm. + */ + public @ProviderStatus int getSecondaryProviderStatus() { + return mSecondaryProviderStatus; + } + + /** + * Returns the status of the secondary location time zone provider as reported by the provider + * itself. Can be {@code null} when the provider hasn't reported, or omitted when it has. + */ + @Nullable + public TimeZoneProviderStatus getSecondaryProviderReportedStatus() { + return mSecondaryProviderReportedStatus; + } + + @Override + public String toString() { + return "LocationTimeZoneAlgorithmStatus{" + + "mAlgorithmStatus=" + detectionAlgorithmStatusToString(mStatus) + + ", mPrimaryProviderStatus=" + providerStatusToString(mPrimaryProviderStatus) + + ", mPrimaryProviderReportedStatus=" + mPrimaryProviderReportedStatus + + ", mSecondaryProviderStatus=" + providerStatusToString(mSecondaryProviderStatus) + + ", mSecondaryProviderReportedStatus=" + mSecondaryProviderReportedStatus + + '}'; + } + + /** + * Parses a {@link LocationTimeZoneAlgorithmStatus} from a toString() string for manual + * command-line testing. + */ + @NonNull + public static LocationTimeZoneAlgorithmStatus parseCommandlineArg(@NonNull String arg) { + // Note: "}" has to be escaped on Android with "\\}" because the regexp library is not based + // on OpenJDK code. + Pattern pattern = Pattern.compile("LocationTimeZoneAlgorithmStatus\\{" + + "mAlgorithmStatus=(.+)" + + ", mPrimaryProviderStatus=([^,]+)" + + ", mPrimaryProviderReportedStatus=(null|TimeZoneProviderStatus\\{[^}]+\\})" + + ", mSecondaryProviderStatus=([^,]+)" + + ", mSecondaryProviderReportedStatus=(null|TimeZoneProviderStatus\\{[^}]+\\})" + + "\\}" + ); + Matcher matcher = pattern.matcher(arg); + if (!matcher.matches()) { + throw new IllegalArgumentException("Unable to parse algorithm status arg: " + arg); + } + @DetectionAlgorithmStatus int algorithmStatus = + detectionAlgorithmStatusFromString(matcher.group(1)); + @ProviderStatus int primaryProviderStatus = providerStatusFromString(matcher.group(2)); + TimeZoneProviderStatus primaryProviderReportedStatus = + parseTimeZoneProviderStatusOrNull(matcher.group(3)); + @ProviderStatus int secondaryProviderStatus = providerStatusFromString(matcher.group(4)); + TimeZoneProviderStatus secondaryProviderReportedStatus = + parseTimeZoneProviderStatusOrNull(matcher.group(5)); + return new LocationTimeZoneAlgorithmStatus( + algorithmStatus, primaryProviderStatus, primaryProviderReportedStatus, + secondaryProviderStatus, secondaryProviderReportedStatus); + } + + @Nullable + private static TimeZoneProviderStatus parseTimeZoneProviderStatusOrNull( + String providerReportedStatusString) { + TimeZoneProviderStatus providerReportedStatus; + if ("null".equals(providerReportedStatusString)) { + providerReportedStatus = null; + } else { + providerReportedStatus = + TimeZoneProviderStatus.parseProviderStatus(providerReportedStatusString); + } + return providerReportedStatus; + } + + @NonNull + public static final Creator CREATOR = new Creator<>() { + @Override + public LocationTimeZoneAlgorithmStatus createFromParcel(Parcel in) { + @DetectionAlgorithmStatus int algorithmStatus = in.readInt(); + @ProviderStatus int primaryProviderStatus = in.readInt(); + TimeZoneProviderStatus primaryProviderReportedStatus = + in.readParcelable(getClass().getClassLoader(), TimeZoneProviderStatus.class); + @ProviderStatus int secondaryProviderStatus = in.readInt(); + TimeZoneProviderStatus secondaryProviderReportedStatus = + in.readParcelable(getClass().getClassLoader(), TimeZoneProviderStatus.class); + return new LocationTimeZoneAlgorithmStatus( + algorithmStatus, primaryProviderStatus, primaryProviderReportedStatus, + secondaryProviderStatus, secondaryProviderReportedStatus); + } + + @Override + public LocationTimeZoneAlgorithmStatus[] newArray(int size) { + return new LocationTimeZoneAlgorithmStatus[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int flags) { + parcel.writeInt(mStatus); + parcel.writeInt(mPrimaryProviderStatus); + parcel.writeParcelable(mPrimaryProviderReportedStatus, flags); + parcel.writeInt(mSecondaryProviderStatus); + parcel.writeParcelable(mSecondaryProviderReportedStatus, flags); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LocationTimeZoneAlgorithmStatus that = (LocationTimeZoneAlgorithmStatus) o; + return mStatus == that.mStatus + && mPrimaryProviderStatus == that.mPrimaryProviderStatus + && Objects.equals( + mPrimaryProviderReportedStatus, that.mPrimaryProviderReportedStatus) + && mSecondaryProviderStatus == that.mSecondaryProviderStatus + && Objects.equals( + mSecondaryProviderReportedStatus, that.mSecondaryProviderReportedStatus); + } + + @Override + public int hashCode() { + return Objects.hash(mStatus, + mPrimaryProviderStatus, mPrimaryProviderReportedStatus, + mSecondaryProviderStatus, mSecondaryProviderReportedStatus); + } + + /** @hide */ + @VisibleForTesting + @NonNull + public static String providerStatusToString(@ProviderStatus int providerStatus) { + switch (providerStatus) { + case PROVIDER_STATUS_NOT_PRESENT: + return "NOT_PRESENT"; + case PROVIDER_STATUS_NOT_READY: + return "NOT_READY"; + case PROVIDER_STATUS_IS_CERTAIN: + return "IS_CERTAIN"; + case PROVIDER_STATUS_IS_UNCERTAIN: + return "IS_UNCERTAIN"; + default: + throw new IllegalArgumentException("Unknown status: " + providerStatus); + } + } + + /** @hide */ + @VisibleForTesting public static @ProviderStatus int providerStatusFromString( + @Nullable String providerStatusString) { + if (TextUtils.isEmpty(providerStatusString)) { + throw new IllegalArgumentException("Empty status: " + providerStatusString); + } + + switch (providerStatusString) { + case "NOT_PRESENT": + return PROVIDER_STATUS_NOT_PRESENT; + case "NOT_READY": + return PROVIDER_STATUS_NOT_READY; + case "IS_CERTAIN": + return PROVIDER_STATUS_IS_CERTAIN; + case "IS_UNCERTAIN": + return PROVIDER_STATUS_IS_UNCERTAIN; + default: + throw new IllegalArgumentException("Unknown status: " + providerStatusString); + } + } + + private static boolean hasProviderReported(@ProviderStatus int providerStatus) { + return providerStatus == PROVIDER_STATUS_IS_CERTAIN + || providerStatus == PROVIDER_STATUS_IS_UNCERTAIN; + } + + /** @hide */ + @VisibleForTesting public static @ProviderStatus int requireValidProviderStatus( + @ProviderStatus int providerStatus) { + if (providerStatus < PROVIDER_STATUS_NOT_PRESENT + || providerStatus > PROVIDER_STATUS_IS_UNCERTAIN) { + throw new IllegalArgumentException( + "Invalid provider status: " + providerStatus); + } + return providerStatus; + } +} diff --git a/core/java/android/app/time/TelephonyTimeZoneAlgorithmStatus.aidl b/core/java/android/app/time/TelephonyTimeZoneAlgorithmStatus.aidl new file mode 100644 index 0000000000000..0eb5b63b7ffb3 --- /dev/null +++ b/core/java/android/app/time/TelephonyTimeZoneAlgorithmStatus.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2022 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 android.app.time; + +parcelable TelephonyTimeZoneAlgorithmStatus; diff --git a/core/java/android/app/time/TelephonyTimeZoneAlgorithmStatus.java b/core/java/android/app/time/TelephonyTimeZoneAlgorithmStatus.java new file mode 100644 index 0000000000000..95240c00fa3fa --- /dev/null +++ b/core/java/android/app/time/TelephonyTimeZoneAlgorithmStatus.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2022 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 android.app.time; + +import static android.app.time.DetectorStatusTypes.detectionAlgorithmStatusToString; +import static android.app.time.DetectorStatusTypes.requireValidDetectionAlgorithmStatus; + +import android.annotation.NonNull; +import android.app.time.DetectorStatusTypes.DetectionAlgorithmStatus; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Information about the status of the telephony-based time zone detection algorithm. + * + * @hide + */ +public final class TelephonyTimeZoneAlgorithmStatus implements Parcelable { + + private final @DetectionAlgorithmStatus int mAlgorithmStatus; + + public TelephonyTimeZoneAlgorithmStatus(@DetectionAlgorithmStatus int algorithmStatus) { + mAlgorithmStatus = requireValidDetectionAlgorithmStatus(algorithmStatus); + } + + /** + * Returns the status of the detection algorithm. + */ + public @DetectionAlgorithmStatus int getAlgorithmStatus() { + return mAlgorithmStatus; + } + + @Override + public String toString() { + return "TelephonyTimeZoneAlgorithmStatus{" + + "mAlgorithmStatus=" + detectionAlgorithmStatusToString(mAlgorithmStatus) + + '}'; + } + + @NonNull + public static final Creator CREATOR = new Creator<>() { + @Override + public TelephonyTimeZoneAlgorithmStatus createFromParcel(Parcel in) { + @DetectionAlgorithmStatus int algorithmStatus = in.readInt(); + return new TelephonyTimeZoneAlgorithmStatus(algorithmStatus); + } + + @Override + public TelephonyTimeZoneAlgorithmStatus[] newArray(int size) { + return new TelephonyTimeZoneAlgorithmStatus[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int flags) { + parcel.writeInt(mAlgorithmStatus); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TelephonyTimeZoneAlgorithmStatus that = (TelephonyTimeZoneAlgorithmStatus) o; + return mAlgorithmStatus == that.mAlgorithmStatus; + } + + @Override + public int hashCode() { + return Objects.hash(mAlgorithmStatus); + } +} diff --git a/core/java/android/app/time/TimeZoneCapabilitiesAndConfig.java b/core/java/android/app/time/TimeZoneCapabilitiesAndConfig.java index cd91b0431b28e..4684c6ad811c8 100644 --- a/core/java/android/app/time/TimeZoneCapabilitiesAndConfig.java +++ b/core/java/android/app/time/TimeZoneCapabilitiesAndConfig.java @@ -23,27 +23,40 @@ import android.os.Parcel; import android.os.Parcelable; import java.util.Objects; +import java.util.concurrent.Executor; /** - * A pair containing a user's {@link TimeZoneCapabilities} and {@link TimeZoneConfiguration}. + * An object containing a user's {@link TimeZoneCapabilities} and {@link TimeZoneConfiguration}. * * @hide */ @SystemApi public final class TimeZoneCapabilitiesAndConfig implements Parcelable { - public static final @NonNull Creator CREATOR = - new Creator() { - public TimeZoneCapabilitiesAndConfig createFromParcel(Parcel in) { - return TimeZoneCapabilitiesAndConfig.createFromParcel(in); - } - - public TimeZoneCapabilitiesAndConfig[] newArray(int size) { - return new TimeZoneCapabilitiesAndConfig[size]; - } - }; + public static final @NonNull Creator CREATOR = new Creator<>() { + public TimeZoneCapabilitiesAndConfig createFromParcel(Parcel in) { + return TimeZoneCapabilitiesAndConfig.createFromParcel(in); + } + public TimeZoneCapabilitiesAndConfig[] newArray(int size) { + return new TimeZoneCapabilitiesAndConfig[size]; + } + }; + /** + * The time zone detector status. + * + * Implementation note for future platform engineers: This field is only needed by SettingsUI + * initially and so it has not been added to the SDK API. {@link TimeZoneDetectorStatus} + * contains details about the internals of the time zone detector so thought should be given to + * abstraction / exposing a lightweight version if something unbundled needs access to detector + * details. Also, that could be good time to add separate APIs for bundled components, or add + * new APIs that return something more extensible and generic like a Bundle or a less + * constraining name. See also {@link + * TimeManager#addTimeZoneDetectorListener(Executor, TimeManager.TimeZoneDetectorListener)}, + * which notified of changes to any fields in this class, including the detector status. + */ + @NonNull private final TimeZoneDetectorStatus mDetectorStatus; @NonNull private final TimeZoneCapabilities mCapabilities; @NonNull private final TimeZoneConfiguration mConfiguration; @@ -53,25 +66,40 @@ public final class TimeZoneCapabilitiesAndConfig implements Parcelable { * @hide */ public TimeZoneCapabilitiesAndConfig( + @NonNull TimeZoneDetectorStatus detectorStatus, @NonNull TimeZoneCapabilities capabilities, @NonNull TimeZoneConfiguration configuration) { - this.mCapabilities = Objects.requireNonNull(capabilities); - this.mConfiguration = Objects.requireNonNull(configuration); + mDetectorStatus = Objects.requireNonNull(detectorStatus); + mCapabilities = Objects.requireNonNull(capabilities); + mConfiguration = Objects.requireNonNull(configuration); } @NonNull private static TimeZoneCapabilitiesAndConfig createFromParcel(Parcel in) { - TimeZoneCapabilities capabilities = in.readParcelable(null, android.app.time.TimeZoneCapabilities.class); - TimeZoneConfiguration configuration = in.readParcelable(null, android.app.time.TimeZoneConfiguration.class); - return new TimeZoneCapabilitiesAndConfig(capabilities, configuration); + TimeZoneDetectorStatus detectorStatus = + in.readParcelable(null, TimeZoneDetectorStatus.class); + TimeZoneCapabilities capabilities = in.readParcelable(null, TimeZoneCapabilities.class); + TimeZoneConfiguration configuration = in.readParcelable(null, TimeZoneConfiguration.class); + return new TimeZoneCapabilitiesAndConfig(detectorStatus, capabilities, configuration); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeParcelable(mDetectorStatus, flags); dest.writeParcelable(mCapabilities, flags); dest.writeParcelable(mConfiguration, flags); } + /** + * Returns the time zone detector's status. + * + * @hide + */ + @NonNull + public TimeZoneDetectorStatus getDetectorStatus() { + return mDetectorStatus; + } + /** * Returns the user's time zone behavior capabilities. */ @@ -102,7 +130,8 @@ public final class TimeZoneCapabilitiesAndConfig implements Parcelable { return false; } TimeZoneCapabilitiesAndConfig that = (TimeZoneCapabilitiesAndConfig) o; - return mCapabilities.equals(that.mCapabilities) + return mDetectorStatus.equals(that.mDetectorStatus) + && mCapabilities.equals(that.mCapabilities) && mConfiguration.equals(that.mConfiguration); } @@ -114,7 +143,8 @@ public final class TimeZoneCapabilitiesAndConfig implements Parcelable { @Override public String toString() { return "TimeZoneCapabilitiesAndConfig{" - + "mCapabilities=" + mCapabilities + + "mDetectorStatus=" + mDetectorStatus + + ", mCapabilities=" + mCapabilities + ", mConfiguration=" + mConfiguration + '}'; } diff --git a/core/java/android/app/time/TimeZoneDetectorStatus.aidl b/core/java/android/app/time/TimeZoneDetectorStatus.aidl new file mode 100644 index 0000000000000..32204df6d698c --- /dev/null +++ b/core/java/android/app/time/TimeZoneDetectorStatus.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2022 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 android.app.time; + +parcelable TimeZoneDetectorStatus; diff --git a/core/java/android/app/time/TimeZoneDetectorStatus.java b/core/java/android/app/time/TimeZoneDetectorStatus.java new file mode 100644 index 0000000000000..16374639b77cd --- /dev/null +++ b/core/java/android/app/time/TimeZoneDetectorStatus.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2022 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 android.app.time; + +import static android.app.time.DetectorStatusTypes.DetectorStatus; +import static android.app.time.DetectorStatusTypes.requireValidDetectorStatus; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Information about the status of the automatic time zone detector. Used by SettingsUI to display + * status information to the user. + * + * @hide + */ +public final class TimeZoneDetectorStatus implements Parcelable { + + private final @DetectorStatus int mDetectorStatus; + @NonNull private final TelephonyTimeZoneAlgorithmStatus mTelephonyTimeZoneAlgorithmStatus; + @NonNull private final LocationTimeZoneAlgorithmStatus mLocationTimeZoneAlgorithmStatus; + + public TimeZoneDetectorStatus( + @DetectorStatus int detectorStatus, + @NonNull TelephonyTimeZoneAlgorithmStatus telephonyTimeZoneAlgorithmStatus, + @NonNull LocationTimeZoneAlgorithmStatus locationTimeZoneAlgorithmStatus) { + mDetectorStatus = requireValidDetectorStatus(detectorStatus); + mTelephonyTimeZoneAlgorithmStatus = + Objects.requireNonNull(telephonyTimeZoneAlgorithmStatus); + mLocationTimeZoneAlgorithmStatus = Objects.requireNonNull(locationTimeZoneAlgorithmStatus); + } + + public @DetectorStatus int getDetectorStatus() { + return mDetectorStatus; + } + + @NonNull + public TelephonyTimeZoneAlgorithmStatus getTelephonyTimeZoneAlgorithmStatus() { + return mTelephonyTimeZoneAlgorithmStatus; + } + + @NonNull + public LocationTimeZoneAlgorithmStatus getLocationTimeZoneAlgorithmStatus() { + return mLocationTimeZoneAlgorithmStatus; + } + + @Override + public String toString() { + return "TimeZoneDetectorStatus{" + + "mDetectorStatus=" + DetectorStatusTypes.detectorStatusToString(mDetectorStatus) + + ", mTelephonyTimeZoneAlgorithmStatus=" + mTelephonyTimeZoneAlgorithmStatus + + ", mLocationTimeZoneAlgorithmStatus=" + mLocationTimeZoneAlgorithmStatus + + '}'; + } + + public static final @NonNull Creator CREATOR = new Creator<>() { + @Override + public TimeZoneDetectorStatus createFromParcel(Parcel in) { + @DetectorStatus int detectorStatus = in.readInt(); + TelephonyTimeZoneAlgorithmStatus telephonyTimeZoneAlgorithmStatus = + in.readParcelable(getClass().getClassLoader(), + TelephonyTimeZoneAlgorithmStatus.class); + LocationTimeZoneAlgorithmStatus locationTimeZoneAlgorithmStatus = + in.readParcelable(getClass().getClassLoader(), + LocationTimeZoneAlgorithmStatus.class); + return new TimeZoneDetectorStatus(detectorStatus, + telephonyTimeZoneAlgorithmStatus, locationTimeZoneAlgorithmStatus); + } + + @Override + public TimeZoneDetectorStatus[] newArray(int size) { + return new TimeZoneDetectorStatus[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int flags) { + parcel.writeInt(mDetectorStatus); + parcel.writeParcelable(mTelephonyTimeZoneAlgorithmStatus, flags); + parcel.writeParcelable(mLocationTimeZoneAlgorithmStatus, flags); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TimeZoneDetectorStatus that = (TimeZoneDetectorStatus) o; + return mDetectorStatus == that.mDetectorStatus + && mTelephonyTimeZoneAlgorithmStatus.equals(that.mTelephonyTimeZoneAlgorithmStatus) + && mLocationTimeZoneAlgorithmStatus.equals(that.mLocationTimeZoneAlgorithmStatus); + } + + @Override + public int hashCode() { + return Objects.hash(mDetectorStatus, mTelephonyTimeZoneAlgorithmStatus, + mLocationTimeZoneAlgorithmStatus); + } +} diff --git a/core/java/android/app/timezonedetector/TimeZoneDetector.java b/core/java/android/app/timezonedetector/TimeZoneDetector.java index 0e9e28be88188..f357fb243fe18 100644 --- a/core/java/android/app/timezonedetector/TimeZoneDetector.java +++ b/core/java/android/app/timezonedetector/TimeZoneDetector.java @@ -81,11 +81,11 @@ public interface TimeZoneDetector { String SHELL_COMMAND_SET_GEO_DETECTION_ENABLED = "set_geo_detection_enabled"; /** - * A shell command that injects a geolocation time zone suggestion (as if from the + * A shell command that injects a location algorithm event (as if from the * location_time_zone_manager). * @hide */ - String SHELL_COMMAND_SUGGEST_GEO_LOCATION_TIME_ZONE = "suggest_geo_location_time_zone"; + String SHELL_COMMAND_HANDLE_LOCATION_ALGORITHM_EVENT = "handle_location_algorithm_event"; /** * A shell command that injects a manual time zone suggestion (as if from the SettingsUI or diff --git a/core/proto/android/app/location_time_zone_manager.proto b/core/proto/android/app/location_time_zone_manager.proto index 5fdcfdf35a37a..7037a6c4f68a9 100644 --- a/core/proto/android/app/location_time_zone_manager.proto +++ b/core/proto/android/app/location_time_zone_manager.proto @@ -40,7 +40,7 @@ enum ControllerStateEnum { message LocationTimeZoneManagerServiceStateProto { option (android.msg_privacy).dest = DEST_AUTOMATIC; - optional GeolocationTimeZoneSuggestionProto last_suggestion = 1; + optional LocationTimeZoneProviderEventProto last_event = 1; repeated TimeZoneProviderStateProto primary_provider_states = 2; repeated TimeZoneProviderStateProto secondary_provider_states = 3; repeated ControllerStateEnum controller_states = 4; diff --git a/core/proto/android/app/time_zone_detector.proto b/core/proto/android/app/time_zone_detector.proto index b52aa828bef98..cd4a36fafef04 100644 --- a/core/proto/android/app/time_zone_detector.proto +++ b/core/proto/android/app/time_zone_detector.proto @@ -22,13 +22,38 @@ import "frameworks/base/core/proto/android/privacy.proto"; option java_multiple_files = true; option java_outer_classname = "TimeZoneDetectorProto"; -// Represents a GeolocationTimeZoneSuggestion that can be / has been passed to the time zone +// Represents a LocationTimeZoneProviderEvent that can be / has been passed to the time zone // detector. +message LocationTimeZoneProviderEventProto { + option (android.msg_privacy).dest = DEST_AUTOMATIC; + + optional GeolocationTimeZoneSuggestionProto suggestion = 1; + repeated string debug_info = 2; + optional LocationTimeZoneAlgorithmStatusProto algorithm_status = 3; +} + +// Represents a LocationTimeZoneAlgorithmStatus that can be / has been passed to the time zone +// detector. +message LocationTimeZoneAlgorithmStatusProto { + option (android.msg_privacy).dest = DEST_AUTOMATIC; + + optional DetectionAlgorithmStatusEnum status = 1; +} + +// The state enum for detection algorithms. +enum DetectionAlgorithmStatusEnum { + DETECTION_ALGORITHM_STATUS_UNKNOWN = 0; + DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED = 1; + DETECTION_ALGORITHM_STATUS_NOT_RUNNING = 2; + DETECTION_ALGORITHM_STATUS_RUNNING = 3; +} + +// Represents a GeolocationTimeZoneSuggestion that can be contained in a +// LocationTimeZoneProviderEvent. message GeolocationTimeZoneSuggestionProto { option (android.msg_privacy).dest = DEST_AUTOMATIC; repeated string zone_ids = 1; - repeated string debug_info = 2; } /* diff --git a/core/tests/coretests/src/android/app/time/DetectorStatusTypesTest.java b/core/tests/coretests/src/android/app/time/DetectorStatusTypesTest.java new file mode 100644 index 0000000000000..f57ee43b76c89 --- /dev/null +++ b/core/tests/coretests/src/android/app/time/DetectorStatusTypesTest.java @@ -0,0 +1,103 @@ +/* + * Copyright 2022 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 android.app.time; + +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_UNKNOWN; +import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_UNKNOWN; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import android.app.time.DetectorStatusTypes.DetectionAlgorithmStatus; +import android.app.time.DetectorStatusTypes.DetectorStatus; + +import org.junit.Test; + +public class DetectorStatusTypesTest { + + @Test + public void testRequireValidDetectionAlgorithmStatus() { + for (@DetectionAlgorithmStatus int status = DETECTION_ALGORITHM_STATUS_UNKNOWN; + status <= DETECTION_ALGORITHM_STATUS_RUNNING; status++) { + assertEquals(status, DetectorStatusTypes.requireValidDetectionAlgorithmStatus(status)); + } + + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.requireValidDetectionAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_UNKNOWN - 1)); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.requireValidDetectionAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING + 1)); + } + + @Test + public void testFormatAndParseDetectionAlgorithmStatus() { + for (@DetectionAlgorithmStatus int status = DETECTION_ALGORITHM_STATUS_UNKNOWN; + status <= DETECTION_ALGORITHM_STATUS_RUNNING; status++) { + assertEquals(status, DetectorStatusTypes.detectionAlgorithmStatusFromString( + DetectorStatusTypes.detectionAlgorithmStatusToString(status))); + } + + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusToString( + DETECTION_ALGORITHM_STATUS_UNKNOWN - 1)); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusToString( + DETECTION_ALGORITHM_STATUS_RUNNING + 1)); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusFromString(null)); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusFromString("")); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusFromString("FOO")); + } + + @Test + public void testRequireValidDetectorStatus() { + for (@DetectorStatus int status = DETECTOR_STATUS_UNKNOWN; + status <= DETECTOR_STATUS_RUNNING; status++) { + assertEquals(status, DetectorStatusTypes.requireValidDetectorStatus(status)); + } + + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.requireValidDetectorStatus(DETECTOR_STATUS_UNKNOWN - 1)); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.requireValidDetectorStatus(DETECTOR_STATUS_RUNNING + 1)); + } + + @Test + public void testFormatAndParseDetectorStatus() { + for (@DetectorStatus int status = DETECTOR_STATUS_UNKNOWN; + status <= DETECTOR_STATUS_RUNNING; status++) { + assertEquals(status, DetectorStatusTypes.detectorStatusFromString( + DetectorStatusTypes.detectorStatusToString(status))); + } + + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusToString(DETECTOR_STATUS_UNKNOWN - 1)); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusToString(DETECTOR_STATUS_RUNNING + 1)); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusFromString(null)); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusFromString("")); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusFromString("FOO")); + } +} diff --git a/core/tests/coretests/src/android/app/time/LocationTimeZoneAlgorithmStatusTest.java b/core/tests/coretests/src/android/app/time/LocationTimeZoneAlgorithmStatusTest.java new file mode 100644 index 0000000000000..a648a885aea2b --- /dev/null +++ b/core/tests/coretests/src/android/app/time/LocationTimeZoneAlgorithmStatusTest.java @@ -0,0 +1,210 @@ +/* + * Copyright 2022 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 android.app.time; + +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_CERTAIN; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_UNCERTAIN; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY; +import static android.app.time.ParcelableTestSupport.assertEqualsAndHashCode; +import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable; +import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_OK; +import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_OK; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; + +import android.app.time.LocationTimeZoneAlgorithmStatus.ProviderStatus; +import android.service.timezone.TimeZoneProviderStatus; + +import org.junit.Test; + +public class LocationTimeZoneAlgorithmStatusTest { + + private static final TimeZoneProviderStatus ARBITRARY_PROVIDER_RUNNING_STATUS = + new TimeZoneProviderStatus.Builder() + .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_OK) + .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK) + .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_OK) + .build(); + + @Test + public void testConstructorValidation() { + // Sample some invalid cases + + // There can't be a reported provider status if the algorithm isn't running. + new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS, + PROVIDER_STATUS_IS_UNCERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS); + assertThrows(IllegalArgumentException.class, + () -> new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_NOT_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS, + PROVIDER_STATUS_IS_UNCERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS)); + + new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS, + PROVIDER_STATUS_NOT_PRESENT, null); + assertThrows(IllegalArgumentException.class, + () -> new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_NOT_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS, + PROVIDER_STATUS_NOT_PRESENT, null)); + + new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_NOT_PRESENT, null, + PROVIDER_STATUS_IS_UNCERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS); + assertThrows(IllegalArgumentException.class, + () -> new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_NOT_RUNNING, + PROVIDER_STATUS_NOT_PRESENT, null, + PROVIDER_STATUS_IS_UNCERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS)); + + // No reported provider status expected if the associated provider isn't ready / present. + new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_NOT_PRESENT, null, + PROVIDER_STATUS_NOT_PRESENT, null); + assertThrows(IllegalArgumentException.class, + () -> new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_NOT_PRESENT, ARBITRARY_PROVIDER_RUNNING_STATUS, + PROVIDER_STATUS_NOT_PRESENT, null)); + new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_NOT_READY, null, + PROVIDER_STATUS_NOT_PRESENT, null); + assertThrows(IllegalArgumentException.class, + () -> new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_NOT_READY, null, + PROVIDER_STATUS_NOT_PRESENT, ARBITRARY_PROVIDER_RUNNING_STATUS)); + } + + @Test + public void testEquals() { + LocationTimeZoneAlgorithmStatus one = new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS, + PROVIDER_STATUS_NOT_PRESENT, null); + assertEqualsAndHashCode(one, one); + + { + LocationTimeZoneAlgorithmStatus two = new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS, + PROVIDER_STATUS_NOT_PRESENT, null); + assertEqualsAndHashCode(one, two); + } + + { + LocationTimeZoneAlgorithmStatus three = new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING, + PROVIDER_STATUS_NOT_READY, null, + PROVIDER_STATUS_NOT_PRESENT, null); + assertNotEquals(one, three); + assertNotEquals(three, one); + } + } + + @Test + public void testParcelable() { + // Primary provider only. + { + LocationTimeZoneAlgorithmStatus locationAlgorithmStatus = + new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS, + PROVIDER_STATUS_NOT_PRESENT, null); + assertRoundTripParcelable(locationAlgorithmStatus); + } + + // Secondary provider only + { + LocationTimeZoneAlgorithmStatus locationAlgorithmStatus = + new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_NOT_PRESENT, null, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS); + assertRoundTripParcelable(locationAlgorithmStatus); + } + + // Algorithm not running. + { + LocationTimeZoneAlgorithmStatus locationAlgorithmStatus = + new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING, + PROVIDER_STATUS_NOT_PRESENT, null, + PROVIDER_STATUS_NOT_PRESENT, null); + assertRoundTripParcelable(locationAlgorithmStatus); + } + } + + @Test + public void testRequireValidProviderStatus() { + for (@ProviderStatus int status = PROVIDER_STATUS_NOT_PRESENT; + status <= PROVIDER_STATUS_IS_UNCERTAIN; status++) { + assertEquals(status, + LocationTimeZoneAlgorithmStatus.requireValidProviderStatus(status)); + } + + assertThrows(IllegalArgumentException.class, + () -> LocationTimeZoneAlgorithmStatus.requireValidProviderStatus( + PROVIDER_STATUS_NOT_PRESENT - 1)); + assertThrows(IllegalArgumentException.class, + () -> LocationTimeZoneAlgorithmStatus.requireValidProviderStatus( + PROVIDER_STATUS_IS_UNCERTAIN + 1)); + } + + @Test + public void testFormatAndParseProviderStatus() { + for (@ProviderStatus int status = PROVIDER_STATUS_NOT_PRESENT; + status <= PROVIDER_STATUS_IS_UNCERTAIN; status++) { + assertEquals(status, LocationTimeZoneAlgorithmStatus.providerStatusFromString( + LocationTimeZoneAlgorithmStatus.providerStatusToString(status))); + } + + assertThrows(IllegalArgumentException.class, + () -> LocationTimeZoneAlgorithmStatus.providerStatusToString( + PROVIDER_STATUS_NOT_PRESENT - 1)); + assertThrows(IllegalArgumentException.class, + () -> LocationTimeZoneAlgorithmStatus.providerStatusToString( + PROVIDER_STATUS_IS_UNCERTAIN + 1)); + assertThrows(IllegalArgumentException.class, + () -> LocationTimeZoneAlgorithmStatus.providerStatusFromString(null)); + assertThrows(IllegalArgumentException.class, + () -> LocationTimeZoneAlgorithmStatus.providerStatusFromString("")); + assertThrows(IllegalArgumentException.class, + () -> LocationTimeZoneAlgorithmStatus.providerStatusFromString("FOO")); + } + + @Test + public void testParseCommandlineArg_noNullReportedStatuses() { + LocationTimeZoneAlgorithmStatus status = new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS, + PROVIDER_STATUS_IS_UNCERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS); + assertEquals(status, + LocationTimeZoneAlgorithmStatus.parseCommandlineArg(status.toString())); + } + + @Test + public void testParseCommandlineArg_withNullReportedStatuses() { + LocationTimeZoneAlgorithmStatus status = new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, null, + PROVIDER_STATUS_IS_UNCERTAIN, null); + assertEquals(status, + LocationTimeZoneAlgorithmStatus.parseCommandlineArg(status.toString())); + } +} diff --git a/core/tests/coretests/src/android/app/time/TelephonyTimeZoneAlgorithmStatusTest.java b/core/tests/coretests/src/android/app/time/TelephonyTimeZoneAlgorithmStatusTest.java new file mode 100644 index 0000000000000..b90c485bbbb61 --- /dev/null +++ b/core/tests/coretests/src/android/app/time/TelephonyTimeZoneAlgorithmStatusTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2022 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 android.app.time; + +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; +import static android.app.time.ParcelableTestSupport.assertEqualsAndHashCode; +import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable; + +import static org.junit.Assert.assertNotEquals; + +import org.junit.Test; + +public class TelephonyTimeZoneAlgorithmStatusTest { + + @Test + public void testEquals() { + TelephonyTimeZoneAlgorithmStatus one = new TelephonyTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING); + assertEqualsAndHashCode(one, one); + + { + TelephonyTimeZoneAlgorithmStatus two = new TelephonyTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING); + assertEqualsAndHashCode(one, two); + } + + { + TelephonyTimeZoneAlgorithmStatus three = new TelephonyTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING); + assertNotEquals(one, three); + assertNotEquals(three, one); + } + } + + @Test + public void testParcelable() { + // Algorithm running. + { + TelephonyTimeZoneAlgorithmStatus locationAlgorithmStatus = + new TelephonyTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING); + assertRoundTripParcelable(locationAlgorithmStatus); + } + + // Algorithm not running. + { + TelephonyTimeZoneAlgorithmStatus locationAlgorithmStatus = + new TelephonyTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING); + assertRoundTripParcelable(locationAlgorithmStatus); + } + } +} diff --git a/core/tests/coretests/src/android/app/time/TimeZoneDetectorStatusTest.java b/core/tests/coretests/src/android/app/time/TimeZoneDetectorStatusTest.java new file mode 100644 index 0000000000000..dfff7ecdf9891 --- /dev/null +++ b/core/tests/coretests/src/android/app/time/TimeZoneDetectorStatusTest.java @@ -0,0 +1,105 @@ +/* + * Copyright 2022 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 android.app.time; + +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_NOT_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_RUNNING; +import static android.app.time.ParcelableTestSupport.assertEqualsAndHashCode; +import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable; + +import static org.junit.Assert.assertNotEquals; + +import org.junit.Test; + +public class TimeZoneDetectorStatusTest { + + private static final TelephonyTimeZoneAlgorithmStatus ARBITRARY_TELEPHONY_ALGORITHM_STATUS = + new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING); + + private static final LocationTimeZoneAlgorithmStatus ARBITRARY_LOCATION_ALGORITHM_STATUS = + new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, + LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY, null, + LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT, null); + + @Test + public void testEquals() { + TimeZoneDetectorStatus one = new TimeZoneDetectorStatus(DETECTOR_STATUS_RUNNING, + ARBITRARY_TELEPHONY_ALGORITHM_STATUS, ARBITRARY_LOCATION_ALGORITHM_STATUS); + assertEqualsAndHashCode(one, one); + + { + TimeZoneDetectorStatus two = new TimeZoneDetectorStatus(DETECTOR_STATUS_RUNNING, + ARBITRARY_TELEPHONY_ALGORITHM_STATUS, ARBITRARY_LOCATION_ALGORITHM_STATUS); + assertEqualsAndHashCode(one, two); + } + + { + TimeZoneDetectorStatus three = new TimeZoneDetectorStatus(DETECTOR_STATUS_NOT_RUNNING, + ARBITRARY_TELEPHONY_ALGORITHM_STATUS, ARBITRARY_LOCATION_ALGORITHM_STATUS); + assertNotEquals(one, three); + assertNotEquals(three, one); + } + + { + TelephonyTimeZoneAlgorithmStatus telephonyAlgorithmStatus = + new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_NOT_RUNNING); + assertNotEquals(telephonyAlgorithmStatus, ARBITRARY_TELEPHONY_ALGORITHM_STATUS); + + TimeZoneDetectorStatus three = new TimeZoneDetectorStatus(DETECTOR_STATUS_NOT_RUNNING, + telephonyAlgorithmStatus, ARBITRARY_LOCATION_ALGORITHM_STATUS); + assertNotEquals(one, three); + assertNotEquals(three, one); + } + + { + LocationTimeZoneAlgorithmStatus locationAlgorithmStatus = + new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING, + LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY, null, + LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY, null); + assertNotEquals(locationAlgorithmStatus, ARBITRARY_LOCATION_ALGORITHM_STATUS); + + TimeZoneDetectorStatus three = new TimeZoneDetectorStatus(DETECTOR_STATUS_NOT_RUNNING, + ARBITRARY_TELEPHONY_ALGORITHM_STATUS, locationAlgorithmStatus); + assertNotEquals(one, three); + assertNotEquals(three, one); + } + } + + @Test + public void testParcelable() { + // Detector running. + { + TimeZoneDetectorStatus locationAlgorithmStatus = new TimeZoneDetectorStatus( + DETECTOR_STATUS_RUNNING, ARBITRARY_TELEPHONY_ALGORITHM_STATUS, + ARBITRARY_LOCATION_ALGORITHM_STATUS); + assertRoundTripParcelable(locationAlgorithmStatus); + } + + // Detector not running. + { + TimeZoneDetectorStatus locationAlgorithmStatus = + new TimeZoneDetectorStatus(DETECTOR_STATUS_NOT_RUNNING, + ARBITRARY_TELEPHONY_ALGORITHM_STATUS, + ARBITRARY_LOCATION_ALGORITHM_STATUS); + assertRoundTripParcelable(locationAlgorithmStatus); + } + } +} diff --git a/services/core/java/com/android/server/timezonedetector/GeolocationTimeZoneSuggestion.java b/services/core/java/com/android/server/timezonedetector/GeolocationTimeZoneSuggestion.java index 8218fa5c4643b..80d959977b0f0 100644 --- a/services/core/java/com/android/server/timezonedetector/GeolocationTimeZoneSuggestion.java +++ b/services/core/java/com/android/server/timezonedetector/GeolocationTimeZoneSuggestion.java @@ -19,20 +19,15 @@ 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; -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 the location_time_zone_manager service to the time_zone_detector - * service. + * A time zone suggestion from the location_time_zone_manager service (AKA the location-based time + * zone detection algorithm). * *

Geolocation-based suggestions have the following properties: * @@ -63,24 +58,16 @@ import java.util.StringTokenizer; * 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. - *

  • {@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)}. *
  • * - * - * @hide */ public final class GeolocationTimeZoneSuggestion { @ElapsedRealtimeLong private final long mEffectiveFromElapsedMillis; @Nullable private final List mZoneIds; - @Nullable private ArrayList mDebugInfo; private GeolocationTimeZoneSuggestion( - @ElapsedRealtimeLong long effectiveFromElapsedMillis, - @Nullable List zoneIds) { + @ElapsedRealtimeLong long effectiveFromElapsedMillis, @Nullable List zoneIds) { mEffectiveFromElapsedMillis = effectiveFromElapsedMillis; if (zoneIds == null) { // Unopinionated @@ -104,8 +91,7 @@ public final class GeolocationTimeZoneSuggestion { */ @NonNull public static GeolocationTimeZoneSuggestion createCertainSuggestion( - @ElapsedRealtimeLong long effectiveFromElapsedMillis, - @NonNull List zoneIds) { + @ElapsedRealtimeLong long effectiveFromElapsedMillis, @NonNull List zoneIds) { return new GeolocationTimeZoneSuggestion(effectiveFromElapsedMillis, zoneIds); } @@ -126,25 +112,6 @@ public final class GeolocationTimeZoneSuggestion { return mZoneIds; } - /** Returns debug information. See {@link GeolocationTimeZoneSuggestion} for details. */ - @NonNull - public List 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) { @@ -169,59 +136,6 @@ public final class GeolocationTimeZoneSuggestion { return "GeolocationTimeZoneSuggestion{" + "mEffectiveFromElapsedMillis=" + mEffectiveFromElapsedMillis + ", 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); - } - } - } - - if (zoneIdsString == null) { - throw new IllegalArgumentException("Missing --zone_ids"); - } - - long elapsedRealtimeMillis = SystemClock.elapsedRealtime(); - List zoneIds = parseZoneIdsArg(zoneIdsString); - GeolocationTimeZoneSuggestion suggestion = - new GeolocationTimeZoneSuggestion(elapsedRealtimeMillis, zoneIds); - suggestion.addDebugInfo("Command line injection"); - return suggestion; - } - - private static List parseZoneIdsArg(String zoneIdsString) { - if ("UNCERTAIN".equals(zoneIdsString)) { - return null; - } else if ("EMPTY".equals(zoneIdsString)) { - return Collections.emptyList(); - } else { - ArrayList 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|+}"); - pw.println(); - pw.println("See " + GeolocationTimeZoneSuggestion.class.getName() - + " for more information"); - } } diff --git a/services/core/java/com/android/server/timezonedetector/LocationAlgorithmEvent.java b/services/core/java/com/android/server/timezonedetector/LocationAlgorithmEvent.java new file mode 100644 index 0000000000000..1ffd9a11b300f --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/LocationAlgorithmEvent.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2022 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.app.time.LocationTimeZoneAlgorithmStatus; +import android.os.ShellCommand; +import android.os.SystemClock; + +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; + +/** + * An event from the location_time_zone_manager service (AKA the location-based time zone detection + * algorithm). An event can represent a new time zone recommendation, an algorithm status change, or + * both. + * + *

    Events have the following properties: + * + *

      + *
    • {@code algorithmStatus}: The current status of the location-based time zone detection + * algorithm.
    • + *
    • {@code suggestion}: The latest time zone suggestion, if there is one.
    • + *
    • {@code debugInfo} contains debugging metadata associated with the suggestion. This is + * used to record why the event exists and how information contained within 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)}. + *
    • + *
    + */ +public final class LocationAlgorithmEvent { + + @NonNull private final LocationTimeZoneAlgorithmStatus mAlgorithmStatus; + @Nullable private final GeolocationTimeZoneSuggestion mSuggestion; + @Nullable private ArrayList mDebugInfo; + + /** Creates a new instance. */ + public LocationAlgorithmEvent( + @NonNull LocationTimeZoneAlgorithmStatus algorithmStatus, + @Nullable GeolocationTimeZoneSuggestion suggestion) { + mAlgorithmStatus = Objects.requireNonNull(algorithmStatus); + mSuggestion = suggestion; + } + + /** + * Returns the status of the location time zone detector algorithm. + */ + @NonNull + public LocationTimeZoneAlgorithmStatus getAlgorithmStatus() { + return mAlgorithmStatus; + } + + /** + * Returns the latest location algorithm suggestion. See {@link LocationAlgorithmEvent} for + * details. + */ + @Nullable + public GeolocationTimeZoneSuggestion getSuggestion() { + return mSuggestion; + } + + /** Returns debug information. See {@link LocationAlgorithmEvent} for details. */ + @NonNull + public List 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; + } + LocationAlgorithmEvent that = (LocationAlgorithmEvent) o; + return mAlgorithmStatus.equals(that.mAlgorithmStatus) + && Objects.equals(mSuggestion, that.mSuggestion); + } + + @Override + public int hashCode() { + return Objects.hash(mAlgorithmStatus, mSuggestion); + } + + @Override + public String toString() { + return "LocationAlgorithmEvent{" + + "mAlgorithmStatus=" + mAlgorithmStatus + + ", mSuggestion=" + mSuggestion + + ", mDebugInfo=" + mDebugInfo + + '}'; + } + + static LocationAlgorithmEvent parseCommandLineArg(@NonNull ShellCommand cmd) { + String suggestionString = null; + LocationTimeZoneAlgorithmStatus algorithmStatus = null; + String opt; + while ((opt = cmd.getNextArg()) != null) { + switch (opt) { + case "--status": { + algorithmStatus = LocationTimeZoneAlgorithmStatus.parseCommandlineArg( + cmd.getNextArgRequired()); + break; + } + case "--suggestion": { + suggestionString = cmd.getNextArgRequired(); + break; + } + default: { + throw new IllegalArgumentException("Unknown option: " + opt); + } + } + } + + if (algorithmStatus == null) { + throw new IllegalArgumentException("Missing --status"); + } + + GeolocationTimeZoneSuggestion suggestion = null; + if (suggestionString != null) { + List zoneIds = parseZoneIds(suggestionString); + long elapsedRealtimeMillis = SystemClock.elapsedRealtime(); + if (zoneIds == null) { + suggestion = GeolocationTimeZoneSuggestion.createUncertainSuggestion( + elapsedRealtimeMillis); + } else { + suggestion = GeolocationTimeZoneSuggestion.createCertainSuggestion( + elapsedRealtimeMillis, zoneIds); + } + } + + LocationAlgorithmEvent event = new LocationAlgorithmEvent(algorithmStatus, suggestion); + event.addDebugInfo("Command line injection"); + return event; + } + + private static List parseZoneIds(String zoneIdsString) { + if ("UNCERTAIN".equals(zoneIdsString)) { + return null; + } else if ("EMPTY".equals(zoneIdsString)) { + return Collections.emptyList(); + } else { + ArrayList zoneIds = new ArrayList<>(); + StringTokenizer tokenizer = new StringTokenizer(zoneIdsString, ","); + while (tokenizer.hasMoreTokens()) { + zoneIds.add(tokenizer.nextToken()); + } + return zoneIds; + } + } + + static void printCommandLineOpts(@NonNull PrintWriter pw) { + pw.println("Location algorithm event options:"); + pw.println(" --status {LocationTimeZoneAlgorithmStatus toString() format}"); + pw.println(" [--suggestion {UNCERTAIN|EMPTY|+}]"); + pw.println(); + pw.println("See " + LocationAlgorithmEvent.class.getName() + " for more information"); + } +} diff --git a/services/core/java/com/android/server/timezonedetector/MetricsTimeZoneDetectorState.java b/services/core/java/com/android/server/timezonedetector/MetricsTimeZoneDetectorState.java index 6c36989320e8f..aad53596fc190 100644 --- a/services/core/java/com/android/server/timezonedetector/MetricsTimeZoneDetectorState.java +++ b/services/core/java/com/android/server/timezonedetector/MetricsTimeZoneDetectorState.java @@ -89,7 +89,7 @@ public final class MetricsTimeZoneDetectorState { @NonNull String deviceTimeZoneId, @Nullable ManualTimeZoneSuggestion latestManualSuggestion, @Nullable TelephonyTimeZoneSuggestion latestTelephonySuggestion, - @Nullable GeolocationTimeZoneSuggestion latestGeolocationSuggestion) { + @Nullable LocationAlgorithmEvent latestLocationAlgorithmEvent) { boolean includeZoneIds = configurationInternal.isEnhancedMetricsCollectionEnabled(); String metricDeviceTimeZoneId = includeZoneIds ? deviceTimeZoneId : null; @@ -101,9 +101,13 @@ public final class MetricsTimeZoneDetectorState { MetricsTimeZoneSuggestion latestCanonicalTelephonySuggestion = createMetricsTimeZoneSuggestion( tzIdOrdinalGenerator, latestTelephonySuggestion, includeZoneIds); - MetricsTimeZoneSuggestion latestCanonicalGeolocationSuggestion = - createMetricsTimeZoneSuggestion( - tzIdOrdinalGenerator, latestGeolocationSuggestion, includeZoneIds); + + MetricsTimeZoneSuggestion latestCanonicalGeolocationSuggestion = null; + if (latestLocationAlgorithmEvent != null) { + GeolocationTimeZoneSuggestion suggestion = latestLocationAlgorithmEvent.getSuggestion(); + latestCanonicalGeolocationSuggestion = createMetricsTimeZoneSuggestion( + tzIdOrdinalGenerator, suggestion, includeZoneIds); + } return new MetricsTimeZoneDetectorState( configurationInternal, deviceTimeZoneIdOrdinal, metricDeviceTimeZoneId, diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java index 80cf1d6b9031b..74a518bf83821 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java @@ -59,11 +59,11 @@ public interface TimeZoneDetectorInternal { boolean setManualTimeZoneForDpm(@NonNull ManualTimeZoneSuggestion timeZoneSuggestion); /** - * 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. This method may be implemented asynchronously. + * Handles the supplied {@link LocationAlgorithmEvent}. The detector may ignore the event based + * on system settings, whether better information is available, and so on. This method may be + * implemented asynchronously. */ - void suggestGeolocationTimeZone(@NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion); + void handleLocationAlgorithmEvent(@NonNull LocationAlgorithmEvent locationAlgorithmEvent); /** Generates a state snapshot for metrics. */ @NonNull diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java index dfb44df7b993f..07d04737c3e25 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java @@ -76,13 +76,14 @@ public final class TimeZoneDetectorInternalImpl implements TimeZoneDetectorInter } @Override - public void suggestGeolocationTimeZone( - @NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion) { - Objects.requireNonNull(timeZoneSuggestion); + public void handleLocationAlgorithmEvent( + @NonNull LocationAlgorithmEvent locationAlgorithmEvent) { + Objects.requireNonNull(locationAlgorithmEvent); // This call can take place on the mHandler thread because there is no return value. mHandler.post( - () -> mTimeZoneDetectorStrategy.suggestGeolocationTimeZone(timeZoneSuggestion)); + () -> mTimeZoneDetectorStrategy.handleLocationAlgorithmEvent( + locationAlgorithmEvent)); } @Override diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java index f415cf03fdecc..f8c1c9269ff39 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java @@ -300,12 +300,13 @@ 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) { + void handleLocationAlgorithmEvent(@NonNull LocationAlgorithmEvent locationAlgorithmEvent) { enforceSuggestGeolocationTimeZonePermission(); - Objects.requireNonNull(timeZoneSuggestion); + Objects.requireNonNull(locationAlgorithmEvent); mHandler.post( - () -> mTimeZoneDetectorStrategy.suggestGeolocationTimeZone(timeZoneSuggestion)); + () -> mTimeZoneDetectorStrategy.handleLocationAlgorithmEvent( + locationAlgorithmEvent)); } @Override diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java index 1b9f8e6cd66f6..69274dba78250 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java @@ -19,6 +19,7 @@ import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_CONFIR import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_DUMP_METRICS; import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_ENABLE_TELEPHONY_FALLBACK; import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_GET_TIME_ZONE_STATE; +import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_HANDLE_LOCATION_ALGORITHM_EVENT; import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_IS_AUTO_DETECTION_ENABLED; import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_IS_GEO_DETECTION_ENABLED; import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_IS_GEO_DETECTION_SUPPORTED; @@ -27,7 +28,6 @@ import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SERVIC import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SET_AUTO_DETECTION_ENABLED; import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SET_GEO_DETECTION_ENABLED; import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SET_TIME_ZONE_STATE; -import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SUGGEST_GEO_LOCATION_TIME_ZONE; import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SUGGEST_MANUAL_TIME_ZONE; import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SUGGEST_TELEPHONY_TIME_ZONE; import static android.provider.DeviceConfig.NAMESPACE_SYSTEM_TIME; @@ -79,8 +79,8 @@ class TimeZoneDetectorShellCommand extends ShellCommand { return runIsGeoDetectionEnabled(); case SHELL_COMMAND_SET_GEO_DETECTION_ENABLED: return runSetGeoDetectionEnabled(); - case SHELL_COMMAND_SUGGEST_GEO_LOCATION_TIME_ZONE: - return runSuggestGeolocationTimeZone(); + case SHELL_COMMAND_HANDLE_LOCATION_ALGORITHM_EVENT: + return runHandleLocationEvent(); case SHELL_COMMAND_SUGGEST_MANUAL_TIME_ZONE: return runSuggestManualTimeZone(); case SHELL_COMMAND_SUGGEST_TELEPHONY_TIME_ZONE: @@ -153,34 +153,34 @@ class TimeZoneDetectorShellCommand extends ShellCommand { return mInterface.updateConfiguration(userId, configuration) ? 0 : 1; } - private int runSuggestGeolocationTimeZone() { - return runSuggestTimeZone( - () -> GeolocationTimeZoneSuggestion.parseCommandLineArg(this), - mInterface::suggestGeolocationTimeZone); + private int runHandleLocationEvent() { + return runSingleArgMethod( + () -> LocationAlgorithmEvent.parseCommandLineArg(this), + mInterface::handleLocationAlgorithmEvent); } private int runSuggestManualTimeZone() { - return runSuggestTimeZone( + return runSingleArgMethod( () -> ManualTimeZoneSuggestion.parseCommandLineArg(this), mInterface::suggestManualTimeZone); } private int runSuggestTelephonyTimeZone() { - return runSuggestTimeZone( + return runSingleArgMethod( () -> TelephonyTimeZoneSuggestion.parseCommandLineArg(this), mInterface::suggestTelephonyTimeZone); } - private int runSuggestTimeZone(Supplier suggestionParser, Consumer invoker) { + private int runSingleArgMethod(Supplier argParser, Consumer invoker) { final PrintWriter pw = getOutPrintWriter(); try { - T suggestion = suggestionParser.get(); - if (suggestion == null) { - pw.println("Error: suggestion not specified"); + T arg = argParser.get(); + if (arg == null) { + pw.println("Error: arg not specified"); return 1; } - invoker.accept(suggestion); - pw.println("Suggestion " + suggestion + " injected."); + invoker.accept(arg); + pw.println("Arg " + arg + " injected."); return 0; } catch (RuntimeException e) { pw.println(e); @@ -263,18 +263,18 @@ class TimeZoneDetectorShellCommand extends ShellCommand { pw.printf(" Sets the geolocation time zone detection enabled setting.\n"); pw.printf(" %s\n", SHELL_COMMAND_ENABLE_TELEPHONY_FALLBACK); pw.printf(" Signals that telephony time zone detection fall back can be used if" - + " geolocation detection is supported and enabled. This is a temporary state until" - + " geolocation detection becomes \"certain\". To have an effect this requires that" - + " the telephony fallback feature is supported on the device, see below for" - + " for device_config flags.\n"); - pw.println(); - pw.printf(" %s \n", - SHELL_COMMAND_SUGGEST_GEO_LOCATION_TIME_ZONE); - pw.printf(" Suggests a time zone as if via the \"location\" origin.\n"); + + " geolocation detection is supported and enabled.\n)"); + pw.printf(" This is a temporary state until geolocation detection becomes \"certain\"." + + "\n"); + pw.printf(" To have an effect this requires that the telephony fallback feature is" + + " supported on the device, see below for device_config flags.\n"); + pw.printf(" %s \n", SHELL_COMMAND_HANDLE_LOCATION_ALGORITHM_EVENT); + pw.printf(" Simulates an event from the location time zone detection algorithm.\n"); pw.printf(" %s \n", SHELL_COMMAND_SUGGEST_MANUAL_TIME_ZONE); - pw.printf(" Suggests a time zone as if via the \"manual\" origin.\n"); + pw.printf(" Suggests a time zone as if supplied by a user manually.\n"); pw.printf(" %s \n", SHELL_COMMAND_SUGGEST_TELEPHONY_TIME_ZONE); - pw.printf(" Suggests a time zone as if via the \"telephony\" origin.\n"); + pw.printf(" Simulates a time zone suggestion from the telephony time zone detection" + + " algorithm.\n"); pw.printf(" %s\n", SHELL_COMMAND_GET_TIME_ZONE_STATE); pw.printf(" Returns the current time zone setting state.\n"); pw.printf(" %s