aboutsummaryrefslogtreecommitdiff
path: root/src/java/com/android/internal/telephony/nitz/NewNitzStateMachineImpl.java
blob: edc3e674965d32aad7f0e368591137e0b5f48434 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.telephony.nitz;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.timedetector.PhoneTimeSuggestion;
import android.content.Context;
import android.telephony.Rlog;
import android.util.TimestampedValue;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.NitzData;
import com.android.internal.telephony.NitzStateMachine;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.TimeZoneLookupHelper;
import com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion;
import com.android.internal.util.IndentingPrintWriter;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Objects;

// TODO Update this comment when NitzStateMachineImpl is deleted - it will no longer be appropriate
// to contrast the behavior of the two implementations.
/**
 * A new and more testable implementation of {@link NitzStateMachine}. It is intended to replace
 * {@link com.android.internal.telephony.NitzStateMachineImpl}.
 *
 * <p>This implementation differs in a number of ways:
 * <ul>
 *     <li>It is decomposed into multiple classes that perform specific, well-defined, usually
 *     stateless, testable behaviors.
 *     </li>
 *     <li>It splits responsibility for setting the device time zone with a "time zone detection
 *     service". The time zone detection service is stateful, recording the latest suggestion from
 *     possibly multiple sources. The {@link NewNitzStateMachineImpl} must now actively signal when
 *     it has no answer for the current time zone, allowing the service to arbitrate between
 *     multiple sources without polling each of them.
 *     </li>
 *     <li>Rate limiting of NITZ signals is performed for time zone as well as time detection.</li>
 * </ul>
 */
public final class NewNitzStateMachineImpl implements NitzStateMachine {

    /**
     * An interface for predicates applied to incoming NITZ signals to determine whether they must
     * be processed. See {@link NitzSignalInputFilterPredicateFactory#create(Context, DeviceState)}
     * for the real implementation. The use of an interface means the behavior can be tested
     * independently and easily replaced for tests.
     */
    @VisibleForTesting
    @FunctionalInterface
    public interface NitzSignalInputFilterPredicate {

        /**
         * See {@link NitzSignalInputFilterPredicate}.
         */
        boolean mustProcessNitzSignal(
                @Nullable TimestampedValue<NitzData> oldSignal,
                @NonNull TimestampedValue<NitzData> newSignal);
    }

    /**
     * An interface for the stateless component that generates suggestions using country and/or NITZ
     * information. The use of an interface means the behavior can be tested independently.
     */
    @VisibleForTesting
    public interface TimeZoneSuggester {

        /**
         * Generates a {@link PhoneTimeZoneSuggestion} given the information available. This method
         * must always return a non-null {@link PhoneTimeZoneSuggestion} but that object does not
         * have to contain a time zone if the available information is not sufficient to determine
         * one. {@link PhoneTimeZoneSuggestion#getDebugInfo()} provides debugging / logging
         * information explaining the choice.
         */
        @NonNull
        PhoneTimeZoneSuggestion getTimeZoneSuggestion(
                int phoneId, @Nullable String countryIsoCode,
                @Nullable TimestampedValue<NitzData> nitzSignal);
    }

    static final String LOG_TAG = "NewNitzStateMachineImpl";
    static final boolean DBG = true;

    // Miscellaneous dependencies and helpers not related to detection state.
    private final int mPhoneId;
    /** Accesses global information about the device. */
    private final DeviceState mDeviceState;
    /** Applied to NITZ signals during input filtering. */
    private final NitzSignalInputFilterPredicate mNitzSignalInputFilter;
    /** Creates {@link PhoneTimeZoneSuggestion} for passing to the time zone detection service. */
    private final TimeZoneSuggester mTimeZoneSuggester;
    /** A facade to the time / time zone detection services. */
    private final NewTimeServiceHelper mNewTimeServiceHelper;

    // Shared detection state.

    /**
     * The last / latest NITZ signal <em>processed</em> (i.e. after input filtering). It is used for
     * input filtering (e.g. rate limiting) and provides the NITZ information when time / time zone
     * needs to be recalculated when something else has changed.
     */
    @Nullable
    private TimestampedValue<NitzData> mLatestNitzSignal;

    // Time Zone detection state.

    /**
     * Records whether the device should have a country code available via
     * {@link DeviceState#getNetworkCountryIsoForPhone()}. Before this an NITZ signal
     * received is (almost always) not enough to determine time zone. On test networks the country
     * code should be available but can still be an empty string but this flag indicates that the
     * information available is unlikely to improve.
     */
    private boolean mGotCountryCode = false;

