blob: 270436d65614d0c2633ac1e070c1d87be8178d74 [file] [log] [blame]
Neil Fuller3352cfc2019-11-07 15:35:05 +00001/*
2 * Copyright 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.timezonedetector;
18
19import static android.app.timezonedetector.PhoneTimeZoneSuggestion.MATCH_TYPE_EMULATOR_ZONE_ID;
20import static android.app.timezonedetector.PhoneTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET;
21import static android.app.timezonedetector.PhoneTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_ONLY;
22import static android.app.timezonedetector.PhoneTimeZoneSuggestion.MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY;
23import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS;
24import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET;
25import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_SINGLE_ZONE;
26
Neil Fuller2c6d5102019-11-29 09:02:39 +000027import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_HIGH;
28import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_HIGHEST;
29import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_LOW;
30import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_MEDIUM;
31import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_NONE;
32import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_USAGE_THRESHOLD;
Neil Fuller3352cfc2019-11-07 15:35:05 +000033
34import static org.junit.Assert.assertEquals;
35import static org.junit.Assert.assertFalse;
36import static org.junit.Assert.assertNull;
37import static org.junit.Assert.assertTrue;
38
Neil Fuller2c6d5102019-11-29 09:02:39 +000039import android.app.timezonedetector.ManualTimeZoneSuggestion;
Neil Fuller3352cfc2019-11-07 15:35:05 +000040import android.app.timezonedetector.PhoneTimeZoneSuggestion;
41import android.app.timezonedetector.PhoneTimeZoneSuggestion.MatchType;
42import android.app.timezonedetector.PhoneTimeZoneSuggestion.Quality;
43
44import com.android.server.timezonedetector.TimeZoneDetectorStrategy.QualifiedPhoneTimeZoneSuggestion;
45
46import org.junit.Before;
47import org.junit.Test;
48
49import java.util.ArrayList;
50import java.util.Arrays;
51import java.util.Collections;
52import java.util.LinkedList;
Neil Fuller2c6d5102019-11-29 09:02:39 +000053import java.util.Objects;
Neil Fuller3352cfc2019-11-07 15:35:05 +000054
55/**
56 * White-box unit tests for {@link TimeZoneDetectorStrategy}.
57 */
58public class TimeZoneDetectorStrategyTest {
59
60 /** A time zone used for initialization that does not occur elsewhere in tests. */
61 private static final String ARBITRARY_TIME_ZONE_ID = "Etc/UTC";
62 private static final int PHONE1_ID = 10000;
63 private static final int PHONE2_ID = 20000;
64
65 // Suggestion test cases are ordered so that each successive one is of the same or higher score
66 // than the previous.
67 private static final SuggestionTestCase[] TEST_CASES = new SuggestionTestCase[] {
68 newTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY,
Neil Fuller2c6d5102019-11-29 09:02:39 +000069 QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS, PHONE_SCORE_LOW),
Neil Fuller3352cfc2019-11-07 15:35:05 +000070 newTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY, QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET,
Neil Fuller2c6d5102019-11-29 09:02:39 +000071 PHONE_SCORE_MEDIUM),
Neil Fuller3352cfc2019-11-07 15:35:05 +000072 newTestCase(MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET,
Neil Fuller2c6d5102019-11-29 09:02:39 +000073 QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET, PHONE_SCORE_MEDIUM),
74 newTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY, QUALITY_SINGLE_ZONE, PHONE_SCORE_HIGH),
75 newTestCase(MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, QUALITY_SINGLE_ZONE,
76 PHONE_SCORE_HIGH),
Neil Fuller3352cfc2019-11-07 15:35:05 +000077 newTestCase(MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY,
Neil Fuller2c6d5102019-11-29 09:02:39 +000078 QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET, PHONE_SCORE_HIGHEST),
79 newTestCase(MATCH_TYPE_EMULATOR_ZONE_ID, QUALITY_SINGLE_ZONE, PHONE_SCORE_HIGHEST),
Neil Fuller3352cfc2019-11-07 15:35:05 +000080 };
81
82 private TimeZoneDetectorStrategy mTimeZoneDetectorStrategy;
83 private FakeTimeZoneDetectorStrategyCallback mFakeTimeZoneDetectorStrategyCallback;
84
85 @Before
86 public void setUp() {
87 mFakeTimeZoneDetectorStrategyCallback = new FakeTimeZoneDetectorStrategyCallback();
88 mTimeZoneDetectorStrategy =
89 new TimeZoneDetectorStrategy(mFakeTimeZoneDetectorStrategyCallback);
90 }
91
92 @Test
Neil Fuller2c6d5102019-11-29 09:02:39 +000093 public void testEmptyPhoneSuggestions() {
Neil Fuller3352cfc2019-11-07 15:35:05 +000094 PhoneTimeZoneSuggestion phone1TimeZoneSuggestion = createEmptyPhone1Suggestion();
95 PhoneTimeZoneSuggestion phone2TimeZoneSuggestion = createEmptyPhone2Suggestion();
96 Script script = new Script()
Neil Fuller2c6d5102019-11-29 09:02:39 +000097 .initializeAutoTimeZoneDetection(true)
Neil Fuller3352cfc2019-11-07 15:35:05 +000098 .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID);
99
100 script.suggestPhoneTimeZone(phone1TimeZoneSuggestion)
101 .verifyTimeZoneNotSet();
102
103 // Assert internal service state.
104 QualifiedPhoneTimeZoneSuggestion expectedPhone1ScoredSuggestion =
Neil Fuller2c6d5102019-11-29 09:02:39 +0000105 new QualifiedPhoneTimeZoneSuggestion(phone1TimeZoneSuggestion, PHONE_SCORE_NONE);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000106 assertEquals(expectedPhone1ScoredSuggestion,
107 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
108 assertNull(mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE2_ID));
109 assertEquals(expectedPhone1ScoredSuggestion,
Neil Fuller2c6d5102019-11-29 09:02:39 +0000110 mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
Neil Fuller3352cfc2019-11-07 15:35:05 +0000111
112 script.suggestPhoneTimeZone(phone2TimeZoneSuggestion)
113 .verifyTimeZoneNotSet();
114
115 // Assert internal service state.
116 QualifiedPhoneTimeZoneSuggestion expectedPhone2ScoredSuggestion =
Neil Fuller2c6d5102019-11-29 09:02:39 +0000117 new QualifiedPhoneTimeZoneSuggestion(phone2TimeZoneSuggestion, PHONE_SCORE_NONE);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000118 assertEquals(expectedPhone1ScoredSuggestion,
119 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
120 assertEquals(expectedPhone2ScoredSuggestion,
121 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE2_ID));
122 // Phone 1 should always beat phone 2, all other things being equal.
123 assertEquals(expectedPhone1ScoredSuggestion,
Neil Fuller2c6d5102019-11-29 09:02:39 +0000124 mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
Neil Fuller3352cfc2019-11-07 15:35:05 +0000125 }
126
127 @Test
Neil Fuller2c6d5102019-11-29 09:02:39 +0000128 public void testFirstPlausiblePhoneSuggestionAcceptedWhenTimeZoneUninitialized() {
Neil Fuller3352cfc2019-11-07 15:35:05 +0000129 SuggestionTestCase testCase = newTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY,
Neil Fuller2c6d5102019-11-29 09:02:39 +0000130 QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS, PHONE_SCORE_LOW);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000131 PhoneTimeZoneSuggestion lowQualitySuggestion =
132 testCase.createSuggestion(PHONE1_ID, "America/New_York");
133
134 // The device time zone setting is left uninitialized.
135 Script script = new Script()
Neil Fuller2c6d5102019-11-29 09:02:39 +0000136 .initializeAutoTimeZoneDetection(true);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000137
138 // The very first suggestion will be taken.
139 script.suggestPhoneTimeZone(lowQualitySuggestion)
140 .verifyTimeZoneSetAndReset(lowQualitySuggestion);
141
142 // Assert internal service state.
143 QualifiedPhoneTimeZoneSuggestion expectedScoredSuggestion =
144 new QualifiedPhoneTimeZoneSuggestion(lowQualitySuggestion, testCase.expectedScore);
145 assertEquals(expectedScoredSuggestion,
146 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
147 assertEquals(expectedScoredSuggestion,
Neil Fuller2c6d5102019-11-29 09:02:39 +0000148 mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
Neil Fuller3352cfc2019-11-07 15:35:05 +0000149
150 // Another low quality suggestion will be ignored now that the setting is initialized.
151 PhoneTimeZoneSuggestion lowQualitySuggestion2 =
152 testCase.createSuggestion(PHONE1_ID, "America/Los_Angeles");
153 script.suggestPhoneTimeZone(lowQualitySuggestion2)
154 .verifyTimeZoneNotSet();
155
156 // Assert internal service state.
157 QualifiedPhoneTimeZoneSuggestion expectedScoredSuggestion2 =
158 new QualifiedPhoneTimeZoneSuggestion(lowQualitySuggestion2, testCase.expectedScore);
159 assertEquals(expectedScoredSuggestion2,
160 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
161 assertEquals(expectedScoredSuggestion2,
Neil Fuller2c6d5102019-11-29 09:02:39 +0000162 mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
Neil Fuller3352cfc2019-11-07 15:35:05 +0000163 }
164
165 /**
166 * Confirms that toggling the auto time zone detection setting has the expected behavior when
167 * the strategy is "opinionated".
168 */
169 @Test
Neil Fuller2c6d5102019-11-29 09:02:39 +0000170 public void testTogglingAutoTimeZoneDetection() {
Neil Fuller3352cfc2019-11-07 15:35:05 +0000171 Script script = new Script();
172
173 for (SuggestionTestCase testCase : TEST_CASES) {
174 // Start with the device in a known state.
Neil Fuller2c6d5102019-11-29 09:02:39 +0000175 script.initializeAutoTimeZoneDetection(false)
Neil Fuller3352cfc2019-11-07 15:35:05 +0000176 .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID);
177
178 PhoneTimeZoneSuggestion suggestion =
179 testCase.createSuggestion(PHONE1_ID, "Europe/London");
180 script.suggestPhoneTimeZone(suggestion);
181
182 // When time zone detection is not enabled, the time zone suggestion will not be set
183 // regardless of the score.
184 script.verifyTimeZoneNotSet();
185
186 // Assert internal service state.
187 QualifiedPhoneTimeZoneSuggestion expectedScoredSuggestion =
188 new QualifiedPhoneTimeZoneSuggestion(suggestion, testCase.expectedScore);
189 assertEquals(expectedScoredSuggestion,
190 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
191 assertEquals(expectedScoredSuggestion,
Neil Fuller2c6d5102019-11-29 09:02:39 +0000192 mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
Neil Fuller3352cfc2019-11-07 15:35:05 +0000193
194 // Toggling the time zone setting on should cause the device setting to be set.
Neil Fuller2c6d5102019-11-29 09:02:39 +0000195 script.autoTimeZoneDetectionEnabled(true);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000196
197 // When time zone detection is already enabled the suggestion (if it scores highly
198 // enough) should be set immediately.
Neil Fuller2c6d5102019-11-29 09:02:39 +0000199 if (testCase.expectedScore >= PHONE_SCORE_USAGE_THRESHOLD) {
Neil Fuller3352cfc2019-11-07 15:35:05 +0000200 script.verifyTimeZoneSetAndReset(suggestion);
201 } else {
202 script.verifyTimeZoneNotSet();
203 }
204
205 // Assert internal service state.
206 assertEquals(expectedScoredSuggestion,
207 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
208 assertEquals(expectedScoredSuggestion,
Neil Fuller2c6d5102019-11-29 09:02:39 +0000209 mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
Neil Fuller3352cfc2019-11-07 15:35:05 +0000210
211 // Toggling the time zone setting should off should do nothing.
Neil Fuller2c6d5102019-11-29 09:02:39 +0000212 script.autoTimeZoneDetectionEnabled(false)
Neil Fuller3352cfc2019-11-07 15:35:05 +0000213 .verifyTimeZoneNotSet();
214
215 // Assert internal service state.
216 assertEquals(expectedScoredSuggestion,
217 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
218 assertEquals(expectedScoredSuggestion,
Neil Fuller2c6d5102019-11-29 09:02:39 +0000219 mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
Neil Fuller3352cfc2019-11-07 15:35:05 +0000220 }
221 }
222
223 @Test
Neil Fuller2c6d5102019-11-29 09:02:39 +0000224 public void testPhoneSuggestionsSinglePhone() {
Neil Fuller3352cfc2019-11-07 15:35:05 +0000225 Script script = new Script()
Neil Fuller2c6d5102019-11-29 09:02:39 +0000226 .initializeAutoTimeZoneDetection(true)
Neil Fuller3352cfc2019-11-07 15:35:05 +0000227 .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID);
228
229 for (SuggestionTestCase testCase : TEST_CASES) {
230 makePhone1SuggestionAndCheckState(script, testCase);
231 }
232
233 /*
234 * This is the same test as above but the test cases are in
235 * reverse order of their expected score. New suggestions always replace previous ones:
236 * there's effectively no history and so ordering shouldn't make any difference.
237 */
238
239 // Each test case will have the same or lower score than the last.
240 ArrayList<SuggestionTestCase> descendingCasesByScore =
241 new ArrayList<>(Arrays.asList(TEST_CASES));
242 Collections.reverse(descendingCasesByScore);
243
244 for (SuggestionTestCase testCase : descendingCasesByScore) {
245 makePhone1SuggestionAndCheckState(script, testCase);
246 }
247 }
248
249 private void makePhone1SuggestionAndCheckState(Script script, SuggestionTestCase testCase) {
250 // Give the next suggestion a different zone from the currently set device time zone;
251 String currentZoneId = mFakeTimeZoneDetectorStrategyCallback.getDeviceTimeZone();
252 String suggestionZoneId =
253 "Europe/London".equals(currentZoneId) ? "Europe/Paris" : "Europe/London";
254 PhoneTimeZoneSuggestion zonePhone1Suggestion =
255 testCase.createSuggestion(PHONE1_ID, suggestionZoneId);
256 QualifiedPhoneTimeZoneSuggestion expectedZonePhone1ScoredSuggestion =
257 new QualifiedPhoneTimeZoneSuggestion(zonePhone1Suggestion, testCase.expectedScore);
258
259 script.suggestPhoneTimeZone(zonePhone1Suggestion);
Neil Fuller2c6d5102019-11-29 09:02:39 +0000260 if (testCase.expectedScore >= PHONE_SCORE_USAGE_THRESHOLD) {
Neil Fuller3352cfc2019-11-07 15:35:05 +0000261 script.verifyTimeZoneSetAndReset(zonePhone1Suggestion);
262 } else {
263 script.verifyTimeZoneNotSet();
264 }
265
266 // Assert internal service state.
267 assertEquals(expectedZonePhone1ScoredSuggestion,
268 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
269 assertEquals(expectedZonePhone1ScoredSuggestion,
Neil Fuller2c6d5102019-11-29 09:02:39 +0000270 mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
Neil Fuller3352cfc2019-11-07 15:35:05 +0000271 }
272
273 /**
274 * Tries a set of test cases to see if the phone with the lowest ID is given preference. This
275 * test also confirms that the time zone setting would only be set if a suggestion is of
276 * sufficient quality.
277 */
278 @Test
279 public void testMultiplePhoneSuggestionScoringAndPhoneIdBias() {
280 String[] zoneIds = { "Europe/London", "Europe/Paris" };
281 PhoneTimeZoneSuggestion emptyPhone1Suggestion = createEmptyPhone1Suggestion();
282 PhoneTimeZoneSuggestion emptyPhone2Suggestion = createEmptyPhone2Suggestion();
283 QualifiedPhoneTimeZoneSuggestion expectedEmptyPhone1ScoredSuggestion =
Neil Fuller2c6d5102019-11-29 09:02:39 +0000284 new QualifiedPhoneTimeZoneSuggestion(emptyPhone1Suggestion, PHONE_SCORE_NONE);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000285 QualifiedPhoneTimeZoneSuggestion expectedEmptyPhone2ScoredSuggestion =
Neil Fuller2c6d5102019-11-29 09:02:39 +0000286 new QualifiedPhoneTimeZoneSuggestion(emptyPhone2Suggestion, PHONE_SCORE_NONE);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000287
288 Script script = new Script()
Neil Fuller2c6d5102019-11-29 09:02:39 +0000289 .initializeAutoTimeZoneDetection(true)
Neil Fuller3352cfc2019-11-07 15:35:05 +0000290 .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
291 // Initialize the latest suggestions as empty so we don't need to worry about nulls
292 // below for the first loop.
293 .suggestPhoneTimeZone(emptyPhone1Suggestion)
294 .suggestPhoneTimeZone(emptyPhone2Suggestion)
295 .resetState();
296
297 for (SuggestionTestCase testCase : TEST_CASES) {
298 PhoneTimeZoneSuggestion zonePhone1Suggestion =
299 testCase.createSuggestion(PHONE1_ID, zoneIds[0]);
300 PhoneTimeZoneSuggestion zonePhone2Suggestion =
301 testCase.createSuggestion(PHONE2_ID, zoneIds[1]);
302 QualifiedPhoneTimeZoneSuggestion expectedZonePhone1ScoredSuggestion =
303 new QualifiedPhoneTimeZoneSuggestion(zonePhone1Suggestion,
304 testCase.expectedScore);
305 QualifiedPhoneTimeZoneSuggestion expectedZonePhone2ScoredSuggestion =
306 new QualifiedPhoneTimeZoneSuggestion(zonePhone2Suggestion,
307 testCase.expectedScore);
308
309 // Start the test by making a suggestion for phone 1.
310 script.suggestPhoneTimeZone(zonePhone1Suggestion);
Neil Fuller2c6d5102019-11-29 09:02:39 +0000311 if (testCase.expectedScore >= PHONE_SCORE_USAGE_THRESHOLD) {
Neil Fuller3352cfc2019-11-07 15:35:05 +0000312 script.verifyTimeZoneSetAndReset(zonePhone1Suggestion);
313 } else {
314 script.verifyTimeZoneNotSet();
315 }
316
317 // Assert internal service state.
318 assertEquals(expectedZonePhone1ScoredSuggestion,
319 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
320 assertEquals(expectedEmptyPhone2ScoredSuggestion,
321 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE2_ID));
322 assertEquals(expectedZonePhone1ScoredSuggestion,
Neil Fuller2c6d5102019-11-29 09:02:39 +0000323 mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
Neil Fuller3352cfc2019-11-07 15:35:05 +0000324
325 // Phone 2 then makes an alternative suggestion with an identical score. Phone 1's
326 // suggestion should still "win" if it is above the required threshold.
327 script.suggestPhoneTimeZone(zonePhone2Suggestion);
328 script.verifyTimeZoneNotSet();
329
330 // Assert internal service state.
331 assertEquals(expectedZonePhone1ScoredSuggestion,
332 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
333 assertEquals(expectedZonePhone2ScoredSuggestion,
334 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE2_ID));
335 // Phone 1 should always beat phone 2, all other things being equal.
336 assertEquals(expectedZonePhone1ScoredSuggestion,
Neil Fuller2c6d5102019-11-29 09:02:39 +0000337 mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
Neil Fuller3352cfc2019-11-07 15:35:05 +0000338
339 // Withdrawing phone 1's suggestion should leave phone 2 as the new winner. Since the
340 // zoneId is different, the time zone setting should be updated if the score is high
341 // enough.
342 script.suggestPhoneTimeZone(emptyPhone1Suggestion);
Neil Fuller2c6d5102019-11-29 09:02:39 +0000343 if (testCase.expectedScore >= PHONE_SCORE_USAGE_THRESHOLD) {
Neil Fuller3352cfc2019-11-07 15:35:05 +0000344 script.verifyTimeZoneSetAndReset(zonePhone2Suggestion);
345 } else {
346 script.verifyTimeZoneNotSet();
347 }
348
349 // Assert internal service state.
350 assertEquals(expectedEmptyPhone1ScoredSuggestion,
351 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
352 assertEquals(expectedZonePhone2ScoredSuggestion,
353 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE2_ID));
354 assertEquals(expectedZonePhone2ScoredSuggestion,
Neil Fuller2c6d5102019-11-29 09:02:39 +0000355 mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
Neil Fuller3352cfc2019-11-07 15:35:05 +0000356
357 // Reset the state for the next loop.
358 script.suggestPhoneTimeZone(emptyPhone2Suggestion)
359 .verifyTimeZoneNotSet();
360 assertEquals(expectedEmptyPhone1ScoredSuggestion,
361 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
362 assertEquals(expectedEmptyPhone2ScoredSuggestion,
363 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE2_ID));
364 }
365 }
366
367 /**
368 * The {@link TimeZoneDetectorStrategy.Callback} is left to detect whether changing the time
369 * zone is actually necessary. This test proves that the service doesn't assume it knows the
370 * current setting.
371 */
372 @Test
373 public void testTimeZoneDetectorStrategyDoesNotAssumeCurrentSetting() {
374 Script script = new Script()
Neil Fuller2c6d5102019-11-29 09:02:39 +0000375 .initializeAutoTimeZoneDetection(true);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000376
377 SuggestionTestCase testCase =
Neil Fuller2c6d5102019-11-29 09:02:39 +0000378 newTestCase(MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, QUALITY_SINGLE_ZONE,
379 PHONE_SCORE_HIGH);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000380 PhoneTimeZoneSuggestion losAngelesSuggestion =
381 testCase.createSuggestion(PHONE1_ID, "America/Los_Angeles");
382 PhoneTimeZoneSuggestion newYorkSuggestion =
383 testCase.createSuggestion(PHONE1_ID, "America/New_York");
384
385 // Initialization.
386 script.suggestPhoneTimeZone(losAngelesSuggestion)
387 .verifyTimeZoneSetAndReset(losAngelesSuggestion);
388 // Suggest it again - it should not be set because it is already set.
389 script.suggestPhoneTimeZone(losAngelesSuggestion)
390 .verifyTimeZoneNotSet();
391
392 // Toggling time zone detection should set the device time zone only if the current setting
393 // value is different from the most recent phone suggestion.
Neil Fuller2c6d5102019-11-29 09:02:39 +0000394 script.autoTimeZoneDetectionEnabled(false)
Neil Fuller3352cfc2019-11-07 15:35:05 +0000395 .verifyTimeZoneNotSet()
Neil Fuller2c6d5102019-11-29 09:02:39 +0000396 .autoTimeZoneDetectionEnabled(true)
Neil Fuller3352cfc2019-11-07 15:35:05 +0000397 .verifyTimeZoneNotSet();
398
399 // Simulate a user turning auto detection off, a new suggestion being made while auto
400 // detection is off, and the user turning it on again.
Neil Fuller2c6d5102019-11-29 09:02:39 +0000401 script.autoTimeZoneDetectionEnabled(false)
Neil Fuller3352cfc2019-11-07 15:35:05 +0000402 .suggestPhoneTimeZone(newYorkSuggestion)
403 .verifyTimeZoneNotSet();
404 // Latest suggestion should be used.
Neil Fuller2c6d5102019-11-29 09:02:39 +0000405 script.autoTimeZoneDetectionEnabled(true)
Neil Fuller3352cfc2019-11-07 15:35:05 +0000406 .verifyTimeZoneSetAndReset(newYorkSuggestion);
407 }
408
Neil Fuller2c6d5102019-11-29 09:02:39 +0000409 @Test
410 public void testManualSuggestion_autoTimeZoneDetectionEnabled() {
411 Script script = new Script()
412 .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
413 .initializeAutoTimeZoneDetection(true);
414
415 // Auto time zone detection is enabled so the manual suggestion should be ignored.
416 script.suggestManualTimeZone(createManualSuggestion("Europe/Paris"))
417 .verifyTimeZoneNotSet();
418 }
419
420
421 @Test
422 public void testManualSuggestion_autoTimeZoneDetectionDisabled() {
423 Script script = new Script()
424 .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
425 .initializeAutoTimeZoneDetection(false);
426
427 // Auto time zone detection is disabled so the manual suggestion should be used.
428 ManualTimeZoneSuggestion manualSuggestion = createManualSuggestion("Europe/Paris");
429 script.suggestManualTimeZone(manualSuggestion)
430 .verifyTimeZoneSetAndReset(manualSuggestion);
431 }
432
433 private ManualTimeZoneSuggestion createManualSuggestion(String zoneId) {
434 return new ManualTimeZoneSuggestion(zoneId);
435 }
436
Neil Fuller3352cfc2019-11-07 15:35:05 +0000437 private static PhoneTimeZoneSuggestion createEmptyPhone1Suggestion() {
438 return new PhoneTimeZoneSuggestion.Builder(PHONE1_ID).build();
439 }
440
441 private static PhoneTimeZoneSuggestion createEmptyPhone2Suggestion() {
442 return new PhoneTimeZoneSuggestion.Builder(PHONE2_ID).build();
443 }
444
Neil Fuller2c6d5102019-11-29 09:02:39 +0000445 static class FakeTimeZoneDetectorStrategyCallback implements TimeZoneDetectorStrategy.Callback {
Neil Fuller3352cfc2019-11-07 15:35:05 +0000446
Neil Fuller2c6d5102019-11-29 09:02:39 +0000447 private boolean mAutoTimeZoneDetectionEnabled;
448 private TestState<TimeZoneChange> mTimeZoneChanges = new TestState<>();
449 private String mTimeZoneId;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000450
451 @Override
Neil Fuller2c6d5102019-11-29 09:02:39 +0000452 public boolean isAutoTimeZoneDetectionEnabled() {
453 return mAutoTimeZoneDetectionEnabled;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000454 }
455
456 @Override
457 public boolean isDeviceTimeZoneInitialized() {
Neil Fuller2c6d5102019-11-29 09:02:39 +0000458 return mTimeZoneId != null;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000459 }
460
461 @Override
462 public String getDeviceTimeZone() {
Neil Fuller2c6d5102019-11-29 09:02:39 +0000463 return mTimeZoneId;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000464 }
465
466 @Override
Neil Fuller2c6d5102019-11-29 09:02:39 +0000467 public void setDeviceTimeZone(String zoneId, boolean withNetworkBroadcast) {
468 mTimeZoneId = zoneId;
469 mTimeZoneChanges.set(new TimeZoneChange(zoneId, withNetworkBroadcast));
Neil Fuller3352cfc2019-11-07 15:35:05 +0000470 }
471
Neil Fuller2c6d5102019-11-29 09:02:39 +0000472 void initializeAutoTimeZoneDetection(boolean enabled) {
473 mAutoTimeZoneDetectionEnabled = enabled;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000474 }
475
476 void initializeTimeZone(String zoneId) {
Neil Fuller2c6d5102019-11-29 09:02:39 +0000477 mTimeZoneId = zoneId;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000478 }
479
Neil Fuller2c6d5102019-11-29 09:02:39 +0000480 void setAutoTimeZoneDetectionEnabled(boolean enabled) {
481 mAutoTimeZoneDetectionEnabled = enabled;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000482 }
483
484 void assertTimeZoneNotSet() {
Neil Fuller2c6d5102019-11-29 09:02:39 +0000485 mTimeZoneChanges.assertHasNotBeenSet();
Neil Fuller3352cfc2019-11-07 15:35:05 +0000486 }
487
Neil Fuller2c6d5102019-11-29 09:02:39 +0000488 void assertTimeZoneSet(String timeZoneId, boolean withNetworkBroadcast) {
489 mTimeZoneChanges.assertHasBeenSet();
490 mTimeZoneChanges.assertChangeCount(1);
491 TimeZoneChange expectedChange = new TimeZoneChange(timeZoneId, withNetworkBroadcast);
492 mTimeZoneChanges.assertLatestEquals(expectedChange);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000493 }
494
495 void commitAllChanges() {
Neil Fuller2c6d5102019-11-29 09:02:39 +0000496 mTimeZoneChanges.commitLatest();
497 }
498 }
499
500 private static class TimeZoneChange {
501 private final String mTimeZoneId;
502 private final boolean mWithNetworkBroadcast;
503
504 private TimeZoneChange(String timeZoneId, boolean withNetworkBroadcast) {
505 mTimeZoneId = timeZoneId;
506 mWithNetworkBroadcast = withNetworkBroadcast;
507 }
508
509 @Override
510 public boolean equals(Object o) {
511 if (this == o) {
512 return true;
513 }
514 if (o == null || getClass() != o.getClass()) {
515 return false;
516 }
517 TimeZoneChange that = (TimeZoneChange) o;
518 return mWithNetworkBroadcast == that.mWithNetworkBroadcast
519 && mTimeZoneId.equals(that.mTimeZoneId);
520 }
521
522 @Override
523 public int hashCode() {
524 return Objects.hash(mTimeZoneId, mWithNetworkBroadcast);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000525 }
526 }
527
528 /** Some piece of state that tests want to track. */
529 private static class TestState<T> {
530 private T mInitialValue;
531 private LinkedList<T> mValues = new LinkedList<>();
532
533 void init(T value) {
534 mValues.clear();
535 mInitialValue = value;
536 }
537
538 void set(T value) {
539 mValues.addFirst(value);
540 }
541
542 boolean hasBeenSet() {
543 return mValues.size() > 0;
544 }
545
546 void assertHasNotBeenSet() {
547 assertFalse(hasBeenSet());
548 }
549
550 void assertHasBeenSet() {
551 assertTrue(hasBeenSet());
552 }
553
554 void commitLatest() {
555 if (hasBeenSet()) {
556 mInitialValue = mValues.getLast();
557 mValues.clear();
558 }
559 }
560
561 void assertLatestEquals(T expected) {
562 assertEquals(expected, getLatest());
563 }
564
565 void assertChangeCount(int expectedCount) {
566 assertEquals(expectedCount, mValues.size());
567 }
568
569 public T getLatest() {
570 if (hasBeenSet()) {
571 return mValues.getFirst();
572 }
573 return mInitialValue;
574 }
575 }
576
577 /**
578 * A "fluent" class allows reuse of code in tests: initialization, simulation and verification
579 * logic.
580 */
581 private class Script {
582
Neil Fuller2c6d5102019-11-29 09:02:39 +0000583 Script initializeAutoTimeZoneDetection(boolean enabled) {
584 mFakeTimeZoneDetectorStrategyCallback.initializeAutoTimeZoneDetection(enabled);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000585 return this;
586 }
587
588 Script initializeTimeZoneSetting(String zoneId) {
589 mFakeTimeZoneDetectorStrategyCallback.initializeTimeZone(zoneId);
590 return this;
591 }
592
Neil Fuller2c6d5102019-11-29 09:02:39 +0000593 Script autoTimeZoneDetectionEnabled(boolean enabled) {
594 mFakeTimeZoneDetectorStrategyCallback.setAutoTimeZoneDetectionEnabled(enabled);
595 mTimeZoneDetectorStrategy.handleAutoTimeZoneDetectionChange();
Neil Fuller3352cfc2019-11-07 15:35:05 +0000596 return this;
597 }
598
Neil Fuller2c6d5102019-11-29 09:02:39 +0000599 /** Simulates the time zone detection strategy receiving a phone-originated suggestion. */
Neil Fuller3352cfc2019-11-07 15:35:05 +0000600 Script suggestPhoneTimeZone(PhoneTimeZoneSuggestion phoneTimeZoneSuggestion) {
601 mTimeZoneDetectorStrategy.suggestPhoneTimeZone(phoneTimeZoneSuggestion);
602 return this;
603 }
604
Neil Fuller2c6d5102019-11-29 09:02:39 +0000605 /** Simulates the time zone detection strategy receiving a user-originated suggestion. */
606 Script suggestManualTimeZone(ManualTimeZoneSuggestion manualTimeZoneSuggestion) {
607 mTimeZoneDetectorStrategy.suggestManualTimeZone(manualTimeZoneSuggestion);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000608 return this;
609 }
610
611 Script verifyTimeZoneNotSet() {
612 mFakeTimeZoneDetectorStrategyCallback.assertTimeZoneNotSet();
613 return this;
614 }
615
Neil Fuller2c6d5102019-11-29 09:02:39 +0000616 Script verifyTimeZoneSetAndReset(PhoneTimeZoneSuggestion suggestion) {
617 // Phone suggestions should cause a TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE
618 // broadcast.
619 boolean withNetworkBroadcast = true;
620 mFakeTimeZoneDetectorStrategyCallback.assertTimeZoneSet(
621 suggestion.getZoneId(), withNetworkBroadcast);
622 mFakeTimeZoneDetectorStrategyCallback.commitAllChanges();
623 return this;
624 }
625
626 Script verifyTimeZoneSetAndReset(ManualTimeZoneSuggestion suggestion) {
627 // Manual suggestions should not cause a TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE
628 // broadcast.
629 boolean withNetworkBroadcast = false;
630 mFakeTimeZoneDetectorStrategyCallback.assertTimeZoneSet(
631 suggestion.getZoneId(), withNetworkBroadcast);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000632 mFakeTimeZoneDetectorStrategyCallback.commitAllChanges();
633 return this;
634 }
635
636 Script resetState() {
637 mFakeTimeZoneDetectorStrategyCallback.commitAllChanges();
638 return this;
639 }
640 }
641
642 private static class SuggestionTestCase {
643 public final int matchType;
644 public final int quality;
645 public final int expectedScore;
646
647 SuggestionTestCase(int matchType, int quality, int expectedScore) {
648 this.matchType = matchType;
649 this.quality = quality;
650 this.expectedScore = expectedScore;
651 }
652
653 private PhoneTimeZoneSuggestion createSuggestion(int phoneId, String zoneId) {
654 return new PhoneTimeZoneSuggestion.Builder(phoneId)
655 .setZoneId(zoneId)
656 .setMatchType(matchType)
657 .setQuality(quality)
658 .build();
659 }
660 }
661
662 private static SuggestionTestCase newTestCase(
663 @MatchType int matchType, @Quality int quality, int expectedScore) {
664 return new SuggestionTestCase(matchType, quality, expectedScore);
665 }
666}