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;