    /**
     * Creates an instance for the supplied {@link Phone}.
     */
    public static NewNitzStateMachineImpl createInstance(@NonNull Phone phone) {
        Objects.requireNonNull(phone);

        int phoneId = phone.getPhoneId();
        DeviceState deviceState = new DeviceStateImpl(phone);
        TimeZoneLookupHelper timeZoneLookupHelper = new TimeZoneLookupHelper();
        TimeZoneSuggester timeZoneSuggester =
                new TimeZoneSuggesterImpl(deviceState, timeZoneLookupHelper);
        NewTimeServiceHelper newTimeServiceHelper = new NewTimeServiceHelperImpl(phone);
        NitzSignalInputFilterPredicate nitzSignalFilter =
                NitzSignalInputFilterPredicateFactory.create(phone.getContext(), deviceState);
        return new NewNitzStateMachineImpl(
                phoneId, nitzSignalFilter, timeZoneSuggester, newTimeServiceHelper, deviceState);
    }

    /**
     * Creates an instance using the supplied components. Used during tests to supply fakes.
     * See {@link #createInstance(Phone)}
     */
    @VisibleForTesting
    public NewNitzStateMachineImpl(int phoneId,
            @NonNull NitzSignalInputFilterPredicate nitzSignalInputFilter,
            @NonNull TimeZoneSuggester timeZoneSuggester,
            @NonNull NewTimeServiceHelper newTimeServiceHelper, @NonNull DeviceState deviceState) {
        mPhoneId = phoneId;
        mTimeZoneSuggester = Objects.requireNonNull(timeZoneSuggester);
        mNewTimeServiceHelper = Objects.requireNonNull(newTimeServiceHelper);
        mDeviceState = Objects.requireNonNull(deviceState);
        mNitzSignalInputFilter = Objects.requireNonNull(nitzSignalInputFilter);
    }

    @Override
    public void handleNetworkAvailable() {
        // Assume any previous NITZ signals received are now invalid.
        mLatestNitzSignal = null;

        String countryIsoCode =
                mGotCountryCode ? mDeviceState.getNetworkCountryIsoForPhone() : null;

        if (DBG) {
            Rlog.d(LOG_TAG, "handleNetworkAvailable: countryIsoCode=" + countryIsoCode
                    + ", mLatestNitzSignal=" + mLatestNitzSignal);
        }

        String reason = "handleNetworkAvailable()";

        // Generate a new time zone suggestion and update the service as needed.
        doTimeZoneDetection(countryIsoCode, null /* nitzSignal */, reason);

        // Generate a new time suggestion and update the service as needed.
        doTimeDetection(null /* nitzSignal */, reason);
    }

    @Override
    public void handleNetworkCountryCodeSet(boolean countryChanged) {
        if (DBG) {
            Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet: countryChanged=" + countryChanged
                    + ", mLatestNitzSignal=" + mLatestNitzSignal);
        }

        mGotCountryCode = true;

