• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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