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

Change-Id: I770e101b009b86b8d94c5c6faa510baf042619b8
This commit is contained in:
Automerger Merge Worker
2020-02-05 17:21:13 +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.io.PrintWriter;
import java.util.Objects; import java.util.Objects;
/**
* The implementation of ITimeDetectorService.aidl.
*/
public final class TimeDetectorService extends ITimeDetectorService.Stub { public final class TimeDetectorService extends ITimeDetectorService.Stub {
private static final String TAG = "TimeDetectorService"; private static final String TAG = "TimeDetectorService";
@@ -75,7 +78,7 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub {
Settings.Global.getUriFor(Settings.Global.AUTO_TIME), true, Settings.Global.getUriFor(Settings.Global.AUTO_TIME), true,
new ContentObserver(handler) { new ContentObserver(handler) {
public void onChange(boolean selfChange) { public void onChange(boolean selfChange) {
timeDetectorService.handleAutoTimeDetectionToggle(); timeDetectorService.handleAutoTimeDetectionChanged();
} }
}); });
@@ -114,8 +117,9 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub {
mHandler.post(() -> mTimeDetectorStrategy.suggestNetworkTime(timeSignal)); mHandler.post(() -> mTimeDetectorStrategy.suggestNetworkTime(timeSignal));
} }
/** Internal method for handling the auto time setting being changed. */
@VisibleForTesting @VisibleForTesting
public void handleAutoTimeDetectionToggle() { public void handleAutoTimeDetectionChanged() {
mHandler.post(mTimeDetectorStrategy::handleAutoTimeDetectionChanged); mHandler.post(mTimeDetectorStrategy::handleAutoTimeDetectionChanged);
} }

View File

@@ -26,8 +26,8 @@ import android.os.TimestampedValue;
import java.io.PrintWriter; import java.io.PrintWriter;
/** /**
* The interface for classes that implement the time detection algorithm used by the * The interface for the class that implements the time detection algorithm used by the
* TimeDetectorService. * {@link TimeDetectorService}.
* *
* <p>Most calls will be handled by a single thread but that is not true for all calls. For example * <p>Most calls will be handled by a single thread but that is not true for all calls. For example
* {@link #dump(PrintWriter, String[])}) may be called on a different thread so implementations must * {@link #dump(PrintWriter, String[])}) may be called on a different thread so implementations must

View File

@@ -38,7 +38,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
/** /**
* An implementation of TimeDetectorStrategy that passes phone and manual suggestions to * An implementation of {@link TimeDetectorStrategy} that passes phone and manual suggestions to
* {@link AlarmManager}. When there are multiple phone sources, the one with the lowest ID is used * {@link AlarmManager}. When there are multiple phone sources, the one with the lowest ID is used
* unless the data becomes too stale. * unless the data becomes too stale.
* *

View File

@@ -24,9 +24,9 @@ import android.os.SystemProperties;
import android.provider.Settings; import android.provider.Settings;
/** /**
* The real implementation of {@link TimeZoneDetectorStrategy.Callback}. * The real implementation of {@link TimeZoneDetectorStrategyImpl.Callback}.
*/ */
public final class TimeZoneDetectorCallbackImpl implements TimeZoneDetectorStrategy.Callback { public final class TimeZoneDetectorCallbackImpl implements TimeZoneDetectorStrategyImpl.Callback {
private static final String TIMEZONE_PROPERTY = "persist.sys.timezone"; private static final String TIMEZONE_PROPERTY = "persist.sys.timezone";

View File

@@ -67,19 +67,21 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub
private static TimeZoneDetectorService create(@NonNull Context context) { private static TimeZoneDetectorService create(@NonNull Context context) {
final TimeZoneDetectorStrategy timeZoneDetectorStrategy = final TimeZoneDetectorStrategy timeZoneDetectorStrategy =
TimeZoneDetectorStrategy.create(context); TimeZoneDetectorStrategyImpl.create(context);
Handler handler = FgThread.getHandler(); Handler handler = FgThread.getHandler();
TimeZoneDetectorService service =
new TimeZoneDetectorService(context, handler, timeZoneDetectorStrategy);
ContentResolver contentResolver = context.getContentResolver(); ContentResolver contentResolver = context.getContentResolver();
contentResolver.registerContentObserver( contentResolver.registerContentObserver(
Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE), true, Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE), true,
new ContentObserver(handler) { new ContentObserver(handler) {
public void onChange(boolean selfChange) { public void onChange(boolean selfChange) {
timeZoneDetectorStrategy.handleAutoTimeZoneDetectionChange(); service.handleAutoTimeZoneDetectionChanged();
} }
}); });
return service;
return new TimeZoneDetectorService(context, handler, timeZoneDetectorStrategy);
} }
@VisibleForTesting @VisibleForTesting
@@ -111,17 +113,25 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub
@Nullable String[] args) { @Nullable String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
mTimeZoneDetectorStrategy.dumpState(pw, args); mTimeZoneDetectorStrategy.dump(pw, args);
}
/** Internal method for handling the auto time zone setting being changed. */
@VisibleForTesting
public void handleAutoTimeZoneDetectionChanged() {
mHandler.post(mTimeZoneDetectorStrategy::handleAutoTimeZoneDetectionChanged);
} }
private void enforceSuggestPhoneTimeZonePermission() { private void enforceSuggestPhoneTimeZonePermission() {
mContext.enforceCallingPermission( mContext.enforceCallingPermission(
android.Manifest.permission.SET_TIME_ZONE, "set time zone"); android.Manifest.permission.SUGGEST_PHONE_TIME_AND_ZONE,
"suggest phone time and time zone");
} }
private void enforceSuggestManualTimeZonePermission() { private void enforceSuggestManualTimeZonePermission() {
mContext.enforceCallingOrSelfPermission( mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.SET_TIME_ZONE, "set time zone"); android.Manifest.permission.SUGGEST_MANUAL_TIME_AND_ZONE,
"suggest manual time and time zone");
} }
} }

