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