Telecom plumbing for SIM call manager voice status

When PhoneAccountRegistrar sees a PhoneAccount with
CAPABILITY_VOICE_CALLING_AVAILABLE registered, it will inform telephony
on the corresponding subId(s) for propagation to ServiceState.

Bug: 205737545
Test: com.android.server.telecom.tests.PhoneAccountRegistrarTest
Change-Id: Ia5894e6ac060975b6e1bd9b78e839e05361e3687
Merged-In: Ia5894e6ac060975b6e1bd9b78e839e05361e3687
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index 4e8524e..d90337e 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -454,6 +454,31 @@
     }
 
     /**
+     * Loops through all SIM accounts ({@link #getSimPhoneAccounts}) and returns those with SIM call
+     * manager components specified in carrier config that match {@code simCallManagerHandle}.
+     *
+     * <p>Note that this will return handles even when {@code simCallManagerHandle} has not yet been
+     * registered or was recently unregistered.
+     *
+     * <p>If the given {@code simCallManagerHandle} is not the SIM call manager for any active SIMs,
+     * returns an empty list.
+     */
+    public @NonNull List<PhoneAccountHandle> getSimPhoneAccountsFromSimCallManager(
+            @NonNull PhoneAccountHandle simCallManagerHandle) {
+        List<PhoneAccountHandle> matchingSimHandles = new ArrayList<>();
+        for (PhoneAccountHandle simHandle :
+                getSimPhoneAccounts(simCallManagerHandle.getUserHandle())) {
+            ComponentName simCallManager =
+                    getSystemSimCallManagerComponent(getSubscriptionIdForPhoneAccount(simHandle));
+            if (simCallManager == null) continue;
+            if (simCallManager.equals(simCallManagerHandle.getComponentName())) {
+                matchingSimHandles.add(simHandle);
+            }
+        }
+        return matchingSimHandles;
+    }
+
+    /**
      * Sets a filter for which {@link PhoneAccount}s will be returned from
      * {@link #filterRestrictedPhoneAccounts(List)}. If non-null, only {@link PhoneAccount}s
      * with the package name packageNameFilter will be returned. If null, no filter is set.
@@ -866,6 +891,9 @@
         } else {
             fireAccountChanged(account);
         }
+        // If this is the SIM call manager, tell telephony when the voice ServiceState override
+        // needs to be updated.
+        maybeNotifyTelephonyForVoiceServiceState(account, /* registered= */ true);
     }
 
     public void unregisterPhoneAccount(PhoneAccountHandle accountHandle) {
@@ -875,6 +903,9 @@
                 write();
                 fireAccountsChanged();
                 fireAccountUnRegistered(accountHandle);
+                // If this is the SIM call manager, tell telephony when the voice ServiceState
+                // override needs to be updated.
+                maybeNotifyTelephonyForVoiceServiceState(account, /* registered= */ false);
             }
         }
     }
@@ -1017,6 +1048,72 @@
         }
     }
 