View File

@@ -15,192 +15,26 @@
*/ */
package com.android.server.timezonedetector; package com.android.server.timezonedetector;
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.MATCH_TYPE_EMULATOR_ZONE_ID;
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY;
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS;
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET;
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_SINGLE_ZONE;
import android.annotation.IntDef;
import android.annotation.NonNull; import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.ManualTimeZoneSuggestion;
import android.app.timezonedetector.PhoneTimeZoneSuggestion; import android.app.timezonedetector.PhoneTimeZoneSuggestion;
import android.content.Context;
import android.util.LocalLog;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
/** /**
* A singleton, stateful time zone detection strategy that is aware of user (manual) suggestions and * The interface for the class that implement the time detection algorithm used by the
* suggestions from multiple phone devices. Suggestions are acted on or ignored as needed, dependent * {@link TimeZoneDetectorService}.
* on the current "auto time zone detection" setting.
* *
* <p>For automatic detection it keeps track of the most recent suggestion from each phone it uses * <p>Most calls will be handled by a single thread but that is not true for all calls. For example
* the best suggestion based on a scoring algorithm. If several phones provide the same score then * {@link #dump(PrintWriter, String[])}) may be called on a different thread so implementations must
* the phone with the lowest numeric ID "wins". If the situation changes and it is no longer * handle thread safety.
* possible to be confident about the time zone, phones must submit an empty suggestion in order to *
* "withdraw" their previous suggestion. * @hide
*/ */
public class TimeZoneDetectorStrategy { public interface TimeZoneDetectorStrategy {
/** /** Process the suggested manually-entered (i.e. user sourced) time zone. */
* Used by {@link TimeZoneDetectorStrategy} to interact with the surrounding service. It can be void suggestManualTimeZone(@NonNull ManualTimeZoneSuggestion suggestion);
* faked for tests.
*
* <p>Note: Because the system properties-derived values like
* {@link #isAutoTimeZoneDetectionEnabled()}, {@link #isAutoTimeZoneDetectionEnabled()},
* {@link #getDeviceTimeZone()} can be modified independently and from different threads (and
* processes!), their use are prone to race conditions. That will be true until the
* responsibility for setting their values is moved to {@link TimeZoneDetectorStrategy}.
*/
@VisibleForTesting
public interface Callback {
/**
* Returns true if automatic time zone detection is enabled in settings.
*/
boolean isAutoTimeZoneDetectionEnabled();
/**
* Returns true if the device has had an explicit time zone set.
*/
boolean isDeviceTimeZoneInitialized();
/**
* Returns the device's currently configured time zone.
*/
String getDeviceTimeZone();
/**
* Sets the device's time zone.
*/
void setDeviceTimeZone(@NonNull String zoneId);
}
private static final String LOG_TAG = "TimeZoneDetectorStrategy";
private static final boolean DBG = false;
@IntDef({ ORIGIN_PHONE, ORIGIN_MANUAL })
@Retention(RetentionPolicy.SOURCE)
public @interface Origin {}
/** Used when a time value originated from a telephony signal. */
@Origin
private static final int ORIGIN_PHONE = 1;
/** Used when a time value originated from a user / manual settings. */
@Origin
private static final int ORIGIN_MANUAL = 2;
/**
* The abstract score for an empty or invalid phone suggestion.
*
* Used to score phone suggestions where there is no zone.
*/
@VisibleForTesting
public static final int PHONE_SCORE_NONE = 0;
/**
* The abstract score for a low quality phone suggestion.
*
* Used to score suggestions where:
* The suggested zone ID is one of several possibilities, and the possibilities have different
* offsets.
*
* You would have to be quite desperate to want to use this choice.
*/
@VisibleForTesting
public static final int PHONE_SCORE_LOW = 1;
/**
* The abstract score for a medium quality phone suggestion.
*
* Used for:
* The suggested zone ID is one of several possibilities but at least the possibilities have the
* same offset. Users would get the correct time but for the wrong reason. i.e. their device may
* switch to DST at the wrong time and (for example) their calendar events.
*/
@VisibleForTesting
public static final int PHONE_SCORE_MEDIUM = 2;
/**
* The abstract score for a high quality phone suggestion.
*
* Used for:
* The suggestion was for one zone ID and the answer was unambiguous and likely correct given
* the info available.
*/
@VisibleForTesting
public static final int PHONE_SCORE_HIGH = 3;
/**
* The abstract score for a highest quality phone suggestion.
*
* Used for:
* Suggestions that must "win" because they constitute test or emulator zone ID.
*/
@VisibleForTesting
public static final int PHONE_SCORE_HIGHEST = 4;
/**
* The threshold at which phone suggestions are good enough to use to set the device's time
* zone.
*/
@VisibleForTesting
public static final int PHONE_SCORE_USAGE_THRESHOLD = PHONE_SCORE_MEDIUM;
/** The number of previous phone suggestions to keep for each ID (for use during debugging). */
private static final int KEEP_PHONE_SUGGESTION_HISTORY_SIZE = 30;
@NonNull
private final Callback mCallback;
/**
* A log that records the decisions / decision metadata that affected the device's time zone
* (for use during debugging).
*/
@NonNull
private final LocalLog mTimeZoneChangesLog = new LocalLog(30, false /* useLocalTimestamps */);
/**
* A mapping from slotIndex to a phone time zone suggestion. We typically expect one or two
* mappings: devices will have a small number of telephony devices and slotIndexs are assumed to
* be stable.
*/
@GuardedBy("this")
private ArrayMapWithHistory<Integer, QualifiedPhoneTimeZoneSuggestion> mSuggestionBySlotIndex =
new ArrayMapWithHistory<>(KEEP_PHONE_SUGGESTION_HISTORY_SIZE);
/**
* Creates a new instance of {@link TimeZoneDetectorStrategy}.
*/
public static TimeZoneDetectorStrategy create(Context context) {
Callback timeZoneDetectionServiceHelper = new TimeZoneDetectorCallbackImpl(context);
return new TimeZoneDetectorStrategy(timeZoneDetectionServiceHelper);
}
@VisibleForTesting
public TimeZoneDetectorStrategy(Callback callback) {
mCallback = Objects.requireNonNull(callback);
}
/** Process the suggested manually- / user-entered time zone. */
public synchronized void suggestManualTimeZone(@NonNull ManualTimeZoneSuggestion suggestion) {
Objects.requireNonNull(suggestion);
String timeZoneId = suggestion.getZoneId();
String cause = "Manual time suggestion received: suggestion=" + suggestion;
setDeviceTimeZoneIfRequired(ORIGIN_MANUAL, timeZoneId, cause);
}
/** /**
* Suggests a time zone for the device, or withdraws a previous suggestion if * Suggests a time zone for the device, or withdraws a previous suggestion if
@@ -210,312 +44,15 @@ public class TimeZoneDetectorStrategy {
* suggestion. The strategy uses suggestions to decide whether to modify the device's time zone * suggestion. The strategy uses suggestions to decide whether to modify the device's time zone
* setting and what to set it to. * setting and what to set it to.
*/ */
public synchronized void suggestPhoneTimeZone(@NonNull PhoneTimeZoneSuggestion suggestion) { void suggestPhoneTimeZone(@NonNull PhoneTimeZoneSuggestion suggestion);
if (DBG) {
Slog.d(LOG_TAG, "Phone suggestion received. newSuggestion=" + suggestion);
}
Objects.requireNonNull(suggestion);
// Score the suggestion.
int score = scorePhoneSuggestion(suggestion);
QualifiedPhoneTimeZoneSuggestion scoredSuggestion =
new QualifiedPhoneTimeZoneSuggestion(suggestion, score);
// Store the suggestion against the correct slotIndex.
mSuggestionBySlotIndex.put(suggestion.getSlotIndex(), scoredSuggestion);
// Now perform auto time zone detection. The new suggestion may be used to modify the time
// zone setting.
String reason = "New phone time suggested. suggestion=" + suggestion;
doAutoTimeZoneDetection(reason);
}
private static int scorePhoneSuggestion(@NonNull PhoneTimeZoneSuggestion suggestion) {
int score;
if (suggestion.getZoneId() == null) {
score = PHONE_SCORE_NONE;
} else if (suggestion.getMatchType() == MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY
|| suggestion.getMatchType() == MATCH_TYPE_EMULATOR_ZONE_ID) {
// Handle emulator / test cases : These suggestions should always just be used.
score = PHONE_SCORE_HIGHEST;
} else if (suggestion.getQuality() == QUALITY_SINGLE_ZONE) {
score = PHONE_SCORE_HIGH;
} else if (suggestion.getQuality() == QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET) {
// The suggestion may be wrong, but at least the offset should be correct.
score = PHONE_SCORE_MEDIUM;
} else if (suggestion.getQuality() == QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS) {
// The suggestion has a good chance of being wrong.
score = PHONE_SCORE_LOW;
} else {
throw new AssertionError();
}
return score;
}
/**
* Finds the best available time zone suggestion from all phones. If it is high-enough quality
* and automatic time zone detection is enabled then it will be set on the device. The outcome
* can be that this strategy becomes / remains un-opinionated and nothing is set.
*/
@GuardedBy("this")
private void doAutoTimeZoneDetection(@NonNull String detectionReason) {
if (!mCallback.isAutoTimeZoneDetectionEnabled()) {
// Avoid doing unnecessary work with this (race-prone) check.
return;
}
QualifiedPhoneTimeZoneSuggestion bestPhoneSuggestion = findBestPhoneSuggestion();
// Work out what to do with the best suggestion.
if (bestPhoneSuggestion == null) {
// There is no phone suggestion available at all. Become un-opinionated.
if (DBG) {
Slog.d(LOG_TAG, "Could not determine time zone: No best phone suggestion."
+ " detectionReason=" + detectionReason);
}
return;
}
// Special case handling for uninitialized devices. This should only happen once.
String newZoneId = bestPhoneSuggestion.suggestion.getZoneId();
if (newZoneId != null && !mCallback.isDeviceTimeZoneInitialized()) {
String cause = "Device has no time zone set. Attempting to set the device to the best"
+ " available suggestion."
+ " bestPhoneSuggestion=" + bestPhoneSuggestion
+ ", detectionReason=" + detectionReason;
Slog.i(LOG_TAG, cause);
setDeviceTimeZoneIfRequired(ORIGIN_PHONE, newZoneId, cause);
return;
}
boolean suggestionGoodEnough = bestPhoneSuggestion.score >= PHONE_SCORE_USAGE_THRESHOLD;
if (!suggestionGoodEnough) {
if (DBG) {
Slog.d(LOG_TAG, "Best suggestion not good enough."
+ " bestPhoneSuggestion=" + bestPhoneSuggestion
+ ", detectionReason=" + detectionReason);
}
return;
}
// Paranoia: Every suggestion above the SCORE_USAGE_THRESHOLD should have a non-null time
// zone ID.
if (newZoneId == null) {
Slog.w(LOG_TAG, "Empty zone suggestion scored higher than expected. This is an error:"
+ " bestPhoneSuggestion=" + bestPhoneSuggestion
+ " detectionReason=" + detectionReason);
return;
}
String zoneId = bestPhoneSuggestion.suggestion.getZoneId();
String cause = "Found good suggestion."
+ ", bestPhoneSuggestion=" + bestPhoneSuggestion
+ ", detectionReason=" + detectionReason;
setDeviceTimeZoneIfRequired(ORIGIN_PHONE, zoneId, cause);
}
@GuardedBy("this")
private void setDeviceTimeZoneIfRequired(
@Origin int origin, @NonNull String newZoneId, @NonNull String cause) {
Objects.requireNonNull(newZoneId);
Objects.requireNonNull(cause);
boolean isOriginAutomatic = isOriginAutomatic(origin);
if (isOriginAutomatic) {
if (!mCallback.isAutoTimeZoneDetectionEnabled()) {
if (DBG) {
Slog.d(LOG_TAG, "Auto time zone detection is not enabled."
+ " origin=" + origin
+ ", newZoneId=" + newZoneId
+ ", cause=" + cause);
}
return;
}
} else {
if (mCallback.isAutoTimeZoneDetectionEnabled()) {
if (DBG) {
Slog.d(LOG_TAG, "Auto time zone detection is enabled."
+ " origin=" + origin
+ ", newZoneId=" + newZoneId
+ ", cause=" + cause);
}
return;
}
}
String currentZoneId = mCallback.getDeviceTimeZone();
// Avoid unnecessary changes / intents.
if (newZoneId.equals(currentZoneId)) {
// No need to set the device time zone - the setting is already what we would be
// suggesting.
if (DBG) {
Slog.d(LOG_TAG, "No need to change the time zone;"
+ " device is already set to the suggested zone."
+ " origin=" + origin
+ ", newZoneId=" + newZoneId
+ ", cause=" + cause);
}
return;
}
mCallback.setDeviceTimeZone(newZoneId);
String msg = "Set device time zone."
+ " origin=" + origin
+ ", currentZoneId=" + currentZoneId
+ ", newZoneId=" + newZoneId
+ ", cause=" + cause;
if (DBG) {
Slog.d(LOG_TAG, msg);
}
mTimeZoneChangesLog.log(msg);
}
private static boolean isOriginAutomatic(@Origin int origin) {
return origin != ORIGIN_MANUAL;
}
@GuardedBy("this")
@Nullable
private QualifiedPhoneTimeZoneSuggestion findBestPhoneSuggestion() {
QualifiedPhoneTimeZoneSuggestion bestSuggestion = null;
// Iterate over the latest QualifiedPhoneTimeZoneSuggestion objects received for each phone
// and find the best. Note that we deliberately do not look at age: the caller can
// rate-limit so age is not a strong indicator of confidence. Instead, the callers are
// expected to withdraw suggestions they no longer have confidence in.
for (int i = 0; i < mSuggestionBySlotIndex.size(); i++) {
QualifiedPhoneTimeZoneSuggestion candidateSuggestion =
mSuggestionBySlotIndex.valueAt(i);
if (candidateSuggestion == null) {
// Unexpected
continue;
}
if (bestSuggestion == null) {
bestSuggestion = candidateSuggestion;
} else if (candidateSuggestion.score > bestSuggestion.score) {
bestSuggestion = candidateSuggestion;
} else if (candidateSuggestion.score == bestSuggestion.score) {
// Tie! Use the suggestion with the lowest slotIndex.
int candidateSlotIndex = candidateSuggestion.suggestion.getSlotIndex();
int bestSlotIndex = bestSuggestion.suggestion.getSlotIndex();
if (candidateSlotIndex < bestSlotIndex) {
bestSuggestion = candidateSuggestion;
}
}
}
return bestSuggestion;
}
/**
* Returns the current best phone suggestion. Not intended for general use: it is used during
* tests to check strategy behavior.
*/
@VisibleForTesting
@Nullable
public synchronized QualifiedPhoneTimeZoneSuggestion findBestPhoneSuggestionForTests() {
return findBestPhoneSuggestion();
}
/** /**
* Called when there has been a change to the automatic time zone detection setting. * Called when there has been a change to the automatic time zone detection setting.
*/ */
@VisibleForTesting void handleAutoTimeZoneDetectionChanged();
public synchronized void handleAutoTimeZoneDetectionChange() {
if (DBG) {
Slog.d(LOG_TAG, "handleTimeZoneDetectionChange() called");
}
if (mCallback.isAutoTimeZoneDetectionEnabled()) {
// When the user enabled time zone detection, run the time zone detection and change the
// device time zone if possible.
String reason = "Auto time zone detection setting enabled.";
doAutoTimeZoneDetection(reason);
}
}
/** /**
* Dumps internal state such as field values. * Dumps internal state such as field values.
*/ */
public synchronized void dumpState(PrintWriter pw, String[] args) { void dump(PrintWriter pw, String[] args);
IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
ipw.println("TimeZoneDetectorStrategy:");
ipw.increaseIndent(); // level 1
ipw.println("mCallback.isTimeZoneDetectionEnabled()="
+ mCallback.isAutoTimeZoneDetectionEnabled());
ipw.println("mCallback.isDeviceTimeZoneInitialized()="
+ mCallback.isDeviceTimeZoneInitialized());
ipw.println("mCallback.getDeviceTimeZone()="
+ mCallback.getDeviceTimeZone());
ipw.println("Time zone change log:");
ipw.increaseIndent(); // level 2
mTimeZoneChangesLog.dump(ipw);
ipw.decreaseIndent(); // level 2
ipw.println("Phone suggestion history:");
ipw.increaseIndent(); // level 2
mSuggestionBySlotIndex.dump(ipw);
ipw.decreaseIndent(); // level 2
ipw.decreaseIndent(); // level 1
ipw.flush();
}
/**
* A method used to inspect strategy state during tests. Not intended for general use.
*/
@VisibleForTesting
public synchronized QualifiedPhoneTimeZoneSuggestion getLatestPhoneSuggestion(int slotIndex) {
return mSuggestionBySlotIndex.get(slotIndex);
}
/**
* A {@link PhoneTimeZoneSuggestion} with additional qualifying metadata.
*/
@VisibleForTesting
public static class QualifiedPhoneTimeZoneSuggestion {
@VisibleForTesting
public final PhoneTimeZoneSuggestion suggestion;
/**
* The score the suggestion has been given. This can be used to rank against other
* suggestions of the same type.
*/
@VisibleForTesting
public final int score;
@VisibleForTesting
public QualifiedPhoneTimeZoneSuggestion(PhoneTimeZoneSuggestion suggestion, int score) {
this.suggestion = suggestion;
this.score = score;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
QualifiedPhoneTimeZoneSuggestion that = (QualifiedPhoneTimeZoneSuggestion) o;
return score == that.score
&& suggestion.equals(that.suggestion);
}
@Override
public int hashCode() {
return Objects.hash(score, suggestion);
}
@Override
public String toString() {
return "QualifiedPhoneTimeZoneSuggestion{"
+ "suggestion=" + suggestion
+ ", score=" + score
+ '}';
}
}
} }

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.app.timedetector.PhoneTimeSuggestion;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.TimestampedValue; import android.os.TimestampedValue;
import androidx.test.runner.AndroidJUnit4; import androidx.test.runner.AndroidJUnit4;
import com.android.server.timezonedetector.TestHandler;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -108,7 +107,7 @@ public class TimeDetectorServiceTest {
eq(android.Manifest.permission.SUGGEST_PHONE_TIME_AND_ZONE), eq(android.Manifest.permission.SUGGEST_PHONE_TIME_AND_ZONE),
anyString()); anyString());
mTestHandler.waitForEmptyQueue(); mTestHandler.waitForMessagesToBeProcessed();
mStubbedTimeDetectorStrategy.verifySuggestPhoneTimeCalled(phoneTimeSuggestion); mStubbedTimeDetectorStrategy.verifySuggestPhoneTimeCalled(phoneTimeSuggestion);
} }
@@ -140,7 +139,7 @@ public class TimeDetectorServiceTest {
eq(android.Manifest.permission.SUGGEST_MANUAL_TIME_AND_ZONE), eq(android.Manifest.permission.SUGGEST_MANUAL_TIME_AND_ZONE),
anyString()); anyString());
mTestHandler.waitForEmptyQueue(); mTestHandler.waitForMessagesToBeProcessed();
mStubbedTimeDetectorStrategy.verifySuggestManualTimeCalled(manualTimeSuggestion); mStubbedTimeDetectorStrategy.verifySuggestManualTimeCalled(manualTimeSuggestion);
} }
@@ -170,7 +169,7 @@ public class TimeDetectorServiceTest {
verify(mMockContext).enforceCallingOrSelfPermission( verify(mMockContext).enforceCallingOrSelfPermission(
eq(android.Manifest.permission.SET_TIME), anyString()); eq(android.Manifest.permission.SET_TIME), anyString());
mTestHandler.waitForEmptyQueue(); mTestHandler.waitForMessagesToBeProcessed();
mStubbedTimeDetectorStrategy.verifySuggestNetworkTimeCalled(NetworkTimeSuggestion); mStubbedTimeDetectorStrategy.verifySuggestNetworkTimeCalled(NetworkTimeSuggestion);
} }
@@ -187,21 +186,23 @@ public class TimeDetectorServiceTest {
@Test @Test
public void testAutoTimeDetectionToggle() throws Exception { public void testAutoTimeDetectionToggle() throws Exception {
mTimeDetectorService.handleAutoTimeDetectionToggle(); mTimeDetectorService.handleAutoTimeDetectionChanged();
mTestHandler.assertTotalMessagesEnqueued(1); mTestHandler.assertTotalMessagesEnqueued(1);
mTestHandler.waitForEmptyQueue(); mTestHandler.waitForMessagesToBeProcessed();
mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionToggleCalled(); mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionChangedCalled();
mTimeDetectorService.handleAutoTimeDetectionToggle(); mStubbedTimeDetectorStrategy.resetCallTracking();
mTimeDetectorService.handleAutoTimeDetectionChanged();
mTestHandler.assertTotalMessagesEnqueued(2); mTestHandler.assertTotalMessagesEnqueued(2);
mTestHandler.waitForEmptyQueue(); mTestHandler.waitForMessagesToBeProcessed();
mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionToggleCalled(); mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionChangedCalled();
} }
private static PhoneTimeSuggestion createPhoneTimeSuggestion() { private static PhoneTimeSuggestion createPhoneTimeSuggestion() {
int phoneId = 1234; int slotIndex = 1234;
TimestampedValue<Long> timeValue = new TimestampedValue<>(100L, 1_000_000L); TimestampedValue<Long> timeValue = new TimestampedValue<>(100L, 1_000_000L);
return new PhoneTimeSuggestion.Builder(phoneId) return new PhoneTimeSuggestion.Builder(slotIndex)
.setUtcTime(timeValue) .setUtcTime(timeValue)
.build(); .build();
} }
@@ -222,7 +223,7 @@ public class TimeDetectorServiceTest {
private PhoneTimeSuggestion mLastPhoneSuggestion; private PhoneTimeSuggestion mLastPhoneSuggestion;
private ManualTimeSuggestion mLastManualSuggestion; private ManualTimeSuggestion mLastManualSuggestion;
private NetworkTimeSuggestion mLastNetworkSuggestion; private NetworkTimeSuggestion mLastNetworkSuggestion;
private boolean mLastAutoTimeDetectionToggleCalled; private boolean mHandleAutoTimeDetectionChangedCalled;
private boolean mDumpCalled; private boolean mDumpCalled;
@Override @Override
@@ -231,31 +232,26 @@ public class TimeDetectorServiceTest {
@Override @Override
public void suggestPhoneTime(PhoneTimeSuggestion timeSuggestion) { public void suggestPhoneTime(PhoneTimeSuggestion timeSuggestion) {
resetCallTracking();
mLastPhoneSuggestion = timeSuggestion; mLastPhoneSuggestion = timeSuggestion;
} }
@Override @Override
public void suggestManualTime(ManualTimeSuggestion timeSuggestion) { public void suggestManualTime(ManualTimeSuggestion timeSuggestion) {
resetCallTracking();
mLastManualSuggestion = timeSuggestion; mLastManualSuggestion = timeSuggestion;
} }
@Override @Override
public void suggestNetworkTime(NetworkTimeSuggestion timeSuggestion) { public void suggestNetworkTime(NetworkTimeSuggestion timeSuggestion) {
resetCallTracking();
mLastNetworkSuggestion = timeSuggestion; mLastNetworkSuggestion = timeSuggestion;
} }
@Override @Override
public void handleAutoTimeDetectionChanged() { public void handleAutoTimeDetectionChanged() {
resetCallTracking(); mHandleAutoTimeDetectionChangedCalled = true;
mLastAutoTimeDetectionToggleCalled = true;
} }
@Override @Override
public void dump(PrintWriter pw, String[] args) { public void dump(PrintWriter pw, String[] args) {
resetCallTracking();
mDumpCalled = true; mDumpCalled = true;
} }
@@ -263,7 +259,7 @@ public class TimeDetectorServiceTest {
mLastPhoneSuggestion = null; mLastPhoneSuggestion = null;
mLastManualSuggestion = null; mLastManualSuggestion = null;
mLastNetworkSuggestion = null; mLastNetworkSuggestion = null;
mLastAutoTimeDetectionToggleCalled = false; mHandleAutoTimeDetectionChangedCalled = false;
mDumpCalled = false; mDumpCalled = false;
} }
@@ -279,45 +275,12 @@ public class TimeDetectorServiceTest {
assertEquals(expectedSuggestion, mLastNetworkSuggestion); assertEquals(expectedSuggestion, mLastNetworkSuggestion);
} }
void verifyHandleAutoTimeDetectionToggleCalled() { void verifyHandleAutoTimeDetectionChangedCalled() {
assertTrue(mLastAutoTimeDetectionToggleCalled); assertTrue(mHandleAutoTimeDetectionChangedCalled);
} }
void verifyDumpCalled() { void verifyDumpCalled() {
assertTrue(mDumpCalled); assertTrue(mDumpCalled);
} }
} }
/**
* A Handler that can track posts/sends and wait for work to be completed.
*/
private static class TestHandler extends Handler {
private int mMessagesSent;
TestHandler(Looper looper) {
super(looper);
}
@Override
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
mMessagesSent++;
return super.sendMessageAtTime(msg, uptimeMillis);
}
/** Asserts the number of messages posted or sent is as expected. */
void assertTotalMessagesEnqueued(int expected) {
assertEquals(expected, mMessagesSent);
}
/**
* Waits for all currently enqueued work due to be processed to be completed before
* returning.
*/
void waitForEmptyQueue() throws InterruptedException {
while (!getLooper().getQueue().isIdle()) {
Thread.sleep(100);
}
}
}
} }

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_MULTIPLE_ZONES_WITH_SAME_OFFSET;
import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_SINGLE_ZONE; import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_SINGLE_ZONE;
import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_HIGH; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.PHONE_SCORE_HIGH;
import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_HIGHEST; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.PHONE_SCORE_HIGHEST;
import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_LOW; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.PHONE_SCORE_LOW;
import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_MEDIUM; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.PHONE_SCORE_MEDIUM;
import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_NONE; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.PHONE_SCORE_NONE;
import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_USAGE_THRESHOLD; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.PHONE_SCORE_USAGE_THRESHOLD;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@@ -41,7 +41,7 @@ import android.app.timezonedetector.PhoneTimeZoneSuggestion;
import android.app.timezonedetector.PhoneTimeZoneSuggestion.MatchType; import android.app.timezonedetector.PhoneTimeZoneSuggestion.MatchType;
import android.app.timezonedetector.PhoneTimeZoneSuggestion.Quality; import android.app.timezonedetector.PhoneTimeZoneSuggestion.Quality;
import com.android.server.timezonedetector.TimeZoneDetectorStrategy.QualifiedPhoneTimeZoneSuggestion; import com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.QualifiedPhoneTimeZoneSuggestion;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -52,9 +52,9 @@ import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
/** /**
* White-box unit tests for {@link TimeZoneDetectorStrategy}. * White-box unit tests for {@link TimeZoneDetectorStrategyImpl}.
*/ */
public class TimeZoneDetectorStrategyTest { public class TimeZoneDetectorStrategyImplTest {
/** A time zone used for initialization that does not occur elsewhere in tests. */ /** A time zone used for initialization that does not occur elsewhere in tests. */
private static final String ARBITRARY_TIME_ZONE_ID = "Etc/UTC"; private static final String ARBITRARY_TIME_ZONE_ID = "Etc/UTC";
@@ -78,14 +78,14 @@ public class TimeZoneDetectorStrategyTest {
newTestCase(MATCH_TYPE_EMULATOR_ZONE_ID, QUALITY_SINGLE_ZONE, PHONE_SCORE_HIGHEST), newTestCase(MATCH_TYPE_EMULATOR_ZONE_ID, QUALITY_SINGLE_ZONE, PHONE_SCORE_HIGHEST),
}; };
private TimeZoneDetectorStrategy mTimeZoneDetectorStrategy; private TimeZoneDetectorStrategyImpl mTimeZoneDetectorStrategy;
private FakeTimeZoneDetectorStrategyCallback mFakeTimeZoneDetectorStrategyCallback; private FakeTimeZoneDetectorStrategyCallback mFakeTimeZoneDetectorStrategyCallback;
@Before @Before
public void setUp() { public void setUp() {
mFakeTimeZoneDetectorStrategyCallback = new FakeTimeZoneDetectorStrategyCallback(); mFakeTimeZoneDetectorStrategyCallback = new FakeTimeZoneDetectorStrategyCallback();
mTimeZoneDetectorStrategy = mTimeZoneDetectorStrategy =
new TimeZoneDetectorStrategy(mFakeTimeZoneDetectorStrategyCallback); new TimeZoneDetectorStrategyImpl(mFakeTimeZoneDetectorStrategyCallback);
} }
@Test @Test
@@ -364,7 +364,7 @@ public class TimeZoneDetectorStrategyTest {
} }
/** /**
* The {@link TimeZoneDetectorStrategy.Callback} is left to detect whether changing the time * The {@link TimeZoneDetectorStrategyImpl.Callback} is left to detect whether changing the time
* zone is actually necessary. This test proves that the service doesn't assume it knows the * zone is actually necessary. This test proves that the service doesn't assume it knows the
* current setting. * current setting.
*/ */
@@ -441,7 +441,8 @@ public class TimeZoneDetectorStrategyTest {
return new PhoneTimeZoneSuggestion.Builder(PHONE2_ID).build(); return new PhoneTimeZoneSuggestion.Builder(PHONE2_ID).build();
} }
static class FakeTimeZoneDetectorStrategyCallback implements TimeZoneDetectorStrategy.Callback { static class FakeTimeZoneDetectorStrategyCallback
implements TimeZoneDetectorStrategyImpl.Callback {
private boolean mAutoTimeZoneDetectionEnabled; private boolean mAutoTimeZoneDetectionEnabled;
private TestState<String> mTimeZoneId = new TestState<>(); private TestState<String> mTimeZoneId = new TestState<>();
@@ -560,7 +561,7 @@ public class TimeZoneDetectorStrategyTest {
Script autoTimeZoneDetectionEnabled(boolean enabled) { Script autoTimeZoneDetectionEnabled(boolean enabled) {
mFakeTimeZoneDetectorStrategyCallback.setAutoTimeZoneDetectionEnabled(enabled); mFakeTimeZoneDetectorStrategyCallback.setAutoTimeZoneDetectionEnabled(enabled);
mTimeZoneDetectorStrategy.handleAutoTimeZoneDetectionChange(); mTimeZoneDetectorStrategy.handleAutoTimeZoneDetectionChanged();
return this; return this;
} }