1 /*
2  * Copyright (C) 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 
17 package android.timezone;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.icu.util.TimeZone;
22 
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.List;
26 import java.util.Objects;
27 
28 /**
29  * Information about a country's time zones.
30  *
31  * @hide
32  */
33 public final class CountryTimeZones {
34 
35     /**
36      * A mapping to a time zone ID with some associated metadata.
37      *
38      * @hide
39      */
40     public static final class TimeZoneMapping {
41 
42         @NonNull
43         private com.android.i18n.timezone.CountryTimeZones.TimeZoneMapping mDelegate;
44 
TimeZoneMapping(com.android.i18n.timezone.CountryTimeZones.TimeZoneMapping delegate)45         TimeZoneMapping(com.android.i18n.timezone.CountryTimeZones.TimeZoneMapping delegate) {
46             this.mDelegate = Objects.requireNonNull(delegate);
47         }
48 
49         /**
50          * Returns the ID for this mapping. The ID is a tzdb time zone identifier like
51          * "America/Los_Angeles" that can be used with methods such as {@link
52          * TimeZone#getFrozenTimeZone(String)}. See {@link #getTimeZone()} which returns a frozen
53          * {@link TimeZone} object.
54          */
55         @NonNull
getTimeZoneId()56         public String getTimeZoneId() {
57             return mDelegate.getTimeZoneId();
58         }
59 
60         /**
61          * Returns a frozen {@link TimeZone} object for this mapping.
62          */
63         @NonNull
getTimeZone()64         public TimeZone getTimeZone() {
65             return mDelegate.getTimeZone();
66         }
67 
68         @Override
equals(Object o)69         public boolean equals(Object o) {
70             if (this == o) {
71                 return true;
72             }
73             if (o == null || getClass() != o.getClass()) {
74                 return false;
75             }
76             TimeZoneMapping that = (TimeZoneMapping) o;
77             return this.mDelegate.equals(that.mDelegate);
78         }
79 
80         @Override
hashCode()81         public int hashCode() {
82             return this.mDelegate.hashCode();
83         }
84 
85         @Override
toString()86         public String toString() {
87             return mDelegate.toString();
88         }
89     }
90 
91     /**
92      * The result of lookup up a time zone using offset information (and possibly more).
93      *
94      * @hide
95      */
96     public static final class OffsetResult {
97 
98         private final TimeZone mTimeZone;
99         private final boolean mIsOnlyMatch;
100 
101         /** Creates an instance with the supplied information. */
OffsetResult(@onNull TimeZone timeZone, boolean isOnlyMatch)102         public OffsetResult(@NonNull TimeZone timeZone, boolean isOnlyMatch) {
103             mTimeZone = Objects.requireNonNull(timeZone);
104             mIsOnlyMatch = isOnlyMatch;
105         }
106 
107         /**
108          * Returns a time zone that matches the supplied criteria.
109          */
110         @NonNull
getTimeZone()111         public TimeZone getTimeZone() {
112             return mTimeZone;
113         }
114 
115         /**
116          * Returns {@code true} if there is only one matching time zone for the supplied criteria.
117          */
isOnlyMatch()118         public boolean isOnlyMatch() {
119             return mIsOnlyMatch;
120         }
121 
122         @Override
equals(Object o)123         public boolean equals(Object o) {
124             if (this == o) {
125                 return true;
126             }
127             if (o == null || getClass() != o.getClass()) {
128                 return false;
129             }
130             OffsetResult that = (OffsetResult) o;
131             return mIsOnlyMatch == that.mIsOnlyMatch
132                     && mTimeZone.getID().equals(that.mTimeZone.getID());
133         }
134 
135         @Override
hashCode()136         public int hashCode() {
137             return Objects.hash(mTimeZone, mIsOnlyMatch);
138         }
139 
140         @Override
toString()141         public String toString() {
142             return "OffsetResult{"
143                     + "mTimeZone(ID)=" + mTimeZone.getID()
144                     + ", mIsOnlyMatch=" + mIsOnlyMatch
145                     + '}';
146         }
147     }
148 
149     @NonNull
150     private final com.android.i18n.timezone.CountryTimeZones mDelegate;
151 
CountryTimeZones(com.android.i18n.timezone.CountryTimeZones delegate)152     CountryTimeZones(com.android.i18n.timezone.CountryTimeZones delegate) {
153         mDelegate = delegate;
154     }
155 
156     /**
157      * Returns true if the ISO code for the country is a case-insensitive match for the one
158      * supplied.
159      */
matchesCountryCode(@onNull String countryIso)160     public boolean matchesCountryCode(@NonNull String countryIso) {
161         return mDelegate.matchesCountryCode(countryIso);
162     }
163 
164     /**
165      * Returns the default time zone ID for the country. Can return {@code null} in cases when no
166      * data is available or the time zone ID was not recognized.
167      */
168     @Nullable
getDefaultTimeZoneId()169     public String getDefaultTimeZoneId() {
170         return mDelegate.getDefaultTimeZoneId();
171     }
172 
173     /**
174      * Returns the default time zone for the country. Can return {@code null} in cases when no data
175      * is available or the time zone ID was not recognized.
176      */
177     @Nullable
getDefaultTimeZone()178     public TimeZone getDefaultTimeZone() {
179         return mDelegate.getDefaultTimeZone();
180     }
181 
182     /**
183      * Qualifier for a country's default time zone. {@code true} indicates that the country's
184      * default time zone would be a good choice <em>generally</em> when there's no UTC offset
185      * information available. This will only be {@code true} in countries with multiple zones where
186      * a large majority of the population is covered by only one of them.
187      */
isDefaultTimeZoneBoosted()188     public boolean isDefaultTimeZoneBoosted() {
189         return mDelegate.isDefaultTimeZoneBoosted();
190     }
191 
192     /**
193      * Returns {@code true} if the country has at least one time zone that uses UTC at the given
194      * time. This is an efficient check when trying to validate received UTC offset information.
195      * For example, there are situations when a detected zero UTC offset cannot be distinguished
196      * from "no information available" or a corrupted signal. This method is useful because checking
197      * offset information for large countries is relatively expensive but it is generally only the
198      * countries close to the prime meridian that use UTC at <em>any</em> time of the year.
199      *
200      * @param whenMillis the time the offset information is for in milliseconds since the beginning
201      *     of the Unix epoch
202      */
hasUtcZone(long whenMillis)203     public boolean hasUtcZone(long whenMillis) {
204         return mDelegate.hasUtcZone(whenMillis);
205     }
206 
207     /**
208      * Returns a time zone for the country, if there is one, that matches the supplied properties.
209      * If there are multiple matches and the {@code bias} is one of them then it is returned,
210      * otherwise an arbitrary match is returned based on the {@link
211      * #getEffectiveTimeZoneMappingsAt(long)} ordering.
212      *
213      * @param whenMillis the UTC time to match against
214      * @param bias the time zone to prefer, can be {@code null} to indicate there is no preference
215      * @param totalOffsetMillis the offset from UTC at {@code whenMillis}
216      * @param isDst the Daylight Savings Time state at {@code whenMillis}. {@code true} means DST,
217      *     {@code false} means not DST
218      * @return an {@link OffsetResult} with information about a matching zone, or {@code null} if
219      *     there is no match
220      */
221     @Nullable
lookupByOffsetWithBias(long whenMillis, @Nullable TimeZone bias, int totalOffsetMillis, boolean isDst)222     public OffsetResult lookupByOffsetWithBias(long whenMillis, @Nullable TimeZone bias,
223             int totalOffsetMillis, boolean isDst) {
224         com.android.i18n.timezone.CountryTimeZones.OffsetResult delegateOffsetResult =
225                 mDelegate.lookupByOffsetWithBias(
226                         whenMillis, bias, totalOffsetMillis, isDst);
227         return delegateOffsetResult == null ? null :
228                 new OffsetResult(
229                         delegateOffsetResult.getTimeZone(), delegateOffsetResult.isOnlyMatch());
230     }
231 
232     /**
233      * Returns a time zone for the country, if there is one, that matches the supplied properties.
234      * If there are multiple matches and the {@code bias} is one of them then it is returned,
235      * otherwise an arbitrary match is returned based on the {@link
236      * #getEffectiveTimeZoneMappingsAt(long)} ordering.
237      *
238      * @param whenMillis the UTC time to match against
239      * @param bias the time zone to prefer, can be {@code null} to indicate there is no preference
240      * @param totalOffsetMillis the offset from UTC at {@code whenMillis}
241      * @return an {@link OffsetResult} with information about a matching zone, or {@code null} if
242      *     there is no match
243      */
244     @Nullable
lookupByOffsetWithBias(long whenMillis, @Nullable TimeZone bias, int totalOffsetMillis)245     public OffsetResult lookupByOffsetWithBias(long whenMillis, @Nullable TimeZone bias,
246             int totalOffsetMillis) {
247         com.android.i18n.timezone.CountryTimeZones.OffsetResult delegateOffsetResult =
248                 mDelegate.lookupByOffsetWithBias(whenMillis, bias, totalOffsetMillis);
249         return delegateOffsetResult == null ? null :
250                 new OffsetResult(
251                         delegateOffsetResult.getTimeZone(), delegateOffsetResult.isOnlyMatch());
252     }
253 
254     /**
255      * Returns an immutable, ordered list of time zone mappings for the country in an undefined but
256      * "priority" order, filtered so that only "effective" time zone IDs are returned. An
257      * "effective" time zone is one that differs from another time zone used in the country after
258      * {@code whenMillis}. The list can be empty if there were no zones configured or the configured
259      * zone IDs were not recognized.
260      */
261     @NonNull
getEffectiveTimeZoneMappingsAt(long whenMillis)262     public List<TimeZoneMapping> getEffectiveTimeZoneMappingsAt(long whenMillis) {
263         List<com.android.i18n.timezone.CountryTimeZones.TimeZoneMapping> delegateList =
264                 mDelegate.getEffectiveTimeZoneMappingsAt(whenMillis);
265 
266         List<TimeZoneMapping> toReturn = new ArrayList<>(delegateList.size());
267         for (com.android.i18n.timezone.CountryTimeZones.TimeZoneMapping delegateMapping
268                 : delegateList) {
269             toReturn.add(new TimeZoneMapping(delegateMapping));
270         }
271         return Collections.unmodifiableList(toReturn);
272     }
273 
274     @Override
equals(Object o)275     public boolean equals(Object o) {
276         if (this == o) {
277             return true;
278         }
279         if (o == null || getClass() != o.getClass()) {
280             return false;
281         }
282         CountryTimeZones that = (CountryTimeZones) o;
283         return mDelegate.equals(that.mDelegate);
284     }
285 
286     @Override
hashCode()287     public int hashCode() {
288         return Objects.hash(mDelegate);
289     }
290 
291     @Override
toString()292     public String toString() {
293         return mDelegate.toString();
294     }
295 }
296