/* * 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 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 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 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. * *

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.

*/ 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. * *

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.

*/ public void resetUpdate() { try { mUpdateEngine.resetStatus(); } catch (Exception e) { Log.w(TAG, "UpdateEngine failed to reset the update", e); } } /** * Applies the given update. * *

UpdateEngine works asynchronously. This method doesn't wait until * end of the update.

*/ 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 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 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 prepareExtraProperties(UpdateConfig config) { List 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. * *

UpdateEngine works asynchronously. This method doesn't wait until * end of the update.

* *

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.

* * @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 extraProperties) { mLastPayloadSpec = payloadSpec; ArrayList 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 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); } } }