Add flag to wipe eUICC data

Add a new flag in the DevicePolicyManager so that we can Use
EuiccManager#eraseSubscriptions(PendingIntent) to erase all the carrier data
from eUICC chip if the user choose to "ERASE" from the Android device manager.

Bug: 37277944
Test: E2E
Change-Id: Ia78090a00d956c645725be4fd591e02ded8ec467
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 9eacd21..ea0829f 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -55,7 +55,6 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.ContactsContract.Directory;
-import android.provider.Settings;
 import android.security.Credentials;
 import android.service.restrictions.RestrictionsReceiver;
 import android.telephony.TelephonyManager;
@@ -3134,6 +3133,14 @@
     public static final int WIPE_RESET_PROTECTION_DATA = 0x0002;
 
     /**
+     * Flag for {@link #wipeData(int)}: also erase the device's eUICC data.
+     *
+     * TODO(b/35851809): make this public.
+     * @hide
+     */
+    public static final int WIPE_EUICC = 0x0004;
+
+    /**
      * Ask that all user data be wiped. If called as a secondary user, the user will be removed and
      * other users will remain unaffected. Calling from the primary user will cause the device to
      * reboot, erasing all device data - including all the secondary users and their data - while
diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java
index db9f28b..6f458e0 100644
--- a/core/java/android/os/RecoverySystem.java
+++ b/core/java/android/os/RecoverySystem.java
@@ -22,20 +22,26 @@
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
+import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.os.UserManager;
+import android.provider.Settings;
+import android.telephony.euicc.EuiccManager;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.Display;
 import android.view.WindowManager;
 
+import com.android.internal.logging.MetricsLogger;
+
 import libcore.io.Streams;
 
-import java.io.ByteArrayInputStream;
 import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -46,22 +52,19 @@
 import java.io.RandomAccessFile;
 import java.security.GeneralSecurityException;
 import java.security.PublicKey;
-import java.security.Signature;
 import java.security.SignatureException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Enumeration;
 import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
 import java.util.Locale;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 import java.util.zip.ZipInputStream;
 
-import com.android.internal.logging.MetricsLogger;
-
 import sun.security.pkcs.PKCS7;
 import sun.security.pkcs.SignerInfo;
 
@@ -84,11 +87,19 @@
     /** Send progress to listeners no more often than this (in ms). */
     private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500;
 
+    private static final long DEFAULT_EUICC_WIPING_TIMEOUT_MILLIS = 30000L; // 30 s
+
+    private static final long MIN_EUICC_WIPING_TIMEOUT_MILLIS = 5000L; // 5 s
+
+    private static final long MAX_EUICC_WIPING_TIMEOUT_MILLIS = 60000L; // 60 s
+
     /** Used to communicate with recovery.  See bootable/recovery/recovery.cpp. */
     private static final File RECOVERY_DIR = new File("/cache/recovery");
     private static final File LOG_FILE = new File(RECOVERY_DIR, "log");
     private static final File LAST_INSTALL_FILE = new File(RECOVERY_DIR, "last_install");
     private static final String LAST_PREFIX = "last_";
