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