+    private void maybeNotifyTelephonyForVoiceServiceState(
+            @NonNull PhoneAccount account, boolean registered) {
+        // TODO(b/215419665) what about SIM_SUBSCRIPTION accounts? They could theoretically also use
+        // these capabilities, but don't today. If they do start using them, then there will need to
+        // be a kind of "or" logic between SIM_SUBSCRIPTION and CONNECTION_MANAGER accounts to get
+        // the correct value of hasService for a given SIM.
+        boolean hasService = false;
+        List<PhoneAccountHandle> simHandlesToNotify;
+        if (account.hasCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER)) {
+            // When we unregister the SIM call manager account, we always set hasService back to
+            // false since it is no longer providing OTT calling capability once unregistered.
+            if (registered) {
+                // Note: we do *not* early return when the SUPPORTS capability is not present
+                // because it's possible the SIM call manager could remove either capability at
+                // runtime and re-register. However, it is an error to use the AVAILABLE capability
+                // without also setting SUPPORTS.
+                hasService =
+                        account.hasCapabilities(
+                                PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS
+                                        | PhoneAccount.CAPABILITY_VOICE_CALLING_AVAILABLE);
+            }
+            // Notify for all SIMs that named this component as their SIM call manager in carrier
+            // config, since there may be more than one impacted SIM here.
+            simHandlesToNotify = getSimPhoneAccountsFromSimCallManager(account.getAccountHandle());
+        } else if (account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
+            // When new SIMs get registered, we notify them of their current voice status override.
+            // If there is no SIM call manager for this SIM, we treat that as hasService = false and
+            // still notify to ensure consistency.
+            if (!registered) {
+                // We don't do anything when SIMs are unregistered because we won't have an active
+                // subId to map back to phoneId and tell telephony about; that case is handled by
+                // telephony internally.
+                return;
+            }
+            PhoneAccountHandle simCallManagerHandle =
+                    getSimCallManagerFromHandle(
+                            account.getAccountHandle(), account.getAccountHandle().getUserHandle());
+            if (simCallManagerHandle != null) {
+                PhoneAccount simCallManager = getPhoneAccountUnchecked(simCallManagerHandle);
+                hasService =
+                        simCallManager != null
+                                && simCallManager.hasCapabilities(
+                                        PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS
+                                                | PhoneAccount.CAPABILITY_VOICE_CALLING_AVAILABLE);
+            }
+            simHandlesToNotify = Collections.singletonList(account.getAccountHandle());
+        } else {
+            // Not a relevant account - we only care about CONNECTION_MANAGER and SIM_SUBSCRIPTION.
+            return;
+        }
+        if (simHandlesToNotify.isEmpty()) return;
+        Log.i(
+                this,
+                "Notifying telephony of voice service override change for %d SIMs, hasService = %b",
+                simHandlesToNotify.size(),
+                hasService);
+        TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+        for (PhoneAccountHandle simHandle : simHandlesToNotify) {
+            // This may be null if there are no active SIMs but the device is still camped for
+            // emergency calls and registered a SIM_SUBSCRIPTION for that purpose.
+            TelephonyManager simTm = tm.createForPhoneAccountHandle(simHandle);
+            if (simTm == null) continue;
+            simTm.setVoiceServiceStateOverride(hasService);
+        }
+    }
+
     /**
      * Determines if the connection service specified by a {@link PhoneAccountHandle} requires the
      * {@link Manifest.permission#BIND_TELECOM_CONNECTION_SERVICE} permission.
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index c765a6e..2045f13 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -532,6 +532,15 @@
                         if (account.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
                             enforceRegisterMultiUser();
                         }
+                        // These capabilities are for SIM-based accounts only, so only the platform
+                        // and carrier-designated SIM call manager can register accounts with these
+                        // capabilities.
+                        if (account.hasCapabilities(
+                                        PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS)
+                                || account.hasCapabilities(
+                                        PhoneAccount.CAPABILITY_VOICE_CALLING_AVAILABLE)) {
+                            enforceRegisterVoiceCallingIndicationCapabilities(account);
+                        }
                         Bundle extras = account.getExtras();
                         if (extras != null
                                 && extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING)) {
@@ -2343,6 +2352,24 @@
         }
     }
 
+    private void enforceRegisterVoiceCallingIndicationCapabilities(PhoneAccount account) {
+        // Caller must be able to register a SIM PhoneAccount or be the SIM call manager (as named
+        // in carrier config) to declare the two voice indication capabilities.
+        boolean prerequisiteCapabilitiesOk =
+                account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
+                        || account.hasCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER);
+        boolean permissionsOk =
+                isCallerSimCallManagerForAnySim(account.getAccountHandle())
+                        || mContext.checkCallingOrSelfPermission(REGISTER_SIM_SUBSCRIPTION)
+                                == PackageManager.PERMISSION_GRANTED;
+        if (!prerequisiteCapabilitiesOk || !permissionsOk) {
+            throw new SecurityException(
+                    "Only SIM subscriptions and connection managers are allowed to declare "
+                            + "CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS and "
+                            + "CAPABILITY_VOICE_CALLING_AVAILABLE");
+        }
+    }
+
     private void enforceRegisterSkipCallFiltering() {
         if (!isCallerSystemApp()) {
             throw new SecurityException(
@@ -2553,6 +2580,29 @@
         return false;
     }
 
+    /**
+     * Similar to {@link #isCallerSimCallManager}, but works for all SIMs and does not require
+     * {@code accountHandle} to be registered yet.
+     */
+    private boolean isCallerSimCallManagerForAnySim(PhoneAccountHandle accountHandle) {
+        if (isCallerSimCallManager(accountHandle)) {
+            // The caller has already registered a CONNECTION_MANAGER PhoneAccount, so let them pass
+            // (this allows the SIM call manager through in case of SIM switches, where carrier
+            // config may be in a transient state)
+            return true;
+        }
+        // If the caller isn't already registered, then we have to look at the active PSTN
+        // PhoneAccounts and check their carrier configs to see if any point to this one's component
+        final long token = Binder.clearCallingIdentity();
+        try {
+            return !mPhoneAccountRegistrar
+                    .getSimPhoneAccountsFromSimCallManager(accountHandle)
+                    .isEmpty();
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
     private boolean isPrivilegedDialerCalling(String callingPackage) {
         mAppOpsManager.checkPackage(Binder.getCallingUid(), callingPackage);
 
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index 94c4321..47a6177 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -739,6 +739,10 @@
         return mTelephonyManager;
     }
 
+    public CarrierConfigManager getCarrierConfigManager() {
+        return mCarrierConfigManager;
+    }
+
     public NotificationManager getNotificationManager() {
         return mNotificationManager;
     }
diff --git a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
index 6232396..e7cb75d 100644
--- a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
+++ b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
@@ -22,10 +22,16 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageInfo;
@@ -36,6 +42,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Parcel;
+import android.os.PersistableBundle;
 import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -43,6 +50,7 @@
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
+import android.telephony.CarrierConfigManager;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.test.suitebuilder.annotation.MediumTest;
 import android.util.Xml;
@@ -1066,6 +1074,156 @@
         assertEquals(1, deletedAccounts);
     }
 
+    @Test
+    public void testGetSimPhoneAccountsFromSimCallManager() throws Exception {
+        // Register the SIM PhoneAccounts
+        mComponentContextFixture.addConnectionService(
+                makeQuickConnectionServiceComponentName(), Mockito.mock(IConnectionService.class));
+        PhoneAccount sim1Account = makeQuickSimAccount(1);
+        PhoneAccountHandle sim1Handle = sim1Account.getAccountHandle();
+        registerAndEnableAccount(sim1Account);
+        PhoneAccount sim2Account = makeQuickSimAccount(2);
+        PhoneAccountHandle sim2Handle = sim2Account.getAccountHandle();
+        registerAndEnableAccount(sim2Account);
+
+        assertEquals(
+            List.of(sim1Handle, sim2Handle), mRegistrar.getSimPhoneAccountsOfCurrentUser());
+
+        // Set up the SIM call manager app + carrier configs
+        ComponentName simCallManagerComponent =
+                new ComponentName("com.carrier.app", "CarrierConnectionService");
+        PhoneAccountHandle simCallManagerHandle =
+                makeQuickAccountHandle(simCallManagerComponent, "sim-call-manager");
+        setSimCallManagerCarrierConfig(
+                1, new ComponentName("com.other.carrier", "OtherConnectionService"));
+        setSimCallManagerCarrierConfig(2, simCallManagerComponent);
+
+        // Since SIM 1 names another app, so we only get the handle for SIM 2
+        assertEquals(
+                List.of(sim2Handle),
+                mRegistrar.getSimPhoneAccountsFromSimCallManager(simCallManagerHandle));
+        // We do exact component matching, not just package name matching
+        assertEquals(
+                List.of(),
+                mRegistrar.getSimPhoneAccountsFromSimCallManager(
+                        makeQuickAccountHandle(
+                                new ComponentName("com.carrier.app", "SomeOtherUnrelatedService"),
+                                "same-pkg-but-diff-svc")));
+
+        // Results are identical after we register the PhoneAccount
+        mComponentContextFixture.addConnectionService(
+                simCallManagerComponent, Mockito.mock(IConnectionService.class));
+        PhoneAccount simCallManagerAccount =
+                new PhoneAccount.Builder(simCallManagerHandle, "SIM call manager")
+                        .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER)
+                        .build();
+        mRegistrar.registerPhoneAccount(simCallManagerAccount);
+        assertEquals(
+                List.of(sim2Handle),
+                mRegistrar.getSimPhoneAccountsFromSimCallManager(simCallManagerHandle));
+    }
+
+    @Test
+    public void testMaybeNotifyTelephonyForVoiceServiceState() throws Exception {
+        // Register the SIM PhoneAccounts
+        mComponentContextFixture.addConnectionService(
+                makeQuickConnectionServiceComponentName(), Mockito.mock(IConnectionService.class));
+        PhoneAccount sim1Account = makeQuickSimAccount(1);
+        registerAndEnableAccount(sim1Account);
+        PhoneAccount sim2Account = makeQuickSimAccount(2);
+        registerAndEnableAccount(sim2Account);
+        // Telephony is notified by default when new SIM accounts are registered
+        verify(mComponentContextFixture.getTelephonyManager(), times(2))
+                .setVoiceServiceStateOverride(false);
+        clearInvocations(mComponentContextFixture.getTelephonyManager());
+
+        // Set up the SIM call manager app + carrier configs
+        ComponentName simCallManagerComponent =
+                new ComponentName("com.carrier.app", "CarrierConnectionService");
+        PhoneAccountHandle simCallManagerHandle =
+                makeQuickAccountHandle(simCallManagerComponent, "sim-call-manager");
+        mComponentContextFixture.addConnectionService(
+                simCallManagerComponent, Mockito.mock(IConnectionService.class));
+        setSimCallManagerCarrierConfig(1, simCallManagerComponent);
+        setSimCallManagerCarrierConfig(2, simCallManagerComponent);
+
+        // When the SIM call manager is registered without the SUPPORTS capability, telephony is
+        // still notified for consistency (e.g. runtime capability removal + re-registration).
+        PhoneAccount simCallManagerAccount =
+                new PhoneAccount.Builder(simCallManagerHandle, "SIM call manager")
+                        .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER)
+                        .build();
+        mRegistrar.registerPhoneAccount(simCallManagerAccount);
+        verify(mComponentContextFixture.getTelephonyManager(), times(2))
+                .setVoiceServiceStateOverride(false);
+        clearInvocations(mComponentContextFixture.getTelephonyManager());
+
+        // Adding the SUPPORTS capability causes the SIMs to get notified with false again for
+        // consistency purposes
+        simCallManagerAccount =
+                copyPhoneAccountAndAddCapabilities(
+                        simCallManagerAccount,
+                        PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS);
+        mRegistrar.registerPhoneAccount(simCallManagerAccount);
+        verify(mComponentContextFixture.getTelephonyManager(), times(2))
+                .setVoiceServiceStateOverride(false);
+        clearInvocations(mComponentContextFixture.getTelephonyManager());
+
+        // Adding the AVAILABLE capability updates the SIMs again, this time with hasService = true
+        simCallManagerAccount =
+                copyPhoneAccountAndAddCapabilities(
+                        simCallManagerAccount, PhoneAccount.CAPABILITY_VOICE_CALLING_AVAILABLE);
+        mRegistrar.registerPhoneAccount(simCallManagerAccount);
+        verify(mComponentContextFixture.getTelephonyManager(), times(2))
+                .setVoiceServiceStateOverride(true);
+        clearInvocations(mComponentContextFixture.getTelephonyManager());
+
+        // Removing a SIM account does nothing, regardless of SIM call manager capabilities
+        mRegistrar.unregisterPhoneAccount(sim1Account.getAccountHandle());
+        verify(mComponentContextFixture.getTelephonyManager(), never())
+                .setVoiceServiceStateOverride(anyBoolean());
+        clearInvocations(mComponentContextFixture.getTelephonyManager());
+
+        // Adding a SIM account while a SIM call manager with both capabilities is registered causes
+        // a call to telephony with hasService = true
+        mRegistrar.registerPhoneAccount(sim1Account);
+        verify(mComponentContextFixture.getTelephonyManager(), times(1))
+                .setVoiceServiceStateOverride(true);
+        clearInvocations(mComponentContextFixture.getTelephonyManager());
+
+        // Removing the SIM call manager while it has both capabilities causes a call to telephony
+        // with hasService = false
+        mRegistrar.unregisterPhoneAccount(simCallManagerHandle);
+        verify(mComponentContextFixture.getTelephonyManager(), times(2))
+                .setVoiceServiceStateOverride(false);
+        clearInvocations(mComponentContextFixture.getTelephonyManager());
+
+        // Removing the SIM call manager while it has the SUPPORTS capability but not AVAILABLE
+        // still causes a call to telephony with hasService = false for consistency
+        simCallManagerAccount =
+                copyPhoneAccountAndRemoveCapabilities(
+                        simCallManagerAccount, PhoneAccount.CAPABILITY_VOICE_CALLING_AVAILABLE);
+        mRegistrar.registerPhoneAccount(simCallManagerAccount);
+        clearInvocations(mComponentContextFixture.getTelephonyManager()); // from re-registration
+        mRegistrar.unregisterPhoneAccount(simCallManagerHandle);
+        verify(mComponentContextFixture.getTelephonyManager(), times(2))
+                .setVoiceServiceStateOverride(false);
+        clearInvocations(mComponentContextFixture.getTelephonyManager());
+
+        // Finally, removing the SIM call manager while it has neither capability still causes a
+        // call to telephony with hasService = false for consistency
+        simCallManagerAccount =
+                copyPhoneAccountAndRemoveCapabilities(
+                        simCallManagerAccount,
+                        PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS);
+        mRegistrar.registerPhoneAccount(simCallManagerAccount);
+        clearInvocations(mComponentContextFixture.getTelephonyManager()); // from re-registration
+        mRegistrar.unregisterPhoneAccount(simCallManagerHandle);
+        verify(mComponentContextFixture.getTelephonyManager(), times(2))
+                .setVoiceServiceStateOverride(false);
+        clearInvocations(mComponentContextFixture.getTelephonyManager());
+    }
+
     private static ComponentName makeQuickConnectionServiceComponentName() {
         return new ComponentName(
                 "com.android.server.telecom.tests",
@@ -1086,6 +1244,23 @@
                 "label" + idx);
     }
 
