1 /*
2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.telephony.nitz;
18 
19 import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.createEmptySuggestion;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
24 import android.text.TextUtils;
25 import android.timezone.CountryTimeZones.OffsetResult;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 import com.android.internal.telephony.NitzData;
29 import com.android.internal.telephony.NitzSignal;
30 import com.android.internal.telephony.NitzStateMachine.DeviceState;
31 import com.android.internal.telephony.nitz.NitzStateMachineImpl.TimeZoneSuggester;
32 import com.android.internal.telephony.nitz.TimeZoneLookupHelper.CountryResult;
33 import com.android.telephony.Rlog;
34 
35 import java.util.Objects;
36 
37 /**
38  * The real implementation of {@link TimeZoneSuggester}.
39  */
40 @VisibleForTesting
41 public class TimeZoneSuggesterImpl implements TimeZoneSuggester {
42 
43     private static final String LOG_TAG = NitzStateMachineImpl.LOG_TAG;
44 
45     private final DeviceState mDeviceState;
46     private final TimeZoneLookupHelper mTimeZoneLookupHelper;
47 
48     @VisibleForTesting
TimeZoneSuggesterImpl( @onNull DeviceState deviceState, @NonNull TimeZoneLookupHelper timeZoneLookupHelper)49     public TimeZoneSuggesterImpl(
50             @NonNull DeviceState deviceState, @NonNull TimeZoneLookupHelper timeZoneLookupHelper) {
51         mDeviceState = Objects.requireNonNull(deviceState);
52         mTimeZoneLookupHelper = Objects.requireNonNull(timeZoneLookupHelper);
53     }
54 
55     @Override
56     @NonNull
getTimeZoneSuggestion(int slotIndex, @Nullable String countryIsoCode, @Nullable NitzSignal nitzSignal)57     public TelephonyTimeZoneSuggestion getTimeZoneSuggestion(int slotIndex,
58             @Nullable String countryIsoCode, @Nullable NitzSignal nitzSignal) {
59         try {
60             // Check for overriding NITZ-based signals from Android running in an emulator.
61             TelephonyTimeZoneSuggestion overridingSuggestion = null;
62             if (nitzSignal != null) {
63                 NitzData nitzData = nitzSignal.getNitzData();
64                 if (nitzData.getEmulatorHostTimeZone() != null) {
65                     TelephonyTimeZoneSuggestion.Builder builder =
66                             new TelephonyTimeZoneSuggestion.Builder(slotIndex)
67                             .setZoneId(nitzData.getEmulatorHostTimeZone().getID())
68                             .setMatchType(TelephonyTimeZoneSuggestion.MATCH_TYPE_EMULATOR_ZONE_ID)
69                             .setQuality(TelephonyTimeZoneSuggestion.QUALITY_SINGLE_ZONE)
70                             .addDebugInfo("Emulator time zone override: " + nitzData);
71                     overridingSuggestion = builder.build();
72                 }
73             }
74 
75             TelephonyTimeZoneSuggestion suggestion;
76             if (overridingSuggestion != null) {
77                 suggestion = overridingSuggestion;
78             } else if (countryIsoCode == null) {
79                 if (nitzSignal == null) {
80                     suggestion = createEmptySuggestion(slotIndex,
81                             "getTimeZoneSuggestion: nitzSignal=null, countryIsoCode=null");
82                 } else {
83                     // NITZ only - wait until we have a country.
84                     suggestion = createEmptySuggestion(slotIndex, "getTimeZoneSuggestion:"
85                             + " nitzSignal=" + nitzSignal + ", countryIsoCode=null");
86                 }
87             } else { // countryIsoCode != null
88                 if (nitzSignal == null) {
89                     if (countryIsoCode.isEmpty()) {
90                         // This is assumed to be a test network with no NITZ data to go on.
91                         suggestion = createEmptySuggestion(slotIndex,
92                                 "getTimeZoneSuggestion: nitzSignal=null, countryIsoCode=\"\"");
93                     } else {
94                         // Country only
95                         suggestion = findTimeZoneFromNetworkCountryCode(
96                                 slotIndex, countryIsoCode, mDeviceState.currentTimeMillis());
97                     }
98                 } else { // nitzSignal != null
99                     if (countryIsoCode.isEmpty()) {
100                         // We have been told we have a country code but it's empty. This is most
101                         // likely because we're on a test network that's using a bogus MCC
102                         // (eg, "001"). Obtain a TimeZone based only on the NITZ parameters: without
103                         // a country it will be arbitrary, but it should at least have the correct
104                         // offset.
105                         suggestion = findTimeZoneForTestNetwork(slotIndex, nitzSignal);
106                     } else {
107                         // We have both NITZ and Country code.
108                         suggestion = findTimeZoneFromCountryAndNitz(
109                                 slotIndex, countryIsoCode, nitzSignal);
110                     }
111                 }
112             }
113 
114             // Ensure the return value is never null.
115             Objects.requireNonNull(suggestion);
116 
117             return suggestion;
118         } catch (RuntimeException e) {
119             // This would suggest a coding error. Log at a high level and try to avoid leaving the
120             // device in a bad state by making an "empty" suggestion.
121             String message = "getTimeZoneSuggestion: Error during lookup: "
122                     + " countryIsoCode=" + countryIsoCode
123                     + ", nitzSignal=" + nitzSignal
124                     + ", e=" + e.getMessage();
125             TelephonyTimeZoneSuggestion errorSuggestion = createEmptySuggestion(slotIndex, message);
126             Rlog.w(LOG_TAG, message, e);
127             return errorSuggestion;
128         }
129     }
130 
131     /**
132      * Creates a {@link TelephonyTimeZoneSuggestion} using only NITZ. This happens when the device
133      * is attached to a test cell with an unrecognized MCC. In these cases we try to return a
134      * suggestion for an arbitrary time zone that matches the NITZ offset information.
135      */
136     @NonNull
findTimeZoneForTestNetwork( int slotIndex, @NonNull NitzSignal nitzSignal)137     private TelephonyTimeZoneSuggestion findTimeZoneForTestNetwork(
138             int slotIndex, @NonNull NitzSignal nitzSignal) {
139         Objects.requireNonNull(nitzSignal);
140         NitzData nitzData = Objects.requireNonNull(nitzSignal.getNitzData());
141 
142         TelephonyTimeZoneSuggestion.Builder suggestionBuilder =
143                 new TelephonyTimeZoneSuggestion.Builder(slotIndex);
144         suggestionBuilder.addDebugInfo("findTimeZoneForTestNetwork: nitzSignal=" + nitzSignal);
145         OffsetResult lookupResult =
146                 mTimeZoneLookupHelper.lookupByNitz(nitzData);
147         if (lookupResult == null) {
148             suggestionBuilder.addDebugInfo("findTimeZoneForTestNetwork: No zone found");
149         } else {
150             suggestionBuilder.setZoneId(lookupResult.getTimeZone().getID());
151             suggestionBuilder.setMatchType(
152                     TelephonyTimeZoneSuggestion.MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY);
153             int quality = lookupResult.isOnlyMatch()
154                     ? TelephonyTimeZoneSuggestion.QUALITY_SINGLE_ZONE
155                     : TelephonyTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET;
156             suggestionBuilder.setQuality(quality);
157             suggestionBuilder.addDebugInfo(
158                     "findTimeZoneForTestNetwork: lookupResult=" + lookupResult);
159         }
160         return suggestionBuilder.build();
161     }
162 
163     /**
164      * Creates a {@link TelephonyTimeZoneSuggestion} using network country code and NITZ.
165      */
166     @NonNull
findTimeZoneFromCountryAndNitz( int slotIndex, @NonNull String countryIsoCode, @NonNull NitzSignal nitzSignal)167     private TelephonyTimeZoneSuggestion findTimeZoneFromCountryAndNitz(
168             int slotIndex, @NonNull String countryIsoCode,
169             @NonNull NitzSignal nitzSignal) {
170         Objects.requireNonNull(countryIsoCode);
171         Objects.requireNonNull(nitzSignal);
172 
173         TelephonyTimeZoneSuggestion.Builder suggestionBuilder =
174                 new TelephonyTimeZoneSuggestion.Builder(slotIndex);
175         suggestionBuilder.addDebugInfo("findTimeZoneFromCountryAndNitz:"
176                 + " countryIsoCode=" + countryIsoCode
177                 + ", nitzSignal=" + nitzSignal);
178         NitzData nitzData = Objects.requireNonNull(nitzSignal.getNitzData());
179         if (isNitzSignalOffsetInfoBogus(countryIsoCode, nitzData)) {
180             suggestionBuilder.addDebugInfo(
181                     "findTimeZoneFromCountryAndNitz: NITZ signal looks bogus");
182             return suggestionBuilder.build();
183         }
184 
185         // Try to find a match using both country + NITZ signal.
186         OffsetResult lookupResult =
187                 mTimeZoneLookupHelper.lookupByNitzCountry(nitzData, countryIsoCode);
188         if (lookupResult != null) {
189             suggestionBuilder.setZoneId(lookupResult.getTimeZone().getID());
190             suggestionBuilder.setMatchType(
191                     TelephonyTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET);
192             int quality = lookupResult.isOnlyMatch()
193                     ? TelephonyTimeZoneSuggestion.QUALITY_SINGLE_ZONE
194                     : TelephonyTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET;
195             suggestionBuilder.setQuality(quality);
196             suggestionBuilder.addDebugInfo("findTimeZoneFromCountryAndNitz:"
197                     + " lookupResult=" + lookupResult);
198             return suggestionBuilder.build();
199         }
200 
201         // The country + offset provided no match, so see if the country by itself would be enough.
202         CountryResult countryResult = mTimeZoneLookupHelper.lookupByCountry(
203                 countryIsoCode, nitzData.getCurrentTimeInMillis());
204         if (countryResult == null) {
205             // Country not recognized.
206             suggestionBuilder.addDebugInfo(
207                     "findTimeZoneFromCountryAndNitz: lookupByCountry() country not recognized");
208             return suggestionBuilder.build();
209         }
210 
211         // If the country has a single zone, or it has multiple zones but the default zone is
212         // "boosted" (i.e. the country default is considered a good suggestion in most cases) then
213         // use it.
214         if (countryResult.quality == CountryResult.QUALITY_SINGLE_ZONE
215                 || countryResult.quality == CountryResult.QUALITY_DEFAULT_BOOSTED) {
216             suggestionBuilder.setZoneId(countryResult.zoneId);
217             suggestionBuilder.setMatchType(
218                     TelephonyTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_ONLY);
219             suggestionBuilder.setQuality(TelephonyTimeZoneSuggestion.QUALITY_SINGLE_ZONE);
220             suggestionBuilder.addDebugInfo(
221                     "findTimeZoneFromCountryAndNitz: high quality country-only suggestion:"
222                             + " countryResult=" + countryResult);
223             return suggestionBuilder.build();
224         }
225 
226         // Quality is not high enough to set the zone using country only.
227         suggestionBuilder.addDebugInfo("findTimeZoneFromCountryAndNitz: country-only suggestion"
228                 + " quality not high enough. countryResult=" + countryResult);
229         return suggestionBuilder.build();
230     }
231 
232     /**
233      * Creates a {@link TelephonyTimeZoneSuggestion} using only network country code; works well on
234      * countries which only have one time zone or multiple zones with the same offset.
235      *
236      * @param countryIsoCode country code from network MCC
237      * @param whenMillis the time to use when looking at time zone rules data
238      */
239     @NonNull
findTimeZoneFromNetworkCountryCode( int slotIndex, @NonNull String countryIsoCode, long whenMillis)240     private TelephonyTimeZoneSuggestion findTimeZoneFromNetworkCountryCode(
241             int slotIndex, @NonNull String countryIsoCode, long whenMillis) {
242         Objects.requireNonNull(countryIsoCode);
243         if (TextUtils.isEmpty(countryIsoCode)) {
244             throw new IllegalArgumentException("countryIsoCode must not be empty");
245         }
246 
247         TelephonyTimeZoneSuggestion.Builder suggestionBuilder =
248                 new TelephonyTimeZoneSuggestion.Builder(slotIndex);
249         suggestionBuilder.addDebugInfo("findTimeZoneFromNetworkCountryCode:"
250                 + " whenMillis=" + whenMillis + ", countryIsoCode=" + countryIsoCode);
251         CountryResult lookupResult = mTimeZoneLookupHelper.lookupByCountry(
252                 countryIsoCode, whenMillis);
253         if (lookupResult != null) {
254             suggestionBuilder.setZoneId(lookupResult.zoneId);
255             suggestionBuilder.setMatchType(
256                     TelephonyTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_ONLY);
257 
258             int quality;
259             if (lookupResult.quality == CountryResult.QUALITY_SINGLE_ZONE
260                     || lookupResult.quality == CountryResult.QUALITY_DEFAULT_BOOSTED) {
261                 quality = TelephonyTimeZoneSuggestion.QUALITY_SINGLE_ZONE;
262             } else if (lookupResult.quality == CountryResult.QUALITY_MULTIPLE_ZONES_SAME_OFFSET) {
263                 quality = TelephonyTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET;
264             } else if (lookupResult.quality
265                     == CountryResult.QUALITY_MULTIPLE_ZONES_DIFFERENT_OFFSETS) {
266                 quality = TelephonyTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS;
267             } else {
268                 // This should never happen.
269                 throw new IllegalArgumentException(
270                         "lookupResult.quality not recognized: countryIsoCode=" + countryIsoCode
271                                 + ", whenMillis=" + whenMillis + ", lookupResult=" + lookupResult);
272             }
273             suggestionBuilder.setQuality(quality);
274             suggestionBuilder.addDebugInfo(
275                     "findTimeZoneFromNetworkCountryCode: lookupResult=" + lookupResult);
276         } else {
277             suggestionBuilder.addDebugInfo(
278                     "findTimeZoneFromNetworkCountryCode: Country not recognized?");
279         }
280         return suggestionBuilder.build();
281     }
282 
283     /**
284      * Returns true if the NITZ signal is definitely bogus, assuming that the country is correct.
285      */
isNitzSignalOffsetInfoBogus(String countryIsoCode, NitzData nitzData)286     private boolean isNitzSignalOffsetInfoBogus(String countryIsoCode, NitzData nitzData) {
287         if (TextUtils.isEmpty(countryIsoCode)) {
288             // We cannot say for sure.
289             return false;
290         }
291 
292         boolean zeroOffsetNitz = nitzData.getLocalOffsetMillis() == 0;
293         return zeroOffsetNitz && !countryUsesUtc(countryIsoCode, nitzData);
294     }
295 
countryUsesUtc(String countryIsoCode, NitzData nitzData)296     private boolean countryUsesUtc(String countryIsoCode, NitzData nitzData) {
297         return mTimeZoneLookupHelper.countryUsesUtc(
298                 countryIsoCode, nitzData.getCurrentTimeInMillis());
299     }
300 }
301