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