summaryrefslogtreecommitdiffstats
path: root/updater_sample/src
diff options
context:
space:
mode:
Diffstat (limited to 'updater_sample/src')
-rw-r--r--updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java38
-rw-r--r--updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java249
-rw-r--r--updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java35
-rw-r--r--updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java13
-rw-r--r--updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java26
5 files changed, 334 insertions, 27 deletions
diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
index 23510e426..9bdd8b9e8 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
@@ -25,6 +25,7 @@ import org.json.JSONObject;
import java.io.File;
import java.io.Serializable;
+import java.util.Optional;
/**
* An update description. It will be parsed from JSON, which is intended to
@@ -69,16 +70,22 @@ public class UpdateConfig implements Parcelable {
if (c.mAbInstallType == AB_INSTALL_TYPE_STREAMING) {
JSONObject meta = o.getJSONObject("ab_streaming_metadata");
JSONArray propertyFilesJson = meta.getJSONArray("property_files");
- InnerFile[] propertyFiles =
- new InnerFile[propertyFilesJson.length()];
+ PackageFile[] propertyFiles =
+ new PackageFile[propertyFilesJson.length()];
for (int i = 0; i < propertyFilesJson.length(); i++) {
JSONObject p = propertyFilesJson.getJSONObject(i);
- propertyFiles[i] = new InnerFile(
+ propertyFiles[i] = new PackageFile(
p.getString("filename"),
p.getLong("offset"),
p.getLong("size"));
}
- c.mAbStreamingMetadata = new StreamingMetadata(propertyFiles);
+ String authorization = null;
+ if (meta.has("authorization")) {
+ authorization = meta.getString("authorization");
+ }
+ c.mAbStreamingMetadata = new StreamingMetadata(
+ propertyFiles,
+ authorization);
}
c.mRawJson = json;
return c;
@@ -176,25 +183,31 @@ public class UpdateConfig implements Parcelable {
private static final long serialVersionUID = 31042L;
/** defines beginning of update data in archive */
- private InnerFile[] mPropertyFiles;
+ private PackageFile[] mPropertyFiles;
- public StreamingMetadata() {
- mPropertyFiles = new InnerFile[0];
- }
+ /** SystemUpdaterSample receives the authorization token from the OTA server, in addition
+ * to the package URL. It passes on the info to update_engine, so that the latter can
+ * fetch the data from the package server directly with the token. */
+ private String mAuthorization;
- public StreamingMetadata(InnerFile[] propertyFiles) {
+ public StreamingMetadata(PackageFile[] propertyFiles, String authorization) {
this.mPropertyFiles = propertyFiles;
+ this.mAuthorization = authorization;
}
- public InnerFile[] getPropertyFiles() {
+ public PackageFile[] getPropertyFiles() {
return mPropertyFiles;
}
+
+ public Optional<String> getAuthorization() {
+ return mAuthorization == null ? Optional.empty() : Optional.of(mAuthorization);
+ }
}
/**
* Description of a file in an OTA package zip file.
*/
- public static class InnerFile implements Serializable {
+ public static class PackageFile implements Serializable {
private static final long serialVersionUID = 31043L;
@@ -207,7 +220,7 @@ public class UpdateConfig implements Parcelable {
/** size of the update data in archive */
private long mSize;
- public InnerFile(String filename, long offset, long size) {
+ public PackageFile(String filename, long offset, long size) {
this.mFilename = filename;
this.mOffset = offset;
this.mSize = size;
@@ -224,7 +237,6 @@ public class UpdateConfig implements Parcelable {
public long getSize() {
return mSize;
}
-
}
}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java
new file mode 100644
index 000000000..222bb0a58
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java
@@ -0,0 +1,249 @@
+/*
+ * 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.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.
+ *
+ * PrepareStreamingService runs on it's own thread. It will notify activity
+ * using interface {@link UpdateResultCallback} when update is ready to install.
+ */
+public class PrepareStreamingService extends IntentService {
+
+ /**
+ * UpdateResultCallback result codes.
+ */
+ public static final int RESULT_CODE_SUCCESS = 0;
+ public static final int RESULT_CODE_ERROR = 1;
+
+ /**
+ * This interface is used to send results from {@link PrepareStreamingService} 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 PrepareStreamingService}
+ * @param payloadSpec prepared payload spec for streaming update
+ */
+ void onReceiveResult(int resultCode, PayloadSpec payloadSpec);
+ }
+
+ /**
+ * Starts PrepareStreamingService.
+ *
+ * @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,
+ UpdateResultCallback resultCallback) {
+ Log.d(TAG, "Starting PrepareStreamingService");
+ ResultReceiver receiver = new CallbackResultReceiver(new Handler(), resultCallback);
+ Intent intent = new Intent(context, PrepareStreamingService.class);
+ intent.putExtra(EXTRA_PARAM_CONFIG, config);
+ intent.putExtra(EXTRA_PARAM_RESULT_RECEIVER, receiver);
+ context.startService(intent);
+ }
+
+ public PrepareStreamingService() {
+ super(TAG);
+ }
+
+ private static final String TAG = "PrepareStreamingService";
+
+ /**
+ * Extra params that will be sent from Activity to IntentService.
+ */
+ private static final String EXTRA_PARAM_CONFIG = "config";
+ private static final String EXTRA_PARAM_RESULT_RECEIVER = "result-receiver";
+
+ /**
+ * The files that should be downloaded before streaming.
+ */
+ private static final ImmutableSet<String> 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
+ );
+
+ @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 static PayloadSpec execute(UpdateConfig config)
+ throws IOException, PreparationFailedException {
+
+ downloadPreStreamingFiles(config, OTA_PACKAGE_DIR);
+
+ Optional<UpdateConfig.PackageFile> 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 PayloadSpecs.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#getStreamingMetadata()}
+ * and exists in {@code PRE_STREAMING_FILES_SET}, and put them
+ * in directory {@code dir}.
+ * @throws IOException when can't download a file
+ */
+ private static 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.getStreamingMetadata().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 static 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 PrepareStreamingService} 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);
+ }
+ }
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
index d6a6ce3f5..170825635 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
@@ -34,12 +34,14 @@ import android.widget.Toast;
import com.example.android.systemupdatersample.PayloadSpec;
import com.example.android.systemupdatersample.R;
import com.example.android.systemupdatersample.UpdateConfig;
+import com.example.android.systemupdatersample.services.PrepareStreamingService;
import com.example.android.systemupdatersample.util.PayloadSpecs;
import com.example.android.systemupdatersample.util.UpdateConfigs;
import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes;
import com.example.android.systemupdatersample.util.UpdateEngineStatuses;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@@ -50,6 +52,10 @@ public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
+ /** 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 TextView mTextViewBuild;
private Spinner mSpinnerConfigs;
private TextView mTextViewConfigsDirHint;
@@ -294,9 +300,25 @@ public class MainActivity extends Activity {
.show();
return;
}
- updateEngineApplyPayload(payload);
+ updateEngineApplyPayload(payload, null);
} else {
Log.d(TAG, "Starting PrepareStreamingService");
+ PrepareStreamingService.startService(this, config, (code, payloadSpec) -> {
+ if (code == PrepareStreamingService.RESULT_CODE_SUCCESS) {
+ List<String> extraProperties = new ArrayList<>();
+ 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);
+ Toast.makeText(
+ MainActivity.this,
+ "PrepareStreamingService failed, result code is " + code,
+ Toast.LENGTH_LONG).show();
+ }
+ });
}
}
@@ -305,14 +327,21 @@ public class MainActivity extends Activity {
*
* UpdateEngine works asynchronously. This method doesn't wait until
* end of the update.
+ *
+ * @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) {
+ private void updateEngineApplyPayload(PayloadSpec payloadSpec, List<String> extraProperties) {
+ ArrayList<String> properties = new ArrayList<>(payloadSpec.getProperties());
+ if (extraProperties != null) {
+ properties.addAll(extraProperties);
+ }
try {
mUpdateEngine.applyPayload(
payloadSpec.getUrl(),
payloadSpec.getOffset(),
payloadSpec.getSize(),
- payloadSpec.getProperties().toArray(new String[0]));
+ properties.toArray(new String[0]));
} catch (Exception e) {
Log.e(TAG, "UpdateEngine failed to apply the update", e);
Toast.makeText(
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java
index 5c1d71117..ddd0919b8 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java
@@ -38,22 +38,23 @@ public final class FileDownloader {
private String mUrl;
private long mOffset;
private long mSize;
- private File mOut;
+ private File mDestination;
- public FileDownloader(String url, long offset, long size, File out) {
+ public FileDownloader(String url, long offset, long size, File destination) {
this.mUrl = url;
this.mOffset = offset;
this.mSize = size;
- this.mOut = out;
+ this.mDestination = destination;
}
/**
* Downloads the file with given offset and size.
+ * @throws IOException when can't download the file
*/
public void download() throws IOException {
- Log.d("FileDownloader", "downloading " + mOut.getName()
+ Log.d("FileDownloader", "downloading " + mDestination.getName()
+ " from " + mUrl
- + " to " + mOut.getAbsolutePath());
+ + " to " + mDestination.getAbsolutePath());
URL url = new URL(mUrl);
URLConnection connection = url.openConnection();
@@ -61,7 +62,7 @@ public final class FileDownloader {
// download the file
try (InputStream input = connection.getInputStream()) {
- try (OutputStream output = new FileOutputStream(mOut)) {
+ try (OutputStream output = new FileOutputStream(mDestination)) {
long skipped = input.skip(mOffset);
if (skipped != mOffset) {
throw new IOException("Can't download file "
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java
index 71d4df8ab..5080cb6d8 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java
@@ -26,14 +26,16 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.Optional;
/**
* Utility class for working with json update configurations.
*/
public final class UpdateConfigs {
- private static final String UPDATE_CONFIGS_ROOT = "configs/";
+ public static final String UPDATE_CONFIGS_ROOT = "configs/";
/**
* @param configs update configs
@@ -48,13 +50,12 @@ public final class UpdateConfigs {
* @return configs root directory
*/
public static String getConfigsRoot(Context context) {
- return Paths.get(context.getFilesDir().toString(),
- UPDATE_CONFIGS_ROOT).toString();
+ return Paths
+ .get(context.getFilesDir().toString(), UPDATE_CONFIGS_ROOT)
+ .toString();
}
/**
- * It parses only {@code .json} files.
- *
* @param context application context
* @return list of configs from directory {@link UpdateConfigs#getConfigsRoot}
*/
@@ -80,5 +81,20 @@ public final class UpdateConfigs {
return configs;
}
+ /**
+ * @param filename searches by given filename
+ * @param config searches in {@link UpdateConfig#getStreamingMetadata()}
+ * @return offset and size of {@code filename} in the package zip file
+ * stored as {@link UpdateConfig.PackageFile}.
+ */
+ public static Optional<UpdateConfig.PackageFile> getPropertyFile(
+ final String filename,
+ UpdateConfig config) {
+ return Arrays
+ .stream(config.getStreamingMetadata().getPropertyFiles())
+ .filter(file -> filename.equals(file.getFilename()))
+ .findFirst();
+ }
+
private UpdateConfigs() {}
}