diff options
Diffstat (limited to '')
-rw-r--r-- | updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java | 345 |
1 files changed, 345 insertions, 0 deletions
diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java new file mode 100644 index 000000000..9f0a04e33 --- /dev/null +++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2018 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 com.example.android.systemupdatersample; + +import android.content.Context; +import android.os.UpdateEngine; +import android.os.UpdateEngineCallback; +import android.util.Log; + +import com.example.android.systemupdatersample.services.PrepareStreamingService; +import com.example.android.systemupdatersample.util.PayloadSpecs; +import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes; +import com.example.android.systemupdatersample.util.UpdateEngineProperties; +import com.google.common.util.concurrent.AtomicDouble; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.DoubleConsumer; +import java.util.function.IntConsumer; + +/** + * Manages the update flow. Asynchronously interacts with the {@link UpdateEngine}. + */ +public class UpdateManager { + + private static final String TAG = "UpdateManager"; + + /** HTTP Header: User-Agent; it will be sent to the server when streaming the payload. */ + private static final String HTTP_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36"; + + private final UpdateEngine mUpdateEngine; + private final PayloadSpecs mPayloadSpecs; + + private AtomicInteger mUpdateEngineStatus = + new AtomicInteger(UpdateEngine.UpdateStatusConstants.IDLE); + private AtomicInteger mEngineErrorCode = new AtomicInteger(UpdateEngineErrorCodes.UNKNOWN); + private AtomicDouble mProgress = new AtomicDouble(0); + + private final UpdateManager.UpdateEngineCallbackImpl + mUpdateEngineCallback = new UpdateManager.UpdateEngineCallbackImpl(); + + private PayloadSpec mLastPayloadSpec; + private AtomicBoolean mManualSwitchSlotRequired = new AtomicBoolean(true); + + private IntConsumer mOnEngineStatusUpdateCallback = null; + private DoubleConsumer mOnProgressUpdateCallback = null; + private IntConsumer mOnEngineCompleteCallback = null; + + private final Object mLock = new Object(); + + public UpdateManager(UpdateEngine updateEngine, PayloadSpecs payloadSpecs) { + this.mUpdateEngine = updateEngine; + this.mPayloadSpecs = payloadSpecs; + } + + /** + * Binds to {@link UpdateEngine}. + */ + public void bind() { + this.mUpdateEngine.bind(mUpdateEngineCallback); + } + + /** + * Unbinds from {@link UpdateEngine}. + */ + public void unbind() { + this.mUpdateEngine.unbind(); + } + + /** + * @return a number from {@code 0.0} to {@code 1.0}. + */ + public float getProgress() { + return (float) this.mProgress.get(); + } + + /** + * Returns true if manual switching slot is required. Value depends on + * the update config {@code ab_config.force_switch_slot}. + */ + public boolean manualSwitchSlotRequired() { + return mManualSwitchSlotRequired.get(); + } + + /** + * Sets update engine status update callback. Value of {@code status} will + * be one of the values from {@link UpdateEngine.UpdateStatusConstants}. + * + * @param onStatusUpdateCallback a callback with parameter {@code status}. + */ + public void setOnEngineStatusUpdateCallback(IntConsumer onStatusUpdateCallback) { + synchronized (mLock) { + this.mOnEngineStatusUpdateCallback = onStatusUpdateCallback; + } + } + + private Optional<IntConsumer> getOnEngineStatusUpdateCallback() { + synchronized (mLock) { + return mOnEngineStatusUpdateCallback == null + ? Optional.empty() + : Optional.of(mOnEngineStatusUpdateCallback); + } + } + + /** + * Sets update engine payload application complete callback. Value of {@code errorCode} will + * be one of the values from {@link UpdateEngine.ErrorCodeConstants}. + * + * @param onComplete a callback with parameter {@code errorCode}. + */ + public void setOnEngineCompleteCallback(IntConsumer onComplete) { + synchronized (mLock) { + this.mOnEngineCompleteCallback = onComplete; + } + } + + private Optional<IntConsumer> getOnEngineCompleteCallback() { + synchronized (mLock) { + return mOnEngineCompleteCallback == null + ? Optional.empty() + : Optional.of(mOnEngineCompleteCallback); + } + } + + /** + * Sets progress update callback. Progress is a number from {@code 0.0} to {@code 1.0}. + * + * @param onProgressCallback a callback with parameter {@code progress}. + */ + public void setOnProgressUpdateCallback(DoubleConsumer onProgressCallback) { + synchronized (mLock) { + this.mOnProgressUpdateCallback = onProgressCallback; + } + } + + private Optional<DoubleConsumer> getOnProgressUpdateCallback() { + synchronized (mLock) { + return mOnProgressUpdateCallback == null + ? Optional.empty() + : Optional.of(mOnProgressUpdateCallback); + } + } + + /** + * Requests update engine to stop any ongoing update. If an update has been applied, + * leave it as is. + * + * <p>Sometimes it's possible that the + * update engine would throw an error when the method is called, and the only way to + * handle it is to catch the exception.</p> + */ + public void cancelRunningUpdate() { + try { + mUpdateEngine.cancel(); + } catch (Exception e) { + Log.w(TAG, "UpdateEngine failed to stop the ongoing update", e); + } + } + + /** + * Resets update engine to IDLE state. If an update has been applied it reverts it. + * + * <p>Sometimes it's possible that the + * update engine would throw an error when the method is called, and the only way to + * handle it is to catch the exception.</p> + */ + public void resetUpdate() { + try { + mUpdateEngine.resetStatus(); + } catch (Exception e) { + Log.w(TAG, "UpdateEngine failed to reset the update", e); + } + } + + /** + * Applies the given update. + * + * <p>UpdateEngine works asynchronously. This method doesn't wait until + * end of the update.</p> + */ + public void applyUpdate(Context context, UpdateConfig config) { + mEngineErrorCode.set(UpdateEngineErrorCodes.UNKNOWN); + + if (!config.getAbConfig().getForceSwitchSlot()) { + mManualSwitchSlotRequired.set(true); + } else { + mManualSwitchSlotRequired.set(false); + } + + if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) { + applyAbNonStreamingUpdate(config); + } else { + applyAbStreamingUpdate(context, config); + } + } + + private void applyAbNonStreamingUpdate(UpdateConfig config) { + List<String> extraProperties = prepareExtraProperties(config); + + PayloadSpec payload; + try { + payload = mPayloadSpecs.forNonStreaming(config.getUpdatePackageFile()); + } catch (IOException e) { + Log.e(TAG, "Error creating payload spec", e); + return; + } + updateEngineApplyPayload(payload, extraProperties); + } + + private void applyAbStreamingUpdate(Context context, UpdateConfig config) { + List<String> extraProperties = prepareExtraProperties(config); + + Log.d(TAG, "Starting PrepareStreamingService"); + PrepareStreamingService.startService(context, config, (code, payloadSpec) -> { + if (code == PrepareStreamingService.RESULT_CODE_SUCCESS) { + extraProperties.add("USER_AGENT=" + HTTP_USER_AGENT); + config.getStreamingMetadata() + .getAuthorization() + .ifPresent(s -> extraProperties.add("AUTHORIZATION=" + s)); + updateEngineApplyPayload(payloadSpec, extraProperties); + } else { + Log.e(TAG, "PrepareStreamingService failed, result code is " + code); + } + }); + } + + private List<String> prepareExtraProperties(UpdateConfig config) { + List<String> extraProperties = new ArrayList<>(); + + if (!config.getAbConfig().getForceSwitchSlot()) { + // Disable switch slot on reboot, which is enabled by default. + // User will enable it manually by clicking "Switch Slot" button on the screen. + extraProperties.add(UpdateEngineProperties.PROPERTY_DISABLE_SWITCH_SLOT_ON_REBOOT); + } + return extraProperties; + } + + /** + * Applies given payload. + * + * <p>UpdateEngine works asynchronously. This method doesn't wait until + * end of the update.</p> + * + * <p>It's possible that the update engine throws a generic error, such as upon seeing invalid + * payload properties (which come from OTA packages), or failing to set up the network + * with the given id.</p> + * + * @param payloadSpec contains url, offset and size to {@code PAYLOAD_BINARY_FILE_NAME} + * @param extraProperties additional properties to pass to {@link UpdateEngine#applyPayload} + */ + private void updateEngineApplyPayload(PayloadSpec payloadSpec, List<String> extraProperties) { + mLastPayloadSpec = payloadSpec; + + ArrayList<String> properties = new ArrayList<>(payloadSpec.getProperties()); + if (extraProperties != null) { + properties.addAll(extraProperties); + } + try { + mUpdateEngine.applyPayload( + payloadSpec.getUrl(), + payloadSpec.getOffset(), + payloadSpec.getSize(), + properties.toArray(new String[0])); + } catch (Exception e) { + Log.e(TAG, "UpdateEngine failed to apply the update", e); + } + } + + /** + * Sets the new slot that has the updated partitions as the active slot, + * which device will boot into next time. + * This method is only supposed to be called after the payload is applied. + * + * Invoking {@link UpdateEngine#applyPayload} with the same payload url, offset, size + * and payload metadata headers doesn't trigger new update. It can be used to just switch + * active A/B slot. + * + * {@link UpdateEngine#applyPayload} might take several seconds to finish, and it will + * invoke callbacks {@link this#onStatusUpdate} and {@link this#onPayloadApplicationComplete)}. + */ + public void setSwitchSlotOnReboot() { + Log.d(TAG, "setSwitchSlotOnReboot invoked"); + List<String> extraProperties = new ArrayList<>(); + // PROPERTY_SKIP_POST_INSTALL should be passed on to skip post-installation hooks. + extraProperties.add(UpdateEngineProperties.PROPERTY_SKIP_POST_INSTALL); + // It sets property SWITCH_SLOT_ON_REBOOT=1 by default. + // HTTP headers are not required, UpdateEngine is not expected to stream payload. + updateEngineApplyPayload(mLastPayloadSpec, extraProperties); + } + + private void onStatusUpdate(int status, float progress) { + int previousStatus = mUpdateEngineStatus.get(); + mUpdateEngineStatus.set(status); + mProgress.set(progress); + + getOnProgressUpdateCallback().ifPresent(callback -> callback.accept(progress)); + + if (previousStatus != status) { + getOnEngineStatusUpdateCallback().ifPresent(callback -> callback.accept(status)); + } + } + + private void onPayloadApplicationComplete(int errorCode) { + Log.d(TAG, "onPayloadApplicationComplete invoked, errorCode=" + errorCode); + mEngineErrorCode.set(errorCode); + + getOnEngineCompleteCallback() + .ifPresent(callback -> callback.accept(errorCode)); + } + + /** + * Helper class to delegate {@code update_engine} callbacks to UpdateManager + */ + class UpdateEngineCallbackImpl extends UpdateEngineCallback { + @Override + public void onStatusUpdate(int status, float percent) { + UpdateManager.this.onStatusUpdate(status, percent); + } + + @Override + public void onPayloadApplicationComplete(int errorCode) { + UpdateManager.this.onPayloadApplicationComplete(errorCode); + } + } + +} |