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();
+    }
 }