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