+    private static final String ACTION_WIPE_EUICC_DATA =
+            "com.android.internal.action.WIPE_EUICC_DATA";
 
     /**
      * The recovery image uses this file to identify the location (i.e. blocks)
@@ -673,18 +684,26 @@
      */
     public static void rebootWipeUserData(Context context) throws IOException {
         rebootWipeUserData(context, false /* shutdown */, context.getPackageName(),
-                false /* force */);
+                false /* force */, false /* wipeEuicc */);
     }
 
     /** {@hide} */
     public static void rebootWipeUserData(Context context, String reason) throws IOException {
-        rebootWipeUserData(context, false /* shutdown */, reason, false /* force */);
+        rebootWipeUserData(context, false /* shutdown */, reason, false /* force */,
+                false /* wipeEuicc */);
     }
 
     /** {@hide} */
     public static void rebootWipeUserData(Context context, boolean shutdown)
             throws IOException {
-        rebootWipeUserData(context, shutdown, context.getPackageName(), false /* force */);
+        rebootWipeUserData(context, shutdown, context.getPackageName(), false /* force */,
+                false /* wipeEuicc */);
+    }
+
+    /** {@hide} */
+    public static void rebootWipeUserData(Context context, boolean shutdown, String reason,
+            boolean force) throws IOException {
+        rebootWipeUserData(context, shutdown, reason, force, false /* wipeEuicc */);
     }
 
     /**
@@ -701,6 +720,7 @@
      * @param reason    the reason for the wipe that is visible in the logs
      * @param force     whether the {@link UserManager.DISALLOW_FACTORY_RESET} user restriction
      *                  should be ignored
+     * @param wipeEuicc whether wipe the euicc data
      *
      * @throws IOException  if writing the recovery command file
      * fails, or if the reboot itself fails.
@@ -709,7 +729,7 @@
      * @hide
      */
     public static void rebootWipeUserData(Context context, boolean shutdown, String reason,
-            boolean force) throws IOException {
+            boolean force, boolean wipeEuicc) throws IOException {
         UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
         if (!force && um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) {
             throw new SecurityException("Wiping data is not allowed for this user.");
@@ -731,6 +751,10 @@
         // Block until the ordered broadcast has completed.
         condition.block();
 
+        if (wipeEuicc) {
+            wipeEuiccData(context);
+        }
+
         String shutdownArg = null;
         if (shutdown) {
             shutdownArg = "--shutdown_after";
@@ -745,6 +769,61 @@
         bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);
     }
 
+    private static void wipeEuiccData(Context context) {
+        EuiccManager euiccManager = (EuiccManager) context.getSystemService(
+                Context.EUICC_SERVICE);
+        if (euiccManager != null && euiccManager.isEnabled()) {
+            CountDownLatch euiccFactoryResetLatch = new CountDownLatch(1);
+
+            BroadcastReceiver euiccWipeFinishReceiver = new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    if (ACTION_WIPE_EUICC_DATA.equals(intent.getAction())) {
+                        if (getResultCode() != EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_OK) {
+                            int detailedCode = intent.getIntExtra(
+                                    EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE, 0);
+                            Log.e(TAG, "Error wiping euicc data, Detailed code = "
+                                    + detailedCode);
+                        } else {
+                            Log.d(TAG, "Successfully wiped euicc data.");
+                        }
+                        euiccFactoryResetLatch.countDown();
+                    }
+                }
+            };
+
+            Intent intent = new Intent(ACTION_WIPE_EUICC_DATA);
+            intent.setPackage("android");
+            PendingIntent callbackIntent = PendingIntent.getBroadcastAsUser(
+                    context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT, UserHandle.SYSTEM);
+            IntentFilter filterConsent = new IntentFilter();
+            filterConsent.addAction(ACTION_WIPE_EUICC_DATA);
+            HandlerThread euiccHandlerThread = new HandlerThread("euiccWipeFinishReceiverThread");
+            euiccHandlerThread.start();
+            Handler euiccHandler = new Handler(euiccHandlerThread.getLooper());
+            context.registerReceiver(euiccWipeFinishReceiver, filterConsent, null, euiccHandler);
+            euiccManager.eraseSubscriptions(callbackIntent);
+            try {
+                long waitingTimeMillis = Settings.Global.getLong(
+                        context.getContentResolver(),
+                        Settings.Global.EUICC_WIPING_TIMEOUT_MILLIS,
+                        DEFAULT_EUICC_WIPING_TIMEOUT_MILLIS);
+                if (waitingTimeMillis < MIN_EUICC_WIPING_TIMEOUT_MILLIS) {
+                    waitingTimeMillis = MIN_EUICC_WIPING_TIMEOUT_MILLIS;
+                } else if (waitingTimeMillis > MAX_EUICC_WIPING_TIMEOUT_MILLIS) {
+                    waitingTimeMillis = MAX_EUICC_WIPING_TIMEOUT_MILLIS;
+                }
+                if (!euiccFactoryResetLatch.await(waitingTimeMillis, TimeUnit.MILLISECONDS)) {
+                    Log.e(TAG, "Timeout wiping eUICC data.");
+                }
+                context.unregisterReceiver(euiccWipeFinishReceiver);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                Log.e(TAG, "Wiping eUICC data interrupted", e);
+            }
+        }
+    }
+
     /** {@hide} */
     public static void rebootPromptAndWipeUserData(Context context, String reason)
             throws IOException {
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index b4c5025..b72f8c5 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -302,6 +302,7 @@
     <protected-broadcast android:name="com.android.server.WifiManager.action.DEVICE_IDLE" />
     <protected-broadcast android:name="com.android.server.action.REMOTE_BUGREPORT_SHARING_ACCEPTED" />
     <protected-broadcast android:name="com.android.server.action.REMOTE_BUGREPORT_SHARING_DECLINED" />
+    <protected-broadcast android:name="com.android.server.action.WIPE_EUICC_DATA" />
     <protected-broadcast android:name="com.android.server.usb.ACTION_OPEN_IN_APPS" />
     <protected-broadcast android:name="com.android.server.am.DELETE_DUMPHEAP" />
     <protected-broadcast android:name="com.android.server.net.action.SNOOZE_WARNING" />
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 136d335..8cac6e0 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -43,6 +43,7 @@
 import static android.app.admin.DevicePolicyManager.DELEGATION_PERMISSION_GRANT;
 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
 import static android.app.admin.DevicePolicyManager.PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER;
+import static android.app.admin.DevicePolicyManager.WIPE_EUICC;
 import static android.app.admin.DevicePolicyManager.WIPE_EXTERNAL_STORAGE;
 import static android.app.admin.DevicePolicyManager.WIPE_RESET_PROTECTION_DATA;
 import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES;
@@ -1691,9 +1692,9 @@
             mContext.getSystemService(PowerManager.class).reboot(reason);
         }
 
