diff --git a/frameworks/base/core/java/android/os/UpdateEngine.java b/frameworks/base/core/java/android/os/UpdateEngine.java new file mode 100644 index 0000000..7f89663 --- /dev/null +++ b/frameworks/base/core/java/android/os/UpdateEngine.java @@ -0,0 +1,702 @@ +/* + * Copyright (C) 2016 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 android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.annotation.WorkerThread; +import android.content.res.AssetFileDescriptor; +import android.os.IUpdateEngine; +import android.os.IUpdateEngineCallback; +import android.os.RemoteException; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * UpdateEngine handles calls to the update engine which takes care of A/B OTA + * updates. It wraps up the update engine Binder APIs and exposes them as + * SystemApis, which will be called by the system app responsible for OTAs. + * On a Google device, this will be GmsCore. + * + * The minimal flow is: + *
    + *
  1. Create a new UpdateEngine instance. + *
  2. Call {@link #bind}, optionally providing callbacks. + *
  3. Call {@link #applyPayload}. + *
+ * + * In addition, methods are provided to {@link #cancel} or + * {@link #suspend}/{@link #resume} application of an update. + * + * The APIs defined in this class and UpdateEngineCallback class must be in + * sync with the ones in + * {@code system/update_engine/binder_bindings/android/os/IUpdateEngine.aidl} + * and + * {@code system/update_engine/binder_bindings/android/os/IUpdateEngineCallback.aidl}. + * + * {@hide} + */ +@SystemApi +public class UpdateEngine { + private static final String TAG = "UpdateEngine"; + + private static final String UPDATE_ENGINE_SERVICE = "android.os.UpdateEngineService"; + + /** + * Error codes from update engine upon finishing a call to + * {@link applyPayload}. Values will be passed via the callback function + * {@link UpdateEngineCallback#onPayloadApplicationComplete}. Values must + * agree with the ones in {@code system/update_engine/common/error_code.h}. + */ + public static final class ErrorCodeConstants { + /** + * Error code: a request finished successfully. + */ + public static final int SUCCESS = 0; + /** + * Error code: a request failed due to a generic error. + */ + public static final int ERROR = 1; + /** + * Error code: an update failed to apply due to filesystem copier + * error. + */ + public static final int FILESYSTEM_COPIER_ERROR = 4; + /** + * Error code: an update failed to apply due to an error in running + * post-install hooks. + */ + public static final int POST_INSTALL_RUNNER_ERROR = 5; + /** + * Error code: an update failed to apply due to a mismatching payload. + * + *

For example, the given payload uses a feature that's not + * supported by the current update engine. + */ + public static final int PAYLOAD_MISMATCHED_TYPE_ERROR = 6; + /** + * Error code: an update failed to apply due to an error in opening + * devices. + */ + public static final int INSTALL_DEVICE_OPEN_ERROR = 7; + /** + * Error code: an update failed to apply due to an error in opening + * kernel device. + */ + public static final int KERNEL_DEVICE_OPEN_ERROR = 8; + /** + * Error code: an update failed to apply due to an error in fetching + * the payload. + * + *

For example, this could be a result of bad network connection + * when streaming an update. + */ + public static final int DOWNLOAD_TRANSFER_ERROR = 9; + /** + * Error code: an update failed to apply due to a mismatch in payload + * hash. + * + *

Update engine does validity checks for the given payload and its + * metadata. + */ + public static final int PAYLOAD_HASH_MISMATCH_ERROR = 10; + + /** + * Error code: an update failed to apply due to a mismatch in payload + * size. + */ + public static final int PAYLOAD_SIZE_MISMATCH_ERROR = 11; + + /** + * Error code: an update failed to apply due to failing to verify + * payload signatures. + */ + public static final int DOWNLOAD_PAYLOAD_VERIFICATION_ERROR = 12; + + /** + * Error code: an update failed to apply due to a downgrade in payload + * timestamp. + * + *

The timestamp of a build is encoded into the payload, which will + * be enforced during install to prevent downgrading a device. + */ + public static final int PAYLOAD_TIMESTAMP_ERROR = 51; + + /** + * Error code: an update has been applied successfully but the new slot + * hasn't been set to active. + * + *

It indicates a successful finish of calling {@link #applyPayload} with + * {@code SWITCH_SLOT_ON_REBOOT=0}. See {@link #applyPayload}. + */ + public static final int UPDATED_BUT_NOT_ACTIVE = 52; + + /** + * Error code: there is not enough space on the device to apply the update. User should + * be prompted to free up space and re-try the update. + * + *

See {@link UpdateEngine#allocateSpace}. + */ + public static final int NOT_ENOUGH_SPACE = 60; + + /** + * Error code: the device is corrupted and no further updates may be applied. + * + *

See {@link UpdateEngine#cleanupAppliedPayload}. + */ + public static final int DEVICE_CORRUPTED = 61; + } + + /** @hide */ + @IntDef(value = { + ErrorCodeConstants.SUCCESS, + ErrorCodeConstants.ERROR, + ErrorCodeConstants.FILESYSTEM_COPIER_ERROR, + ErrorCodeConstants.POST_INSTALL_RUNNER_ERROR, + ErrorCodeConstants.PAYLOAD_MISMATCHED_TYPE_ERROR, + ErrorCodeConstants.INSTALL_DEVICE_OPEN_ERROR, + ErrorCodeConstants.KERNEL_DEVICE_OPEN_ERROR, + ErrorCodeConstants.DOWNLOAD_TRANSFER_ERROR, + ErrorCodeConstants.PAYLOAD_HASH_MISMATCH_ERROR, + ErrorCodeConstants.PAYLOAD_SIZE_MISMATCH_ERROR, + ErrorCodeConstants.DOWNLOAD_PAYLOAD_VERIFICATION_ERROR, + ErrorCodeConstants.PAYLOAD_TIMESTAMP_ERROR, + ErrorCodeConstants.UPDATED_BUT_NOT_ACTIVE, + ErrorCodeConstants.NOT_ENOUGH_SPACE, + ErrorCodeConstants.DEVICE_CORRUPTED, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ErrorCode {} + + /** + * Status codes for update engine. Values must agree with the ones in + * {@code system/update_engine/client_library/include/update_engine/update_status.h}. + */ + public static final class UpdateStatusConstants { + /** + * Update status code: update engine is in idle state. + */ + public static final int IDLE = 0; + + /** + * Update status code: update engine is checking for update. + */ + public static final int CHECKING_FOR_UPDATE = 1; + + /** + * Update status code: an update is available. + */ + public static final int UPDATE_AVAILABLE = 2; + + /** + * Update status code: update engine is downloading an update. + */ + public static final int DOWNLOADING = 3; + + /** + * Update status code: update engine is verifying an update. + */ + public static final int VERIFYING = 4; + + /** + * Update status code: update engine is finalizing an update. + */ + public static final int FINALIZING = 5; + + /** + * Update status code: an update has been applied and is pending for + * reboot. + */ + public static final int UPDATED_NEED_REBOOT = 6; + + /** + * Update status code: update engine is reporting an error event. + */ + public static final int REPORTING_ERROR_EVENT = 7; + + /** + * Update status code: update engine is attempting to rollback an + * update. + */ + public static final int ATTEMPTING_ROLLBACK = 8; + + /** + * Update status code: update engine is in disabled state. + */ + public static final int DISABLED = 9; + } + + private final IUpdateEngine mUpdateEngine; + private IUpdateEngineCallback mUpdateEngineCallback = null; + private final Object mUpdateEngineCallbackLock = new Object(); + + /** + * Creates a new instance. + */ + public UpdateEngine() { + mUpdateEngine = IUpdateEngine.Stub.asInterface( + ServiceManager.getService(UPDATE_ENGINE_SERVICE)); + if (mUpdateEngine == null) { + throw new IllegalStateException("Failed to find update_engine"); + } + } + + /** + * Prepares this instance for use. The callback will be notified on any + * status change, and when the update completes. A handler can be supplied + * to control which thread runs the callback, or null. + */ + public boolean bind(final UpdateEngineCallback callback, final Handler handler) { + synchronized (mUpdateEngineCallbackLock) { + mUpdateEngineCallback = new IUpdateEngineCallback.Stub() { + @Override + public void onStatusUpdate(final int status, final float percent) { + if (handler != null) { + handler.post(new Runnable() { + @Override + public void run() { + callback.onStatusUpdate(status, percent); + } + }); + } else { + callback.onStatusUpdate(status, percent); + } + } + + @Override + public void onPayloadApplicationComplete(final int errorCode) { + if (handler != null) { + handler.post(new Runnable() { + @Override + public void run() { + callback.onPayloadApplicationComplete(errorCode); + } + }); + } else { + callback.onPayloadApplicationComplete(errorCode); + } + } + }; + + try { + return mUpdateEngine.bind(mUpdateEngineCallback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Equivalent to {@code bind(callback, null)}. + */ + public boolean bind(final UpdateEngineCallback callback) { + return bind(callback, null); + } + + /** + * Applies the payload found at the given {@code url}. For non-streaming + * updates, the URL can be a local file using the {@code file://} scheme. + * + *

The {@code offset} and {@code size} parameters specify the location + * of the payload within the file represented by the URL. This is useful + * if the downloadable package at the URL contains more than just the + * update_engine payload (such as extra metadata). This is true for + * Google's OTA system, where the URL points to a zip file in which the + * payload is stored uncompressed within the zip file alongside other + * data. + * + *

The {@code headerKeyValuePairs} parameter is used to pass metadata + * to update_engine. In Google's implementation, this is stored as + * {@code payload_properties.txt} in the zip file. It's generated by the + * script {@code system/update_engine/scripts/brillo_update_payload}. + * The complete list of keys and their documentation is in + * {@code system/update_engine/common/constants.cc}, but an example + * might be: + *

+     * String[] pairs = {
+     *   "FILE_HASH=lURPCIkIAjtMOyB/EjQcl8zDzqtD6Ta3tJef6G/+z2k=",
+     *   "FILE_SIZE=871903868",
+     *   "METADATA_HASH=tBvj43QOB0Jn++JojcpVdbRLz0qdAuL+uTkSy7hokaw=",
+     *   "METADATA_SIZE=70604"
+     * };
+     * 
+ * + *

The callback functions registered via {@code #bind} will be called + * during and at the end of the payload application. + * + *

By default the newly updated slot will be set active upon + * successfully finishing an update. Device will attempt to boot into the + * new slot on next reboot. This behavior can be customized by specifying + * {@code SWITCH_SLOT_ON_REBOOT=0} in {@code headerKeyValuePairs}, which + * allows the caller to later determine a good time to boot into the new + * slot. Calling {@code applyPayload} again with the same payload but with + * {@code SWITCH_SLOT_ON_REBOOT=1} will do the minimal work to set the new + * slot active, after verifying its integrity. + */ + public void applyPayload(String url, long offset, long size, String[] headerKeyValuePairs) { + try { + mUpdateEngine.applyPayload(url, offset, size, headerKeyValuePairs); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Applies the payload passed as AssetFileDescriptor {@code assetFd} + * instead of using the {@code file://} scheme. + * + *

See {@link #applyPayload(String)} for {@code offset}, {@code size} and + * {@code headerKeyValuePairs} parameters. + */ + public void applyPayload(@NonNull AssetFileDescriptor assetFd, + @NonNull String[] headerKeyValuePairs) { + try { + mUpdateEngine.applyPayloadFd(assetFd.getParcelFileDescriptor(), + assetFd.getStartOffset(), assetFd.getLength(), headerKeyValuePairs); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Permanently cancels an in-progress update. + * + *

See {@link #resetStatus} to undo a finshed update (only available + * before the updated system has been rebooted). + * + *

See {@link #suspend} for a way to temporarily stop an in-progress + * update with the ability to resume it later. + */ + public void cancel() { + try { + mUpdateEngine.cancel(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Suspends an in-progress update. This can be undone by calling + * {@link #resume}. + */ + public void suspend() { + try { + mUpdateEngine.suspend(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Resumes a suspended update. + */ + public void resume() { + try { + mUpdateEngine.resume(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Resets the bootable flag on the non-current partition and all internal + * update_engine state. Note this call will clear the entire update + * progress. So a subsequent {@link #applyPayload} will apply the update + * from scratch. + * + *

After this call completes, update_engine will no longer report + * {@code UPDATED_NEED_REBOOT}, so your callback can remove any outstanding + * notification that rebooting into the new system is possible. + */ + public void resetStatus() { + try { + mUpdateEngine.resetStatus(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Sets the A/B slot switch for the next boot after applying an ota update. If + * {@link #applyPayload} hasn't switched the slot, the updater APP can call + * this API to switch the slot and apply the update on next boot. + * + * @param payloadMetadataFilename the location of the metadata without the + * {@code file://} prefix. + */ + public void setShouldSwitchSlotOnReboot(@NonNull String payloadMetadataFilename) { + try { + mUpdateEngine.setShouldSwitchSlotOnReboot(payloadMetadataFilename); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Resets the boot slot to the source/current slot, without cancelling the + * update progress. This can be called after the update is installed, and to + * prevent the device from accidentally taking the update when it reboots. + * + * This is useful when users don't want to take the update immediately; or + * the updater determines some condition hasn't met, e.g. insufficient space + * for boot. + */ + public void resetShouldSwitchSlotOnReboot() { + try { + mUpdateEngine.resetShouldSwitchSlotOnReboot(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public void setPerformanceMode(boolean enable) { + // No-op implementation for AOSP compatibility + /*try { + mUpdateEngine.setPerformanceMode(enable); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + }*/ + } + + /** + * Unbinds the last bound callback function. + */ + public boolean unbind() { + synchronized (mUpdateEngineCallbackLock) { + if (mUpdateEngineCallback == null) { + return true; + } + try { + boolean result = mUpdateEngine.unbind(mUpdateEngineCallback); + mUpdateEngineCallback = null; + return result; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Verifies that a payload associated with the given payload metadata + * {@code payloadMetadataFilename} can be safely applied to ths device. + * Returns {@code true} if the update can successfully be applied and + * returns {@code false} otherwise. + * + * @param payloadMetadataFilename the location of the metadata without the + * {@code file://} prefix. + */ + public boolean verifyPayloadMetadata(String payloadMetadataFilename) { + try { + return mUpdateEngine.verifyPayloadApplicable(payloadMetadataFilename); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return value of {@link #allocateSpace.} + */ + public static final class AllocateSpaceResult { + private @ErrorCode int mErrorCode = ErrorCodeConstants.SUCCESS; + private long mFreeSpaceRequired = 0; + private AllocateSpaceResult() {} + /** + * Error code. + * + * @return The following error codes: + *

+ */ + @ErrorCode + public int getErrorCode() { + return mErrorCode; + } + + /** + * Estimated total space that needs to be available on the userdata partition to apply the + * payload (in bytes). + * + *

+ * Note that in practice, more space needs to be made available before applying the payload + * to keep the device working. + * + * @return The following values: + *

+ * @throws IllegalStateException if {@link #getErrorCode} is not one of the above. + * + */ + public long getFreeSpaceRequired() { + if (mErrorCode == ErrorCodeConstants.SUCCESS) { + return 0; + } + if (mErrorCode == ErrorCodeConstants.NOT_ENOUGH_SPACE) { + return mFreeSpaceRequired; + } + throw new IllegalStateException(String.format( + "getFreeSpaceRequired() is not available when error code is %d", mErrorCode)); + } + } + + /** + * Initialize partitions for a payload associated with the given payload + * metadata {@code payloadMetadataFilename} by preallocating required space. + * + *

This function should be called after payload has been verified after + * {@link #verifyPayloadMetadata}. This function does not verify whether + * the given payload is applicable or not. + * + *

Implementation of {@code allocateSpace} uses + * {@code headerKeyValuePairs} to determine whether space has been allocated + * for a different or same payload previously. If space has been allocated + * for a different payload before, space will be reallocated for the given + * payload. If space has been allocated for the same payload, no actions to + * storage devices are taken. + * + *

This function is synchronous and may take a non-trivial amount of + * time. Callers should call this function in a background thread. + * + * @param payloadMetadataFilename See {@link #verifyPayloadMetadata}. + * @param headerKeyValuePairs See {@link #applyPayload}. + * @return See {@link AllocateSpaceResult#getErrorCode} and + * {@link AllocateSpaceResult#getFreeSpaceRequired}. + */ + @WorkerThread + @NonNull + public AllocateSpaceResult allocateSpace( + @NonNull String payloadMetadataFilename, + @NonNull String[] headerKeyValuePairs) { + AllocateSpaceResult result = new AllocateSpaceResult(); + try { + result.mFreeSpaceRequired = mUpdateEngine.allocateSpaceForPayload( + payloadMetadataFilename, + headerKeyValuePairs); + result.mErrorCode = result.mFreeSpaceRequired == 0 + ? ErrorCodeConstants.SUCCESS + : ErrorCodeConstants.NOT_ENOUGH_SPACE; + return result; + } catch (ServiceSpecificException e) { + result.mErrorCode = e.errorCode; + result.mFreeSpaceRequired = 0; + return result; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private static class CleanupAppliedPayloadCallback extends IUpdateEngineCallback.Stub { + private int mErrorCode = ErrorCodeConstants.ERROR; + private boolean mCompleted = false; + private Object mLock = new Object(); + private int getResult() { + synchronized (mLock) { + while (!mCompleted) { + try { + mLock.wait(); + } catch (InterruptedException ex) { + // do nothing, just wait again. + } + } + return mErrorCode; + } + } + + @Override + public void onStatusUpdate(int status, float percent) { + } + + @Override + public void onPayloadApplicationComplete(int errorCode) { + synchronized (mLock) { + mErrorCode = errorCode; + mCompleted = true; + mLock.notifyAll(); + } + } + } + + /** + * Cleanup files used by the previous update and free up space after the + * device has been booted successfully into the new build. + * + *

In particular, this function waits until delta files for snapshots for + * Virtual A/B update are merged to OS partitions, then delete these delta + * files. + * + *

This function is synchronous and may take a non-trivial amount of + * time. Callers should call this function in a background thread. + * + *

This function does not delete payload binaries downloaded for a + * non-streaming OTA update. + * + * @return One of the following: + *

+ */ + @WorkerThread + @ErrorCode + public int cleanupAppliedPayload() { + CleanupAppliedPayloadCallback callback = new CleanupAppliedPayloadCallback(); + try { + mUpdateEngine.cleanupSuccessfulUpdate(callback); + return callback.getResult(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Run postinstall script for specified partition |partition| + * + * @param partition The partition to trigger postinstall runs + * + * @throws ServiceSpecificException error code of this exception would be one of + * https://cs.android.com/android/platform/superproject/main/+/main:system/update_engine/common/error_code.h + * @hide + */ + @FlaggedApi(Flags.FLAG_UPDATE_ENGINE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public void triggerPostinstall(@NonNull String partition) { + try { + mUpdateEngine.triggerPostinstall(partition); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} \ No newline at end of file