        // Generate a new time zone suggestion and update the service as needed.
        String countryIsoCode = mDeviceState.getNetworkCountryIsoForPhone();
        doTimeZoneDetection(countryIsoCode, mLatestNitzSignal,
                "handleNetworkCountryCodeSet(" + countryChanged + ")");
    }

    @Override
    public void handleNetworkCountryCodeUnavailable() {
        if (DBG) {
            Rlog.d(LOG_TAG, "handleNetworkCountryCodeUnavailable:"
                    + " mLatestNitzSignal=" + mLatestNitzSignal);
        }
        mGotCountryCode = false;

        // Generate a new time zone suggestion and update the service as needed.
        doTimeZoneDetection(null /* countryIsoCode */, mLatestNitzSignal,
                "handleNetworkCountryCodeUnavailable()");
    }

    @Override
    public void handleNitzReceived(@NonNull TimestampedValue<NitzData> nitzSignal) {
        if (DBG) {
            Rlog.d(LOG_TAG, "handleNitzReceived: nitzSignal=" + nitzSignal);
        }
        Objects.requireNonNull(nitzSignal);

        // Perform input filtering to filter bad data and avoid processing signals too often.
        TimestampedValue<NitzData> previousNitzSignal = mLatestNitzSignal;
        if (!mNitzSignalInputFilter.mustProcessNitzSignal(previousNitzSignal, nitzSignal)) {
            return;
        }

        // Always store the latest valid NITZ signal to be processed.
        mLatestNitzSignal = nitzSignal;

        String reason = "handleNitzReceived(" + nitzSignal + ")";

        // Generate a new time zone suggestion and update the service as needed.
        String countryIsoCode =
                mGotCountryCode ? mDeviceState.getNetworkCountryIsoForPhone() : null;
        doTimeZoneDetection(countryIsoCode, nitzSignal, reason);

        // Generate a new time suggestion and update the service as needed.
        doTimeDetection(nitzSignal, reason);
    }

    @Override
    public void handleAirplaneModeChanged(boolean on) {
        if (DBG) {
            Rlog.d(LOG_TAG, "handleAirplaneModeChanged: on=" + on);
        }

        // Treat entry / exit from airplane mode as a strong signal that the user wants to clear
        // cached state. If the user really is boarding a plane they won't want cached state from
        // before their flight influencing behavior.
        //
        // State is cleared on entry AND exit: on entry because the detection code shouldn't be
        // opinionated while in airplane mode, and on exit to avoid any unexpected signals received
        // while in airplane mode from influencing behavior afterwards.
        //
        // After clearing detection state, the time zone detection should work out from first
        // principles what the time / time zone is. This assumes calls like handleNetworkAvailable()
        // will be made after airplane mode is re-enabled as the device re-establishes network
        // connectivity.

        // Clear shared state.
        mLatestNitzSignal = null;

        // Clear time zone detection state.
        mGotCountryCode = false;

        String reason = "handleAirplaneModeChanged(" + on + ")";

        // Generate a new time zone suggestion and update the service as needed.
        doTimeZoneDetection(null /* countryIsoCode */, null /* nitzSignal */,
                reason);

        // Generate a new time suggestion and update the service as needed.
        doTimeDetection(null /* nitzSignal */, reason);
    }

    /**
     * Perform a round of time zone detection and notify the time zone detection service as needed.
     */
    private void doTimeZoneDetection(
            @Nullable String countryIsoCode, @Nullable TimestampedValue<NitzData> nitzSignal,
            @NonNull String reason) {
        try {
            Objects.requireNonNull(reason);

            PhoneTimeZoneSuggestion suggestion =
                    mTimeZoneSuggester.getTimeZoneSuggestion(mPhoneId, countryIsoCode, nitzSignal);
            suggestion.addDebugInfo("Detection reason=" + reason);
            if (DBG) {
                Rlog.d(LOG_TAG, "doTimeZoneDetection: countryIsoCode=" + countryIsoCode
                        + ", nitzSignal=" + nitzSignal + ", suggestion=" + suggestion
                        + ", reason=" + reason);
            }
            mNewTimeServiceHelper.maybeSuggestDeviceTimeZone(suggestion);
        } catch (RuntimeException ex) {
            Rlog.e(LOG_TAG, "doTimeZoneDetection: Exception thrown"
                    + " mPhoneId=" + mPhoneId
                    + ", countryIsoCode=" + countryIsoCode
                    + ", nitzSignal=" + nitzSignal
                    + ", reason=" + reason
                    + ", ex=" + ex, ex);
        }
    }

    /**
     * Perform a round of time detection and notify the time detection service as needed.
     */
    private void doTimeDetection(@Nullable TimestampedValue<NitzData> nitzSignal,
            @NonNull String reason) {
        try {
            Objects.requireNonNull(reason);
            if (nitzSignal == null) {
                // Do nothing to withdraw previous suggestions: the service currently does not
                // support withdrawing suggestions.
                return;
            }

            Objects.requireNonNull(nitzSignal.getValue());

            TimestampedValue<Long> newNitzTime = new TimestampedValue<>(
                    nitzSignal.getReferenceTimeMillis(),
                    nitzSignal.getValue().getCurrentTimeInMillis());
            PhoneTimeSuggestion timeSuggestion = new PhoneTimeSuggestion(mPhoneId, newNitzTime);
            timeSuggestion.addDebugInfo("doTimeDetection: NITZ signal used"
                    + " nitzSignal=" + nitzSignal
                    + ", newNitzTime=" + newNitzTime
                    + ", reason=" + reason);
            mNewTimeServiceHelper.suggestDeviceTime(timeSuggestion);
        } catch (RuntimeException ex) {
            Rlog.e(LOG_TAG, "doTimeDetection: Exception thrown"
                    + " mPhoneId=" + mPhoneId
                    + ", nitzSignal=" + nitzSignal
                    + ", reason=" + reason
                    + ", ex=" + ex, ex);
        }
    }

    @Override
    public void dumpState(PrintWriter pw) {
        pw.println(" NewNitzStateMachineImpl.mLatestNitzSignal=" + mLatestNitzSignal);
        pw.println(" NewNitzStateMachineImpl.mGotCountryCode=" + mGotCountryCode);
        mNewTimeServiceHelper.dumpState(pw);
        pw.flush();
    }

    @Override
    public void dumpLogs(FileDescriptor fd, IndentingPrintWriter ipw, String[] args) {
        mNewTimeServiceHelper.dumpLogs(ipw);
    }

    @Nullable
    public NitzData getCachedNitzData() {
        return mLatestNitzSignal != null ? mLatestNitzSignal.getValue() : null;
    }
}