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 android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.timedetector.TelephonyTimeSuggestion; 22 import android.app.timezonedetector.TelephonyTimeZoneSuggestion; 23 import android.content.Context; 24 import android.os.TimestampedValue; 25 26 import com.android.internal.annotations.VisibleForTesting; 27 import com.android.internal.telephony.NitzData; 28 import com.android.internal.telephony.NitzSignal; 29 import com.android.internal.telephony.NitzStateMachine; 30 import com.android.internal.telephony.Phone; 31 import com.android.internal.util.IndentingPrintWriter; 32 import com.android.telephony.Rlog; 33 34 import java.io.FileDescriptor; 35 import java.io.PrintWriter; 36 import java.util.Objects; 37 38 /** 39 * An implementation of {@link NitzStateMachine} responsible for telephony time and time zone 40 * detection. 41 * 42 * <p>This implementation has a number of notable characteristics: 43 * <ul> 44 * <li>It is decomposed into multiple classes that perform specific, well-defined, usually 45 * stateless, testable behaviors. 46 * </li> 47 * <li>It splits responsibility for setting the device time zone with a "time zone detection 48 * service". The time zone detection service is stateful, recording the latest suggestion from 49 * several sources. The {@link NitzStateMachineImpl} actively signals when it has no answer 50 * for the current time zone, allowing the service to arbitrate between the multiple sources 51 * without polling each of them. 52 * </li> 53 * <li>Rate limiting of NITZ signals is performed for time zone as well as time detection.</li> 54 * </ul> 55 */ 56 public final class NitzStateMachineImpl implements NitzStateMachine { 57 58 /** 59 * An interface for predicates applied to incoming NITZ signals to determine whether they must 60 * be processed. See {@link NitzSignalInputFilterPredicateFactory#create(Context, DeviceState)} 61 * for the real implementation. The use of an interface means the behavior can be tested 62 * independently and easily replaced for tests. 63 */ 64 @VisibleForTesting 65 @FunctionalInterface 66 public interface NitzSignalInputFilterPredicate { 67 68 /** 69 * See {@link NitzSignalInputFilterPredicate}. 70 */ mustProcessNitzSignal( @ullable NitzSignal oldSignal, @NonNull NitzSignal newSignal)71 boolean mustProcessNitzSignal( 72 @Nullable NitzSignal oldSignal, 73 @NonNull NitzSignal newSignal); 74 } 75 76 /** 77 * An interface for the stateless component that generates suggestions using country and/or NITZ 78 * information. The use of an interface means the behavior can be tested independently. 79 */ 80 @VisibleForTesting 81 public interface TimeZoneSuggester { 82 83 /** 84 * Generates a {@link TelephonyTimeZoneSuggestion} given the information available. This 85 * method must always return a non-null {@link TelephonyTimeZoneSuggestion} but that object 86 * does not have to contain a time zone if the available information is not sufficient to 87 * determine one. {@link TelephonyTimeZoneSuggestion#getDebugInfo()} provides debugging / 88 * logging information explaining the choice. 89 */ 90 @NonNull getTimeZoneSuggestion( int slotIndex, @Nullable String countryIsoCode, @Nullable NitzSignal nitzSignal)91 TelephonyTimeZoneSuggestion getTimeZoneSuggestion( 92 int slotIndex, @Nullable String countryIsoCode, 93 @Nullable NitzSignal nitzSignal); 94 } 95 96 static final String LOG_TAG = "NitzStateMachineImpl"; 97 static final boolean DBG = true; 98 99 // Miscellaneous dependencies and helpers not related to detection state. 100 private final int mSlotIndex; 101 @NonNull private final DeviceState mDeviceState; 102 /** Applied to NITZ signals during input filtering. */ 103 @NonNull private final NitzSignalInputFilterPredicate mNitzSignalInputFilter; 104 /** 105 * Creates a {@link TelephonyTimeZoneSuggestion} for passing to the time zone detection service. 106 */ 107 @NonNull private final TimeZoneSuggester mTimeZoneSuggester; 108 /** A facade to the time / time zone detection services. */ 109 @NonNull private final TimeServiceHelper mTimeServiceHelper; 110 111 // Shared detection state. 112 113 /** 114 * The latest active NITZ signal <em>processed</em> (i.e. after input filtering). It is used for 115 * input filtering (e.g. rate limiting) and provides the NITZ information when time / time zone 116 * needs to be recalculated when something else has changed. 117 */ 118 @Nullable private NitzSignal mLatestNitzSignal; 119 120 /** 121 * The last NITZ received, which has been cleared from {@link #mLatestNitzSignal} because of a 122 * loss of connectivity. The TimestampedValue reference time is the time according to the 123 * elapsed realtime clock when {@link #mLatestNitzSignal} was cleared. This field is used to 124 * hold the NITZ for later restoration after transient network disconnections. This can be null, 125 * but the NitzSignal referenced by the TimestampedValue will never be. 126 */ 127 @Nullable private TimestampedValue<NitzSignal> mLastNitzSignalCleared; 128 129 // Time Zone detection state. 130 131 /** 132 * Records the country to use for time zone detection. It can be a valid ISO 3166 alpha-2 code 133 * (lower case), empty (test network) or null (no country detected). A country code is required 134 * to determine time zone except when on a test network. 135 */ 136 @Nullable private String mCountryIsoCode; 137 138 /** 139 * Creates an instance for the supplied {@link Phone}. 140 */ createInstance(@onNull Phone phone)141 public static NitzStateMachineImpl createInstance(@NonNull Phone phone) { 142 Objects.requireNonNull(phone); 143 144 int slotIndex = phone.getPhoneId(); 145 DeviceState deviceState = new DeviceStateImpl(phone); 146 TimeZoneLookupHelper timeZoneLookupHelper = new TimeZoneLookupHelper(); 147 TimeZoneSuggester timeZoneSuggester = 148 new TimeZoneSuggesterImpl(deviceState, timeZoneLookupHelper); 149 TimeServiceHelper newTimeServiceHelper = new TimeServiceHelperImpl(phone); 150 NitzSignalInputFilterPredicate nitzSignalFilter = 151 NitzSignalInputFilterPredicateFactory.create(phone.getContext(), deviceState); 152 return new NitzStateMachineImpl( 153 slotIndex, deviceState, nitzSignalFilter, timeZoneSuggester, newTimeServiceHelper); 154 } 155 156 /** 157 * Creates an instance using the supplied components. Used during tests to supply fakes. 158 * See {@link #createInstance(Phone)} 159 */ 160 @VisibleForTesting NitzStateMachineImpl(int slotIndex, @NonNull DeviceState deviceState, @NonNull NitzSignalInputFilterPredicate nitzSignalInputFilter, @NonNull TimeZoneSuggester timeZoneSuggester, @NonNull TimeServiceHelper newTimeServiceHelper)161 public NitzStateMachineImpl(int slotIndex, 162 @NonNull DeviceState deviceState, 163 @NonNull NitzSignalInputFilterPredicate nitzSignalInputFilter, 164 @NonNull TimeZoneSuggester timeZoneSuggester, 165 @NonNull TimeServiceHelper newTimeServiceHelper) { 166 mSlotIndex = slotIndex; 167 mDeviceState = Objects.requireNonNull(deviceState); 168 mTimeZoneSuggester = Objects.requireNonNull(timeZoneSuggester); 169 mTimeServiceHelper = Objects.requireNonNull(newTimeServiceHelper); 170 mNitzSignalInputFilter = Objects.requireNonNull(nitzSignalInputFilter); 171 } 172 173 @Override handleNetworkAvailable()174 public void handleNetworkAvailable() { 175 String reason = "handleNetworkAvailable"; 176 restoreNetworkStateAndRerunDetection(reason); 177 } 178 179 @Override handleNetworkUnavailable()180 public void handleNetworkUnavailable() { 181 boolean networkStateChanged = clearNetworkState(false /* fullyClearNitz */); 182 if (networkStateChanged) { 183 String reason = "handleNetworkUnavailable"; 184 runDetection(reason); 185 } 186 } 187 188 @Override handleCountryDetected(@onNull String countryIsoCode)189 public void handleCountryDetected(@NonNull String countryIsoCode) { 190 if (DBG) { 191 Rlog.d(LOG_TAG, "handleCountryDetected: countryIsoCode=" + countryIsoCode 192 + ", mLatestNitzSignal=" + mLatestNitzSignal); 193 } 194 195 String oldCountryIsoCode = mCountryIsoCode; 196 mCountryIsoCode = Objects.requireNonNull(countryIsoCode); 197 if (!Objects.equals(oldCountryIsoCode, mCountryIsoCode)) { 198 // Generate a new time zone suggestion and update the service as needed. 199 doTimeZoneDetection(countryIsoCode, mLatestNitzSignal, 200 "handleCountryDetected(\"" + countryIsoCode + "\")"); 201 } 202 } 203 204 @Override handleCountryUnavailable()205 public void handleCountryUnavailable() { 206 if (DBG) { 207 Rlog.d(LOG_TAG, "handleCountryUnavailable:" 208 + " mLatestNitzSignal=" + mLatestNitzSignal); 209 } 210 mCountryIsoCode = null; 211 212 // Generate a new time zone suggestion and update the service as needed. 213 doTimeZoneDetection(null /* countryIsoCode */, mLatestNitzSignal, 214 "handleCountryUnavailable"); 215 } 216 217 @Override handleNitzReceived(@onNull NitzSignal nitzSignal)218 public void handleNitzReceived(@NonNull NitzSignal nitzSignal) { 219 Objects.requireNonNull(nitzSignal); 220 221 // Perform input filtering to filter bad data and avoid processing signals too often. 222 NitzSignal previousNitzSignal = mLatestNitzSignal; 223 if (!mNitzSignalInputFilter.mustProcessNitzSignal(previousNitzSignal, nitzSignal)) { 224 if (DBG) { 225 Rlog.d(LOG_TAG, "handleNitzReceived: previousNitzSignal=" + previousNitzSignal 226 + ", nitzSignal=" + nitzSignal + ": NITZ filtered"); 227 } 228 return; 229 } 230 231 // Always store the latest valid NITZ signal to be processed. 232 mLatestNitzSignal = nitzSignal; 233 234 // Clear any retained NITZ signal: The value now in mLatestNitzSignal means it isn't needed. 235 mLastNitzSignalCleared = null; 236 237 String reason = "handleNitzReceived(" + nitzSignal + ")"; 238 runDetection(reason); 239 } 240 241 @Override handleAirplaneModeChanged(boolean on)242 public void handleAirplaneModeChanged(boolean on) { 243 // Treat entry / exit from airplane mode as a strong signal that the user wants to clear 244 // cached state. If the user really is boarding a plane they won't want cached state from 245 // before their flight influencing behavior. 246 // 247 // State is cleared on entry AND exit: on entry because the detection code shouldn't be 248 // opinionated while in airplane mode, and on exit to avoid any unexpected signals received 249 // while in airplane mode from influencing behavior afterwards. 250 // 251 // After clearing detection state, the time zone detection should work out from first 252 // principles what the time zone is. This assumes calls like handleNetworkAvailable() will 253 // be made after airplane mode is re-enabled as the device re-establishes network 254 // connectivity. 255 256 // Clear country detection state. 257 boolean countryStateChanged = mCountryIsoCode != null; 258 mCountryIsoCode = null; 259 260 boolean networkStateChanged = clearNetworkState(true /* fullyClearNitz */); 261 262 if (countryStateChanged || networkStateChanged) { 263 String reason = "handleAirplaneModeChanged(" + on + ")"; 264 runDetection(reason); 265 } 266 } 267 restoreNetworkStateAndRerunDetection(String reason)268 private void restoreNetworkStateAndRerunDetection(String reason) { 269 // Restore the last NITZ signal if the network has been unavailable for only a short period. 270 if (mLastNitzSignalCleared == null) { 271 if (DBG) { 272 Rlog.d(LOG_TAG, reason + ": mLastNitzSignalCleared is null."); 273 } 274 // Nothing has changed. No work to do. 275 return; 276 } 277 278 long timeSinceNitzClearedMillis = mDeviceState.elapsedRealtimeMillis() 279 - mLastNitzSignalCleared.getReferenceTimeMillis(); 280 boolean canRestoreNitz = timeSinceNitzClearedMillis 281 < mDeviceState.getNitzNetworkDisconnectRetentionMillis(); 282 if (canRestoreNitz) { 283 reason = reason + ", mLatestNitzSignal restored from mLastNitzSignalCleared=" 284 + mLastNitzSignalCleared.getValue(); 285 mLatestNitzSignal = mLastNitzSignalCleared.getValue(); 286 287 // NITZ was restored, so we do not need the retained value anymore. 288 mLastNitzSignalCleared = null; 289 290 runDetection(reason); 291 } else { 292 if (DBG) { 293 Rlog.d(LOG_TAG, reason + ": mLastNitzSignalCleared is too old."); 294 } 295 // The retained NITZ is judged too old, so it could be cleared here, but it's kept for 296 // debugging and in case mDeviceState.getNitzNetworkDisconnectRetentionMillis() changes. 297 } 298 } 299 300 private boolean clearNetworkState(boolean fullyClearNitz) { 301 if (fullyClearNitz) { 302 mLastNitzSignalCleared = null; 303 } else { 304 mLastNitzSignalCleared = new TimestampedValue<>( 305 mDeviceState.elapsedRealtimeMillis(), mLatestNitzSignal); 306 } 307 308 boolean networkStateChanged = mLatestNitzSignal != null; 309 mLatestNitzSignal = null; 310 return networkStateChanged; 311 } 312 313 private void runDetection(String reason) { 314 // countryIsoCode can be assigned null here, in which case the doTimeZoneDetection() call 315 // below will do nothing. 316 String countryIsoCode = mCountryIsoCode; 317 318 NitzSignal nitzSignal = mLatestNitzSignal; 319 if (DBG) { 320 Rlog.d(LOG_TAG, "runDetection: reason=" + reason + ", countryIsoCode=" + countryIsoCode 321 + ", nitzSignal=" + nitzSignal); 322 } 323 324 // Generate a new time zone suggestion (which could be an empty suggestion) and update the 325 // service as needed. 326 doTimeZoneDetection(countryIsoCode, nitzSignal, reason); 327 328 // Generate a new time suggestion and update the service as needed. 329 doTimeDetection(nitzSignal, reason); 330 } 331 332 /** 333 * Perform a round of time zone detection and notify the time zone detection service as needed. 334 */ 335 private void doTimeZoneDetection( 336 @Nullable String countryIsoCode, @Nullable NitzSignal nitzSignal, 337 @NonNull String reason) { 338 try { 339 Objects.requireNonNull(reason); 340 341 TelephonyTimeZoneSuggestion suggestion = mTimeZoneSuggester.getTimeZoneSuggestion( 342 mSlotIndex, countryIsoCode, nitzSignal); 343 suggestion.addDebugInfo("Detection reason=" + reason); 344 345 if (DBG) { 346 Rlog.d(LOG_TAG, "doTimeZoneDetection: countryIsoCode=" + countryIsoCode 347 + ", nitzSignal=" + nitzSignal + ", suggestion=" + suggestion 348 + ", reason=" + reason); 349 } 350 mTimeServiceHelper.maybeSuggestDeviceTimeZone(suggestion); 351 } catch (RuntimeException ex) { 352 Rlog.e(LOG_TAG, "doTimeZoneDetection: Exception thrown" 353 + " mSlotIndex=" + mSlotIndex 354 + ", countryIsoCode=" + countryIsoCode 355 + ", nitzSignal=" + nitzSignal 356 + ", reason=" + reason 357 + ", ex=" + ex, ex); 358 } 359 } 360 361 /** 362 * Perform a round of time detection and notify the time detection service as needed. 363 */ 364 private void doTimeDetection(@Nullable NitzSignal nitzSignal, 365 @NonNull String reason) { 366 try { 367 Objects.requireNonNull(reason); 368 369 TelephonyTimeSuggestion.Builder builder = 370 new TelephonyTimeSuggestion.Builder(mSlotIndex); 371 if (nitzSignal == null) { 372 builder.addDebugInfo("Clearing time suggestion" 373 + " reason=" + reason); 374 } else { 375 TimestampedValue<Long> newNitzTime = nitzSignal.createTimeSignal(); 376 builder.setUnixEpochTime(newNitzTime); 377 builder.addDebugInfo("Sending new time suggestion" 378 + " nitzSignal=" + nitzSignal 379 + ", reason=" + reason); 380 } 381 mTimeServiceHelper.suggestDeviceTime(builder.build()); 382 } catch (RuntimeException ex) { 383 Rlog.e(LOG_TAG, "doTimeDetection: Exception thrown" 384 + " mSlotIndex=" + mSlotIndex 385 + ", nitzSignal=" + nitzSignal 386 + ", reason=" + reason 387 + ", ex=" + ex, ex); 388 } 389 } 390 391 @Override 392 public void dumpState(PrintWriter pw) { 393 pw.println(" NitzStateMachineImpl.mLatestNitzSignal=" + mLatestNitzSignal); 394 pw.println(" NitzStateMachineImpl.mCountryIsoCode=" + mCountryIsoCode); 395 mTimeServiceHelper.dumpState(pw); 396 pw.flush(); 397 } 398 399 @Override 400 public void dumpLogs(FileDescriptor fd, IndentingPrintWriter ipw, String[] args) { 401 mTimeServiceHelper.dumpLogs(ipw); 402 } 403 404 @VisibleForTesting 405 @Nullable 406 public NitzData getLatestNitzData() { 407 return mLatestNitzSignal != null ? mLatestNitzSignal.getNitzData() : null; 408 } 409 410 @VisibleForTesting 411 @Nullable 412 public NitzData getLastNitzDataCleared() { 413 return mLastNitzSignalCleared != null 414 ? mLastNitzSignalCleared.getValue().getNitzData() : null; 415 } 416 } 417