• 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.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