Snap for 4775956 from 76e6734173 to qt-release
Change-Id: Id8ce9b7b51140d42e063f76c9016ebb1588f6c29
This commit is contained in:
@@ -1,3 +1,33 @@
|
||||
# bootable/recovery project uses repohook to apply `clang-format` to the changed lines, with the
|
||||
# local style file in `.clang-format`. This will be triggered automatically with `repo upload`.
|
||||
# Alternatively, one can stage and format a change with `git clang-format` directly.
|
||||
#
|
||||
# $ git add <files>
|
||||
# $ git clang-format --style file
|
||||
#
|
||||
# Or to format a committed change.
|
||||
#
|
||||
# $ git clang-format --style file HEAD~1
|
||||
#
|
||||
# `--style file` will pick up the local style file in `.clang-format`. This can be configured as the
|
||||
# default behavior for bootable/recovery project.
|
||||
#
|
||||
# $ git config --local clangFormat.style file
|
||||
#
|
||||
# Note that `repo upload` calls the `clang-format` binary in Android repo (i.e.
|
||||
# `$ANDROID_BUILD_TOP/prebuilts/clang/host/linux-x86/clang-stable/bin/clang-format`), which might
|
||||
# give slightly different results from the one installed in host machine (e.g.
|
||||
# `/usr/bin/clang-format`). Specifying the file with `--binary` will ensure consistent results.
|
||||
#
|
||||
# $ git clang-format --binary \
|
||||
# /path/to/aosp-master/prebuilts/clang/host/linux-x86/clang-stable/bin/clang-format
|
||||
#
|
||||
# Or to do one-time setup to make it default.
|
||||
#
|
||||
# $ git config --local clangFormat.binary \
|
||||
# /path/to/aosp-master/prebuilts/clang/host/linux-x86/clang-stable/bin/clang-format
|
||||
#
|
||||
|
||||
BasedOnStyle: Google
|
||||
AllowShortBlocksOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: Empty
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -51,11 +52,15 @@ class Device {
|
||||
explicit Device(RecoveryUI* ui) : ui_(ui) {}
|
||||
virtual ~Device() {}
|
||||
|
||||
// Called to obtain the UI object that should be used to display the recovery user interface for
|
||||
// this device. You should not have called Init() on the UI object already, the caller will do
|
||||
// that after this method returns.
|
||||
// Returns a raw pointer to the RecoveryUI object.
|
||||
virtual RecoveryUI* GetUI() {
|
||||
return ui_;
|
||||
return ui_.get();
|
||||
}
|
||||
|
||||
// Resets the UI object to the given UI. Used to override the default UI in case initialization
|
||||
// failed, or we want a different UI for some reason. The device object will take the ownership.
|
||||
virtual void ResetUI(RecoveryUI* ui) {
|
||||
ui_.reset(ui);
|
||||
}
|
||||
|
||||
// Called when recovery starts up (after the UI has been obtained and initialized and after the
|
||||
@@ -64,16 +69,15 @@ class Device {
|
||||
|
||||
// Called from the main thread when recovery is at the main menu and waiting for input, and a key
|
||||
// is pressed. (Note that "at" the main menu does not necessarily mean the menu is visible;
|
||||
// recovery will be at the main menu with it invisible after an unsuccessful operation [ie OTA
|
||||
// package failure], or if recovery is started with no command.)
|
||||
// recovery will be at the main menu with it invisible after an unsuccessful operation, such as
|
||||
// failed to install an OTA package, or if recovery is started with no command.)
|
||||
//
|
||||
// 'key' is the code of the key just pressed. (You can call IsKeyPressed() on the RecoveryUI
|
||||
// object you returned from GetUI if you want to find out if other keys are held down.)
|
||||
// object you returned from GetUI() if you want to find out if other keys are held down.)
|
||||
//
|
||||
// 'visible' is true if the menu is visible.
|
||||
//
|
||||
// Returns one of the defined constants below in order to:
|
||||
//
|
||||
// - move the menu highlight (kHighlight{Up,Down}: negative value)
|
||||
// - invoke the highlighted item (kInvokeItem: negative value)
|
||||
// - do nothing (kNoAction: negative value)
|
||||
@@ -81,7 +85,7 @@ class Device {
|
||||
virtual int HandleMenuKey(int key, bool visible);
|
||||
|
||||
// Returns the list of menu items (a vector of strings). The menu_position passed to
|
||||
// InvokeMenuItem will correspond to the indexes into this array.
|
||||
// InvokeMenuItem() will correspond to the indexes into this array.
|
||||
virtual const std::vector<std::string>& GetMenuItems();
|
||||
|
||||
// Performs a recovery action selected from the menu. 'menu_position' will be the index of the
|
||||
@@ -106,7 +110,8 @@ class Device {
|
||||
}
|
||||
|
||||
private:
|
||||
RecoveryUI* ui_;
|
||||
// The RecoveryUI object that should be used to display the user interface for this device.
|
||||
std::unique_ptr<RecoveryUI> ui_;
|
||||
};
|
||||
|
||||
// The device-specific library must define this function (or the default one will be used, if there
|
||||
|
||||
+8
-9
@@ -64,7 +64,6 @@
|
||||
#include "fuse_sideload.h"
|
||||
#include "install.h"
|
||||
#include "logging.h"
|
||||
#include "minui/minui.h"
|
||||
#include "otautil/dirutil.h"
|
||||
#include "otautil/error_code.h"
|
||||
#include "otautil/paths.h"
|
||||
@@ -89,7 +88,6 @@ static constexpr const char* SDCARD_ROOT = "/sdcard";
|
||||
// into target_files.zip. Assert the version defined in code and in Android.mk are consistent.
|
||||
static_assert(kRecoveryApiVersion == RECOVERY_API_VERSION, "Mismatching recovery API versions.");
|
||||
|
||||
static std::string locale;
|
||||
static bool has_cache = false;
|
||||
|
||||
RecoveryUI* ui = nullptr;
|
||||
@@ -233,6 +231,7 @@ static void set_sdcard_update_bootloader_message() {
|
||||
// copy our log file to cache as well (for the system to read). This function is
|
||||
// idempotent: call it as many times as you like.
|
||||
static void finish_recovery() {
|
||||
std::string locale = ui->GetLocale();
|
||||
// Save the locale to cache, so if recovery is next started up without a '--locale' argument
|
||||
// (e.g., directly from the bootloader) it will use the last-known locale.
|
||||
if (!locale.empty() && has_cache) {
|
||||
@@ -897,7 +896,7 @@ static Device::BuiltinAction prompt_and_wait(Device* device, int status) {
|
||||
|
||||
case Device::RUN_LOCALE_TEST: {
|
||||
ScreenRecoveryUI* screen_ui = static_cast<ScreenRecoveryUI*>(ui);
|
||||
screen_ui->CheckBackgroundTextImages(locale);
|
||||
screen_ui->CheckBackgroundTextImages();
|
||||
break;
|
||||
}
|
||||
case Device::MOUNT_SYSTEM:
|
||||
@@ -1125,6 +1124,7 @@ int start_recovery(int argc, char** argv) {
|
||||
bool shutdown_after = false;
|
||||
int retry_count = 0;
|
||||
bool security_update = false;
|
||||
std::string locale;
|
||||
|
||||
int arg;
|
||||
int option_index;
|
||||
@@ -1193,15 +1193,14 @@ int start_recovery(int argc, char** argv) {
|
||||
Device* device = make_device();
|
||||
if (android::base::GetBoolProperty("ro.boot.quiescent", false)) {
|
||||
printf("Quiescent recovery mode.\n");
|
||||
ui = new StubRecoveryUI();
|
||||
device->ResetUI(new StubRecoveryUI());
|
||||
} else {
|
||||
ui = device->GetUI();
|
||||
|
||||
if (!ui->Init(locale)) {
|
||||
printf("Failed to initialize UI, use stub UI instead.\n");
|
||||
ui = new StubRecoveryUI();
|
||||
if (!device->GetUI()->Init(locale)) {
|
||||
printf("Failed to initialize UI; using stub UI instead.\n");
|
||||
device->ResetUI(new StubRecoveryUI());
|
||||
}
|
||||
}
|
||||
ui = device->GetUI();
|
||||
|
||||
// Set background string to "installing security update" for security update,
|
||||
// otherwise set it to "installing system update".
|
||||
|
||||
+14
-7
@@ -372,7 +372,7 @@ void ScreenRecoveryUI::SelectAndShowBackgroundText(const std::vector<std::string
|
||||
std::string header = "Show background text image";
|
||||
text_y += DrawTextLine(text_x, text_y, header, true);
|
||||
std::string locale_selection = android::base::StringPrintf(
|
||||
"Current locale: %s, %zu/%zu", locales_entries[sel].c_str(), sel, locales_entries.size());
|
||||
"Current locale: %s, %zu/%zu", locales_entries[sel].c_str(), sel + 1, locales_entries.size());
|
||||
// clang-format off
|
||||
std::vector<std::string> instruction = {
|
||||
locale_selection,
|
||||
@@ -395,13 +395,14 @@ void ScreenRecoveryUI::SelectAndShowBackgroundText(const std::vector<std::string
|
||||
pthread_mutex_unlock(&updateMutex);
|
||||
}
|
||||
|
||||
void ScreenRecoveryUI::CheckBackgroundTextImages(const std::string& saved_locale) {
|
||||
void ScreenRecoveryUI::CheckBackgroundTextImages() {
|
||||
// Load a list of locales embedded in one of the resource files.
|
||||
std::vector<std::string> locales_entries = get_locales_in_png("installing_text");
|
||||
if (locales_entries.empty()) {
|
||||
Print("Failed to load locales from the resource files\n");
|
||||
return;
|
||||
}
|
||||
std::string saved_locale = locale_;
|
||||
size_t selected = 0;
|
||||
SelectAndShowBackgroundText(locales_entries, selected);
|
||||
|
||||
@@ -467,20 +468,22 @@ int ScreenRecoveryUI::DrawTextLines(int x, int y, const std::vector<std::string>
|
||||
|
||||
int ScreenRecoveryUI::DrawWrappedTextLines(int x, int y,
|
||||
const std::vector<std::string>& lines) const {
|
||||
// Keep symmetrical margins based on the given offset (i.e. x).
|
||||
size_t text_cols = (ScreenWidth() - x * 2) / char_width_;
|
||||
int offset = 0;
|
||||
for (const auto& line : lines) {
|
||||
size_t next_start = 0;
|
||||
while (next_start < line.size()) {
|
||||
std::string sub = line.substr(next_start, text_cols_ + 1);
|
||||
if (sub.size() <= text_cols_) {
|
||||
std::string sub = line.substr(next_start, text_cols + 1);
|
||||
if (sub.size() <= text_cols) {
|
||||
next_start += sub.size();
|
||||
} else {
|
||||
// Line too long and must be wrapped to text_cols_ columns.
|
||||
// Line too long and must be wrapped to text_cols columns.
|
||||
size_t last_space = sub.find_last_of(" \t\n");
|
||||
if (last_space == std::string::npos) {
|
||||
// No space found, just draw as much as we can.
|
||||
sub.resize(text_cols_);
|
||||
next_start += text_cols_;
|
||||
sub.resize(text_cols);
|
||||
next_start += text_cols;
|
||||
} else {
|
||||
sub.resize(last_space);
|
||||
next_start += last_space + 1;
|
||||
@@ -748,6 +751,10 @@ bool ScreenRecoveryUI::Init(const std::string& locale) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string ScreenRecoveryUI::GetLocale() const {
|
||||
return locale_;
|
||||
}
|
||||
|
||||
void ScreenRecoveryUI::LoadAnimation() {
|
||||
std::unique_ptr<DIR, decltype(&closedir)> dir(opendir("/res/images"), closedir);
|
||||
dirent* de;
|
||||
|
||||
+7
-5
@@ -114,6 +114,7 @@ class ScreenRecoveryUI : public RecoveryUI {
|
||||
explicit ScreenRecoveryUI(bool scrollable_menu);
|
||||
|
||||
bool Init(const std::string& locale) override;
|
||||
std::string GetLocale() const override;
|
||||
|
||||
// overall recovery state ("background image")
|
||||
void SetBackground(Icon icon) override;
|
||||
@@ -147,9 +148,9 @@ class ScreenRecoveryUI : public RecoveryUI {
|
||||
|
||||
void SetColor(UIElement e) const;
|
||||
|
||||
// Check the background text image. Use volume up/down button to cycle through the locales
|
||||
// embedded in the png file, and power button to go back to recovery main menu.
|
||||
void CheckBackgroundTextImages(const std::string& saved_locale);
|
||||
// Checks the background text image, for debugging purpose. It iterates the locales embedded in
|
||||
// the on-device resource files and shows the localized text, for manual inspection.
|
||||
void CheckBackgroundTextImages();
|
||||
|
||||
protected:
|
||||
// The margin that we don't want to use for showing texts (e.g. round screen, or screen with
|
||||
@@ -223,8 +224,9 @@ class ScreenRecoveryUI : public RecoveryUI {
|
||||
virtual void DrawTextIcon(int x, int y, GRSurface* surface) const;
|
||||
// Draws multiple text lines. Returns the offset it should be moving along Y-axis.
|
||||
int DrawTextLines(int x, int y, const std::vector<std::string>& lines) const;
|
||||
// Similar to DrawTextLines() to draw multiple text lines, but additionally wraps long lines.
|
||||
// Returns the offset it should be moving along Y-axis.
|
||||
// Similar to DrawTextLines() to draw multiple text lines, but additionally wraps long lines. It
|
||||
// keeps symmetrical margins of 'x' at each end of a line. Returns the offset it should be moving
|
||||
// along Y-axis.
|
||||
int DrawWrappedTextLines(int x, int y, const std::vector<std::string>& lines) const;
|
||||
|
||||
Icon currentIcon;
|
||||
|
||||
@@ -28,6 +28,9 @@ class StubRecoveryUI : public RecoveryUI {
|
||||
public:
|
||||
StubRecoveryUI() = default;
|
||||
|
||||
std::string GetLocale() const override {
|
||||
return "";
|
||||
}
|
||||
void SetBackground(Icon /* icon */) override {}
|
||||
void SetSystemUpdateText(bool /* security_update */) override {}
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ class RecoveryUI {
|
||||
// the given locale. Returns true on success.
|
||||
virtual bool Init(const std::string& locale);
|
||||
|
||||
virtual std::string GetLocale() const = 0;
|
||||
|
||||
// Shows a stage indicator. Called immediately after Init().
|
||||
virtual void SetStage(int current, int max) = 0;
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service android:name=".services.PrepareStreamingService"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -61,6 +61,17 @@ purpose only.
|
||||
6. Push OTA packages to the device.
|
||||
|
||||
|
||||
## Sending HTTP headers from UpdateEngine
|
||||
|
||||
Sometimes OTA package server might require some HTTP headers to be present,
|
||||
e.g. `Authorization` header to contain valid auth token. While performing
|
||||
streaming update, `UpdateEngine` allows passing on certain HTTP headers;
|
||||
as of writing this sample app, these headers are `Authorization` and `User-Agent`.
|
||||
|
||||
`android.os.UpdateEngine#applyPayload` contains information on
|
||||
which HTTP headers are supported.
|
||||
|
||||
|
||||
## Development
|
||||
|
||||
- [x] Create a UI with list of configs, current version,
|
||||
@@ -69,13 +80,14 @@ purpose only.
|
||||
update zip file
|
||||
- [x] Add `UpdateConfig` for working with json config files
|
||||
- [x] Add applying non-streaming update
|
||||
- [ ] Prepare streaming update (partially downloading package)
|
||||
- [ ] Add applying streaming update
|
||||
- [x] Prepare streaming update (partially downloading package)
|
||||
- [x] Add applying streaming update
|
||||
- [x] Add stop/reset the update
|
||||
- [x] Add demo for passing HTTP headers to `UpdateEngine#applyPayload`
|
||||
- [x] [Package compatibility check](https://source.android.com/devices/architecture/vintf/match-rules)
|
||||
- [ ] Add tests for `MainActivity`
|
||||
- [ ] Add stop/reset the update
|
||||
- [ ] Verify system partition checksum for package
|
||||
- [ ] HAL compatibility check
|
||||
- [ ] Change partition demo
|
||||
- [ ] Verify system partition checksum for package
|
||||
- [ ] Add non-A/B updates demo
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"__name": "name will be visible on UI",
|
||||
"__url": "https:// or file:// uri to update package (zip, xz, ...)",
|
||||
"__type": "NON_STREAMING (from a local file) OR STREAMING (on the fly)",
|
||||
"__ab_install_type": "NON_STREAMING (from a local file) OR STREAMING (on the fly)",
|
||||
"name": "SAMPLE-cake-release BUILD-12345",
|
||||
"url": "http://foo.bar/builds/ota-001.zip",
|
||||
"ab_install_type": "NON_STREAMING",
|
||||
"ab_streaming_metadata": {
|
||||
"__": "streaming_metadata is required only for streaming update",
|
||||
"__property_files": "name, offset and size of files",
|
||||
"__authorization": "it will be sent to OTA package server as value of HTTP header - Authorization",
|
||||
"property_files": [
|
||||
{
|
||||
"__filename": "name of the file in package",
|
||||
@@ -17,6 +18,7 @@
|
||||
"offset": 531,
|
||||
"size": 5012323
|
||||
}
|
||||
]
|
||||
],
|
||||
"authorization": "Basic my-secret-token"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+249
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -3,30 +3,35 @@
|
||||
"ab_install_type": "STREAMING",
|
||||
"ab_streaming_metadata": {
|
||||
"property_files": [
|
||||
{
|
||||
"filename": "payload_metadata.bin",
|
||||
"offset": 41,
|
||||
"size": 827
|
||||
},
|
||||
{
|
||||
"filename": "payload.bin",
|
||||
"offset": 41,
|
||||
"size": 7
|
||||
"size": 1392
|
||||
},
|
||||
{
|
||||
"filename": "payload_properties.txt",
|
||||
"offset": 100,
|
||||
"size": 18
|
||||
"offset": 1485,
|
||||
"size": 147
|
||||
},
|
||||
{
|
||||
"filename": "care_map.txt",
|
||||
"offset": 160,
|
||||
"size": 8
|
||||
"offset": 1674,
|
||||
"size": 12
|
||||
},
|
||||
{
|
||||
"filename": "compatibility.zip",
|
||||
"offset": 215,
|
||||
"size": 13
|
||||
"offset": 1733,
|
||||
"size": 17
|
||||
},
|
||||
{
|
||||
"filename": "metadata",
|
||||
"offset": 287,
|
||||
"size": 8
|
||||
"offset": 1809,
|
||||
"size": 29
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+3
-3
@@ -16,7 +16,7 @@
|
||||
|
||||
package com.example.android.systemupdatersample.util;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
@@ -70,11 +70,11 @@ public class FileDownloaderTest {
|
||||
.toFile();
|
||||
Files.deleteIfExists(outFile.toPath());
|
||||
// download a chunk of ota.zip
|
||||
FileDownloader downloader = new FileDownloader(url, 160, 8, outFile);
|
||||
FileDownloader downloader = new FileDownloader(url, 1674, 12, outFile);
|
||||
downloader.download();
|
||||
String downloadedContent = String.join("\n", Files.readAllLines(outFile.toPath()));
|
||||
// archive contains text files with uppercase filenames
|
||||
assertEquals("CARE_MAP", downloadedContent);
|
||||
assertEquals("CARE_MAP-TXT", downloadedContent);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
-40
@@ -17,8 +17,6 @@
|
||||
package com.example.android.systemupdatersample.util;
|
||||
|
||||
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 static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -29,6 +27,7 @@ import android.support.test.filters.SmallTest;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.example.android.systemupdatersample.PayloadSpec;
|
||||
import com.example.android.systemupdatersample.tests.R;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.io.Files;
|
||||
|
||||
@@ -39,12 +38,8 @@ import org.junit.rules.ExpectedException;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* Tests if PayloadSpecs parses update package zip file correctly.
|
||||
@@ -54,12 +49,11 @@ import java.util.zip.ZipOutputStream;
|
||||
public class PayloadSpecsTest {
|
||||
|
||||
private static final String PROPERTIES_CONTENTS = "k1=val1\nkey2=val2";
|
||||
private static final String PAYLOAD_CONTENTS = "hello\nworld";
|
||||
private static final int PAYLOAD_SIZE = PAYLOAD_CONTENTS.length();
|
||||
|
||||
private File mTestDir;
|
||||
|
||||
private Context mTargetContext;
|
||||
private Context mTestContext;
|
||||
|
||||
@Rule
|
||||
public final ExpectedException thrown = ExpectedException.none();
|
||||
@@ -67,21 +61,30 @@ public class PayloadSpecsTest {
|
||||
@Before
|
||||
public void setUp() {
|
||||
mTargetContext = InstrumentationRegistry.getTargetContext();
|
||||
mTestContext = InstrumentationRegistry.getContext();
|
||||
|
||||
mTestDir = mTargetContext.getFilesDir();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forNonStreaming_works() throws Exception {
|
||||
File packageFile = createMockZipFile();
|
||||
// Prepare the target file
|
||||
File packageFile = Paths
|
||||
.get(mTargetContext.getCacheDir().getAbsolutePath(), "ota.zip")
|
||||
.toFile();
|
||||
java.nio.file.Files.deleteIfExists(packageFile.toPath());
|
||||
java.nio.file.Files.copy(mTestContext.getResources().openRawResource(R.raw.ota_002_package),
|
||||
packageFile.toPath());
|
||||
PayloadSpec spec = PayloadSpecs.forNonStreaming(packageFile);
|
||||
|
||||
assertEquals("correct url", "file://" + packageFile.getAbsolutePath(), spec.getUrl());
|
||||
assertEquals("correct payload offset",
|
||||
30 + PAYLOAD_BINARY_FILE_NAME.length(), spec.getOffset());
|
||||
assertEquals("correct payload size", PAYLOAD_SIZE, spec.getSize());
|
||||
assertArrayEquals("correct properties",
|
||||
new String[]{"k1=val1", "key2=val2"}, spec.getProperties().toArray(new String[0]));
|
||||
assertEquals("correct payload size", 1392, spec.getSize());
|
||||
assertEquals(4, spec.getProperties().size());
|
||||
assertEquals(
|
||||
"FILE_HASH=sEAK/NMbU7GGe01xt55FsPafIPk8IYyBOAd6SiDpiMs=",
|
||||
spec.getProperties().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -105,33 +108,6 @@ public class PayloadSpecsTest {
|
||||
new String[]{"k1=val1", "key2=val2"}, spec.getProperties().toArray(new String[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates package zip file that contains payload.bin and payload_properties.txt
|
||||
*/
|
||||
private File createMockZipFile() throws IOException {
|
||||
File testFile = new File(mTestDir, "test.zip");
|
||||
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(testFile))) {
|
||||
// Add payload.bin entry.
|
||||
ZipEntry entry = new ZipEntry(PAYLOAD_BINARY_FILE_NAME);
|
||||
entry.setMethod(ZipEntry.STORED);
|
||||
entry.setCompressedSize(PAYLOAD_SIZE);
|
||||
entry.setSize(PAYLOAD_SIZE);
|
||||
CRC32 crc = new CRC32();
|
||||
crc.update(PAYLOAD_CONTENTS.getBytes(StandardCharsets.UTF_8));
|
||||
entry.setCrc(crc.getValue());
|
||||
zos.putNextEntry(entry);
|
||||
zos.write(PAYLOAD_CONTENTS.getBytes(StandardCharsets.UTF_8));
|
||||
zos.closeEntry();
|
||||
|
||||
// Add payload properties entry.
|
||||
ZipEntry propertiesEntry = new ZipEntry(PAYLOAD_PROPERTIES_FILE_NAME);
|
||||
zos.putNextEntry(propertiesEntry);
|
||||
zos.write(PROPERTIES_CONTENTS.getBytes(StandardCharsets.UTF_8));
|
||||
zos.closeEntry();
|
||||
}
|
||||
return testFile;
|
||||
}
|
||||
|
||||
private File createMockPropertiesFile() throws IOException {
|
||||
File propertiesFile = new File(mTestDir, PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME);
|
||||
Files.asCharSink(propertiesFile, Charsets.UTF_8).write(PROPERTIES_CONTENTS);
|
||||
|
||||
@@ -17,11 +17,13 @@
|
||||
"""
|
||||
Given a OTA package file, produces update config JSON file.
|
||||
|
||||
Example: tools/gen_update_config.py \\
|
||||
--ab_install_type=STREAMING \\
|
||||
ota-build-001.zip \\
|
||||
my-config-001.json \\
|
||||
http://foo.bar/ota-builds/ota-build-001.zip
|
||||
Example:
|
||||
$ PYTHONPATH=$ANDROID_BUILD_TOP/build/make/tools/releasetools:$PYTHONPATH \\
|
||||
bootable/recovery/updater_sample/tools/gen_update_config.py \\
|
||||
--ab_install_type=STREAMING \\
|
||||
ota-build-001.zip \\
|
||||
my-config-001.json \\
|
||||
http://foo.bar/ota-builds/ota-build-001.zip
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -30,6 +32,8 @@ import os.path
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
import ota_from_target_files # pylint: disable=import-error
|
||||
|
||||
|
||||
class GenUpdateConfig(object):
|
||||
"""
|
||||
@@ -41,7 +45,6 @@ class GenUpdateConfig(object):
|
||||
|
||||
AB_INSTALL_TYPE_STREAMING = 'STREAMING'
|
||||
AB_INSTALL_TYPE_NON_STREAMING = 'NON_STREAMING'
|
||||
METADATA_NAME = 'META-INF/com/android/metadata'
|
||||
|
||||
def __init__(self, package, url, ab_install_type):
|
||||
self.package = package
|
||||
@@ -82,37 +85,27 @@ class GenUpdateConfig(object):
|
||||
def _gen_ab_streaming_metadata(self):
|
||||
"""Builds metadata for files required for streaming update."""
|
||||
with zipfile.ZipFile(self.package, 'r') as package_zip:
|
||||
property_files = self._get_property_files(package_zip)
|
||||
|
||||
metadata = {
|
||||
'property_files': property_files
|
||||
'property_files': self._get_property_files(package_zip)
|
||||
}
|
||||
|
||||
return metadata
|
||||
|
||||
def _get_property_files(self, zip_file):
|
||||
@staticmethod
|
||||
def _get_property_files(package_zip):
|
||||
"""Constructs the property-files list for A/B streaming metadata."""
|
||||
|
||||
def compute_entry_offset_size(name):
|
||||
"""Computes the zip entry offset and size."""
|
||||
info = zip_file.getinfo(name)
|
||||
offset = info.header_offset + len(info.FileHeader())
|
||||
size = info.file_size
|
||||
return {
|
||||
'filename': os.path.basename(name),
|
||||
'offset': offset,
|
||||
'size': size,
|
||||
}
|
||||
|
||||
ab_ota = ota_from_target_files.AbOtaPropertyFiles()
|
||||
property_str = ab_ota.GetPropertyFilesString(package_zip, False)
|
||||
property_files = []
|
||||
for entry in self.streaming_required:
|
||||
property_files.append(compute_entry_offset_size(entry))
|
||||
for entry in self.streaming_optional:
|
||||
if entry in zip_file.namelist():
|
||||
property_files.append(compute_entry_offset_size(entry))
|
||||
|
||||
# 'META-INF/com/android/metadata' is required
|
||||
property_files.append(compute_entry_offset_size(GenUpdateConfig.METADATA_NAME))
|
||||
for file in property_str.split(','):
|
||||
filename, offset, size = file.split(':')
|
||||
inner_file = {
|
||||
'filename': filename,
|
||||
'offset': int(offset),
|
||||
'size': int(size)
|
||||
}
|
||||
property_files.append(inner_file)
|
||||
|
||||
return property_files
|
||||
|
||||
|
||||
+16
-10
@@ -15,7 +15,11 @@
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Tests gen_update_config.py
|
||||
Tests gen_update_config.py.
|
||||
|
||||
Example:
|
||||
$ PYTHONPATH=$ANDROID_BUILD_TOP/build/make/tools/releasetools:$PYTHONPATH \\
|
||||
python3 -m unittest test_gen_update_config
|
||||
"""
|
||||
|
||||
import os.path
|
||||
@@ -29,15 +33,21 @@ class GenUpdateConfigTest(unittest.TestCase): # pylint: disable=missing-docstrin
|
||||
"""tests if streaming property files' offset and size are generated properly"""
|
||||
config, package = self._generate_config()
|
||||
property_files = config['ab_streaming_metadata']['property_files']
|
||||
self.assertEqual(len(property_files), 5)
|
||||
self.assertEqual(len(property_files), 6)
|
||||
with open(package, 'rb') as pkg_file:
|
||||
for prop in property_files:
|
||||
filename, offset, size = prop['filename'], prop['offset'], prop['size']
|
||||
pkg_file.seek(offset)
|
||||
data = pkg_file.read(size).decode('ascii')
|
||||
# data in the archive are just uppercase filenames without extension
|
||||
expected_data = filename.split('.')[0].upper()
|
||||
self.assertEqual(data, expected_data)
|
||||
raw_data = pkg_file.read(size)
|
||||
if filename in ['payload.bin', 'payload_metadata.bin']:
|
||||
pass
|
||||
elif filename == 'payload_properties.txt':
|
||||
pass
|
||||
elif filename == 'metadata':
|
||||
self.assertEqual(raw_data.decode('ascii'), 'META-INF/COM/ANDROID/METADATA')
|
||||
else:
|
||||
expected_data = filename.replace('.', '-').upper()
|
||||
self.assertEqual(raw_data.decode('ascii'), expected_data)
|
||||
|
||||
@staticmethod
|
||||
def _generate_config():
|
||||
@@ -49,7 +59,3 @@ class GenUpdateConfigTest(unittest.TestCase): # pylint: disable=missing-docstrin
|
||||
GenUpdateConfig.AB_INSTALL_TYPE_STREAMING)
|
||||
gen.run()
|
||||
return gen.config, ota_package
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user