-        void recoverySystemRebootWipeUserData(boolean shutdown, String reason, boolean force)
-                throws IOException {
-            RecoverySystem.rebootWipeUserData(mContext, shutdown, reason, force);
+        void recoverySystemRebootWipeUserData(boolean shutdown, String reason, boolean force,
+                boolean wipeEuicc) throws IOException {
+            RecoverySystem.rebootWipeUserData(mContext, shutdown, reason, force, wipeEuicc);
         }
 
         boolean systemPropertiesGetBoolean(String key, boolean def) {
@@ -5302,7 +5303,7 @@
         }
     }
 
-    private void forceWipeDeviceNoLock(boolean wipeExtRequested, String reason) {
+    private void forceWipeDeviceNoLock(boolean wipeExtRequested, String reason, boolean wipeEuicc) {
         wtfIfInLock();
 
         if (wipeExtRequested) {
@@ -5312,7 +5313,7 @@
         }
         try {
             mInjector.recoverySystemRebootWipeUserData(
-                    /*shutdown=*/ false, reason, /*force=*/ true);
+                    /*shutdown=*/ false, reason, /*force=*/ true, /*wipeEuicc=*/ wipeEuicc);
         } catch (IOException | SecurityException e) {
             Slog.w(LOG_TAG, "Failed requesting data wipe", e);
         }
@@ -5389,7 +5390,7 @@
             // removes that user (but still clears FRP...)
             if (userId == UserHandle.USER_SYSTEM) {
                 forceWipeDeviceNoLock(/*wipeExtRequested=*/ (flags & WIPE_EXTERNAL_STORAGE) != 0,
-                        reason);
+                        reason, /*wipeEuicc=*/ (flags & WIPE_EUICC) != 0);
             } else {
                 forceWipeUser(userId);
             }
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
index a33153e..5471715 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
@@ -269,9 +269,9 @@
         }
 
         @Override
-        void recoverySystemRebootWipeUserData(boolean shutdown, String reason, boolean force)
-                throws IOException {
-            services.recoverySystem.rebootWipeUserData(shutdown, reason, force);
+        void recoverySystemRebootWipeUserData(boolean shutdown, String reason, boolean force,
+                boolean wipeEuicc) throws IOException {
+            services.recoverySystem.rebootWipeUserData(shutdown, reason, force, wipeEuicc);
         }
 
         @Override
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index 6393b0b..c58b733 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -17,6 +17,7 @@
 
 import static android.app.admin.DevicePolicyManager.DELEGATION_APP_RESTRICTIONS;
 import static android.app.admin.DevicePolicyManager.DELEGATION_CERT_INSTALL;
+import static android.app.admin.DevicePolicyManager.WIPE_EUICC;
 import static android.os.UserManagerInternal.CAMERA_DISABLED_GLOBALLY;
 import static android.os.UserManagerInternal.CAMERA_DISABLED_LOCALLY;
 import static android.os.UserManagerInternal.CAMERA_NOT_DISABLED;
@@ -3451,7 +3452,21 @@
 
         dpm.wipeData(0);
         verify(getServices().recoverySystem).rebootWipeUserData(
-                /*shutdown=*/ eq(false), anyString(), /*force=*/ eq(true));
+                /*shutdown=*/ eq(false), anyString(), /*force=*/ eq(true),
+                /*wipeEuicc=*/ eq(false));
+    }
+
+    public void testWipeEuiccDataEnabled() throws Exception {
+        setDeviceOwner();
+        when(getServices().userManager.getUserRestrictionSource(
+            UserManager.DISALLOW_FACTORY_RESET,
+            UserHandle.SYSTEM))
+            .thenReturn(UserManager.RESTRICTION_SOURCE_DEVICE_OWNER);
+
+        dpm.wipeData(WIPE_EUICC);
+        verify(getServices().recoverySystem).rebootWipeUserData(
+                /*shutdown=*/ eq(false), anyString(), /*force=*/ eq(true),
+                /*wipeEuicc=*/ eq(true));
     }
 
     public void testWipeDataDeviceOwnerDisallowed() throws Exception {
@@ -3549,7 +3564,8 @@
         // The device should be wiped even if DISALLOW_FACTORY_RESET is enabled, because both the
         // user restriction and the policy were set by the DO.
         verify(getServices().recoverySystem).rebootWipeUserData(
-                /*shutdown=*/ eq(false), anyString(), /*force=*/ eq(true));
+                /*shutdown=*/ eq(false), anyString(), /*force=*/ eq(true),
+                /*wipeEuicc=*/ eq(false));
     }
 
     public void testMaximumFailedPasswordAttemptsReachedDeviceOwnerDisallowed() throws Exception {
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java
index ed8de05..8121bcf 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java
@@ -314,8 +314,8 @@
     }
 
     public static class RecoverySystemForMock {
-        public void rebootWipeUserData(
-                boolean shutdown, String reason, boolean force) throws IOException {
+        public void rebootWipeUserData(boolean shutdown, String reason, boolean force,
+                boolean wipeEuicc) throws IOException {
         }
     }