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