Added remove subscription support
Bug: 239607619
Test: atest SubscriptionManagerServiceTest
atest SubscriptionDatabaseManagerTest
Merged-In: Ia19ea13132420278b50ddb6c0faba29d9ddacbf2
Change-Id: Ia19ea13132420278b50ddb6c0faba29d9ddacbf2
diff --git a/src/java/com/android/internal/telephony/SubscriptionController.java b/src/java/com/android/internal/telephony/SubscriptionController.java
index c05ab03..25e196c 100644
--- a/src/java/com/android/internal/telephony/SubscriptionController.java
+++ b/src/java/com/android/internal/telephony/SubscriptionController.java
@@ -2712,7 +2712,6 @@
/**
* @return the number of records cleared
*/
- @Override
public int clearSubInfo() {
enforceModifyPhoneState("clearSubInfo");
diff --git a/src/java/com/android/internal/telephony/subscription/SubscriptionDatabaseManager.java b/src/java/com/android/internal/telephony/subscription/SubscriptionDatabaseManager.java
index dd1c083..501d4f0 100644
--- a/src/java/com/android/internal/telephony/subscription/SubscriptionDatabaseManager.java
+++ b/src/java/com/android/internal/telephony/subscription/SubscriptionDatabaseManager.java
@@ -770,6 +770,34 @@
}
/**
+ * Remove a subscription record from the database.
+ *
+ * @param subId The subscription id of the subscription to be deleted.
+ *
+ * @throws IllegalArgumentException If {@code subId} is invalid.
+ */
+ public void removeSubscriptionInfo(int subId) {
+ if (!mAllSubscriptionInfoInternalCache.containsKey(subId)) {
+ throw new IllegalArgumentException("subId " + subId + " is invalid.");
+ }
+
+ mReadWriteLock.writeLock().lock();
+ try {
+ if (mContext.getContentResolver().delete(SimInfo.CONTENT_URI,
+ SimInfo.COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID + "=?",
+ new String[]{Integer.toString(subId)}) > 0) {
+ mAllSubscriptionInfoInternalCache.remove(subId);
+ } else {
+ logel("Failed to remove subscription with subId=" + subId);
+ }
+ } finally {
+ mReadWriteLock.writeLock().unlock();
+ }
+
+ mCallback.invokeFromExecutor(() -> mCallback.onSubscriptionChanged(subId));
+ }
+
+ /**
* Update a subscription in the database (synchronously or asynchronously).
*
* @param subId The subscription id of the subscription to be updated.
diff --git a/src/java/com/android/internal/telephony/subscription/SubscriptionManagerService.java b/src/java/com/android/internal/telephony/subscription/SubscriptionManagerService.java
index be933e6..ea5c020 100644
--- a/src/java/com/android/internal/telephony/subscription/SubscriptionManagerService.java
+++ b/src/java/com/android/internal/telephony/subscription/SubscriptionManagerService.java
@@ -101,6 +101,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
+import java.util.stream.IntStream;
/**
* The subscription manager service is the backend service of {@link SubscriptionManager}.
@@ -830,8 +831,11 @@
public void markSubscriptionsInactive(int simSlotIndex) {
mSubscriptionDatabaseManager.getAllSubscriptions().stream()
.filter(subInfo -> subInfo.getSimSlotIndex() == simSlotIndex)
- .forEach(subInfo -> mSubscriptionDatabaseManager.setSimSlotIndex(
- subInfo.getSubscriptionId(), SubscriptionManager.INVALID_SIM_SLOT_INDEX));
+ .forEach(subInfo -> {
+ mSubscriptionDatabaseManager.setSimSlotIndex(subInfo.getSubscriptionId(),
+ SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+ mSlotIndexToSubId.remove(simSlotIndex);
+ });
}
/**
@@ -1364,8 +1368,9 @@
* @see SubscriptionManager#requestEmbeddedSubscriptionInfoListRefresh
*/
@Override
+ // TODO: Remove this after SubscriptionController is removed.
public void requestEmbeddedSubscriptionInfoListRefresh(int cardId) {
-
+ updateEmbeddedSubscriptions(List.of(cardId), null);
}
/**
@@ -1430,17 +1435,40 @@
}
/**
- * Remove subscription info record for the given device.
+ * Remove subscription info record from the subscription database.
*
* @param uniqueId This is the unique identifier for the subscription within the specific
* subscription type.
- * @param subscriptionType the type of subscription to be removed
+ * @param subscriptionType the type of subscription to be removed.
*
- * @return 0 if success, < 0 on error
+ * // TODO: Remove this terrible return value once SubscriptionController is removed.
+ * @return 0 if success, < 0 on error.
+ *
+ * @throws NullPointerException if {@code uniqueId} is {@code null}.
+ * @throws SecurityException if callers do not hold the required permission.
*/
@Override
+ @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
public int removeSubInfo(@NonNull String uniqueId, int subscriptionType) {
- return 0;
+ enforcePermissions("removeSubInfo", Manifest.permission.MODIFY_PHONE_STATE);
+
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ SubscriptionInfoInternal subInfo = mSubscriptionDatabaseManager
+ .getSubscriptionInfoInternalByIccId(uniqueId);
+ if (subInfo == null) {
+ loge("Cannot find subscription with uniqueId " + uniqueId);
+ return -1;
+ }
+ if (subInfo.getSubscriptionType() != subscriptionType) {
+ loge("The subscription type does not match.");
+ return -1;
+ }
+ mSubscriptionDatabaseManager.removeSubscriptionInfo(subInfo.getSubscriptionId());
+ return 0;
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
}
/**
@@ -1631,6 +1659,10 @@
* @throws SecurityException if callers do not hold the required permission.
*/
@Override
+ @RequiresPermission(anyOf = {
+ Manifest.permission.MODIFY_PHONE_STATE,
+ "carrier privileges",
+ })
public int setOpportunistic(boolean opportunistic, int subId, @NonNull String callingPackage) {
// Verify that the callingPackage belongs to the calling UID
mAppOpsManager.checkPackage(Binder.getCallingUid(), callingPackage);
@@ -1835,11 +1867,76 @@
}
}
+ /**
+ * Remove a list of subscriptions from their subscription group.
+ *
+ * @param subIdList list of subId that need removing from their groups.
+ * @param groupUuid The UUID of the subscription group.
+ * @param callingPackage The package making the call.
+ *
+ * @throws SecurityException if the caller doesn't meet the requirements outlined above.
+ * @throws IllegalArgumentException if the some subscriptions in the list doesn't belong the
+ * specified group.
+ *
+ * @see SubscriptionManager#createSubscriptionGroup(List)
+ */
@Override
- public void removeSubscriptionsFromGroup(int[] subIdList, @NonNull ParcelUuid groupUuid,
- @NonNull String callingPackage) {
+ @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
+ public void removeSubscriptionsFromGroup(@NonNull int[] subIdList,
+ @NonNull ParcelUuid groupUuid, @NonNull String callingPackage) {
// Verify that the callingPackage belongs to the calling UID
mAppOpsManager.checkPackage(Binder.getCallingUid(), callingPackage);
+
+ // If it doesn't have modify phone state permission, or carrier privilege permission,
+ // a SecurityException will be thrown. If it's due to invalid parameter or internal state,
+ // it will return null.
+ if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ != PackageManager.PERMISSION_GRANTED
+ && !(checkCarrierPrivilegeOnSubList(subIdList, callingPackage)
+ && canPackageManageGroup(groupUuid, callingPackage))) {
+ throw new SecurityException("removeSubscriptionsFromGroup needs MODIFY_PHONE_STATE or"
+ + " carrier privilege permission on all specified subscriptions.");
+ }
+
+ Objects.requireNonNull(subIdList);
+ Objects.requireNonNull(groupUuid);
+
+ if (subIdList.length == 0) {
+ throw new IllegalArgumentException("subIdList is empty.");
+ }
+
+ long identity = Binder.clearCallingIdentity();
+
+ try {
+ for (int subId : subIdList) {
+ SubscriptionInfoInternal subInfo = mSubscriptionDatabaseManager
+ .getSubscriptionInfoInternal(subId);
+ if (subInfo == null) {
+ throw new IllegalArgumentException("The provided sub id " + subId
+ + " is not valid.");
+ }
+ if (!groupUuid.toString().equals(subInfo.getGroupUuid())) {
+ throw new IllegalArgumentException("Subscription " + subInfo.getSubscriptionId()
+ + " doesn't belong to group " + groupUuid);
+ }
+ }
+
+ for (SubscriptionInfoInternal subInfo :
+ mSubscriptionDatabaseManager.getAllSubscriptions()) {
+ if (IntStream.of(subIdList).anyMatch(
+ subId -> subId == subInfo.getSubscriptionId())) {
+ mSubscriptionDatabaseManager.setGroupUuid(subInfo.getSubscriptionId(), "");
+ mSubscriptionDatabaseManager.setGroupOwner(subInfo.getSubscriptionId(), "");
+ } else if (subInfo.getGroupUuid().equals(groupUuid.toString())) {
+ // Pre-T behavior. If there are still subscriptions having the same UUID, update
+ // to the new owner.
+ mSubscriptionDatabaseManager.setGroupOwner(
+ subInfo.getSubscriptionId(), callingPackage);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
}
/**
@@ -1850,17 +1947,18 @@
*
* @param subIdList list of subId that need adding into the group
* @param groupUuid the groupUuid the subscriptions are being added to.
+ * @param callingPackage The package making the call.
*
* @throws SecurityException if the caller doesn't meet the requirements outlined above.
* @throws IllegalArgumentException if the some subscriptions in the list doesn't exist.
*
* @see SubscriptionManager#createSubscriptionGroup(List)
*/
+ @Override
@RequiresPermission(anyOf = {
Manifest.permission.MODIFY_PHONE_STATE,
"carrier privileges",
})
- @Override
public void addSubscriptionsIntoGroup(@NonNull int[] subIdList, @NonNull ParcelUuid groupUuid,
@NonNull String callingPackage) {
// Verify that the callingPackage belongs to the calling UID
@@ -1975,7 +2073,7 @@
*
* @param subId The subscription id.
*
- * @return Logical slot indexx (i.e. phone id) as a positive integer or
+ * @return Logical slot index (i.e. phone id) as a positive integer or
* {@link SubscriptionManager#INVALID_SIM_SLOT_INDEX} if the supplied {@code subId} doesn't have
* an associated slot index.
*/
@@ -2058,16 +2156,22 @@
}
}
+ /**
+ * @return The default subscription id.
+ */
@Override
public int getDefaultSubId() {
return mDefaultSubId.get();
}
- @Override
- public int clearSubInfo() {
- return 0;
- }
-
+ /**
+ * Get phone id from the subscription id. In the implementation, the logical SIM slot index
+ * is equivalent to phone id. So this method is same as {@link #getSlotIndex(int)}.
+ *
+ * @param subId The subscription id.
+ *
+ * @return The phone id.
+ */
@Override
public int getPhoneId(int subId) {
// slot index and phone id are equivalent in the current implementation.
@@ -2123,6 +2227,9 @@
}
}
+ /**
+ * @return The default subscription id for voice.
+ */
@Override
public int getDefaultVoiceSubId() {
return mDefaultVoiceSubId.get();
@@ -2173,6 +2280,9 @@
}
}
+ /**
+ * @return The default subscription id for SMS.
+ */
@Override
public int getDefaultSmsSubId() {
return mDefaultSmsSubId.get();
@@ -2351,6 +2461,9 @@
@Override
public boolean setSubscriptionEnabled(boolean enable, int subId) {
+ enforcePermissions("setSubscriptionEnabled", Manifest.permission.MODIFY_PHONE_STATE);
+
+
return true;
}
@@ -2362,7 +2475,7 @@
* @return {@code true} if the subscription is active.
*
* @throws IllegalArgumentException if the provided slot index is invalid.
- * @throws SecurityException if callers do not hold the required permission. *
+ * @throws SecurityException if callers do not hold the required permission.
*/
@Override
@RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
@@ -2621,7 +2734,7 @@
*
* <p>Note the assumption is that one subscription (which usually means one SIM) has
* only one phone number. The multiple sources backup each other so hopefully at least one
- * is availavle. For example, for a carrier that doesn't typically set phone numbers
+ * is available. For example, for a carrier that doesn't typically set phone numbers
* on {@link SubscriptionManager#PHONE_NUMBER_SOURCE_UICC UICC}, the source
* {@link SubscriptionManager#PHONE_NUMBER_SOURCE_IMS IMS} may provide one. Or, a carrier may
* decide to provide the phone number via source
@@ -3010,15 +3123,38 @@
@NonNull String[] args) {
IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " ");
pw.println(SubscriptionManagerService.class.getSimpleName() + ":");
+ pw.println("Logical SIM slot sub id mapping:");
+ pw.increaseIndent();
+ mSlotIndexToSubId.forEach((slotIndex, subId)
+ -> pw.println("Logical slot " + slotIndex + ": subId=" + subId));
+ pw.increaseIndent();
+
+ pw.println("defaultSubId=" + getDefaultSubId());
+ pw.println("defaultVoiceSubId=" + getDefaultVoiceSubId());
+ pw.println("defaultDataSubId=" + getDefaultDataSubId());
+ pw.println("activeDataSubId=" + getActiveDataSubscriptionId());
+ pw.println("defaultSmsSubId=" + getDefaultSmsSubId());
+
+ pw.println("Active subscriptions:");
+ pw.increaseIndent();
+ mSubscriptionDatabaseManager.getAllSubscriptions().stream()
+ .filter(SubscriptionInfoInternal::isActive).forEach(pw::println);
+ pw.decreaseIndent();
+ pw.println("Embedded subscriptions:");
+ pw.increaseIndent();
+ mSubscriptionDatabaseManager.getAllSubscriptions().stream()
+ .filter(SubscriptionInfoInternal::isEmbedded).forEach(pw::println);
+ pw.decreaseIndent();
+ pw.println("Opportunistic subscriptions:");
+ pw.increaseIndent();
+ mSubscriptionDatabaseManager.getAllSubscriptions().stream()
+ .filter(SubscriptionInfoInternal::isOpportunistic).forEach(pw::println);
+ pw.decreaseIndent();
pw.println("All subscriptions:");
pw.increaseIndent();
mSubscriptionDatabaseManager.getAllSubscriptions().forEach(pw::println);
pw.decreaseIndent();
- pw.println("defaultSubId=" + getDefaultSubId());
- pw.println("defaultVoiceSubId=" + getDefaultVoiceSubId());
- pw.println("defaultDataSubId" + getDefaultDataSubId());
- pw.println("activeDataSubId" + getActiveDataSubscriptionId());
- pw.println("defaultSmsSubId" + getDefaultSmsSubId());
+
if (mEuiccManager != null) {
pw.println("Euicc enabled=" + mEuiccManager.isEnabled());
}
diff --git a/tests/telephonytests/src/com/android/internal/telephony/subscription/SubscriptionDatabaseManagerTest.java b/tests/telephonytests/src/com/android/internal/telephony/subscription/SubscriptionDatabaseManagerTest.java
index 2810b12..3f3a2c6 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/subscription/SubscriptionDatabaseManagerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/subscription/SubscriptionDatabaseManagerTest.java
@@ -273,10 +273,17 @@
}
int subId = Integer.parseInt(uri.getLastPathSegment());
- assertThat(mDatabase.size()).isAtLeast(subId);
-
- ContentValues existingValues = mDatabase.get(subId - 1);
logd("update: subId=" + subId + ", contentValues=" + values);
+
+ ContentValues existingValues = mDatabase.stream()
+ .filter(contentValues -> contentValues.get(
+ SimInfo.COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID).equals(subId))
+ .findFirst()
+ .orElse(null);
+ if (existingValues == null) {
+ throw new IllegalArgumentException("Invalid sub id " + subId);
+ }
+
for (Map.Entry<String, Object> entry : values.valueSet()) {
String column = entry.getKey();
Object value = entry.getValue();
@@ -290,7 +297,24 @@
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
- throw new UnsupportedOperationException("delete is not supported uri=" + uri);
+ if (!uri.isPathPrefixMatch(SimInfo.CONTENT_URI)) {
+ throw new UnsupportedOperationException("Unsupported uri=" + uri);
+ }
+
+ logd("delete: uri=" + uri + ", selection=" + selection + ", selectionArgs="
+ + Arrays.toString(selectionArgs));
+ if (!selection.equals(SimInfo.COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID + "=?")) {
+ throw new UnsupportedOperationException("Only support delete by sub id.");
+ }
+
+ int rowsRemoved = 0;
+ for (String selectionArg : selectionArgs) {
+ int subId = Integer.parseInt(selectionArg);
+ // Clear it to null instead of removing it.
+ rowsRemoved += mDatabase.removeIf(contentValues -> contentValues.get(
+ SimInfo.COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID).equals(subId)) ? 1 : 0;
+ }
+ return rowsRemoved;
}
@Override
@@ -305,7 +329,14 @@
throw new IllegalArgumentException("Insert with unknown column " + column);
}
}
- int subId = mDatabase.size() + 1;
+ // The last row's subId + 1
+ int subId;
+ if (mDatabase.isEmpty()) {
+ subId = 1;
+ } else {
+ subId = (int) mDatabase.get(mDatabase.size() - 1)
+ .get(SimInfo.COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID) + 1;
+ }
values.put(SimInfo.COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID, subId);
mDatabase.add(values);
return ContentUris.withAppendedId(SimInfo.CONTENT_URI, subId);
@@ -1566,4 +1597,44 @@
assertThat(mDatabaseManagerUT.getSubscriptionInfoInternal(2)
.getIccId()).isEqualTo(FAKE_ICCID2);
}
+
+ @Test
+ public void testRemoveSubscriptionInfo() throws Exception {
+ insertSubscriptionAndVerify(FAKE_SUBSCRIPTION_INFO1);
+ insertSubscriptionAndVerify(FAKE_SUBSCRIPTION_INFO2);
+ Mockito.clearInvocations(mSubscriptionDatabaseManagerCallback);
+
+ mDatabaseManagerUT.removeSubscriptionInfo(1);
+ assertThat(mDatabaseManagerUT.getSubscriptionInfoInternal(1)).isNull();
+ assertThat(mDatabaseManagerUT.getSubscriptionInfoInternal(2))
+ .isEqualTo(FAKE_SUBSCRIPTION_INFO2);
+ verify(mSubscriptionDatabaseManagerCallback).onSubscriptionChanged(eq(1));
+
+ // Insert a new one. Should become sub 3.
+ Mockito.clearInvocations(mSubscriptionDatabaseManagerCallback);
+ insertSubscriptionAndVerify(FAKE_SUBSCRIPTION_INFO1);
+ assertThat(mDatabaseManagerUT.getSubscriptionInfoInternal(1)).isNull();
+ assertThat(mDatabaseManagerUT.getSubscriptionInfoInternal(2))
+ .isEqualTo(FAKE_SUBSCRIPTION_INFO2);
+ assertThat(mDatabaseManagerUT.getSubscriptionInfoInternal(3))
+ .isEqualTo(new SubscriptionInfoInternal.Builder(FAKE_SUBSCRIPTION_INFO1)
+ .setId(3).build());
+ verify(mSubscriptionDatabaseManagerCallback).onSubscriptionChanged(eq(3));
+
+ Mockito.clearInvocations(mSubscriptionDatabaseManagerCallback);
+ mDatabaseManagerUT.removeSubscriptionInfo(2);
+ assertThat(mDatabaseManagerUT.getSubscriptionInfoInternal(1)).isNull();
+ assertThat(mDatabaseManagerUT.getSubscriptionInfoInternal(2)).isNull();
+ assertThat(mDatabaseManagerUT.getSubscriptionInfoInternal(3))
+ .isEqualTo(new SubscriptionInfoInternal.Builder(FAKE_SUBSCRIPTION_INFO1)
+ .setId(3).build());
+ verify(mSubscriptionDatabaseManagerCallback).onSubscriptionChanged(eq(2));
+
+ Mockito.clearInvocations(mSubscriptionDatabaseManagerCallback);
+ mDatabaseManagerUT.removeSubscriptionInfo(3);
+ assertThat(mDatabaseManagerUT.getSubscriptionInfoInternal(1)).isNull();
+ assertThat(mDatabaseManagerUT.getSubscriptionInfoInternal(2)).isNull();
+ assertThat(mDatabaseManagerUT.getSubscriptionInfoInternal(3)).isNull();
+ verify(mSubscriptionDatabaseManagerCallback).onSubscriptionChanged(eq(3));
+ }
}
diff --git a/tests/telephonytests/src/com/android/internal/telephony/subscription/SubscriptionManagerServiceTest.java b/tests/telephonytests/src/com/android/internal/telephony/subscription/SubscriptionManagerServiceTest.java
index 6448f40..0c85582 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/subscription/SubscriptionManagerServiceTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/subscription/SubscriptionManagerServiceTest.java
@@ -1633,4 +1633,23 @@
.getUserId()).isEqualTo(mSubscriptionManagerServiceUT
.getSubscriptionInfoInternal(1).getUserId());
}
+
+ @Test
+ public void testRemoveSubInfo() {
+ insertSubscription(FAKE_SUBSCRIPTION_INFO1);
+ insertSubscription(FAKE_SUBSCRIPTION_INFO2);
+
+ assertThrows(SecurityException.class, () -> mSubscriptionManagerServiceUT
+ .removeSubInfo(FAKE_ICCID1, SubscriptionManager.SUBSCRIPTION_TYPE_LOCAL_SIM));
+
+ mContextFixture.addCallingOrSelfPermission(Manifest.permission.MODIFY_PHONE_STATE);
+ assertThat(mSubscriptionManagerServiceUT.removeSubInfo(FAKE_ICCID1,
+ SubscriptionManager.SUBSCRIPTION_TYPE_LOCAL_SIM)).isEqualTo(0);
+ assertThat(mSubscriptionManagerServiceUT.removeSubInfo(FAKE_ICCID2,
+ SubscriptionManager.SUBSCRIPTION_TYPE_LOCAL_SIM)).isEqualTo(0);
+
+ mContextFixture.addCallingOrSelfPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
+ assertThat(mSubscriptionManagerServiceUT.getAllSubInfoList(
+ CALLING_PACKAGE, CALLING_FEATURE).isEmpty()).isTrue();
+ }
}