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

Commit 16138732 authored by Sarah Chin's avatar Sarah Chin
Browse files

5G meteredness for telephony framework

Update isNetworkUnmetered to use SubscriptionPlan
Separate subscription override logic and unmetered logic
Add tests for reevaluteUnmeteredConnections

Bug: 139070884
Test: atest FrameworksTelephonyTests:DcTrackerTest
Change-Id: I83104fad4662a847c5f0aa1301676298c18e979b
parent ef4f56d9
Loading
Loading
Loading
Loading
+24 −2
Original line number Diff line number Diff line
@@ -266,6 +266,7 @@ public class DataConnection extends StateMachine {
    private static final String NULL_IP = "0.0.0.0";
    private Object mUserData;
    private int mSubscriptionOverride;
    private boolean mUnmeteredOverride;
    private int mRilRat = ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN;
    private int mDataRegState = Integer.MAX_VALUE;
    private NetworkInfo mNetworkInfo;
@@ -320,9 +321,9 @@ public class DataConnection extends StateMachine {
    static final int EVENT_REEVALUATE_RESTRICTED_STATE = BASE + 25;
    static final int EVENT_REEVALUATE_DATA_CONNECTION_PROPERTIES = BASE + 26;
    static final int EVENT_NR_STATE_CHANGED = BASE + 27;

    static final int EVENT_DATA_CONNECTION_METEREDNESS_CHANGED = BASE + 28;
    private static final int CMD_TO_STRING_COUNT =
            EVENT_NR_STATE_CHANGED - BASE + 1;
            EVENT_DATA_CONNECTION_METEREDNESS_CHANGED - BASE + 1;

    private static String[] sCmdToString = new String[CMD_TO_STRING_COUNT];
    static {
@@ -360,6 +361,8 @@ public class DataConnection extends StateMachine {
                "EVENT_REEVALUATE_DATA_CONNECTION_PROPERTIES";
        sCmdToString[EVENT_NR_STATE_CHANGED - BASE] =
                "EVENT_NR_STATE_CHANGED";
        sCmdToString[EVENT_DATA_CONNECTION_METEREDNESS_CHANGED - BASE] =
                "EVENT_DATA_CONNECTION_METEREDNESS_CHANGED";
    }
    // Convert cmd to string or null if unknown
    static String cmdToString(int cmd) {
@@ -740,6 +743,15 @@ public class DataConnection extends StateMachine {
        sendMessage(obtainMessage(EVENT_DATA_CONNECTION_OVERRIDE_CHANGED));
    }

    /**
     * Update NetworkCapabilities.NET_CAPABILITY_NOT_METERED based on meteredness
     * @param isUnmetered whether this DC should be set to unmetered or not
     */
    public void onMeterednessChanged(boolean isUnmetered) {
        mUnmeteredOverride = isUnmetered;
        sendMessage(obtainMessage(EVENT_DATA_CONNECTION_METEREDNESS_CHANGED));
    }

    /**
     * TearDown the data connection when the deactivation is complete a Message with
     * msg.what == EVENT_DEACTIVATE_DONE
@@ -909,6 +921,8 @@ public class DataConnection extends StateMachine {
        mDcFailCause = DataFailCause.NONE;
        mDisabledApnTypeBitMask = 0;
        mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
        mSubscriptionOverride = 0;
        mUnmeteredOverride = false;
    }

    /**
@@ -1326,6 +1340,11 @@ public class DataConnection extends StateMachine {
            result.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED);
        }

        // Override set by DcTracker
        if (mUnmeteredOverride) {
            result.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
        }

        return result;
    }

@@ -1615,6 +1634,7 @@ public class DataConnection extends StateMachine {
                    break;
                case EVENT_DATA_CONNECTION_ROAM_ON:
                case EVENT_DATA_CONNECTION_ROAM_OFF:
                case EVENT_DATA_CONNECTION_METEREDNESS_CHANGED:
                case EVENT_DATA_CONNECTION_OVERRIDE_CHANGED:
                    updateNetworkInfo();
                    if (mNetworkAgent != null) {
@@ -2210,6 +2230,7 @@ public class DataConnection extends StateMachine {
                }
                case EVENT_DATA_CONNECTION_ROAM_ON:
                case EVENT_DATA_CONNECTION_ROAM_OFF:
                case EVENT_DATA_CONNECTION_METEREDNESS_CHANGED:
                case EVENT_DATA_CONNECTION_OVERRIDE_CHANGED: {
                    updateNetworkInfo();
                    if (mNetworkAgent != null) {
@@ -2960,6 +2981,7 @@ public class DataConnection extends StateMachine {
        pw.println("mSubscriptionOverride=" + Integer.toHexString(mSubscriptionOverride));
        pw.println("mRestrictedNetworkOverride=" + mRestrictedNetworkOverride);
        pw.println("mUnmeteredUseOnly=" + mUnmeteredUseOnly);
        pw.println("mUnmeteredOverride=" + mUnmeteredOverride);
        pw.println("disallowedApnTypes="
                + ApnSetting.getApnTypesStringFromBitmask(getDisallowedApnTypes()));
        pw.println("mInstanceNumber=" + mInstanceNumber);
+69 −38
Original line number Diff line number Diff line
@@ -17,8 +17,6 @@
package com.android.internal.telephony.dataconnection;

import static android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE;
import static android.net.NetworkPolicyManager.OVERRIDE_UNMETERED;
import static android.telephony.NetworkRegistrationInfo.NR_STATE_CONNECTED;
import static android.telephony.TelephonyManager.NETWORK_TYPE_LTE;
import static android.telephony.TelephonyManager.NETWORK_TYPE_NR;

@@ -70,8 +68,8 @@ import android.provider.Settings.SettingNotFoundException;
import android.provider.Telephony;
import android.telephony.AccessNetworkConstants;
import android.telephony.AccessNetworkConstants.TransportType;
import android.telephony.Annotation.DataFailureCause;
import android.telephony.Annotation.ApnType;
import android.telephony.Annotation.DataFailureCause;
import android.telephony.Annotation.NetworkType;
import android.telephony.Annotation.RilRadioTechnology;
import android.telephony.CarrierConfigManager;
@@ -83,6 +81,7 @@ import android.telephony.Rlog;
import android.telephony.ServiceState;
import android.telephony.SubscriptionManager;
import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
import android.telephony.SubscriptionPlan;
import android.telephony.TelephonyManager;
import android.telephony.cdma.CdmaCellLocation;
import android.telephony.data.ApnSetting;
@@ -121,6 +120,7 @@ import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@@ -352,12 +352,12 @@ public class DcTracker extends Handler {
    private boolean mHysteresis = false;
    private boolean mWatchdog = false;

    /* List of SubscriptionPlans, updated on SubscriptionManager.setSubscriptionPlans */
    private List<SubscriptionPlan> mSubscriptionPlans = null;

    /* Used to check whether phone was recently connected to 5G. */
    private boolean m5GWasConnected = false;

    /* Used to keep track of unmetered overrides per network type */
    private long mUnmeteredOverrideBitMask = 0;

    private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver () {
        @Override
        public void onReceive(Context context, Intent intent) {
@@ -456,17 +456,21 @@ public class DcTracker extends Handler {
    private final INetworkPolicyListener mNetworkPolicyListener =
            new NetworkPolicyManager.Listener() {
        @Override
        public void onSubscriptionOverride(int subId, int overrideMask, int overrideValue,
                long networkTypeMask) {
        public void onSubscriptionOverride(int subId, int overrideMask, int overrideValue) {
            if (mPhone == null || mPhone.getSubId() != subId) return;

            if (overrideMask == OVERRIDE_UNMETERED) {
                mUnmeteredOverrideBitMask = overrideValue == 0 ? 0 : networkTypeMask;
                reevaluateUnmeteredConnections();
            } else {
                overrideDataConnections(overrideMask, overrideValue);
            for (DataConnection dataConnection : mDataConnections.values()) {
                dataConnection.onSubscriptionOverride(overrideMask, overrideValue);
            }
        }

        @Override
        public void onSubscriptionPlansChanged(int subId, SubscriptionPlan[] plans) {
            if (mPhone == null || mPhone.getSubId() != subId) return;

            mSubscriptionPlans = plans == null ? null : Arrays.asList(plans);
            reevaluateUnmeteredConnections();
        }
    };

    private final SettingsObserver mSettingsObserver;
@@ -839,7 +843,8 @@ public class DcTracker extends Handler {
                DctConstants.EVENT_PS_RESTRICT_DISABLED, null);
        mPhone.getServiceStateTracker().registerForDataRegStateOrRatChanged(mTransportType, this,
                DctConstants.EVENT_DATA_RAT_CHANGED, null);
        mPhone.registerForServiceStateChanged(this, DctConstants.EVENT_5G_NETWORK_CHANGED, null);
        // listens for PhysicalChannelConfig changes
        mPhone.registerForServiceStateChanged(this, DctConstants.EVENT_SERVICE_STATE_CHANGED, null);
    }

    public void unregisterServiceStateTrackerEvents() {
@@ -950,12 +955,6 @@ public class DcTracker extends Handler {
        }
    }

    private void overrideDataConnections(int overrideMask, int overrideValue) {
        for (DataConnection dataConnection : mDataConnections.values()) {
            dataConnection.onSubscriptionOverride(overrideMask, overrideValue);
        }
    }

    public long getSubId() {
        return mPhone.getSubId();
    }
@@ -3836,7 +3835,7 @@ public class DcTracker extends Handler {
            case DctConstants.EVENT_DATA_ENABLED_OVERRIDE_RULES_CHANGED:
                onDataEnabledOverrideRulesChanged();
                break;
            case DctConstants.EVENT_5G_NETWORK_CHANGED:
            case DctConstants.EVENT_SERVICE_STATE_CHANGED:
                reevaluateUnmeteredConnections();
                break;
            case DctConstants.EVENT_5G_TIMER_HYSTERESIS:
@@ -4007,10 +4006,11 @@ public class DcTracker extends Handler {

    private void reevaluateUnmeteredConnections() {
        if (isNetworkTypeUnmetered(NETWORK_TYPE_NR)) {
            if (mPhone.getServiceState().getNrState() == NR_STATE_CONNECTED) {
            if (mPhone.getServiceState().getNrState()
                    == NetworkRegistrationInfo.NR_STATE_CONNECTED) {
                if (!m5GWasConnected) { // 4G -> 5G
                    stopHysteresisAlarm();
                    overrideDataConnections(OVERRIDE_UNMETERED, OVERRIDE_UNMETERED);
                    setDataConnectionUnmetered(true);
                }
                if (!mWatchdog) {
                    startWatchdogAlarm();
@@ -4021,15 +4021,15 @@ public class DcTracker extends Handler {
                    if (!mHysteresis && !startHysteresisAlarm()) {
                        // hysteresis is not active but carrier does not support hysteresis
                        stopWatchdogAlarm();
                        overrideMeterednessForNetworkType(
                                mTelephonyManager.getNetworkType(mPhone.getSubId()));
                        setDataConnectionUnmetered(isNetworkTypeUnmetered(
                                mTelephonyManager.getNetworkType(mPhone.getSubId())));
                    }
                    m5GWasConnected = false;
                } else { // 4G -> 4G
                    if (!hasMessages(DctConstants.EVENT_5G_TIMER_HYSTERESIS)) {
                        stopWatchdogAlarm();
                        overrideMeterednessForNetworkType(
                                mTelephonyManager.getNetworkType(mPhone.getSubId()));
                        setDataConnectionUnmetered(isNetworkTypeUnmetered(
                                mTelephonyManager.getNetworkType(mPhone.getSubId())));
                    }
                    // do nothing if waiting for hysteresis alarm to go off
                }
@@ -4037,19 +4037,51 @@ public class DcTracker extends Handler {
        } else {
            stopWatchdogAlarm();
            stopHysteresisAlarm();
            overrideMeterednessForNetworkType(mTelephonyManager.getNetworkType(mPhone.getSubId()));
            setDataConnectionUnmetered(isNetworkTypeUnmetered(
                    mTelephonyManager.getNetworkType(mPhone.getSubId())));
            m5GWasConnected = false;
        }
    }

    private void overrideMeterednessForNetworkType(@NetworkType int networkType) {
        int overrideValue = isNetworkTypeUnmetered(networkType) ? OVERRIDE_UNMETERED : 0;
        overrideDataConnections(OVERRIDE_UNMETERED, overrideValue);
    private void setDataConnectionUnmetered(boolean isUnmetered) {
        for (DataConnection dataConnection : mDataConnections.values()) {
            dataConnection.onMeterednessChanged(isUnmetered);
        }
    }

    private boolean isNetworkTypeUnmetered(@NetworkType int networkType) {
        long networkTypeMask = TelephonyManager.getBitMaskForNetworkType(networkType);
        return (mUnmeteredOverrideBitMask & networkTypeMask) == networkTypeMask;
        if (mSubscriptionPlans == null || mSubscriptionPlans.size() == 0) {
            // safe return false if unable to get subscription plans or plans don't exist
            return false;
        }

        long bitmask = TelephonyManager.getBitMaskForNetworkType(networkType);
        boolean isGeneralUnmetered = true;
        for (SubscriptionPlan plan : mSubscriptionPlans) {
            // check plan applies to given network type
            if ((plan.getNetworkTypesBitMask() & bitmask) == bitmask) {
                // check plan is general or specific
                if (plan.getNetworkTypes() == null) {
                    if (!isPlanUnmetered(plan)) {
                        // metered takes precedence over unmetered for safety
                        isGeneralUnmetered = false;
                    }
                } else {
                    // ensure network type unknown returns general value
                    if (networkType != TelephonyManager.NETWORK_TYPE_UNKNOWN) {
                        // there is only 1 specific plan per network type, so return value if found
                        return isPlanUnmetered(plan);
                    }
                }
            }
        }
        return isGeneralUnmetered;
    }

    private boolean isPlanUnmetered(SubscriptionPlan plan) {
        return plan.getDataLimitBytes() == SubscriptionPlan.BYTES_UNLIMITED
                && (plan.getDataLimitBehavior() == SubscriptionPlan.LIMIT_BEHAVIOR_UNKNOWN
                || plan.getDataLimitBehavior() == SubscriptionPlan.LIMIT_BEHAVIOR_THROTTLED);
    }

    private void log(String s) {
@@ -4886,8 +4918,7 @@ public class DcTracker extends Handler {
    }

    private void startWatchdogAlarm() {
        sendMessageDelayed(obtainMessage(DctConstants.EVENT_5G_TIMER_WATCHDOG),
                mWatchdogTimeMs);
        sendMessageDelayed(obtainMessage(DctConstants.EVENT_5G_TIMER_WATCHDOG), mWatchdogTimeMs);
        mWatchdog = true;
    }

+219 −0
Original line number Diff line number Diff line
@@ -69,6 +69,7 @@ import android.telephony.NetworkRegistrationInfo;
import android.telephony.ServiceState;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.SubscriptionPlan;
import android.telephony.TelephonyManager;
import android.telephony.data.ApnSetting;
import android.telephony.data.DataProfile;
@@ -99,9 +100,15 @@ import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.time.Period;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class DcTrackerTest extends TelephonyTest {

@@ -1759,4 +1766,216 @@ public class DcTrackerTest extends TelephonyTest {
        assertEquals(reason, (int) result.second);
        clearInvocations(mHandler);
    }

    private void setUpSubscriptionPlans(boolean is5GUnmetered) throws Exception {
        List<SubscriptionPlan> plans = new ArrayList<>();
        if (is5GUnmetered) {
            plans.add(SubscriptionPlan.Builder
                    .createRecurring(ZonedDateTime.parse("2007-03-14T00:00:00.000Z"),
                            Period.ofMonths(1))
                    .setTitle("Some NR 5G unmetered workaround plan")
                    .setDataLimit(SubscriptionPlan.BYTES_UNLIMITED,
                            SubscriptionPlan.LIMIT_BEHAVIOR_THROTTLED)
                    .setNetworkTypes(new int[] {TelephonyManager.NETWORK_TYPE_NR})
                    .build());
        }
        plans.add(SubscriptionPlan.Builder
                .createRecurring(ZonedDateTime.parse("2007-03-14T00:00:00.000Z"),
                        Period.ofMonths(1))
                .setTitle("Some 5GB Plan")
                .setDataLimit(1_000_000_000, SubscriptionPlan.LIMIT_BEHAVIOR_DISABLED)
                .setDataUsage(500_000_000, System.currentTimeMillis())
                .build());
        replaceInstance(DcTracker.class, "mSubscriptionPlans", mDct, plans);
    }

    private boolean isNetworkTypeUnmetered(int networkType) throws Exception {
        Method method = DcTracker.class.getDeclaredMethod(
                "isNetworkTypeUnmetered", int.class);
        method.setAccessible(true);
        return (boolean) method.invoke(mDct, networkType);
    }

    private void setUpDataConnection() throws Exception {
        Field dc = DcTracker.class.getDeclaredField("mDataConnections");
        dc.setAccessible(true);
        Field uig = DcTracker.class.getDeclaredField("mUniqueIdGenerator");
        uig.setAccessible(true);
        ((HashMap<Integer, DataConnection>) dc.get(mDct)).put(
                ((AtomicInteger) uig.get(mDct)).getAndIncrement(), mDataConnection);
    }

    private void setUpWatchdogTimer() {
        // Watchdog active for 10s
        mBundle.putLong(CarrierConfigManager.KEY_5G_WATCHDOG_TIME_MS_LONG, 10000);
        Intent intent = new Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
        intent.putExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, mPhone.getSubId());
        mContext.sendBroadcast(intent);
        waitForLastHandlerAction(mDcTrackerTestHandler.getThreadHandler());
    }

    private boolean getHysteresisStatus() throws Exception {
        Field field = DcTracker.class.getDeclaredField(("mHysteresis"));
        field.setAccessible(true);
        return (boolean) field.get(mDct);
    }

    private boolean getWatchdogStatus() throws Exception {
        Field field = DcTracker.class.getDeclaredField(("mWatchdog"));
        field.setAccessible(true);
        return (boolean) field.get(mDct);
    }

    @Test
    public void testIsNetworkTypeUnmetered() throws Exception {
        initApns(PhoneConstants.APN_TYPE_DEFAULT, new String[]{PhoneConstants.APN_TYPE_ALL});

        // only 5G unmetered
        setUpSubscriptionPlans(true);

        assertTrue(isNetworkTypeUnmetered(TelephonyManager.NETWORK_TYPE_NR));
        assertFalse(isNetworkTypeUnmetered(TelephonyManager.NETWORK_TYPE_LTE));
        assertFalse(isNetworkTypeUnmetered(TelephonyManager.NETWORK_TYPE_UNKNOWN));

        // all network types metered
        setUpSubscriptionPlans(false);

        assertFalse(isNetworkTypeUnmetered(TelephonyManager.NETWORK_TYPE_NR));
        assertFalse(isNetworkTypeUnmetered(TelephonyManager.NETWORK_TYPE_LTE));
        assertFalse(isNetworkTypeUnmetered(TelephonyManager.NETWORK_TYPE_UNKNOWN));

        // all network types unmetered
        List<SubscriptionPlan> plans = new ArrayList<>();
        plans.add(SubscriptionPlan.Builder
                .createRecurring(ZonedDateTime.parse("2007-03-14T00:00:00.000Z"),
                        Period.ofMonths(1))
                .setTitle("Some 5GB Plan")
                .setDataLimit(SubscriptionPlan.BYTES_UNLIMITED,
                        SubscriptionPlan.LIMIT_BEHAVIOR_THROTTLED)
                .build());
        replaceInstance(DcTracker.class, "mSubscriptionPlans", mDct, plans);

        assertTrue(isNetworkTypeUnmetered(TelephonyManager.NETWORK_TYPE_NR));
        assertTrue(isNetworkTypeUnmetered(TelephonyManager.NETWORK_TYPE_LTE));
        assertTrue(isNetworkTypeUnmetered(TelephonyManager.NETWORK_TYPE_UNKNOWN));
    }

    @Test
    public void testReevaluateUnmeteredConnectionsOnNetworkChange() throws Exception {
        initApns(PhoneConstants.APN_TYPE_DEFAULT, new String[]{PhoneConstants.APN_TYPE_ALL});
        setUpDataConnection();
        setUpSubscriptionPlans(true);
        setUpWatchdogTimer();

        // NetCapability should be unmetered when connected to 5G
        doReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED).when(mServiceState).getNrState();
        mDct.sendMessage(mDct.obtainMessage(DctConstants.EVENT_SERVICE_STATE_CHANGED));
        waitForLastHandlerAction(mDcTrackerTestHandler.getThreadHandler());
        verify(mDataConnection, times(1)).onMeterednessChanged(true);

        // NetCapability should be metered when disconnected from 5G
        doReturn(NetworkRegistrationInfo.NR_STATE_NONE).when(mServiceState).getNrState();
        mDct.sendMessage(mDct.obtainMessage(DctConstants.EVENT_SERVICE_STATE_CHANGED));
        waitForLastHandlerAction(mDcTrackerTestHandler.getThreadHandler());
        verify(mDataConnection, times(1)).onMeterednessChanged(false);
    }

    @Test
    public void testReevaluateUnmeteredConnectionsOnHysteresis() throws Exception {
        initApns(PhoneConstants.APN_TYPE_DEFAULT, new String[]{PhoneConstants.APN_TYPE_ALL});
        setUpDataConnection();
        setUpSubscriptionPlans(true);
        setUpWatchdogTimer();

        // Hysteresis active for 10s
        mBundle.putLong(CarrierConfigManager.KEY_5G_ICON_DISPLAY_GRACE_PERIOD_SEC_INT, 10000);
        Intent intent = new Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
        intent.putExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, mPhone.getSubId());
        mContext.sendBroadcast(intent);
        waitForLastHandlerAction(mDcTrackerTestHandler.getThreadHandler());

        // Hysteresis inactive when unmetered and never connected to 5G
        doReturn(NetworkRegistrationInfo.NR_STATE_NONE).when(mServiceState).getNrState();
        mDct.sendMessage(mDct.obtainMessage(DctConstants.EVENT_5G_TIMER_HYSTERESIS));
        waitForLastHandlerAction(mDcTrackerTestHandler.getThreadHandler());
        assertFalse(getHysteresisStatus());

        // Hysteresis inactive when unmetered and connected to 5G
        doReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED).when(mServiceState).getNrState();
        mDct.sendMessage(mDct.obtainMessage(DctConstants.EVENT_SERVICE_STATE_CHANGED));
        waitForLastHandlerAction(mDcTrackerTestHandler.getThreadHandler());
        assertFalse(getHysteresisStatus());

        // Hysteresis active when unmetered and disconnected after connected to 5G
        doReturn(NetworkRegistrationInfo.NR_STATE_NONE).when(mServiceState).getNrState();
        mDct.sendMessage(mDct.obtainMessage(DctConstants.EVENT_SERVICE_STATE_CHANGED));
        waitForLastHandlerAction(mDcTrackerTestHandler.getThreadHandler());
        assertTrue(getHysteresisStatus());

        // NetCapability metered when hysteresis timer goes off
        mDct.sendMessage(mDct.obtainMessage(DctConstants.EVENT_5G_TIMER_HYSTERESIS));
        waitForLastHandlerAction(mDcTrackerTestHandler.getThreadHandler());
        assertFalse(getHysteresisStatus());
        verify(mDataConnection, times(1)).onMeterednessChanged(true);

        // Hysteresis inactive when reconnected after timer goes off
        doReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED).when(mServiceState).getNrState();
        mDct.sendMessage(mDct.obtainMessage(DctConstants.EVENT_SERVICE_STATE_CHANGED));
        waitForLastHandlerAction(mDcTrackerTestHandler.getThreadHandler());
        assertFalse(getHysteresisStatus());

        // Hysteresis disabled
        mBundle.putLong(CarrierConfigManager.KEY_5G_ICON_DISPLAY_GRACE_PERIOD_SEC_INT, 0);
        intent = new Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
        intent.putExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, mPhone.getSubId());
        mContext.sendBroadcast(intent);
        waitForLastHandlerAction(mDcTrackerTestHandler.getThreadHandler());

        // Hysteresis inactive when CarrierConfig is set to 0
        doReturn(NetworkRegistrationInfo.NR_STATE_NONE).when(mServiceState).getNrState();
        mDct.sendMessage(mDct.obtainMessage(DctConstants.EVENT_SERVICE_STATE_CHANGED));
        waitForLastHandlerAction(mDcTrackerTestHandler.getThreadHandler());
        assertFalse(getHysteresisStatus());
    }

    @Test
    public void testReevaluateUnmeteredConnectionsOnWatchdog() throws Exception {
        initApns(PhoneConstants.APN_TYPE_DEFAULT, new String[]{PhoneConstants.APN_TYPE_ALL});
        setUpDataConnection();
        setUpSubscriptionPlans(true);
        setUpWatchdogTimer();

        // Watchdog inactive when unmetered and never connected to 5G
        doReturn(NetworkRegistrationInfo.NR_STATE_NONE).when(mServiceState).getNrState();
        mDct.sendMessage(mDct.obtainMessage(DctConstants.EVENT_5G_TIMER_WATCHDOG));
        waitForLastHandlerAction(mDcTrackerTestHandler.getThreadHandler());
        assertFalse(getWatchdogStatus());

        // Hysteresis active for 10s
        mBundle.putLong(CarrierConfigManager.KEY_5G_ICON_DISPLAY_GRACE_PERIOD_SEC_INT, 10000);
        Intent intent = new Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
        intent.putExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, mPhone.getSubId());
        mContext.sendBroadcast(intent);
        waitForLastHandlerAction(mDcTrackerTestHandler.getThreadHandler());

        // Watchdog active when unmetered and connected to 5G
        doReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED).when(mServiceState).getNrState();
        mDct.sendMessage(mDct.obtainMessage(DctConstants.EVENT_SERVICE_STATE_CHANGED));
        waitForLastHandlerAction(mDcTrackerTestHandler.getThreadHandler());
        assertTrue(getWatchdogStatus());
        assertFalse(getHysteresisStatus());

        // Watchdog active during hysteresis
        doReturn(NetworkRegistrationInfo.NR_STATE_NONE).when(mServiceState).getNrState();
        mDct.sendMessage(mDct.obtainMessage(DctConstants.EVENT_SERVICE_STATE_CHANGED));
        waitForLastHandlerAction(mDcTrackerTestHandler.getThreadHandler());
        assertTrue(getHysteresisStatus());
        assertTrue(getWatchdogStatus());

        // Watchdog inactive when metered
        setUpSubscriptionPlans(false);
        mDct.sendMessage(mDct.obtainMessage(DctConstants.EVENT_SERVICE_STATE_CHANGED));
        waitForLastHandlerAction(mDcTrackerTestHandler.getThreadHandler());
        assertFalse(getWatchdogStatus());
    }
}