Merge "updater_sample: add streaming support"
am: c23a53c272
Change-Id: I4aa8a41a4dd3afd4b1c5fb3d6955abea45c16fc4
This commit is contained in:
@@ -31,6 +31,7 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<service android:name=".services.PrepareStreamingService"/>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -69,11 +69,11 @@ public class UpdateConfig implements Parcelable {
|
|||||||
if (c.mAbInstallType == AB_INSTALL_TYPE_STREAMING) {
|
if (c.mAbInstallType == AB_INSTALL_TYPE_STREAMING) {
|
||||||
JSONObject meta = o.getJSONObject("ab_streaming_metadata");
|
JSONObject meta = o.getJSONObject("ab_streaming_metadata");
|
||||||
JSONArray propertyFilesJson = meta.getJSONArray("property_files");
|
JSONArray propertyFilesJson = meta.getJSONArray("property_files");
|
||||||
InnerFile[] propertyFiles =
|
PackageFile[] propertyFiles =
|
||||||
new InnerFile[propertyFilesJson.length()];
|
new PackageFile[propertyFilesJson.length()];
|
||||||
for (int i = 0; i < propertyFilesJson.length(); i++) {
|
for (int i = 0; i < propertyFilesJson.length(); i++) {
|
||||||
JSONObject p = propertyFilesJson.getJSONObject(i);
|
JSONObject p = propertyFilesJson.getJSONObject(i);
|
||||||
propertyFiles[i] = new InnerFile(
|
propertyFiles[i] = new PackageFile(
|
||||||
p.getString("filename"),
|
p.getString("filename"),
|
||||||
p.getLong("offset"),
|
p.getLong("offset"),
|
||||||
p.getLong("size"));
|
p.getLong("size"));
|
||||||
@@ -176,17 +176,17 @@ public class UpdateConfig implements Parcelable {
|
|||||||
private static final long serialVersionUID = 31042L;
|
private static final long serialVersionUID = 31042L;
|
||||||
|
|
||||||
/** defines beginning of update data in archive */
|
/** defines beginning of update data in archive */
|
||||||
private InnerFile[] mPropertyFiles;
|
private PackageFile[] mPropertyFiles;
|
||||||
|
|
||||||
public StreamingMetadata() {
|
public StreamingMetadata() {
|
||||||
mPropertyFiles = new InnerFile[0];
|
mPropertyFiles = new PackageFile[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
public StreamingMetadata(InnerFile[] propertyFiles) {
|
public StreamingMetadata(PackageFile[] propertyFiles) {
|
||||||
this.mPropertyFiles = propertyFiles;
|
this.mPropertyFiles = propertyFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
public InnerFile[] getPropertyFiles() {
|
public PackageFile[] getPropertyFiles() {
|
||||||
return mPropertyFiles;
|
return mPropertyFiles;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,7 +194,7 @@ public class UpdateConfig implements Parcelable {
|
|||||||
/**
|
/**
|
||||||
* Description of a file in an OTA package zip file.
|
* 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;
|
private static final long serialVersionUID = 31043L;
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ public class UpdateConfig implements Parcelable {
|
|||||||
/** size of the update data in archive */
|
/** size of the update data in archive */
|
||||||
private long mSize;
|
private long mSize;
|
||||||
|
|
||||||
public InnerFile(String filename, long offset, long size) {
|
public PackageFile(String filename, long offset, long size) {
|
||||||
this.mFilename = filename;
|
this.mFilename = filename;
|
||||||
this.mOffset = offset;
|
this.mOffset = offset;
|
||||||
this.mSize = size;
|
this.mSize = size;
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
/*
|
||||||
|
* 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.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.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.IOException;
|
||||||
|
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 {
|
||||||
|
downloadPreStreamingFiles(config, OTA_PACKAGE_DIR);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Failed to download pre-streaming files", e);
|
||||||
|
resultReceiver.send(RESULT_CODE_ERROR, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<UpdateConfig.PackageFile> payloadBinary =
|
||||||
|
UpdateConfigs.getPropertyFile(PAYLOAD_BINARY_FILE_NAME, config);
|
||||||
|
|
||||||
|
if (!payloadBinary.isPresent()) {
|
||||||
|
Log.e(TAG, "Failed to find " + PAYLOAD_BINARY_FILE_NAME + " in config");
|
||||||
|
resultReceiver.send(RESULT_CODE_ERROR, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<UpdateConfig.PackageFile> properties =
|
||||||
|
UpdateConfigs.getPropertyFile(PAYLOAD_PROPERTIES_FILE_NAME, config);
|
||||||
|
|
||||||
|
if (!properties.isPresent()) {
|
||||||
|
Log.e(TAG, "Failed to find " + PAYLOAD_PROPERTIES_FILE_NAME + " in config");
|
||||||
|
resultReceiver.send(RESULT_CODE_ERROR, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PayloadSpec spec;
|
||||||
|
try {
|
||||||
|
spec = PayloadSpecs.forStreaming(config.getUrl(),
|
||||||
|
payloadBinary.get().getOffset(),
|
||||||
|
payloadBinary.get().getSize(),
|
||||||
|
Paths.get(OTA_PACKAGE_DIR, properties.get().getFilename()).toFile()
|
||||||
|
);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "PayloadSpecs failed to create PayloadSpec for streaming", e);
|
||||||
|
resultReceiver.send(RESULT_CODE_ERROR, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultReceiver.send(RESULT_CODE_SUCCESS, CallbackResultReceiver.createBundle(spec));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, "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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ import android.widget.Toast;
|
|||||||
import com.example.android.systemupdatersample.PayloadSpec;
|
import com.example.android.systemupdatersample.PayloadSpec;
|
||||||
import com.example.android.systemupdatersample.R;
|
import com.example.android.systemupdatersample.R;
|
||||||
import com.example.android.systemupdatersample.UpdateConfig;
|
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.PayloadSpecs;
|
||||||
import com.example.android.systemupdatersample.util.UpdateConfigs;
|
import com.example.android.systemupdatersample.util.UpdateConfigs;
|
||||||
import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes;
|
import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes;
|
||||||
@@ -297,6 +298,17 @@ public class MainActivity extends Activity {
|
|||||||
updateEngineApplyPayload(payload);
|
updateEngineApplyPayload(payload);
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Starting PrepareStreamingService");
|
Log.d(TAG, "Starting PrepareStreamingService");
|
||||||
|
PrepareStreamingService.startService(this, config, (code, payloadSpec) -> {
|
||||||
|
if (code == PrepareStreamingService.RESULT_CODE_SUCCESS) {
|
||||||
|
updateEngineApplyPayload(payloadSpec);
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "PrepareStreamingService failed, result code is " + code);
|
||||||
|
Toast.makeText(
|
||||||
|
MainActivity.this,
|
||||||
|
"PrepareStreamingService failed, result code is " + code,
|
||||||
|
Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,22 +38,23 @@ public final class FileDownloader {
|
|||||||
private String mUrl;
|
private String mUrl;
|
||||||
private long mOffset;
|
private long mOffset;
|
||||||
private long mSize;
|
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.mUrl = url;
|
||||||
this.mOffset = offset;
|
this.mOffset = offset;
|
||||||
this.mSize = size;
|
this.mSize = size;
|
||||||
this.mOut = out;
|
this.mDestination = destination;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads the file with given offset and size.
|
* Downloads the file with given offset and size.
|
||||||
|
* @throws IOException when can't download the file
|
||||||
*/
|
*/
|
||||||
public void download() throws IOException {
|
public void download() throws IOException {
|
||||||
Log.d("FileDownloader", "downloading " + mOut.getName()
|
Log.d("FileDownloader", "downloading " + mDestination.getName()
|
||||||
+ " from " + mUrl
|
+ " from " + mUrl
|
||||||
+ " to " + mOut.getAbsolutePath());
|
+ " to " + mDestination.getAbsolutePath());
|
||||||
|
|
||||||
URL url = new URL(mUrl);
|
URL url = new URL(mUrl);
|
||||||
URLConnection connection = url.openConnection();
|
URLConnection connection = url.openConnection();
|
||||||
@@ -61,7 +62,7 @@ public final class FileDownloader {
|
|||||||
|
|
||||||
// download the file
|
// download the file
|
||||||
try (InputStream input = connection.getInputStream()) {
|
try (InputStream input = connection.getInputStream()) {
|
||||||
try (OutputStream output = new FileOutputStream(mOut)) {
|
try (OutputStream output = new FileOutputStream(mDestination)) {
|
||||||
long skipped = input.skip(mOffset);
|
long skipped = input.skip(mOffset);
|
||||||
if (skipped != mOffset) {
|
if (skipped != mOffset) {
|
||||||
throw new IOException("Can't download file "
|
throw new IOException("Can't download file "
|
||||||
|
|||||||
@@ -26,14 +26,16 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class for working with json update configurations.
|
* Utility class for working with json update configurations.
|
||||||
*/
|
*/
|
||||||
public final class UpdateConfigs {
|
public final class UpdateConfigs {
|
||||||
|
|
||||||
private static final String UPDATE_CONFIGS_ROOT = "configs/";
|
public static final String UPDATE_CONFIGS_ROOT = "configs/";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param configs update configs
|
* @param configs update configs
|
||||||
@@ -48,13 +50,12 @@ public final class UpdateConfigs {
|
|||||||
* @return configs root directory
|
* @return configs root directory
|
||||||
*/
|
*/
|
||||||
public static String getConfigsRoot(Context context) {
|
public static String getConfigsRoot(Context context) {
|
||||||
return Paths.get(context.getFilesDir().toString(),
|
return Paths
|
||||||
UPDATE_CONFIGS_ROOT).toString();
|
.get(context.getFilesDir().toString(), UPDATE_CONFIGS_ROOT)
|
||||||
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It parses only {@code .json} files.
|
|
||||||
*
|
|
||||||
* @param context application context
|
* @param context application context
|
||||||
* @return list of configs from directory {@link UpdateConfigs#getConfigsRoot}
|
* @return list of configs from directory {@link UpdateConfigs#getConfigsRoot}
|
||||||
*/
|
*/
|
||||||
@@ -80,5 +81,20 @@ public final class UpdateConfigs {
|
|||||||
return configs;
|
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() {}
|
private UpdateConfigs() {}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user