diff options
11 files changed, 899 insertions, 561 deletions
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorService.java b/services/core/java/com/android/server/timedetector/TimeDetectorService.java index b7d63609cff9..0bb0f94d1243 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorService.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorService.java @@ -37,6 +37,9 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Objects; +/** + * The implementation of ITimeDetectorService.aidl. + */ public final class TimeDetectorService extends ITimeDetectorService.Stub { 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, new ContentObserver(handler) { 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)); } + /** Internal method for handling the auto time setting being changed. */ @VisibleForTesting - public void handleAutoTimeDetectionToggle() { + public void handleAutoTimeDetectionChanged() { mHandler.post(mTimeDetectorStrategy::handleAutoTimeDetectionChanged); } diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java index 468b806d6dce..a7c3b4dad552 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java @@ -26,8 +26,8 @@ import android.os.TimestampedValue; import java.io.PrintWriter; /** - * The interface for classes that implement the time detection algorithm used by the - * TimeDetectorService. + * The interface for the class that implements the time detection algorithm used by the + * {@link TimeDetectorService}. * * <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 diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java index a1e643f15a8e..19435ee16660 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java @@ -38,7 +38,7 @@ import java.lang.annotation.Retention; 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 * unless the data becomes too stale. * diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorCallbackImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorCallbackImpl.java index adf6d7e51f4f..2520316b5d54 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorCallbackImpl.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorCallbackImpl.java @@ -24,9 +24,9 @@ import android.os.SystemProperties; 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"; diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java index 9a1fe6501221..381ee101e125 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java @@ -67,19 +67,21 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub private static TimeZoneDetectorService create(@NonNull Context context) { final TimeZoneDetectorStrategy timeZoneDetectorStrategy = - TimeZoneDetectorStrategy.create(context); + TimeZoneDetectorStrategyImpl.create(context); Handler handler = FgThread.getHandler(); + TimeZoneDetectorService service = + new TimeZoneDetectorService(context, handler, timeZoneDetectorStrategy); + ContentResolver contentResolver = context.getContentResolver(); contentResolver.registerContentObserver( Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE), true, new ContentObserver(handler) { public void onChange(boolean selfChange) { - timeZoneDetectorStrategy.handleAutoTimeZoneDetectionChange(); + service.handleAutoTimeZoneDetectionChanged(); } }); - - return new TimeZoneDetectorService(context, handler, timeZoneDetectorStrategy); + return service; } @VisibleForTesting @@ -111,17 +113,25 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub @Nullable String[] args) { 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() { 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() { 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"); } } diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java index b0e006908231..1d439e93a1f7 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java @@ -15,192 +15,26 @@ */ 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; /** - * A singleton, stateful time zone detection strategy that is aware of user (manual) suggestions and - * suggestions from multiple phone devices. Suggestions are acted on or ignored as needed, dependent - * on the current "auto time zone detection" setting. + * The interface for the class that implement the time detection algorithm used by the + * {@link TimeZoneDetectorService}. + * + * <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 + * handle thread safety. * - * <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. + * @hide */ -public class TimeZoneDetectorStrategy { - - /** - * Used by {@link TimeZoneDetectorStrategy} 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 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; +public interface TimeZoneDetectorStrategy { - @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); - } + /** Process the suggested manually-entered (i.e. user sourced) time zone. */ + void suggestManualTimeZone(@NonNull ManualTimeZoneSuggestion suggestion); /** * 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 * setting and what to set it to. */ - 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(); - } + void suggestPhoneTimeZone(@NonNull PhoneTimeZoneSuggestion suggestion); /** * Called when there has been a change to the automatic time zone detection setting. */ - @VisibleForTesting - 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); - } - } + void handleAutoTimeZoneDetectionChanged(); /** * Dumps internal state such as field values. */ - public synchronized void dumpState(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 - + '}'; - } - } + void dump(PrintWriter pw, String[] args); } diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java new file mode 100644 index 000000000000..f85f9fe998a5 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java @@ -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 + + '}'; + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java index ae5369204428..218f43c9495d 100644 --- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java @@ -33,14 +33,13 @@ import android.app.timedetector.NetworkTimeSuggestion; import android.app.timedetector.PhoneTimeSuggestion; import android.content.Context; import android.content.pm.PackageManager; -import android.os.Handler; import android.os.HandlerThread; -import android.os.Looper; -import android.os.Message; import android.os.TimestampedValue; import androidx.test.runner.AndroidJUnit4; +import com.android.server.timezonedetector.TestHandler; + import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -108,7 +107,7 @@ public class TimeDetectorServiceTest { eq(android.Manifest.permission.SUGGEST_PHONE_TIME_AND_ZONE), anyString()); - mTestHandler.waitForEmptyQueue(); + mTestHandler.waitForMessagesToBeProcessed(); mStubbedTimeDetectorStrategy.verifySuggestPhoneTimeCalled(phoneTimeSuggestion); } @@ -140,7 +139,7 @@ public class TimeDetectorServiceTest { eq(android.Manifest.permission.SUGGEST_MANUAL_TIME_AND_ZONE), anyString()); - mTestHandler.waitForEmptyQueue(); + mTestHandler.waitForMessagesToBeProcessed(); mStubbedTimeDetectorStrategy.verifySuggestManualTimeCalled(manualTimeSuggestion); } @@ -170,7 +169,7 @@ public class TimeDetectorServiceTest { verify(mMockContext).enforceCallingOrSelfPermission( eq(android.Manifest.permission.SET_TIME), anyString()); - mTestHandler.waitForEmptyQueue(); + mTestHandler.waitForMessagesToBeProcessed(); mStubbedTimeDetectorStrategy.verifySuggestNetworkTimeCalled(NetworkTimeSuggestion); } @@ -187,21 +186,23 @@ public class TimeDetectorServiceTest { @Test public void testAutoTimeDetectionToggle() throws Exception { - mTimeDetectorService.handleAutoTimeDetectionToggle(); + mTimeDetectorService.handleAutoTimeDetectionChanged(); mTestHandler.assertTotalMessagesEnqueued(1); - mTestHandler.waitForEmptyQueue(); - mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionToggleCalled(); + mTestHandler.waitForMessagesToBeProcessed(); + mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionChangedCalled(); + + mStubbedTimeDetectorStrategy.resetCallTracking(); - mTimeDetectorService.handleAutoTimeDetectionToggle(); + mTimeDetectorService.handleAutoTimeDetectionChanged(); mTestHandler.assertTotalMessagesEnqueued(2); - mTestHandler.waitForEmptyQueue(); - mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionToggleCalled(); + mTestHandler.waitForMessagesToBeProcessed(); + mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionChangedCalled(); } private static PhoneTimeSuggestion createPhoneTimeSuggestion() { - int phoneId = 1234; + int slotIndex = 1234; TimestampedValue<Long> timeValue = new TimestampedValue<>(100L, 1_000_000L); - return new PhoneTimeSuggestion.Builder(phoneId) + return new PhoneTimeSuggestion.Builder(slotIndex) .setUtcTime(timeValue) .build(); } @@ -222,7 +223,7 @@ public class TimeDetectorServiceTest { private PhoneTimeSuggestion mLastPhoneSuggestion; private ManualTimeSuggestion mLastManualSuggestion; private NetworkTimeSuggestion mLastNetworkSuggestion; - private boolean mLastAutoTimeDetectionToggleCalled; + private boolean mHandleAutoTimeDetectionChangedCalled; private boolean mDumpCalled; @Override @@ -231,31 +232,26 @@ public class TimeDetectorServiceTest { @Override public void suggestPhoneTime(PhoneTimeSuggestion timeSuggestion) { - resetCallTracking(); mLastPhoneSuggestion = timeSuggestion; } @Override public void suggestManualTime(ManualTimeSuggestion timeSuggestion) { - resetCallTracking(); mLastManualSuggestion = timeSuggestion; } @Override public void suggestNetworkTime(NetworkTimeSuggestion timeSuggestion) { - resetCallTracking(); mLastNetworkSuggestion = timeSuggestion; } @Override public void handleAutoTimeDetectionChanged() { - resetCallTracking(); - mLastAutoTimeDetectionToggleCalled = true; + mHandleAutoTimeDetectionChangedCalled = true; } @Override public void dump(PrintWriter pw, String[] args) { - resetCallTracking(); mDumpCalled = true; } @@ -263,7 +259,7 @@ public class TimeDetectorServiceTest { mLastPhoneSuggestion = null; mLastManualSuggestion = null; mLastNetworkSuggestion = null; - mLastAutoTimeDetectionToggleCalled = false; + mHandleAutoTimeDetectionChangedCalled = false; mDumpCalled = false; } @@ -279,45 +275,12 @@ public class TimeDetectorServiceTest { assertEquals(expectedSuggestion, mLastNetworkSuggestion); } - void verifyHandleAutoTimeDetectionToggleCalled() { - assertTrue(mLastAutoTimeDetectionToggleCalled); + void verifyHandleAutoTimeDetectionChangedCalled() { + assertTrue(mHandleAutoTimeDetectionChangedCalled); } void verifyDumpCalled() { 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); - } - } - } } diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TestHandler.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TestHandler.java new file mode 100644 index 000000000000..21c9685b05d2 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TestHandler.java @@ -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(); + } + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java new file mode 100644 index 000000000000..3e7d40a0335a --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java @@ -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); + } + } + +} diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java index 2429cfc1bcd0..1e387110ed43 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyTest.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java @@ -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_SINGLE_ZONE; -import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_HIGH; -import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_HIGHEST; -import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_LOW; -import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_MEDIUM; -import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_NONE; -import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_USAGE_THRESHOLD; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.PHONE_SCORE_HIGH; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.PHONE_SCORE_HIGHEST; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.PHONE_SCORE_LOW; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.PHONE_SCORE_MEDIUM; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.PHONE_SCORE_NONE; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.PHONE_SCORE_USAGE_THRESHOLD; import static org.junit.Assert.assertEquals; 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.Quality; -import com.android.server.timezonedetector.TimeZoneDetectorStrategy.QualifiedPhoneTimeZoneSuggestion; +import com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.QualifiedPhoneTimeZoneSuggestion; import org.junit.Before; import org.junit.Test; @@ -52,9 +52,9 @@ import java.util.Collections; 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. */ 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), }; - private TimeZoneDetectorStrategy mTimeZoneDetectorStrategy; + private TimeZoneDetectorStrategyImpl mTimeZoneDetectorStrategy; private FakeTimeZoneDetectorStrategyCallback mFakeTimeZoneDetectorStrategyCallback; @Before public void setUp() { mFakeTimeZoneDetectorStrategyCallback = new FakeTimeZoneDetectorStrategyCallback(); mTimeZoneDetectorStrategy = - new TimeZoneDetectorStrategy(mFakeTimeZoneDetectorStrategyCallback); + new TimeZoneDetectorStrategyImpl(mFakeTimeZoneDetectorStrategyCallback); } @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 * current setting. */ @@ -441,7 +441,8 @@ public class TimeZoneDetectorStrategyTest { return new PhoneTimeZoneSuggestion.Builder(PHONE2_ID).build(); } - static class FakeTimeZoneDetectorStrategyCallback implements TimeZoneDetectorStrategy.Callback { + static class FakeTimeZoneDetectorStrategyCallback + implements TimeZoneDetectorStrategyImpl.Callback { private boolean mAutoTimeZoneDetectionEnabled; private TestState<String> mTimeZoneId = new TestState<>(); @@ -560,7 +561,7 @@ public class TimeZoneDetectorStrategyTest { Script autoTimeZoneDetectionEnabled(boolean enabled) { mFakeTimeZoneDetectorStrategyCallback.setAutoTimeZoneDetectionEnabled(enabled); - mTimeZoneDetectorStrategy.handleAutoTimeZoneDetectionChange(); + mTimeZoneDetectorStrategy.handleAutoTimeZoneDetectionChanged(); return this; } |