Merge "Add a new time zone detection service"
am: 7d8a624085
Change-Id: Ic30cd636b4863a507dda0bdb05e982ee411d936f
This commit is contained in:
@@ -31,6 +31,7 @@ import android.app.role.RoleManager;
|
|||||||
import android.app.slice.SliceManager;
|
import android.app.slice.SliceManager;
|
||||||
import android.app.timedetector.TimeDetector;
|
import android.app.timedetector.TimeDetector;
|
||||||
import android.app.timezone.RulesManager;
|
import android.app.timezone.RulesManager;
|
||||||
|
import android.app.timezonedetector.TimeZoneDetector;
|
||||||
import android.app.trust.TrustManager;
|
import android.app.trust.TrustManager;
|
||||||
import android.app.usage.IStorageStatsManager;
|
import android.app.usage.IStorageStatsManager;
|
||||||
import android.app.usage.IUsageStatsManager;
|
import android.app.usage.IUsageStatsManager;
|
||||||
@@ -1236,6 +1237,14 @@ final class SystemServiceRegistry {
|
|||||||
return new TimeDetector();
|
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,
|
registerService(Context.PERMISSION_SERVICE, PermissionManager.class,
|
||||||
new CachedServiceFetcher<PermissionManager>() {
|
new CachedServiceFetcher<PermissionManager>() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
core/java/android/app/timezonedetector/TimeZoneDetector.java
Normal file
59
core/java/android/app/timezonedetector/TimeZoneDetector.java
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3376,6 +3376,7 @@ public abstract class Context {
|
|||||||
CROSS_PROFILE_APPS_SERVICE,
|
CROSS_PROFILE_APPS_SERVICE,
|
||||||
//@hide: SYSTEM_UPDATE_SERVICE,
|
//@hide: SYSTEM_UPDATE_SERVICE,
|
||||||
//@hide: TIME_DETECTOR_SERVICE,
|
//@hide: TIME_DETECTOR_SERVICE,
|
||||||
|
//@hide: TIME_ZONE_DETECTOR_SERVICE,
|
||||||
PERMISSION_SERVICE,
|
PERMISSION_SERVICE,
|
||||||
})
|
})
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@@ -4793,13 +4794,22 @@ public abstract class Context {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Use with {@link #getSystemService(String)} to retrieve an
|
* Use with {@link #getSystemService(String)} to retrieve an
|
||||||
* {@link android.app.timedetector.ITimeDetectorService}.
|
* {@link android.app.timedetector.TimeDetector}.
|
||||||
* @hide
|
* @hide
|
||||||
*
|
*
|
||||||
* @see #getSystemService(String)
|
* @see #getSystemService(String)
|
||||||
*/
|
*/
|
||||||
public static final String TIME_DETECTOR_SERVICE = "time_detector";
|
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}.
|
* Binder service name for {@link AppBindingService}.
|
||||||
* @hide
|
* @hide
|
||||||
|
|||||||
@@ -633,6 +633,11 @@
|
|||||||
|
|
||||||
<protected-broadcast android:name="android.intent.action.DEVICE_CUSTOMIZATION_READY" />
|
<protected-broadcast android:name="android.intent.action.DEVICE_CUSTOMIZATION_READY" />
|
||||||
|
|
||||||
|
<!-- NETWORK_SET_TIME / NETWORK_SET_TIMEZONE moved from com.android.phone to system server.
|
||||||
|
They should ultimately be removed. -->
|
||||||
|
<protected-broadcast android:name="android.intent.action.NETWORK_SET_TIME" />
|
||||||
|
<protected-broadcast android:name="android.intent.action.NETWORK_SET_TIMEZONE" />
|
||||||
|
|
||||||
<!-- For tether entitlement recheck-->
|
<!-- For tether entitlement recheck-->
|
||||||
<protected-broadcast
|
<protected-broadcast
|
||||||
android:name="com.android.server.connectivity.tethering.PROVISIONING_RECHECK_ALARM" />
|
android:name="com.android.server.connectivity.tethering.PROVISIONING_RECHECK_ALARM" />
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
/*
|
||||||
|
* 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 static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class PhoneTimeZoneSuggestionTest {
|
||||||
|
private static final int PHONE_ID = 99999;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEquals() {
|
||||||
|
PhoneTimeZoneSuggestion.Builder builder1 = new PhoneTimeZoneSuggestion.Builder(PHONE_ID);
|
||||||
|
{
|
||||||
|
PhoneTimeZoneSuggestion one = builder1.build();
|
||||||
|
assertEquals(one, one);
|
||||||
|
}
|
||||||
|
|
||||||
|
PhoneTimeZoneSuggestion.Builder builder2 = new PhoneTimeZoneSuggestion.Builder(PHONE_ID);
|
||||||
|
{
|
||||||
|
PhoneTimeZoneSuggestion one = builder1.build();
|
||||||
|
PhoneTimeZoneSuggestion two = builder2.build();
|
||||||
|
assertEquals(one, two);
|
||||||
|
assertEquals(two, one);
|
||||||
|
}
|
||||||
|
|
||||||
|
PhoneTimeZoneSuggestion.Builder builder3 =
|
||||||
|
new PhoneTimeZoneSuggestion.Builder(PHONE_ID + 1);
|
||||||
|
{
|
||||||
|
PhoneTimeZoneSuggestion one = builder1.build();
|
||||||
|
PhoneTimeZoneSuggestion three = builder3.build();
|
||||||
|
assertNotEquals(one, three);
|
||||||
|
assertNotEquals(three, one);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder1.setZoneId("Europe/London");
|
||||||
|
builder1.setMatchType(PhoneTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_ONLY);
|
||||||
|
builder1.setQuality(PhoneTimeZoneSuggestion.QUALITY_SINGLE_ZONE);
|
||||||
|
{
|
||||||
|
PhoneTimeZoneSuggestion one = builder1.build();
|
||||||
|
PhoneTimeZoneSuggestion two = builder2.build();
|
||||||
|
assertNotEquals(one, two);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder2.setZoneId("Europe/Paris");
|
||||||
|
builder2.setMatchType(PhoneTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_ONLY);
|
||||||
|
builder2.setQuality(PhoneTimeZoneSuggestion.QUALITY_SINGLE_ZONE);
|
||||||
|
{
|
||||||
|
PhoneTimeZoneSuggestion one = builder1.build();
|
||||||
|
PhoneTimeZoneSuggestion two = builder2.build();
|
||||||
|
assertNotEquals(one, two);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder1.setZoneId("Europe/Paris");
|
||||||
|
{
|
||||||
|
PhoneTimeZoneSuggestion one = builder1.build();
|
||||||
|
PhoneTimeZoneSuggestion two = builder2.build();
|
||||||
|
assertEquals(one, two);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder1.setMatchType(PhoneTimeZoneSuggestion.MATCH_TYPE_EMULATOR_ZONE_ID);
|
||||||
|
builder2.setMatchType(PhoneTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_ONLY);
|
||||||
|
{
|
||||||
|
PhoneTimeZoneSuggestion one = builder1.build();
|
||||||
|
PhoneTimeZoneSuggestion two = builder2.build();
|
||||||
|
assertNotEquals(one, two);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder1.setMatchType(PhoneTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_ONLY);
|
||||||
|
{
|
||||||
|
PhoneTimeZoneSuggestion one = builder1.build();
|
||||||
|
PhoneTimeZoneSuggestion two = builder2.build();
|
||||||
|
assertEquals(one, two);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder1.setQuality(PhoneTimeZoneSuggestion.QUALITY_SINGLE_ZONE);
|
||||||
|
builder2.setQuality(PhoneTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS);
|
||||||
|
{
|
||||||
|
PhoneTimeZoneSuggestion one = builder1.build();
|
||||||
|
PhoneTimeZoneSuggestion two = builder2.build();
|
||||||
|
assertNotEquals(one, two);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder1.setQuality(PhoneTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS);
|
||||||
|
{
|
||||||
|
PhoneTimeZoneSuggestion one = builder1.build();
|
||||||
|
PhoneTimeZoneSuggestion two = builder2.build();
|
||||||
|
assertEquals(one, two);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DebugInfo must not be considered in equals().
|
||||||
|
{
|
||||||
|
PhoneTimeZoneSuggestion one = builder1.build();
|
||||||
|
PhoneTimeZoneSuggestion two = builder2.build();
|
||||||
|
one.addDebugInfo("Debug info 1");
|
||||||
|
two.addDebugInfo("Debug info 2");
|
||||||
|
assertEquals(one, two);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = RuntimeException.class)
|
||||||
|
public void testBuilderValidates_emptyZone_badMatchType() {
|
||||||
|
PhoneTimeZoneSuggestion.Builder builder = new PhoneTimeZoneSuggestion.Builder(PHONE_ID);
|
||||||
|
// No zone ID, so match type should be left unset.
|
||||||
|
builder.setMatchType(PhoneTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET);
|
||||||
|
builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = RuntimeException.class)
|
||||||
|
public void testBuilderValidates_zoneSet_badMatchType() {
|
||||||
|
PhoneTimeZoneSuggestion.Builder builder = new PhoneTimeZoneSuggestion.Builder(PHONE_ID);
|
||||||
|
builder.setZoneId("Europe/London");
|
||||||
|
builder.setQuality(PhoneTimeZoneSuggestion.QUALITY_SINGLE_ZONE);
|
||||||
|
builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParcelable() {
|
||||||
|
PhoneTimeZoneSuggestion.Builder builder = new PhoneTimeZoneSuggestion.Builder(PHONE_ID);
|
||||||
|
assertRoundTripParcelable(builder.build());
|
||||||
|
|
||||||
|
builder.setZoneId("Europe/London");
|
||||||
|
builder.setMatchType(PhoneTimeZoneSuggestion.MATCH_TYPE_EMULATOR_ZONE_ID);
|
||||||
|
builder.setQuality(PhoneTimeZoneSuggestion.QUALITY_SINGLE_ZONE);
|
||||||
|
PhoneTimeZoneSuggestion suggestion1 = builder.build();
|
||||||
|
assertRoundTripParcelable(suggestion1);
|
||||||
|
|
||||||
|
// DebugInfo should also be stored (but is not checked by equals()
|
||||||
|
String debugString = "This is debug info";
|
||||||
|
suggestion1.addDebugInfo(debugString);
|
||||||
|
PhoneTimeZoneSuggestion suggestion1_2 = roundTripParcelable(suggestion1);
|
||||||
|
assertEquals(suggestion1, suggestion1_2);
|
||||||
|
assertTrue(suggestion1_2.getDebugInfo().contains(debugString));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertRoundTripParcelable(PhoneTimeZoneSuggestion instance) {
|
||||||
|
assertEquals(instance, roundTripParcelable(instance));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T extends Parcelable> T roundTripParcelable(T one) {
|
||||||
|
Parcel parcel = Parcel.obtain();
|
||||||
|
parcel.writeTypedObject(one, 0);
|
||||||
|
parcel.setDataPosition(0);
|
||||||
|
|
||||||
|
T toReturn = (T) parcel.readTypedObject(PhoneTimeZoneSuggestion.CREATOR);
|
||||||
|
parcel.recycle();
|
||||||
|
return toReturn;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.android.server.timezonedetector;
|
||||||
|
|
||||||
|
import android.annotation.Nullable;
|
||||||
|
import android.app.AlarmManager;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.SystemProperties;
|
||||||
|
import android.os.UserHandle;
|
||||||
|
import android.provider.Settings;
|
||||||
|
|
||||||
|
import com.android.internal.telephony.TelephonyIntents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The real implementation of {@link TimeZoneDetectorStrategy.Callback}.
|
||||||
|
*/
|
||||||
|
public final class TimeZoneDetectorCallbackImpl implements TimeZoneDetectorStrategy.Callback {
|
||||||
|
|
||||||
|
private static final String TIMEZONE_PROPERTY = "persist.sys.timezone";
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
private final ContentResolver mCr;
|
||||||
|
|
||||||
|
TimeZoneDetectorCallbackImpl(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
mCr = context.getContentResolver();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTimeZoneDetectionEnabled() {
|
||||||
|
return Settings.Global.getInt(mCr, Settings.Global.AUTO_TIME_ZONE, 1 /* default */) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDeviceTimeZoneInitialized() {
|
||||||
|
// timezone.equals("GMT") will be true and only true if the time zone was
|
||||||
|
// set to a default value by the system server (when starting, system server
|
||||||
|
// sets the persist.sys.timezone to "GMT" if it's not set). "GMT" is not used by
|
||||||
|
// any code that sets it explicitly (in case where something sets GMT explicitly,
|
||||||
|
// "Etc/GMT" Olson ID would be used).
|
||||||
|
|
||||||
|
String timeZoneId = getDeviceTimeZone();
|
||||||
|
return timeZoneId != null && timeZoneId.length() > 0 && !timeZoneId.equals("GMT");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public String getDeviceTimeZone() {
|
||||||
|
return SystemProperties.get(TIMEZONE_PROPERTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDeviceTimeZone(String zoneId) {
|
||||||
|
AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class);
|
||||||
|
alarmManager.setTimeZone(zoneId);
|
||||||
|
|
||||||
|
// TODO Nothing in the platform appears to listen for this. Remove it.
|
||||||
|
Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE);
|
||||||
|
intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
|
||||||
|
intent.putExtra("time-zone", zoneId);
|
||||||
|
mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.android.server.timezonedetector;
|
||||||
|
|
||||||
|
import android.annotation.NonNull;
|
||||||
|
import android.annotation.Nullable;
|
||||||
|
import android.app.timezonedetector.ITimeZoneDetectorService;
|
||||||
|
import android.app.timezonedetector.PhoneTimeZoneSuggestion;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.ContentObserver;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.provider.Settings;
|
||||||
|
|
||||||
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
|
import com.android.internal.util.DumpUtils;
|
||||||
|
import com.android.internal.util.IndentingPrintWriter;
|
||||||
|
import com.android.server.FgThread;
|
||||||
|
import com.android.server.SystemService;
|
||||||
|
|
||||||
|
import java.io.FileDescriptor;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The implementation of ITimeZoneDetectorService.aidl.
|
||||||
|
*/
|
||||||
|
public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub {
|
||||||
|
private static final String TAG = "TimeZoneDetectorService";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the lifecycle for {@link TimeZoneDetectorService}.
|
||||||
|
*/
|
||||||
|
public static class Lifecycle extends SystemService {
|
||||||
|
|
||||||
|
public Lifecycle(@NonNull Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
TimeZoneDetectorService service = TimeZoneDetectorService.create(getContext());
|
||||||
|
|
||||||
|
// Publish the binder service so it can be accessed from other (appropriately
|
||||||
|
// permissioned) processes.
|
||||||
|
publishBinderService(Context.TIME_ZONE_DETECTOR_SERVICE, service);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull private final Context mContext;
|
||||||
|
@NonNull private final Handler mHandler;
|
||||||
|
@NonNull private final TimeZoneDetectorStrategy mTimeZoneDetectorStrategy;
|
||||||
|
|
||||||
|
private static TimeZoneDetectorService create(@NonNull Context context) {
|
||||||
|
final TimeZoneDetectorStrategy timeZoneDetectorStrategy =
|
||||||
|
TimeZoneDetectorStrategy.create(context);
|
||||||
|
|
||||||
|
Handler handler = FgThread.getHandler();
|
||||||
|
ContentResolver contentResolver = context.getContentResolver();
|
||||||
|
contentResolver.registerContentObserver(
|
||||||
|
Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE), true,
|
||||||
|
new ContentObserver(handler) {
|
||||||
|
public void onChange(boolean selfChange) {
|
||||||
|
timeZoneDetectorStrategy.handleTimeZoneDetectionChange();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new TimeZoneDetectorService(context, handler, timeZoneDetectorStrategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public TimeZoneDetectorService(@NonNull Context context, @NonNull Handler handler,
|
||||||
|
@NonNull TimeZoneDetectorStrategy timeZoneDetectorStrategy) {
|
||||||
|
mContext = Objects.requireNonNull(context);
|
||||||
|
mHandler = Objects.requireNonNull(handler);
|
||||||
|
mTimeZoneDetectorStrategy = Objects.requireNonNull(timeZoneDetectorStrategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void suggestPhoneTimeZone(@NonNull PhoneTimeZoneSuggestion timeZoneSuggestion) {
|
||||||
|
enforceSetTimeZonePermission();
|
||||||
|
Objects.requireNonNull(timeZoneSuggestion);
|
||||||
|
|
||||||
|
mHandler.post(() -> mTimeZoneDetectorStrategy.suggestPhoneTimeZone(timeZoneSuggestion));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw,
|
||||||
|
@Nullable String[] args) {
|
||||||
|
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
|
||||||
|
|
||||||
|
mTimeZoneDetectorStrategy.dumpState(pw);
|
||||||
|
mTimeZoneDetectorStrategy.dumpLogs(new IndentingPrintWriter(pw, " "));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enforceSetTimeZonePermission() {
|
||||||
|
mContext.enforceCallingPermission(
|
||||||
|
android.Manifest.permission.SET_TIME_ZONE, "set time zone");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,507 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.android.server.timezonedetector;
|
||||||
|
|
||||||
|
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.MATCH_TYPE_EMULATOR_ZONE_ID;
|
||||||
|
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY;
|
||||||
|
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS;
|
||||||
|
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET;
|
||||||
|
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_SINGLE_ZONE;
|
||||||
|
|
||||||
|
import android.annotation.NonNull;
|
||||||
|
import android.annotation.Nullable;
|
||||||
|
import android.app.timezonedetector.PhoneTimeZoneSuggestion;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.ArrayMap;
|
||||||
|
import android.util.LocalLog;
|
||||||
|
import android.util.Slog;
|
||||||
|
|
||||||
|
import com.android.internal.annotations.GuardedBy;
|
||||||
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
|
import com.android.internal.util.IndentingPrintWriter;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A singleton, stateful time zone detection strategy that is aware of multiple phone devices. It
|
||||||
|
* keeps track of the most recent suggestion from each phone and it uses the best based on a scoring
|
||||||
|
* algorithm. If several phones provide the same score then the phone with the lowest numeric ID
|
||||||
|
* "wins". If the situation changes and it is no longer possible to be confident about the time
|
||||||
|
* zone, phones must submit an empty suggestion in order to "withdraw" their previous suggestion.
|
||||||
|
*/
|
||||||
|
public class TimeZoneDetectorStrategy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by {@link TimeZoneDetectorStrategy} to interact with the surrounding service. It can be
|
||||||
|
* faked for tests.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
public interface Callback {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if automatic time zone detection is enabled in settings.
|
||||||
|
*/
|
||||||
|
boolean isTimeZoneDetectionEnabled();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the device has had an explicit time zone set.
|
||||||
|
*/
|
||||||
|
boolean isDeviceTimeZoneInitialized();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the device's currently configured time zone.
|
||||||
|
*/
|
||||||
|
String getDeviceTimeZone();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the device's time zone.
|
||||||
|
*/
|
||||||
|
void setDeviceTimeZone(@NonNull String zoneId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final String LOG_TAG = "TimeZoneDetectorStrategy";
|
||||||
|
static final boolean DBG = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The abstract score for an empty or invalid suggestion.
|
||||||
|
*
|
||||||
|
* Used to score suggestions where there is no zone.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
public static final int SCORE_NONE = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The abstract score for a low quality suggestion.
|
||||||
|
*
|
||||||
|
* Used to score suggestions where:
|
||||||
|
* The suggested zone ID is one of several possibilities, and the possibilities have different
|
||||||
|
* offsets.
|
||||||
|
*
|
||||||
|
* You would have to be quite desperate to want to use this choice.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
public static final int SCORE_LOW = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The abstract score for a medium quality suggestion.
|
||||||
|
*
|
||||||
|
* Used for:
|
||||||
|
* The suggested zone ID is one of several possibilities but at least the possibilities have the
|
||||||
|
* same offset. Users would get the correct time but for the wrong reason. i.e. their device may
|
||||||
|
* switch to DST at the wrong time and (for example) their calendar events.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
public static final int SCORE_MEDIUM = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The abstract score for a high quality suggestion.
|
||||||
|
*
|
||||||
|
* Used for:
|
||||||
|
* The suggestion was for one zone ID and the answer was unambiguous and likely correct given
|
||||||
|
* the info available.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
public static final int SCORE_HIGH = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The abstract score for a highest quality suggestion.
|
||||||
|
*
|
||||||
|
* Used for:
|
||||||
|
* Suggestions that must "win" because they constitute test or emulator zone ID.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
public static final int SCORE_HIGHEST = 4;
|
||||||
|
|
||||||
|
/** The threshold at which suggestions are good enough to use to set the device's time zone. */
|
||||||
|
@VisibleForTesting
|
||||||
|
public static final int SCORE_USAGE_THRESHOLD = SCORE_MEDIUM;
|
||||||
|
|
||||||
|
/** The number of previous phone suggestions to keep for each ID (for use during debugging). */
|
||||||
|
private static final int KEEP_SUGGESTION_HISTORY_SIZE = 30;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final Callback mCallback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A log that records the decisions / decision metadata that affected the device's time zone
|
||||||
|
* (for use during debugging).
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private final LocalLog mTimeZoneChangesLog = new LocalLog(30);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mapping from phoneId to a linked list of time zone suggestions (the head being the latest).
|
||||||
|
* We typically expect one or two entries in this Map: devices will have a small number
|
||||||
|
* of telephony devices and phoneIds are assumed to be stable. The LinkedList associated with
|
||||||
|
* the ID will not exceed {@link #KEEP_SUGGESTION_HISTORY_SIZE} in size.
|
||||||
|
*/
|
||||||
|
@GuardedBy("this")
|
||||||
|
private ArrayMap<Integer, LinkedList<QualifiedPhoneTimeZoneSuggestion>> mSuggestionByPhoneId =
|
||||||
|
new ArrayMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The most recent best guess of time zone from all phones. Can be {@code null} to indicate
|
||||||
|
* there would be no current suggestion.
|
||||||
|
*/
|
||||||
|
@GuardedBy("this")
|
||||||
|
@Nullable
|
||||||
|
private QualifiedPhoneTimeZoneSuggestion mCurrentSuggestion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of {@link TimeZoneDetectorStrategy}.
|
||||||
|
*/
|
||||||
|
public static TimeZoneDetectorStrategy create(Context context) {
|
||||||
|
Callback timeZoneDetectionServiceHelper = new TimeZoneDetectorCallbackImpl(context);
|
||||||
|
return new TimeZoneDetectorStrategy(timeZoneDetectionServiceHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public TimeZoneDetectorStrategy(Callback callback) {
|
||||||
|
mCallback = Objects.requireNonNull(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suggests a time zone for the device, or withdraws a previous suggestion if
|
||||||
|
* {@link PhoneTimeZoneSuggestion#getZoneId()} is {@code null}. The suggestion is scoped to a
|
||||||
|
* specific {@link PhoneTimeZoneSuggestion#getPhoneId() phone}.
|
||||||
|
* See {@link PhoneTimeZoneSuggestion} for an explanation of the metadata associated with a
|
||||||
|
* suggestion. The service uses suggestions to decide whether to modify the device's time zone
|
||||||
|
* setting and what to set it to.
|
||||||
|
*/
|
||||||
|
public synchronized void suggestPhoneTimeZone(@NonNull PhoneTimeZoneSuggestion newSuggestion) {
|
||||||
|
if (DBG) {
|
||||||
|
Slog.d(LOG_TAG, "suggestPhoneTimeZone: newSuggestion=" + newSuggestion);
|
||||||
|
}
|
||||||
|
Objects.requireNonNull(newSuggestion);
|
||||||
|
|
||||||
|
int score = scoreSuggestion(newSuggestion);
|
||||||
|
QualifiedPhoneTimeZoneSuggestion scoredSuggestion =
|
||||||
|
new QualifiedPhoneTimeZoneSuggestion(newSuggestion, score);
|
||||||
|
|
||||||
|
// Record the suggestion against the correct phoneId.
|
||||||
|
LinkedList<QualifiedPhoneTimeZoneSuggestion> suggestions =
|
||||||
|
mSuggestionByPhoneId.get(newSuggestion.getPhoneId());
|
||||||
|
if (suggestions == null) {
|
||||||
|
suggestions = new LinkedList<>();
|
||||||
|
mSuggestionByPhoneId.put(newSuggestion.getPhoneId(), suggestions);
|
||||||
|
}
|
||||||
|
suggestions.addFirst(scoredSuggestion);
|
||||||
|
if (suggestions.size() > KEEP_SUGGESTION_HISTORY_SIZE) {
|
||||||
|
suggestions.removeLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now run the competition between the phones' suggestions.
|
||||||
|
doTimeZoneDetection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int scoreSuggestion(@NonNull PhoneTimeZoneSuggestion suggestion) {
|
||||||
|
int score;
|
||||||
|
if (suggestion.getZoneId() == null) {
|
||||||
|
score = SCORE_NONE;
|
||||||
|
} else if (suggestion.getMatchType() == MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY
|
||||||
|
|| suggestion.getMatchType() == MATCH_TYPE_EMULATOR_ZONE_ID) {
|
||||||
|
// Handle emulator / test cases : These suggestions should always just be used.
|
||||||
|
score = SCORE_HIGHEST;
|
||||||
|
} else if (suggestion.getQuality() == QUALITY_SINGLE_ZONE) {
|
||||||
|
score = SCORE_HIGH;
|
||||||
|
} else if (suggestion.getQuality() == QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET) {
|
||||||
|
// The suggestion may be wrong, but at least the offset should be correct.
|
||||||
|
score = SCORE_MEDIUM;
|
||||||
|
} else if (suggestion.getQuality() == QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS) {
|
||||||
|
// The suggestion has a good chance of being wrong.
|
||||||
|
score = SCORE_LOW;
|
||||||
|
} else {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the best available time zone suggestion from all phones. If it is high-enough quality
|
||||||
|
* and automatic time zone detection is enabled then it will be set on the device. The outcome
|
||||||
|
* can be that this service becomes / remains un-opinionated and nothing is set.
|
||||||
|
*/
|
||||||
|
@GuardedBy("this")
|
||||||
|
private void doTimeZoneDetection() {
|
||||||
|
QualifiedPhoneTimeZoneSuggestion bestSuggestion = findBestSuggestion();
|
||||||
|
boolean timeZoneDetectionEnabled = mCallback.isTimeZoneDetectionEnabled();
|
||||||
|
|
||||||
|
// Work out what to do with the best suggestion.
|
||||||
|
if (bestSuggestion == null) {
|
||||||
|
// There is no suggestion. Become un-opinionated.
|
||||||
|
if (DBG) {
|
||||||
|
Slog.d(LOG_TAG, "doTimeZoneDetection: No good suggestion."
|
||||||
|
+ " bestSuggestion=null"
|
||||||
|
+ ", timeZoneDetectionEnabled=" + timeZoneDetectionEnabled);
|
||||||
|
}
|
||||||
|
mCurrentSuggestion = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case handling for uninitialized devices. This should only happen once.
|
||||||
|
String newZoneId = bestSuggestion.suggestion.getZoneId();
|
||||||
|
if (newZoneId != null && !mCallback.isDeviceTimeZoneInitialized()) {
|
||||||
|
Slog.i(LOG_TAG, "doTimeZoneDetection: Device has no time zone set so might set the"
|
||||||
|
+ " device to the best available suggestion."
|
||||||
|
+ " bestSuggestion=" + bestSuggestion
|
||||||
|
+ ", timeZoneDetectionEnabled=" + timeZoneDetectionEnabled);
|
||||||
|
|
||||||
|
mCurrentSuggestion = bestSuggestion;
|
||||||
|
if (timeZoneDetectionEnabled) {
|
||||||
|
setDeviceTimeZone(bestSuggestion.suggestion);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean suggestionGoodEnough = bestSuggestion.score >= SCORE_USAGE_THRESHOLD;
|
||||||
|
if (!suggestionGoodEnough) {
|
||||||
|
if (DBG) {
|
||||||
|
Slog.d(LOG_TAG, "doTimeZoneDetection: Suggestion not good enough."
|
||||||
|
+ " bestSuggestion=" + bestSuggestion);
|
||||||
|
}
|
||||||
|
mCurrentSuggestion = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paranoia: Every suggestion above the SCORE_USAGE_THRESHOLD should have a non-null time
|
||||||
|
// zone ID.
|
||||||
|
if (newZoneId == null) {
|
||||||
|
Slog.w(LOG_TAG, "Empty zone suggestion scored higher than expected. This is an error:"
|
||||||
|
+ " bestSuggestion=" + bestSuggestion);
|
||||||
|
mCurrentSuggestion = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is a good suggestion. Store the suggestion and set the device time zone if
|
||||||
|
// settings allow.
|
||||||
|
mCurrentSuggestion = bestSuggestion;
|
||||||
|
|
||||||
|
// Only set the device time zone if time zone detection is enabled.
|
||||||
|
if (!timeZoneDetectionEnabled) {
|
||||||
|
if (DBG) {
|
||||||
|
Slog.d(LOG_TAG, "doTimeZoneDetection: Not setting the time zone because time zone"
|
||||||
|
+ " detection is disabled."
|
||||||
|
+ " bestSuggestion=" + bestSuggestion);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PhoneTimeZoneSuggestion suggestion = bestSuggestion.suggestion;
|
||||||
|
setDeviceTimeZone(suggestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setDeviceTimeZone(@NonNull PhoneTimeZoneSuggestion suggestion) {
|
||||||
|
String currentZoneId = mCallback.getDeviceTimeZone();
|
||||||
|
String newZoneId = suggestion.getZoneId();
|
||||||
|
|
||||||
|
// Paranoia: This should never happen.
|
||||||
|
if (newZoneId == null) {
|
||||||
|
Slog.w(LOG_TAG, "setDeviceTimeZone: Suggested zone is null."
|
||||||
|
+ " timeZoneSuggestion=" + suggestion);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid unnecessary changes / intents.
|
||||||
|
if (newZoneId.equals(currentZoneId)) {
|
||||||
|
// No need to set the device time zone - the setting is already what we would be
|
||||||
|
// suggesting.
|
||||||
|
if (DBG) {
|
||||||
|
Slog.d(LOG_TAG, "setDeviceTimeZone: No need to change the time zone;"
|
||||||
|
+ " device is already set to the suggested zone."
|
||||||
|
+ " timeZoneSuggestion=" + suggestion);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String msg = "Changing device time zone. currentZoneId=" + currentZoneId
|
||||||
|
+ ", timeZoneSuggestion=" + suggestion;
|
||||||
|
if (DBG) {
|
||||||
|
Slog.d(LOG_TAG, msg);
|
||||||
|
}
|
||||||
|
mTimeZoneChangesLog.log(msg);
|
||||||
|
mCallback.setDeviceTimeZone(newZoneId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GuardedBy("this")
|
||||||
|
@Nullable
|
||||||
|
private QualifiedPhoneTimeZoneSuggestion findBestSuggestion() {
|
||||||
|
QualifiedPhoneTimeZoneSuggestion bestSuggestion = null;
|
||||||
|
|
||||||
|
// Iterate over the latest QualifiedPhoneTimeZoneSuggestion objects received for each phone
|
||||||
|
// and find the best. Note that we deliberately do not look at age: the caller can
|
||||||
|
// rate-limit so age is not a strong indicator of confidence. Instead, the callers are
|
||||||
|
// expected to withdraw suggestions they no longer have confidence in.
|
||||||
|
for (int i = 0; i < mSuggestionByPhoneId.size(); i++) {
|
||||||
|
LinkedList<QualifiedPhoneTimeZoneSuggestion> phoneSuggestions =
|
||||||
|
mSuggestionByPhoneId.valueAt(i);
|
||||||
|
if (phoneSuggestions == null) {
|
||||||
|
// Unexpected
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QualifiedPhoneTimeZoneSuggestion candidateSuggestion = phoneSuggestions.getFirst();
|
||||||
|
if (candidateSuggestion == null) {
|
||||||
|
// Unexpected
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestSuggestion == null) {
|
||||||
|
bestSuggestion = candidateSuggestion;
|
||||||
|
} else if (candidateSuggestion.score > bestSuggestion.score) {
|
||||||
|
bestSuggestion = candidateSuggestion;
|
||||||
|
} else if (candidateSuggestion.score == bestSuggestion.score) {
|
||||||
|
// Tie! Use the suggestion with the lowest phoneId.
|
||||||
|
int candidatePhoneId = candidateSuggestion.suggestion.getPhoneId();
|
||||||
|
int bestPhoneId = bestSuggestion.suggestion.getPhoneId();
|
||||||
|
if (candidatePhoneId < bestPhoneId) {
|
||||||
|
bestSuggestion = candidateSuggestion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestSuggestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current best suggestion. Not intended for general use: it is used during tests
|
||||||
|
* to check service behavior.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
@Nullable
|
||||||
|
public synchronized QualifiedPhoneTimeZoneSuggestion findBestSuggestionForTests() {
|
||||||
|
return findBestSuggestion();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the has been a change to the automatic time zone detection setting.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
public synchronized void handleTimeZoneDetectionChange() {
|
||||||
|
if (DBG) {
|
||||||
|
Slog.d(LOG_TAG, "handleTimeZoneDetectionChange() called");
|
||||||
|
}
|
||||||
|
if (mCallback.isTimeZoneDetectionEnabled()) {
|
||||||
|
// When the user enabled time zone detection, run the time zone detection and change the
|
||||||
|
// device time zone if possible.
|
||||||
|
doTimeZoneDetection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dumps any logs held to the supplied writer.
|
||||||
|
*/
|
||||||
|
public synchronized void dumpLogs(IndentingPrintWriter ipw) {
|
||||||
|
ipw.println("TimeZoneDetectorStrategy:");
|
||||||
|
|
||||||
|
ipw.increaseIndent(); // level 1
|
||||||
|
|
||||||
|
ipw.println("Time zone change log:");
|
||||||
|
ipw.increaseIndent(); // level 2
|
||||||
|
mTimeZoneChangesLog.dump(ipw);
|
||||||
|
ipw.decreaseIndent(); // level 2
|
||||||
|
|
||||||
|
ipw.println("Phone suggestion history:");
|
||||||
|
ipw.increaseIndent(); // level 2
|
||||||
|
for (Map.Entry<Integer, LinkedList<QualifiedPhoneTimeZoneSuggestion>> entry
|
||||||
|
: mSuggestionByPhoneId.entrySet()) {
|
||||||
|
ipw.println("Phone " + entry.getKey());
|
||||||
|
|
||||||
|
ipw.increaseIndent(); // level 3
|
||||||
|
for (QualifiedPhoneTimeZoneSuggestion suggestion : entry.getValue()) {
|
||||||
|
ipw.println(suggestion);
|
||||||
|
}
|
||||||
|
ipw.decreaseIndent(); // level 3
|
||||||
|
}
|
||||||
|
ipw.decreaseIndent(); // level 2
|
||||||
|
ipw.decreaseIndent(); // level 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dumps internal state such as field values.
|
||||||
|
*/
|
||||||
|
public synchronized void dumpState(PrintWriter pw) {
|
||||||
|
pw.println("mCurrentSuggestion=" + mCurrentSuggestion);
|
||||||
|
pw.println("mCallback.isTimeZoneDetectionEnabled()="
|
||||||
|
+ mCallback.isTimeZoneDetectionEnabled());
|
||||||
|
pw.println("mCallback.isDeviceTimeZoneInitialized()="
|
||||||
|
+ mCallback.isDeviceTimeZoneInitialized());
|
||||||
|
pw.println("mCallback.getDeviceTimeZone()="
|
||||||
|
+ mCallback.getDeviceTimeZone());
|
||||||
|
pw.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A method used to inspect service state during tests. Not intended for general use.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
public synchronized QualifiedPhoneTimeZoneSuggestion getLatestPhoneSuggestion(int phoneId) {
|
||||||
|
LinkedList<QualifiedPhoneTimeZoneSuggestion> suggestions =
|
||||||
|
mSuggestionByPhoneId.get(phoneId);
|
||||||
|
if (suggestions == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return suggestions.getFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link PhoneTimeZoneSuggestion} with additional qualifying metadata.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
public static class QualifiedPhoneTimeZoneSuggestion {
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public final PhoneTimeZoneSuggestion suggestion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The score the suggestion has been given. This can be used to rank against other
|
||||||
|
* suggestions of the same type.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
public final int score;
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public QualifiedPhoneTimeZoneSuggestion(PhoneTimeZoneSuggestion suggestion, int score) {
|
||||||
|
this.suggestion = suggestion;
|
||||||
|
this.score = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (o == null || getClass() != o.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QualifiedPhoneTimeZoneSuggestion that = (QualifiedPhoneTimeZoneSuggestion) o;
|
||||||
|
return score == that.score
|
||||||
|
&& suggestion.equals(that.suggestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(score, suggestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "QualifiedPhoneTimeZoneSuggestion{"
|
||||||
|
+ "suggestion=" + suggestion
|
||||||
|
+ ", score=" + score
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -274,6 +274,8 @@ public final class SystemServer {
|
|||||||
"com.android.internal.car.CarServiceHelperService";
|
"com.android.internal.car.CarServiceHelperService";
|
||||||
private static final String TIME_DETECTOR_SERVICE_CLASS =
|
private static final String TIME_DETECTOR_SERVICE_CLASS =
|
||||||
"com.android.server.timedetector.TimeDetectorService$Lifecycle";
|
"com.android.server.timedetector.TimeDetectorService$Lifecycle";
|
||||||
|
private static final String TIME_ZONE_DETECTOR_SERVICE_CLASS =
|
||||||
|
"com.android.server.timezonedetector.TimeZoneDetectorService$Lifecycle";
|
||||||
private static final String ACCESSIBILITY_MANAGER_SERVICE_CLASS =
|
private static final String ACCESSIBILITY_MANAGER_SERVICE_CLASS =
|
||||||
"com.android.server.accessibility.AccessibilityManagerService$Lifecycle";
|
"com.android.server.accessibility.AccessibilityManagerService$Lifecycle";
|
||||||
private static final String ADB_SERVICE_CLASS =
|
private static final String ADB_SERVICE_CLASS =
|
||||||
@@ -1474,6 +1476,14 @@ public final class SystemServer {
|
|||||||
}
|
}
|
||||||
traceEnd();
|
traceEnd();
|
||||||
|
|
||||||
|
traceBeginAndSlog("StartTimeZoneDetectorService");
|
||||||
|
try {
|
||||||
|
mSystemServiceManager.startService(TIME_ZONE_DETECTOR_SERVICE_CLASS);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
reportWtf("starting StartTimeZoneDetectorService service", e);
|
||||||
|
}
|
||||||
|
traceEnd();
|
||||||
|
|
||||||
if (!isWatch) {
|
if (!isWatch) {
|
||||||
traceBeginAndSlog("StartSearchManagerService");
|
traceBeginAndSlog("StartSearchManagerService");
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,592 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.android.server.timezonedetector;
|
||||||
|
|
||||||
|
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.MATCH_TYPE_EMULATOR_ZONE_ID;
|
||||||
|
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET;
|
||||||
|
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_ONLY;
|
||||||
|
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY;
|
||||||
|
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS;
|
||||||
|
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET;
|
||||||
|
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_SINGLE_ZONE;
|
||||||
|
|
||||||
|
import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.SCORE_HIGH;
|
||||||
|
import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.SCORE_HIGHEST;
|
||||||
|
import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.SCORE_LOW;
|
||||||
|
import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.SCORE_MEDIUM;
|
||||||
|
import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.SCORE_NONE;
|
||||||
|
import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.SCORE_USAGE_THRESHOLD;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import android.app.timezonedetector.PhoneTimeZoneSuggestion;
|
||||||
|
import android.app.timezonedetector.PhoneTimeZoneSuggestion.MatchType;
|
||||||
|
import android.app.timezonedetector.PhoneTimeZoneSuggestion.Quality;
|
||||||
|
|
||||||
|
import com.android.server.timezonedetector.TimeZoneDetectorStrategy.QualifiedPhoneTimeZoneSuggestion;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* White-box unit tests for {@link TimeZoneDetectorStrategy}.
|
||||||
|
*/
|
||||||
|
public class TimeZoneDetectorStrategyTest {
|
||||||
|
|
||||||
|
/** A time zone used for initialization that does not occur elsewhere in tests. */
|
||||||
|
private static final String ARBITRARY_TIME_ZONE_ID = "Etc/UTC";
|
||||||
|
private static final int PHONE1_ID = 10000;
|
||||||
|
private static final int PHONE2_ID = 20000;
|
||||||
|
|
||||||
|
// Suggestion test cases are ordered so that each successive one is of the same or higher score
|
||||||
|
// than the previous.
|
||||||
|
private static final SuggestionTestCase[] TEST_CASES = new SuggestionTestCase[] {
|
||||||
|
newTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY,
|
||||||
|
QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS, SCORE_LOW),
|
||||||
|
newTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY, QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET,
|
||||||
|
SCORE_MEDIUM),
|
||||||
|
newTestCase(MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET,
|
||||||
|
QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET, SCORE_MEDIUM),
|
||||||
|
newTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY, QUALITY_SINGLE_ZONE, SCORE_HIGH),
|
||||||
|
newTestCase(MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, QUALITY_SINGLE_ZONE, SCORE_HIGH),
|
||||||
|
newTestCase(MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY,
|
||||||
|
QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET, SCORE_HIGHEST),
|
||||||
|
newTestCase(MATCH_TYPE_EMULATOR_ZONE_ID, QUALITY_SINGLE_ZONE, SCORE_HIGHEST),
|
||||||
|
};
|
||||||
|
|
||||||
|
private TimeZoneDetectorStrategy mTimeZoneDetectorStrategy;
|
||||||
|
private FakeTimeZoneDetectorStrategyCallback mFakeTimeZoneDetectorStrategyCallback;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
mFakeTimeZoneDetectorStrategyCallback = new FakeTimeZoneDetectorStrategyCallback();
|
||||||
|
mTimeZoneDetectorStrategy =
|
||||||
|
new TimeZoneDetectorStrategy(mFakeTimeZoneDetectorStrategyCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmptySuggestions() {
|
||||||
|
PhoneTimeZoneSuggestion phone1TimeZoneSuggestion = createEmptyPhone1Suggestion();
|
||||||
|
PhoneTimeZoneSuggestion phone2TimeZoneSuggestion = createEmptyPhone2Suggestion();
|
||||||
|
Script script = new Script()
|
||||||
|
.initializeTimeZoneDetectionEnabled(true)
|
||||||
|
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID);
|
||||||
|
|
||||||
|
script.suggestPhoneTimeZone(phone1TimeZoneSuggestion)
|
||||||
|
.verifyTimeZoneNotSet();
|
||||||
|
|
||||||
|
// Assert internal service state.
|
||||||
|
QualifiedPhoneTimeZoneSuggestion expectedPhone1ScoredSuggestion =
|
||||||
|
new QualifiedPhoneTimeZoneSuggestion(phone1TimeZoneSuggestion, SCORE_NONE);
|
||||||
|
assertEquals(expectedPhone1ScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
|
||||||
|
assertNull(mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE2_ID));
|
||||||
|
assertEquals(expectedPhone1ScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.findBestSuggestionForTests());
|
||||||
|
|
||||||
|
script.suggestPhoneTimeZone(phone2TimeZoneSuggestion)
|
||||||
|
.verifyTimeZoneNotSet();
|
||||||
|
|
||||||
|
// Assert internal service state.
|
||||||
|
QualifiedPhoneTimeZoneSuggestion expectedPhone2ScoredSuggestion =
|
||||||
|
new QualifiedPhoneTimeZoneSuggestion(phone2TimeZoneSuggestion, SCORE_NONE);
|
||||||
|
assertEquals(expectedPhone1ScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
|
||||||
|
assertEquals(expectedPhone2ScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE2_ID));
|
||||||
|
// Phone 1 should always beat phone 2, all other things being equal.
|
||||||
|
assertEquals(expectedPhone1ScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.findBestSuggestionForTests());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFirstPlausibleSuggestionAcceptedWhenTimeZoneUninitialized() {
|
||||||
|
SuggestionTestCase testCase = newTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY,
|
||||||
|
QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS, SCORE_LOW);
|
||||||
|
PhoneTimeZoneSuggestion lowQualitySuggestion =
|
||||||
|
testCase.createSuggestion(PHONE1_ID, "America/New_York");
|
||||||
|
|
||||||
|
// The device time zone setting is left uninitialized.
|
||||||
|
Script script = new Script()
|
||||||
|
.initializeTimeZoneDetectionEnabled(true);
|
||||||
|
|
||||||
|
// The very first suggestion will be taken.
|
||||||
|
script.suggestPhoneTimeZone(lowQualitySuggestion)
|
||||||
|
.verifyTimeZoneSetAndReset(lowQualitySuggestion);
|
||||||
|
|
||||||
|
// Assert internal service state.
|
||||||
|
QualifiedPhoneTimeZoneSuggestion expectedScoredSuggestion =
|
||||||
|
new QualifiedPhoneTimeZoneSuggestion(lowQualitySuggestion, testCase.expectedScore);
|
||||||
|
assertEquals(expectedScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
|
||||||
|
assertEquals(expectedScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.findBestSuggestionForTests());
|
||||||
|
|
||||||
|
// Another low quality suggestion will be ignored now that the setting is initialized.
|
||||||
|
PhoneTimeZoneSuggestion lowQualitySuggestion2 =
|
||||||
|
testCase.createSuggestion(PHONE1_ID, "America/Los_Angeles");
|
||||||
|
script.suggestPhoneTimeZone(lowQualitySuggestion2)
|
||||||
|
.verifyTimeZoneNotSet();
|
||||||
|
|
||||||
|
// Assert internal service state.
|
||||||
|
QualifiedPhoneTimeZoneSuggestion expectedScoredSuggestion2 =
|
||||||
|
new QualifiedPhoneTimeZoneSuggestion(lowQualitySuggestion2, testCase.expectedScore);
|
||||||
|
assertEquals(expectedScoredSuggestion2,
|
||||||
|
mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
|
||||||
|
assertEquals(expectedScoredSuggestion2,
|
||||||
|
mTimeZoneDetectorStrategy.findBestSuggestionForTests());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirms that toggling the auto time zone detection setting has the expected behavior when
|
||||||
|
* the strategy is "opinionated".
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testTogglingTimeZoneDetection() {
|
||||||
|
Script script = new Script();
|
||||||
|
|
||||||
|
for (SuggestionTestCase testCase : TEST_CASES) {
|
||||||
|
// Start with the device in a known state.
|
||||||
|
script.initializeTimeZoneDetectionEnabled(false)
|
||||||
|
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID);
|
||||||
|
|
||||||
|
PhoneTimeZoneSuggestion suggestion =
|
||||||
|
testCase.createSuggestion(PHONE1_ID, "Europe/London");
|
||||||
|
script.suggestPhoneTimeZone(suggestion);
|
||||||
|
|
||||||
|
// When time zone detection is not enabled, the time zone suggestion will not be set
|
||||||
|
// regardless of the score.
|
||||||
|
script.verifyTimeZoneNotSet();
|
||||||
|
|
||||||
|
// Assert internal service state.
|
||||||
|
QualifiedPhoneTimeZoneSuggestion expectedScoredSuggestion =
|
||||||
|
new QualifiedPhoneTimeZoneSuggestion(suggestion, testCase.expectedScore);
|
||||||
|
assertEquals(expectedScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
|
||||||
|
assertEquals(expectedScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.findBestSuggestionForTests());
|
||||||
|
|
||||||
|
// Toggling the time zone setting on should cause the device setting to be set.
|
||||||
|
script.timeZoneDetectionEnabled(true);
|
||||||
|
|
||||||
|
// When time zone detection is already enabled the suggestion (if it scores highly
|
||||||
|
// enough) should be set immediately.
|
||||||
|
if (testCase.expectedScore >= SCORE_USAGE_THRESHOLD) {
|
||||||
|
script.verifyTimeZoneSetAndReset(suggestion);
|
||||||
|
} else {
|
||||||
|
script.verifyTimeZoneNotSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert internal service state.
|
||||||
|
assertEquals(expectedScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
|
||||||
|
assertEquals(expectedScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.findBestSuggestionForTests());
|
||||||
|
|
||||||
|
// Toggling the time zone setting should off should do nothing.
|
||||||
|
script.timeZoneDetectionEnabled(false)
|
||||||
|
.verifyTimeZoneNotSet();
|
||||||
|
|
||||||
|
// Assert internal service state.
|
||||||
|
assertEquals(expectedScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
|
||||||
|
assertEquals(expectedScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.findBestSuggestionForTests());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuggestionsSinglePhone() {
|
||||||
|
Script script = new Script()
|
||||||
|
.initializeTimeZoneDetectionEnabled(true)
|
||||||
|
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID);
|
||||||
|
|
||||||
|
for (SuggestionTestCase testCase : TEST_CASES) {
|
||||||
|
makePhone1SuggestionAndCheckState(script, testCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This is the same test as above but the test cases are in
|
||||||
|
* reverse order of their expected score. New suggestions always replace previous ones:
|
||||||
|
* there's effectively no history and so ordering shouldn't make any difference.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Each test case will have the same or lower score than the last.
|
||||||
|
ArrayList<SuggestionTestCase> descendingCasesByScore =
|
||||||
|
new ArrayList<>(Arrays.asList(TEST_CASES));
|
||||||
|
Collections.reverse(descendingCasesByScore);
|
||||||
|
|
||||||
|
for (SuggestionTestCase testCase : descendingCasesByScore) {
|
||||||
|
makePhone1SuggestionAndCheckState(script, testCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void makePhone1SuggestionAndCheckState(Script script, SuggestionTestCase testCase) {
|
||||||
|
// Give the next suggestion a different zone from the currently set device time zone;
|
||||||
|
String currentZoneId = mFakeTimeZoneDetectorStrategyCallback.getDeviceTimeZone();
|
||||||
|
String suggestionZoneId =
|
||||||
|
"Europe/London".equals(currentZoneId) ? "Europe/Paris" : "Europe/London";
|
||||||
|
PhoneTimeZoneSuggestion zonePhone1Suggestion =
|
||||||
|
testCase.createSuggestion(PHONE1_ID, suggestionZoneId);
|
||||||
|
QualifiedPhoneTimeZoneSuggestion expectedZonePhone1ScoredSuggestion =
|
||||||
|
new QualifiedPhoneTimeZoneSuggestion(zonePhone1Suggestion, testCase.expectedScore);
|
||||||
|
|
||||||
|
script.suggestPhoneTimeZone(zonePhone1Suggestion);
|
||||||
|
if (testCase.expectedScore >= SCORE_USAGE_THRESHOLD) {
|
||||||
|
script.verifyTimeZoneSetAndReset(zonePhone1Suggestion);
|
||||||
|
} else {
|
||||||
|
script.verifyTimeZoneNotSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert internal service state.
|
||||||
|
assertEquals(expectedZonePhone1ScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
|
||||||
|
assertEquals(expectedZonePhone1ScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.findBestSuggestionForTests());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries a set of test cases to see if the phone with the lowest ID is given preference. This
|
||||||
|
* test also confirms that the time zone setting would only be set if a suggestion is of
|
||||||
|
* sufficient quality.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testMultiplePhoneSuggestionScoringAndPhoneIdBias() {
|
||||||
|
String[] zoneIds = { "Europe/London", "Europe/Paris" };
|
||||||
|
PhoneTimeZoneSuggestion emptyPhone1Suggestion = createEmptyPhone1Suggestion();
|
||||||
|
PhoneTimeZoneSuggestion emptyPhone2Suggestion = createEmptyPhone2Suggestion();
|
||||||
|
QualifiedPhoneTimeZoneSuggestion expectedEmptyPhone1ScoredSuggestion =
|
||||||
|
new QualifiedPhoneTimeZoneSuggestion(emptyPhone1Suggestion, SCORE_NONE);
|
||||||
|
QualifiedPhoneTimeZoneSuggestion expectedEmptyPhone2ScoredSuggestion =
|
||||||
|
new QualifiedPhoneTimeZoneSuggestion(emptyPhone2Suggestion, SCORE_NONE);
|
||||||
|
|
||||||
|
Script script = new Script()
|
||||||
|
.initializeTimeZoneDetectionEnabled(true)
|
||||||
|
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
|
||||||
|
// Initialize the latest suggestions as empty so we don't need to worry about nulls
|
||||||
|
// below for the first loop.
|
||||||
|
.suggestPhoneTimeZone(emptyPhone1Suggestion)
|
||||||
|
.suggestPhoneTimeZone(emptyPhone2Suggestion)
|
||||||
|
.resetState();
|
||||||
|
|
||||||
|
for (SuggestionTestCase testCase : TEST_CASES) {
|
||||||
|
PhoneTimeZoneSuggestion zonePhone1Suggestion =
|
||||||
|
testCase.createSuggestion(PHONE1_ID, zoneIds[0]);
|
||||||
|
PhoneTimeZoneSuggestion zonePhone2Suggestion =
|
||||||
|
testCase.createSuggestion(PHONE2_ID, zoneIds[1]);
|
||||||
|
QualifiedPhoneTimeZoneSuggestion expectedZonePhone1ScoredSuggestion =
|
||||||
|
new QualifiedPhoneTimeZoneSuggestion(zonePhone1Suggestion,
|
||||||
|
testCase.expectedScore);
|
||||||
|
QualifiedPhoneTimeZoneSuggestion expectedZonePhone2ScoredSuggestion =
|
||||||
|
new QualifiedPhoneTimeZoneSuggestion(zonePhone2Suggestion,
|
||||||
|
testCase.expectedScore);
|
||||||
|
|
||||||
|
// Start the test by making a suggestion for phone 1.
|
||||||
|
script.suggestPhoneTimeZone(zonePhone1Suggestion);
|
||||||
|
if (testCase.expectedScore >= SCORE_USAGE_THRESHOLD) {
|
||||||
|
script.verifyTimeZoneSetAndReset(zonePhone1Suggestion);
|
||||||
|
} else {
|
||||||
|
script.verifyTimeZoneNotSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert internal service state.
|
||||||
|
assertEquals(expectedZonePhone1ScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
|
||||||
|
assertEquals(expectedEmptyPhone2ScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE2_ID));
|
||||||
|
assertEquals(expectedZonePhone1ScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.findBestSuggestionForTests());
|
||||||
|
|
||||||
|
// Phone 2 then makes an alternative suggestion with an identical score. Phone 1's
|
||||||
|
// suggestion should still "win" if it is above the required threshold.
|
||||||
|
script.suggestPhoneTimeZone(zonePhone2Suggestion);
|
||||||
|
script.verifyTimeZoneNotSet();
|
||||||
|
|
||||||
|
// Assert internal service state.
|
||||||
|
assertEquals(expectedZonePhone1ScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
|
||||||
|
assertEquals(expectedZonePhone2ScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE2_ID));
|
||||||
|
// Phone 1 should always beat phone 2, all other things being equal.
|
||||||
|
assertEquals(expectedZonePhone1ScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.findBestSuggestionForTests());
|
||||||
|
|
||||||
|
// Withdrawing phone 1's suggestion should leave phone 2 as the new winner. Since the
|
||||||
|
// zoneId is different, the time zone setting should be updated if the score is high
|
||||||
|
// enough.
|
||||||
|
script.suggestPhoneTimeZone(emptyPhone1Suggestion);
|
||||||
|
if (testCase.expectedScore >= SCORE_USAGE_THRESHOLD) {
|
||||||
|
script.verifyTimeZoneSetAndReset(zonePhone2Suggestion);
|
||||||
|
} else {
|
||||||
|
script.verifyTimeZoneNotSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert internal service state.
|
||||||
|
assertEquals(expectedEmptyPhone1ScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
|
||||||
|
assertEquals(expectedZonePhone2ScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE2_ID));
|
||||||
|
assertEquals(expectedZonePhone2ScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.findBestSuggestionForTests());
|
||||||
|
|
||||||
|
// Reset the state for the next loop.
|
||||||
|
script.suggestPhoneTimeZone(emptyPhone2Suggestion)
|
||||||
|
.verifyTimeZoneNotSet();
|
||||||
|
assertEquals(expectedEmptyPhone1ScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
|
||||||
|
assertEquals(expectedEmptyPhone2ScoredSuggestion,
|
||||||
|
mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE2_ID));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link TimeZoneDetectorStrategy.Callback} is left to detect whether changing the time
|
||||||
|
* zone is actually necessary. This test proves that the service doesn't assume it knows the
|
||||||
|
* current setting.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testTimeZoneDetectorStrategyDoesNotAssumeCurrentSetting() {
|
||||||
|
Script script = new Script()
|
||||||
|
.initializeTimeZoneDetectionEnabled(true);
|
||||||
|
|
||||||
|
SuggestionTestCase testCase =
|
||||||
|
newTestCase(MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, QUALITY_SINGLE_ZONE, SCORE_HIGH);
|
||||||
|
PhoneTimeZoneSuggestion losAngelesSuggestion =
|
||||||
|
testCase.createSuggestion(PHONE1_ID, "America/Los_Angeles");
|
||||||
|
PhoneTimeZoneSuggestion newYorkSuggestion =
|
||||||
|
testCase.createSuggestion(PHONE1_ID, "America/New_York");
|
||||||
|
|
||||||
|
// Initialization.
|
||||||
|
script.suggestPhoneTimeZone(losAngelesSuggestion)
|
||||||
|
.verifyTimeZoneSetAndReset(losAngelesSuggestion);
|
||||||
|
// Suggest it again - it should not be set because it is already set.
|
||||||
|
script.suggestPhoneTimeZone(losAngelesSuggestion)
|
||||||
|
.verifyTimeZoneNotSet();
|
||||||
|
|
||||||
|
// Toggling time zone detection should set the device time zone only if the current setting
|
||||||
|
// value is different from the most recent phone suggestion.
|
||||||
|
script.timeZoneDetectionEnabled(false)
|
||||||
|
.verifyTimeZoneNotSet()
|
||||||
|
.timeZoneDetectionEnabled(true)
|
||||||
|
.verifyTimeZoneNotSet();
|
||||||
|
|
||||||
|
// Simulate a user turning auto detection off, a new suggestion being made while auto
|
||||||
|
// detection is off, and the user turning it on again.
|
||||||
|
script.timeZoneDetectionEnabled(false)
|
||||||
|
.suggestPhoneTimeZone(newYorkSuggestion)
|
||||||
|
.verifyTimeZoneNotSet();
|
||||||
|
// Latest suggestion should be used.
|
||||||
|
script.timeZoneDetectionEnabled(true)
|
||||||
|
.verifyTimeZoneSetAndReset(newYorkSuggestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PhoneTimeZoneSuggestion createEmptyPhone1Suggestion() {
|
||||||
|
return new PhoneTimeZoneSuggestion.Builder(PHONE1_ID).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PhoneTimeZoneSuggestion createEmptyPhone2Suggestion() {
|
||||||
|
return new PhoneTimeZoneSuggestion.Builder(PHONE2_ID).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeTimeZoneDetectorStrategyCallback implements TimeZoneDetectorStrategy.Callback {
|
||||||
|
|
||||||
|
private boolean mTimeZoneDetectionEnabled;
|
||||||
|
private TestState<String> mTimeZoneId = new TestState<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTimeZoneDetectionEnabled() {
|
||||||
|
return mTimeZoneDetectionEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDeviceTimeZoneInitialized() {
|
||||||
|
return mTimeZoneId.getLatest() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDeviceTimeZone() {
|
||||||
|
return mTimeZoneId.getLatest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDeviceTimeZone(String zoneId) {
|
||||||
|
mTimeZoneId.set(zoneId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void initializeTimeZoneDetectionEnabled(boolean enabled) {
|
||||||
|
mTimeZoneDetectionEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void initializeTimeZone(String zoneId) {
|
||||||
|
mTimeZoneId.init(zoneId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setTimeZoneDetectionEnabled(boolean enabled) {
|
||||||
|
mTimeZoneDetectionEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void assertTimeZoneNotSet() {
|
||||||
|
mTimeZoneId.assertHasNotBeenSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
void assertTimeZoneSet(String timeZoneId) {
|
||||||
|
mTimeZoneId.assertHasBeenSet();
|
||||||
|
mTimeZoneId.assertChangeCount(1);
|
||||||
|
mTimeZoneId.assertLatestEquals(timeZoneId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void commitAllChanges() {
|
||||||
|
mTimeZoneId.commitLatest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Some piece of state that tests want to track. */
|
||||||
|
private static class TestState<T> {
|
||||||
|
private T mInitialValue;
|
||||||
|
private LinkedList<T> mValues = new LinkedList<>();
|
||||||
|
|
||||||
|
void init(T value) {
|
||||||
|
mValues.clear();
|
||||||
|
mInitialValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set(T value) {
|
||||||
|
mValues.addFirst(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasBeenSet() {
|
||||||
|
return mValues.size() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void assertHasNotBeenSet() {
|
||||||
|
assertFalse(hasBeenSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
void assertHasBeenSet() {
|
||||||
|
assertTrue(hasBeenSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
void commitLatest() {
|
||||||
|
if (hasBeenSet()) {
|
||||||
|
mInitialValue = mValues.getLast();
|
||||||
|
mValues.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void assertLatestEquals(T expected) {
|
||||||
|
assertEquals(expected, getLatest());
|
||||||
|
}
|
||||||
|
|
||||||
|
void assertChangeCount(int expectedCount) {
|
||||||
|
assertEquals(expectedCount, mValues.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public T getLatest() {
|
||||||
|
if (hasBeenSet()) {
|
||||||
|
return mValues.getFirst();
|
||||||
|
}
|
||||||
|
return mInitialValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A "fluent" class allows reuse of code in tests: initialization, simulation and verification
|
||||||
|
* logic.
|
||||||
|
*/
|
||||||
|
private class Script {
|
||||||
|
|
||||||
|
Script initializeTimeZoneDetectionEnabled(boolean enabled) {
|
||||||
|
mFakeTimeZoneDetectorStrategyCallback.initializeTimeZoneDetectionEnabled(enabled);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Script initializeTimeZoneSetting(String zoneId) {
|
||||||
|
mFakeTimeZoneDetectorStrategyCallback.initializeTimeZone(zoneId);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Script timeZoneDetectionEnabled(boolean enabled) {
|
||||||
|
mFakeTimeZoneDetectorStrategyCallback.setTimeZoneDetectionEnabled(enabled);
|
||||||
|
mTimeZoneDetectorStrategy.handleTimeZoneDetectionChange();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Simulates the time zone detection service receiving a phone-originated suggestion. */
|
||||||
|
Script suggestPhoneTimeZone(PhoneTimeZoneSuggestion phoneTimeZoneSuggestion) {
|
||||||
|
mTimeZoneDetectorStrategy.suggestPhoneTimeZone(phoneTimeZoneSuggestion);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Simulates the user manually setting the time zone. */
|
||||||
|
Script manuallySetTimeZone(String timeZoneId) {
|
||||||
|
// Assert the test code is correct to call this method.
|
||||||
|
assertFalse(mFakeTimeZoneDetectorStrategyCallback.isTimeZoneDetectionEnabled());
|
||||||
|
|
||||||
|
mFakeTimeZoneDetectorStrategyCallback.initializeTimeZone(timeZoneId);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Script verifyTimeZoneNotSet() {
|
||||||
|
mFakeTimeZoneDetectorStrategyCallback.assertTimeZoneNotSet();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Script verifyTimeZoneSetAndReset(PhoneTimeZoneSuggestion timeZoneSuggestion) {
|
||||||
|
mFakeTimeZoneDetectorStrategyCallback.assertTimeZoneSet(timeZoneSuggestion.getZoneId());
|
||||||
|
mFakeTimeZoneDetectorStrategyCallback.commitAllChanges();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Script resetState() {
|
||||||
|
mFakeTimeZoneDetectorStrategyCallback.commitAllChanges();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SuggestionTestCase {
|
||||||
|
public final int matchType;
|
||||||
|
public final int quality;
|
||||||
|
public final int expectedScore;
|
||||||
|
|
||||||
|
SuggestionTestCase(int matchType, int quality, int expectedScore) {
|
||||||
|
this.matchType = matchType;
|
||||||
|
this.quality = quality;
|
||||||
|
this.expectedScore = expectedScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PhoneTimeZoneSuggestion createSuggestion(int phoneId, String zoneId) {
|
||||||
|
return new PhoneTimeZoneSuggestion.Builder(phoneId)
|
||||||
|
.setZoneId(zoneId)
|
||||||
|
.setMatchType(matchType)
|
||||||
|
.setQuality(quality)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SuggestionTestCase newTestCase(
|
||||||
|
@MatchType int matchType, @Quality int quality, int expectedScore) {
|
||||||
|
return new SuggestionTestCase(matchType, quality, expectedScore);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user