content: /*
   * Copyright (C) 2010 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.os;
  
  import static java.nio.charset.StandardCharsets.UTF_8;
  
  import android.annotation.RequiresPermission;
  import android.annotation.SuppressLint;
  import android.annotation.SystemApi;
  import android.annotation.SystemService;
  import android.annotation.UnsupportedAppUsage;
  import android.app.PendingIntent;
  import android.content.BroadcastReceiver;
  import android.content.ContentResolver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.content.pm.PackageManager;
  import android.provider.Settings;
  import android.telephony.SubscriptionInfo;
  import android.telephony.SubscriptionManager;
  import android.telephony.euicc.EuiccManager;
  import android.text.TextUtils;
  import android.text.format.DateFormat;
  import android.util.Log;
  import android.view.Display;
  import android.view.WindowManager;
  
  import libcore.io.Streams;
  
  import java.io.ByteArrayInputStream;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileNotFoundException;
  import java.io.FileWriter;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.RandomAccessFile;
  import java.security.GeneralSecurityException;
  import java.security.PublicKey;
  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.List;
  import java.util.Locale;
  import java.util.concurrent.CountDownLatch;
  import java.util.concurrent.TimeUnit;
  import java.util.concurrent.atomic.AtomicBoolean;
  import java.util.concurrent.atomic.AtomicInteger;
  import java.util.zip.ZipEntry;
  import java.util.zip.ZipFile;
  import java.util.zip.ZipInputStream;
  
  import sun.security.pkcs.PKCS7;
  import sun.security.pkcs.SignerInfo;
  
  /**
   * RecoverySystem contains methods for interacting with the Android
   * recovery system (the separate partition that can be used to install
   * system updates, wipe user data, etc.)
   */
  @SystemService(Context.RECOVERY_SERVICE)
  public class RecoverySystem {
      private static final String TAG = "RecoverySystem";
  
      /**
       * Default location of zip file containing public keys (X509
       * certs) authorized to sign OTA updates.
       */
      private static final File DEFAULT_KEYSTORE =
          new File("/system/etc/security/otacerts.zip");
  
      /** 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_FACTORY_RESET_TIMEOUT_MILLIS = 30000L; // 30 s
      private static final long MIN_EUICC_FACTORY_RESET_TIMEOUT_MILLIS = 5000L; // 5 s
      private static final long MAX_EUICC_FACTORY_RESET_TIMEOUT_MILLIS = 60000L; // 60 s
  
      private static final long DEFAULT_EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS =
              45000L; // 45 s
      private static final long MIN_EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS = 15000L; // 15 s
      private static final long MAX_EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS = 90000L; // 90 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 String LAST_INSTALL_PATH = "last_install";
      private static final String LAST_PREFIX = "last_";
      private static final String ACTION_EUICC_FACTORY_RESET =
              "com.android.internal.action.EUICC_FACTORY_RESET";
      private static final String ACTION_EUICC_REMOVE_INVISIBLE_SUBSCRIPTIONS =
              "com.android.internal.action.EUICC_REMOVE_INVISIBLE_SUBSCRIPTIONS";
  
      /**
       * Used in {@link #wipeEuiccData} & {@link #removeEuiccInvisibleSubs} as package name of
       * callback intent.
       */
      private static final String PACKAGE_NAME_EUICC_DATA_MANAGEMENT_CALLBACK = "android";
  
      /**
       * The recovery image uses this file to identify the location (i.e. blocks)
       * of an OTA package on the /data partition. The block map file is
       * generated by uncrypt.
       *
       * @hide
       */
      public static final File BLOCK_MAP_FILE = new File(RECOVERY_DIR, "block.map");
  
      /**
       * UNCRYPT_PACKAGE_FILE stores the filename to be uncrypt'd, which will be
       * read by uncrypt.
       *
       * @hide
       */
      public static final File UNCRYPT_PACKAGE_FILE = new File(RECOVERY_DIR, "uncrypt_file");
  
      /**
       * UNCRYPT_STATUS_FILE stores the time cost (and error code in the case of a failure)
       * of uncrypt.
       *
       * @hide
       */
      public static final File UNCRYPT_STATUS_FILE = new File(RECOVERY_DIR, "uncrypt_status");
  
      // Length limits for reading files.
      private static final int LOG_FILE_MAX_LENGTH = 64 * 1024;
  
      // Prevent concurrent execution of requests.
      private static final Object sRequestLock = new Object();
  
      private final IRecoverySystem mService;
  
      /**
       * Interface definition for a callback to be invoked regularly as
       * verification proceeds.
       */
      public interface ProgressListener {
          /**
           * Called periodically as the verification progresses.
           *
           * @param progress  the approximate percentage of the
           *        verification that has been completed, ranging from 0
           *        to 100 (inclusive).
           */
          public void onProgress(int progress);
      }
  
      /** @return the set of certs that can be used to sign an OTA package. */
      private static HashSet<X509Certificate> getTrustedCerts(File keystore)
          throws IOException, GeneralSecurityException {
          HashSet<X509Certificate> trusted = new HashSet<X509Certificate>();
          if (keystore == null) {
              keystore = DEFAULT_KEYSTORE;
          }
          ZipFile zip = new ZipFile(keystore);
          try {
              CertificateFactory cf = CertificateFactory.getInstance("X.509");
              Enumeration<? extends ZipEntry> entries = zip.entries();
              while (entries.hasMoreElements()) {
                  ZipEntry entry = entries.nextElement();
                  InputStream is = zip.getInputStream(entry);
                  try {
                      trusted.add((X509Certificate) cf.generateCertificate(is));
                  } finally {
                      is.close();
                  }
              }
          } finally {
              zip.close();
          }
          return trusted;
      }
  
      /**
       * Verify the cryptographic signature of a system update package
       * before installing it.  Note that the package is also verified
       * separately by the installer once the device is rebooted into
       * the recovery system.  This function will return only if the
       * package was successfully verified; otherwise it will throw an
       * exception.
       *
       * Verification of a package can take significant time, so this
       * function should not be called from a UI thread.  Interrupting
       * the thread while this function is in progress will result in a
       * SecurityException being thrown (and the thread's interrupt flag
       * will be cleared).
       *
       * @param packageFile  the package to be verified
       * @param listener     an object to receive periodic progress
       * updates as verification proceeds.  May be null.
       * @param deviceCertsZipFile  the zip file of certificates whose
       * public keys we will accept.  Verification succeeds if the
       * package is signed by the private key corresponding to any
       * public key in this file.  May be null to use the system default
       * file (currently "/system/etc/security/otacerts.zip").
       *
       * @throws IOException if there were any errors reading the
       * package or certs files.
       * @throws GeneralSecurityException if verification failed
       */
      public static void verifyPackage(File packageFile,
                                       ProgressListener listener,
                                       File deviceCertsZipFile)
          throws IOException, GeneralSecurityException {
          final long fileLen = packageFile.length();
  
          final RandomAccessFile raf = new RandomAccessFile(packageFile, "r");
          try {
              final long startTimeMillis = System.currentTimeMillis();
              if (listener != null) {
                  listener.onProgress(0);
              }
  
              raf.seek(fileLen - 6);
              byte[] footer = new byte[6];
              raf.readFully(footer);
  
              if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) {
                  throw new SignatureException("no signature in file (no footer)");
              }
  
              final int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8);
              final int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8);
  
              byte[] eocd = new byte[commentSize + 22];
              raf.seek(fileLen - (commentSize + 22));
              raf.readFully(eocd);
  
              // Check that we have found the start of the
              // end-of-central-directory record.
              if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b ||
                  eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) {
                  throw new SignatureException("no signature in file (bad footer)");
              }
  
              for (int i = 4; i < eocd.length-3; ++i) {
                  if (eocd[i  ] == (byte)0x50 && eocd[i+1] == (byte)0x4b &&
                      eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) {
                      throw new SignatureException("EOCD marker found after start of EOCD");
                  }
              }
  
              // Parse the signature
              PKCS7 block =
                  new PKCS7(new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart));
  
              // Take the first certificate from the signature (packages
              // should contain only one).
              X509Certificate[] certificates = block.getCertificates();
              if (certificates == null || certificates.length == 0) {
                  throw new SignatureException("signature contains no certificates");
              }
              X509Certificate cert = certificates[0];
              PublicKey signatureKey = cert.getPublicKey();
  
              SignerInfo[] signerInfos = block.getSignerInfos();
              if (signerInfos == null || signerInfos.length == 0) {
                  throw new SignatureException("signature contains no signedData");
              }
              SignerInfo signerInfo = signerInfos[0];
  
              // Check that the public key of the certificate contained
              // in the package equals one of our trusted public keys.
              boolean verified = false;
              HashSet<X509Certificate> trusted = getTrustedCerts(
                  deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile);
              for (X509Certificate c : trusted) {
                  if (c.getPublicKey().equals(signatureKey)) {
                      verified = true;
                      break;
                  }
              }
              if (!verified) {
                  throw new SignatureException("signature doesn't match any trusted key");
              }
  
              // The signature cert matches a trusted key.  Now verify that
              // the digest in the cert matches the actual file data.
              raf.seek(0);
              final ProgressListener listenerForInner = listener;
              SignerInfo verifyResult = block.verify(signerInfo, new InputStream() {
                  // The signature covers all of the OTA package except the
                  // archive comment and its 2-byte length.
                  long toRead = fileLen - commentSize - 2;
                  long soFar = 0;
  
                  int lastPercent = 0;
                  long lastPublishTime = startTimeMillis;
  
                  @Override
                  public int read() throws IOException {
                      throw new UnsupportedOperationException();
                  }
  
                  @Override
                  public int read(byte[] b, int off, int len) throws IOException {
                      if (soFar >= toRead) {
                          return -1;
                      }
                      if (Thread.currentThread().isInterrupted()) {
                          return -1;
                      }
  
                      int size = len;
                      if (soFar + size > toRead) {
                          size = (int)(toRead - soFar);
                      }
                      int read = raf.read(b, off, size);
                      soFar += read;
  
                      if (listenerForInner != null) {
                          long now = System.currentTimeMillis();
                          int p = (int)(soFar * 100 / toRead);
                          if (p > lastPercent &&
                              now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
                              lastPercent = p;
                              lastPublishTime = now;
                              listenerForInner.onProgress(lastPercent);
                          }
                      }
  
                      return read;
                  }
              });
  
              final boolean interrupted = Thread.interrupted();
              if (listener != null) {
                  listener.onProgress(100);
              }
  
              if (interrupted) {
                  throw new SignatureException("verification was interrupted");
              }
  
              if (verifyResult == null) {
                  throw new SignatureException("signature digest verification failed");
              }
          } finally {
              raf.close();
          }
  
          // Additionally verify the package compatibility.
          if (!readAndVerifyPackageCompatibilityEntry(packageFile)) {
              throw new SignatureException("package compatibility verification failed");
          }
      }
  
      /**
       * Verifies the compatibility entry from an {@link InputStream}.
       *
       * @return the verification result.
       */
      @UnsupportedAppUsage
      private static boolean verifyPackageCompatibility(InputStream inputStream) throws IOException {
          ArrayList<String> list = new ArrayList<>();
          ZipInputStream zis = new ZipInputStream(inputStream);
          ZipEntry entry;
          while ((entry = zis.getNextEntry()) != null) {
              long entrySize = entry.getSize();
              if (entrySize > Integer.MAX_VALUE || entrySize < 0) {
                  throw new IOException(
                          "invalid entry size (" + entrySize + ") in the compatibility file");
              }
              byte[] bytes = new byte[(int) entrySize];
              Streams.readFully(zis, bytes);
              list.add(new String(bytes, UTF_8));
          }
          if (list.isEmpty()) {
              throw new IOException("no entries found in the compatibility file");
          }
          return (VintfObject.verify(list.toArray(new String[list.size()])) == 0);
      }
  
      /**
       * Reads and verifies the compatibility entry in an OTA zip package. The compatibility entry is
       * a zip file (inside the OTA package zip).
       *
       * @return {@code true} if the entry doesn't exist or verification passes.
       */
      private static boolean readAndVerifyPackageCompatibilityEntry(File packageFile)
              throws IOException {
          try (ZipFile zip = new ZipFile(packageFile)) {
              ZipEntry entry = zip.getEntry("compatibility.zip");
              if (entry == null) {
                  return true;
              }
              InputStream inputStream = zip.getInputStream(entry);
              return verifyPackageCompatibility(inputStream);
          }
      }
  
      /**
       * Verifies the package compatibility info against the current system.
       *
       * @param compatibilityFile the {@link File} that contains the package compatibility info.
       * @throws IOException if there were any errors reading the compatibility file.
       * @return the compatibility verification result.
       *
       * {@hide}
       */
      @SystemApi
      @SuppressLint("Doclava125")
      public static boolean verifyPackageCompatibility(File compatibilityFile) throws IOException {
          try (InputStream inputStream = new FileInputStream(compatibilityFile)) {
              return verifyPackageCompatibility(inputStream);
          }
      }
  
      /**
       * Process a given package with uncrypt. No-op if the package is not on the
       * /data partition.
       *
       * @param Context      the Context to use
       * @param packageFile  the package to be processed
       * @param listener     an object to receive periodic progress updates as
       *                     processing proceeds.  May be null.
       * @param handler      the Handler upon which the callbacks will be
       *                     executed.
       *
       * @throws IOException if there were any errors processing the package file.
       *
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.RECOVERY)
      public static void processPackage(Context context,
                                        File packageFile,
                                        final ProgressListener listener,
                                        final Handler handler)
              throws IOException {
          String filename = packageFile.getCanonicalPath();
          if (!filename.startsWith("/data/")) {
              return;
          }
  
          RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE);
          IRecoverySystemProgressListener progressListener = null;
          if (listener != null) {
              final Handler progressHandler;
              if (handler != null) {
                  progressHandler = handler;
              } else {
                  progressHandler = new Handler(context.getMainLooper());
              }
              progressListener = new IRecoverySystemProgressListener.Stub() {
                  int lastProgress = 0;
                  long lastPublishTime = System.currentTimeMillis();
  
                  @Override
                  public void onProgress(final int progress) {
                      final long now = System.currentTimeMillis();
                      progressHandler.post(new Runnable() {
                          @Override
                          public void run() {
                              if (progress > lastProgress &&
                                      now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
                                  lastProgress = progress;
                                  lastPublishTime = now;
                                  listener.onProgress(progress);
                              }
                          }
                      });
                  }
              };
          }
  
          if (!rs.uncrypt(filename, progressListener)) {
              throw new IOException("process package failed");
          }
      }
  
      /**
       * Process a given package with uncrypt. No-op if the package is not on the
       * /data partition.
       *
       * @param Context      the Context to use
       * @param packageFile  the package to be processed
       * @param listener     an object to receive periodic progress updates as
       *                     processing proceeds.  May be null.
       *
       * @throws IOException if there were any errors processing the package file.
       *
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.RECOVERY)
      public static void processPackage(Context context,
                                        File packageFile,
                                        final ProgressListener listener)
              throws IOException {
          processPackage(context, packageFile, listener, null);
      }
  
      /**
       * Reboots the device in order to install the given update
       * package.
       * Requires the {@link android.Manifest.permission#REBOOT} permission.
       *
       * @param context      the Context to use
       * @param packageFile  the update package to install.  Must be on
       * a partition mountable by recovery.  (The set of partitions
       * known to recovery may vary from device to device.  Generally,
       * /cache and /data are safe.)
       *
       * @throws IOException  if writing the recovery command file
       * fails, or if the reboot itself fails.
       */
      @RequiresPermission(android.Manifest.permission.RECOVERY)
      public static void installPackage(Context context, File packageFile)
              throws IOException {
          installPackage(context, packageFile, false);
      }
  
      /**
       * If the package hasn't been processed (i.e. uncrypt'd), set up
       * UNCRYPT_PACKAGE_FILE and delete BLOCK_MAP_FILE to trigger uncrypt during the
       * reboot.
       *
       * @param context      the Context to use
       * @param packageFile  the update package to install.  Must be on a
       * partition mountable by recovery.
       * @param processed    if the package has been processed (uncrypt'd).
       *
       * @throws IOException if writing the recovery command file fails, or if
       * the reboot itself fails.
       *
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.RECOVERY)
      public static void installPackage(Context context, File packageFile, boolean processed)
              throws IOException {
          synchronized (sRequestLock) {
              LOG_FILE.delete();
              // Must delete the file in case it was created by system server.
              UNCRYPT_PACKAGE_FILE.delete();
  
              String filename = packageFile.getCanonicalPath();
              Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
  
              // If the package name ends with "_s.zip", it's a security update.
              boolean securityUpdate = filename.endsWith("_s.zip");
  
              // If the package is on the /data partition, the package needs to
              // be processed (i.e. uncrypt'd). The caller specifies if that has
              // been done in 'processed' parameter.
              if (filename.startsWith("/data/")) {
                  if (processed) {
                      if (!BLOCK_MAP_FILE.exists()) {
                          Log.e(TAG, "Package claimed to have been processed but failed to find "
                                  + "the block map file.");
                          throw new IOException("Failed to find block map file");
                      }
                  } else {
                      FileWriter uncryptFile = new FileWriter(UNCRYPT_PACKAGE_FILE);
                      try {
                          uncryptFile.write(filename + "\n");
                      } finally {
                          uncryptFile.close();
                      }
                      // UNCRYPT_PACKAGE_FILE needs to be readable and writable
                      // by system server.
                      if (!UNCRYPT_PACKAGE_FILE.setReadable(true, false)
                              || !UNCRYPT_PACKAGE_FILE.setWritable(true, false)) {
                          Log.e(TAG, "Error setting permission for " + UNCRYPT_PACKAGE_FILE);
                      }
  
                      BLOCK_MAP_FILE.delete();
                  }
  
                  // If the package is on the /data partition, use the block map
                  // file as the package name instead.
                  filename = "@/cache/recovery/block.map";
              }
  
              final String filenameArg = "--update_package=" + filename + "\n";
              final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() + "\n";
              final String securityArg = "--security\n";
  
              String command = filenameArg + localeArg;
              if (securityUpdate) {
                  command += securityArg;
              }
  
              RecoverySystem rs = (RecoverySystem) context.getSystemService(
                      Context.RECOVERY_SERVICE);
              if (!rs.setupBcb(command)) {
                  throw new IOException("Setup BCB failed");
              }
  
              // Having set up the BCB (bootloader control block), go ahead and reboot
              PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
              String reason = PowerManager.REBOOT_RECOVERY_UPDATE;
  
              // On TV, reboot quiescently if the screen is off
              if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
                  WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
                  if (wm.getDefaultDisplay().getState() != Display.STATE_ON) {
                      reason += ",quiescent";
                  }
              }
              pm.reboot(reason);
  
              throw new IOException("Reboot failed (no permissions?)");
          }
      }
  
      /**
       * Schedule to install the given package on next boot. The caller needs to
       * ensure that the package must have been processed (uncrypt'd) if needed.
       * It sets up the command in BCB (bootloader control block), which will
       * be read by the bootloader and the recovery image.
       *
       * @param Context      the Context to use.
       * @param packageFile  the package to be installed.
       *
       * @throws IOException if there were any errors setting up the BCB.
       *
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.RECOVERY)
      public static void scheduleUpdateOnBoot(Context context, File packageFile)
              throws IOException {
          String filename = packageFile.getCanonicalPath();
          boolean securityUpdate = filename.endsWith("_s.zip");
  
          // If the package is on the /data partition, use the block map file as
          // the package name instead.
          if (filename.startsWith("/data/")) {
              filename = "@/cache/recovery/block.map";
          }
  
          final String filenameArg = "--update_package=" + filename + "\n";
          final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() + "\n";
          final String securityArg = "--security\n";
  
          String command = filenameArg + localeArg;
          if (securityUpdate) {
              command += securityArg;
          }
  
          RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE);
          if (!rs.setupBcb(command)) {
              throw new IOException("schedule update on boot failed");
          }
      }
  
      /**
       * Cancel any scheduled update by clearing up the BCB (bootloader control
       * block).
       *
       * @param Context      the Context to use.
       *
       * @throws IOException if there were any errors clearing up the BCB.
       *
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.RECOVERY)
      public static void cancelScheduledUpdate(Context context)
              throws IOException {
          RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE);
          if (!rs.clearBcb()) {
              throw new IOException("cancel scheduled update failed");
          }
      }
  
      /**
       * Reboots the device and wipes the user data and cache
       * partitions.  This is sometimes called a "factory reset", which
       * is something of a misnomer because the system partition is not
       * restored to its factory state.  Requires the
       * {@link android.Manifest.permission#REBOOT} permission.
       *
       * @param context  the Context to use
       *
       * @throws IOException  if writing the recovery command file
       * fails, or if the reboot itself fails.
       * @throws SecurityException if the current user is not allowed to wipe data.
       */
      public static void rebootWipeUserData(Context context) throws IOException {
          rebootWipeUserData(context, false /* shutdown */, context.getPackageName(),
                  false /* force */, false /* wipeEuicc */);
      }
  
      /** {@hide} */
      public static void rebootWipeUserData(Context context, String reason) throws IOException {
          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 */,
                  false /* wipeEuicc */);
      }
  
      /** {@hide} */
      public static void rebootWipeUserData(Context context, boolean shutdown, String reason,
              boolean force) throws IOException {
          rebootWipeUserData(context, shutdown, reason, force, false /* wipeEuicc */);
      }
  
      /**
       * Reboots the device and wipes the user data and cache
       * partitions.  This is sometimes called a "factory reset", which
       * is something of a misnomer because the system partition is not
       * restored to its factory state.  Requires the
       * {@link android.Manifest.permission#REBOOT} permission.
       *
       * @param context   the Context to use
       * @param shutdown  if true, the device will be powered down after
       *                  the wipe completes, rather than being rebooted
       *                  back to the regular system.
       * @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.
       * @throws SecurityException if the current user is not allowed to wipe data.
       *
       * @hide
       */
      public static void rebootWipeUserData(Context context, boolean shutdown, String reason,
              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.");
          }
          final ConditionVariable condition = new ConditionVariable();
  
          Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION");
          intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND
                  | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
          context.sendOrderedBroadcastAsUser(intent, UserHandle.SYSTEM,
                  android.Manifest.permission.MASTER_CLEAR,
                  new BroadcastReceiver() {
                      @Override
                      public void onReceive(Context context, Intent intent) {
                          condition.open();
                      }
                  }, null, 0, null, null);
  
          // Block until the ordered broadcast has completed.
          condition.block();
  
          EuiccManager euiccManager = context.getSystemService(EuiccManager.class);
          if (wipeEuicc) {
              wipeEuiccData(context, PACKAGE_NAME_EUICC_DATA_MANAGEMENT_CALLBACK);
          } else {
              removeEuiccInvisibleSubs(context, euiccManager);
          }
  
          String shutdownArg = null;
          if (shutdown) {
              shutdownArg = "--shutdown_after";
          }
  
          String reasonArg = null;
          if (!TextUtils.isEmpty(reason)) {
              String timeStamp = DateFormat.format("yyyy-MM-ddTHH:mm:ssZ", System.currentTimeMillis()).toString();
              reasonArg = "--reason=" + sanitizeArg(reason + "," + timeStamp);
          }
  
          final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() ;
          bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);
      }
  
      /**
       * Returns whether wipe Euicc data successfully or not.
       *
       * @param packageName the package name of the caller app.
       *
       * @hide
       */
      public static boolean wipeEuiccData(Context context, final String packageName) {
          ContentResolver cr = context.getContentResolver();
          if (Settings.Global.getInt(cr, Settings.Global.EUICC_PROVISIONED, 0) == 0) {
              // If the eUICC isn't provisioned, there's no reason to either wipe or retain profiles,
              // as there's nothing to wipe nor retain.
              Log.d(TAG, "Skipping eUICC wipe/retain as it is not provisioned");
              return true;
          }
  
          EuiccManager euiccManager = (EuiccManager) context.getSystemService(
                  Context.EUICC_SERVICE);
          if (euiccManager != null && euiccManager.isEnabled()) {
              CountDownLatch euiccFactoryResetLatch = new CountDownLatch(1);
              final AtomicBoolean wipingSucceeded = new AtomicBoolean(false);
  
              BroadcastReceiver euiccWipeFinishReceiver = new BroadcastReceiver() {
                  @Override
                  public void onReceive(Context context, Intent intent) {
                      if (ACTION_EUICC_FACTORY_RESET.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.");
                              wipingSucceeded.set(true /* newValue */);
                          }
                          euiccFactoryResetLatch.countDown();
                      }
                  }
              };
  
              Intent intent = new Intent(ACTION_EUICC_FACTORY_RESET);
              intent.setPackage(packageName);
              PendingIntent callbackIntent = PendingIntent.getBroadcastAsUser(
                      context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT, UserHandle.SYSTEM);
              IntentFilter filterConsent = new IntentFilter();
              filterConsent.addAction(ACTION_EUICC_FACTORY_RESET);
              HandlerThread euiccHandlerThread = new HandlerThread("euiccWipeFinishReceiverThread");
              euiccHandlerThread.start();
              Handler euiccHandler = new Handler(euiccHandlerThread.getLooper());
              context.getApplicationContext()
                      .registerReceiver(euiccWipeFinishReceiver, filterConsent, null, euiccHandler);
              euiccManager.eraseSubscriptions(callbackIntent);
              try {
                  long waitingTimeMillis = Settings.Global.getLong(
                          context.getContentResolver(),
                          Settings.Global.EUICC_FACTORY_RESET_TIMEOUT_MILLIS,
                          DEFAULT_EUICC_FACTORY_RESET_TIMEOUT_MILLIS);
                  if (waitingTimeMillis < MIN_EUICC_FACTORY_RESET_TIMEOUT_MILLIS) {
                      waitingTimeMillis = MIN_EUICC_FACTORY_RESET_TIMEOUT_MILLIS;
                  } else if (waitingTimeMillis > MAX_EUICC_FACTORY_RESET_TIMEOUT_MILLIS) {
                      waitingTimeMillis = MAX_EUICC_FACTORY_RESET_TIMEOUT_MILLIS;
                  }
                  if (!euiccFactoryResetLatch.await(waitingTimeMillis, TimeUnit.MILLISECONDS)) {
                      Log.e(TAG, "Timeout wiping eUICC data.");
                      return false;
                  }
              } catch (InterruptedException e) {
                  Thread.currentThread().interrupt();
                  Log.e(TAG, "Wiping eUICC data interrupted", e);
                  return false;
              } finally {
                  context.getApplicationContext().unregisterReceiver(euiccWipeFinishReceiver);
              }
              return wipingSucceeded.get();
          }
          return false;
      }
  
      private static void removeEuiccInvisibleSubs(
              Context context, EuiccManager euiccManager) {
          ContentResolver cr = context.getContentResolver();
          if (Settings.Global.getInt(cr, Settings.Global.EUICC_PROVISIONED, 0) == 0) {
              // If the eUICC isn't provisioned, there's no need to remove euicc invisible profiles,
              // as there's nothing to be removed.
              Log.i(TAG, "Skip removing eUICC invisible profiles as it is not provisioned.");
              return;
          } else if (euiccManager == null || !euiccManager.isEnabled()) {
              Log.i(TAG, "Skip removing eUICC invisible profiles as eUICC manager is not available.");
              return;
          }
          SubscriptionManager subscriptionManager =
                  context.getSystemService(SubscriptionManager.class);
          List<SubscriptionInfo> availableSubs =
                  subscriptionManager.getAvailableSubscriptionInfoList();
          if (availableSubs == null || availableSubs.isEmpty()) {
              Log.i(TAG, "Skip removing eUICC invisible profiles as no available profiles found.");
              return;
          }
          List<SubscriptionInfo> invisibleSubs = new ArrayList<>();
          for (SubscriptionInfo sub : availableSubs) {
              if (sub.isEmbedded() && !subscriptionManager.isSubscriptionVisible(sub)) {
                  invisibleSubs.add(sub);
              }
          }
          removeEuiccInvisibleSubs(context, invisibleSubs, euiccManager);
      }
  
      private static boolean removeEuiccInvisibleSubs(
              Context context, List<SubscriptionInfo> subscriptionInfos, EuiccManager euiccManager) {
          if (subscriptionInfos == null || subscriptionInfos.isEmpty()) {
              Log.i(TAG, "There are no eUICC invisible profiles needed to be removed.");
              return true;
          }
          CountDownLatch removeSubsLatch = new CountDownLatch(subscriptionInfos.size());
          final AtomicInteger removedSubsCount = new AtomicInteger(0);
  
          BroadcastReceiver removeEuiccSubsReceiver = new BroadcastReceiver() {
              @Override
              public void onReceive(Context context, Intent intent) {
                  if (ACTION_EUICC_REMOVE_INVISIBLE_SUBSCRIPTIONS.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 removing euicc opportunistic profile, Detailed code = "
                                  + detailedCode);
                      } else {
                          Log.e(TAG, "Successfully remove euicc opportunistic profile.");
                          removedSubsCount.incrementAndGet();
                      }
                      removeSubsLatch.countDown();
                  }
              }
          };
  
          Intent intent = new Intent(ACTION_EUICC_REMOVE_INVISIBLE_SUBSCRIPTIONS);
          intent.setPackage(PACKAGE_NAME_EUICC_DATA_MANAGEMENT_CALLBACK);
          PendingIntent callbackIntent = PendingIntent.getBroadcastAsUser(
                  context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT, UserHandle.SYSTEM);
          IntentFilter intentFilter = new IntentFilter();
          intentFilter.addAction(ACTION_EUICC_REMOVE_INVISIBLE_SUBSCRIPTIONS);
          HandlerThread euiccHandlerThread =
                  new HandlerThread("euiccRemovingSubsReceiverThread");
          euiccHandlerThread.start();
          Handler euiccHandler = new Handler(euiccHandlerThread.getLooper());
          context.getApplicationContext()
                  .registerReceiver(
                          removeEuiccSubsReceiver, intentFilter, null, euiccHandler);
          for (SubscriptionInfo subscriptionInfo : subscriptionInfos) {
              Log.i(
                      TAG,
                      "Remove invisible subscription " + subscriptionInfo.getSubscriptionId()
                              + " from card " + subscriptionInfo.getCardId());
              euiccManager.createForCardId(subscriptionInfo.getCardId())
                      .deleteSubscription(subscriptionInfo.getSubscriptionId(), callbackIntent);
          }
          try {
              long waitingTimeMillis = Settings.Global.getLong(
                      context.getContentResolver(),
                      Settings.Global.EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS,
                      DEFAULT_EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS);
              if (waitingTimeMillis < MIN_EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS) {
                  waitingTimeMillis = MIN_EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS;
              } else if (waitingTimeMillis > MAX_EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS) {
                  waitingTimeMillis = MAX_EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS;
              }
              if (!removeSubsLatch.await(waitingTimeMillis, TimeUnit.MILLISECONDS)) {
                  Log.e(TAG, "Timeout removing invisible euicc profiles.");
                  return false;
              }
          } catch (InterruptedException e) {
              Thread.currentThread().interrupt();
              Log.e(TAG, "Removing invisible euicc profiles interrupted", e);
              return false;
          } finally {
              context.getApplicationContext().unregisterReceiver(removeEuiccSubsReceiver);
              if (euiccHandlerThread != null) {
                  euiccHandlerThread.quit();
              }
          }
          return removedSubsCount.get() == subscriptionInfos.size();
      }
  
      /** {@hide} */
      public static void rebootPromptAndWipeUserData(Context context, String reason)
              throws IOException {
          boolean checkpointing = false;
          boolean needReboot = false;
          IVold vold = null;
          try {
              vold = IVold.Stub.asInterface(ServiceManager.checkService("vold"));
              if (vold != null) {
                  checkpointing = vold.needsCheckpoint();
              } else  {
                  Log.w(TAG, "Failed to get vold");
              }
          } catch (Exception e) {
              Log.w(TAG, "Failed to check for checkpointing");
          }
  
          // If we are running in checkpointing mode, we should not prompt a wipe.
          // Checkpointing may save us. If it doesn't, we will wind up here again.
          if (checkpointing) {
              try {
                  vold.abortChanges("rescueparty", false);
                  Log.i(TAG, "Rescue Party requested wipe. Aborting update");
              } catch (Exception e) {
                  Log.i(TAG, "Rescue Party requested wipe. Rebooting instead.");
                  PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
                  pm.reboot("rescueparty");
              }
              return;
          }
  
          String reasonArg = null;
          if (!TextUtils.isEmpty(reason)) {
              reasonArg = "--reason=" + sanitizeArg(reason);
          }
  
          final String localeArg = "--locale=" + Locale.getDefault().toString();
          bootCommand(context, null, "--prompt_and_wipe_data", reasonArg, localeArg);
      }
  
      /**
       * Reboot into the recovery system to wipe the /cache partition.
       * @throws IOException if something goes wrong.
       */
      public static void rebootWipeCache(Context context) throws IOException {
          rebootWipeCache(context, context.getPackageName());
      }
  
      /** {@hide} */
      public static void rebootWipeCache(Context context, String reason) throws IOException {
          String reasonArg = null;
          if (!TextUtils.isEmpty(reason)) {
              reasonArg = "--reason=" + sanitizeArg(reason);
          }
  
          final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() ;
          bootCommand(context, "--wipe_cache", reasonArg, localeArg);
      }
  
      /**
       * Reboot into recovery and wipe the A/B device.
       *
       * @param Context      the Context to use.
       * @param packageFile  the wipe package to be applied.
       * @param reason       the reason to wipe.
       *
       * @throws IOException if something goes wrong.
       *
       * @hide
       */
      @SystemApi
      @RequiresPermission(allOf = {
              android.Manifest.permission.RECOVERY,
              android.Manifest.permission.REBOOT
      })
      public static void rebootWipeAb(Context context, File packageFile, String reason)
              throws IOException {
          String reasonArg = null;
          if (!TextUtils.isEmpty(reason)) {
              reasonArg = "--reason=" + sanitizeArg(reason);
          }
  
          final String filename = packageFile.getCanonicalPath();
          final String filenameArg = "--wipe_package=" + filename;
          final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() ;
          bootCommand(context, "--wipe_ab", filenameArg, reasonArg, localeArg);
      }
  
      /**
       * Reboot into the recovery system with the supplied argument.
       * @param args to pass to the recovery utility.
       * @throws IOException if something goes wrong.
       */
      private static void bootCommand(Context context, String... args) throws IOException {
          LOG_FILE.delete();
  
          StringBuilder command = new StringBuilder();
          for (String arg : args) {
              if (!TextUtils.isEmpty(arg)) {
                  command.append(arg);
                  command.append("\n");
              }
          }
  
          // Write the command into BCB (bootloader control block) and boot from
          // there. Will not return unless failed.
          RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE);
          rs.rebootRecoveryWithCommand(command.toString());
  
          throw new IOException("Reboot failed (no permissions?)");
      }
  
      /**
       * Called after booting to process and remove recovery-related files.
       * @return the log file from recovery, or null if none was found.
       *
       * @hide
       */
      public static String handleAftermath(Context context) {
          // Record the tail of the LOG_FILE
          String log = null;
          try {
              log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n");
          } catch (FileNotFoundException e) {
              Log.i(TAG, "No recovery log file");
          } catch (IOException e) {
              Log.e(TAG, "Error reading recovery log", e);
          }
  
  
          // Only remove the OTA package if it's partially processed (uncrypt'd).
          boolean reservePackage = BLOCK_MAP_FILE.exists();
          if (!reservePackage && UNCRYPT_PACKAGE_FILE.exists()) {
              String filename = null;
              try {
                  filename = FileUtils.readTextFile(UNCRYPT_PACKAGE_FILE, 0, null);
              } catch (IOException e) {
                  Log.e(TAG, "Error reading uncrypt file", e);
              }
  
              // Remove the OTA package on /data that has been (possibly
              // partially) processed. (Bug: 24973532)
              if (filename != null && filename.startsWith("/data")) {
                  if (UNCRYPT_PACKAGE_FILE.delete()) {
                      Log.i(TAG, "Deleted: " + filename);
                  } else {
                      Log.e(TAG, "Can't delete: " + filename);
                  }
              }
          }
  
          // We keep the update logs (beginning with LAST_PREFIX), and optionally
          // the block map file (BLOCK_MAP_FILE) for a package. BLOCK_MAP_FILE
          // will be created at the end of a successful uncrypt. If seeing this
          // file, we keep the block map file and the file that contains the
          // package name (UNCRYPT_PACKAGE_FILE). This is to reduce the work for
          // GmsCore to avoid re-downloading everything again.
          String[] names = RECOVERY_DIR.list();
          for (int i = 0; names != null && i < names.length; i++) {
              // Do not remove the last_install file since the recovery-persist takes care of it.
              if (names[i].startsWith(LAST_PREFIX) || names[i].equals(LAST_INSTALL_PATH)) continue;
              if (reservePackage && names[i].equals(BLOCK_MAP_FILE.getName())) continue;
              if (reservePackage && names[i].equals(UNCRYPT_PACKAGE_FILE.getName())) continue;
  
              recursiveDelete(new File(RECOVERY_DIR, names[i]));
          }
  
          return log;
      }
  
      /**
       * Internally, delete a given file or directory recursively.
       */
      private static void recursiveDelete(File name) {
          if (name.isDirectory()) {
              String[] files = name.list();
              for (int i = 0; files != null && i < files.length; i++) {
                  File f = new File(name, files[i]);
                  recursiveDelete(f);
              }
          }
  
          if (!name.delete()) {
              Log.e(TAG, "Can't delete: " + name);
          } else {
              Log.i(TAG, "Deleted: " + name);
          }
      }
  
      /**
       * Talks to RecoverySystemService via Binder to trigger uncrypt.
       */
      private boolean uncrypt(String packageFile, IRecoverySystemProgressListener listener) {
          try {
              return mService.uncrypt(packageFile, listener);
          } catch (RemoteException unused) {
          }
          return false;
      }
  
      /**
       * Talks to RecoverySystemService via Binder to set up the BCB.
       */
      private boolean setupBcb(String command) {
          try {
              return mService.setupBcb(command);
          } catch (RemoteException unused) {
          }
          return false;
      }
  
      /**
       * Talks to RecoverySystemService via Binder to clear up the BCB.
       */
      private boolean clearBcb() {
          try {
              return mService.clearBcb();
          } catch (RemoteException unused) {
          }
          return false;
      }
  
      /**
       * Talks to RecoverySystemService via Binder to set up the BCB command and
       * reboot into recovery accordingly.
       */
      private void rebootRecoveryWithCommand(String command) {
          try {
              mService.rebootRecoveryWithCommand(command);
          } catch (RemoteException ignored) {
          }
      }
  
      /**
       * Internally, recovery treats each line of the command file as a separate
       * argv, so we only need to protect against newlines and nulls.
       */
      private static String sanitizeArg(String arg) {
          arg = arg.replace('\0', '?');
          arg = arg.replace('\n', '?');
          return arg;
      }
  
  
      /**
       * @removed Was previously made visible by accident.
       */
      public RecoverySystem() {
          mService = null;
      }
  
      /**
       * @hide
       */
      public RecoverySystem(IRecoverySystem service) {
          mService = service;
      }
  }
  