blob: ab2a88b44a87bf5f2dab3eff5249f4c5ab4d3aeb [file] [log] [blame]
/*
* 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.time.Capabilities.CAPABILITY_POSSESSED;
import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.MATCH_TYPE_EMULATOR_ZONE_ID;
import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY;
import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS;
import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET;
import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.QUALITY_SINGLE_ZONE;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.time.TimeZoneCapabilities;
import android.app.time.TimeZoneCapabilitiesAndConfig;
import android.app.time.TimeZoneConfiguration;
import android.app.timezonedetector.ManualTimeZoneSuggestion;
import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
import android.content.Context;
import android.os.Handler;
import android.util.IndentingPrintWriter;
import android.util.LocalLog;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* The real implementation of {@link TimeZoneDetectorStrategy}.
*
* <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 device configuration / settings
* / system properties. It can be faked for testing.
*
* <p>Note: Because the settings / system properties-derived values can currently be modified
* independently and from different threads (and processes!), their use is prone to race
* conditions.
*/
@VisibleForTesting
public interface Environment {
/**
* Sets a {@link ConfigurationChangeListener} that will be invoked when there are any
* changes that could affect time zone detection. This is invoked during system server
* setup.
*/
void setConfigChangeListener(@NonNull ConfigurationChangeListener listener);
/** Returns the current user at the instant it is called. */
@UserIdInt int getCurrentUserId();
/** Returns the {@link ConfigurationInternal} for the specified user. */
ConfigurationInternal getConfigurationInternal(@UserIdInt int userId);
/**
* 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);
/**
* Stores the configuration properties contained in {@code newConfiguration}.
* All checks about user capabilities must be done by the caller and
* {@link TimeZoneConfiguration#isComplete()} must be {@code true}.
*/
void storeConfiguration(@UserIdInt int userId, TimeZoneConfiguration newConfiguration);
}
private static final String LOG_TAG = TimeZoneDetectorService.TAG;
private static final boolean DBG = false;
/**
* The abstract score for an empty or invalid telephony suggestion.
*
* Used to score telephony suggestions where there is no zone.
*/
@VisibleForTesting
public static final int TELEPHONY_SCORE_NONE = 0;
/**
* The abstract score for a low quality telephony 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 TELEPHONY_SCORE_LOW = 1;
/**
* The abstract score for a medium quality telephony 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 TELEPHONY_SCORE_MEDIUM = 2;
/**
* The abstract score for a high quality telephony 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 TELEPHONY_SCORE_HIGH = 3;
/**
* The abstract score for a highest quality telephony suggestion.
*
* Used for:
* Suggestions that must "win" because they constitute test or emulator zone ID.
*/
@VisibleForTesting
public static final int TELEPHONY_SCORE_HIGHEST = 4;
/**
* The threshold at which telephony suggestions are good enough to use to set the device's time
* zone.
*/
@VisibleForTesting
public static final int TELEPHONY_SCORE_USAGE_THRESHOLD = TELEPHONY_SCORE_MEDIUM;
/**
* The number of suggestions to keep. These are logged in bug reports to assist when debugging
* issues with detection.
*/
private static final int KEEP_SUGGESTION_HISTORY_SIZE = 10;
@NonNull
private final Environment mEnvironment;
@GuardedBy("this")
@NonNull
private List<ConfigurationChangeListener> mConfigChangeListeners = new ArrayList<>();
/**
* A log that records the decisions / decision metadata that affected the device's time zone.
* This is logged in bug reports to assist with debugging issues with detection.
*/
@NonNull
private final LocalLog mTimeZoneChangesLog = new LocalLog(30, false /* useLocalTimestamps */);
/**
* A mapping from slotIndex to a telephony time zone suggestion. We typically expect one or two
* mappings: devices will have a small number of telephony devices and slotIndexes are assumed
* to be stable.
*/
@GuardedBy("this")
private ArrayMapWithHistory<Integer, QualifiedTelephonyTimeZoneSuggestion>
mTelephonySuggestionsBySlotIndex =
new ArrayMapWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE);
/**
* The latest geolocation suggestion received. If the user disabled geolocation time zone
* detection then the latest suggestion is cleared.
*/
@GuardedBy("this")
private ReferenceWithHistory<GeolocationTimeZoneSuggestion> mLatestGeoLocationSuggestion =
new ReferenceWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE);
/**
* The latest manual suggestion received.
*/
@GuardedBy("this")
private ReferenceWithHistory<ManualTimeZoneSuggestion> mLatestManualSuggestion =
new ReferenceWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE);
@GuardedBy("this")
private final List<Dumpable> mDumpables = new ArrayList<>();
/**
* Creates a new instance of {@link TimeZoneDetectorStrategyImpl}.
*/
public static TimeZoneDetectorStrategyImpl create(
@NonNull Context context, @NonNull Handler handler,
@NonNull ServiceConfigAccessor serviceConfigAccessor) {
Environment environment = new EnvironmentImpl(context, handler, serviceConfigAccessor);
return new TimeZoneDetectorStrategyImpl(environment);
}
@VisibleForTesting
public TimeZoneDetectorStrategyImpl(@NonNull Environment environment) {
mEnvironment = Objects.requireNonNull(environment);
mEnvironment.setConfigChangeListener(this::handleConfigChanged);
}
/**
* Adds a listener that allows the strategy to communicate with the surrounding service /
* internal. This must be called before the instance is used.
*/
@Override
public synchronized void addConfigChangeListener(
@NonNull ConfigurationChangeListener listener) {
Objects.requireNonNull(listener);
mConfigChangeListeners.add(listener);
}
@Override
@NonNull
public ConfigurationInternal getConfigurationInternal(@UserIdInt int userId) {
return mEnvironment.getConfigurationInternal(userId);
}
@Override
@NonNull
public synchronized ConfigurationInternal getCurrentUserConfigurationInternal() {
int currentUserId = mEnvironment.getCurrentUserId();
return getConfigurationInternal(currentUserId);
}
@Override
public synchronized boolean updateConfiguration(@UserIdInt int userId,
@NonNull TimeZoneConfiguration requestedConfiguration) {
Objects.requireNonNull(requestedConfiguration);
TimeZoneCapabilitiesAndConfig capabilitiesAndConfig =
getConfigurationInternal(userId).createCapabilitiesAndConfig();
TimeZoneCapabilities capabilities = capabilitiesAndConfig.getCapabilities();
TimeZoneConfiguration oldConfiguration = capabilitiesAndConfig.getConfiguration();
final TimeZoneConfiguration newConfiguration =
capabilities.tryApplyConfigChanges(oldConfiguration, requestedConfiguration);
if (newConfiguration == null) {
// The changes could not be made because the user's capabilities do not allow it.
return false;
}
// Store the configuration / notify as needed. This will cause the mEnvironment to invoke
// handleConfigChanged() asynchronously.
mEnvironment.storeConfiguration(userId, newConfiguration);
String logMsg = "Configuration changed:"
+ " oldConfiguration=" + oldConfiguration
+ ", newConfiguration=" + newConfiguration;
mTimeZoneChangesLog.log(logMsg);
if (DBG) {
Slog.d(LOG_TAG, logMsg);
}
return true;
}
@Override
public synchronized void suggestGeolocationTimeZone(
@NonNull GeolocationTimeZoneSuggestion suggestion) {
int currentUserId = mEnvironment.getCurrentUserId();
ConfigurationInternal currentUserConfig =
mEnvironment.getConfigurationInternal(currentUserId);
if (DBG) {
Slog.d(LOG_TAG, "Geolocation suggestion received."
+ " currentUserConfig=" + currentUserConfig
+ " newSuggestion=" + suggestion);
}
Objects.requireNonNull(suggestion);
if (currentUserConfig.getGeoDetectionEnabledBehavior()) {
// Only store a geolocation suggestion if geolocation detection is currently enabled.
// See also clearGeolocationSuggestionIfNeeded().
mLatestGeoLocationSuggestion.set(suggestion);
// Now perform auto time zone detection. The new suggestion may be used to modify the
// time zone setting.
String reason = "New geolocation time zone suggested. suggestion=" + suggestion;
doAutoTimeZoneDetection(currentUserConfig, reason);
}
}
@Override
public synchronized boolean suggestManualTimeZone(
@UserIdInt int userId, @NonNull ManualTimeZoneSuggestion suggestion) {
int currentUserId = mEnvironment.getCurrentUserId();
if (userId != currentUserId) {
Slog.w(LOG_TAG, "Manual suggestion received but user != current user, userId=" + userId
+ " suggestion=" + suggestion);
// Only listen to changes from the current user.
return false;
}
Objects.requireNonNull(suggestion);
String timeZoneId = suggestion.getZoneId();
String cause = "Manual time suggestion received: suggestion=" + suggestion;
TimeZoneCapabilitiesAndConfig capabilitiesAndConfig =
getConfigurationInternal(userId).createCapabilitiesAndConfig();
TimeZoneCapabilities capabilities = capabilitiesAndConfig.getCapabilities();
if (capabilities.getSuggestManualTimeZoneCapability() != CAPABILITY_POSSESSED) {
Slog.i(LOG_TAG, "User does not have the capability needed to set the time zone manually"
+ ", capabilities=" + capabilities
+ ", timeZoneId=" + timeZoneId
+ ", cause=" + cause);
return false;
}
// Record the manual suggestion for debugging / metrics (but only if manual detection is
// currently enabled).
// Note: This is not used to set the device back to a previous manual suggestion if the user
// later disables automatic time zone detection.
mLatestManualSuggestion.set(suggestion);
setDeviceTimeZoneIfRequired(timeZoneId, cause);
return true;
}
@Override
public synchronized void suggestTelephonyTimeZone(
@NonNull TelephonyTimeZoneSuggestion suggestion) {
int currentUserId = mEnvironment.getCurrentUserId();
ConfigurationInternal currentUserConfig =
mEnvironment.getConfigurationInternal(currentUserId);
if (DBG) {
Slog.d(LOG_TAG, "Telephony suggestion received. currentUserConfig=" + currentUserConfig
+ " newSuggestion=" + suggestion);
}
Objects.requireNonNull(suggestion);
// Score the suggestion.
int score = scoreTelephonySuggestion(suggestion);
QualifiedTelephonyTimeZoneSuggestion scoredSuggestion =
new QualifiedTelephonyTimeZoneSuggestion(suggestion, score);
// Store the suggestion against the correct slotIndex.
mTelephonySuggestionsBySlotIndex.put(suggestion.getSlotIndex(), scoredSuggestion);
// Now perform auto time zone detection. The new suggestion may be used to modify the time
// zone setting.
if (!currentUserConfig.getGeoDetectionEnabledBehavior()) {
String reason = "New telephony time zone suggested. suggestion=" + suggestion;
doAutoTimeZoneDetection(currentUserConfig, reason);
}
}
@Override
@NonNull
public synchronized MetricsTimeZoneDetectorState generateMetricsState() {
int currentUserId = mEnvironment.getCurrentUserId();
// Just capture one telephony suggestion: the one that would be used right now if telephony
// detection is in use.
QualifiedTelephonyTimeZoneSuggestion bestQualifiedTelephonySuggestion =
findBestTelephonySuggestion();
TelephonyTimeZoneSuggestion telephonySuggestion =
bestQualifiedTelephonySuggestion == null
? null : bestQualifiedTelephonySuggestion.suggestion;
// A new generator is created each time: we don't want / require consistency.
OrdinalGenerator<String> tzIdOrdinalGenerator =
new OrdinalGenerator<>(new TimeZoneCanonicalizer());
return MetricsTimeZoneDetectorState.create(
tzIdOrdinalGenerator,
getConfigurationInternal(currentUserId),
mEnvironment.getDeviceTimeZone(),
getLatestManualSuggestion(),
telephonySuggestion,
getLatestGeolocationSuggestion());
}
private static int scoreTelephonySuggestion(@NonNull TelephonyTimeZoneSuggestion suggestion) {
int score;
if (suggestion.getZoneId() == null) {
score = TELEPHONY_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 = TELEPHONY_SCORE_HIGHEST;
} else if (suggestion.getQuality() == QUALITY_SINGLE_ZONE) {
score = TELEPHONY_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 = TELEPHONY_SCORE_MEDIUM;
} else if (suggestion.getQuality() == QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS) {
// The suggestion has a good chance of being wrong.
score = TELEPHONY_SCORE_LOW;
} else {
throw new AssertionError();
}
return score;
}
/**
* Performs automatic time zone detection.
*/
@GuardedBy("this")
private void doAutoTimeZoneDetection(
@NonNull ConfigurationInternal currentUserConfig, @NonNull String detectionReason) {
if (!currentUserConfig.getAutoDetectionEnabledBehavior()) {
// Avoid doing unnecessary work.
return;
}
// Use the right suggestions based on the current configuration.
if (currentUserConfig.getGeoDetectionEnabledBehavior()) {
doGeolocationTimeZoneDetection(detectionReason);
} else {
doTelephonyTimeZoneDetection(detectionReason);
}
}
/**
* Detects the time zone using the latest available geolocation time zone suggestion, if one is
* available. The outcome can be that this strategy becomes / remains un-opinionated and nothing
* is set.
*/
@GuardedBy("this")
private void doGeolocationTimeZoneDetection(@NonNull String detectionReason) {
GeolocationTimeZoneSuggestion latestGeolocationSuggestion =
mLatestGeoLocationSuggestion.get();
if (latestGeolocationSuggestion == null) {
return;
}
List<String> zoneIds = latestGeolocationSuggestion.getZoneIds();
if (zoneIds == null || zoneIds.isEmpty()) {
// This means the client has become uncertain about the time zone or it is certain there
// is no known zone. In either case we must leave the existing time zone setting as it
// is.
return;
}
// GeolocationTimeZoneSuggestion has no measure of quality. We assume all suggestions are
// reliable.
String zoneId;
// Introduce bias towards the device's current zone when there are multiple zone suggested.
String deviceTimeZone = mEnvironment.getDeviceTimeZone();
if (zoneIds.contains(deviceTimeZone)) {
if (DBG) {
Slog.d(LOG_TAG,
"Geo tz suggestion contains current device time zone. Applying bias.");
}
zoneId = deviceTimeZone;
} else {
zoneId = zoneIds.get(0);
}
setDeviceTimeZoneIfRequired(zoneId, detectionReason);
}
/**
* Detects the time zone using the latest available telephony time zone suggestions.
* Finds the best available time zone suggestion from all slotIndexes. 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 doTelephonyTimeZoneDetection(@NonNull String detectionReason) {
QualifiedTelephonyTimeZoneSuggestion bestTelephonySuggestion =
findBestTelephonySuggestion();
// Work out what to do with the best suggestion.
if (bestTelephonySuggestion == null) {
// There is no telephony suggestion available at all. Become un-opinionated.
if (DBG) {
Slog.d(LOG_TAG, "Could not determine time zone: No best telephony suggestion."
+ " detectionReason=" + detectionReason);
}
return;
}
boolean suggestionGoodEnough =
bestTelephonySuggestion.score >= TELEPHONY_SCORE_USAGE_THRESHOLD;
if (!suggestionGoodEnough) {
if (DBG) {
Slog.d(LOG_TAG, "Best suggestion not good enough."
+ " bestTelephonySuggestion=" + bestTelephonySuggestion
+ ", detectionReason=" + detectionReason);
}
return;
}
// Paranoia: Every suggestion above the SCORE_USAGE_THRESHOLD should have a non-null time
// zone ID.
String zoneId = bestTelephonySuggestion.suggestion.getZoneId();
if (zoneId == null) {
Slog.w(LOG_TAG, "Empty zone suggestion scored higher than expected. This is an error:"
+ " bestTelephonySuggestion=" + bestTelephonySuggestion
+ " detectionReason=" + detectionReason);
return;
}
String cause = "Found good suggestion."
+ ", bestTelephonySuggestion=" + bestTelephonySuggestion
+ ", detectionReason=" + detectionReason;
setDeviceTimeZoneIfRequired(zoneId, cause);
}
@GuardedBy("this")
private void setDeviceTimeZoneIfRequired(@NonNull String newZoneId, @NonNull String cause) {
String currentZoneId = mEnvironment.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 newZoneId."
+ ", newZoneId=" + newZoneId
+ ", cause=" + cause);
}
return;
}
mEnvironment.setDeviceTimeZone(newZoneId);
String msg = "Set device time zone."
+ ", currentZoneId=" + currentZoneId
+ ", newZoneId=" + newZoneId
+ ", cause=" + cause;
if (DBG) {
Slog.d(LOG_TAG, msg);
}
mTimeZoneChangesLog.log(msg);
}
@GuardedBy("this")
@Nullable
private QualifiedTelephonyTimeZoneSuggestion findBestTelephonySuggestion() {
QualifiedTelephonyTimeZoneSuggestion bestSuggestion = null;
// Iterate over the latest QualifiedTelephonyTimeZoneSuggestion objects received for each
// slotIndex 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 < mTelephonySuggestionsBySlotIndex.size(); i++) {
QualifiedTelephonyTimeZoneSuggestion candidateSuggestion =
mTelephonySuggestionsBySlotIndex.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 telephony suggestion. Not intended for general use: it is used
* during tests to check strategy behavior.
*/
@VisibleForTesting
@Nullable
public synchronized QualifiedTelephonyTimeZoneSuggestion findBestTelephonySuggestionForTests() {
return findBestTelephonySuggestion();
}
private synchronized void handleConfigChanged() {
if (DBG) {
Slog.d(LOG_TAG, "handleConfigChanged()");
}
clearGeolocationSuggestionIfNeeded();
for (ConfigurationChangeListener listener : mConfigChangeListeners) {
listener.onChange();
}
}
@GuardedBy("this")
private void clearGeolocationSuggestionIfNeeded() {
// This method is called whenever the user changes or the config for any user changes. We
// don't know what happened, so we capture the current user's config, check to see if we
// need to clear state associated with a previous user, and rerun detection.
int currentUserId = mEnvironment.getCurrentUserId();
ConfigurationInternal currentUserConfig =
mEnvironment.getConfigurationInternal(currentUserId);
GeolocationTimeZoneSuggestion latestGeoLocationSuggestion =
mLatestGeoLocationSuggestion.get();
if (latestGeoLocationSuggestion != null
&& !currentUserConfig.getGeoDetectionEnabledBehavior()) {
// The current user's config has geodetection disabled, so clear the latest suggestion.
// This is done to ensure we only ever keep a geolocation suggestion if the user has
// said it is ok to do so.
mLatestGeoLocationSuggestion.set(null);
mTimeZoneChangesLog.log(
"clearGeolocationSuggestionIfNeeded: Cleared latest Geolocation suggestion.");
}
doAutoTimeZoneDetection(currentUserConfig, "clearGeolocationSuggestionIfNeeded()");
}
@Override
public synchronized void addDumpable(@NonNull Dumpable dumpable) {
mDumpables.add(dumpable);
}
/**
* Dumps internal state such as field values.
*/
@Override
public synchronized void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) {
ipw.println("TimeZoneDetectorStrategy:");
ipw.increaseIndent(); // level 1
int currentUserId = mEnvironment.getCurrentUserId();
ipw.println("mEnvironment.getCurrentUserId()=" + currentUserId);
ConfigurationInternal configuration = mEnvironment.getConfigurationInternal(currentUserId);
ipw.println("mEnvironment.getConfiguration(currentUserId)=" + configuration);
ipw.println("[Capabilities=" + configuration.createCapabilitiesAndConfig() + "]");
ipw.println("mEnvironment.isDeviceTimeZoneInitialized()="
+ mEnvironment.isDeviceTimeZoneInitialized());
ipw.println("mEnvironment.getDeviceTimeZone()=" + mEnvironment.getDeviceTimeZone());
ipw.println("Time zone change log:");
ipw.increaseIndent(); // level 2
mTimeZoneChangesLog.dump(ipw);
ipw.decreaseIndent(); // level 2
ipw.println("Manual suggestion history:");
ipw.increaseIndent(); // level 2
mLatestManualSuggestion.dump(ipw);
ipw.decreaseIndent(); // level 2
ipw.println("Geolocation suggestion history:");
ipw.increaseIndent(); // level 2
mLatestGeoLocationSuggestion.dump(ipw);
ipw.decreaseIndent(); // level 2
ipw.println("Telephony suggestion history:");
ipw.increaseIndent(); // level 2
mTelephonySuggestionsBySlotIndex.dump(ipw);
ipw.decreaseIndent(); // level 2
ipw.decreaseIndent(); // level 1
for (Dumpable dumpable : mDumpables) {
dumpable.dump(ipw, args);
}
}
/**
* A method used to inspect strategy state during tests. Not intended for general use.
*/
@VisibleForTesting
public synchronized ManualTimeZoneSuggestion getLatestManualSuggestion() {
return mLatestManualSuggestion.get();
}
/**
* A method used to inspect strategy state during tests. Not intended for general use.
*/
@VisibleForTesting
public synchronized QualifiedTelephonyTimeZoneSuggestion getLatestTelephonySuggestion(
int slotIndex) {
return mTelephonySuggestionsBySlotIndex.get(slotIndex);
}
/**
* A method used to inspect strategy state during tests. Not intended for general use.
*/
@VisibleForTesting
public synchronized GeolocationTimeZoneSuggestion getLatestGeolocationSuggestion() {
return mLatestGeoLocationSuggestion.get();
}
/**
* A {@link TelephonyTimeZoneSuggestion} with additional qualifying metadata.
*/
@VisibleForTesting
public static final class QualifiedTelephonyTimeZoneSuggestion {
@VisibleForTesting
public final TelephonyTimeZoneSuggestion 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 QualifiedTelephonyTimeZoneSuggestion(
TelephonyTimeZoneSuggestion 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;
}
QualifiedTelephonyTimeZoneSuggestion that = (QualifiedTelephonyTimeZoneSuggestion) o;
return score == that.score
&& suggestion.equals(that.suggestion);
}
@Override
public int hashCode() {
return Objects.hash(score, suggestion);
}
@Override
public String toString() {
return "QualifiedTelephonyTimeZoneSuggestion{"
+ "suggestion=" + suggestion
+ ", score=" + score
+ '}';
}
}
}