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 
17 package com.android.internal.telephony.nitz;
18 
19 import static com.android.internal.telephony.nitz.NitzStateMachineTestSupport.ARBITRARY_DEBUG_INFO;
20 import static com.android.internal.telephony.nitz.TimeZoneLookupHelper.CountryResult.QUALITY_DEFAULT_BOOSTED;
21 import static com.android.internal.telephony.nitz.TimeZoneLookupHelper.CountryResult.QUALITY_MULTIPLE_ZONES_DIFFERENT_OFFSETS;
22 import static com.android.internal.telephony.nitz.TimeZoneLookupHelper.CountryResult.QUALITY_MULTIPLE_ZONES_SAME_OFFSET;
23 import static com.android.internal.telephony.nitz.TimeZoneLookupHelper.CountryResult.QUALITY_SINGLE_ZONE;
24 
25 import static org.junit.Assert.assertEquals;
26 import static org.junit.Assert.assertFalse;
27 import static org.junit.Assert.assertNotNull;
28 import static org.junit.Assert.assertNull;
29 import static org.junit.Assert.assertTrue;
30 
31 import android.icu.util.GregorianCalendar;
32 import android.icu.util.TimeZone;
33 import android.timezone.CountryTimeZones.OffsetResult;
34 
35 import com.android.internal.telephony.NitzData;
36 import com.android.internal.telephony.nitz.TimeZoneLookupHelper.CountryResult;
37 
38 import org.junit.After;
39 import org.junit.Before;
40 import org.junit.Test;
41 
42 import java.util.Arrays;
43 import java.util.Calendar;
44 import java.util.Date;
45 import java.util.List;
46 import java.util.concurrent.TimeUnit;
47 
48 public class TimeZoneLookupHelperTest {
49     // Note: Historical dates are used to avoid the test breaking due to data changes.
50     /* Arbitrary summer date in the Northern hemisphere. */
51     private static final long NH_SUMMER_TIME_MILLIS = createUnixEpochTime(2015, 6, 20, 1, 2, 3);
52     /* Arbitrary winter date in the Northern hemisphere. */
53     private static final long NH_WINTER_TIME_MILLIS = createUnixEpochTime(2015, 1, 20, 1, 2, 3);
54 
55     private TimeZoneLookupHelper mTimeZoneLookupHelper;
56 
57     @Before
setUp()58     public void setUp() {
59         mTimeZoneLookupHelper = new TimeZoneLookupHelper();
60     }
61 
62     @After
tearDown()63     public void tearDown() {
64         mTimeZoneLookupHelper = null;
65     }
66 
67     @Test
testLookupByNitzByNitz()68     public void testLookupByNitzByNitz() {
69         // Historical dates are used to avoid the test breaking due to data changes.
70         // However, algorithm updates may change the exact time zone returned, though it shouldn't
71         // ever be a less exact match.
72         long nhSummerTimeMillis = createUnixEpochTime(2015, 6, 20, 1, 2, 3);
73         long nhWinterTimeMillis = createUnixEpochTime(2015, 1, 20, 1, 2, 3);
74 
75         String nhSummerTimeString = "15/06/20,01:02:03";
76         String nhWinterTimeString = "15/01/20,01:02:03";
77 
78         // Tests for London, UK.
79         {
80             String lonSummerTimeString = nhSummerTimeString + "+4";
81             int lonSummerOffsetMillis = (int) TimeUnit.HOURS.toMillis(1);
82             int lonSummerDstOffsetMillis = (int) TimeUnit.HOURS.toMillis(1);
83 
84             String lonWinterTimeString = nhWinterTimeString + "+0";
85             int lonWinterOffsetMillis = 0;
86             int lonWinterDstOffsetMillis = 0;
87 
88             OffsetResult lookupResult;
89 
90             // Summer, known DST state (DST == true).
91             NitzData lonSummerNitzDataWithOffset = NitzData.parse(lonSummerTimeString + ",4");
92             lookupResult = mTimeZoneLookupHelper.lookupByNitz(lonSummerNitzDataWithOffset);
93             assertOffsetResultZoneOffsets(nhSummerTimeMillis, lonSummerOffsetMillis,
94                     lonSummerDstOffsetMillis, lookupResult);
95             assertOffsetResultMetadata(false, lookupResult);
96 
97             // Winter, known DST state (DST == false).
98             NitzData lonWinterNitzDataWithOffset = NitzData.parse(lonWinterTimeString + ",0");
99             lookupResult = mTimeZoneLookupHelper.lookupByNitz(lonWinterNitzDataWithOffset);
100             assertOffsetResultZoneOffsets(nhWinterTimeMillis, lonWinterOffsetMillis,
101                     lonWinterDstOffsetMillis, lookupResult);
102             assertOffsetResultMetadata(false, lookupResult);
103 
104             // Summer, unknown DST state
105             NitzData lonSummerNitzDataWithoutOffset = NitzData.parse(lonSummerTimeString);
106             lookupResult = mTimeZoneLookupHelper.lookupByNitz(lonSummerNitzDataWithoutOffset);
107             assertOffsetResultZoneOffsets(nhSummerTimeMillis, lonSummerOffsetMillis, null,
108                     lookupResult);
109             assertOffsetResultMetadata(false, lookupResult);
110 
111             // Winter, unknown DST state
112             NitzData lonWinterNitzDataWithoutOffset = NitzData.parse(lonWinterTimeString);
113             lookupResult = mTimeZoneLookupHelper.lookupByNitz(lonWinterNitzDataWithoutOffset);
114             assertOffsetResultZoneOffsets(nhWinterTimeMillis, lonWinterOffsetMillis, null,
115                     lookupResult);
116             assertOffsetResultMetadata(false, lookupResult);
117         }
118 
119         // Tests for Mountain View, CA, US.
120         {
121             String mtvSummerTimeString = nhSummerTimeString + "-32";
122             int mtvSummerOffsetMillis = (int) TimeUnit.HOURS.toMillis(-8);
123             int mtvSummerDstOffsetMillis = (int) TimeUnit.HOURS.toMillis(1);
124 
125             String mtvWinterTimeString = nhWinterTimeString + "-28";
126             int mtvWinterOffsetMillis = (int) TimeUnit.HOURS.toMillis(-7);
127             int mtvWinterDstOffsetMillis = 0;
128 
129             OffsetResult lookupResult;
130 
131             // Summer, known DST state (DST == true).
132             NitzData mtvSummerNitzDataWithOffset = NitzData.parse(mtvSummerTimeString + ",4");
133             lookupResult = mTimeZoneLookupHelper.lookupByNitz(mtvSummerNitzDataWithOffset);
134             assertOffsetResultZoneOffsets(nhSummerTimeMillis, mtvSummerOffsetMillis,
135                     mtvSummerDstOffsetMillis, lookupResult);
136             assertOffsetResultMetadata(false, lookupResult);
137 
138             // Winter, known DST state (DST == false).
139             NitzData mtvWinterNitzDataWithOffset = NitzData.parse(mtvWinterTimeString + ",0");
140             lookupResult = mTimeZoneLookupHelper.lookupByNitz(mtvWinterNitzDataWithOffset);
141             assertOffsetResultZoneOffsets(nhWinterTimeMillis, mtvWinterOffsetMillis,
142                     mtvWinterDstOffsetMillis, lookupResult);
143             assertOffsetResultMetadata(false, lookupResult);
144 
145             // Summer, unknown DST state
146             NitzData mtvSummerNitzDataWithoutOffset = NitzData.parse(mtvSummerTimeString);
147             lookupResult = mTimeZoneLookupHelper.lookupByNitz(mtvSummerNitzDataWithoutOffset);
148             assertOffsetResultZoneOffsets(nhSummerTimeMillis, mtvSummerOffsetMillis, null,
149                     lookupResult);
150             assertOffsetResultMetadata(false, lookupResult);
151 
152             // Winter, unknown DST state
153             NitzData mtvWinterNitzDataWithoutOffset = NitzData.parse(mtvWinterTimeString);
154             lookupResult = mTimeZoneLookupHelper.lookupByNitz(mtvWinterNitzDataWithoutOffset);
155             assertOffsetResultZoneOffsets(nhWinterTimeMillis, mtvWinterOffsetMillis, null,
156                     lookupResult);
157             assertOffsetResultMetadata(false, lookupResult);
158         }
159     }
160 
161     @Test
testLookupByNitzCountry_filterByEffectiveDate()162     public void testLookupByNitzCountry_filterByEffectiveDate() {
163         // America/North_Dakota/Beulah was on Mountain Time until 2010-11-07, when it switched to
164         // Central Time.
165         String usIso = "US";
166 
167         // Try MDT / -6 hours in summer before America/North_Dakota/Beulah switched to Central Time.
168         {
169             String nitzString = "10/11/05,00:00:00-24,1"; // 2010-11-05 00:00:00 UTC, UTC-6, DST
170             NitzData nitzData = NitzData.parse(nitzString);
171             // The zone chosen is a side effect of zone ordering in the data files so we just check
172             // the isOnlyMatch value.
173             OffsetResult offsetResult = mTimeZoneLookupHelper.lookupByNitzCountry(nitzData, usIso);
174             assertFalse(offsetResult.isOnlyMatch());
175         }
176 
177         // Try MDT / -6 hours in summer after America/North_Dakota/Beulah switched to central time.
178         {
179             String nitzString = "11/11/05,00:00:00-24,1"; // 2011-11-05 00:00:00 UTC, UTC-6, DST
180             NitzData nitzData = NitzData.parse(nitzString);
181             OffsetResult offsetResult = mTimeZoneLookupHelper.lookupByNitzCountry(nitzData, usIso);
182             assertTrue(offsetResult.isOnlyMatch());
183         }
184     }
185 
186     @Test
testLookupByNitzCountry_multipleMatches()187     public void testLookupByNitzCountry_multipleMatches() {
188         // America/Denver & America/Phoenix share the same Mountain Standard Time offset (i.e.
189         // during winter).
190         String usIso = "US";
191 
192         // Try MDT for a recent summer date: No ambiguity here.
193         {
194             String nitzString = "15/06/01,00:00:00-24,1"; // 2015-06-01 00:00:00 UTC, UTC-6, DST
195             NitzData nitzData = NitzData.parse(nitzString);
196             OffsetResult offsetResult = mTimeZoneLookupHelper.lookupByNitzCountry(nitzData, usIso);
197             assertTrue(offsetResult.isOnlyMatch());
198         }
199 
200         // Try MST for a recent summer date: No ambiguity here.
201         {
202             String nitzString = "15/06/01,00:00:00-28,0"; // 2015-06-01 00:00:00 UTC, UTC-7, not DST
203             NitzData nitzData = NitzData.parse(nitzString);
204             OffsetResult offsetResult = mTimeZoneLookupHelper.lookupByNitzCountry(nitzData, usIso);
205             assertTrue(offsetResult.isOnlyMatch());
206         }
207 
208         // Try MST for a recent winter date: There are multiple zones to pick from because of the
209         // America/Denver & America/Phoenix ambiguity.
210         {
211             String nitzString = "15/01/01,00:00:00-28,0"; // 2015-01-01 00:00:00 UTC, UTC-7, not DST
212             NitzData nitzData = NitzData.parse(nitzString);
213             OffsetResult offsetResult = mTimeZoneLookupHelper.lookupByNitzCountry(nitzData, usIso);
214             assertFalse(offsetResult.isOnlyMatch());
215         }
216     }
217 
218     @Test
testLookupByNitzCountry_dstKnownAndUnknown()219     public void testLookupByNitzCountry_dstKnownAndUnknown() {
220         // Historical dates are used to avoid the test breaking due to data changes.
221         // However, algorithm updates may change the exact time zone returned, though it shouldn't
222         // ever be a less exact match.
223         long nhSummerTimeMillis = createUnixEpochTime(2015, 6, 20, 1, 2, 3);
224         long nhWinterTimeMillis = createUnixEpochTime(2015, 1, 20, 1, 2, 3);
225 
226         // A country in the northern hemisphere with one time zone.
227         String adIso = "AD"; // Andora
228         String summerTimeNitzString = "15/06/20,01:02:03+8"; // 2015-06-20 01:02:03 UTC, UTC+2
229         String winterTimeNitzString = "15/01/20,01:02:03+4"; // 2015-01-20 01:02:03 UTC, UTC+1
230 
231         // Summer, known & correct DST state (DST == true).
232         {
233             String summerTimeNitzStringWithDst = summerTimeNitzString + ",1";
234             NitzData nitzData = NitzData.parse(summerTimeNitzStringWithDst);
235             int expectedUtcOffset = (int) TimeUnit.HOURS.toMillis(2);
236             Integer expectedDstOffset = (int) TimeUnit.HOURS.toMillis(1);
237             assertEquals(expectedUtcOffset, nitzData.getLocalOffsetMillis());
238             assertEquals(expectedDstOffset, nitzData.getDstAdjustmentMillis());
239 
240             OffsetResult adSummerWithDstResult =
241                     mTimeZoneLookupHelper.lookupByNitzCountry(nitzData, adIso);
242             OffsetResult expectedResult =
243                     new OffsetResult(zone("Europe/Andorra"), true /* isOnlyMatch */);
244             assertEquals(expectedResult, adSummerWithDstResult);
245             assertOffsetResultZoneOffsets(nhSummerTimeMillis, expectedUtcOffset, expectedDstOffset,
246                     adSummerWithDstResult);
247         }
248 
249         // Summer, known & incorrect DST state (DST == false)
250         {
251             String summerTimeNitzStringWithNoDst = summerTimeNitzString + ",0";
252             NitzData nitzData = NitzData.parse(summerTimeNitzStringWithNoDst);
253 
254             OffsetResult adSummerWithNoDstResult =
255                     mTimeZoneLookupHelper.lookupByNitzCountry(nitzData, adIso);
256             assertNull(adSummerWithNoDstResult);
257         }
258 
259         // Winter, known & correct DST state (DST == false)
260         {
261             String winterTimeNitzStringWithNoDst = winterTimeNitzString + ",0";
262             NitzData nitzData = NitzData.parse(winterTimeNitzStringWithNoDst);
263             int expectedUtcOffset = (int) TimeUnit.HOURS.toMillis(1);
264             Integer expectedDstOffset = 0;
265             assertEquals(expectedUtcOffset, nitzData.getLocalOffsetMillis());
266             assertEquals(expectedDstOffset, nitzData.getDstAdjustmentMillis());
267 
268             OffsetResult adWinterWithDstResult =
269                     mTimeZoneLookupHelper.lookupByNitzCountry(nitzData, adIso);
270             OffsetResult expectedResult =
271                     new OffsetResult(zone("Europe/Andorra"), true /* isOnlyMatch */);
272             assertEquals(expectedResult, adWinterWithDstResult);
273             assertOffsetResultZoneOffsets(nhWinterTimeMillis, expectedUtcOffset, expectedDstOffset,
274                     adWinterWithDstResult);
275         }
276 
277         // Winter, known & incorrect DST state (DST == true)
278         {
279             String winterTimeNitzStringWithDst = winterTimeNitzString + ",1";
280             NitzData nitzData = NitzData.parse(winterTimeNitzStringWithDst);
281 
282             OffsetResult adWinterWithDstResult =
283                     mTimeZoneLookupHelper.lookupByNitzCountry(nitzData, adIso);
284             assertNull(adWinterWithDstResult);
285         }
286 
287         // Summer, unknown DST state (will match any DST state with the correct offset).
288         {
289             NitzData nitzData = NitzData.parse(summerTimeNitzString);
290             int expectedUtcOffset = (int) TimeUnit.HOURS.toMillis(2);
291             Integer expectedDstOffset = null; // Unknown
292             assertEquals(expectedUtcOffset, nitzData.getLocalOffsetMillis());
293             assertEquals(expectedDstOffset, nitzData.getDstAdjustmentMillis());
294 
295             OffsetResult adSummerUnknownDstResult =
296                     mTimeZoneLookupHelper.lookupByNitzCountry(nitzData, adIso);
297             OffsetResult expectedResult =
298                     new OffsetResult(zone("Europe/Andorra"), true /* isOnlyMatch */);
299             assertEquals(expectedResult, adSummerUnknownDstResult);
300             assertOffsetResultZoneOffsets(nhSummerTimeMillis, expectedUtcOffset, expectedDstOffset,
301                     adSummerUnknownDstResult);
302         }
303 
304         // Winter, unknown DST state (will match any DST state with the correct offset)
305         {
306             NitzData nitzData = NitzData.parse(winterTimeNitzString);
307             int expectedUtcOffset = (int) TimeUnit.HOURS.toMillis(1);
308             Integer expectedDstOffset = null; // Unknown
309             assertEquals(expectedUtcOffset, nitzData.getLocalOffsetMillis());
310             assertEquals(expectedDstOffset, nitzData.getDstAdjustmentMillis());
311 
312             OffsetResult adWinterUnknownDstResult =
313                     mTimeZoneLookupHelper.lookupByNitzCountry(nitzData, adIso);
314             OffsetResult expectedResult =
315                     new OffsetResult(zone("Europe/Andorra"), true /* isOnlyMatch */);
316             assertEquals(expectedResult, adWinterUnknownDstResult);
317             assertOffsetResultZoneOffsets(nhWinterTimeMillis, expectedUtcOffset, expectedDstOffset,
318                     adWinterUnknownDstResult);
319         }
320     }
321 
322     @Test
testLookupByCountry_oneZone()323     public void testLookupByCountry_oneZone() {
324         // GB has one time zone.
325         CountryResult expectedResult =
326                 new CountryResult("Europe/London", QUALITY_SINGLE_ZONE, ARBITRARY_DEBUG_INFO);
327         assertEquals(expectedResult,
328                 mTimeZoneLookupHelper.lookupByCountry("gb", NH_SUMMER_TIME_MILLIS));
329         assertEquals(expectedResult,
330                 mTimeZoneLookupHelper.lookupByCountry("gb", NH_WINTER_TIME_MILLIS));
331     }
332 
333     @Test
testLookupByCountry_oneEffectiveZone()334     public void testLookupByCountry_oneEffectiveZone() {
335         // Historical dates are used to avoid the test breaking due to data changes.
336 
337         // DE has two time zones according to IANA data: Europe/Berlin and Europe/Busingen, but they
338         // become effectively identical after 338950800000 millis (Sun, 28 Sep 1980 01:00:00 GMT).
339         // Android data tells us that Europe/Berlin the one that was "kept".
340         long nhSummerTimeMillis = createUnixEpochTime(1975, 6, 20, 1, 2, 3);
341         long nhWinterTimeMillis = createUnixEpochTime(1975, 1, 20, 1, 2, 3);
342 
343         // Before 1980, quality == QUALITY_MULTIPLE_ZONES_SAME_OFFSET because Europe/Busingen was
344         // relevant.
345         CountryResult expectedResult = new CountryResult(
346                 "Europe/Berlin", QUALITY_MULTIPLE_ZONES_SAME_OFFSET, ARBITRARY_DEBUG_INFO);
347         assertEquals(expectedResult,
348                 mTimeZoneLookupHelper.lookupByCountry("de", nhSummerTimeMillis));
349         assertEquals(expectedResult,
350                 mTimeZoneLookupHelper.lookupByCountry("de", nhWinterTimeMillis));
351 
352         // And in 2015, quality == QUALITY_SINGLE_ZONE because Europe/Busingen became irrelevant
353         // after 1980.
354         nhSummerTimeMillis = createUnixEpochTime(2015, 6, 20, 1, 2, 3);
355         nhWinterTimeMillis = createUnixEpochTime(2015, 1, 20, 1, 2, 3);
356 
357         expectedResult =
358                 new CountryResult("Europe/Berlin", QUALITY_SINGLE_ZONE, ARBITRARY_DEBUG_INFO);
359         assertEquals(expectedResult,
360                 mTimeZoneLookupHelper.lookupByCountry("de", nhSummerTimeMillis));
361         assertEquals(expectedResult,
362                 mTimeZoneLookupHelper.lookupByCountry("de", nhWinterTimeMillis));
363     }
364 
365     @Test
testDefaultBoostBehavior()366     public void testDefaultBoostBehavior() {
367         long timeMillis = createUnixEpochTime(2015, 6, 20, 1, 2, 3);
368 
369         // An example known to be explicitly boosted. New Zealand has two zones but the vast
370         // majority of the population use one of them so Android's data file explicitly boosts the
371         // country default. If that changes in future this test will need to be changed to use
372         // another example.
373         String countryIsoCode = "nz";
374 
375         CountryResult expectedResult = new CountryResult(
376                 "Pacific/Auckland", QUALITY_DEFAULT_BOOSTED, ARBITRARY_DEBUG_INFO);
377         assertEquals(expectedResult,
378                 mTimeZoneLookupHelper.lookupByCountry(countryIsoCode, timeMillis));
379 
380         // Data correct for the North and South Island.
381         int majorityWinterOffset = (int) TimeUnit.HOURS.toMillis(12);
382         NitzData majorityNitzData = NitzData.createForTests(
383                 majorityWinterOffset, 0, timeMillis, null /* emulatorTimeZone */);
384 
385         // Boost doesn't directly affect lookupByNitzCountry()
386         OffsetResult majorityOffsetResult =
387                 mTimeZoneLookupHelper.lookupByNitzCountry(majorityNitzData, countryIsoCode);
388         assertEquals(zone("Pacific/Auckland"), majorityOffsetResult.getTimeZone());
389         assertTrue(majorityOffsetResult.isOnlyMatch());
390 
391         // Data correct for the Chatham Islands.
392         int chathamWinterOffset = majorityWinterOffset + ((int) TimeUnit.MINUTES.toMillis(45));
393         NitzData chathamNitzData = NitzData.createForTests(
394                 chathamWinterOffset, 0, timeMillis, null /* emulatorTimeZone */);
395         OffsetResult chathamOffsetResult =
396                 mTimeZoneLookupHelper.lookupByNitzCountry(chathamNitzData, countryIsoCode);
397         assertEquals(zone("Pacific/Chatham"), chathamOffsetResult.getTimeZone());
398         assertTrue(chathamOffsetResult.isOnlyMatch());
399 
400         // NITZ data that makes no sense for NZ results in no match.
401         int nonsenseOffset = (int) TimeUnit.HOURS.toMillis(5);
402         NitzData nonsenseNitzData = NitzData.createForTests(
403                 nonsenseOffset, 0, timeMillis, null /* emulatorTimeZone */);
404         OffsetResult nonsenseOffsetResult =
405                 mTimeZoneLookupHelper.lookupByNitzCountry(nonsenseNitzData, countryIsoCode);
406         assertNull(nonsenseOffsetResult);
407     }
408 
409     @Test
testNoDefaultBoostBehavior()410     public void testNoDefaultBoostBehavior() {
411         long timeMillis = createUnixEpochTime(2015, 6, 20, 1, 2, 3);
412 
413         // An example known to not be explicitly boosted. Micronesia is spread out and there's no
414         // suitable default.
415         String countryIsoCode = "fm";
416 
417         CountryResult expectedResult = new CountryResult(
418                 "Pacific/Pohnpei", QUALITY_MULTIPLE_ZONES_DIFFERENT_OFFSETS, ARBITRARY_DEBUG_INFO);
419         assertEquals(expectedResult,
420                 mTimeZoneLookupHelper.lookupByCountry(countryIsoCode, timeMillis));
421 
422         // Prove an OffsetResult can be found with the correct offset.
423         int chuukWinterOffset = (int) TimeUnit.HOURS.toMillis(10);
424         NitzData chuukNitzData = NitzData.createForTests(
425                 chuukWinterOffset, 0, timeMillis, null /* emulatorTimeZone */);
426         OffsetResult chuukOffsetResult =
427                 mTimeZoneLookupHelper.lookupByNitzCountry(chuukNitzData, countryIsoCode);
428         assertEquals(zone("Pacific/Chuuk"), chuukOffsetResult.getTimeZone());
429         assertTrue(chuukOffsetResult.isOnlyMatch());
430 
431         // NITZ data that makes no sense for FM: no boost means we should get nothing.
432         int nonsenseOffset = (int) TimeUnit.HOURS.toMillis(5);
433         NitzData nonsenseNitzData = NitzData.createForTests(
434                 nonsenseOffset, 0, timeMillis, null /* emulatorTimeZone */);
435         OffsetResult nonsenseOffsetResult =
436                 mTimeZoneLookupHelper.lookupByNitzCountry(nonsenseNitzData, countryIsoCode);
437         assertNull(nonsenseOffsetResult);
438     }
439 
440     @Test
testLookupByCountry_multipleZones()441     public void testLookupByCountry_multipleZones() {
442         // US has many time zones that have different offsets.
443         CountryResult expectedResult = new CountryResult(
444                 "America/New_York", QUALITY_MULTIPLE_ZONES_DIFFERENT_OFFSETS, ARBITRARY_DEBUG_INFO);
445         assertEquals(expectedResult,
446                 mTimeZoneLookupHelper.lookupByCountry("us", NH_SUMMER_TIME_MILLIS));
447         assertEquals(expectedResult,
448                 mTimeZoneLookupHelper.lookupByCountry("us", NH_WINTER_TIME_MILLIS));
449     }
450 
451     @Test
testCountryUsesUtc()452     public void testCountryUsesUtc() {
453         assertFalse(mTimeZoneLookupHelper.countryUsesUtc("us", NH_SUMMER_TIME_MILLIS));
454         assertFalse(mTimeZoneLookupHelper.countryUsesUtc("us", NH_WINTER_TIME_MILLIS));
455         assertFalse(mTimeZoneLookupHelper.countryUsesUtc("gb", NH_SUMMER_TIME_MILLIS));
456         assertTrue(mTimeZoneLookupHelper.countryUsesUtc("gb", NH_WINTER_TIME_MILLIS));
457     }
458 
459     @Test
regressionTest_Bug167653885()460     public void regressionTest_Bug167653885() {
461         // This NITZ caused an error in Android R because lookupByNitz was returning a time zone
462         // known to android.icu.util.TimeZone but not java.util.TimeZone.
463         NitzData nitzData = NitzData.parse("20/05/08,04:15:48+08,00");
464         OffsetResult offsetResult = mTimeZoneLookupHelper.lookupByNitz(nitzData);
465         assertNotNull(offsetResult);
466 
467         List<String> knownIds = Arrays.asList(java.util.TimeZone.getAvailableIDs());
468         assertTrue(knownIds.contains(offsetResult.getTimeZone().getID()));
469     }
470 
471     /**
472      * Assert the time zone in the OffsetResult has the expected properties at the specified time.
473      */
assertOffsetResultZoneOffsets(long time, int expectedOffsetAtTime, Integer expectedDstAtTime, OffsetResult lookupResult)474     private static void assertOffsetResultZoneOffsets(long time, int expectedOffsetAtTime,
475             Integer expectedDstAtTime, OffsetResult lookupResult) {
476 
477         TimeZone timeZone = lookupResult.getTimeZone();
478         GregorianCalendar calendar = new GregorianCalendar(timeZone);
479         calendar.setTimeInMillis(time);
480         int actualOffsetAtTime =
481                 calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
482         assertEquals(expectedOffsetAtTime, actualOffsetAtTime);
483 
484         if (expectedDstAtTime != null) {
485             Date date = new Date(time);
486             assertEquals(expectedDstAtTime > 0, timeZone.inDaylightTime(date));
487 
488             // The code under test assumes DST means +1 in all cases,
489             // This code makes fewer assumptions.
490             assertEquals(expectedDstAtTime.intValue(), calendar.get(Calendar.DST_OFFSET));
491         }
492     }
493 
assertOffsetResultMetadata(boolean isOnlyMatch, OffsetResult lookupResult)494     private static void assertOffsetResultMetadata(boolean isOnlyMatch, OffsetResult lookupResult) {
495         assertEquals(isOnlyMatch, lookupResult.isOnlyMatch());
496     }
497 
createUnixEpochTime( int year, int monthOfYear, int dayOfMonth, int hourOfDay, int minute, int second)498     private static long createUnixEpochTime(
499             int year, int monthOfYear, int dayOfMonth, int hourOfDay, int minute, int second) {
500         GregorianCalendar calendar = new GregorianCalendar(zone("UTC"));
501         calendar.clear(); // Clear millis, etc.
502         calendar.set(year, monthOfYear - 1, dayOfMonth, hourOfDay, minute, second);
503         return calendar.getTimeInMillis();
504     }
505 
zone(String zoneId)506     private static TimeZone zone(String zoneId) {
507         return TimeZone.getFrozenTimeZone(zoneId);
508     }
509 }
510