Move permission checks out of synchronized block
SubscriptionController#getSubscriptionInfoListFromCacheHelper is
invoked by a majority of the AIDL methods to obtain SubscriptionInfo
objects. This commit cuts down on lock contention by performing a
majority of the permission checks before the synchronized block;
with this only a single carrier privilege check may be required for
each subscription.
Fixes: 157642567
Test: atest SubscriptionControllerTest
Test: Manually invoked test app with multiple background threads
invoking SubscriptionManager methods to query for
SubscriptionInfo objects; saw an average 45-50ms improvement
Change-Id: I4d738955125ea4d39252fa582c41139fc5f6f784
diff --git a/src/java/com/android/internal/telephony/SubscriptionController.java b/src/java/com/android/internal/telephony/SubscriptionController.java
index 0ec9c67..3eeb63a 100644
--- a/src/java/com/android/internal/telephony/SubscriptionController.java
+++ b/src/java/com/android/internal/telephony/SubscriptionController.java
@@ -3611,7 +3611,7 @@
callingPackage, callingFeatureId, "getSubscriptionsInGroup")
|| info.canManageSubscription(mContext, callingPackage);
}).map(subscriptionInfo -> conditionallyRemoveIdentifiers(subscriptionInfo,
- callingPackage, callingFeatureId, "getSubscriptionInfoList"))
+ callingPackage, callingFeatureId, "getSubscriptionsInGroup"))
.collect(Collectors.toList());
}
@@ -3897,43 +3897,62 @@
// They are doing similar things except operating on different cache.
private List<SubscriptionInfo> getSubscriptionInfoListFromCacheHelper(
String callingPackage, String callingFeatureId, List<SubscriptionInfo> cacheSubList) {
- boolean canReadAllPhoneState;
+ boolean canReadPhoneState = false;
+ boolean canReadIdentifiers = false;
+ boolean canReadPhoneNumber = false;
try {
- canReadAllPhoneState = TelephonyPermissions.checkReadPhoneState(mContext,
+ canReadPhoneState = TelephonyPermissions.checkReadPhoneState(mContext,
SubscriptionManager.INVALID_SUBSCRIPTION_ID, Binder.getCallingPid(),
Binder.getCallingUid(), callingPackage, callingFeatureId,
"getSubscriptionInfoList");
// If the calling package has the READ_PHONE_STATE permission then check if the caller
- // also has access to subscriber identifiers to ensure that the ICC ID and any other
- // unique identifiers are removed if the caller should not have access.
- if (canReadAllPhoneState) {
- canReadAllPhoneState = hasSubscriberIdentifierAccess(
+ // also has access to subscriber identifiers and the phone number to ensure that the ICC
+ // ID and any other unique identifiers are removed if the caller should not have access.
+ if (canReadPhoneState) {
+ canReadIdentifiers = hasSubscriberIdentifierAccess(
+ SubscriptionManager.INVALID_SUBSCRIPTION_ID, callingPackage,
+ callingFeatureId, "getSubscriptionInfoList");
+ canReadPhoneNumber = hasPhoneNumberAccess(
SubscriptionManager.INVALID_SUBSCRIPTION_ID, callingPackage,
callingFeatureId, "getSubscriptionInfoList");
}
} catch (SecurityException e) {
- canReadAllPhoneState = false;
+ // If a SecurityException is thrown during the READ_PHONE_STATE check then the only way
+ // to access a subscription is to have carrier privileges for its subId; an app with
+ // carrier privileges for a subscription is also granted access to all identifiers so
+ // the identifier and phone number access checks are not required.
}
synchronized (mSubInfoListLock) {
// If the caller can read all phone state, just return the full list.
- if (canReadAllPhoneState) {
+ if (canReadIdentifiers && canReadPhoneNumber) {
return new ArrayList<>(cacheSubList);
}
-
// Filter the list to only include subscriptions which the caller can manage.
- return cacheSubList.stream()
- .filter(subscriptionInfo -> {
- try {
- return TelephonyPermissions.checkCallingOrSelfReadPhoneState(mContext,
- subscriptionInfo.getSubscriptionId(), callingPackage,
- callingFeatureId, "getSubscriptionInfoList");
- } catch (SecurityException e) {
- return false;
- }
- }).map(subscriptionInfo -> conditionallyRemoveIdentifiers(subscriptionInfo,
- callingPackage, callingFeatureId, "getSubscriptionInfoList"))
- .collect(Collectors.toList());
+ List<SubscriptionInfo> subscriptions = new ArrayList<>(cacheSubList.size());
+ for (SubscriptionInfo subscriptionInfo : cacheSubList) {
+ int subId = subscriptionInfo.getSubscriptionId();
+ boolean hasCarrierPrivileges = TelephonyPermissions.checkCarrierPrivilegeForSubId(
+ mContext, subId);
+ // If the caller does not have the READ_PHONE_STATE permission nor carrier
+ // privileges then they cannot access the current subscription.
+ if (!canReadPhoneState && !hasCarrierPrivileges) {
+ continue;
+ }
+ // If the caller has carrier privileges then they are granted access to all
+ // identifiers for their subscription.
+ if (hasCarrierPrivileges) {
+ subscriptions.add(subscriptionInfo);
+ } else {
+ // The caller does not have carrier privileges for this subId, filter the
+ // identifiers in the subscription based on the results of the initial
+ // permission checks.
+ subscriptions.add(
+ conditionallyRemoveIdentifiers(subscriptionInfo, canReadIdentifiers,
+ canReadPhoneNumber));
+ }
+ }
+ return subscriptions;
}
}
@@ -3954,15 +3973,30 @@
callingFeatureId, message);
boolean hasPhoneNumberAccess = hasPhoneNumberAccess(subId, callingPackage, callingFeatureId,
message);
- if (!hasIdentifierAccess || !hasPhoneNumberAccess) {
- result = new SubscriptionInfo(subInfo);
- if (!hasIdentifierAccess) {
- result.clearIccId();
- result.clearCardString();
- }
- if (!hasPhoneNumberAccess) {
- result.clearNumber();
- }
+ return conditionallyRemoveIdentifiers(subInfo, hasIdentifierAccess, hasPhoneNumberAccess);
+ }
+
+ /**
+ * Conditionally removes identifiers from the provided {@code subInfo} based on if the calling
+ * package {@code hasIdentifierAccess} and {@code hasPhoneNumberAccess} and returns the
+ * potentially modified object.
+ *
+ * <p>If the caller specifies the package does not have identifier or phone number access
+ * a clone of the provided SubscriptionInfo is created and modified to avoid altering
+ * SubscriptionInfo objects in a cache.
+ */
+ private SubscriptionInfo conditionallyRemoveIdentifiers(SubscriptionInfo subInfo,
+ boolean hasIdentifierAccess, boolean hasPhoneNumberAccess) {
+ if (hasIdentifierAccess && hasPhoneNumberAccess) {
+ return subInfo;
+ }
+ SubscriptionInfo result = new SubscriptionInfo(subInfo);
+ if (!hasIdentifierAccess) {
+ result.clearIccId();
+ result.clearCardString();
+ }
+ if (!hasPhoneNumberAccess) {
+ result.clearNumber();
}
return result;
}
diff --git a/tests/telephonytests/src/com/android/internal/telephony/SubscriptionControllerTest.java b/tests/telephonytests/src/com/android/internal/telephony/SubscriptionControllerTest.java
index cf65e45..97cd02a 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/SubscriptionControllerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/SubscriptionControllerTest.java
@@ -1272,6 +1272,54 @@
}
@Test
+ public void testGetActiveSubscriptionInfoListWithCarrierPrivilegesOnOneSubId()
+ throws Exception {
+ // If an app does not have the READ_PHONE_STATE permission but has carrier privileges on one
+ // out of multiple sub IDs then the SubscriptionInfo for that subId should be returned with
+ // the ICC ID and phone number.
+ testInsertSim();
+ doReturn(2).when(mTelephonyManager).getPhoneCount();
+ mSubscriptionControllerUT.addSubInfoRecord("test2", 1);
+ int firstSubId = getFirstSubId();
+ int secondSubId = getSubIdAtIndex(1);
+ mSubscriptionControllerUT.setDisplayNumber(DISPLAY_NUMBER, secondSubId);
+ setupIdentifierCarrierPrivilegesTest();
+ mContextFixture.removeCallingOrSelfPermission(Manifest.permission.READ_PHONE_STATE);
+ setCarrierPrivilegesForSubId(false, firstSubId);
+ setCarrierPrivilegesForSubId(true, secondSubId);
+
+ List<SubscriptionInfo> subInfoList =
+ mSubscriptionControllerUT.getActiveSubscriptionInfoList(mCallingPackage,
+ mCallingFeature);
+
+ assertEquals(1, subInfoList.size());
+ SubscriptionInfo subInfo = subInfoList.get(0);
+ assertEquals("test2", subInfo.getIccId());
+ assertEquals(DISPLAY_NUMBER, subInfo.getNumber());
+ }
+
+ @Test
+ public void testGetActiveSubscriptionInfoListWithIdentifierAccessWithoutNumberAccess()
+ throws Exception {
+ // An app with access to device identifiers may not have access to the device phone number
+ // (ie an app that passes the device / profile owner check or an app that has been granted
+ // the device identifiers appop); this test verifies that an app with identifier access
+ // can read the ICC ID but does not receive the phone number.
+ testInsertSim();
+ setupReadPhoneNumbersTest();
+ setIdentifierAccess(true);
+
+ List<SubscriptionInfo> subInfoList =
+ mSubscriptionControllerUT.getActiveSubscriptionInfoList(mCallingPackage,
+ mCallingFeature);
+
+ assertEquals(1, subInfoList.size());
+ SubscriptionInfo subInfo = subInfoList.get(0);
+ assertEquals("test", subInfo.getIccId());
+ assertEquals(UNAVAILABLE_NUMBER, subInfo.getNumber());
+ }
+
+ @Test
public void testGetActiveSubscriptionInfoListWithPrivilegedPermission() throws Exception {
// If the calling package has the READ_PRIVILEGED_PHONE_STATE permission or carrier
// privileges the ICC ID should be available in the SubscriptionInfo objects in the List.
@@ -1401,9 +1449,13 @@
}
private int getFirstSubId() throws Exception {
- int[] subIds = mSubscriptionControllerUT.getActiveSubIdList(/*visibleOnly*/false);
- assertTrue(subIds != null && subIds.length != 0);
- return subIds[0];
+ return getSubIdAtIndex(0);
+ }
+
+ private int getSubIdAtIndex(int index) throws Exception {
+ int[] subIds = mSubscriptionControllerUT.getActiveSubIdList(/*visibileOnly*/false);
+ assertTrue(subIds != null && subIds.length > index);
+ return subIds[index];
}
@Test
diff --git a/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java b/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java
index 7c0bdd9..4952b39 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java
@@ -102,6 +102,7 @@
import com.android.server.pm.permission.PermissionManagerService;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
@@ -833,6 +834,14 @@
mTelephonyManager).getCarrierPrivilegeStatus(anyInt());
}
+ protected void setCarrierPrivilegesForSubId(boolean hasCarrierPrivileges, int subId) {
+ TelephonyManager mockTelephonyManager = Mockito.mock(TelephonyManager.class);
+ doReturn(mockTelephonyManager).when(mTelephonyManager).createForSubscriptionId(subId);
+ doReturn(hasCarrierPrivileges ? TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS
+ : TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS).when(
+ mockTelephonyManager).getCarrierPrivilegeStatus(anyInt());
+ }
+
protected final void waitForHandlerAction(Handler h, long timeoutMillis) {
final CountDownLatch lock = new CountDownLatch(1);
h.post(lock::countDown);