Merge "Correct a permission check / add a test" am: 05cf3e896c am: 04be13bb01 am: c58c250192
Change-Id: Iccd71c383788a21f12374d805cacbb3ff5b02771
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorService.java b/services/core/java/com/android/server/timedetector/TimeDetectorService.java
index b7d6360..0bb0f94 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.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 @@
Settings.Global.getUriFor(Settings.Global.AUTO_TIME), true,
new ContentObserver(handler) {
public void onChange(boolean selfChange) {
- timeDetectorService.handleAutoTimeDetectionToggle();
+ timeDetectorService.handleAutoTimeDetectionChanged();
}
});
@@ -114,8 +117,9 @@
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 468b806..a7c3b4d 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 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 a1e643f..19435ee 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.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 adf6d7e..2520316 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.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 9a1fe65..381ee10 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 @@
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 @@
@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 b0e0069..1d439e9 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>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 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.
+ *
+ * @hide
*/
-public class TimeZoneDetectorStrategy {
+public interface 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;
-
- @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 @@
* 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 0000000..f85f9fe
--- /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 ae53692..218f43c 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.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 @@
eq(android.Manifest.permission.SUGGEST_PHONE_TIME_AND_ZONE),
anyString());
- mTestHandler.waitForEmptyQueue();
+ mTestHandler.waitForMessagesToBeProcessed();
mStubbedTimeDetectorStrategy.verifySuggestPhoneTimeCalled(phoneTimeSuggestion);
}
@@ -140,7 +139,7 @@
eq(android.Manifest.permission.SUGGEST_MANUAL_TIME_AND_ZONE),
anyString());
- mTestHandler.waitForEmptyQueue();
+ mTestHandler.waitForMessagesToBeProcessed();
mStubbedTimeDetectorStrategy.verifySuggestManualTimeCalled(manualTimeSuggestion);
}
@@ -170,7 +169,7 @@
verify(mMockContext).enforceCallingOrSelfPermission(
eq(android.Manifest.permission.SET_TIME), anyString());
- mTestHandler.waitForEmptyQueue();
+ mTestHandler.waitForMessagesToBeProcessed();
mStubbedTimeDetectorStrategy.verifySuggestNetworkTimeCalled(NetworkTimeSuggestion);
}
@@ -187,21 +186,23 @@
@Test
public void testAutoTimeDetectionToggle() throws Exception {
- mTimeDetectorService.handleAutoTimeDetectionToggle();
+ mTimeDetectorService.handleAutoTimeDetectionChanged();
mTestHandler.assertTotalMessagesEnqueued(1);
- mTestHandler.waitForEmptyQueue();
- mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionToggleCalled();
+ mTestHandler.waitForMessagesToBeProcessed();
+ mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionChangedCalled();
- mTimeDetectorService.handleAutoTimeDetectionToggle();
+ mStubbedTimeDetectorStrategy.resetCallTracking();
+
+ 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 @@
private PhoneTimeSuggestion mLastPhoneSuggestion;
private ManualTimeSuggestion mLastManualSuggestion;
private NetworkTimeSuggestion mLastNetworkSuggestion;
- private boolean mLastAutoTimeDetectionToggleCalled;
+ private boolean mHandleAutoTimeDetectionChangedCalled;
private boolean mDumpCalled;
@Override
@@ -231,31 +232,26 @@
@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 @@
mLastPhoneSuggestion = null;
mLastManualSuggestion = null;
mLastNetworkSuggestion = null;
- mLastAutoTimeDetectionToggleCalled = false;
+ mHandleAutoTimeDetectionChangedCalled = false;
mDumpCalled = false;
}
@@ -279,45 +275,12 @@
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 0000000..21c9685
--- /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 0000000..3e7d40a
--- /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
similarity index 96%
rename from services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyTest.java
rename to services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
index 2429cfc..1e38711 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_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.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.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 @@
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 @@
}
/**
- * 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 @@
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 @@
Script autoTimeZoneDetectionEnabled(boolean enabled) {
mFakeTimeZoneDetectorStrategyCallback.setAutoTimeZoneDetectionEnabled(enabled);
- mTimeZoneDetectorStrategy.handleAutoTimeZoneDetectionChange();
+ mTimeZoneDetectorStrategy.handleAutoTimeZoneDetectionChanged();
return this;
}