1/*
2 * Copyright 2017 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.internal.telephony;
18
19import static org.junit.Assert.assertEquals;
20import static org.junit.Assert.assertFalse;
21import static org.junit.Assert.assertNull;
22import static org.junit.Assert.assertTrue;
23import static org.mockito.ArgumentMatchers.any;
24import static org.mockito.ArgumentMatchers.anyLong;
25import static org.mockito.Mockito.atLeast;
26import static org.mockito.Mockito.clearInvocations;
27import static org.mockito.Mockito.times;
28import static org.mockito.Mockito.verify;
29import static org.mockito.Mockito.verifyNoMoreInteractions;
30import static org.mockito.Mockito.when;
31
32import android.icu.util.Calendar;
33import android.icu.util.GregorianCalendar;
34import android.icu.util.TimeZone;
35
36import com.android.internal.telephony.TimeZoneLookupHelper.CountryResult;
37import com.android.internal.telephony.TimeZoneLookupHelper.OffsetResult;
38import com.android.internal.telephony.util.TimeStampedValue;
39
40import org.junit.After;
41import org.junit.Before;
42import org.junit.Test;
43import org.mockito.ArgumentCaptor;
44import org.mockito.Mock;
45
46public class NitzStateMachineTest extends TelephonyTest {
47
48    @Mock
49    private NitzStateMachine.DeviceState mDeviceState;
50
51    @Mock
52    private TimeServiceHelper mTimeServiceHelper;
53
54    private TimeZoneLookupHelper mRealTimeZoneLookupHelper;
55
56    private NitzStateMachine mNitzStateMachine;
57
58    @Before
59    public void setUp() throws Exception {
60        logd("NitzStateMachineTest +Setup!");
61        super.setUp("NitzStateMachineTest");
62
63        // In tests we use the real TimeZoneLookupHelper.
64        mRealTimeZoneLookupHelper = new TimeZoneLookupHelper();
65        mNitzStateMachine = new NitzStateMachine(
66                mPhone, mTimeServiceHelper, mDeviceState, mRealTimeZoneLookupHelper);
67
68        logd("ServiceStateTrackerTest -Setup!");
69    }
70
71    @After
72    public void tearDown() throws Exception {
73        checkNoUnverifiedSetOperations(mTimeServiceHelper);
74
75        super.tearDown();
76    }
77
78    // A country that has multiple zones, but there is only one matching time zone at the time :
79    // the zone cannot be guessed from the country alone, but can be guessed from the country +
80    // NITZ.
81    private static final Scenario UNIQUE_US_ZONE_SCENARIO = new Scenario.Builder()
82            .setInitialDeviceSystemClockUtc(1977, 1, 1, 12, 0, 0)
83            .setInitialDeviceRealtimeMillis(123456789L)
84            .setTimeZone("America/Los_Angeles")
85            .setActualTimeUtc(2018, 1, 1, 12, 0, 0)
86            .setCountryIso("us")
87            .build();
88
89    @Test
90    public void test_uniqueUsZone_Assumptions() {
91        // Check we'll get the expected behavior from TimeZoneLookupHelper.
92
93        // allZonesHaveSameOffset == false, so we shouldn't pick an arbitrary zone.
94        CountryResult expectedCountryLookupResult = new CountryResult(
95                "America/New_York", false /* allZonesHaveSameOffset */,
96                UNIQUE_US_ZONE_SCENARIO.getInitialSystemClockMillis());
97        CountryResult actualCountryLookupResult =
98                mRealTimeZoneLookupHelper.lookupByCountry(
99                        UNIQUE_US_ZONE_SCENARIO.getNetworkCountryIsoCode(),
100                        UNIQUE_US_ZONE_SCENARIO.getInitialSystemClockMillis());
101        assertEquals(expectedCountryLookupResult, actualCountryLookupResult);
102
103        // isOnlyMatch == true, so the combination of country + NITZ should be enough.
104        OffsetResult expectedLookupResult =
105                new OffsetResult("America/Los_Angeles", true /* isOnlyMatch */);
106        OffsetResult actualLookupResult = mRealTimeZoneLookupHelper.lookupByNitzCountry(
107                UNIQUE_US_ZONE_SCENARIO.getNitzSignal().mValue,
108                UNIQUE_US_ZONE_SCENARIO.getNetworkCountryIsoCode());
109        assertEquals(expectedLookupResult, actualLookupResult);
110    }
111
112    // A country with a single zone : the zone can be guessed from the country.
113    private static final Scenario UNITED_KINGDOM_SCENARIO = new Scenario.Builder()
114            .setInitialDeviceSystemClockUtc(1977, 1, 1, 12, 0, 0)
115            .setInitialDeviceRealtimeMillis(123456789L)
116            .setTimeZone("Europe/London")
117            .setActualTimeUtc(2018, 1, 1, 12, 0, 0)
118            .setCountryIso("gb")
119            .build();
120
121    @Test
122    public void test_unitedKingdom_Assumptions() {
123        // Check we'll get the expected behavior from TimeZoneLookupHelper.
124
125        // allZonesHaveSameOffset == true (not only that, there is only one zone), so we can pick
126        // the zone knowing only the country.
127        CountryResult expectedCountryLookupResult = new CountryResult(
128                "Europe/London", true /* allZonesHaveSameOffset */,
129                UNITED_KINGDOM_SCENARIO.getInitialSystemClockMillis());
130        CountryResult actualCountryLookupResult =
131                mRealTimeZoneLookupHelper.lookupByCountry(
132                        UNITED_KINGDOM_SCENARIO.getNetworkCountryIsoCode(),
133                        UNITED_KINGDOM_SCENARIO.getInitialSystemClockMillis());
134        assertEquals(expectedCountryLookupResult, actualCountryLookupResult);
135
136        OffsetResult expectedLookupResult =
137                new OffsetResult("Europe/London", true /* isOnlyMatch */);
138        OffsetResult actualLookupResult = mRealTimeZoneLookupHelper.lookupByNitzCountry(
139                UNITED_KINGDOM_SCENARIO.getNitzSignal().mValue,
140                UNITED_KINGDOM_SCENARIO.getNetworkCountryIsoCode());
141        assertEquals(expectedLookupResult, actualLookupResult);
142    }
143
144    @Test
145    public void test_uniqueUsZone_timeEnabledTimeZoneEnabled_countryThenNitz() throws Exception {
146        Scenario scenario = UNIQUE_US_ZONE_SCENARIO;
147        Device device = new DeviceBuilder()
148                .setClocksFromScenario(scenario)
149                .setTimeDetectionEnabled(true)
150                .setTimeZoneDetectionEnabled(true)
151                .setTimeZoneSettingInitialized(false)
152                .initialize();
153        Script script = new Script(device);
154
155        int clockIncrement = 1250;
156        long expectedTimeMillis = scenario.getActualTimeMillis() + clockIncrement;
157        script.countryReceived(scenario.getNetworkCountryIsoCode())
158                // Country won't be enough for time zone detection.
159                .verifyNothingWasSetAndReset()
160                // Increment the clock so we can tell the time was adjusted correctly when set.
161                .incrementClocks(clockIncrement)
162                .nitzReceived(scenario.getNitzSignal())
163                // Country + NITZ is enough for both time + time zone detection.
164                .verifyTimeAndZoneSetAndReset(expectedTimeMillis, scenario.getTimeZoneId());
165
166        // Check NitzStateMachine state.
167        assertTrue(mNitzStateMachine.getNitzTimeZoneDetectionSuccessful());
168        assertEquals(scenario.getNitzSignal().mValue, mNitzStateMachine.getCachedNitzData());
169        assertEquals(scenario.getTimeZoneId(), mNitzStateMachine.getSavedTimeZoneId());
170    }
171
172    @Test
173    public void test_unitedKingdom_timeEnabledTimeZoneEnabled_countryThenNitz() throws Exception {
174        Scenario scenario = UNITED_KINGDOM_SCENARIO;
175        Device device = new DeviceBuilder()
176                .setClocksFromScenario(scenario)
177                .setTimeDetectionEnabled(true)
178                .setTimeZoneDetectionEnabled(true)
179                .setTimeZoneSettingInitialized(false)
180                .initialize();
181        Script script = new Script(device);
182
183        int clockIncrement = 1250;
184        long expectedTimeMillis = scenario.getActualTimeMillis() + clockIncrement;
185        script.countryReceived(scenario.getNetworkCountryIsoCode())
186                // Country alone is enough to guess the time zone.
187                .verifyOnlyTimeZoneWasSetAndReset(scenario.getTimeZoneId())
188                // Increment the clock so we can tell the time was adjusted correctly when set.
189                .incrementClocks(clockIncrement)
190                .nitzReceived(scenario.getNitzSignal())
191                // Country + NITZ is enough for both time + time zone detection.
192                .verifyTimeAndZoneSetAndReset(expectedTimeMillis, scenario.getTimeZoneId());
193
194        // Check NitzStateMachine state.
195        assertTrue(mNitzStateMachine.getNitzTimeZoneDetectionSuccessful());
196        assertEquals(scenario.getNitzSignal().mValue, mNitzStateMachine.getCachedNitzData());
197        assertEquals(scenario.getTimeZoneId(), mNitzStateMachine.getSavedTimeZoneId());
198    }
199
200    @Test
201    public void test_uniqueUsZone_timeEnabledTimeZoneDisabled_countryThenNitz() throws Exception {
202        Scenario scenario = UNIQUE_US_ZONE_SCENARIO;
203        Device device = new DeviceBuilder()
204                .setClocksFromScenario(scenario)
205                .setTimeDetectionEnabled(true)
206                .setTimeZoneDetectionEnabled(false)
207                .setTimeZoneSettingInitialized(false)
208                .initialize();
209        Script script = new Script(device);
210
211        int clockIncrement = 1250;
212        script.countryReceived(scenario.getNetworkCountryIsoCode())
213                // Country is not enough to guess the time zone and time zone detection is disabled.
214                .verifyNothingWasSetAndReset()
215                // Increment the clock so we can tell the time was adjusted correctly when set.
216                .incrementClocks(clockIncrement)
217                .nitzReceived(scenario.getNitzSignal())
218                // Time zone detection is disabled, but time should be set from NITZ.
219                .verifyOnlyTimeWasSetAndReset(scenario.getActualTimeMillis() + clockIncrement);
220
221        // Check NitzStateMachine state.
222        assertTrue(mNitzStateMachine.getNitzTimeZoneDetectionSuccessful());
223        assertEquals(scenario.getNitzSignal().mValue, mNitzStateMachine.getCachedNitzData());
224        assertEquals(scenario.getTimeZoneId(), mNitzStateMachine.getSavedTimeZoneId());
225    }
226
227    @Test
228    public void test_unitedKingdom_timeEnabledTimeZoneDisabled_countryThenNitz()
229            throws Exception {
230        Scenario scenario = UNITED_KINGDOM_SCENARIO;
231        Device device = new DeviceBuilder()
232                .setClocksFromScenario(scenario)
233                .setTimeDetectionEnabled(true)
234                .setTimeZoneDetectionEnabled(false)
235                .setTimeZoneSettingInitialized(false)
236                .initialize();
237        Script script = new Script(device);
238
239        int clockIncrement = 1250;
240        script.countryReceived(scenario.getNetworkCountryIsoCode())
241                // Country alone would be enough for time zone detection, but it's disabled.
242                .verifyNothingWasSetAndReset()
243                // Increment the clock so we can tell the time was adjusted correctly when set.
244                .incrementClocks(clockIncrement)
245                .nitzReceived(scenario.getNitzSignal())
246                // Time zone detection is disabled, but time should be set from NITZ.
247                .verifyOnlyTimeWasSetAndReset(scenario.getActualTimeMillis() + clockIncrement);
248
249        // Check NitzStateMachine state.
250        assertTrue(mNitzStateMachine.getNitzTimeZoneDetectionSuccessful());
251        assertEquals(scenario.getNitzSignal().mValue, mNitzStateMachine.getCachedNitzData());
252        assertEquals(scenario.getTimeZoneId(), mNitzStateMachine.getSavedTimeZoneId());
253    }
254
255    @Test
256    public void test_uniqueUsZone_timeDisabledTimeZoneEnabled_countryThenNitz() throws Exception {
257        Scenario scenario = UNIQUE_US_ZONE_SCENARIO;
258        Device device = new DeviceBuilder()
259                .setClocksFromScenario(scenario)
260                .setTimeDetectionEnabled(false)
261                .setTimeZoneDetectionEnabled(true)
262                .setTimeZoneSettingInitialized(false)
263                .initialize();
264        Script script = new Script(device);
265
266        script.countryReceived(scenario.getNetworkCountryIsoCode())
267                // Country won't be enough for time zone detection.
268                .verifyNothingWasSetAndReset()
269                .nitzReceived(scenario.getNitzSignal())
270                // Time detection is disabled, but time zone should be detected from country + NITZ.
271                .verifyOnlyTimeZoneWasSetAndReset(scenario.getTimeZoneId());
272
273        // Check NitzStateMachine state.
274        assertTrue(mNitzStateMachine.getNitzTimeZoneDetectionSuccessful());
275        assertEquals(scenario.getNitzSignal().mValue, mNitzStateMachine.getCachedNitzData());
276        assertEquals(scenario.getTimeZoneId(), mNitzStateMachine.getSavedTimeZoneId());
277    }
278
279    @Test
280    public void test_unitedKingdom_timeDisabledTimeZoneEnabled_countryThenNitz() throws Exception {
281        Scenario scenario = UNITED_KINGDOM_SCENARIO;
282        Device device = new DeviceBuilder()
283                .setClocksFromScenario(scenario)
284                .setTimeDetectionEnabled(false)
285                .setTimeZoneDetectionEnabled(true)
286                .setTimeZoneSettingInitialized(false)
287                .initialize();
288        Script script = new Script(device);
289
290        script.countryReceived(scenario.getNetworkCountryIsoCode())
291                // Country alone is enough to detect time zone.
292                .verifyOnlyTimeZoneWasSetAndReset(scenario.getTimeZoneId())
293                .nitzReceived(scenario.getNitzSignal())
294                // Time detection is disabled, so we don't set the clock from NITZ.
295                .verifyOnlyTimeZoneWasSetAndReset(scenario.getTimeZoneId());
296
297        // Check NitzStateMachine state.
298        assertTrue(mNitzStateMachine.getNitzTimeZoneDetectionSuccessful());
299        assertEquals(scenario.getNitzSignal().mValue, mNitzStateMachine.getCachedNitzData());
300        assertEquals(scenario.getTimeZoneId(), mNitzStateMachine.getSavedTimeZoneId());
301    }
302
303    @Test
304    public void test_uniqueUsZone_timeDisabledTimeZoneDisabled_countryThenNitz() throws Exception {
305        Scenario scenario = UNIQUE_US_ZONE_SCENARIO;
306        Device device = new DeviceBuilder()
307                .setClocksFromScenario(scenario)
308                .setTimeDetectionEnabled(false)
309                .setTimeZoneDetectionEnabled(false)
310                .setTimeZoneSettingInitialized(false)
311                .initialize();
312        Script script = new Script(device);
313
314        script.countryReceived(scenario.getNetworkCountryIsoCode())
315                // Time and time zone detection is disabled.
316                .verifyNothingWasSetAndReset()
317                .nitzReceived(scenario.getNitzSignal())
318                // Time and time zone detection is disabled.
319                .verifyNothingWasSetAndReset();
320
321        // Check NitzStateMachine state.
322        assertTrue(mNitzStateMachine.getNitzTimeZoneDetectionSuccessful());
323        assertEquals(scenario.getNitzSignal().mValue, mNitzStateMachine.getCachedNitzData());
324        assertEquals(scenario.getTimeZoneId(), mNitzStateMachine.getSavedTimeZoneId());
325    }
326
327    @Test
328    public void test_unitedKingdom_timeDisabledTimeZoneDisabled_countryThenNitz() throws Exception {
329        Scenario scenario = UNITED_KINGDOM_SCENARIO;
330        Device device = new DeviceBuilder()
331                .setClocksFromScenario(scenario)
332                .setTimeDetectionEnabled(false)
333                .setTimeZoneDetectionEnabled(false)
334                .setTimeZoneSettingInitialized(false)
335                .initialize();
336        Script script = new Script(device);
337
338        script.countryReceived(scenario.getNetworkCountryIsoCode())
339                // Time and time zone detection is disabled.
340                .verifyNothingWasSetAndReset()
341                .nitzReceived(scenario.getNitzSignal())
342                // Time and time zone detection is disabled.
343                .verifyNothingWasSetAndReset();
344
345        // Check NitzStateMachine state.
346        assertTrue(mNitzStateMachine.getNitzTimeZoneDetectionSuccessful());
347        assertEquals(scenario.getNitzSignal().mValue, mNitzStateMachine.getCachedNitzData());
348        assertEquals(scenario.getTimeZoneId(), mNitzStateMachine.getSavedTimeZoneId());
349    }
350
351    @Test
352    public void test_uniqueUsZone_timeDisabledTimeZoneEnabled_nitzThenCountry() throws Exception {
353        Scenario scenario = UNIQUE_US_ZONE_SCENARIO;
354        Device device = new DeviceBuilder()
355                .setClocksFromScenario(scenario)
356                .setTimeDetectionEnabled(false)
357                .setTimeZoneDetectionEnabled(true)
358                .setTimeZoneSettingInitialized(false)
359                .initialize();
360        Script script = new Script(device);
361
362        // Simulate receiving an NITZ signal.
363        script.nitzReceived(scenario.getNitzSignal())
364                // The NITZ alone isn't enough to detect a time zone.
365                .verifyNothingWasSetAndReset();
366
367        // Check NitzStateMachine state.
368        assertFalse(mNitzStateMachine.getNitzTimeZoneDetectionSuccessful());
369        assertEquals(scenario.getNitzSignal().mValue, mNitzStateMachine.getCachedNitzData());
370        assertNull(mNitzStateMachine.getSavedTimeZoneId());
371
372        // Simulate the country code becoming known.
373        script.countryReceived(scenario.getNetworkCountryIsoCode())
374                // The NITZ + country is enough to detect the time zone.
375                .verifyOnlyTimeZoneWasSetAndReset(scenario.getTimeZoneId());
376
377        // Check NitzStateMachine state.
378        // TODO(nfuller): The following line should probably be assertTrue but the logic under test
379        // may be buggy. Look at whether it needs to change.
380        assertFalse(mNitzStateMachine.getNitzTimeZoneDetectionSuccessful());
381        assertEquals(scenario.getNitzSignal().mValue, mNitzStateMachine.getCachedNitzData());
382        assertEquals(scenario.getTimeZoneId(), mNitzStateMachine.getSavedTimeZoneId());
383    }
384
385    @Test
386    public void test_unitedKingdom_timeDisabledTimeZoneEnabled_nitzThenCountry() throws Exception {
387        Scenario scenario = UNITED_KINGDOM_SCENARIO;
388        Device device = new DeviceBuilder()
389                .setClocksFromScenario(scenario)
390                .setTimeDetectionEnabled(false)
391                .setTimeZoneDetectionEnabled(true)
392                .setTimeZoneSettingInitialized(false)
393                .initialize();
394        Script script = new Script(device);
395
396        // Simulate receiving an NITZ signal.
397        script.nitzReceived(scenario.getNitzSignal())
398                // The NITZ alone isn't enough to detect a time zone.
399                .verifyNothingWasSetAndReset();
400
401        // Check NitzStateMachine state.
402        assertFalse(mNitzStateMachine.getNitzTimeZoneDetectionSuccessful());
403        assertEquals(scenario.getNitzSignal().mValue, mNitzStateMachine.getCachedNitzData());
404        assertNull(mNitzStateMachine.getSavedTimeZoneId());
405
406        // Simulate the country code becoming known.
407        script.countryReceived(scenario.getNetworkCountryIsoCode());
408
409        // The NITZ + country is enough to detect the time zone.
410        // NOTE: setting the timezone happens twice because of a quirk in NitzStateMachine: it
411        // handles the country lookup / set, then combines the country with the NITZ state and does
412        // another lookup / set. We shouldn't require it is set twice but we do for simplicity.
413        script.verifyOnlyTimeZoneWasSetAndReset(scenario.getTimeZoneId(), 2 /* times */);
414
415        // Check NitzStateMachine state.
416        // TODO(nfuller): The following line should probably be assertTrue but the logic under test
417        // may be buggy. Look at whether it needs to change.
418        assertFalse(mNitzStateMachine.getNitzTimeZoneDetectionSuccessful());
419        assertEquals(scenario.getNitzSignal().mValue, mNitzStateMachine.getCachedNitzData());
420        assertEquals(scenario.getTimeZoneId(), mNitzStateMachine.getSavedTimeZoneId());
421    }
422
423    private static long createUtcTime(int year, int monthInYear, int day, int hourOfDay, int minute,
424            int second) {
425        Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("Etc/UTC"));
426        cal.clear();
427        cal.set(year, monthInYear - 1, day, hourOfDay, minute, second);
428        return cal.getTimeInMillis();
429    }
430
431    /**
432     * A helper class for common test operations involving a device.
433     */
434    class Script {
435        private final Device mDevice;
436
437        Script(Device device) {
438            this.mDevice = device;
439        }
440
441        Script countryReceived(String countryIsoCode) {
442            mDevice.networkCountryKnown(countryIsoCode);
443            return this;
444        }
445
446        Script nitzReceived(TimeStampedValue<NitzData> nitzSignal) {
447            mDevice.nitzSignalReceived(nitzSignal);
448            return this;
449        }
450
451        Script incrementClocks(int clockIncrement) {
452            mDevice.incrementClocks(clockIncrement);
453            return this;
454        }
455
456        Script verifyNothingWasSetAndReset() {
457            mDevice.verifyTimeZoneWasNotSet();
458            mDevice.verifyTimeWasNotSet();
459            mDevice.checkNoUnverifiedSetOperations();
460            mDevice.resetInvocations();
461            return this;
462        }
463
464        Script verifyOnlyTimeZoneWasSetAndReset(String timeZoneId, int times) {
465            mDevice.verifyTimeZoneWasSet(timeZoneId, times);
466            mDevice.verifyTimeWasNotSet();
467            mDevice.checkNoUnverifiedSetOperations();
468            mDevice.resetInvocations();
469            return this;
470        }
471
472        Script verifyOnlyTimeZoneWasSetAndReset(String timeZoneId) {
473            return verifyOnlyTimeZoneWasSetAndReset(timeZoneId, 1);
474        }
475
476        Script verifyOnlyTimeWasSetAndReset(long expectedTimeMillis) {
477            mDevice.verifyTimeZoneWasNotSet();
478            mDevice.verifyTimeWasSet(expectedTimeMillis);
479            mDevice.checkNoUnverifiedSetOperations();
480            mDevice.resetInvocations();
481            return this;
482        }
483
484        Script verifyTimeAndZoneSetAndReset(long expectedTimeMillis, String timeZoneId) {
485            mDevice.verifyTimeZoneWasSet(timeZoneId);
486            mDevice.verifyTimeWasSet(expectedTimeMillis);
487            mDevice.checkNoUnverifiedSetOperations();
488            mDevice.resetInvocations();
489            return this;
490        }
491
492        Script reset() {
493            mDevice.checkNoUnverifiedSetOperations();
494            mDevice.resetInvocations();
495            return this;
496        }
497    }
498
499    /**
500     * An abstraction of a device for use in telephony time zone detection tests. It can be used to
501     * retrieve device state, modify device state and verify changes.
502     */
503    class Device {
504
505        private final long mInitialSystemClockMillis;
506        private final long mInitialRealtimeMillis;
507        private final boolean mTimeDetectionEnabled;
508        private final boolean mTimeZoneDetectionEnabled;
509        private final boolean mTimeZoneSettingInitialized;
510
511        Device(long initialSystemClockMillis, long initialRealtimeMillis,
512                boolean timeDetectionEnabled, boolean timeZoneDetectionEnabled,
513                boolean timeZoneSettingInitialized) {
514            mInitialSystemClockMillis = initialSystemClockMillis;
515            mInitialRealtimeMillis = initialRealtimeMillis;
516            mTimeDetectionEnabled = timeDetectionEnabled;
517            mTimeZoneDetectionEnabled = timeZoneDetectionEnabled;
518            mTimeZoneSettingInitialized = timeZoneSettingInitialized;
519        }
520
521        void initialize() {
522            // Set initial configuration.
523            when(mDeviceState.getIgnoreNitz()).thenReturn(false);
524            when(mDeviceState.getNitzUpdateDiffMillis()).thenReturn(2000);
525            when(mDeviceState.getNitzUpdateSpacingMillis()).thenReturn(1000 * 60 * 10);
526
527            // Simulate the country not being known.
528            when(mDeviceState.getNetworkCountryIsoForPhone()).thenReturn("");
529
530            when(mTimeServiceHelper.elapsedRealtime()).thenReturn(mInitialRealtimeMillis);
531            when(mTimeServiceHelper.currentTimeMillis()).thenReturn(mInitialSystemClockMillis);
532            when(mTimeServiceHelper.isTimeDetectionEnabled()).thenReturn(mTimeDetectionEnabled);
533            when(mTimeServiceHelper.isTimeZoneDetectionEnabled())
534                    .thenReturn(mTimeZoneDetectionEnabled);
535            when(mTimeServiceHelper.isTimeZoneSettingInitialized())
536                    .thenReturn(mTimeZoneSettingInitialized);
537        }
538
539        void networkCountryKnown(String countryIsoCode) {
540            when(mDeviceState.getNetworkCountryIsoForPhone()).thenReturn(countryIsoCode);
541            mNitzStateMachine.handleNetworkCountryCodeSet(true);
542        }
543
544        void incrementClocks(int millis) {
545            long currentElapsedRealtime = mTimeServiceHelper.elapsedRealtime();
546            when(mTimeServiceHelper.elapsedRealtime()).thenReturn(currentElapsedRealtime + millis);
547            long currentTimeMillis = mTimeServiceHelper.currentTimeMillis();
548            when(mTimeServiceHelper.currentTimeMillis()).thenReturn(currentTimeMillis + millis);
549        }
550
551        void nitzSignalReceived(TimeStampedValue<NitzData> nitzSignal) {
552            mNitzStateMachine.handleNitzReceived(nitzSignal);
553        }
554
555        void verifyTimeZoneWasNotSet() {
556            verify(mTimeServiceHelper, times(0)).setDeviceTimeZone(any(String.class));
557        }
558
559        void verifyTimeZoneWasSet(String timeZoneId) {
560            verifyTimeZoneWasSet(timeZoneId, 1 /* times */);
561        }
562
563        void verifyTimeZoneWasSet(String timeZoneId, int times) {
564            verify(mTimeServiceHelper, times(times)).setDeviceTimeZone(timeZoneId);
565        }
566
567        void verifyTimeWasNotSet() {
568            verify(mTimeServiceHelper, times(0)).setDeviceTime(anyLong());
569        }
570
571        void verifyTimeWasSet(long expectedTimeMillis) {
572            ArgumentCaptor<Long> timeServiceTimeCaptor = ArgumentCaptor.forClass(Long.TYPE);
573            verify(mTimeServiceHelper, times(1)).setDeviceTime(timeServiceTimeCaptor.capture());
574            assertEquals(expectedTimeMillis, (long) timeServiceTimeCaptor.getValue());
575        }
576
577        /**
578         * Used after calling verify... methods to reset expectations.
579         */
580        void resetInvocations() {
581            clearInvocations(mTimeServiceHelper);
582        }
583
584        void checkNoUnverifiedSetOperations() {
585            NitzStateMachineTest.checkNoUnverifiedSetOperations(mTimeServiceHelper);
586        }
587    }
588
589    /** A class used to construct a Device. */
590    class DeviceBuilder {
591
592        private long mInitialSystemClock;
593        private long mInitialRealtimeMillis;
594        private boolean mTimeDetectionEnabled;
595        private boolean mTimeZoneDetectionEnabled;
596        private boolean mTimeZoneSettingInitialized;
597
598        Device initialize() {
599            Device device = new Device(mInitialSystemClock, mInitialRealtimeMillis,
600                    mTimeDetectionEnabled, mTimeZoneDetectionEnabled, mTimeZoneSettingInitialized);
601            device.initialize();
602            return device;
603        }
604
605        DeviceBuilder setTimeDetectionEnabled(boolean enabled) {
606            mTimeDetectionEnabled = enabled;
607            return this;
608        }
609
610        DeviceBuilder setTimeZoneDetectionEnabled(boolean enabled) {
611            mTimeZoneDetectionEnabled = enabled;
612            return this;
613        }
614
615        DeviceBuilder setTimeZoneSettingInitialized(boolean initialized) {
616            mTimeZoneSettingInitialized = initialized;
617            return this;
618        }
619
620        DeviceBuilder setClocksFromScenario(Scenario scenario) {
621            mInitialRealtimeMillis = scenario.getInitialRealTimeMillis();
622            mInitialSystemClock = scenario.getInitialSystemClockMillis();
623            return this;
624        }
625    }
626
627    /**
628     * A scenario used during tests. Describes a fictional reality.
629     */
630    static class Scenario {
631
632        private final long mInitialDeviceSystemClockMillis;
633        private final long mInitialDeviceRealtimeMillis;
634        private final long mActualTimeMillis;
635        private final TimeZone mZone;
636        private final String mNetworkCountryIsoCode;
637
638        private TimeStampedValue<NitzData> mNitzSignal;
639
640        Scenario(long initialDeviceSystemClock, long elapsedRealtime, long timeMillis,
641                String zoneId, String countryIsoCode) {
642            mInitialDeviceSystemClockMillis = initialDeviceSystemClock;
643            mActualTimeMillis = timeMillis;
644            mInitialDeviceRealtimeMillis = elapsedRealtime;
645            mZone = TimeZone.getTimeZone(zoneId);
646            mNetworkCountryIsoCode = countryIsoCode;
647        }
648
649        TimeStampedValue<NitzData> getNitzSignal() {
650            if (mNitzSignal == null) {
651                int[] offsets = new int[2];
652                mZone.getOffset(mActualTimeMillis, false /* local */, offsets);
653                int zoneOffsetMillis = offsets[0] + offsets[1];
654                NitzData nitzData = NitzData
655                        .createForTests(zoneOffsetMillis, offsets[1], mActualTimeMillis, null);
656                mNitzSignal = new TimeStampedValue<>(nitzData, mInitialDeviceRealtimeMillis);
657            }
658            return mNitzSignal;
659        }
660
661        long getInitialRealTimeMillis() {
662            return mInitialDeviceRealtimeMillis;
663        }
664
665        long getInitialSystemClockMillis() {
666            return mInitialDeviceSystemClockMillis;
667        }
668
669        String getNetworkCountryIsoCode() {
670            return mNetworkCountryIsoCode;
671        }
672
673        String getTimeZoneId() {
674            return mZone.getID();
675        }
676
677        long getActualTimeMillis() {
678            return mActualTimeMillis;
679        }
680
681        static class Builder {
682
683            private long mInitialDeviceSystemClockMillis;
684            private long mInitialDeviceRealtimeMillis;
685            private long mActualTimeMillis;
686            private String mZoneId;
687            private String mCountryIsoCode;
688
689            Builder setInitialDeviceSystemClockUtc(int year, int monthInYear, int day,
690                    int hourOfDay, int minute, int second) {
691                mInitialDeviceSystemClockMillis = createUtcTime(year, monthInYear, day, hourOfDay,
692                        minute, second);
693                return this;
694            }
695
696            Builder setInitialDeviceRealtimeMillis(long realtimeMillis) {
697                mInitialDeviceRealtimeMillis = realtimeMillis;
698                return this;
699            }
700
701            Builder setActualTimeUtc(int year, int monthInYear, int day, int hourOfDay,
702                    int minute, int second) {
703                mActualTimeMillis = createUtcTime(year, monthInYear, day, hourOfDay, minute,
704                        second);
705                return this;
706            }
707
708            Builder setTimeZone(String zoneId) {
709                mZoneId = zoneId;
710                return this;
711            }
712
713            Builder setCountryIso(String isoCode) {
714                mCountryIsoCode = isoCode;
715                return this;
716            }
717
718            Scenario build() {
719                return new Scenario(mInitialDeviceSystemClockMillis, mInitialDeviceRealtimeMillis,
720                        mActualTimeMillis, mZoneId, mCountryIsoCode);
721            }
722        }
723    }
724
725    /**
726     * Confirms all mTimeServiceHelper side effects were verified.
727     */
728    private static void checkNoUnverifiedSetOperations(TimeServiceHelper mTimeServiceHelper) {
729        // We don't care about current auto time / time zone state retrievals / listening so we can
730        // use "at least 0" times to indicate they don't matter.
731        verify(mTimeServiceHelper, atLeast(0)).setListener(any());
732        verify(mTimeServiceHelper, atLeast(0)).isTimeDetectionEnabled();
733        verify(mTimeServiceHelper, atLeast(0)).isTimeZoneDetectionEnabled();
734        verify(mTimeServiceHelper, atLeast(0)).isTimeZoneSettingInitialized();
735        verify(mTimeServiceHelper, atLeast(0)).elapsedRealtime();
736        verify(mTimeServiceHelper, atLeast(0)).currentTimeMillis();
737        verifyNoMoreInteractions(mTimeServiceHelper);
738    }
739}
740