+    private static PhoneAccount copyPhoneAccountAndOverrideCapabilities(
+            PhoneAccount base, int newCapabilities) {
+        return base.toBuilder().setCapabilities(newCapabilities).build();
+    }
+
+    private static PhoneAccount copyPhoneAccountAndAddCapabilities(
+            PhoneAccount base, int capabilitiesToAdd) {
+        return copyPhoneAccountAndOverrideCapabilities(
+                base, base.getCapabilities() | capabilitiesToAdd);
+    }
+
+    private static PhoneAccount copyPhoneAccountAndRemoveCapabilities(
+            PhoneAccount base, int capabilitiesToRemove) {
+        return copyPhoneAccountAndOverrideCapabilities(
+                base, base.getCapabilities() & ~capabilitiesToRemove);
+    }
+
     private PhoneAccount makeQuickAccount(String id, int idx) {
         return makeQuickAccountBuilder(id, idx)
                 .setAddress(Uri.parse("http://foo.com/" + idx))
@@ -1098,6 +1273,44 @@
                 .build();
     }
 
+    /**
+     * Similar to {@link #makeQuickAccount}, but also hooks up {@code TelephonyManager} so that it
+     * returns {@code simId} as the account's subscriptionId.
+     */
+    private PhoneAccount makeQuickSimAccount(int simId) {
+        PhoneAccount simAccount =
+                makeQuickAccountBuilder("sim" + simId, simId)
+                        .setCapabilities(
+                                PhoneAccount.CAPABILITY_CALL_PROVIDER
+                                        | PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
+                        .setIsEnabled(true)
+                        .build();
+        when(mComponentContextFixture
+                        .getTelephonyManager()
+                        .getSubscriptionId(simAccount.getAccountHandle()))
+                .thenReturn(simId);
+        // mComponentContextFixture already sets up the createForSubscriptionId self-reference
+        when(mComponentContextFixture
+                        .getTelephonyManager()
+                        .createForPhoneAccountHandle(simAccount.getAccountHandle()))
+                .thenReturn(mComponentContextFixture.getTelephonyManager());
+        return simAccount;
+    }
+
+    /**
+     * Hooks up carrier config to point to {@code simCallManagerComponent} for the given {@code
+     * subscriptionId}.
+     */
+    private void setSimCallManagerCarrierConfig(
+            int subscriptionId, @Nullable ComponentName simCallManagerComponent) {
+        PersistableBundle config = new PersistableBundle();
+        config.putString(
+                CarrierConfigManager.KEY_DEFAULT_SIM_CALL_MANAGER_STRING,
+                simCallManagerComponent != null ? simCallManagerComponent.flattenToString() : null);
+        when(mComponentContextFixture.getCarrierConfigManager().getConfigForSubId(subscriptionId))
+                .thenReturn(config);
+    }
+
     private static void roundTripPhoneAccount(PhoneAccount original) throws Exception {
         PhoneAccount copy = null;