From 7671f68ab858a75f269fbddefb4840b87078a425 Mon Sep 17 00:00:00 2001 From: Zhomart Mukhamejanov Date: Thu, 24 May 2018 09:11:47 -0700 Subject: updater_sample: Improve update completion handling Currently sample app relies on onPayloadApplicationComplete callback. It might not get invoked when app is unbound and update is complete. On the other hand, onStatusUpdate gets invoked always (except when update_engine fails to init). It's good to rely on onStatusUpdate callback to reapply the update if it's IDLE but sample app state is RUNNING. - Add methods to ensure correct updater state. - Update README.md. BUG: 80205922 Test: on the device Change-Id: Ic2f390e85af43556e227362321ab69f0ff146188 Signed-off-by: Zhomart Mukhamejanov --- updater_sample/README.md | 68 +++++++++++++++++++ .../android/systemupdatersample/UpdateManager.java | 76 +++++++++++++++++++++- .../systemupdatersample/ui/MainActivity.java | 4 ++ 3 files changed, 147 insertions(+), 1 deletion(-) diff --git a/updater_sample/README.md b/updater_sample/README.md index 3f211ddba..f6c63a7b6 100644 --- a/updater_sample/README.md +++ b/updater_sample/README.md @@ -65,6 +65,32 @@ purpose only. 6. Push OTA packages to the device. +## Sample App State vs UpdateEngine Status + +UpdateEngine provides status for different stages of update application +process. But it lacks of proper status codes when update fails. + +This creates two problems: + +1. If sample app is unbound from update_engine (MainActivity is paused, destroyed), + app doesn't receive onStatusUpdate and onPayloadApplicationCompleted notifications. + If app binds to update_engine after update is completed, + only onStatusUpdate is called, but status becomes IDLE in most cases. + And there is no way to know if update was successful or not. + +2. This sample app demostrates suspend/resume using update_engins's + `cancel` and `applyPayload` (which picks up from where it left). + When `cancel` is called, status is set to `IDLE`, which doesn't allow + tracking suspended state properly. + +To solve these problems sample app implements its own separate update +state - `UpdaterState`. To solve the first problem, sample app persists +`UpdaterState` on a device. When app is resumed, it checks if `UpdaterState` +matches the update_engine's status (as onStatusUpdate is guaranteed to be called). +If they doesn't match, sample app calls `applyPayload` again with the same +parameters, and handles update completion properly using `onPayloadApplicationCompleted` +callback. The second problem is solved by adding `PAUSED` updater state. + ## Sending HTTP headers from UpdateEngine Sometimes OTA package server might require some HTTP headers to be present, @@ -76,6 +102,44 @@ as of writing this sample app, these headers are `Authorization` and `User-Agent which HTTP headers are supported. +## Used update_engine APIs + +### UpdateEngine#bind + +Binds given callbacks to update_engine. When update_engine successfully +initialized, it's guaranteed to invoke callback onStatusUpdate. + +### UpdateEngine#applyPayload + +Start an update attempt to download an apply the provided `payload_url` if +no other update is running. The extra `key_value_pair_headers` will be +included when fetching the payload. + +### UpdateEngine#cancel + +Cancel the ongoing update. The update could be running or suspended, but it +can't be canceled after it was done. + +### UpdateEngine#resetStatus + +Reset the already applied update back to an idle state. This method can +only be called when no update attempt is going on, and it will reset the +status back to idle, deleting the currently applied update if any. + +### Callback: onStatusUpdate + +Called whenever the value of `status` or `progress` changes. For +`progress` values changes, this method will be called only if it changes significantly. +At this time of writing this doc, delta for `progress` is `0.005`. + +`onStatusUpdate` is always called when app binds to update_engine, +except when update_engine fails to initialize. + +### Callback: onPayloadApplicationComplete + +Called whenever an update attempt is completed. + + ## Development - [x] Create a UI with list of configs, current version, @@ -90,6 +154,10 @@ which HTTP headers are supported. - [x] Add demo for passing HTTP headers to `UpdateEngine#applyPayload` - [x] [Package compatibility check](https://source.android.com/devices/architecture/vintf/match-rules) - [x] Deferred switch slot demo +- [x] Add UpdateManager; extract update logic from MainActivity +- [x] Add Sample app update state (separate from update_engine status) +- [-] Add smart update completion detection using onStatusUpdate +- [ ] Add pause/resume demo - [ ] Add demo for passing NETWORK_ID to `UpdateEngine#applyPayload` - [ ] Verify system partition checksum for package - [?] Add non-A/B updates demo diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java index c4c8c9c27..f5c2ea5ab 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java +++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java @@ -384,11 +384,85 @@ public class UpdateManager { updateEngineApplyPayload(builder.build()); } + /** + * Verifies if mUpdaterState matches mUpdateEngineStatus. + * If they don't match, runs applyPayload to trigger onPayloadApplicationComplete + * callback, which updates mUpdaterState. + */ + private void ensureCorrectUpdaterState() { + // When mUpdaterState is one of IDLE, PAUSED, ERROR, SLOT_SWITCH_REQUIRED + // then mUpdateEngineStatus must be IDLE. + // When mUpdaterState is RUNNING, + // then mUpdateEngineStatus must not be IDLE or UPDATED_NEED_REBOOT. + // When mUpdaterState is REBOOT_REQUIRED, + // then mUpdateEngineStatus must be UPDATED_NEED_REBOOT. + int state = mUpdaterState.get(); + int updateEngineStatus = mUpdateEngineStatus.get(); + if (state == UpdaterState.IDLE + || state == UpdaterState.ERROR + || state == UpdaterState.PAUSED + || state == UpdaterState.SLOT_SWITCH_REQUIRED) { + ensureUpdateEngineStatusIdle(state, updateEngineStatus); + } else if (state == UpdaterState.RUNNING) { + ensureUpdateEngineStatusRunning(state, updateEngineStatus); + } else if (state == UpdaterState.REBOOT_REQUIRED) { + ensureUpdateEngineStatusReboot(state, updateEngineStatus); + } + } + + private void ensureUpdateEngineStatusIdle(int state, int updateEngineStatus) { + if (updateEngineStatus == UpdateEngine.UpdateStatusConstants.IDLE) { + return; + } + // It might happen when update is started not from the sample app. + // To make the sample app simple, we won't handle this case. + throw new RuntimeException("When mUpdaterState is " + state + + " mUpdateEngineStatus expected to be " + + UpdateEngine.UpdateStatusConstants.IDLE + + ", but it is " + updateEngineStatus); + } + + private void ensureUpdateEngineStatusRunning(int state, int updateEngineStatus) { + if (updateEngineStatus != UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT + && updateEngineStatus != UpdateEngine.UpdateStatusConstants.IDLE) { + return; + } + // Re-apply latest update. It makes update_engine to invoke + // onPayloadApplicationComplete callback. The callback notifies + // if update was successful or not. + updateEngineReApplyPayload(); + } + + private void ensureUpdateEngineStatusReboot(int state, int updateEngineStatus) { + if (updateEngineStatus == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT) { + return; + } + // This might happen when update is installed by other means, + // and sample app is not aware of it. To make the sample app simple, + // we won't handle this case. + throw new RuntimeException("When mUpdaterState is " + state + + " mUpdateEngineStatus expected to be " + + UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT + + ", but it is " + updateEngineStatus); + } + + /** + * Invoked by update_engine whenever update status or progress changes. + * It's also guaranteed to be invoked when app binds to the update_engine, except + * when update_engine fails to initialize (as defined in + * system/update_engine/binder_service_android.cc in + * function BinderUpdateEngineAndroidService::bind). + * + * @param status one of {@link UpdateEngine.UpdateStatusConstants}. + * @param progress a number from 0.0 to 1.0. + */ private void onStatusUpdate(int status, float progress) { int previousStatus = mUpdateEngineStatus.get(); mUpdateEngineStatus.set(status); mProgress.set(progress); + ensureCorrectUpdaterState(); + getOnProgressUpdateCallback().ifPresent(callback -> callback.accept(progress)); if (previousStatus != status) { @@ -413,7 +487,7 @@ public class UpdateManager { } /** - * Helper class to delegate {@code update_engine} callbacks to UpdateManager + * Helper class to delegate {@code update_engine} callback invocations to UpdateManager. */ class UpdateEngineCallbackImpl extends UpdateEngineCallback { @Override diff --git a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java index 0b571cc81..1de72c2d6 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java +++ b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java @@ -108,12 +108,16 @@ public class MainActivity extends Activity { @Override protected void onResume() { super.onResume(); + // TODO(zhomart) load saved states + // Binding to UpdateEngine invokes onStatusUpdate callback, + // persisted updater state has to be loaded and prepared beforehand. this.mUpdateManager.bind(); } @Override protected void onPause() { this.mUpdateManager.unbind(); + // TODO(zhomart) save state super.onPause(); } -- cgit v1.2.3