Merge "Correct a permission check / add a test" am: 05cf3e896c am: 04be13bb01

Change-Id: I88b7d0c0dfa563debe1dda88649d7fb0f43155df
This commit is contained in:
Automerger Merge Worker
2020-02-05 17:31:35 +00:00
11 changed files with 899 additions and 561 deletions

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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.
*

View File

@@ -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";

View File

@@ -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");
}
}

View File

@@ -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 @@ 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);
}

View File

@@ -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
+ '}';
}
}
}

View File

@@ -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();
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 @@ 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);
}
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}