blob: 27f7814c5d23727fdb8d4003b0d04513ffbb21c1 [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.timezonedetector.TelephonyTimeZoneSuggestion.MATCH_TYPE_EMULATOR_ZONE_ID;
import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET;
import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_ONLY;
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 static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGH;
import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGHEST;
import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_LOW;
import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_MEDIUM;
import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_NONE;
import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_USAGE_THRESHOLD;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.timezonedetector.ManualTimeZoneSuggestion;
import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
import android.app.timezonedetector.TelephonyTimeZoneSuggestion.MatchType;
import android.app.timezonedetector.TelephonyTimeZoneSuggestion.Quality;
import com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.QualifiedTelephonyTimeZoneSuggestion;
import org.junit.Before;
import org.junit.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
/**
* White-box unit tests for {@link TimeZoneDetectorStrategyImpl}.
*/
public class TimeZoneDetectorStrategyImplTest {
private static final @UserIdInt int USER_ID = 9876;
private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234;
/** 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 int SLOT_INDEX1 = 10000;
private static final int SLOT_INDEX2 = 20000;
// Telephony test cases are ordered so that each successive one is of the same or higher score
// than the previous.
private static final TelephonyTestCase[] TELEPHONY_TEST_CASES = new TelephonyTestCase[] {
newTelephonyTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY,
QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS, TELEPHONY_SCORE_LOW),
newTelephonyTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY,
QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET, TELEPHONY_SCORE_MEDIUM),
newTelephonyTestCase(MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET,
QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET, TELEPHONY_SCORE_MEDIUM),
newTelephonyTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY, QUALITY_SINGLE_ZONE,
TELEPHONY_SCORE_HIGH),
newTelephonyTestCase(MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, QUALITY_SINGLE_ZONE,
TELEPHONY_SCORE_HIGH),
newTelephonyTestCase(MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY,
QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET, TELEPHONY_SCORE_HIGHEST),
newTelephonyTestCase(MATCH_TYPE_EMULATOR_ZONE_ID, QUALITY_SINGLE_ZONE,
TELEPHONY_SCORE_HIGHEST),
};
private static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_DISABLED =
new ConfigurationInternal.Builder(USER_ID)
.setTelephonyDetectionFeatureSupported(true)
.setGeoDetectionFeatureSupported(true)
.setTelephonyFallbackSupported(false)
.setEnhancedMetricsCollectionEnabled(false)
.setUserConfigAllowed(false)
.setAutoDetectionEnabledSetting(false)
.setLocationEnabledSetting(true)
.setGeoDetectionEnabledSetting(false)
.build();
private static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_ENABLED =
new ConfigurationInternal.Builder(USER_ID)
.setTelephonyDetectionFeatureSupported(true)
.setGeoDetectionFeatureSupported(true)
.setTelephonyFallbackSupported(false)
.setEnhancedMetricsCollectionEnabled(false)
.setUserConfigAllowed(false)
.setAutoDetectionEnabledSetting(true)
.setLocationEnabledSetting(true)
.setGeoDetectionEnabledSetting(true)
.build();
private static final ConfigurationInternal CONFIG_AUTO_DETECT_NOT_SUPPORTED =
new ConfigurationInternal.Builder(USER_ID)
.setTelephonyDetectionFeatureSupported(false)
.setGeoDetectionFeatureSupported(false)
.setTelephonyFallbackSupported(false)
.setEnhancedMetricsCollectionEnabled(false)
.setUserConfigAllowed(true)
.setAutoDetectionEnabledSetting(false)
.setLocationEnabledSetting(true)
.setGeoDetectionEnabledSetting(false)
.build();
private static final ConfigurationInternal CONFIG_AUTO_DISABLED_GEO_DISABLED =
new ConfigurationInternal.Builder(USER_ID)
.setTelephonyDetectionFeatureSupported(true)
.setGeoDetectionFeatureSupported(true)
.setTelephonyFallbackSupported(false)
.setEnhancedMetricsCollectionEnabled(false)
.setUserConfigAllowed(true)
.setAutoDetectionEnabledSetting(false)
.setLocationEnabledSetting(true)
.setGeoDetectionEnabledSetting(false)
.build();
private static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_DISABLED =
new ConfigurationInternal.Builder(USER_ID)
.setTelephonyDetectionFeatureSupported(true)
.setGeoDetectionFeatureSupported(true)
.setTelephonyFallbackSupported(false)
.setEnhancedMetricsCollectionEnabled(false)
.setUserConfigAllowed(true)
.setAutoDetectionEnabledSetting(true)
.setLocationEnabledSetting(true)
.setGeoDetectionEnabledSetting(false)
.build();
private static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_ENABLED =
new ConfigurationInternal.Builder(USER_ID)
.setTelephonyDetectionFeatureSupported(true)
.setGeoDetectionFeatureSupported(true)
.setTelephonyFallbackSupported(false)
.setEnhancedMetricsCollectionEnabled(false)
.setUserConfigAllowed(true)
.setAutoDetectionEnabledSetting(true)
.setLocationEnabledSetting(true)
.setGeoDetectionEnabledSetting(true)
.build();
private TimeZoneDetectorStrategyImpl mTimeZoneDetectorStrategy;
private FakeEnvironment mFakeEnvironment;
@Before
public void setUp() {
mFakeEnvironment = new FakeEnvironment();
mFakeEnvironment.initializeConfig(CONFIG_AUTO_DISABLED_GEO_DISABLED);
mTimeZoneDetectorStrategy = new TimeZoneDetectorStrategyImpl(mFakeEnvironment);
}
@Test
public void testEmptyTelephonySuggestions() {
TelephonyTimeZoneSuggestion slotIndex1TimeZoneSuggestion =
createEmptySlotIndex1Suggestion();
TelephonyTimeZoneSuggestion slotIndex2TimeZoneSuggestion =
createEmptySlotIndex2Suggestion();
Script script = new Script()
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
.simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_DISABLED)
.resetConfigurationTracking();
script.simulateTelephonyTimeZoneSuggestion(slotIndex1TimeZoneSuggestion)
.verifyTimeZoneNotChanged();
// Assert internal service state.
QualifiedTelephonyTimeZoneSuggestion expectedSlotIndex1ScoredSuggestion =
new QualifiedTelephonyTimeZoneSuggestion(slotIndex1TimeZoneSuggestion,
TELEPHONY_SCORE_NONE);
assertEquals(expectedSlotIndex1ScoredSuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
assertNull(mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX2));
assertEquals(expectedSlotIndex1ScoredSuggestion,
mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
script.simulateTelephonyTimeZoneSuggestion(slotIndex2TimeZoneSuggestion)
.verifyTimeZoneNotChanged();
// Assert internal service state.
QualifiedTelephonyTimeZoneSuggestion expectedSlotIndex2ScoredSuggestion =
new QualifiedTelephonyTimeZoneSuggestion(slotIndex2TimeZoneSuggestion,
TELEPHONY_SCORE_NONE);
assertEquals(expectedSlotIndex1ScoredSuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
assertEquals(expectedSlotIndex2ScoredSuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX2));
// SlotIndex1 should always beat slotIndex2, all other things being equal.
assertEquals(expectedSlotIndex1ScoredSuggestion,
mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
}
/**
* Telephony suggestions have quality metadata. Ordinarily, low scoring suggestions are not
* used, but this is not true if the device's time zone setting is uninitialized.
*/
@Test
public void testTelephonySuggestionsWhenTimeZoneUninitialized() {
assertTrue(TELEPHONY_SCORE_LOW < TELEPHONY_SCORE_USAGE_THRESHOLD);
assertTrue(TELEPHONY_SCORE_HIGH >= TELEPHONY_SCORE_USAGE_THRESHOLD);
TelephonyTestCase testCase = newTelephonyTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY,
QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS, TELEPHONY_SCORE_LOW);
TelephonyTestCase testCase2 = newTelephonyTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY,
QUALITY_SINGLE_ZONE, TELEPHONY_SCORE_HIGH);
Script script = new Script()
.simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_DISABLED)
.resetConfigurationTracking();
// A low quality suggestions will not be taken: The device time zone setting is left
// uninitialized.
{
TelephonyTimeZoneSuggestion lowQualitySuggestion =
testCase.createSuggestion(SLOT_INDEX1, "America/New_York");
script.simulateTelephonyTimeZoneSuggestion(lowQualitySuggestion)
.verifyTimeZoneNotChanged();
// Assert internal service state.
QualifiedTelephonyTimeZoneSuggestion expectedScoredSuggestion =
new QualifiedTelephonyTimeZoneSuggestion(
lowQualitySuggestion, testCase.expectedScore);
assertEquals(expectedScoredSuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
assertEquals(expectedScoredSuggestion,
mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
}
// A good quality suggestion will be used.
{
TelephonyTimeZoneSuggestion goodQualitySuggestion =
testCase2.createSuggestion(SLOT_INDEX1, "Europe/London");
script.simulateTelephonyTimeZoneSuggestion(goodQualitySuggestion)
.verifyTimeZoneChangedAndReset(goodQualitySuggestion);
// Assert internal service state.
QualifiedTelephonyTimeZoneSuggestion expectedScoredSuggestion =
new QualifiedTelephonyTimeZoneSuggestion(
goodQualitySuggestion, testCase2.expectedScore);
assertEquals(expectedScoredSuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
assertEquals(expectedScoredSuggestion,
mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
}
// A low quality suggestion will be accepted, but not used to set the device time zone.
{
TelephonyTimeZoneSuggestion lowQualitySuggestion2 =
testCase.createSuggestion(SLOT_INDEX1, "America/Los_Angeles");
script.simulateTelephonyTimeZoneSuggestion(lowQualitySuggestion2)
.verifyTimeZoneNotChanged();
// Assert internal service state.
QualifiedTelephonyTimeZoneSuggestion expectedScoredSuggestion =
new QualifiedTelephonyTimeZoneSuggestion(
lowQualitySuggestion2, testCase.expectedScore);
assertEquals(expectedScoredSuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
assertEquals(expectedScoredSuggestion,
mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
}
}
/**
* Confirms that toggling the auto time zone detection setting has the expected behavior when
* the strategy is "opinionated" when using telephony auto detection.
*/
@Test
public void testTogglingAutoDetection_autoTelephony() {
Script script = new Script();
for (TelephonyTestCase testCase : TELEPHONY_TEST_CASES) {
// Start with the device in a known state.
script.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
.simulateConfigurationInternalChange(CONFIG_AUTO_DISABLED_GEO_DISABLED)
.resetConfigurationTracking();
TelephonyTimeZoneSuggestion suggestion =
testCase.createSuggestion(SLOT_INDEX1, "Europe/London");
script.simulateTelephonyTimeZoneSuggestion(suggestion);
// When time zone detection is not enabled, the time zone suggestion will not be set
// regardless of the score.
script.verifyTimeZoneNotChanged();
// Assert internal service state.
QualifiedTelephonyTimeZoneSuggestion expectedScoredSuggestion =
new QualifiedTelephonyTimeZoneSuggestion(suggestion, testCase.expectedScore);
assertEquals(expectedScoredSuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
assertEquals(expectedScoredSuggestion,
mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
// Toggling the time zone setting on should cause the device setting to be set.
script.simulateSetAutoMode(true);
// When time zone detection is already enabled the suggestion (if it scores highly
// enough) should be set immediately.
if (testCase.expectedScore >= TELEPHONY_SCORE_USAGE_THRESHOLD) {
script.verifyTimeZoneChangedAndReset(suggestion);
} else {
script.verifyTimeZoneNotChanged();
}
// Assert internal service state.
assertEquals(expectedScoredSuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
assertEquals(expectedScoredSuggestion,
mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
// Toggling the time zone setting should off should do nothing.
script.simulateSetAutoMode(false)
.verifyTimeZoneNotChanged();
// Assert internal service state.
assertEquals(expectedScoredSuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
assertEquals(expectedScoredSuggestion,
mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
}
}
@Test
public void testTelephonySuggestionsSingleSlotId() {
Script script = new Script()
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
.simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_DISABLED)
.resetConfigurationTracking();
for (TelephonyTestCase testCase : TELEPHONY_TEST_CASES) {
makeSlotIndex1SuggestionAndCheckState(script, testCase);
}
/*
* This is the same test as above but the test cases are in
* reverse order of their expected score. New suggestions always replace previous ones:
* there's effectively no history and so ordering shouldn't make any difference.
*/
// Each test case will have the same or lower score than the last.
List<TelephonyTestCase> descendingCasesByScore = Arrays.asList(TELEPHONY_TEST_CASES);
Collections.reverse(descendingCasesByScore);
for (TelephonyTestCase testCase : descendingCasesByScore) {
makeSlotIndex1SuggestionAndCheckState(script, testCase);
}
}
private void makeSlotIndex1SuggestionAndCheckState(Script script, TelephonyTestCase testCase) {
// Give the next suggestion a different zone from the currently set device time zone;
String currentZoneId = mFakeEnvironment.getDeviceTimeZone();
String suggestionZoneId =
"Europe/London".equals(currentZoneId) ? "Europe/Paris" : "Europe/London";
TelephonyTimeZoneSuggestion zoneSlotIndex1Suggestion =
testCase.createSuggestion(SLOT_INDEX1, suggestionZoneId);
QualifiedTelephonyTimeZoneSuggestion expectedZoneSlotIndex1ScoredSuggestion =
new QualifiedTelephonyTimeZoneSuggestion(
zoneSlotIndex1Suggestion, testCase.expectedScore);
script.simulateTelephonyTimeZoneSuggestion(zoneSlotIndex1Suggestion);
if (testCase.expectedScore >= TELEPHONY_SCORE_USAGE_THRESHOLD) {
script.verifyTimeZoneChangedAndReset(zoneSlotIndex1Suggestion);
} else {
script.verifyTimeZoneNotChanged();
}
// Assert internal service state.
assertEquals(expectedZoneSlotIndex1ScoredSuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
assertEquals(expectedZoneSlotIndex1ScoredSuggestion,
mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
}
/**
* Tries a set of test cases to see if the slotIndex with the lowest numeric value is given
* preference. This test also confirms that the time zone setting would only be set if a
* suggestion is of sufficient quality.
*/
@Test
public void testTelephonySuggestionMultipleSlotIndexSuggestionScoringAndSlotIndexBias() {
String[] zoneIds = { "Europe/London", "Europe/Paris" };
TelephonyTimeZoneSuggestion emptySlotIndex1Suggestion = createEmptySlotIndex1Suggestion();
TelephonyTimeZoneSuggestion emptySlotIndex2Suggestion = createEmptySlotIndex2Suggestion();
QualifiedTelephonyTimeZoneSuggestion expectedEmptySlotIndex1ScoredSuggestion =
new QualifiedTelephonyTimeZoneSuggestion(emptySlotIndex1Suggestion,
TELEPHONY_SCORE_NONE);
QualifiedTelephonyTimeZoneSuggestion expectedEmptySlotIndex2ScoredSuggestion =
new QualifiedTelephonyTimeZoneSuggestion(emptySlotIndex2Suggestion,
TELEPHONY_SCORE_NONE);
Script script = new Script()
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
.simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_DISABLED)
.resetConfigurationTracking()
// Initialize the latest suggestions as empty so we don't need to worry about nulls
// below for the first loop.
.simulateTelephonyTimeZoneSuggestion(emptySlotIndex1Suggestion)
.simulateTelephonyTimeZoneSuggestion(emptySlotIndex2Suggestion)
.resetConfigurationTracking();
for (TelephonyTestCase testCase : TELEPHONY_TEST_CASES) {
TelephonyTimeZoneSuggestion zoneSlotIndex1Suggestion =
testCase.createSuggestion(SLOT_INDEX1, zoneIds[0]);
TelephonyTimeZoneSuggestion zoneSlotIndex2Suggestion =
testCase.createSuggestion(SLOT_INDEX2, zoneIds[1]);
QualifiedTelephonyTimeZoneSuggestion expectedZoneSlotIndex1ScoredSuggestion =
new QualifiedTelephonyTimeZoneSuggestion(zoneSlotIndex1Suggestion,
testCase.expectedScore);
QualifiedTelephonyTimeZoneSuggestion expectedZoneSlotIndex2ScoredSuggestion =
new QualifiedTelephonyTimeZoneSuggestion(zoneSlotIndex2Suggestion,
testCase.expectedScore);
// Start the test by making a suggestion for slotIndex1.
script.simulateTelephonyTimeZoneSuggestion(zoneSlotIndex1Suggestion);
if (testCase.expectedScore >= TELEPHONY_SCORE_USAGE_THRESHOLD) {
script.verifyTimeZoneChangedAndReset(zoneSlotIndex1Suggestion);
} else {
script.verifyTimeZoneNotChanged();
}
// Assert internal service state.
assertEquals(expectedZoneSlotIndex1ScoredSuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
assertEquals(expectedEmptySlotIndex2ScoredSuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX2));
assertEquals(expectedZoneSlotIndex1ScoredSuggestion,
mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
// SlotIndex2 then makes an alternative suggestion with an identical score. SlotIndex1's
// suggestion should still "win" if it is above the required threshold.
script.simulateTelephonyTimeZoneSuggestion(zoneSlotIndex2Suggestion);
script.verifyTimeZoneNotChanged();
// Assert internal service state.
assertEquals(expectedZoneSlotIndex1ScoredSuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
assertEquals(expectedZoneSlotIndex2ScoredSuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX2));
// SlotIndex1 should always beat slotIndex2, all other things being equal.
assertEquals(expectedZoneSlotIndex1ScoredSuggestion,
mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
// Withdrawing slotIndex1's suggestion should leave slotIndex2 as the new winner. Since
// the zoneId is different, the time zone setting should be updated if the score is high
// enough.
script.simulateTelephonyTimeZoneSuggestion(emptySlotIndex1Suggestion);
if (testCase.expectedScore >= TELEPHONY_SCORE_USAGE_THRESHOLD) {
script.verifyTimeZoneChangedAndReset(zoneSlotIndex2Suggestion);
} else {
script.verifyTimeZoneNotChanged();
}
// Assert internal service state.
assertEquals(expectedEmptySlotIndex1ScoredSuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
assertEquals(expectedZoneSlotIndex2ScoredSuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX2));
assertEquals(expectedZoneSlotIndex2ScoredSuggestion,
mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
// Reset the state for the next loop.
script.simulateTelephonyTimeZoneSuggestion(emptySlotIndex2Suggestion)
.verifyTimeZoneNotChanged();
assertEquals(expectedEmptySlotIndex1ScoredSuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
assertEquals(expectedEmptySlotIndex2ScoredSuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX2));
}
}
/**
* The {@link TimeZoneDetectorStrategyImpl.Environment} is left to detect whether changing the
* time zone is actually necessary. This test proves that the strategy doesn't assume it knows
* the current settings.
*/
@Test
public void testTelephonySuggestionStrategyDoesNotAssumeCurrentSetting_autoTelephony() {
Script script = new Script()
.simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_DISABLED)
.resetConfigurationTracking();
TelephonyTestCase testCase = newTelephonyTestCase(
MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, QUALITY_SINGLE_ZONE, TELEPHONY_SCORE_HIGH);
TelephonyTimeZoneSuggestion losAngelesSuggestion =
testCase.createSuggestion(SLOT_INDEX1, "America/Los_Angeles");
TelephonyTimeZoneSuggestion newYorkSuggestion =
testCase.createSuggestion(SLOT_INDEX1, "America/New_York");
// Initialization.
script.simulateTelephonyTimeZoneSuggestion(losAngelesSuggestion)
.verifyTimeZoneChangedAndReset(losAngelesSuggestion);
// Suggest it again - it should not be set because it is already set.
script.simulateTelephonyTimeZoneSuggestion(losAngelesSuggestion)
.verifyTimeZoneNotChanged();
// Toggling time zone detection should set the device time zone only if the current setting
// value is different from the most recent telephony suggestion.
script.simulateSetAutoMode(false)
.verifyTimeZoneNotChanged()
.simulateSetAutoMode(true)
.verifyTimeZoneNotChanged();
// Simulate a user turning auto detection off, a new suggestion being made while auto
// detection is off, and the user turning it on again.
script.simulateSetAutoMode(false)
.simulateTelephonyTimeZoneSuggestion(newYorkSuggestion)
.verifyTimeZoneNotChanged();
// Latest suggestion should be used.
script.simulateSetAutoMode(true)
.verifyTimeZoneChangedAndReset(newYorkSuggestion);
}
@Test
public void testManualSuggestion_unrestricted_autoDetectionEnabled_autoTelephony() {
checkManualSuggestion_unrestricted_autoDetectionEnabled(false /* geoDetectionEnabled */);
}
@Test
public void testManualSuggestion_unrestricted_autoDetectionEnabled_autoGeo() {
checkManualSuggestion_unrestricted_autoDetectionEnabled(true /* geoDetectionEnabled */);
}
private void checkManualSuggestion_unrestricted_autoDetectionEnabled(
boolean geoDetectionEnabled) {
ConfigurationInternal geoTzEnabledConfig =
new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED_GEO_DISABLED)
.setGeoDetectionEnabledSetting(geoDetectionEnabled)
.build();
Script script = new Script()
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
.simulateConfigurationInternalChange(geoTzEnabledConfig)
.resetConfigurationTracking();
// Auto time zone detection is enabled so the manual suggestion should be ignored.
script.simulateManualTimeZoneSuggestion(
USER_ID, createManualSuggestion("Europe/Paris"), false /* expectedResult */)
.verifyTimeZoneNotChanged();
assertNull(mTimeZoneDetectorStrategy.getLatestManualSuggestion());
}
@Test
public void testManualSuggestion_restricted_simulateAutoTimeZoneEnabled() {
Script script = new Script()
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
.simulateConfigurationInternalChange(CONFIG_USER_RESTRICTED_AUTO_ENABLED)
.resetConfigurationTracking();
// User is restricted so the manual suggestion should be ignored.
script.simulateManualTimeZoneSuggestion(
USER_ID, createManualSuggestion("Europe/Paris"), false /* expectedResult */)
.verifyTimeZoneNotChanged();
assertNull(mTimeZoneDetectorStrategy.getLatestManualSuggestion());
}
@Test
public void testManualSuggestion_unrestricted_autoTimeZoneDetectionDisabled() {
Script script = new Script()
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
.simulateConfigurationInternalChange(CONFIG_AUTO_DISABLED_GEO_DISABLED)
.resetConfigurationTracking();
// Auto time zone detection is disabled so the manual suggestion should be used.
ManualTimeZoneSuggestion manualSuggestion = createManualSuggestion("Europe/Paris");
script.simulateManualTimeZoneSuggestion(
USER_ID, manualSuggestion, true /* expectedResult */)
.verifyTimeZoneChangedAndReset(manualSuggestion);
assertEquals(manualSuggestion, mTimeZoneDetectorStrategy.getLatestManualSuggestion());
}
@Test
public void testManualSuggestion_restricted_autoTimeZoneDetectionDisabled() {
Script script = new Script()
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
.simulateConfigurationInternalChange(CONFIG_USER_RESTRICTED_AUTO_DISABLED)
.resetConfigurationTracking();
// Restricted users do not have the capability.
ManualTimeZoneSuggestion manualSuggestion = createManualSuggestion("Europe/Paris");
script.simulateManualTimeZoneSuggestion(
USER_ID, manualSuggestion, false /* expectedResult */)
.verifyTimeZoneNotChanged();
assertNull(mTimeZoneDetectorStrategy.getLatestManualSuggestion());
}
@Test
public void testManualSuggestion_autoDetectNotSupported() {
Script script = new Script()
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
.simulateConfigurationInternalChange(CONFIG_AUTO_DETECT_NOT_SUPPORTED)
.resetConfigurationTracking();
// Unrestricted users have the capability.
ManualTimeZoneSuggestion manualSuggestion = createManualSuggestion("Europe/Paris");
script.simulateManualTimeZoneSuggestion(
USER_ID, manualSuggestion, true /* expectedResult */)
.verifyTimeZoneChangedAndReset(manualSuggestion);
assertEquals(manualSuggestion, mTimeZoneDetectorStrategy.getLatestManualSuggestion());
}
@Test
public void testGeoSuggestion_uncertain() {
Script script = new Script()
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
.simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_ENABLED)
.resetConfigurationTracking();
GeolocationTimeZoneSuggestion uncertainSuggestion = createUncertainGeolocationSuggestion();
script.simulateGeolocationTimeZoneSuggestion(uncertainSuggestion)
.verifyTimeZoneNotChanged();
// Assert internal service state.
assertEquals(uncertainSuggestion,
mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion());
}
@Test
public void testGeoSuggestion_noZones() {
Script script = new Script()
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
.simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_ENABLED)
.resetConfigurationTracking();
GeolocationTimeZoneSuggestion noZonesSuggestion = createCertainGeolocationSuggestion();
script.simulateGeolocationTimeZoneSuggestion(noZonesSuggestion)
.verifyTimeZoneNotChanged();
// Assert internal service state.
assertEquals(noZonesSuggestion, mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion());
}
@Test
public void testGeoSuggestion_oneZone() {
GeolocationTimeZoneSuggestion suggestion =
createCertainGeolocationSuggestion("Europe/London");
Script script = new Script()
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
.simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_ENABLED)
.resetConfigurationTracking();
script.simulateGeolocationTimeZoneSuggestion(suggestion)
.verifyTimeZoneChangedAndReset(suggestion);
// Assert internal service state.
assertEquals(suggestion, mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion());
}
/**
* In the current implementation, the first zone ID is always used unless the device is set to
* one of the other options. This is "stickiness" - the device favors the zone it is currently
* set to until that unambiguously can't be correct.
*/
@Test
public void testGeoSuggestion_multiZone() {
GeolocationTimeZoneSuggestion londonOnlySuggestion =
createCertainGeolocationSuggestion("Europe/London");
GeolocationTimeZoneSuggestion londonOrParisSuggestion =
createCertainGeolocationSuggestion("Europe/Paris", "Europe/London");
GeolocationTimeZoneSuggestion parisOnlySuggestion =
createCertainGeolocationSuggestion("Europe/Paris");
Script script = new Script()
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
.simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_ENABLED)
.resetConfigurationTracking();
script.simulateGeolocationTimeZoneSuggestion(londonOnlySuggestion)
.verifyTimeZoneChangedAndReset(londonOnlySuggestion);
assertEquals(londonOnlySuggestion,
mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion());
// Confirm bias towards the current device zone when there's multiple zones to choose from.
script.simulateGeolocationTimeZoneSuggestion(londonOrParisSuggestion)
.verifyTimeZoneNotChanged();
assertEquals(londonOrParisSuggestion,
mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion());
script.simulateGeolocationTimeZoneSuggestion(parisOnlySuggestion)
.verifyTimeZoneChangedAndReset(parisOnlySuggestion);
assertEquals(parisOnlySuggestion,
mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion());
// Now the suggestion that previously left the device on Europe/London will leave the device
// on Europe/Paris.
script.simulateGeolocationTimeZoneSuggestion(londonOrParisSuggestion)
.verifyTimeZoneNotChanged();
assertEquals(londonOrParisSuggestion,
mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion());
}
/**
* Confirms that changing the geolocation time zone detection enabled setting has the expected
* behavior, i.e. immediately recompute the detected time zone using different signals.
*/
@Test
public void testChangingGeoDetectionEnabled() {
GeolocationTimeZoneSuggestion geolocationSuggestion =
createCertainGeolocationSuggestion("Europe/London");
TelephonyTimeZoneSuggestion telephonySuggestion = createTelephonySuggestion(
SLOT_INDEX1, MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, QUALITY_SINGLE_ZONE,
"Europe/Paris");
Script script = new Script()
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
.simulateConfigurationInternalChange(CONFIG_AUTO_DISABLED_GEO_DISABLED)
.resetConfigurationTracking();
// Add suggestions. Nothing should happen as time zone detection is disabled.
script.simulateGeolocationTimeZoneSuggestion(geolocationSuggestion)
.verifyTimeZoneNotChanged();
assertEquals(geolocationSuggestion,
mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion());
script.simulateTelephonyTimeZoneSuggestion(telephonySuggestion)
.verifyTimeZoneNotChanged();
assertEquals(telephonySuggestion,
mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1).suggestion);
// Toggling the time zone detection enabled setting on should cause the device setting to be
// set from the telephony signal, as we've started with geolocation time zone detection
// disabled.
script.simulateSetAutoMode(true)
.verifyTimeZoneChangedAndReset(telephonySuggestion);
// Changing the detection to enable geo detection will cause the device tz setting to
// change to use the latest geolocation suggestion.
script.simulateSetGeoDetectionEnabled(true)
.verifyTimeZoneChangedAndReset(geolocationSuggestion);
// Changing the detection to disable geo detection should cause the device tz setting to
// change to the telephony suggestion.
script.simulateSetGeoDetectionEnabled(false)
.verifyTimeZoneChangedAndReset(telephonySuggestion);
assertEquals(geolocationSuggestion,
mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion());
}
@Test
public void testTelephonyFallback() {
ConfigurationInternal config = new ConfigurationInternal.Builder(
CONFIG_AUTO_ENABLED_GEO_ENABLED)
.setTelephonyFallbackSupported(true)
.build();
Script script = new Script()
.initializeClock(ARBITRARY_ELAPSED_REALTIME_MILLIS)
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
.simulateConfigurationInternalChange(config)
.resetConfigurationTracking();
// Confirm initial state is as expected.
script.verifyTelephonyFallbackIsEnabled(true)
.verifyTimeZoneNotChanged();
// Although geolocation detection is enabled, telephony fallback should be used initially
// and until a suitable "certain" geolocation suggestion is received.
{
TelephonyTimeZoneSuggestion telephonySuggestion = createTelephonySuggestion(
SLOT_INDEX1, MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, QUALITY_SINGLE_ZONE,
"Europe/Paris");
script.simulateIncrementClock()
.simulateTelephonyTimeZoneSuggestion(telephonySuggestion)
.verifyTimeZoneChangedAndReset(telephonySuggestion)
.verifyTelephonyFallbackIsEnabled(true);
}
// Receiving an "uncertain" geolocation suggestion should have no effect.
{
GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion =
createUncertainGeolocationSuggestion();
script.simulateIncrementClock()
.simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion)
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(true);
}
// Receiving a "certain" geolocation suggestion should disable telephony fallback mode.
{
GeolocationTimeZoneSuggestion geolocationSuggestion =
createCertainGeolocationSuggestion("Europe/London");
script.simulateIncrementClock()
.simulateGeolocationTimeZoneSuggestion(geolocationSuggestion)
.verifyTimeZoneChangedAndReset(geolocationSuggestion)
.verifyTelephonyFallbackIsEnabled(false);
}
// Used to record the last telephony suggestion received, which will be used when fallback
// takes place.
TelephonyTimeZoneSuggestion lastTelephonySuggestion;
// Telephony suggestions should now be ignored and geolocation detection is "in control".
{
TelephonyTimeZoneSuggestion telephonySuggestion = createTelephonySuggestion(
SLOT_INDEX1, MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, QUALITY_SINGLE_ZONE,
"Europe/Berlin");
script.simulateIncrementClock()
.simulateTelephonyTimeZoneSuggestion(telephonySuggestion)
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(false);
lastTelephonySuggestion = telephonySuggestion;
}
// Geolocation suggestions should continue to be used as normal (previous telephony
// suggestions are not used, even when the geolocation suggestion is uncertain).
{
GeolocationTimeZoneSuggestion geolocationSuggestion =
createCertainGeolocationSuggestion("Europe/Rome");
script.simulateIncrementClock()
.simulateGeolocationTimeZoneSuggestion(geolocationSuggestion)
.verifyTimeZoneChangedAndReset(geolocationSuggestion)
.verifyTelephonyFallbackIsEnabled(false);
GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion =
createUncertainGeolocationSuggestion();
script.simulateIncrementClock()
.simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion)
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(false);
script.simulateIncrementClock()
.simulateGeolocationTimeZoneSuggestion(geolocationSuggestion)
// No change needed, device will already be set to Europe/Rome.
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(false);
}
// Enable telephony fallback. Nothing will change, because the geolocation is still certain,
// but fallback will remain enabled.
{
script.simulateIncrementClock()
.simulateEnableTelephonyFallback()
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(true);
}
// Make the geolocation algorithm uncertain.
{
GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion =
createUncertainGeolocationSuggestion();
script.simulateIncrementClock()
.simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion)
.verifyTimeZoneChangedAndReset(lastTelephonySuggestion)
.verifyTelephonyFallbackIsEnabled(true);
}
// Make the geolocation algorithm certain, disabling telephony fallback.
{
GeolocationTimeZoneSuggestion geolocationSuggestion =
createCertainGeolocationSuggestion("Europe/Lisbon");
script.simulateIncrementClock()
.simulateGeolocationTimeZoneSuggestion(geolocationSuggestion)
.verifyTimeZoneChangedAndReset(geolocationSuggestion)
.verifyTelephonyFallbackIsEnabled(false);
}
// Demonstrate what happens when geolocation is uncertain when telephony fallback is
// enabled.
{
GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion =
createUncertainGeolocationSuggestion();
script.simulateIncrementClock()
.simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion)
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(false)
.simulateEnableTelephonyFallback()
.verifyTimeZoneChangedAndReset(lastTelephonySuggestion)
.verifyTelephonyFallbackIsEnabled(true);
}
}
@Test
public void testTelephonyFallback_noTelephonySuggestionToFallBackTo() {
ConfigurationInternal config = new ConfigurationInternal.Builder(
CONFIG_AUTO_ENABLED_GEO_ENABLED)
.setTelephonyFallbackSupported(true)
.build();
Script script = new Script()
.initializeClock(ARBITRARY_ELAPSED_REALTIME_MILLIS)
.initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
.simulateConfigurationInternalChange(config)
.resetConfigurationTracking();
// Confirm initial state is as expected.
script.verifyTelephonyFallbackIsEnabled(true)
.verifyTimeZoneNotChanged();
// Receiving an "uncertain" geolocation suggestion should have no effect.
{
GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion =
createUncertainGeolocationSuggestion();
script.simulateIncrementClock()
.simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion)
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(true);
}
// Make an uncertain geolocation suggestion, there is no telephony suggestion to fall back
// to
{
GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion =
createUncertainGeolocationSuggestion();
script.simulateIncrementClock()
.simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion)
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(true);
}
// Similar to the case above, but force a fallback attempt after making a "certain"
// geolocation suggestion.
// Geolocation suggestions should continue to be used as normal (previous telephony
// suggestions are not used, even when the geolocation suggestion is uncertain).
{
GeolocationTimeZoneSuggestion geolocationSuggestion =
createCertainGeolocationSuggestion("Europe/Rome");
script.simulateIncrementClock()
.simulateGeolocationTimeZoneSuggestion(geolocationSuggestion)
.verifyTimeZoneChangedAndReset(geolocationSuggestion)
.verifyTelephonyFallbackIsEnabled(false);
GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion =
createUncertainGeolocationSuggestion();
script.simulateIncrementClock()
.simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion)
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(false);
script.simulateIncrementClock()
.simulateEnableTelephonyFallback()
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(true);
}
}
@Test
public void testGenerateMetricsState_enhancedMetricsCollection() {
testGenerateMetricsState(true);
}
@Test
public void testGenerateMetricsState_notEnhancedMetricsCollection() {
testGenerateMetricsState(false);
}
private void testGenerateMetricsState(boolean enhancedMetricsCollection) {
ConfigurationInternal expectedInternalConfig =
new ConfigurationInternal.Builder(CONFIG_AUTO_DISABLED_GEO_DISABLED)
.setEnhancedMetricsCollectionEnabled(enhancedMetricsCollection)
.build();
String expectedDeviceTimeZoneId = "InitialZoneId";
Script script = new Script()
.initializeTimeZoneSetting(expectedDeviceTimeZoneId)
.simulateConfigurationInternalChange(expectedInternalConfig)
.resetConfigurationTracking();
assertMetricsState(expectedInternalConfig, expectedDeviceTimeZoneId, null, null,
null, MetricsTimeZoneDetectorState.DETECTION_MODE_MANUAL);
// Make sure the manual suggestion is recorded.
ManualTimeZoneSuggestion manualSuggestion = createManualSuggestion("Zone1");
script.simulateManualTimeZoneSuggestion(USER_ID, manualSuggestion,
true /* expectedResult */)
.verifyTimeZoneChangedAndReset(manualSuggestion);
expectedDeviceTimeZoneId = manualSuggestion.getZoneId();
assertMetricsState(expectedInternalConfig, expectedDeviceTimeZoneId,
manualSuggestion, null, null,
MetricsTimeZoneDetectorState.DETECTION_MODE_MANUAL);
// With time zone auto detection off, telephony and geo suggestions will be recorded.
TelephonyTimeZoneSuggestion telephonySuggestion =
createTelephonySuggestion(0 /* slotIndex */, MATCH_TYPE_NETWORK_COUNTRY_ONLY,
QUALITY_SINGLE_ZONE, "Zone2");
GeolocationTimeZoneSuggestion geolocationTimeZoneSuggestion =
createCertainGeolocationSuggestion("Zone3", "Zone2");
script.simulateTelephonyTimeZoneSuggestion(telephonySuggestion)
.verifyTimeZoneNotChanged()
.simulateGeolocationTimeZoneSuggestion(geolocationTimeZoneSuggestion)
.verifyTimeZoneNotChanged();
assertMetricsState(expectedInternalConfig, expectedDeviceTimeZoneId,
manualSuggestion, telephonySuggestion, geolocationTimeZoneSuggestion,
MetricsTimeZoneDetectorState.DETECTION_MODE_MANUAL);
// Update the config and confirm that the config metrics state updates also.
expectedInternalConfig = new ConfigurationInternal.Builder(expectedInternalConfig)
.setAutoDetectionEnabledSetting(true)
.setGeoDetectionEnabledSetting(true)
.build();
expectedDeviceTimeZoneId = geolocationTimeZoneSuggestion.getZoneIds().get(0);
script.simulateConfigurationInternalChange(expectedInternalConfig)
.verifyTimeZoneChangedAndReset(expectedDeviceTimeZoneId);
assertMetricsState(expectedInternalConfig, expectedDeviceTimeZoneId,
manualSuggestion, telephonySuggestion, geolocationTimeZoneSuggestion,
MetricsTimeZoneDetectorState.DETECTION_MODE_GEO);
}
/**
* Asserts that the information returned by {@link
* TimeZoneDetectorStrategy#generateMetricsState()} matches expectations.
*/
private void assertMetricsState(
ConfigurationInternal expectedInternalConfig,
String expectedDeviceTimeZoneId, ManualTimeZoneSuggestion expectedManualSuggestion,
TelephonyTimeZoneSuggestion expectedTelephonySuggestion,
GeolocationTimeZoneSuggestion expectedGeolocationTimeZoneSuggestion,
int expectedDetectionMode) {
MetricsTimeZoneDetectorState actualState = mTimeZoneDetectorStrategy.generateMetricsState();
// Check the various feature state values are what we expect.
assertFeatureStateMatchesConfig(expectedInternalConfig, actualState, expectedDetectionMode);
OrdinalGenerator<String> tzIdOrdinalGenerator = new OrdinalGenerator<>(Function.identity());
MetricsTimeZoneDetectorState expectedState =
MetricsTimeZoneDetectorState.create(
tzIdOrdinalGenerator, expectedInternalConfig, expectedDeviceTimeZoneId,
expectedManualSuggestion, expectedTelephonySuggestion,
expectedGeolocationTimeZoneSuggestion);
// Rely on MetricsTimeZoneDetectorState.equals() for time zone ID / ID ordinal comparisons.
assertEquals(expectedState, actualState);
}
private static void assertFeatureStateMatchesConfig(ConfigurationInternal config,
MetricsTimeZoneDetectorState actualState, int expectedDetectionMode) {
assertEquals(config.isTelephonyDetectionSupported(),
actualState.isTelephonyDetectionSupported());
assertEquals(config.isGeoDetectionSupported(), actualState.isGeoDetectionSupported());
assertEquals(config.isTelephonyFallbackSupported(),
actualState.isTelephonyTimeZoneFallbackSupported());
assertEquals(config.getAutoDetectionEnabledSetting(),
actualState.getAutoDetectionEnabledSetting());
assertEquals(config.getGeoDetectionEnabledSetting(),
actualState.getGeoDetectionEnabledSetting());
assertEquals(expectedDetectionMode, actualState.getDetectionMode());
}
private static ManualTimeZoneSuggestion createManualSuggestion(String zoneId) {
return new ManualTimeZoneSuggestion(zoneId);
}
private static TelephonyTimeZoneSuggestion createTelephonySuggestion(
int slotIndex, @MatchType int matchType, @Quality int quality, String zoneId) {
return new TelephonyTimeZoneSuggestion.Builder(slotIndex)
.setMatchType(matchType)
.setQuality(quality)
.setZoneId(zoneId)
.build();
}
private static TelephonyTimeZoneSuggestion createEmptySlotIndex1Suggestion() {
return new TelephonyTimeZoneSuggestion.Builder(SLOT_INDEX1).build();
}
private static TelephonyTimeZoneSuggestion createEmptySlotIndex2Suggestion() {
return new TelephonyTimeZoneSuggestion.Builder(SLOT_INDEX2).build();
}
private GeolocationTimeZoneSuggestion createUncertainGeolocationSuggestion() {
return GeolocationTimeZoneSuggestion.createCertainSuggestion(
mFakeEnvironment.elapsedRealtimeMillis(), null);
}
private GeolocationTimeZoneSuggestion createCertainGeolocationSuggestion(
@NonNull String... zoneIds) {
assertNotNull(zoneIds);
GeolocationTimeZoneSuggestion suggestion =
GeolocationTimeZoneSuggestion.createCertainSuggestion(
mFakeEnvironment.elapsedRealtimeMillis(), Arrays.asList(zoneIds));
suggestion.addDebugInfo("Test suggestion");
return suggestion;
}
static class FakeEnvironment implements TimeZoneDetectorStrategyImpl.Environment {
private final TestState<String> mTimeZoneId = new TestState<>();
private ConfigurationInternal mConfigurationInternal;
private @ElapsedRealtimeLong long mElapsedRealtimeMillis;
private ConfigurationChangeListener mConfigurationInternalChangeListener;
void initializeConfig(ConfigurationInternal configurationInternal) {
mConfigurationInternal = configurationInternal;
}
void initializeClock(@ElapsedRealtimeLong long elapsedRealtimeMillis) {
mElapsedRealtimeMillis = elapsedRealtimeMillis;
}
void initializeTimeZoneSetting(String zoneId) {
mTimeZoneId.init(zoneId);
}
void incrementClock() {
mElapsedRealtimeMillis++;
}
@Override
public void setConfigurationInternalChangeListener(ConfigurationChangeListener listener) {
mConfigurationInternalChangeListener = listener;
}
@Override
public ConfigurationInternal getCurrentUserConfigurationInternal() {
return mConfigurationInternal;
}
@Override
public boolean isDeviceTimeZoneInitialized() {
return mTimeZoneId.getLatest() != null;
}
@Override
public String getDeviceTimeZone() {
return mTimeZoneId.getLatest();
}
@Override
public void setDeviceTimeZone(String zoneId) {
mTimeZoneId.set(zoneId);
}
void simulateConfigurationInternalChange(ConfigurationInternal configurationInternal) {
mConfigurationInternal = configurationInternal;
mConfigurationInternalChangeListener.onChange();
}
void assertTimeZoneNotChanged() {
mTimeZoneId.assertHasNotBeenSet();
}
void assertTimeZoneChangedTo(String timeZoneId) {
mTimeZoneId.assertHasBeenSet();
mTimeZoneId.assertChangeCount(1);
mTimeZoneId.assertLatestEquals(timeZoneId);
}
void commitAllChanges() {
mTimeZoneId.commitLatest();
}
@Override
@ElapsedRealtimeLong
public long elapsedRealtimeMillis() {
return mElapsedRealtimeMillis;
}
}
/**
* A "fluent" class allows reuse of code in tests: initialization, simulation and verification
* logic.
*/
private class Script {
Script initializeTimeZoneSetting(String zoneId) {
mFakeEnvironment.initializeTimeZoneSetting(zoneId);
return this;
}
Script initializeClock(long elapsedRealtimeMillis) {
mFakeEnvironment.initializeClock(elapsedRealtimeMillis);
return this;
}
Script simulateIncrementClock() {
mFakeEnvironment.incrementClock();
return this;
}
/**
* Simulates the user / user's configuration changing.
*/
Script simulateConfigurationInternalChange(ConfigurationInternal configurationInternal) {
mFakeEnvironment.simulateConfigurationInternalChange(configurationInternal);
return this;
}
/**
* Simulates automatic time zone detection being set to the specified value.
*/
Script simulateSetAutoMode(boolean autoDetectionEnabled) {
ConfigurationInternal newConfig = new ConfigurationInternal.Builder(
mFakeEnvironment.getCurrentUserConfigurationInternal())
.setAutoDetectionEnabledSetting(autoDetectionEnabled)
.build();
simulateConfigurationInternalChange(newConfig);
return this;
}
/**
* Simulates automatic geolocation time zone detection being set to the specified value.
*/
Script simulateSetGeoDetectionEnabled(boolean geoDetectionEnabled) {
ConfigurationInternal newConfig = new ConfigurationInternal.Builder(
mFakeEnvironment.getCurrentUserConfigurationInternal())
.setGeoDetectionEnabledSetting(geoDetectionEnabled)
.build();
simulateConfigurationInternalChange(newConfig);
return this;
}
/**
* Simulates the time zone detection strategy receiving a geolocation-originated
* suggestion.
*/
Script simulateGeolocationTimeZoneSuggestion(GeolocationTimeZoneSuggestion suggestion) {
mTimeZoneDetectorStrategy.suggestGeolocationTimeZone(suggestion);
return this;
}
/** Simulates the time zone detection strategy receiving a user-originated suggestion. */
Script simulateManualTimeZoneSuggestion(
@UserIdInt int userId, ManualTimeZoneSuggestion manualTimeZoneSuggestion,
boolean expectedResult) {
boolean actualResult = mTimeZoneDetectorStrategy.suggestManualTimeZone(
userId, manualTimeZoneSuggestion);
assertEquals(expectedResult, actualResult);
return this;
}
/**
* Simulates the time zone detection strategy receiving a telephony-originated suggestion.
*/
Script simulateTelephonyTimeZoneSuggestion(TelephonyTimeZoneSuggestion timeZoneSuggestion) {
mTimeZoneDetectorStrategy.suggestTelephonyTimeZone(timeZoneSuggestion);
return this;
}
/**
* Simulates the time zone detection strategty receiving a signal that allows it to do
* telephony fallback.
*/
Script simulateEnableTelephonyFallback() {
mTimeZoneDetectorStrategy.enableTelephonyTimeZoneFallback();
return this;
}
/**
* Confirms that the device's time zone has not been set by previous actions since the test
* state was last reset.
*/
Script verifyTimeZoneNotChanged() {
mFakeEnvironment.assertTimeZoneNotChanged();
return this;
}
/** Verifies the device's time zone has been set and clears change tracking history. */
Script verifyTimeZoneChangedAndReset(String zoneId) {
mFakeEnvironment.assertTimeZoneChangedTo(zoneId);
mFakeEnvironment.commitAllChanges();
return this;
}
Script verifyTimeZoneChangedAndReset(ManualTimeZoneSuggestion suggestion) {
mFakeEnvironment.assertTimeZoneChangedTo(suggestion.getZoneId());
mFakeEnvironment.commitAllChanges();
return this;
}
Script verifyTimeZoneChangedAndReset(TelephonyTimeZoneSuggestion suggestion) {
mFakeEnvironment.assertTimeZoneChangedTo(suggestion.getZoneId());
mFakeEnvironment.commitAllChanges();
return this;
}
Script verifyTimeZoneChangedAndReset(GeolocationTimeZoneSuggestion suggestion) {
assertEquals("Only use this method with unambiguous geo suggestions",
1, suggestion.getZoneIds().size());
mFakeEnvironment.assertTimeZoneChangedTo(suggestion.getZoneIds().get(0));
mFakeEnvironment.commitAllChanges();
return this;
}
/** Verifies the state for telephony fallback. */
Script verifyTelephonyFallbackIsEnabled(boolean expectedEnabled) {
assertEquals(expectedEnabled,
mTimeZoneDetectorStrategy.isTelephonyFallbackEnabledForTests());
return this;
}
Script resetConfigurationTracking() {
mFakeEnvironment.commitAllChanges();
return this;
}
}
private static class TelephonyTestCase {
public final int matchType;
public final int quality;
public final int expectedScore;
TelephonyTestCase(int matchType, int quality, int expectedScore) {
this.matchType = matchType;
this.quality = quality;
this.expectedScore = expectedScore;
}
private TelephonyTimeZoneSuggestion createSuggestion(int slotIndex, String zoneId) {
return new TelephonyTimeZoneSuggestion.Builder(slotIndex)
.setZoneId(zoneId)
.setMatchType(matchType)
.setQuality(quality)
.build();
}
}
private static TelephonyTestCase newTelephonyTestCase(
@MatchType int matchType, @Quality int quality, int expectedScore) {
return new TelephonyTestCase(matchType, quality, expectedScore);
}
}