Merge "updater_sample: Improve update completion handling" am: 9455173f87

am: 86f05074d2

Change-Id: Iaefa58f57d11bf8b7091506326b721fe47f00058
This commit is contained in:
Zhomart Mukhamejanov 2018-05-31 17:19:00 -07:00 committed by android-build-merger
commit 3bb7b616a3
3 changed files with 147 additions and 1 deletions

View file

@ -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

View file

@ -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

View file

@ -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();
}