Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e5214bd3 authored by Neil Fuller's avatar Neil Fuller
Browse files

A new NitzStateMachine implementation and tests

A new NitzStateMachine implementation and tests. It is intended as a
drop-in replacement for NitzStateMachineImpl and the behavior should be
broadly the same. The implementation is deliberately decomposed to allow
for easier testing.

The new implementation is *not* enabled in this commit.

Bug:140712361
Test: atest com.android.internal.telephony.nitz
Change-Id: I02a96b2c7ea646b548bc874bdadcbffa0caa9ab8
parent a4b5c1f1
Loading
Loading
Loading
Loading
+8 −1
Original line number Diff line number Diff line
@@ -42,6 +42,7 @@ import com.android.internal.telephony.emergency.EmergencyNumberTracker;
import com.android.internal.telephony.imsphone.ImsExternalCallTracker;
import com.android.internal.telephony.imsphone.ImsPhone;
import com.android.internal.telephony.imsphone.ImsPhoneCallTracker;
import com.android.internal.telephony.nitz.NewNitzStateMachineImpl;
import com.android.internal.telephony.uicc.IccCardStatus;
import com.android.internal.telephony.uicc.UiccCard;
import com.android.internal.telephony.uicc.UiccProfile;
@@ -292,12 +293,18 @@ public class TelephonyComponentFactory {
        return new EmergencyNumberTracker(phone, ci);
    }

    private static final boolean USE_NEW_NITZ_STATE_MACHINE = false;

    /**
     * Returns a new {@link NitzStateMachine} instance.
     */
    public NitzStateMachine makeNitzStateMachine(GsmCdmaPhone phone) {
        if (USE_NEW_NITZ_STATE_MACHINE) {
            return NewNitzStateMachineImpl.createInstance(phone);
        } else {
            return new NitzStateMachineImpl(phone);
        }
    }

    public SimActivationTracker makeSimActivationTracker(Phone phone) {
        return new SimActivationTracker(phone);
+357 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+60 −0
Original line number Diff line number Diff line
/*
 * 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.app.timedetector.PhoneTimeSuggestion;
import android.app.timedetector.TimeDetector;

import com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion;
import com.android.internal.util.IndentingPrintWriter;

import java.io.PrintWriter;

/**
 * An interface to various time / time zone detection behaviors that should be centralized into
 * new services.
 */
public interface NewTimeServiceHelper {

    /**
     * Suggests the time to the {@link TimeDetector}.
     *
     * @param suggestion the time
     */
    void suggestDeviceTime(@NonNull PhoneTimeSuggestion suggestion);

    /**
     * Suggests the time zone to the time zone detector.
     *
     * <p>NOTE: The PhoneTimeZoneSuggestion cannot be null. The zoneId it contains can be null to
     * indicate there is no active suggestion; this can be used to clear a previous suggestion.
     *
     * @param suggestion the time zone
     */
    void maybeSuggestDeviceTimeZone(@NonNull PhoneTimeZoneSuggestion suggestion);

    /**
     * Dumps any logs held to the supplied writer.
     */
    void dumpLogs(IndentingPrintWriter ipw);

    /**
     * Dumps internal state such as field values.
     */
    void dumpState(PrintWriter pw);
}
+122 −0
Original line number Diff line number Diff line
/*
 * 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.app.timedetector.TimeDetector;
import android.content.Context;
import android.util.LocalLog;
import android.util.TimestampedValue;

import com.android.internal.telephony.Phone;
import com.android.internal.telephony.metrics.TelephonyMetrics;
import com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion;
import com.android.internal.telephony.nitz.service.TimeZoneDetectionService;
import com.android.internal.util.IndentingPrintWriter;

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

/**
 * The real implementation of {@link NewTimeServiceHelper}.
 */
public final class NewTimeServiceHelperImpl implements NewTimeServiceHelper {

    private final int mPhoneId;
    private final TimeDetector mTimeDetector;
    private final TimeZoneDetectionService mTimeZoneDetector;

    private final LocalLog mTimeZoneLog = new LocalLog(30);
    private final LocalLog mTimeLog = new LocalLog(30);

    /**
     * Records the last time zone suggestion made. Used to avoid sending duplicate suggestions to
     * the time zone service. The value can be {@code null} to indicate no previous suggestion has
     * been made.
     */
    @NonNull
    private PhoneTimeZoneSuggestion mLastSuggestedTimeZone;

    public NewTimeServiceHelperImpl(@NonNull Phone phone) {
        mPhoneId = phone.getPhoneId();
        Context context = Objects.requireNonNull(phone.getContext());
        mTimeDetector = Objects.requireNonNull(context.getSystemService(TimeDetector.class));
        mTimeZoneDetector = Objects.requireNonNull(TimeZoneDetectionService.getInstance(context));
    }

    @Override
    public void suggestDeviceTime(@NonNull PhoneTimeSuggestion phoneTimeSuggestion) {
        mTimeLog.log("Suggesting system clock update: " + phoneTimeSuggestion);

        // 3 nullness assertions in 1 line
        Objects.requireNonNull(phoneTimeSuggestion.getUtcTime().getValue());

        TimestampedValue<Long> utcTime = phoneTimeSuggestion.getUtcTime();
        TelephonyMetrics.getInstance().writeNITZEvent(mPhoneId, utcTime.getValue());
        mTimeDetector.suggestPhoneTime(phoneTimeSuggestion);
    }

    @Override
    public void maybeSuggestDeviceTimeZone(@NonNull PhoneTimeZoneSuggestion newSuggestion) {
        Objects.requireNonNull(newSuggestion);

        PhoneTimeZoneSuggestion oldSuggestion = mLastSuggestedTimeZone;
        if (shouldSendNewTimeZoneSuggestion(oldSuggestion, newSuggestion)) {
            mTimeZoneLog.log("Suggesting time zone update: " + newSuggestion);
            mTimeZoneDetector.suggestPhoneTimeZone(newSuggestion);
            mLastSuggestedTimeZone = newSuggestion;
        }
    }

    private static boolean shouldSendNewTimeZoneSuggestion(
            @Nullable PhoneTimeZoneSuggestion oldSuggestion,
            @NonNull PhoneTimeZoneSuggestion newSuggestion) {
        if (oldSuggestion == null) {
            // No previous suggestion.
            return true;
        }
        // This code relies on PhoneTimeZoneSuggestion.equals() to only check meaningful fields.
        return !Objects.equals(newSuggestion, oldSuggestion);
    }

    @Override
    public void dumpLogs(IndentingPrintWriter ipw) {
        ipw.println("NewTimeServiceHelperImpl:");
        ipw.increaseIndent();
        ipw.println("Time Logs:");
        ipw.increaseIndent();
        mTimeLog.dump(ipw);
        ipw.decreaseIndent();

        ipw.println("Time zone Logs:");
        ipw.increaseIndent();
        mTimeZoneLog.dump(ipw);
        ipw.decreaseIndent();
        ipw.decreaseIndent();

        // TODO Remove this line when the service moves to the system server.
        mTimeZoneDetector.dumpLogs(ipw);
    }

    @Override
    public void dumpState(PrintWriter pw) {
        pw.println(" NewTimeServiceHelperImpl.mLastSuggestedTimeZone=" + mLastSuggestedTimeZone);
        mTimeZoneDetector.dumpState(pw);
    }
}
+273 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading