diff options
Diffstat (limited to 'core/java/android/os/RecoverySystem.java')
| -rw-r--r-- | core/java/android/os/RecoverySystem.java | 333 |
1 files changed, 288 insertions, 45 deletions
diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index 154c9bbab312..ddcd63520549 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -16,6 +16,7 @@ package android.os; +import android.annotation.SystemApi; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -66,15 +67,34 @@ public class RecoverySystem { private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500; /** Used to communicate with recovery. See bootable/recovery/recovery.cpp. */ - private static File RECOVERY_DIR = new File("/cache/recovery"); - private static File BLOCK_MAP_FILE = new File(RECOVERY_DIR, "block.map"); - private static File COMMAND_FILE = new File(RECOVERY_DIR, "command"); - private static File UNCRYPT_FILE = new File(RECOVERY_DIR, "uncrypt_file"); - private static File LOG_FILE = new File(RECOVERY_DIR, "log"); - private static String LAST_PREFIX = "last_"; + 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_PREFIX = "last_"; + + /** + * 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"); // Length limits for reading files. - private static int LOG_FILE_MAX_LENGTH = 64 * 1024; + 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 @@ -287,6 +307,89 @@ public class RecoverySystem { } /** + * 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 + 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 + 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. @@ -301,30 +404,127 @@ public class RecoverySystem { * fails, or if the reboot itself fails. */ public static void installPackage(Context context, File packageFile) - throws IOException { - String filename = packageFile.getCanonicalPath(); + throws IOException { + installPackage(context, packageFile, false); + } - FileWriter uncryptFile = new FileWriter(UNCRYPT_FILE); - try { - uncryptFile.write(filename + "\n"); - } finally { - uncryptFile.close(); - } - // UNCRYPT_FILE needs to be readable by system server on bootup. - if (!UNCRYPT_FILE.setReadable(true, false)) { - Log.e(TAG, "Error setting readable for " + UNCRYPT_FILE.getCanonicalPath()); + /** + * 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 + 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 (!processed && filename.startsWith("/data/")) { + 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. + if (filename.startsWith("/data/")) { + filename = "@/cache/recovery/block.map"; + } + + final String filenameArg = "--update_package=" + filename + "\n"; + final String localeArg = "--locale=" + Locale.getDefault().toString() + "\n"; + final String command = filenameArg + localeArg; + + 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); + pm.reboot(PowerManager.REBOOT_RECOVERY_UPDATE); + + throw new IOException("Reboot failed (no permissions?)"); } - Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!"); + } - // If the package is on the /data partition, write the block map file - // into COMMAND_FILE instead. + /** + * 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 + public static void scheduleUpdateOnBoot(Context context, File packageFile) + throws IOException { + String filename = packageFile.getCanonicalPath(); + + // 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; - final String localeArg = "--locale=" + Locale.getDefault().toString(); - bootCommand(context, filenameArg, localeArg); + final String filenameArg = "--update_package=" + filename + "\n"; + final String localeArg = "--locale=" + Locale.getDefault().toString() + "\n"; + final String command = filenameArg + localeArg; + + 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 + 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"); + } } /** @@ -434,27 +634,28 @@ public class RecoverySystem { * @throws IOException if something goes wrong. */ private static void bootCommand(Context context, String... args) throws IOException { - RECOVERY_DIR.mkdirs(); // In case we need it - COMMAND_FILE.delete(); // In case it's not writable - LOG_FILE.delete(); + synchronized (sRequestLock) { + LOG_FILE.delete(); - FileWriter command = new FileWriter(COMMAND_FILE); - try { + StringBuilder command = new StringBuilder(); for (String arg : args) { if (!TextUtils.isEmpty(arg)) { - command.write(arg); - command.write("\n"); + command.append(arg); + command.append("\n"); } } - } finally { - command.close(); - } - // Having written the command file, go ahead and reboot - PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - pm.reboot(PowerManager.REBOOT_RECOVERY); + // Write the command into BCB (bootloader control block). + RecoverySystem rs = (RecoverySystem) context.getSystemService( + Context.RECOVERY_SERVICE); + rs.setupBcb(command.toString()); - throw new IOException("Reboot failed (no permissions?)"); + // Having set up the BCB, go ahead and reboot. + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + pm.reboot(PowerManager.REBOOT_RECOVERY); + + throw new IOException("Reboot failed (no permissions?)"); + } } /** @@ -476,10 +677,10 @@ public class RecoverySystem { // Only remove the OTA package if it's partially processed (uncrypt'd). boolean reservePackage = BLOCK_MAP_FILE.exists(); - if (!reservePackage && UNCRYPT_FILE.exists()) { + if (!reservePackage && UNCRYPT_PACKAGE_FILE.exists()) { String filename = null; try { - filename = FileUtils.readTextFile(UNCRYPT_FILE, 0, null); + filename = FileUtils.readTextFile(UNCRYPT_PACKAGE_FILE, 0, null); } catch (IOException e) { Log.e(TAG, "Error reading uncrypt file", e); } @@ -487,7 +688,7 @@ public class RecoverySystem { // Remove the OTA package on /data that has been (possibly // partially) processed. (Bug: 24973532) if (filename != null && filename.startsWith("/data")) { - if (UNCRYPT_FILE.delete()) { + if (UNCRYPT_PACKAGE_FILE.delete()) { Log.i(TAG, "Deleted: " + filename); } else { Log.e(TAG, "Can't delete: " + filename); @@ -499,13 +700,13 @@ public class RecoverySystem { // 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_FILE). This is to reduce the work for GmsCore - // to avoid re-downloading everything again. + // 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++) { if (names[i].startsWith(LAST_PREFIX)) continue; if (reservePackage && names[i].equals(BLOCK_MAP_FILE.getName())) continue; - if (reservePackage && names[i].equals(UNCRYPT_FILE.getName())) continue; + if (reservePackage && names[i].equals(UNCRYPT_PACKAGE_FILE.getName())) continue; recursiveDelete(new File(RECOVERY_DIR, names[i])); } @@ -533,6 +734,39 @@ public class RecoverySystem { } /** + * 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; + } + + /** * Internally, recovery treats each line of the command file as a separate * argv, so we only need to protect against newlines and nulls. */ @@ -546,5 +780,14 @@ public class RecoverySystem { /** * @removed Was previously made visible by accident. */ - public RecoverySystem() { } + public RecoverySystem() { + mService = null; + } + + /** + * @hide + */ + public RecoverySystem(IRecoverySystem service) { + mService = service; + } } |
