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

Commit b3b55e96 authored by Jesse Melhuish's avatar Jesse Melhuish
Browse files

CarrierService: Disable marching dots on carrier app crash/lost

If the carrier app enables marching dots via notifyCarrierNetworkChange
and then crashes, the marching dots may unintentionally be permanently
enabled. These changes enable automatically disabling the marching dots
when:

1. The carrier app disconnects (ServiceConnection.onServiceDisconnected)
2. The carrier app binding dies (ServiceConnection.onBindingDied)

Of note, this does not handle the case where the SIM card is disabled or
removed due to a lack of subscriptionId in that scenario.

Bug: 333571417
Test: atest CtsTelephonyTestCases
Flag: com.android.internal.telephony.flags.disable_carrier_network_change_on_carrier_app_lost
Change-Id: Ibdebe5f6bead458a53e3f8224c05160f41861909
parent fa270d23
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -30,3 +30,14 @@ flag {
    description: "Enable temporary failures in CarrierMessagingService"
    bug:"326610112"
}

# OWNER=melhuishj TARGET=25Q3
flag {
    name: "disable_carrier_network_change_on_carrier_app_lost"
    namespace: "telephony"
    description: "Set carrier network change status to false whenever the carrier app is lost"
    bug:"333571417"
    metadata {
      purpose: PURPOSE_BUGFIX
    }
}
+29 −1
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ import android.os.UserHandle;
import android.service.carrier.CarrierService;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.telephony.TelephonyRegistryManager;
import android.text.TextUtils;
import android.util.LocalLog;
import android.util.Log;
@@ -335,7 +336,7 @@ public class CarrierServiceBindHelper {
            bindCount++;
            lastBindStartMillis = System.currentTimeMillis();

            connection = new CarrierServiceConnection();
            connection = new CarrierServiceConnection(getPhoneId());

            String error;
            try {
@@ -431,6 +432,11 @@ public class CarrierServiceBindHelper {

    private class CarrierServiceConnection implements ServiceConnection {
        private boolean connected;
        private final int mPhoneId;

        CarrierServiceConnection(int phoneId) {
            mPhoneId = phoneId;
        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
@@ -438,16 +444,38 @@ public class CarrierServiceBindHelper {
            connected = true;
        }

        private void maybeDisableCarrierNetworkChangeNotification() {
            int subscriptionId = SubscriptionManager.getSubscriptionId(mPhoneId);
            // TODO(b/117525047): switch to phoneId-based solution when available in
            // TelephonyRegistryManager to address SIM remove/disable case.
            if (subscriptionId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
                logdWithLocalLog(
                        "No valid subscription found when trying to disable carrierNetworkChange"
                                + " for phoneId: "
                                + mPhoneId);
                return;
            }
            TelephonyRegistryManager telephonyRegistryManager =
                    mContext.getSystemService(TelephonyRegistryManager.class);
            telephonyRegistryManager.notifyCarrierNetworkChange(subscriptionId, false);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            logdWithLocalLog("Disconnected from carrier app: " + name.flattenToString());
            connected = false;
            if (Flags.disableCarrierNetworkChangeOnCarrierAppLost()) {
                maybeDisableCarrierNetworkChangeNotification();
            }
        }

        @Override
        public void onBindingDied(ComponentName name) {
            logdWithLocalLog("Binding from carrier app died: " + name.flattenToString());
            connected = false;
            if (Flags.disableCarrierNetworkChangeOnCarrierAppLost()) {
                maybeDisableCarrierNetworkChangeNotification();
            }
        }

        @Override
+137 −11
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.internal.telephony;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
@@ -25,18 +26,30 @@ import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.Bundle;
import android.os.Message;
import android.service.carrier.CarrierService;
import android.service.carrier.ICarrierService;
import android.telephony.TelephonyManager.CarrierPrivilegesCallback;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;

import androidx.test.filters.SmallTest;

import com.android.internal.telephony.flags.Flags;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;

@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@@ -101,6 +114,16 @@ public class CarrierServiceBindHelperTest extends TelephonyTest {
                        new Integer(0)));
    }

    // Verify a CarrierPrivilegesCallback is registered and return the callback object. May return
    // null if no callback is captured.
    private CarrierPrivilegesCallback expectRegisterCarrierPrivilegesCallback(int phoneId) {
        ArgumentCaptor<CarrierPrivilegesCallback> callbackCaptor =
                ArgumentCaptor.forClass(CarrierPrivilegesCallback.class);
        verify(mTelephonyManager)
                .registerCarrierPrivilegesCallback(eq(phoneId), any(), callbackCaptor.capture());
        return callbackCaptor.getAllValues().get(0);
    }

    @Test
    public void testCarrierPrivilegesCallbackRegistration() {
        // Device starts with DSDS mode
@@ -110,18 +133,11 @@ public class CarrierServiceBindHelperTest extends TelephonyTest {

        // Verify that CarrierPrivilegesCallbacks are registered on both phones.
        // Capture the callbacks for further verification
        ArgumentCaptor<CarrierPrivilegesCallback> phone0CallbackCaptor = ArgumentCaptor.forClass(
                CarrierPrivilegesCallback.class);
        verify(mTelephonyManager).registerCarrierPrivilegesCallback(eq(PHONE_ID_0), any(),
                phone0CallbackCaptor.capture());
        CarrierPrivilegesCallback phone0Callback = phone0CallbackCaptor.getAllValues().get(0);
        CarrierPrivilegesCallback phone0Callback =
                expectRegisterCarrierPrivilegesCallback(PHONE_ID_0);
        assertNotNull(phone0Callback);

        ArgumentCaptor<CarrierPrivilegesCallback> phone1CallbackCaptor = ArgumentCaptor.forClass(
                CarrierPrivilegesCallback.class);
        verify(mTelephonyManager).registerCarrierPrivilegesCallback(eq(PHONE_ID_1), any(),
                phone1CallbackCaptor.capture());
        CarrierPrivilegesCallback phone1Callback = phone1CallbackCaptor.getAllValues().get(0);
        CarrierPrivilegesCallback phone1Callback =
                expectRegisterCarrierPrivilegesCallback(PHONE_ID_1);
        assertNotNull(phone1Callback);

        // Switch back to single SIM.
@@ -133,5 +149,115 @@ public class CarrierServiceBindHelperTest extends TelephonyTest {
        verify(mTelephonyManager).unregisterCarrierPrivilegesCallback(eq(phone1Callback));
        verify(mTelephonyManager, never()).unregisterCarrierPrivilegesCallback(eq(phone0Callback));
    }

    @Test
    public void testCarrierAppConnectionLost_resetsCarrierNetworkChange() {
        if (!Flags.disableCarrierNetworkChangeOnCarrierAppLost()) {
            return;
        }
        // Static test data
        String carrierServicePackageName = "android.test.package.carrier";
        ComponentName carrierServiceComponentName =
                new ComponentName("android.test.package", "carrier");
        ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
                ArgumentCaptor.forClass(ServiceConnection.class);
        ResolveInfo resolveInfo = new ResolveInfo();
        ServiceInfo serviceInfo = new ServiceInfo();
        serviceInfo.packageName = carrierServicePackageName;
        serviceInfo.name = "carrier";
        serviceInfo.metaData = new Bundle();
        serviceInfo.metaData.putBoolean("android.service.carrier.LONG_LIVED_BINDING", true);
        resolveInfo.serviceInfo = serviceInfo;

        // Set up expectations for construction/initialization.
        doReturn(carrierServicePackageName)
                .when(mTelephonyManager)
                .getCarrierServicePackageNameForLogicalSlot(PHONE_ID_0);
        doReturn(1).when(mTelephonyManager).getActiveModemCount();
        doReturn(resolveInfo)
                .when(mPackageManager)
                .resolveService(any(), eq(PackageManager.GET_META_DATA));
        ICarrierService carrierServiceInterface = Mockito.mock(ICarrierService.class);
        mContextFixture.addService(
                CarrierService.CARRIER_SERVICE_INTERFACE,
                carrierServiceComponentName,
                carrierServicePackageName,
                carrierServiceInterface,
                serviceInfo);

        mCarrierServiceBindHelper = new CarrierServiceBindHelper(mContext);
        processAllMessages();

        CarrierPrivilegesCallback phoneCallback =
                expectRegisterCarrierPrivilegesCallback(PHONE_ID_0);
        assertNotNull(phoneCallback);
        phoneCallback.onCarrierServiceChanged(null, 0);
        processAllMessages();

        // Grab the ServiceConnection for CarrierService
        verify(mContext)
                .bindService(any(Intent.class), anyInt(), any(), serviceConnectionCaptor.capture());
        ServiceConnection serviceConnection = serviceConnectionCaptor.getAllValues().get(0);
        assertNotNull(serviceConnection);

        // Test CarrierService disconnection
        serviceConnection.onServiceDisconnected(carrierServiceComponentName);
        verify(mTelephonyRegistryManager).notifyCarrierNetworkChange(PHONE_ID_0, false);
    }

    @Test
    public void testCarrierAppBindingLost_resetsCarrierNetworkChange() {
        if (!Flags.disableCarrierNetworkChangeOnCarrierAppLost()) {
            return;
        }
        // Static test data
        String carrierServicePackageName = "android.test.package.carrier";
        ComponentName carrierServiceComponentName =
                new ComponentName("android.test.package", "carrier");
        ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
                ArgumentCaptor.forClass(ServiceConnection.class);
        ResolveInfo resolveInfo = new ResolveInfo();
        ServiceInfo serviceInfo = new ServiceInfo();
        serviceInfo.packageName = carrierServicePackageName;
        serviceInfo.name = "carrier";
        serviceInfo.metaData = new Bundle();
        serviceInfo.metaData.putBoolean("android.service.carrier.LONG_LIVED_BINDING", true);
        resolveInfo.serviceInfo = serviceInfo;

        // Set up expectations for construction/initialization.
        doReturn(carrierServicePackageName)
                .when(mTelephonyManager)
                .getCarrierServicePackageNameForLogicalSlot(PHONE_ID_0);
        doReturn(1).when(mTelephonyManager).getActiveModemCount();
        doReturn(resolveInfo)
                .when(mPackageManager)
                .resolveService(any(), eq(PackageManager.GET_META_DATA));
        ICarrierService carrierServiceInterface = Mockito.mock(ICarrierService.class);
        mContextFixture.addService(
                CarrierService.CARRIER_SERVICE_INTERFACE,
                carrierServiceComponentName,
                carrierServicePackageName,
                carrierServiceInterface,
                serviceInfo);

        mCarrierServiceBindHelper = new CarrierServiceBindHelper(mContext);
        processAllMessages();

        CarrierPrivilegesCallback phoneCallback =
                expectRegisterCarrierPrivilegesCallback(PHONE_ID_0);
        assertNotNull(phoneCallback);
        phoneCallback.onCarrierServiceChanged(null, 0);
        processAllMessages();

        // Grab the ServiceConnection for CarrierService
        verify(mContext)
                .bindService(any(Intent.class), anyInt(), any(), serviceConnectionCaptor.capture());
        ServiceConnection serviceConnection = serviceConnectionCaptor.getAllValues().get(0);
        assertNotNull(serviceConnection);

        // Test CarrierService disconnection
        serviceConnection.onBindingDied(carrierServiceComponentName);
        verify(mTelephonyRegistryManager).notifyCarrierNetworkChange(PHONE_ID_0, false);
    }
    // TODO (b/232461097): Add UT cases to cover more scenarios (user unlock, SIM state change...)
}
+20 −3
Original line number Diff line number Diff line
@@ -105,6 +105,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;

/**
 * Controls a test {@link Context} as would be provided by the Android framework to an
@@ -226,20 +227,36 @@ public class ContextFixture implements TestFixture<Context> {
            if (mServiceByServiceConnection.containsKey(connection)) {
                throw new RuntimeException("ServiceConnection already bound: " + connection);
            }
            IInterface service = mServiceByComponentName.get(serviceIntent.getComponent());
            IInterface service = null;
            if (serviceIntent.getComponent() != null) {
                service = mServiceByComponentName.get(serviceIntent.getComponent());
            }
            if (service == null) {
                service = mServiceByPackageName.get(serviceIntent.getPackage());
            }
            if (service == null) {
                throw new RuntimeException(
                        String.format("ServiceConnection not found for component: %s, package: %s",
                        String.format(
                                "ServiceConnection not found for component: %s, package: %s",
                                serviceIntent.getComponent(), serviceIntent.getPackage()));
            }
            mServiceByServiceConnection.put(connection, service);
            connection.onServiceConnected(serviceIntent.getComponent(), service.asBinder());
            ComponentName componentName = null;
            if (mComponentNameByService.containsKey(service)) {
                componentName = mComponentNameByService.get(service);
            } else {
                componentName = serviceIntent.getComponent();
            }
            connection.onServiceConnected(componentName, service.asBinder());
            return true;
        }

        @Override
        public boolean bindService(
                Intent serviceIntent, int flags, Executor executor, ServiceConnection connection) {
            return bindService(serviceIntent, connection, flags);
        }

        @Override
        public boolean bindServiceAsUser(
                Intent serviceIntent,