Merge "Correct a permission check / add a test" am: 05cf3e896c
Change-Id: I770e101b009b86b8d94c5c6faa510baf042619b8
This commit is contained in:
@@ -37,6 +37,9 @@ import java.io.FileDescriptor;
|
|||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The implementation of ITimeDetectorService.aidl.
|
||||||
|
*/
|
||||||
public final class TimeDetectorService extends ITimeDetectorService.Stub {
|
public final class TimeDetectorService extends ITimeDetectorService.Stub {
|
||||||
private static final String TAG = "TimeDetectorService";
|
private static final String TAG = "TimeDetectorService";
|
||||||
|
|
||||||
@@ -75,7 +78,7 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub {
|
|||||||
Settings.Global.getUriFor(Settings.Global.AUTO_TIME), true,
|
Settings.Global.getUriFor(Settings.Global.AUTO_TIME), true,
|
||||||
new ContentObserver(handler) {
|
new ContentObserver(handler) {
|
||||||
public void onChange(boolean selfChange) {
|
public void onChange(boolean selfChange) {
|
||||||
timeDetectorService.handleAutoTimeDetectionToggle();
|
timeDetectorService.handleAutoTimeDetectionChanged();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -114,8 +117,9 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub {
|
|||||||
mHandler.post(() -> mTimeDetectorStrategy.suggestNetworkTime(timeSignal));
|
mHandler.post(() -> mTimeDetectorStrategy.suggestNetworkTime(timeSignal));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Internal method for handling the auto time setting being changed. */
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public void handleAutoTimeDetectionToggle() {
|
public void handleAutoTimeDetectionChanged() {
|
||||||
mHandler.post(mTimeDetectorStrategy::handleAutoTimeDetectionChanged);
|
mHandler.post(mTimeDetectorStrategy::handleAutoTimeDetectionChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ import android.os.TimestampedValue;
|
|||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The interface for classes that implement the time detection algorithm used by the
|
* The interface for the class that implements the time detection algorithm used by the
|
||||||
* TimeDetectorService.
|
* {@link TimeDetectorService}.
|
||||||
*
|
*
|
||||||
* <p>Most calls will be handled by a single thread but that is not true for all calls. For example
|
* <p>Most calls will be handled by a single thread but that is not true for all calls. For example
|
||||||
* {@link #dump(PrintWriter, String[])}) may be called on a different thread so implementations must
|
* {@link #dump(PrintWriter, String[])}) may be called on a different thread so implementations must
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import java.lang.annotation.Retention;
|
|||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of TimeDetectorStrategy that passes phone and manual suggestions to
|
* An implementation of {@link TimeDetectorStrategy} that passes phone and manual suggestions to
|
||||||
* {@link AlarmManager}. When there are multiple phone sources, the one with the lowest ID is used
|
* {@link AlarmManager}. When there are multiple phone sources, the one with the lowest ID is used
|
||||||
* unless the data becomes too stale.
|
* unless the data becomes too stale.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ import android.os.SystemProperties;
|
|||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The real implementation of {@link TimeZoneDetectorStrategy.Callback}.
|
* The real implementation of {@link TimeZoneDetectorStrategyImpl.Callback}.
|
||||||
*/
|
*/
|
||||||
public final class TimeZoneDetectorCallbackImpl implements TimeZoneDetectorStrategy.Callback {
|
public final class TimeZoneDetectorCallbackImpl implements TimeZoneDetectorStrategyImpl.Callback {
|
||||||
|
|
||||||
private static final String TIMEZONE_PROPERTY = "persist.sys.timezone";
|
private static final String TIMEZONE_PROPERTY = "persist.sys.timezone";
|
||||||
|
|
||||||
|
|||||||
@@ -67,19 +67,21 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub
|
|||||||
|
|
||||||
private static TimeZoneDetectorService create(@NonNull Context context) {
|
private static TimeZoneDetectorService create(@NonNull Context context) {
|
||||||
final TimeZoneDetectorStrategy timeZoneDetectorStrategy =
|
final TimeZoneDetectorStrategy timeZoneDetectorStrategy =
|
||||||
TimeZoneDetectorStrategy.create(context);
|
TimeZoneDetectorStrategyImpl.create(context);
|
||||||
|
|
||||||
Handler handler = FgThread.getHandler();
|
Handler handler = FgThread.getHandler();
|
||||||
|
TimeZoneDetectorService service =
|
||||||
|
new TimeZoneDetectorService(context, handler, timeZoneDetectorStrategy);
|
||||||
|
|
||||||
ContentResolver contentResolver = context.getContentResolver();
|
ContentResolver contentResolver = context.getContentResolver();
|
||||||
contentResolver.registerContentObserver(
|
contentResolver.registerContentObserver(
|
||||||
Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE), true,
|
Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE), true,
|
||||||
new ContentObserver(handler) {
|
new ContentObserver(handler) {
|
||||||
public void onChange(boolean selfChange) {
|
public void onChange(boolean selfChange) {
|
||||||
timeZoneDetectorStrategy.handleAutoTimeZoneDetectionChange();
|
service.handleAutoTimeZoneDetectionChanged();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return service;
|
||||||
return new TimeZoneDetectorService(context, handler, timeZoneDetectorStrategy);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@@ -111,17 +113,25 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub
|
|||||||
@Nullable String[] args) {
|
@Nullable String[] args) {
|
||||||
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
|
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
|
||||||
|
|
||||||
mTimeZoneDetectorStrategy.dumpState(pw, args);
|
mTimeZoneDetectorStrategy.dump(pw, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Internal method for handling the auto time zone setting being changed. */
|
||||||
|
@VisibleForTesting
|
||||||
|
public void handleAutoTimeZoneDetectionChanged() {
|
||||||
|
mHandler.post(mTimeZoneDetectorStrategy::handleAutoTimeZoneDetectionChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void enforceSuggestPhoneTimeZonePermission() {
|
private void enforceSuggestPhoneTimeZonePermission() {
|
||||||
mContext.enforceCallingPermission(
|
mContext.enforceCallingPermission(
|
||||||
android.Manifest.permission.SET_TIME_ZONE, "set time zone");
|
android.Manifest.permission.SUGGEST_PHONE_TIME_AND_ZONE,
|
||||||
|
"suggest phone time and time zone");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void enforceSuggestManualTimeZonePermission() {
|
private void enforceSuggestManualTimeZonePermission() {
|
||||||
mContext.enforceCallingOrSelfPermission(
|
mContext.enforceCallingOrSelfPermission(
|
||||||
android.Manifest.permission.SET_TIME_ZONE, "set time zone");
|
android.Manifest.permission.SUGGEST_MANUAL_TIME_AND_ZONE,
|
||||||
|
"suggest manual time and time zone");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,192 +15,26 @@
|
|||||||
*/
|
*/
|
||||||
package com.android.server.timezonedetector;
|
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.IntDef;
|
|
||||||
import android.annotation.NonNull;
|
import android.annotation.NonNull;
|
||||||
import android.annotation.Nullable;
|
|
||||||
import android.app.timezonedetector.ManualTimeZoneSuggestion;
|
import android.app.timezonedetector.ManualTimeZoneSuggestion;
|
||||||
import android.app.timezonedetector.PhoneTimeZoneSuggestion;
|
import android.app.timezonedetector.PhoneTimeZoneSuggestion;
|
||||||
import android.content.Context;
|
|
||||||
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.io.PrintWriter;
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A singleton, stateful time zone detection strategy that is aware of user (manual) suggestions and
|
* The interface for the class that implement the time detection algorithm used by the
|
||||||
* suggestions from multiple phone devices. Suggestions are acted on or ignored as needed, dependent
|
* {@link TimeZoneDetectorService}.
|
||||||
* on the current "auto time zone detection" setting.
|
|
||||||
*
|
*
|
||||||
* <p>For automatic detection it keeps track of the most recent suggestion from each phone it uses
|
* <p>Most calls will be handled by a single thread but that is not true for all calls. For example
|
||||||
* the best suggestion based on a scoring algorithm. If several phones provide the same score then
|
* {@link #dump(PrintWriter, String[])}) may be called on a different thread so implementations must
|
||||||
* the phone with the lowest numeric ID "wins". If the situation changes and it is no longer
|
* handle thread safety.
|
||||||
* possible to be confident about the time zone, phones must submit an empty suggestion in order to
|
*
|
||||||
* "withdraw" their previous suggestion.
|
* @hide
|
||||||
*/
|
*/
|
||||||
public class TimeZoneDetectorStrategy {
|
public interface TimeZoneDetectorStrategy {
|
||||||
|
|
||||||
/**
|
/** Process the suggested manually-entered (i.e. user sourced) time zone. */
|
||||||
* Used by {@link TimeZoneDetectorStrategy} to interact with the surrounding service. It can be
|
void suggestManualTimeZone(@NonNull ManualTimeZoneSuggestion suggestion);
|
||||||
* faked for tests.
|
|
||||||
*
|
|
||||||
* <p>Note: Because the system properties-derived values like
|
|
||||||
* {@link #isAutoTimeZoneDetectionEnabled()}, {@link #isAutoTimeZoneDetectionEnabled()},
|
|
||||||
* {@link #getDeviceTimeZone()} can be modified independently and from different threads (and
|
|
||||||
* processes!), their use are prone to race conditions. That will be true until the
|
|
||||||
* responsibility for setting their values is moved to {@link TimeZoneDetectorStrategy}.
|
|
||||||
*/
|
|
||||||
@VisibleForTesting
|
|
||||||
public interface Callback {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if automatic time zone detection is enabled in settings.
|
|
||||||
*/
|
|
||||||
boolean isAutoTimeZoneDetectionEnabled();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String LOG_TAG = "TimeZoneDetectorStrategy";
|
|
||||||
private static final boolean DBG = false;
|
|
||||||
|
|
||||||
@IntDef({ ORIGIN_PHONE, ORIGIN_MANUAL })
|
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
|
||||||
public @interface Origin {}
|
|
||||||
|
|
||||||
/** Used when a time value originated from a telephony signal. */
|
|
||||||
@Origin
|
|
||||||
private static final int ORIGIN_PHONE = 1;
|
|
||||||
|
|
||||||
/** Used when a time value originated from a user / manual settings. */
|
|
||||||
@Origin
|
|
||||||
private static final int ORIGIN_MANUAL = 2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The abstract score for an empty or invalid phone suggestion.
|
|
||||||
*
|
|
||||||
* Used to score phone suggestions where there is no zone.
|
|
||||||
*/
|
|
||||||
@VisibleForTesting
|
|
||||||
public static final int PHONE_SCORE_NONE = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The abstract score for a low quality phone 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 PHONE_SCORE_LOW = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The abstract score for a medium quality phone 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 PHONE_SCORE_MEDIUM = 2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The abstract score for a high quality phone 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 PHONE_SCORE_HIGH = 3;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The abstract score for a highest quality phone suggestion.
|
|
||||||
*
|
|
||||||
* Used for:
|
|
||||||
* Suggestions that must "win" because they constitute test or emulator zone ID.
|
|
||||||
*/
|
|
||||||
@VisibleForTesting
|
|
||||||
public static final int PHONE_SCORE_HIGHEST = 4;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The threshold at which phone suggestions are good enough to use to set the device's time
|
|
||||||
* zone.
|
|
||||||
*/
|
|
||||||
@VisibleForTesting
|
|
||||||
public static final int PHONE_SCORE_USAGE_THRESHOLD = PHONE_SCORE_MEDIUM;
|
|
||||||
|
|
||||||
/** The number of previous phone suggestions to keep for each ID (for use during debugging). */
|
|
||||||
private static final int KEEP_PHONE_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, false /* useLocalTimestamps */);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A mapping from slotIndex to a phone time zone suggestion. We typically expect one or two
|
|
||||||
* mappings: devices will have a small number of telephony devices and slotIndexs are assumed to
|
|
||||||
* be stable.
|
|
||||||
*/
|
|
||||||
@GuardedBy("this")
|
|
||||||
private ArrayMapWithHistory<Integer, QualifiedPhoneTimeZoneSuggestion> mSuggestionBySlotIndex =
|
|
||||||
new ArrayMapWithHistory<>(KEEP_PHONE_SUGGESTION_HISTORY_SIZE);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Process the suggested manually- / user-entered time zone. */
|
|
||||||
public synchronized void suggestManualTimeZone(@NonNull ManualTimeZoneSuggestion suggestion) {
|
|
||||||
Objects.requireNonNull(suggestion);
|
|
||||||
|
|
||||||
String timeZoneId = suggestion.getZoneId();
|
|
||||||
String cause = "Manual time suggestion received: suggestion=" + suggestion;
|
|
||||||
setDeviceTimeZoneIfRequired(ORIGIN_MANUAL, timeZoneId, cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Suggests a time zone for the device, or withdraws a previous suggestion if
|
* Suggests a time zone for the device, or withdraws a previous suggestion if
|
||||||
@@ -210,312 +44,15 @@ public class TimeZoneDetectorStrategy {
|
|||||||
* suggestion. The strategy uses suggestions to decide whether to modify the device's time zone
|
* suggestion. The strategy uses suggestions to decide whether to modify the device's time zone
|
||||||
* setting and what to set it to.
|
* setting and what to set it to.
|
||||||
*/
|
*/
|
||||||
public synchronized void suggestPhoneTimeZone(@NonNull PhoneTimeZoneSuggestion suggestion) {
|
void suggestPhoneTimeZone(@NonNull PhoneTimeZoneSuggestion suggestion);
|
||||||
if (DBG) {
|
|
||||||
Slog.d(LOG_TAG, "Phone suggestion received. newSuggestion=" + suggestion);
|
|
||||||
}
|
|
||||||
Objects.requireNonNull(suggestion);
|
|
||||||
|
|
||||||
// Score the suggestion.
|
|
||||||
int score = scorePhoneSuggestion(suggestion);
|
|
||||||
QualifiedPhoneTimeZoneSuggestion scoredSuggestion =
|
|
||||||
new QualifiedPhoneTimeZoneSuggestion(suggestion, score);
|
|
||||||
|
|
||||||
// Store the suggestion against the correct slotIndex.
|
|
||||||
mSuggestionBySlotIndex.put(suggestion.getSlotIndex(), scoredSuggestion);
|
|
||||||
|
|
||||||
// Now perform auto time zone detection. The new suggestion may be used to modify the time
|
|
||||||
// zone setting.
|
|
||||||
String reason = "New phone time suggested. suggestion=" + suggestion;
|
|
||||||
doAutoTimeZoneDetection(reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int scorePhoneSuggestion(@NonNull PhoneTimeZoneSuggestion suggestion) {
|
|
||||||
int score;
|
|
||||||
if (suggestion.getZoneId() == null) {
|
|
||||||
score = PHONE_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 = PHONE_SCORE_HIGHEST;
|
|
||||||
} else if (suggestion.getQuality() == QUALITY_SINGLE_ZONE) {
|
|
||||||
score = PHONE_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 = PHONE_SCORE_MEDIUM;
|
|
||||||
} else if (suggestion.getQuality() == QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS) {
|
|
||||||
// The suggestion has a good chance of being wrong.
|
|
||||||
score = PHONE_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 strategy becomes / remains un-opinionated and nothing is set.
|
|
||||||
*/
|
|
||||||
@GuardedBy("this")
|
|
||||||
private void doAutoTimeZoneDetection(@NonNull String detectionReason) {
|
|
||||||
if (!mCallback.isAutoTimeZoneDetectionEnabled()) {
|
|
||||||
// Avoid doing unnecessary work with this (race-prone) check.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QualifiedPhoneTimeZoneSuggestion bestPhoneSuggestion = findBestPhoneSuggestion();
|
|
||||||
|
|
||||||
// Work out what to do with the best suggestion.
|
|
||||||
if (bestPhoneSuggestion == null) {
|
|
||||||
// There is no phone suggestion available at all. Become un-opinionated.
|
|
||||||
if (DBG) {
|
|
||||||
Slog.d(LOG_TAG, "Could not determine time zone: No best phone suggestion."
|
|
||||||
+ " detectionReason=" + detectionReason);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case handling for uninitialized devices. This should only happen once.
|
|
||||||
String newZoneId = bestPhoneSuggestion.suggestion.getZoneId();
|
|
||||||
if (newZoneId != null && !mCallback.isDeviceTimeZoneInitialized()) {
|
|
||||||
String cause = "Device has no time zone set. Attempting to set the device to the best"
|
|
||||||
+ " available suggestion."
|
|
||||||
+ " bestPhoneSuggestion=" + bestPhoneSuggestion
|
|
||||||
+ ", detectionReason=" + detectionReason;
|
|
||||||
Slog.i(LOG_TAG, cause);
|
|
||||||
setDeviceTimeZoneIfRequired(ORIGIN_PHONE, newZoneId, cause);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean suggestionGoodEnough = bestPhoneSuggestion.score >= PHONE_SCORE_USAGE_THRESHOLD;
|
|
||||||
if (!suggestionGoodEnough) {
|
|
||||||
if (DBG) {
|
|
||||||
Slog.d(LOG_TAG, "Best suggestion not good enough."
|
|
||||||
+ " bestPhoneSuggestion=" + bestPhoneSuggestion
|
|
||||||
+ ", detectionReason=" + detectionReason);
|
|
||||||
}
|
|
||||||
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:"
|
|
||||||
+ " bestPhoneSuggestion=" + bestPhoneSuggestion
|
|
||||||
+ " detectionReason=" + detectionReason);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String zoneId = bestPhoneSuggestion.suggestion.getZoneId();
|
|
||||||
String cause = "Found good suggestion."
|
|
||||||
+ ", bestPhoneSuggestion=" + bestPhoneSuggestion
|
|
||||||
+ ", detectionReason=" + detectionReason;
|
|
||||||
setDeviceTimeZoneIfRequired(ORIGIN_PHONE, zoneId, cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GuardedBy("this")
|
|
||||||
private void setDeviceTimeZoneIfRequired(
|
|
||||||
@Origin int origin, @NonNull String newZoneId, @NonNull String cause) {
|
|
||||||
Objects.requireNonNull(newZoneId);
|
|
||||||
Objects.requireNonNull(cause);
|
|
||||||
|
|
||||||
boolean isOriginAutomatic = isOriginAutomatic(origin);
|
|
||||||
if (isOriginAutomatic) {
|
|
||||||
if (!mCallback.isAutoTimeZoneDetectionEnabled()) {
|
|
||||||
if (DBG) {
|
|
||||||
Slog.d(LOG_TAG, "Auto time zone detection is not enabled."
|
|
||||||
+ " origin=" + origin
|
|
||||||
+ ", newZoneId=" + newZoneId
|
|
||||||
+ ", cause=" + cause);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (mCallback.isAutoTimeZoneDetectionEnabled()) {
|
|
||||||
if (DBG) {
|
|
||||||
Slog.d(LOG_TAG, "Auto time zone detection is enabled."
|
|
||||||
+ " origin=" + origin
|
|
||||||
+ ", newZoneId=" + newZoneId
|
|
||||||
+ ", cause=" + cause);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String currentZoneId = mCallback.getDeviceTimeZone();
|
|
||||||
|
|
||||||
// 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, "No need to change the time zone;"
|
|
||||||
+ " device is already set to the suggested zone."
|
|
||||||
+ " origin=" + origin
|
|
||||||
+ ", newZoneId=" + newZoneId
|
|
||||||
+ ", cause=" + cause);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mCallback.setDeviceTimeZone(newZoneId);
|
|
||||||
String msg = "Set device time zone."
|
|
||||||
+ " origin=" + origin
|
|
||||||
+ ", currentZoneId=" + currentZoneId
|
|
||||||
+ ", newZoneId=" + newZoneId
|
|
||||||
+ ", cause=" + cause;
|
|
||||||
if (DBG) {
|
|
||||||
Slog.d(LOG_TAG, msg);
|
|
||||||
}
|
|
||||||
mTimeZoneChangesLog.log(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isOriginAutomatic(@Origin int origin) {
|
|
||||||
return origin != ORIGIN_MANUAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GuardedBy("this")
|
|
||||||
@Nullable
|
|
||||||
private QualifiedPhoneTimeZoneSuggestion findBestPhoneSuggestion() {
|
|
||||||
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 < mSuggestionBySlotIndex.size(); i++) {
|
|
||||||
QualifiedPhoneTimeZoneSuggestion candidateSuggestion =
|
|
||||||
mSuggestionBySlotIndex.valueAt(i);
|
|
||||||
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 slotIndex.
|
|
||||||
int candidateSlotIndex = candidateSuggestion.suggestion.getSlotIndex();
|
|
||||||
int bestSlotIndex = bestSuggestion.suggestion.getSlotIndex();
|
|
||||||
if (candidateSlotIndex < bestSlotIndex) {
|
|
||||||
bestSuggestion = candidateSuggestion;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bestSuggestion;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current best phone suggestion. Not intended for general use: it is used during
|
|
||||||
* tests to check strategy behavior.
|
|
||||||
*/
|
|
||||||
@VisibleForTesting
|
|
||||||
@Nullable
|
|
||||||
public synchronized QualifiedPhoneTimeZoneSuggestion findBestPhoneSuggestionForTests() {
|
|
||||||
return findBestPhoneSuggestion();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when there has been a change to the automatic time zone detection setting.
|
* Called when there has been a change to the automatic time zone detection setting.
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
void handleAutoTimeZoneDetectionChanged();
|
||||||
public synchronized void handleAutoTimeZoneDetectionChange() {
|
|
||||||
if (DBG) {
|
|
||||||
Slog.d(LOG_TAG, "handleTimeZoneDetectionChange() called");
|
|
||||||
}
|
|
||||||
if (mCallback.isAutoTimeZoneDetectionEnabled()) {
|
|
||||||
// When the user enabled time zone detection, run the time zone detection and change the
|
|
||||||
// device time zone if possible.
|
|
||||||
String reason = "Auto time zone detection setting enabled.";
|
|
||||||
doAutoTimeZoneDetection(reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dumps internal state such as field values.
|
* Dumps internal state such as field values.
|
||||||
*/
|
*/
|
||||||
public synchronized void dumpState(PrintWriter pw, String[] args) {
|
void dump(PrintWriter pw, String[] args);
|
||||||
IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
|
|
||||||
ipw.println("TimeZoneDetectorStrategy:");
|
|
||||||
|
|
||||||
ipw.increaseIndent(); // level 1
|
|
||||||
ipw.println("mCallback.isTimeZoneDetectionEnabled()="
|
|
||||||
+ mCallback.isAutoTimeZoneDetectionEnabled());
|
|
||||||
ipw.println("mCallback.isDeviceTimeZoneInitialized()="
|
|
||||||
+ mCallback.isDeviceTimeZoneInitialized());
|
|
||||||
ipw.println("mCallback.getDeviceTimeZone()="
|
|
||||||
+ mCallback.getDeviceTimeZone());
|
|
||||||
|
|
||||||
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
|
|
||||||
mSuggestionBySlotIndex.dump(ipw);
|
|
||||||
ipw.decreaseIndent(); // level 2
|
|
||||||
ipw.decreaseIndent(); // level 1
|
|
||||||
ipw.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A method used to inspect strategy state during tests. Not intended for general use.
|
|
||||||
*/
|
|
||||||
@VisibleForTesting
|
|
||||||
public synchronized QualifiedPhoneTimeZoneSuggestion getLatestPhoneSuggestion(int slotIndex) {
|
|
||||||
return mSuggestionBySlotIndex.get(slotIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
+ '}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,514 @@
|
|||||||
|
/*
|
||||||
|
* 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.IntDef;
|
||||||
|
import android.annotation.NonNull;
|
||||||
|
import android.annotation.Nullable;
|
||||||
|
import android.app.timezonedetector.ManualTimeZoneSuggestion;
|
||||||
|
import android.app.timezonedetector.PhoneTimeZoneSuggestion;
|
||||||
|
import android.content.Context;
|
||||||
|
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.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of {@link TimeZoneDetectorStrategy} that handle telephony and manual
|
||||||
|
* suggestions. Suggestions are acted on or ignored as needed, dependent on the current "auto time
|
||||||
|
* zone detection" setting.
|
||||||
|
*
|
||||||
|
* <p>For automatic detection it keeps track of the most recent suggestion from each phone it uses
|
||||||
|
* the best suggestion 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.
|
||||||
|
*
|
||||||
|
* <p>Most public methods are marked synchronized to ensure thread safety around internal state.
|
||||||
|
*/
|
||||||
|
public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrategy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by {@link TimeZoneDetectorStrategyImpl} to interact with the surrounding service. It can
|
||||||
|
* be faked for tests.
|
||||||
|
*
|
||||||
|
* <p>Note: Because the system properties-derived values like
|
||||||
|
* {@link #isAutoTimeZoneDetectionEnabled()}, {@link #isAutoTimeZoneDetectionEnabled()},
|
||||||
|
* {@link #getDeviceTimeZone()} can be modified independently and from different threads (and
|
||||||
|
* processes!), their use are prone to race conditions. That will be true until the
|
||||||
|
* responsibility for setting their values is moved to {@link TimeZoneDetectorStrategyImpl}.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
public interface Callback {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if automatic time zone detection is enabled in settings.
|
||||||
|
*/
|
||||||
|
boolean isAutoTimeZoneDetectionEnabled();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TimeZoneDetectorStrategy";
|
||||||
|
private static final boolean DBG = false;
|
||||||
|
|
||||||
|
@IntDef({ ORIGIN_PHONE, ORIGIN_MANUAL })
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
public @interface Origin {}
|
||||||
|
|
||||||
|
/** Used when a time value originated from a telephony signal. */
|
||||||
|
@Origin
|
||||||
|
private static final int ORIGIN_PHONE = 1;
|
||||||
|
|
||||||
|
/** Used when a time value originated from a user / manual settings. */
|
||||||
|
@Origin
|
||||||
|
private static final int ORIGIN_MANUAL = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The abstract score for an empty or invalid phone suggestion.
|
||||||
|
*
|
||||||
|
* Used to score phone suggestions where there is no zone.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
public static final int PHONE_SCORE_NONE = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The abstract score for a low quality phone 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 PHONE_SCORE_LOW = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The abstract score for a medium quality phone 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 PHONE_SCORE_MEDIUM = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The abstract score for a high quality phone 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 PHONE_SCORE_HIGH = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The abstract score for a highest quality phone suggestion.
|
||||||
|
*
|
||||||
|
* Used for:
|
||||||
|
* Suggestions that must "win" because they constitute test or emulator zone ID.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
public static final int PHONE_SCORE_HIGHEST = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The threshold at which phone suggestions are good enough to use to set the device's time
|
||||||
|
* zone.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
public static final int PHONE_SCORE_USAGE_THRESHOLD = PHONE_SCORE_MEDIUM;
|
||||||
|
|
||||||
|
/** The number of previous phone suggestions to keep for each ID (for use during debugging). */
|
||||||
|
private static final int KEEP_PHONE_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, false /* useLocalTimestamps */);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mapping from slotIndex to a phone time zone suggestion. We typically expect one or two
|
||||||
|
* mappings: devices will have a small number of telephony devices and slotIndexs are assumed to
|
||||||
|
* be stable.
|
||||||
|
*/
|
||||||
|
@GuardedBy("this")
|
||||||
|
private ArrayMapWithHistory<Integer, QualifiedPhoneTimeZoneSuggestion> mSuggestionBySlotIndex =
|
||||||
|
new ArrayMapWithHistory<>(KEEP_PHONE_SUGGESTION_HISTORY_SIZE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of {@link TimeZoneDetectorStrategyImpl}.
|
||||||
|
*/
|
||||||
|
public static TimeZoneDetectorStrategyImpl create(Context context) {
|
||||||
|
Callback timeZoneDetectionServiceHelper = new TimeZoneDetectorCallbackImpl(context);
|
||||||
|
return new TimeZoneDetectorStrategyImpl(timeZoneDetectionServiceHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public TimeZoneDetectorStrategyImpl(Callback callback) {
|
||||||
|
mCallback = Objects.requireNonNull(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void suggestManualTimeZone(@NonNull ManualTimeZoneSuggestion suggestion) {
|
||||||
|
Objects.requireNonNull(suggestion);
|
||||||
|
|
||||||
|
String timeZoneId = suggestion.getZoneId();
|
||||||
|
String cause = "Manual time suggestion received: suggestion=" + suggestion;
|
||||||
|
setDeviceTimeZoneIfRequired(ORIGIN_MANUAL, timeZoneId, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void suggestPhoneTimeZone(@NonNull PhoneTimeZoneSuggestion suggestion) {
|
||||||
|
if (DBG) {
|
||||||
|
Slog.d(LOG_TAG, "Phone suggestion received. newSuggestion=" + suggestion);
|
||||||
|
}
|
||||||
|
Objects.requireNonNull(suggestion);
|
||||||
|
|
||||||
|
// Score the suggestion.
|
||||||
|
int score = scorePhoneSuggestion(suggestion);
|
||||||
|
QualifiedPhoneTimeZoneSuggestion scoredSuggestion =
|
||||||
|
new QualifiedPhoneTimeZoneSuggestion(suggestion, score);
|
||||||
|
|
||||||
|
// Store the suggestion against the correct slotIndex.
|
||||||
|
mSuggestionBySlotIndex.put(suggestion.getSlotIndex(), scoredSuggestion);
|
||||||
|
|
||||||
|
// Now perform auto time zone detection. The new suggestion may be used to modify the time
|
||||||
|
// zone setting.
|
||||||
|
String reason = "New phone time suggested. suggestion=" + suggestion;
|
||||||
|
doAutoTimeZoneDetection(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int scorePhoneSuggestion(@NonNull PhoneTimeZoneSuggestion suggestion) {
|
||||||
|
int score;
|
||||||
|
if (suggestion.getZoneId() == null) {
|
||||||
|
score = PHONE_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 = PHONE_SCORE_HIGHEST;
|
||||||
|
} else if (suggestion.getQuality() == QUALITY_SINGLE_ZONE) {
|
||||||
|
score = PHONE_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 = PHONE_SCORE_MEDIUM;
|
||||||
|
} else if (suggestion.getQuality() == QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS) {
|
||||||
|
// The suggestion has a good chance of being wrong.
|
||||||
|
score = PHONE_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 strategy becomes / remains un-opinionated and nothing is set.
|
||||||
|
*/
|
||||||
|
@GuardedBy("this")
|
||||||
|
private void doAutoTimeZoneDetection(@NonNull String detectionReason) {
|
||||||
|
if (!mCallback.isAutoTimeZoneDetectionEnabled()) {
|
||||||
|
// Avoid doing unnecessary work with this (race-prone) check.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QualifiedPhoneTimeZoneSuggestion bestPhoneSuggestion = findBestPhoneSuggestion();
|
||||||
|
|
||||||
|
// Work out what to do with the best suggestion.
|
||||||
|
if (bestPhoneSuggestion == null) {
|
||||||
|
// There is no phone suggestion available at all. Become un-opinionated.
|
||||||
|
if (DBG) {
|
||||||
|
Slog.d(LOG_TAG, "Could not determine time zone: No best phone suggestion."
|
||||||
|
+ " detectionReason=" + detectionReason);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case handling for uninitialized devices. This should only happen once.
|
||||||
|
String newZoneId = bestPhoneSuggestion.suggestion.getZoneId();
|
||||||
|
if (newZoneId != null && !mCallback.isDeviceTimeZoneInitialized()) {
|
||||||
|
String cause = "Device has no time zone set. Attempting to set the device to the best"
|
||||||
|
+ " available suggestion."
|
||||||
|
+ " bestPhoneSuggestion=" + bestPhoneSuggestion
|
||||||
|
+ ", detectionReason=" + detectionReason;
|
||||||
|
Slog.i(LOG_TAG, cause);
|
||||||
|
setDeviceTimeZoneIfRequired(ORIGIN_PHONE, newZoneId, cause);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean suggestionGoodEnough = bestPhoneSuggestion.score >= PHONE_SCORE_USAGE_THRESHOLD;
|
||||||
|
if (!suggestionGoodEnough) {
|
||||||
|
if (DBG) {
|
||||||
|
Slog.d(LOG_TAG, "Best suggestion not good enough."
|
||||||
|
+ " bestPhoneSuggestion=" + bestPhoneSuggestion
|
||||||
|
+ ", detectionReason=" + detectionReason);
|
||||||
|
}
|
||||||
|
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:"
|
||||||
|
+ " bestPhoneSuggestion=" + bestPhoneSuggestion
|
||||||
|
+ " detectionReason=" + detectionReason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String zoneId = bestPhoneSuggestion.suggestion.getZoneId();
|
||||||
|
String cause = "Found good suggestion."
|
||||||
|
+ ", bestPhoneSuggestion=" + bestPhoneSuggestion
|
||||||
|
+ ", detectionReason=" + detectionReason;
|
||||||
|
setDeviceTimeZoneIfRequired(ORIGIN_PHONE, zoneId, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GuardedBy("this")
|
||||||
|
private void setDeviceTimeZoneIfRequired(
|
||||||
|
@Origin int origin, @NonNull String newZoneId, @NonNull String cause) {
|
||||||
|
Objects.requireNonNull(newZoneId);
|
||||||
|
Objects.requireNonNull(cause);
|
||||||
|
|
||||||
|
boolean isOriginAutomatic = isOriginAutomatic(origin);
|
||||||
|
if (isOriginAutomatic) {
|
||||||
|
if (!mCallback.isAutoTimeZoneDetectionEnabled()) {
|
||||||
|
if (DBG) {
|
||||||
|
Slog.d(LOG_TAG, "Auto time zone detection is not enabled."
|
||||||
|
+ " origin=" + origin
|
||||||
|
+ ", newZoneId=" + newZoneId
|
||||||
|
+ ", cause=" + cause);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mCallback.isAutoTimeZoneDetectionEnabled()) {
|
||||||
|
if (DBG) {
|
||||||
|
Slog.d(LOG_TAG, "Auto time zone detection is enabled."
|
||||||
|
+ " origin=" + origin
|
||||||
|
+ ", newZoneId=" + newZoneId
|
||||||
|
+ ", cause=" + cause);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String currentZoneId = mCallback.getDeviceTimeZone();
|
||||||
|
|
||||||
|
// 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, "No need to change the time zone;"
|
||||||
|
+ " device is already set to the suggested zone."
|
||||||
|
+ " origin=" + origin
|
||||||
|
+ ", newZoneId=" + newZoneId
|
||||||
|
+ ", cause=" + cause);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mCallback.setDeviceTimeZone(newZoneId);
|
||||||
|
String msg = "Set device time zone."
|
||||||
|
+ " origin=" + origin
|
||||||
|
+ ", currentZoneId=" + currentZoneId
|
||||||
|
+ ", newZoneId=" + newZoneId
|
||||||
|
+ ", cause=" + cause;
|
||||||
|
if (DBG) {
|
||||||
|
Slog.d(LOG_TAG, msg);
|
||||||
|
}
|
||||||
|
mTimeZoneChangesLog.log(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isOriginAutomatic(@Origin int origin) {
|
||||||
|
return origin != ORIGIN_MANUAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GuardedBy("this")
|
||||||
|
@Nullable
|
||||||
|
private QualifiedPhoneTimeZoneSuggestion findBestPhoneSuggestion() {
|
||||||
|
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 < mSuggestionBySlotIndex.size(); i++) {
|
||||||
|
QualifiedPhoneTimeZoneSuggestion candidateSuggestion =
|
||||||
|
mSuggestionBySlotIndex.valueAt(i);
|
||||||
|
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 slotIndex.
|
||||||
|
int candidateSlotIndex = candidateSuggestion.suggestion.getSlotIndex();
|
||||||
|
int bestSlotIndex = bestSuggestion.suggestion.getSlotIndex();
|
||||||
|
if (candidateSlotIndex < bestSlotIndex) {
|
||||||
|
bestSuggestion = candidateSuggestion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestSuggestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current best phone suggestion. Not intended for general use: it is used during
|
||||||
|
* tests to check strategy behavior.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
@Nullable
|
||||||
|
public synchronized QualifiedPhoneTimeZoneSuggestion findBestPhoneSuggestionForTests() {
|
||||||
|
return findBestPhoneSuggestion();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void handleAutoTimeZoneDetectionChanged() {
|
||||||
|
if (DBG) {
|
||||||
|
Slog.d(LOG_TAG, "handleTimeZoneDetectionChange() called");
|
||||||
|
}
|
||||||
|
if (mCallback.isAutoTimeZoneDetectionEnabled()) {
|
||||||
|
// When the user enabled time zone detection, run the time zone detection and change the
|
||||||
|
// device time zone if possible.
|
||||||
|
String reason = "Auto time zone detection setting enabled.";
|
||||||
|
doAutoTimeZoneDetection(reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dumps internal state such as field values.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized void dump(PrintWriter pw, String[] args) {
|
||||||
|
IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
|
||||||
|
ipw.println("TimeZoneDetectorStrategy:");
|
||||||
|
|
||||||
|
ipw.increaseIndent(); // level 1
|
||||||
|
ipw.println("mCallback.isTimeZoneDetectionEnabled()="
|
||||||
|
+ mCallback.isAutoTimeZoneDetectionEnabled());
|
||||||
|
ipw.println("mCallback.isDeviceTimeZoneInitialized()="
|
||||||
|
+ mCallback.isDeviceTimeZoneInitialized());
|
||||||
|
ipw.println("mCallback.getDeviceTimeZone()="
|
||||||
|
+ mCallback.getDeviceTimeZone());
|
||||||
|
|
||||||
|
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
|
||||||
|
mSuggestionBySlotIndex.dump(ipw);
|
||||||
|
ipw.decreaseIndent(); // level 2
|
||||||
|
ipw.decreaseIndent(); // level 1
|
||||||
|
ipw.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A method used to inspect strategy state during tests. Not intended for general use.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
public synchronized QualifiedPhoneTimeZoneSuggestion getLatestPhoneSuggestion(int slotIndex) {
|
||||||
|
return mSuggestionBySlotIndex.get(slotIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,14 +33,13 @@ import android.app.timedetector.NetworkTimeSuggestion;
|
|||||||
import android.app.timedetector.PhoneTimeSuggestion;
|
import android.app.timedetector.PhoneTimeSuggestion;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.HandlerThread;
|
import android.os.HandlerThread;
|
||||||
import android.os.Looper;
|
|
||||||
import android.os.Message;
|
|
||||||
import android.os.TimestampedValue;
|
import android.os.TimestampedValue;
|
||||||
|
|
||||||
import androidx.test.runner.AndroidJUnit4;
|
import androidx.test.runner.AndroidJUnit4;
|
||||||
|
|
||||||
|
import com.android.server.timezonedetector.TestHandler;
|
||||||
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -108,7 +107,7 @@ public class TimeDetectorServiceTest {
|
|||||||
eq(android.Manifest.permission.SUGGEST_PHONE_TIME_AND_ZONE),
|
eq(android.Manifest.permission.SUGGEST_PHONE_TIME_AND_ZONE),
|
||||||
anyString());
|
anyString());
|
||||||
|
|
||||||
mTestHandler.waitForEmptyQueue();
|
mTestHandler.waitForMessagesToBeProcessed();
|
||||||
mStubbedTimeDetectorStrategy.verifySuggestPhoneTimeCalled(phoneTimeSuggestion);
|
mStubbedTimeDetectorStrategy.verifySuggestPhoneTimeCalled(phoneTimeSuggestion);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +139,7 @@ public class TimeDetectorServiceTest {
|
|||||||
eq(android.Manifest.permission.SUGGEST_MANUAL_TIME_AND_ZONE),
|
eq(android.Manifest.permission.SUGGEST_MANUAL_TIME_AND_ZONE),
|
||||||
anyString());
|
anyString());
|
||||||
|
|
||||||
mTestHandler.waitForEmptyQueue();
|
mTestHandler.waitForMessagesToBeProcessed();
|
||||||
mStubbedTimeDetectorStrategy.verifySuggestManualTimeCalled(manualTimeSuggestion);
|
mStubbedTimeDetectorStrategy.verifySuggestManualTimeCalled(manualTimeSuggestion);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +169,7 @@ public class TimeDetectorServiceTest {
|
|||||||
verify(mMockContext).enforceCallingOrSelfPermission(
|
verify(mMockContext).enforceCallingOrSelfPermission(
|
||||||
eq(android.Manifest.permission.SET_TIME), anyString());
|
eq(android.Manifest.permission.SET_TIME), anyString());
|
||||||
|
|
||||||
mTestHandler.waitForEmptyQueue();
|
mTestHandler.waitForMessagesToBeProcessed();
|
||||||
mStubbedTimeDetectorStrategy.verifySuggestNetworkTimeCalled(NetworkTimeSuggestion);
|
mStubbedTimeDetectorStrategy.verifySuggestNetworkTimeCalled(NetworkTimeSuggestion);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,21 +186,23 @@ public class TimeDetectorServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAutoTimeDetectionToggle() throws Exception {
|
public void testAutoTimeDetectionToggle() throws Exception {
|
||||||
mTimeDetectorService.handleAutoTimeDetectionToggle();
|
mTimeDetectorService.handleAutoTimeDetectionChanged();
|
||||||
mTestHandler.assertTotalMessagesEnqueued(1);
|
mTestHandler.assertTotalMessagesEnqueued(1);
|
||||||
mTestHandler.waitForEmptyQueue();
|
mTestHandler.waitForMessagesToBeProcessed();
|
||||||
mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionToggleCalled();
|
mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionChangedCalled();
|
||||||
|
|
||||||
mTimeDetectorService.handleAutoTimeDetectionToggle();
|
mStubbedTimeDetectorStrategy.resetCallTracking();
|
||||||
|
|
||||||
|
mTimeDetectorService.handleAutoTimeDetectionChanged();
|
||||||
mTestHandler.assertTotalMessagesEnqueued(2);
|
mTestHandler.assertTotalMessagesEnqueued(2);
|
||||||
mTestHandler.waitForEmptyQueue();
|
mTestHandler.waitForMessagesToBeProcessed();
|
||||||
mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionToggleCalled();
|
mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionChangedCalled();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PhoneTimeSuggestion createPhoneTimeSuggestion() {
|
private static PhoneTimeSuggestion createPhoneTimeSuggestion() {
|
||||||
int phoneId = 1234;
|
int slotIndex = 1234;
|
||||||
TimestampedValue<Long> timeValue = new TimestampedValue<>(100L, 1_000_000L);
|
TimestampedValue<Long> timeValue = new TimestampedValue<>(100L, 1_000_000L);
|
||||||
return new PhoneTimeSuggestion.Builder(phoneId)
|
return new PhoneTimeSuggestion.Builder(slotIndex)
|
||||||
.setUtcTime(timeValue)
|
.setUtcTime(timeValue)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
@@ -222,7 +223,7 @@ public class TimeDetectorServiceTest {
|
|||||||
private PhoneTimeSuggestion mLastPhoneSuggestion;
|
private PhoneTimeSuggestion mLastPhoneSuggestion;
|
||||||
private ManualTimeSuggestion mLastManualSuggestion;
|
private ManualTimeSuggestion mLastManualSuggestion;
|
||||||
private NetworkTimeSuggestion mLastNetworkSuggestion;
|
private NetworkTimeSuggestion mLastNetworkSuggestion;
|
||||||
private boolean mLastAutoTimeDetectionToggleCalled;
|
private boolean mHandleAutoTimeDetectionChangedCalled;
|
||||||
private boolean mDumpCalled;
|
private boolean mDumpCalled;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -231,31 +232,26 @@ public class TimeDetectorServiceTest {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void suggestPhoneTime(PhoneTimeSuggestion timeSuggestion) {
|
public void suggestPhoneTime(PhoneTimeSuggestion timeSuggestion) {
|
||||||
resetCallTracking();
|
|
||||||
mLastPhoneSuggestion = timeSuggestion;
|
mLastPhoneSuggestion = timeSuggestion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void suggestManualTime(ManualTimeSuggestion timeSuggestion) {
|
public void suggestManualTime(ManualTimeSuggestion timeSuggestion) {
|
||||||
resetCallTracking();
|
|
||||||
mLastManualSuggestion = timeSuggestion;
|
mLastManualSuggestion = timeSuggestion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void suggestNetworkTime(NetworkTimeSuggestion timeSuggestion) {
|
public void suggestNetworkTime(NetworkTimeSuggestion timeSuggestion) {
|
||||||
resetCallTracking();
|
|
||||||
mLastNetworkSuggestion = timeSuggestion;
|
mLastNetworkSuggestion = timeSuggestion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleAutoTimeDetectionChanged() {
|
public void handleAutoTimeDetectionChanged() {
|
||||||
resetCallTracking();
|
mHandleAutoTimeDetectionChangedCalled = true;
|
||||||
mLastAutoTimeDetectionToggleCalled = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dump(PrintWriter pw, String[] args) {
|
public void dump(PrintWriter pw, String[] args) {
|
||||||
resetCallTracking();
|
|
||||||
mDumpCalled = true;
|
mDumpCalled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +259,7 @@ public class TimeDetectorServiceTest {
|
|||||||
mLastPhoneSuggestion = null;
|
mLastPhoneSuggestion = null;
|
||||||
mLastManualSuggestion = null;
|
mLastManualSuggestion = null;
|
||||||
mLastNetworkSuggestion = null;
|
mLastNetworkSuggestion = null;
|
||||||
mLastAutoTimeDetectionToggleCalled = false;
|
mHandleAutoTimeDetectionChangedCalled = false;
|
||||||
mDumpCalled = false;
|
mDumpCalled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,45 +275,12 @@ public class TimeDetectorServiceTest {
|
|||||||
assertEquals(expectedSuggestion, mLastNetworkSuggestion);
|
assertEquals(expectedSuggestion, mLastNetworkSuggestion);
|
||||||
}
|
}
|
||||||
|
|
||||||
void verifyHandleAutoTimeDetectionToggleCalled() {
|
void verifyHandleAutoTimeDetectionChangedCalled() {
|
||||||
assertTrue(mLastAutoTimeDetectionToggleCalled);
|
assertTrue(mHandleAutoTimeDetectionChangedCalled);
|
||||||
}
|
}
|
||||||
|
|
||||||
void verifyDumpCalled() {
|
void verifyDumpCalled() {
|
||||||
assertTrue(mDumpCalled);
|
assertTrue(mDumpCalled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A Handler that can track posts/sends and wait for work to be completed.
|
|
||||||
*/
|
|
||||||
private static class TestHandler extends Handler {
|
|
||||||
|
|
||||||
private int mMessagesSent;
|
|
||||||
|
|
||||||
TestHandler(Looper looper) {
|
|
||||||
super(looper);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
|
|
||||||
mMessagesSent++;
|
|
||||||
return super.sendMessageAtTime(msg, uptimeMillis);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Asserts the number of messages posted or sent is as expected. */
|
|
||||||
void assertTotalMessagesEnqueued(int expected) {
|
|
||||||
assertEquals(expected, mMessagesSent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Waits for all currently enqueued work due to be processed to be completed before
|
|
||||||
* returning.
|
|
||||||
*/
|
|
||||||
void waitForEmptyQueue() throws InterruptedException {
|
|
||||||
while (!getLooper().getQueue().isIdle()) {
|
|
||||||
Thread.sleep(100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.android.server.timezonedetector;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.Message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Handler that can track posts/sends and wait for them to be completed.
|
||||||
|
*/
|
||||||
|
public class TestHandler extends Handler {
|
||||||
|
|
||||||
|
private final Object mMonitor = new Object();
|
||||||
|
private int mMessagesProcessed = 0;
|
||||||
|
private int mMessagesSent = 0;
|
||||||
|
|
||||||
|
public TestHandler(Looper looper) {
|
||||||
|
super(looper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
|
||||||
|
synchronized (mMonitor) {
|
||||||
|
mMessagesSent++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Runnable callback = msg.getCallback();
|
||||||
|
// Have the callback increment the mMessagesProcessed when it is done. It will notify
|
||||||
|
// any threads waiting for all messages to be processed if appropriate.
|
||||||
|
Runnable newCallback = () -> {
|
||||||
|
callback.run();
|
||||||
|
synchronized (mMonitor) {
|
||||||
|
mMessagesProcessed++;
|
||||||
|
if (mMessagesSent == mMessagesProcessed) {
|
||||||
|
mMonitor.notifyAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
msg.setCallback(newCallback);
|
||||||
|
return super.sendMessageAtTime(msg, uptimeMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Asserts the number of messages posted or sent is as expected. */
|
||||||
|
public void assertTotalMessagesEnqueued(int expected) {
|
||||||
|
synchronized (mMonitor) {
|
||||||
|
assertEquals(expected, mMessagesSent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for all enqueued work to be completed before returning.
|
||||||
|
*/
|
||||||
|
public void waitForMessagesToBeProcessed() throws InterruptedException {
|
||||||
|
synchronized (mMonitor) {
|
||||||
|
if (mMessagesSent != mMessagesProcessed) {
|
||||||
|
mMonitor.wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.server.timezonedetector;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.doNothing;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import android.app.timezonedetector.ManualTimeZoneSuggestion;
|
||||||
|
import android.app.timezonedetector.PhoneTimeZoneSuggestion;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.HandlerThread;
|
||||||
|
|
||||||
|
import androidx.test.runner.AndroidJUnit4;
|
||||||
|
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class TimeZoneDetectorServiceTest {
|
||||||
|
|
||||||
|
private Context mMockContext;
|
||||||
|
private StubbedTimeZoneDetectorStrategy mStubbedTimeZoneDetectorStrategy;
|
||||||
|
|
||||||
|
private TimeZoneDetectorService mTimeZoneDetectorService;
|
||||||
|
private HandlerThread mHandlerThread;
|
||||||
|
private TestHandler mTestHandler;
|
||||||
|
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
mMockContext = mock(Context.class);
|
||||||
|
|
||||||
|
// Create a thread + handler for processing the work that the service posts.
|
||||||
|
mHandlerThread = new HandlerThread("TimeZoneDetectorServiceTest");
|
||||||
|
mHandlerThread.start();
|
||||||
|
mTestHandler = new TestHandler(mHandlerThread.getLooper());
|
||||||
|
|
||||||
|
mStubbedTimeZoneDetectorStrategy = new StubbedTimeZoneDetectorStrategy();
|
||||||
|
|
||||||
|
mTimeZoneDetectorService = new TimeZoneDetectorService(
|
||||||
|
mMockContext, mTestHandler, mStubbedTimeZoneDetectorStrategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() throws Exception {
|
||||||
|
mHandlerThread.quit();
|
||||||
|
mHandlerThread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = SecurityException.class)
|
||||||
|
public void testSuggestPhoneTime_withoutPermission() {
|
||||||
|
doThrow(new SecurityException("Mock"))
|
||||||
|
.when(mMockContext).enforceCallingPermission(anyString(), any());
|
||||||
|
PhoneTimeZoneSuggestion timeZoneSuggestion = createPhoneTimeZoneSuggestion();
|
||||||
|
|
||||||
|
try {
|
||||||
|
mTimeZoneDetectorService.suggestPhoneTimeZone(timeZoneSuggestion);
|
||||||
|
fail();
|
||||||
|
} finally {
|
||||||
|
verify(mMockContext).enforceCallingPermission(
|
||||||
|
eq(android.Manifest.permission.SUGGEST_PHONE_TIME_AND_ZONE),
|
||||||
|
anyString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuggestPhoneTimeZone() throws Exception {
|
||||||
|
doNothing().when(mMockContext).enforceCallingPermission(anyString(), any());
|
||||||
|
|
||||||
|
PhoneTimeZoneSuggestion timeZoneSuggestion = createPhoneTimeZoneSuggestion();
|
||||||
|
mTimeZoneDetectorService.suggestPhoneTimeZone(timeZoneSuggestion);
|
||||||
|
mTestHandler.assertTotalMessagesEnqueued(1);
|
||||||
|
|
||||||
|
verify(mMockContext).enforceCallingPermission(
|
||||||
|
eq(android.Manifest.permission.SUGGEST_PHONE_TIME_AND_ZONE),
|
||||||
|
anyString());
|
||||||
|
|
||||||
|
mTestHandler.waitForMessagesToBeProcessed();
|
||||||
|
mStubbedTimeZoneDetectorStrategy.verifySuggestPhoneTimeZoneCalled(timeZoneSuggestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = SecurityException.class)
|
||||||
|
public void testSuggestManualTime_withoutPermission() {
|
||||||
|
doThrow(new SecurityException("Mock"))
|
||||||
|
.when(mMockContext).enforceCallingOrSelfPermission(anyString(), any());
|
||||||
|
ManualTimeZoneSuggestion timeZoneSuggestion = createManualTimeZoneSuggestion();
|
||||||
|
|
||||||
|
try {
|
||||||
|
mTimeZoneDetectorService.suggestManualTimeZone(timeZoneSuggestion);
|
||||||
|
fail();
|
||||||
|
} finally {
|
||||||
|
verify(mMockContext).enforceCallingOrSelfPermission(
|
||||||
|
eq(android.Manifest.permission.SUGGEST_MANUAL_TIME_AND_ZONE),
|
||||||
|
anyString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuggestManualTimeZone() throws Exception {
|
||||||
|
doNothing().when(mMockContext).enforceCallingOrSelfPermission(anyString(), any());
|
||||||
|
|
||||||
|
ManualTimeZoneSuggestion timeZoneSuggestion = createManualTimeZoneSuggestion();
|
||||||
|
mTimeZoneDetectorService.suggestManualTimeZone(timeZoneSuggestion);
|
||||||
|
mTestHandler.assertTotalMessagesEnqueued(1);
|
||||||
|
|
||||||
|
verify(mMockContext).enforceCallingOrSelfPermission(
|
||||||
|
eq(android.Manifest.permission.SUGGEST_MANUAL_TIME_AND_ZONE),
|
||||||
|
anyString());
|
||||||
|
|
||||||
|
mTestHandler.waitForMessagesToBeProcessed();
|
||||||
|
mStubbedTimeZoneDetectorStrategy.verifySuggestManualTimeZoneCalled(timeZoneSuggestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDump() {
|
||||||
|
when(mMockContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP))
|
||||||
|
.thenReturn(PackageManager.PERMISSION_GRANTED);
|
||||||
|
|
||||||
|
mTimeZoneDetectorService.dump(null, null, null);
|
||||||
|
|
||||||
|
verify(mMockContext).checkCallingOrSelfPermission(eq(android.Manifest.permission.DUMP));
|
||||||
|
mStubbedTimeZoneDetectorStrategy.verifyDumpCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAutoTimeZoneDetectionChanged() throws Exception {
|
||||||
|
mTimeZoneDetectorService.handleAutoTimeZoneDetectionChanged();
|
||||||
|
mTestHandler.assertTotalMessagesEnqueued(1);
|
||||||
|
mTestHandler.waitForMessagesToBeProcessed();
|
||||||
|
mStubbedTimeZoneDetectorStrategy.verifyHandleAutoTimeZoneDetectionChangedCalled();
|
||||||
|
|
||||||
|
mStubbedTimeZoneDetectorStrategy.resetCallTracking();
|
||||||
|
|
||||||
|
mTimeZoneDetectorService.handleAutoTimeZoneDetectionChanged();
|
||||||
|
mTestHandler.assertTotalMessagesEnqueued(2);
|
||||||
|
mTestHandler.waitForMessagesToBeProcessed();
|
||||||
|
mStubbedTimeZoneDetectorStrategy.verifyHandleAutoTimeZoneDetectionChangedCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PhoneTimeZoneSuggestion createPhoneTimeZoneSuggestion() {
|
||||||
|
int slotIndex = 1234;
|
||||||
|
return new PhoneTimeZoneSuggestion.Builder(slotIndex)
|
||||||
|
.setZoneId("TestZoneId")
|
||||||
|
.setMatchType(PhoneTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET)
|
||||||
|
.setQuality(PhoneTimeZoneSuggestion.QUALITY_SINGLE_ZONE)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ManualTimeZoneSuggestion createManualTimeZoneSuggestion() {
|
||||||
|
return new ManualTimeZoneSuggestion("TestZoneId");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class StubbedTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy {
|
||||||
|
|
||||||
|
// Call tracking.
|
||||||
|
private PhoneTimeZoneSuggestion mLastPhoneSuggestion;
|
||||||
|
private ManualTimeZoneSuggestion mLastManualSuggestion;
|
||||||
|
private boolean mHandleAutoTimeZoneDetectionChangedCalled;
|
||||||
|
private boolean mDumpCalled;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void suggestPhoneTimeZone(PhoneTimeZoneSuggestion timeZoneSuggestion) {
|
||||||
|
mLastPhoneSuggestion = timeZoneSuggestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void suggestManualTimeZone(ManualTimeZoneSuggestion timeZoneSuggestion) {
|
||||||
|
mLastManualSuggestion = timeZoneSuggestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleAutoTimeZoneDetectionChanged() {
|
||||||
|
mHandleAutoTimeZoneDetectionChangedCalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dump(PrintWriter pw, String[] args) {
|
||||||
|
mDumpCalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetCallTracking() {
|
||||||
|
mLastPhoneSuggestion = null;
|
||||||
|
mLastManualSuggestion = null;
|
||||||
|
mHandleAutoTimeZoneDetectionChangedCalled = false;
|
||||||
|
mDumpCalled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void verifySuggestPhoneTimeZoneCalled(PhoneTimeZoneSuggestion expectedSuggestion) {
|
||||||
|
assertEquals(expectedSuggestion, mLastPhoneSuggestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void verifySuggestManualTimeZoneCalled(ManualTimeZoneSuggestion expectedSuggestion) {
|
||||||
|
assertEquals(expectedSuggestion, mLastManualSuggestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
void verifyHandleAutoTimeZoneDetectionChangedCalled() {
|
||||||
|
assertTrue(mHandleAutoTimeZoneDetectionChangedCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
void verifyDumpCalled() {
|
||||||
|
assertTrue(mDumpCalled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -24,12 +24,12 @@ import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_MULTI
|
|||||||
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET;
|
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET;
|
||||||
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_SINGLE_ZONE;
|
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_SINGLE_ZONE;
|
||||||
|
|
||||||
import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_HIGH;
|
import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.PHONE_SCORE_HIGH;
|
||||||
import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_HIGHEST;
|
import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.PHONE_SCORE_HIGHEST;
|
||||||
import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_LOW;
|
import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.PHONE_SCORE_LOW;
|
||||||
import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_MEDIUM;
|
import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.PHONE_SCORE_MEDIUM;
|
||||||
import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_NONE;
|
import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.PHONE_SCORE_NONE;
|
||||||
import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_USAGE_THRESHOLD;
|
import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.PHONE_SCORE_USAGE_THRESHOLD;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
@@ -41,7 +41,7 @@ import android.app.timezonedetector.PhoneTimeZoneSuggestion;
|
|||||||
import android.app.timezonedetector.PhoneTimeZoneSuggestion.MatchType;
|
import android.app.timezonedetector.PhoneTimeZoneSuggestion.MatchType;
|
||||||
import android.app.timezonedetector.PhoneTimeZoneSuggestion.Quality;
|
import android.app.timezonedetector.PhoneTimeZoneSuggestion.Quality;
|
||||||
|
|
||||||
import com.android.server.timezonedetector.TimeZoneDetectorStrategy.QualifiedPhoneTimeZoneSuggestion;
|
import com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.QualifiedPhoneTimeZoneSuggestion;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -52,9 +52,9 @@ import java.util.Collections;
|
|||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* White-box unit tests for {@link TimeZoneDetectorStrategy}.
|
* White-box unit tests for {@link TimeZoneDetectorStrategyImpl}.
|
||||||
*/
|
*/
|
||||||
public class TimeZoneDetectorStrategyTest {
|
public class TimeZoneDetectorStrategyImplTest {
|
||||||
|
|
||||||
/** A time zone used for initialization that does not occur elsewhere in tests. */
|
/** 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 String ARBITRARY_TIME_ZONE_ID = "Etc/UTC";
|
||||||
@@ -78,14 +78,14 @@ public class TimeZoneDetectorStrategyTest {
|
|||||||
newTestCase(MATCH_TYPE_EMULATOR_ZONE_ID, QUALITY_SINGLE_ZONE, PHONE_SCORE_HIGHEST),
|
newTestCase(MATCH_TYPE_EMULATOR_ZONE_ID, QUALITY_SINGLE_ZONE, PHONE_SCORE_HIGHEST),
|
||||||
};
|
};
|
||||||
|
|
||||||
private TimeZoneDetectorStrategy mTimeZoneDetectorStrategy;
|
private TimeZoneDetectorStrategyImpl mTimeZoneDetectorStrategy;
|
||||||
private FakeTimeZoneDetectorStrategyCallback mFakeTimeZoneDetectorStrategyCallback;
|
private FakeTimeZoneDetectorStrategyCallback mFakeTimeZoneDetectorStrategyCallback;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
mFakeTimeZoneDetectorStrategyCallback = new FakeTimeZoneDetectorStrategyCallback();
|
mFakeTimeZoneDetectorStrategyCallback = new FakeTimeZoneDetectorStrategyCallback();
|
||||||
mTimeZoneDetectorStrategy =
|
mTimeZoneDetectorStrategy =
|
||||||
new TimeZoneDetectorStrategy(mFakeTimeZoneDetectorStrategyCallback);
|
new TimeZoneDetectorStrategyImpl(mFakeTimeZoneDetectorStrategyCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -364,7 +364,7 @@ public class TimeZoneDetectorStrategyTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link TimeZoneDetectorStrategy.Callback} is left to detect whether changing the time
|
* The {@link TimeZoneDetectorStrategyImpl.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
|
* zone is actually necessary. This test proves that the service doesn't assume it knows the
|
||||||
* current setting.
|
* current setting.
|
||||||
*/
|
*/
|
||||||
@@ -441,7 +441,8 @@ public class TimeZoneDetectorStrategyTest {
|
|||||||
return new PhoneTimeZoneSuggestion.Builder(PHONE2_ID).build();
|
return new PhoneTimeZoneSuggestion.Builder(PHONE2_ID).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
static class FakeTimeZoneDetectorStrategyCallback implements TimeZoneDetectorStrategy.Callback {
|
static class FakeTimeZoneDetectorStrategyCallback
|
||||||
|
implements TimeZoneDetectorStrategyImpl.Callback {
|
||||||
|
|
||||||
private boolean mAutoTimeZoneDetectionEnabled;
|
private boolean mAutoTimeZoneDetectionEnabled;
|
||||||
private TestState<String> mTimeZoneId = new TestState<>();
|
private TestState<String> mTimeZoneId = new TestState<>();
|
||||||
@@ -560,7 +561,7 @@ public class TimeZoneDetectorStrategyTest {
|
|||||||
|
|
||||||
Script autoTimeZoneDetectionEnabled(boolean enabled) {
|
Script autoTimeZoneDetectionEnabled(boolean enabled) {
|
||||||
mFakeTimeZoneDetectorStrategyCallback.setAutoTimeZoneDetectionEnabled(enabled);
|
mFakeTimeZoneDetectorStrategyCallback.setAutoTimeZoneDetectionEnabled(enabled);
|
||||||
mTimeZoneDetectorStrategy.handleAutoTimeZoneDetectionChange();
|
mTimeZoneDetectorStrategy.handleAutoTimeZoneDetectionChanged();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user