From bc07775393675517ecf9369f805a43e5b0929dcf Mon Sep 17 00:00:00 2001 From: Zhomart Mukhamejanov Date: Fri, 14 Dec 2018 09:36:32 -0800 Subject: Add PrepareUpdateService. It's moved from PrepareStreamingService intent service. Now PrepareUpdateService takes an UpdateConfig and builds PayloadSpec for UpdateEngine for both streaming and non-streaming update. It allows us to do all preparations in intent service's thread, without blocking UI. We will also add checksum verification to PrepareUpdateService. Test: device, junit Bug: 77150191 Change-Id: I15c0bc58e3238bea6ea1c4f13063575e2def89c1 Merged-In: Iea69acd9aa41e17538c26aff60f7598093ca7744 --- .../services/PrepareUpdateService.java | 258 +++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 updater_sample/src/com/example/android/systemupdatersample/services/PrepareUpdateService.java (limited to 'updater_sample/src/com/example/android/systemupdatersample/services/PrepareUpdateService.java') diff --git a/updater_sample/src/com/example/android/systemupdatersample/services/PrepareUpdateService.java b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareUpdateService.java new file mode 100644 index 000000000..06581bee3 --- /dev/null +++ b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareUpdateService.java @@ -0,0 +1,258 @@ +/* + * 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.services; + +import static com.example.android.systemupdatersample.util.PackageFiles.COMPATIBILITY_ZIP_FILE_NAME; +import static com.example.android.systemupdatersample.util.PackageFiles.OTA_PACKAGE_DIR; +import static com.example.android.systemupdatersample.util.PackageFiles.PAYLOAD_BINARY_FILE_NAME; +import static com.example.android.systemupdatersample.util.PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.RecoverySystem; +import android.os.ResultReceiver; +import android.os.UpdateEngine; +import android.util.Log; + +import com.example.android.systemupdatersample.PayloadSpec; +import com.example.android.systemupdatersample.UpdateConfig; +import com.example.android.systemupdatersample.util.FileDownloader; +import com.example.android.systemupdatersample.util.PackageFiles; +import com.example.android.systemupdatersample.util.PayloadSpecs; +import com.example.android.systemupdatersample.util.UpdateConfigs; +import com.google.common.collect.ImmutableSet; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Optional; + +/** + * This IntentService will download/extract the necessary files from the package zip + * without downloading the whole package. And it constructs {@link PayloadSpec}. + * All this work required to install streaming A/B updates. + * + * PrepareUpdateService runs on it's own thread. It will notify activity + * using interface {@link UpdateResultCallback} when update is ready to install. + */ +public class PrepareUpdateService extends IntentService { + + /** + * UpdateResultCallback result codes. + */ + public static final int RESULT_CODE_SUCCESS = 0; + public static final int RESULT_CODE_ERROR = 1; + + /** + * Extra params that will be sent to IntentService. + */ + public static final String EXTRA_PARAM_CONFIG = "config"; + public static final String EXTRA_PARAM_RESULT_RECEIVER = "result-receiver"; + + /** + * This interface is used to send results from {@link PrepareUpdateService} to + * {@code MainActivity}. + */ + public interface UpdateResultCallback { + /** + * Invoked when files are downloaded and payload spec is constructed. + * + * @param resultCode result code, values are defined in {@link PrepareUpdateService} + * @param payloadSpec prepared payload spec for streaming update + */ + void onReceiveResult(int resultCode, PayloadSpec payloadSpec); + } + + /** + * Starts PrepareUpdateService. + * + * @param context application context + * @param config update config + * @param resultCallback callback that will be called when the update is ready to be installed + */ + public static void startService(Context context, + UpdateConfig config, + Handler handler, + UpdateResultCallback resultCallback) { + Log.d(TAG, "Starting PrepareUpdateService"); + ResultReceiver receiver = new CallbackResultReceiver(handler, resultCallback); + Intent intent = new Intent(context, PrepareUpdateService.class); + intent.putExtra(EXTRA_PARAM_CONFIG, config); + intent.putExtra(EXTRA_PARAM_RESULT_RECEIVER, receiver); + context.startService(intent); + } + + public PrepareUpdateService() { + super(TAG); + } + + private static final String TAG = "PrepareUpdateService"; + + /** + * The files that should be downloaded before streaming. + */ + private static final ImmutableSet PRE_STREAMING_FILES_SET = + ImmutableSet.of( + PackageFiles.CARE_MAP_FILE_NAME, + PackageFiles.COMPATIBILITY_ZIP_FILE_NAME, + PackageFiles.METADATA_FILE_NAME, + PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME + ); + + private final PayloadSpecs mPayloadSpecs = new PayloadSpecs(); + private final UpdateEngine mUpdateEngine = new UpdateEngine(); + + @Override + protected void onHandleIntent(Intent intent) { + Log.d(TAG, "On handle intent is called"); + UpdateConfig config = intent.getParcelableExtra(EXTRA_PARAM_CONFIG); + ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_PARAM_RESULT_RECEIVER); + + try { + PayloadSpec spec = execute(config); + resultReceiver.send(RESULT_CODE_SUCCESS, CallbackResultReceiver.createBundle(spec)); + } catch (Exception e) { + Log.e(TAG, "Failed to prepare streaming update", e); + resultReceiver.send(RESULT_CODE_ERROR, null); + } + } + + /** + * 1. Downloads files for streaming updates. + * 2. Makes sure required files are present. + * 3. Checks OTA package compatibility with the device. + * 4. Constructs {@link PayloadSpec} for streaming update. + */ + private PayloadSpec execute(UpdateConfig config) + throws IOException, PreparationFailedException { + + if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) { + return mPayloadSpecs.forNonStreaming(config.getUpdatePackageFile()); + } + + downloadPreStreamingFiles(config, OTA_PACKAGE_DIR); + + Optional payloadBinary = + UpdateConfigs.getPropertyFile(PAYLOAD_BINARY_FILE_NAME, config); + + if (!payloadBinary.isPresent()) { + throw new PreparationFailedException( + "Failed to find " + PAYLOAD_BINARY_FILE_NAME + " in config"); + } + + if (!UpdateConfigs.getPropertyFile(PAYLOAD_PROPERTIES_FILE_NAME, config).isPresent() + || !Paths.get(OTA_PACKAGE_DIR, PAYLOAD_PROPERTIES_FILE_NAME).toFile().exists()) { + throw new IOException(PAYLOAD_PROPERTIES_FILE_NAME + " not found"); + } + + File compatibilityFile = Paths.get(OTA_PACKAGE_DIR, COMPATIBILITY_ZIP_FILE_NAME).toFile(); + if (compatibilityFile.isFile()) { + Log.i(TAG, "Verifying OTA package for compatibility with the device"); + if (!verifyPackageCompatibility(compatibilityFile)) { + throw new PreparationFailedException( + "OTA package is not compatible with this device"); + } + } + + return mPayloadSpecs.forStreaming(config.getUrl(), + payloadBinary.get().getOffset(), + payloadBinary.get().getSize(), + Paths.get(OTA_PACKAGE_DIR, PAYLOAD_PROPERTIES_FILE_NAME).toFile()); + } + + /** + * Downloads files defined in {@link UpdateConfig#getAbConfig()} + * and exists in {@code PRE_STREAMING_FILES_SET}, and put them + * in directory {@code dir}. + * + * @throws IOException when can't download a file + */ + private void downloadPreStreamingFiles(UpdateConfig config, String dir) + throws IOException { + Log.d(TAG, "Deleting existing files from " + dir); + for (String file : PRE_STREAMING_FILES_SET) { + Files.deleteIfExists(Paths.get(OTA_PACKAGE_DIR, file)); + } + Log.d(TAG, "Downloading files to " + dir); + for (UpdateConfig.PackageFile file : config.getAbConfig().getPropertyFiles()) { + if (PRE_STREAMING_FILES_SET.contains(file.getFilename())) { + Log.d(TAG, "Downloading file " + file.getFilename()); + FileDownloader downloader = new FileDownloader( + config.getUrl(), + file.getOffset(), + file.getSize(), + Paths.get(dir, file.getFilename()).toFile()); + downloader.download(); + } + } + } + + /** + * @param file physical location of {@link PackageFiles#COMPATIBILITY_ZIP_FILE_NAME} + * @return true if OTA package is compatible with this device + */ + private boolean verifyPackageCompatibility(File file) { + try { + return RecoverySystem.verifyPackageCompatibility(file); + } catch (IOException e) { + Log.e(TAG, "Failed to verify package compatibility", e); + return false; + } + } + + /** + * Used by {@link PrepareUpdateService} to pass {@link PayloadSpec} + * to {@link UpdateResultCallback#onReceiveResult}. + */ + private static class CallbackResultReceiver extends ResultReceiver { + + static Bundle createBundle(PayloadSpec payloadSpec) { + Bundle b = new Bundle(); + b.putSerializable(BUNDLE_PARAM_PAYLOAD_SPEC, payloadSpec); + return b; + } + + private static final String BUNDLE_PARAM_PAYLOAD_SPEC = "payload-spec"; + + private UpdateResultCallback mUpdateResultCallback; + + CallbackResultReceiver(Handler handler, UpdateResultCallback updateResultCallback) { + super(handler); + this.mUpdateResultCallback = updateResultCallback; + } + + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + PayloadSpec payloadSpec = null; + if (resultCode == RESULT_CODE_SUCCESS) { + payloadSpec = (PayloadSpec) resultData.getSerializable(BUNDLE_PARAM_PAYLOAD_SPEC); + } + mUpdateResultCallback.onReceiveResult(resultCode, payloadSpec); + } + } + + private static class PreparationFailedException extends Exception { + PreparationFailedException(String message) { + super(message); + } + } + +} -- cgit v1.2.3