diff options
| author | Neil Fuller <nfuller@google.com> | 2019-11-07 15:35:05 +0000 |
|---|---|---|
| committer | Neil Fuller <nfuller@google.com> | 2019-11-28 13:27:55 +0000 |
| commit | 3352cfce2f48d2555ea73e7f80ebab3ba5827640 (patch) | |
| tree | fdab98118ab88544a090e0868f55b2ce4ad90baa /core/java/android | |
| parent | 5f01cb6ddca16e231c9ce57baa6949c075b3cc05 (diff) | |
Add a new time zone detection service
Add a new time zone detection service. Much of the code is from
frameworks/opt/telephony with some changes for naming, threading and
to modify the interaction with the "Callback" class.
Overall goal:
Implementing the service in the system server means it will be easier to
add new time zone detection logic unrelated to telephony in future.
Bug: 140712361
Test: atest com.android.server.timezonedetector
Test: atest android.app.timezonedetector
Change-Id: I89505fc4fecbd3667b60f8e1479b8f177eaa60ae
Merged-In: I89505fc4fecbd3667b60f8e1479b8f177eaa60ae
(cherry picked from commit 3e3b5405b6c5e77a640ad9450eb1cac5b7c80ff1)
Diffstat (limited to 'core/java/android')
6 files changed, 475 insertions, 1 deletions
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 29cb3c1f131b..7531b6ca8f17 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -31,6 +31,7 @@ import android.app.role.RoleManager; import android.app.slice.SliceManager; import android.app.timedetector.TimeDetector; import android.app.timezone.RulesManager; +import android.app.timezonedetector.TimeZoneDetector; import android.app.trust.TrustManager; import android.app.usage.IStorageStatsManager; import android.app.usage.IUsageStatsManager; @@ -1235,6 +1236,14 @@ final class SystemServiceRegistry { return new TimeDetector(); }}); + registerService(Context.TIME_ZONE_DETECTOR_SERVICE, TimeZoneDetector.class, + new CachedServiceFetcher<TimeZoneDetector>() { + @Override + public TimeZoneDetector createService(ContextImpl ctx) + throws ServiceNotFoundException { + return new TimeZoneDetector(); + }}); + registerService(Context.PERMISSION_SERVICE, PermissionManager.class, new CachedServiceFetcher<PermissionManager>() { @Override diff --git a/core/java/android/app/timezonedetector/ITimeZoneDetectorService.aidl b/core/java/android/app/timezonedetector/ITimeZoneDetectorService.aidl new file mode 100644 index 000000000000..260c7df72fba --- /dev/null +++ b/core/java/android/app/timezonedetector/ITimeZoneDetectorService.aidl @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2019 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.timezonedetector; + +import android.app.timezonedetector.PhoneTimeZoneSuggestion; + +/** + * System private API to communicate with time zone detector service. + * + * <p>Used to provide information to the Time Zone Detector Service from other parts of the Android + * system that have access to time zone-related signals, e.g. telephony. + * + * <p>Use the {@link android.app.timezonedetector.TimeZoneDetector} class rather than going through + * this Binder interface directly. See {@link android.app.timezonedetector.TimeZoneDetectorService} + * for more complete documentation. + * + * + * {@hide} + */ +interface ITimeZoneDetectorService { + void suggestPhoneTimeZone(in PhoneTimeZoneSuggestion timeZoneSuggestion); +} diff --git a/core/java/android/app/timezonedetector/PhoneTimeZoneSuggestion.aidl b/core/java/android/app/timezonedetector/PhoneTimeZoneSuggestion.aidl new file mode 100644 index 000000000000..3ad903bb5949 --- /dev/null +++ b/core/java/android/app/timezonedetector/PhoneTimeZoneSuggestion.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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.timezonedetector; + +parcelable PhoneTimeZoneSuggestion; diff --git a/core/java/android/app/timezonedetector/PhoneTimeZoneSuggestion.java b/core/java/android/app/timezonedetector/PhoneTimeZoneSuggestion.java new file mode 100644 index 000000000000..e8162488394c --- /dev/null +++ b/core/java/android/app/timezonedetector/PhoneTimeZoneSuggestion.java @@ -0,0 +1,341 @@ +/* + * Copyright 2019 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.timezonedetector; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * A suggested time zone from a Phone-based signal, e.g. from MCC and NITZ information. + * + * @hide + */ +public final class PhoneTimeZoneSuggestion implements Parcelable { + + @NonNull + public static final Creator<PhoneTimeZoneSuggestion> CREATOR = + new Creator<PhoneTimeZoneSuggestion>() { + public PhoneTimeZoneSuggestion createFromParcel(Parcel in) { + return PhoneTimeZoneSuggestion.createFromParcel(in); + } + + public PhoneTimeZoneSuggestion[] newArray(int size) { + return new PhoneTimeZoneSuggestion[size]; + } + }; + + /** + * Creates an empty time zone suggestion, i.e. one that will cancel previous suggestions with + * the same {@code phoneId}. + */ + @NonNull + public static PhoneTimeZoneSuggestion createEmptySuggestion( + int phoneId, @NonNull String debugInfo) { + return new Builder(phoneId).addDebugInfo(debugInfo).build(); + } + + @IntDef({ MATCH_TYPE_NA, MATCH_TYPE_NETWORK_COUNTRY_ONLY, MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, + MATCH_TYPE_EMULATOR_ZONE_ID, MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY }) + @Retention(RetentionPolicy.SOURCE) + public @interface MatchType {} + + /** Used when match type is not applicable. */ + public static final int MATCH_TYPE_NA = 0; + + /** + * Only the network country is known. + */ + public static final int MATCH_TYPE_NETWORK_COUNTRY_ONLY = 2; + + /** + * Both the network county and offset were known. + */ + public static final int MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET = 3; + + /** + * The device is running in an emulator and an NITZ signal was simulated containing an + * Android extension with an explicit Olson ID. + */ + public static final int MATCH_TYPE_EMULATOR_ZONE_ID = 4; + + /** + * The phone is most likely running in a test network not associated with a country (this is + * distinct from the country just not being known yet). + * Historically, Android has just picked an arbitrary time zone with the correct offset when + * on a test network. + */ + public static final int MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY = 5; + + @IntDef({ QUALITY_NA, QUALITY_SINGLE_ZONE, QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET, + QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS }) + @Retention(RetentionPolicy.SOURCE) + public @interface Quality {} + + /** Used when quality is not applicable. */ + public static final int QUALITY_NA = 0; + + /** There is only one answer */ + public static final int QUALITY_SINGLE_ZONE = 1; + + /** + * There are multiple answers, but they all shared the same offset / DST state at the time + * the suggestion was created. i.e. it might be the wrong zone but the user won't notice + * immediately if it is wrong. + */ + public static final int QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET = 2; + + /** + * There are multiple answers with different offsets. The one given is just one possible. + */ + public static final int QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS = 3; + + /** + * The ID of the phone this suggestion is associated with. For multiple-sim devices this + * helps to establish origin so filtering / stickiness can be implemented. + */ + private final int mPhoneId; + + /** + * The suggestion. {@code null} means there is no current suggestion and any previous suggestion + * should be forgotten. + */ + private final String mZoneId; + + /** + * The type of "match" used to establish the time zone. + */ + @MatchType + private final int mMatchType; + + /** + * A measure of the quality of the time zone suggestion, i.e. how confident one could be in + * it. + */ + @Quality + private final int mQuality; + + /** + * Free-form debug information about how the signal was derived. Used for debug only, + * intentionally not used in equals(), etc. + */ + private List<String> mDebugInfo; + + private PhoneTimeZoneSuggestion(Builder builder) { + mPhoneId = builder.mPhoneId; + mZoneId = builder.mZoneId; + mMatchType = builder.mMatchType; + mQuality = builder.mQuality; + mDebugInfo = builder.mDebugInfo != null ? new ArrayList<>(builder.mDebugInfo) : null; + } + + @SuppressWarnings("unchecked") + private static PhoneTimeZoneSuggestion createFromParcel(Parcel in) { + // Use the Builder so we get validation during build(). + int phoneId = in.readInt(); + PhoneTimeZoneSuggestion suggestion = new Builder(phoneId) + .setZoneId(in.readString()) + .setMatchType(in.readInt()) + .setQuality(in.readInt()) + .build(); + List<String> debugInfo = in.readArrayList(PhoneTimeZoneSuggestion.class.getClassLoader()); + if (debugInfo != null) { + suggestion.addDebugInfo(debugInfo); + } + return suggestion; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mPhoneId); + dest.writeString(mZoneId); + dest.writeInt(mMatchType); + dest.writeInt(mQuality); + dest.writeList(mDebugInfo); + } + + @Override + public int describeContents() { + return 0; + } + + public int getPhoneId() { + return mPhoneId; + } + + @Nullable + public String getZoneId() { + return mZoneId; + } + + @MatchType + public int getMatchType() { + return mMatchType; + } + + @Quality + public int getQuality() { + return mQuality; + } + + @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(@NonNull String debugInfo) { + if (mDebugInfo == null) { + mDebugInfo = new ArrayList<>(); + } + mDebugInfo.add(debugInfo); + } + + /** + * 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(@NonNull List<String> debugInfo) { + if (mDebugInfo == null) { + mDebugInfo = new ArrayList<>(debugInfo.size()); + } + mDebugInfo.addAll(debugInfo); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PhoneTimeZoneSuggestion that = (PhoneTimeZoneSuggestion) o; + return mPhoneId == that.mPhoneId + && mMatchType == that.mMatchType + && mQuality == that.mQuality + && Objects.equals(mZoneId, that.mZoneId); + } + + @Override + public int hashCode() { + return Objects.hash(mPhoneId, mZoneId, mMatchType, mQuality); + } + + @Override + public String toString() { + return "PhoneTimeZoneSuggestion{" + + "mPhoneId=" + mPhoneId + + ", mZoneId='" + mZoneId + '\'' + + ", mMatchType=" + mMatchType + + ", mQuality=" + mQuality + + ", mDebugInfo=" + mDebugInfo + + '}'; + } + + /** + * Builds {@link PhoneTimeZoneSuggestion} instances. + * + * @hide + */ + public static class Builder { + private final int mPhoneId; + private String mZoneId; + @MatchType private int mMatchType; + @Quality private int mQuality; + private List<String> mDebugInfo; + + public Builder(int phoneId) { + mPhoneId = phoneId; + } + + /** Returns the builder for call chaining. */ + public Builder setZoneId(String zoneId) { + mZoneId = zoneId; + return this; + } + + /** Returns the builder for call chaining. */ + public Builder setMatchType(@MatchType int matchType) { + mMatchType = matchType; + return this; + } + + /** Returns the builder for call chaining. */ + public Builder setQuality(@Quality int quality) { + mQuality = quality; + return this; + } + + /** Returns the builder for call chaining. */ + public Builder addDebugInfo(@NonNull String debugInfo) { + if (mDebugInfo == null) { + mDebugInfo = new ArrayList<>(); + } + mDebugInfo.add(debugInfo); + return this; + } + + /** + * Performs basic structural validation of this instance. e.g. Are all the fields populated + * that must be? Are the enum ints set to valid values? + */ + void validate() { + int quality = mQuality; + int matchType = mMatchType; + if (mZoneId == null) { + if (quality != QUALITY_NA || matchType != MATCH_TYPE_NA) { + throw new RuntimeException("Invalid quality or match type for null zone ID." + + " quality=" + quality + ", matchType=" + matchType); + } + } else { + boolean qualityValid = (quality == QUALITY_SINGLE_ZONE + || quality == QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET + || quality == QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS); + boolean matchTypeValid = (matchType == MATCH_TYPE_NETWORK_COUNTRY_ONLY + || matchType == MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET + || matchType == MATCH_TYPE_EMULATOR_ZONE_ID + || matchType == MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY); + if (!qualityValid || !matchTypeValid) { + throw new RuntimeException("Invalid quality or match type with zone ID." + + " quality=" + quality + ", matchType=" + matchType); + } + } + } + + /** Returns the {@link PhoneTimeZoneSuggestion}. */ + public PhoneTimeZoneSuggestion build() { + validate(); + return new PhoneTimeZoneSuggestion(this); + } + } +} diff --git a/core/java/android/app/timezonedetector/TimeZoneDetector.java b/core/java/android/app/timezonedetector/TimeZoneDetector.java new file mode 100644 index 000000000000..909cbc2ccdf7 --- /dev/null +++ b/core/java/android/app/timezonedetector/TimeZoneDetector.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2019 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.timezonedetector; + +import android.annotation.NonNull; +import android.annotation.SystemService; +import android.content.Context; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.ServiceManager.ServiceNotFoundException; +import android.util.Log; + +/** + * The interface through which system components can send signals to the TimeZoneDetectorService. + * @hide + */ +@SystemService(Context.TIME_ZONE_DETECTOR_SERVICE) +public final class TimeZoneDetector { + private static final String TAG = "timezonedetector.TimeZoneDetector"; + private static final boolean DEBUG = false; + + private final ITimeZoneDetectorService mITimeZoneDetectorService; + + public TimeZoneDetector() throws ServiceNotFoundException { + mITimeZoneDetectorService = ITimeZoneDetectorService.Stub.asInterface( + ServiceManager.getServiceOrThrow(Context.TIME_ZONE_DETECTOR_SERVICE)); + } + + /** + * Suggests the current time zone to the detector. The detector may ignore the signal if better + * signals are available such as those that come from more reliable sources or were + * determined more recently. + */ + public void suggestPhoneTimeZone(@NonNull PhoneTimeZoneSuggestion timeZoneSuggestion) { + if (DEBUG) { + Log.d(TAG, "suggestPhoneTimeZone called: " + timeZoneSuggestion); + } + try { + mITimeZoneDetectorService.suggestPhoneTimeZone(timeZoneSuggestion); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + +} diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 507d1d82330c..8f5a6d3949b6 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -3329,6 +3329,7 @@ public abstract class Context { CROSS_PROFILE_APPS_SERVICE, //@hide: SYSTEM_UPDATE_SERVICE, //@hide: TIME_DETECTOR_SERVICE, + //@hide: TIME_ZONE_DETECTOR_SERVICE, PERMISSION_SERVICE, }) @Retention(RetentionPolicy.SOURCE) @@ -4743,7 +4744,7 @@ public abstract class Context { /** * Use with {@link #getSystemService(String)} to retrieve an - * {@link android.app.timedetector.ITimeDetectorService}. + * {@link android.app.timedetector.TimeDetector}. * @hide * * @see #getSystemService(String) @@ -4751,6 +4752,15 @@ public abstract class Context { public static final String TIME_DETECTOR_SERVICE = "time_detector"; /** + * Use with {@link #getSystemService(String)} to retrieve an + * {@link android.app.timezonedetector.TimeZoneDetector}. + * @hide + * + * @see #getSystemService(String) + */ + public static final String TIME_ZONE_DETECTOR_SERVICE = "time_zone_detector"; + + /** * Binder service name for {@link AppBindingService}. * @hide */ |
