Merge "updater_sample: improve updater state handling"

This commit is contained in:
Zhomart Mukhamejanov
2018-06-04 23:18:43 +00:00
committed by Gerrit Code Review
8 changed files with 307 additions and 167 deletions
@@ -64,8 +64,8 @@ public class UpdateManager {
private AtomicBoolean mManualSwitchSlotRequired = new AtomicBoolean(true); private AtomicBoolean mManualSwitchSlotRequired = new AtomicBoolean(true);
/** Validate state only once when app binds to UpdateEngine. */ /** Synchronize state with engine status only once when app binds to UpdateEngine. */
private AtomicBoolean mStateValidityEnsured = new AtomicBoolean(false); private AtomicBoolean mStateSynchronized = new AtomicBoolean(false);
@GuardedBy("mLock") @GuardedBy("mLock")
private UpdateData mLastUpdateData = null; private UpdateData mLastUpdateData = null;
@@ -90,10 +90,12 @@ public class UpdateManager {
} }
/** /**
* Binds to {@link UpdateEngine}. * Binds to {@link UpdateEngine}. Invokes onStateChangeCallback if present.
*/ */
public void bind() { public void bind() {
mStateValidityEnsured.set(false); getOnStateChangeCallback().ifPresent(callback -> callback.accept(mUpdaterState.get()));
mStateSynchronized.set(false);
this.mUpdateEngine.bind(mUpdateEngineCallback); this.mUpdateEngine.bind(mUpdateEngineCallback);
} }
@@ -104,11 +106,8 @@ public class UpdateManager {
this.mUpdateEngine.unbind(); this.mUpdateEngine.unbind();
} }
/** public int getUpdaterState() {
* @return a number from {@code 0.0} to {@code 1.0}. return mUpdaterState.get();
*/
public float getProgress() {
return (float) this.mProgress.get();
} }
/** /**
@@ -202,20 +201,39 @@ public class UpdateManager {
* Updates {@link this.mState} and if state is changed, * Updates {@link this.mState} and if state is changed,
* it also notifies {@link this.mOnStateChangeCallback}. * it also notifies {@link this.mOnStateChangeCallback}.
*/ */
private void setUpdaterState(int updaterState) { private void setUpdaterState(int newUpdaterState)
throws UpdaterState.InvalidTransitionException {
Log.d(TAG, "setUpdaterState invoked newState=" + newUpdaterState);
int previousState = mUpdaterState.get(); int previousState = mUpdaterState.get();
mUpdaterState.set(newUpdaterState);
if (previousState != newUpdaterState) {
getOnStateChangeCallback().ifPresent(callback -> callback.accept(newUpdaterState));
}
}
/**
* Same as {@link this.setUpdaterState}. Logs the error if new state
* cannot be set.
*/
private void setUpdaterStateSilent(int newUpdaterState) {
try { try {
mUpdaterState.set(updaterState); setUpdaterState(newUpdaterState);
} catch (UpdaterState.InvalidTransitionException e) { } catch (UpdaterState.InvalidTransitionException e) {
// Note: invalid state transitions should be handled properly, // Most likely UpdateEngine status and UpdaterSample state got de-synchronized.
// but to make sample app simple, we just throw runtime exception. // To make sample app simple, we don't handle it properly.
throw new RuntimeException("Can't set state " + updaterState, e); Log.e(TAG, "Failed to set updater state", e);
}
if (previousState != updaterState) {
getOnStateChangeCallback().ifPresent(callback -> callback.accept(updaterState));
} }
} }
/**
* Creates new UpdaterState, assigns it to {@link this.mUpdaterState},
* and notifies callbacks.
*/
private void initializeUpdateState(int state) {
this.mUpdaterState = new UpdaterState(state);
getOnStateChangeCallback().ifPresent(callback -> callback.accept(state));
}
/** /**
* Requests update engine to stop any ongoing update. If an update has been applied, * Requests update engine to stop any ongoing update. If an update has been applied,
* leave it as is. * leave it as is.
@@ -224,13 +242,10 @@ public class UpdateManager {
* update engine would throw an error when the method is called, and the only way to * update engine would throw an error when the method is called, and the only way to
* handle it is to catch the exception.</p> * handle it is to catch the exception.</p>
*/ */
public void cancelRunningUpdate() { public synchronized void cancelRunningUpdate() throws UpdaterState.InvalidTransitionException {
try { Log.d(TAG, "cancelRunningUpdate invoked");
mUpdateEngine.cancel(); setUpdaterState(UpdaterState.IDLE);
setUpdaterState(UpdaterState.IDLE); mUpdateEngine.cancel();
} catch (Exception e) {
Log.w(TAG, "UpdateEngine failed to stop the ongoing update", e);
}
} }
/** /**
@@ -240,13 +255,10 @@ public class UpdateManager {
* update engine would throw an error when the method is called, and the only way to * update engine would throw an error when the method is called, and the only way to
* handle it is to catch the exception.</p> * handle it is to catch the exception.</p>
*/ */
public void resetUpdate() { public synchronized void resetUpdate() throws UpdaterState.InvalidTransitionException {
try { Log.d(TAG, "resetUpdate invoked");
mUpdateEngine.resetStatus(); setUpdaterState(UpdaterState.IDLE);
setUpdaterState(UpdaterState.IDLE); mUpdateEngine.resetStatus();
} catch (Exception e) {
Log.w(TAG, "UpdateEngine failed to reset the update", e);
}
} }
/** /**
@@ -255,7 +267,8 @@ public class UpdateManager {
* <p>UpdateEngine works asynchronously. This method doesn't wait until * <p>UpdateEngine works asynchronously. This method doesn't wait until
* end of the update.</p> * end of the update.</p>
*/ */
public void applyUpdate(Context context, UpdateConfig config) { public synchronized void applyUpdate(Context context, UpdateConfig config)
throws UpdaterState.InvalidTransitionException {
mEngineErrorCode.set(UpdateEngineErrorCodes.UNKNOWN); mEngineErrorCode.set(UpdateEngineErrorCodes.UNKNOWN);
setUpdaterState(UpdaterState.RUNNING); setUpdaterState(UpdaterState.RUNNING);
@@ -277,7 +290,8 @@ public class UpdateManager {
} }
} }
private void applyAbNonStreamingUpdate(UpdateConfig config) { private void applyAbNonStreamingUpdate(UpdateConfig config)
throws UpdaterState.InvalidTransitionException {
UpdateData.Builder builder = UpdateData.builder() UpdateData.Builder builder = UpdateData.builder()
.setExtraProperties(prepareExtraProperties(config)); .setExtraProperties(prepareExtraProperties(config));
@@ -306,7 +320,7 @@ public class UpdateManager {
updateEngineApplyPayload(builder.build()); updateEngineApplyPayload(builder.build());
} else { } else {
Log.e(TAG, "PrepareStreamingService failed, result code is " + code); Log.e(TAG, "PrepareStreamingService failed, result code is " + code);
setUpdaterState(UpdaterState.ERROR); setUpdaterStateSilent(UpdaterState.ERROR);
} }
}); });
} }
@@ -333,6 +347,8 @@ public class UpdateManager {
* with the given id.</p> * with the given id.</p>
*/ */
private void updateEngineApplyPayload(UpdateData update) { private void updateEngineApplyPayload(UpdateData update) {
Log.d(TAG, "updateEngineApplyPayload invoked with url " + update.mPayload.getUrl());
synchronized (mLock) { synchronized (mLock) {
mLastUpdateData = update; mLastUpdateData = update;
} }
@@ -348,11 +364,15 @@ public class UpdateManager {
properties.toArray(new String[0])); properties.toArray(new String[0]));
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "UpdateEngine failed to apply the update", e); Log.e(TAG, "UpdateEngine failed to apply the update", e);
setUpdaterState(UpdaterState.ERROR); setUpdaterStateSilent(UpdaterState.ERROR);
} }
} }
/**
* Re-applies {@link this.mLastUpdateData} to update_engine.
*/
private void updateEngineReApplyPayload() { private void updateEngineReApplyPayload() {
Log.d(TAG, "updateEngineReApplyPayload invoked");
UpdateData lastUpdate; UpdateData lastUpdate;
synchronized (mLock) { synchronized (mLock) {
// mLastPayloadSpec might be empty in some cases. // mLastPayloadSpec might be empty in some cases.
@@ -377,8 +397,14 @@ public class UpdateManager {
* {@link UpdateEngine#applyPayload} might take several seconds to finish, and it will * {@link UpdateEngine#applyPayload} might take several seconds to finish, and it will
* invoke callbacks {@link this#onStatusUpdate} and {@link this#onPayloadApplicationComplete)}. * invoke callbacks {@link this#onStatusUpdate} and {@link this#onPayloadApplicationComplete)}.
*/ */
public void setSwitchSlotOnReboot() { public synchronized void setSwitchSlotOnReboot() {
Log.d(TAG, "setSwitchSlotOnReboot invoked"); Log.d(TAG, "setSwitchSlotOnReboot invoked");
// When mManualSwitchSlotRequired set false, next time
// onApplicationPayloadComplete is called,
// it will set updater state to REBOOT_REQUIRED.
mManualSwitchSlotRequired.set(false);
UpdateData.Builder builder; UpdateData.Builder builder;
synchronized (mLock) { synchronized (mLock) {
// To make sample app simple, we don't handle it. // To make sample app simple, we don't handle it.
@@ -396,65 +422,62 @@ public class UpdateManager {
} }
/** /**
* Verifies if mUpdaterState matches mUpdateEngineStatus. * Synchronize UpdaterState with UpdateEngine status.
* If they don't match, runs applyPayload to trigger onPayloadApplicationComplete * Apply necessary UpdateEngine operation if status are out of sync.
* callback, which updates mUpdaterState. *
* It's expected to be called once when sample app binds itself to UpdateEngine.
*/ */
private void ensureCorrectUpdaterState() { private void synchronizeUpdaterStateWithUpdateEngineStatus() {
// When mUpdaterState is one of IDLE, PAUSED, ERROR, SLOT_SWITCH_REQUIRED Log.d(TAG, "synchronizeUpdaterStateWithUpdateEngineStatus is invoked.");
// 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 state = mUpdaterState.get();
int updateEngineStatus = mUpdateEngineStatus.get(); int engineStatus = 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 (engineStatus == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT) {
if (updateEngineStatus == UpdateEngine.UpdateStatusConstants.IDLE) { // If update has been installed before running the sample app,
// set state to REBOOT_REQUIRED.
initializeUpdateState(UpdaterState.REBOOT_REQUIRED);
return; 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) { switch (state) {
if (updateEngineStatus != UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT case UpdaterState.IDLE:
&& updateEngineStatus != UpdateEngine.UpdateStatusConstants.IDLE) { case UpdaterState.ERROR:
return; case UpdaterState.PAUSED:
case UpdaterState.SLOT_SWITCH_REQUIRED:
// It might happen when update is started not from the sample app.
// To make the sample app simple, we won't handle this case.
Preconditions.checkState(
engineStatus == UpdateEngine.UpdateStatusConstants.IDLE,
"When mUpdaterState is %s, mUpdateEngineStatus "
+ "must be 0/IDLE, but it is %s",
state,
engineStatus);
break;
case UpdaterState.RUNNING:
if (engineStatus == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT
|| engineStatus == UpdateEngine.UpdateStatusConstants.IDLE) {
Log.i(TAG, "ensureUpdateEngineStatusIsRunning - re-applying last payload");
// Re-apply latest update. It makes update_engine to invoke
// onPayloadApplicationComplete callback. The callback notifies
// if update was successful or not.
updateEngineReApplyPayload();
}
break;
case UpdaterState.REBOOT_REQUIRED:
// 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.
Preconditions.checkState(
engineStatus == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT,
"When mUpdaterState is %s, mUpdateEngineStatus "
+ "must be 6/UPDATED_NEED_REBOOT, but it is %s",
state,
engineStatus);
break;
default:
throw new IllegalStateException("This block should not be reached.");
} }
// 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);
} }
/** /**
@@ -468,13 +491,19 @@ public class UpdateManager {
* @param progress a number from 0.0 to 1.0. * @param progress a number from 0.0 to 1.0.
*/ */
private void onStatusUpdate(int status, float progress) { private void onStatusUpdate(int status, float progress) {
Log.d(TAG, String.format(
"onStatusUpdate invoked, status=%s, progress=%.2f",
status,
progress));
int previousStatus = mUpdateEngineStatus.get(); int previousStatus = mUpdateEngineStatus.get();
mUpdateEngineStatus.set(status); mUpdateEngineStatus.set(status);
mProgress.set(progress); mProgress.set(progress);
if (!mStateValidityEnsured.getAndSet(true)) { if (!mStateSynchronized.getAndSet(true)) {
// We ensure correct state once only when sample app is bound to UpdateEngine. // We synchronize state with engine status once
ensureCorrectUpdaterState(); // only when sample app is bound to UpdateEngine.
synchronizeUpdaterStateWithUpdateEngineStatus();
} }
getOnProgressUpdateCallback().ifPresent(callback -> callback.accept(progress)); getOnProgressUpdateCallback().ifPresent(callback -> callback.accept(progress));
@@ -489,11 +518,11 @@ public class UpdateManager {
mEngineErrorCode.set(errorCode); mEngineErrorCode.set(errorCode);
if (errorCode == UpdateEngine.ErrorCodeConstants.SUCCESS if (errorCode == UpdateEngine.ErrorCodeConstants.SUCCESS
|| errorCode == UpdateEngineErrorCodes.UPDATED_BUT_NOT_ACTIVE) { || errorCode == UpdateEngineErrorCodes.UPDATED_BUT_NOT_ACTIVE) {
setUpdaterState(isManualSwitchSlotRequired() setUpdaterStateSilent(isManualSwitchSlotRequired()
? UpdaterState.SLOT_SWITCH_REQUIRED ? UpdaterState.SLOT_SWITCH_REQUIRED
: UpdaterState.REBOOT_REQUIRED); : UpdaterState.REBOOT_REQUIRED);
} else if (errorCode != UpdateEngineErrorCodes.USER_CANCELLED) { } else if (errorCode != UpdateEngineErrorCodes.USER_CANCELLED) {
setUpdaterState(UpdaterState.ERROR); setUpdaterStateSilent(UpdaterState.ERROR);
} }
getOnEngineCompleteCallback() getOnEngineCompleteCallback()
@@ -51,12 +51,15 @@ public class UpdaterState {
* are allowed to transition to from key. * are allowed to transition to from key.
*/ */
private static final ImmutableMap<Integer, ImmutableSet<Integer>> TRANSITIONS = private static final ImmutableMap<Integer, ImmutableSet<Integer>> TRANSITIONS =
ImmutableMap.of( ImmutableMap.<Integer, ImmutableSet<Integer>>builder()
IDLE, ImmutableSet.of(RUNNING), .put(IDLE, ImmutableSet.of(ERROR, RUNNING))
RUNNING, ImmutableSet.of(ERROR, PAUSED, REBOOT_REQUIRED, SLOT_SWITCH_REQUIRED), .put(RUNNING, ImmutableSet.of(
PAUSED, ImmutableSet.of(RUNNING), ERROR, PAUSED, REBOOT_REQUIRED, SLOT_SWITCH_REQUIRED))
SLOT_SWITCH_REQUIRED, ImmutableSet.of(ERROR) .put(PAUSED, ImmutableSet.of(ERROR, RUNNING, IDLE))
); .put(SLOT_SWITCH_REQUIRED, ImmutableSet.of(ERROR, IDLE))
.put(ERROR, ImmutableSet.of(IDLE))
.put(REBOOT_REQUIRED, ImmutableSet.of(IDLE))
.build();
private AtomicInteger mState; private AtomicInteger mState;
@@ -88,7 +88,7 @@ public class MainActivity extends Activity {
this.mTextViewConfigsDirHint.setText(UpdateConfigs.getConfigsRoot(this)); this.mTextViewConfigsDirHint.setText(UpdateConfigs.getConfigsRoot(this));
uiReset(); uiResetWidgets();
loadUpdateConfigs(); loadUpdateConfigs();
this.mUpdateManager.setOnStateChangeCallback(this::onUpdaterStateChange); this.mUpdateManager.setOnStateChangeCallback(this::onUpdaterStateChange);
@@ -108,7 +108,6 @@ public class MainActivity extends Activity {
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
// TODO(zhomart) load saved states
// Binding to UpdateEngine invokes onStatusUpdate callback, // Binding to UpdateEngine invokes onStatusUpdate callback,
// persisted updater state has to be loaded and prepared beforehand. // persisted updater state has to be loaded and prepared beforehand.
this.mUpdateManager.bind(); this.mUpdateManager.bind();
@@ -117,7 +116,6 @@ public class MainActivity extends Activity {
@Override @Override
protected void onPause() { protected void onPause() {
this.mUpdateManager.unbind(); this.mUpdateManager.unbind();
// TODO(zhomart) save state
super.onPause(); super.onPause();
} }
@@ -149,14 +147,22 @@ public class MainActivity extends Activity {
.setMessage("Do you really want to apply this update?") .setMessage("Do you really want to apply this update?")
.setIcon(android.R.drawable.ic_dialog_alert) .setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
uiSetUpdating(); uiResetWidgets();
uiResetEngineText(); uiResetEngineText();
mUpdateManager.applyUpdate(this, getSelectedConfig()); applyUpdate(getSelectedConfig());
}) })
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show(); .show();
} }
private void applyUpdate(UpdateConfig config) {
try {
mUpdateManager.applyUpdate(this, config);
} catch (UpdaterState.InvalidTransitionException e) {
Log.e(TAG, "Failed to apply update " + config.getName(), e);
}
}
/** /**
* stop button clicked * stop button clicked
*/ */
@@ -166,11 +172,19 @@ public class MainActivity extends Activity {
.setMessage("Do you really want to cancel running update?") .setMessage("Do you really want to cancel running update?")
.setIcon(android.R.drawable.ic_dialog_alert) .setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
mUpdateManager.cancelRunningUpdate(); cancelRunningUpdate();
}) })
.setNegativeButton(android.R.string.cancel, null).show(); .setNegativeButton(android.R.string.cancel, null).show();
} }
private void cancelRunningUpdate() {
try {
mUpdateManager.cancelRunningUpdate();
} catch (UpdaterState.InvalidTransitionException e) {
Log.e(TAG, "Failed to cancel running update", e);
}
}
/** /**
* reset button clicked * reset button clicked
*/ */
@@ -181,11 +195,19 @@ public class MainActivity extends Activity {
+ " and restore old version?") + " and restore old version?")
.setIcon(android.R.drawable.ic_dialog_alert) .setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
mUpdateManager.resetUpdate(); resetUpdate();
}) })
.setNegativeButton(android.R.string.cancel, null).show(); .setNegativeButton(android.R.string.cancel, null).show();
} }
private void resetUpdate() {
try {
mUpdateManager.resetUpdate();
} catch (UpdaterState.InvalidTransitionException e) {
Log.e(TAG, "Failed to reset update", e);
}
}
/** /**
* switch slot button clicked * switch slot button clicked
*/ */
@@ -199,9 +221,25 @@ public class MainActivity extends Activity {
* values from {@link UpdaterState}. * values from {@link UpdaterState}.
*/ */
private void onUpdaterStateChange(int state) { private void onUpdaterStateChange(int state) {
Log.i(TAG, "onUpdaterStateChange invoked state=" + state); Log.i(TAG, "UpdaterStateChange state="
+ UpdaterState.getStateText(state)
+ "/" + state);
runOnUiThread(() -> { runOnUiThread(() -> {
setUiUpdaterState(state); setUiUpdaterState(state);
if (state == UpdaterState.IDLE) {
uiStateIdle();
} else if (state == UpdaterState.RUNNING) {
uiStateRunning();
} else if (state == UpdaterState.PAUSED) {
uiStatePaused();
} else if (state == UpdaterState.ERROR) {
uiStateError();
} else if (state == UpdaterState.SLOT_SWITCH_REQUIRED) {
uiStateSlotSwitchRequired();
} else if (state == UpdaterState.REBOOT_REQUIRED) {
uiStateRebootRequired();
}
}); });
} }
@@ -210,17 +248,10 @@ public class MainActivity extends Activity {
* be one of the values from {@link UpdateEngine.UpdateStatusConstants}. * be one of the values from {@link UpdateEngine.UpdateStatusConstants}.
*/ */
private void onEngineStatusUpdate(int status) { private void onEngineStatusUpdate(int status) {
Log.i(TAG, "StatusUpdate - status="
+ UpdateEngineStatuses.getStatusText(status)
+ "/" + status);
runOnUiThread(() -> { runOnUiThread(() -> {
Log.e(TAG, "StatusUpdate - status="
+ UpdateEngineStatuses.getStatusText(status)
+ "/" + status);
if (status == UpdateEngine.UpdateStatusConstants.IDLE) {
Log.d(TAG, "status changed, resetting ui");
uiReset();
} else {
Log.d(TAG, "status changed, setting ui to updating mode");
uiSetUpdating();
}
setUiEngineStatus(status); setUiEngineStatus(status);
}); });
} }
@@ -234,19 +265,12 @@ public class MainActivity extends Activity {
final String completionState = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode) final String completionState = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode)
? "SUCCESS" ? "SUCCESS"
: "FAILURE"; : "FAILURE";
Log.i(TAG,
"PayloadApplicationCompleted - errorCode="
+ UpdateEngineErrorCodes.getCodeName(errorCode) + "/" + errorCode
+ " " + completionState);
runOnUiThread(() -> { runOnUiThread(() -> {
Log.i(TAG,
"Completed - errorCode="
+ UpdateEngineErrorCodes.getCodeName(errorCode) + "/" + errorCode
+ " " + completionState);
setUiEngineErrorCode(errorCode); setUiEngineErrorCode(errorCode);
if (errorCode == UpdateEngineErrorCodes.UPDATED_BUT_NOT_ACTIVE) {
// if update was successfully applied.
if (mUpdateManager.isManualSwitchSlotRequired()) {
// Show "Switch Slot" button.
uiShowSwitchSlotInfo();
}
}
}); });
} }
@@ -258,45 +282,66 @@ public class MainActivity extends Activity {
} }
/** resets ui */ /** resets ui */
private void uiReset() { private void uiResetWidgets() {
mTextViewBuild.setText(Build.DISPLAY); mTextViewBuild.setText(Build.DISPLAY);
mSpinnerConfigs.setEnabled(true); mSpinnerConfigs.setEnabled(false);
mButtonReload.setEnabled(true); mButtonReload.setEnabled(false);
mButtonApplyConfig.setEnabled(true); mButtonApplyConfig.setEnabled(false);
mButtonStop.setEnabled(false); mButtonStop.setEnabled(false);
mButtonReset.setEnabled(false); mButtonReset.setEnabled(false);
mProgressBar.setProgress(0);
mProgressBar.setEnabled(false); mProgressBar.setEnabled(false);
mProgressBar.setVisibility(ProgressBar.INVISIBLE); mProgressBar.setVisibility(ProgressBar.INVISIBLE);
uiHideSwitchSlotInfo(); mButtonSwitchSlot.setEnabled(false);
mTextViewUpdateInfo.setTextColor(Color.parseColor("#aaaaaa"));
} }
private void uiResetEngineText() { private void uiResetEngineText() {
mTextViewEngineStatus.setText(R.string.unknown); mTextViewEngineStatus.setText(R.string.unknown);
mTextViewEngineErrorCode.setText(R.string.unknown); mTextViewEngineErrorCode.setText(R.string.unknown);
// Note: Do not reset mTextViewUpdaterState; UpdateManager notifies properly. // Note: Do not reset mTextViewUpdaterState; UpdateManager notifies updater state properly.
} }
/** sets ui updating mode */ private void uiStateIdle() {
private void uiSetUpdating() { uiResetWidgets();
mTextViewBuild.setText(Build.DISPLAY); mSpinnerConfigs.setEnabled(true);
mSpinnerConfigs.setEnabled(false); mButtonReload.setEnabled(true);
mButtonReload.setEnabled(false); mButtonApplyConfig.setEnabled(true);
mButtonApplyConfig.setEnabled(false); mProgressBar.setProgress(0);
mButtonStop.setEnabled(true); }
private void uiStateRunning() {
uiResetWidgets();
mProgressBar.setEnabled(true); mProgressBar.setEnabled(true);
mProgressBar.setVisibility(ProgressBar.VISIBLE);
mButtonStop.setEnabled(true);
}
private void uiStatePaused() {
uiResetWidgets();
mButtonReset.setEnabled(true); mButtonReset.setEnabled(true);
mProgressBar.setEnabled(true);
mProgressBar.setVisibility(ProgressBar.VISIBLE); mProgressBar.setVisibility(ProgressBar.VISIBLE);
} }
private void uiShowSwitchSlotInfo() { private void uiStateSlotSwitchRequired() {
uiResetWidgets();
mButtonReset.setEnabled(true);
mProgressBar.setEnabled(true);
mProgressBar.setVisibility(ProgressBar.VISIBLE);
mButtonSwitchSlot.setEnabled(true); mButtonSwitchSlot.setEnabled(true);
mTextViewUpdateInfo.setTextColor(Color.parseColor("#777777")); mTextViewUpdateInfo.setTextColor(Color.parseColor("#777777"));
} }
private void uiHideSwitchSlotInfo() { private void uiStateError() {
mTextViewUpdateInfo.setTextColor(Color.parseColor("#AAAAAA")); uiResetWidgets();
mButtonSwitchSlot.setEnabled(false); mButtonReset.setEnabled(true);
mProgressBar.setEnabled(true);
mProgressBar.setVisibility(ProgressBar.VISIBLE);
}
private void uiStateRebootRequired() {
uiResetWidgets();
mButtonReset.setEnabled(true);
} }
/** /**
@@ -0,0 +1,9 @@
{
"__": "*** Generated using tools/gen_update_config.py ***",
"ab_config": {
"force_switch_slot": false
},
"ab_install_type": "NON_STREAMING",
"name": "S ota_002_package",
"url": "file:///data/sample-ota-packages/ota_003_package.zip"
}
@@ -60,7 +60,7 @@ public class UpdateConfigTest {
public void setUp() throws Exception { public void setUp() throws Exception {
mContext = InstrumentationRegistry.getContext(); mContext = InstrumentationRegistry.getContext();
mTargetContext = InstrumentationRegistry.getTargetContext(); mTargetContext = InstrumentationRegistry.getTargetContext();
mJsonStreaming001 = readResource(R.raw.update_config_stream_001); mJsonStreaming001 = readResource(R.raw.update_config_001_stream);
} }
@Test @Test
@@ -18,19 +18,22 @@ package com.example.android.systemupdatersample;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.content.Context;
import android.os.UpdateEngine; import android.os.UpdateEngine;
import android.os.UpdateEngineCallback; import android.os.UpdateEngineCallback;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest; import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import com.example.android.systemupdatersample.tests.R;
import com.example.android.systemupdatersample.util.PayloadSpecs; import com.example.android.systemupdatersample.util.PayloadSpecs;
import com.google.common.collect.ImmutableList;
import com.google.common.io.CharStreams;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
@@ -40,7 +43,9 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule; import org.mockito.junit.MockitoRule;
import java.util.function.IntConsumer; import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
/** /**
* Tests for {@link UpdateManager} * Tests for {@link UpdateManager}
@@ -56,37 +61,86 @@ public class UpdateManagerTest {
private UpdateEngine mUpdateEngine; private UpdateEngine mUpdateEngine;
@Mock @Mock
private PayloadSpecs mPayloadSpecs; private PayloadSpecs mPayloadSpecs;
private UpdateManager mUpdateManager; private UpdateManager mSubject;
private Context mContext;
private UpdateConfig mNonStreamingUpdate003;
@Before @Before
public void setUp() { public void setUp() throws Exception {
mUpdateManager = new UpdateManager(mUpdateEngine, mPayloadSpecs); mContext = InstrumentationRegistry.getContext();
mSubject = new UpdateManager(mUpdateEngine, mPayloadSpecs);
mNonStreamingUpdate003 =
UpdateConfig.fromJson(readResource(R.raw.update_config_003_nonstream));
} }
@Test @Test
public void storesProgressThenInvokesCallbacks() { public void applyUpdate_appliesPayloadToUpdateEngine() throws Exception {
IntConsumer statusUpdateCallback = mock(IntConsumer.class); PayloadSpec payload = buildMockPayloadSpec();
when(mPayloadSpecs.forNonStreaming(any(File.class))).thenReturn(payload);
// When UpdateManager is bound to update_engine, it passes
// UpdateManager.UpdateEngineCallbackImpl as a callback to update_engine.
when(mUpdateEngine.bind(any(UpdateEngineCallback.class))).thenAnswer(answer -> { when(mUpdateEngine.bind(any(UpdateEngineCallback.class))).thenAnswer(answer -> {
// When UpdateManager is bound to update_engine, it passes
// UpdateEngineCallback as a callback to update_engine.
UpdateEngineCallback callback = answer.getArgument(0); UpdateEngineCallback callback = answer.getArgument(0);
callback.onStatusUpdate(/*engineStatus*/ 4, /*engineProgress*/ 0.2f); callback.onStatusUpdate(
UpdateEngine.UpdateStatusConstants.IDLE,
/*engineProgress*/ 0.0f);
return null; return null;
}); });
mUpdateManager.setOnEngineStatusUpdateCallback(statusUpdateCallback); mSubject.bind();
mSubject.applyUpdate(null, mNonStreamingUpdate003);
// Making sure that manager.getProgress() returns correct progress verify(mUpdateEngine).applyPayload(
// in "onEngineStatusUpdate" callback. "file://blah",
doAnswer(answer -> { 120,
assertEquals(0.2f, mUpdateManager.getProgress(), 1E-5); 340,
new String[] {
"SWITCH_SLOT_ON_REBOOT=0" // ab_config.force_switch_slot = false
});
}
@Test
public void stateIsRunningAndEngineStatusIsIdle_reApplyLastUpdate() throws Exception {
PayloadSpec payload = buildMockPayloadSpec();
when(mPayloadSpecs.forNonStreaming(any(File.class))).thenReturn(payload);
when(mUpdateEngine.bind(any(UpdateEngineCallback.class))).thenAnswer(answer -> {
// When UpdateManager is bound to update_engine, it passes
// UpdateEngineCallback as a callback to update_engine.
UpdateEngineCallback callback = answer.getArgument(0);
callback.onStatusUpdate(
UpdateEngine.UpdateStatusConstants.IDLE,
/*engineProgress*/ 0.0f);
return null; return null;
}).when(statusUpdateCallback).accept(anyInt()); });
mUpdateManager.bind(); mSubject.bind();
mSubject.applyUpdate(null, mNonStreamingUpdate003);
mSubject.unbind();
mSubject.bind(); // re-bind - now it should re-apply last update
verify(statusUpdateCallback, times(1)).accept(4); assertEquals(mSubject.getUpdaterState(), UpdaterState.RUNNING);
// it should be called 2 times
verify(mUpdateEngine, times(2)).applyPayload(
"file://blah",
120,
340,
new String[] {
"SWITCH_SLOT_ON_REBOOT=0" // ab_config.force_switch_slot = false
});
}
private PayloadSpec buildMockPayloadSpec() {
PayloadSpec payload = mock(PayloadSpec.class);
when(payload.getUrl()).thenReturn("file://blah");
when(payload.getOffset()).thenReturn(120L);
when(payload.getSize()).thenReturn(340L);
when(payload.getProperties()).thenReturn(ImmutableList.of());
return payload;
}
private String readResource(int id) throws IOException {
return CharStreams.toString(new InputStreamReader(
mContext.getResources().openRawResource(id)));
} }
} }