summaryrefslogtreecommitdiffstats
path: root/updater_sample/src/com/example/android
diff options
context:
space:
mode:
Diffstat (limited to 'updater_sample/src/com/example/android')
-rw-r--r--updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java122
-rw-r--r--updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java183
-rw-r--r--updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java314
-rw-r--r--updater_sample/src/com/example/android/systemupdatersample/updates/AbNonStreamingUpdate.java52
-rw-r--r--updater_sample/src/com/example/android/systemupdatersample/util/PackagePropertyFiles.java42
-rw-r--r--updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java117
-rw-r--r--updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java82
-rw-r--r--updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java84
-rw-r--r--updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineStatuses.java51
9 files changed, 1047 insertions, 0 deletions
diff --git a/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java b/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java
new file mode 100644
index 000000000..90c5637ea
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample;
+
+import android.os.UpdateEngine;
+
+import java.util.List;
+
+/**
+ * Payload that will be given to {@link UpdateEngine#applyPayload)}.
+ */
+public class PayloadSpec {
+
+ /**
+ * Creates a payload spec {@link Builder}
+ */
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ private String mUrl;
+ private long mOffset;
+ private long mSize;
+ private List<String> mProperties;
+
+ public PayloadSpec(Builder b) {
+ this.mUrl = b.mUrl;
+ this.mOffset = b.mOffset;
+ this.mSize = b.mSize;
+ this.mProperties = b.mProperties;
+ }
+
+ public String getUrl() {
+ return mUrl;
+ }
+
+ public long getOffset() {
+ return mOffset;
+ }
+
+ public long getSize() {
+ return mSize;
+ }
+
+ public List<String> getProperties() {
+ return mProperties;
+ }
+
+ /**
+ * payload spec builder.
+ *
+ * <p>Usage:</p>
+ *
+ * {@code
+ * PayloadSpec spec = PayloadSpec.newBuilder()
+ * .url("url")
+ * .build();
+ * }
+ */
+ public static class Builder {
+ private String mUrl;
+ private long mOffset;
+ private long mSize;
+ private List<String> mProperties;
+
+ public Builder() {
+ }
+
+ /**
+ * set url
+ */
+ public Builder url(String url) {
+ this.mUrl = url;
+ return this;
+ }
+
+ /**
+ * set offset
+ */
+ public Builder offset(long offset) {
+ this.mOffset = offset;
+ return this;
+ }
+
+ /**
+ * set size
+ */
+ public Builder size(long size) {
+ this.mSize = size;
+ return this;
+ }
+
+ /**
+ * set properties
+ */
+ public Builder properties(List<String> properties) {
+ this.mProperties = properties;
+ return this;
+ }
+
+ /**
+ * build {@link PayloadSpec}
+ */
+ public PayloadSpec build() {
+ return new PayloadSpec(this);
+ }
+ }
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
new file mode 100644
index 000000000..cbee18fcb
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.Serializable;
+
+/**
+ * UpdateConfig describes an update. It will be parsed from JSON, which is intended to
+ * be sent from server to the update app, but in this sample app it will be stored on the device.
+ */
+public class UpdateConfig implements Parcelable {
+
+ public static final int TYPE_NON_STREAMING = 0;
+ public static final int TYPE_STREAMING = 1;
+
+ public static final Parcelable.Creator<UpdateConfig> CREATOR =
+ new Parcelable.Creator<UpdateConfig>() {
+ @Override
+ public UpdateConfig createFromParcel(Parcel source) {
+ return new UpdateConfig(source);
+ }
+
+ @Override
+ public UpdateConfig[] newArray(int size) {
+ return new UpdateConfig[size];
+ }
+ };
+
+ /** parse update config from json */
+ public static UpdateConfig fromJson(String json) throws JSONException {
+ UpdateConfig c = new UpdateConfig();
+
+ JSONObject o = new JSONObject(json);
+ c.mName = o.getString("name");
+ c.mUrl = o.getString("url");
+ if (TYPE_NON_STREAMING_JSON.equals(o.getString("type"))) {
+ c.mInstallType = TYPE_NON_STREAMING;
+ } else if (TYPE_STREAMING_JSON.equals(o.getString("type"))) {
+ c.mInstallType = TYPE_STREAMING;
+ } else {
+ throw new JSONException("Invalid type, expected either "
+ + "NON_STREAMING or STREAMING, got " + o.getString("type"));
+ }
+ if (o.has("metadata")) {
+ c.mMetadata = new Metadata(
+ o.getJSONObject("metadata").getInt("offset"),
+ o.getJSONObject("metadata").getInt("size"));
+ }
+ c.mRawJson = json;
+ return c;
+ }
+
+ /**
+ * these strings are represent types in JSON config files
+ */
+ private static final String TYPE_NON_STREAMING_JSON = "NON_STREAMING";
+ private static final String TYPE_STREAMING_JSON = "STREAMING";
+
+ /** name will be visible on UI */
+ private String mName;
+
+ /** update zip file URI, can be https:// or file:// */
+ private String mUrl;
+
+ /** non-streaming (first saves locally) OR streaming (on the fly) */
+ private int mInstallType;
+
+ /** metadata is required only for streaming update */
+ private Metadata mMetadata;
+
+ private String mRawJson;
+
+ protected UpdateConfig() {
+ }
+
+ protected UpdateConfig(Parcel in) {
+ this.mName = in.readString();
+ this.mUrl = in.readString();
+ this.mInstallType = in.readInt();
+ this.mMetadata = (Metadata) in.readSerializable();
+ this.mRawJson = in.readString();
+ }
+
+ public UpdateConfig(String name, String url, int installType) {
+ this.mName = name;
+ this.mUrl = url;
+ this.mInstallType = installType;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getUrl() {
+ return mUrl;
+ }
+
+ public String getRawJson() {
+ return mRawJson;
+ }
+
+ public int getInstallType() {
+ return mInstallType;
+ }
+
+ /**
+ * "url" must be the file located on the device.
+ *
+ * @return File object for given url
+ */
+ public File getUpdatePackageFile() {
+ if (mInstallType != TYPE_NON_STREAMING) {
+ throw new RuntimeException("Expected non-streaming install type");
+ }
+ if (!mUrl.startsWith("file://")) {
+ throw new RuntimeException("url is expected to start with file://");
+ }
+ return new File(mUrl.substring(7, mUrl.length()));
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mName);
+ dest.writeString(mUrl);
+ dest.writeInt(mInstallType);
+ dest.writeSerializable(mMetadata);
+ dest.writeString(mRawJson);
+ }
+
+ /**
+ * Metadata for STREAMING update
+ */
+ public static class Metadata implements Serializable {
+
+ private static final long serialVersionUID = 31042L;
+
+ /** defines beginning of update data in archive */
+ private long mOffset;
+
+ /** size of the update data in archive */
+ private long mSize;
+
+ public Metadata(long offset, long size) {
+ this.mOffset = offset;
+ this.mSize = size;
+ }
+
+ public long getOffset() {
+ return mOffset;
+ }
+
+ public long getSize() {
+ return mSize;
+ }
+ }
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
new file mode 100644
index 000000000..72e1b2469
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
@@ -0,0 +1,314 @@
+/*
+ * 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.ui;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.UpdateEngine;
+import android.os.UpdateEngineCallback;
+import android.util.Log;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.example.android.systemupdatersample.R;
+import com.example.android.systemupdatersample.UpdateConfig;
+import com.example.android.systemupdatersample.updates.AbNonStreamingUpdate;
+import com.example.android.systemupdatersample.util.UpdateConfigs;
+import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes;
+import com.example.android.systemupdatersample.util.UpdateEngineStatuses;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * UI for SystemUpdaterSample app.
+ */
+public class MainActivity extends Activity {
+
+ private TextView mTextViewBuild;
+ private Spinner mSpinnerConfigs;
+ private TextView mTextViewConfigsDirHint;
+ private Button mButtonReload;
+ private Button mButtonApplyConfig;
+ private Button mButtonStop;
+ private Button mButtonReset;
+ private ProgressBar mProgressBar;
+ private TextView mTextViewStatus;
+
+ private List<UpdateConfig> mConfigs;
+ private AtomicInteger mUpdateEngineStatus =
+ new AtomicInteger(UpdateEngine.UpdateStatusConstants.IDLE);
+ private UpdateEngine mUpdateEngine = new UpdateEngine();
+
+ /**
+ * Listen to {@code update_engine} events.
+ */
+ private UpdateEngineCallbackImpl mUpdateEngineCallback = new UpdateEngineCallbackImpl();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ this.mTextViewBuild = findViewById(R.id.textViewBuild);
+ this.mSpinnerConfigs = findViewById(R.id.spinnerConfigs);
+ this.mTextViewConfigsDirHint = findViewById(R.id.textViewConfigsDirHint);
+ this.mButtonReload = findViewById(R.id.buttonReload);
+ this.mButtonApplyConfig = findViewById(R.id.buttonApplyConfig);
+ this.mButtonStop = findViewById(R.id.buttonStop);
+ this.mButtonReset = findViewById(R.id.buttonReset);
+ this.mProgressBar = findViewById(R.id.progressBar);
+ this.mTextViewStatus = findViewById(R.id.textViewStatus);
+
+ this.mUpdateEngine.bind(mUpdateEngineCallback);
+
+ this.mTextViewConfigsDirHint.setText(UpdateConfigs.getConfigsRoot(this));
+
+ uiReset();
+
+ loadUpdateConfigs();
+ }
+
+ @Override
+ protected void onDestroy() {
+ this.mUpdateEngine.unbind();
+ super.onDestroy();
+ }
+
+ /**
+ * reload button is clicked
+ */
+ public void onReloadClick(View view) {
+ loadUpdateConfigs();
+ }
+
+ /**
+ * view config button is clicked
+ */
+ public void onViewConfigClick(View view) {
+ UpdateConfig config = mConfigs.get(mSpinnerConfigs.getSelectedItemPosition());
+ new AlertDialog.Builder(this)
+ .setTitle(config.getName())
+ .setMessage(config.getRawJson())
+ .setPositiveButton(R.string.close, (dialog, id) -> dialog.dismiss())
+ .show();
+ }
+
+ /**
+ * apply config button is clicked
+ */
+ public void onApplyConfigClick(View view) {
+ new AlertDialog.Builder(this)
+ .setTitle("Apply Update")
+ .setMessage("Do you really want to apply this update?")
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
+ uiSetUpdating();
+ applyUpdate(getSelectedConfig());
+ })
+ .setNegativeButton(android.R.string.cancel, null)
+ .show();
+ }
+
+ /**
+ * stop button clicked
+ */
+ public void onStopClick(View view) {
+ new AlertDialog.Builder(this)
+ .setTitle("Stop Update")
+ .setMessage("Do you really want to cancel running update?")
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
+ uiReset();
+ stopRunningUpdate();
+ })
+ .setNegativeButton(android.R.string.cancel, null).show();
+ }
+
+ /**
+ * reset button clicked
+ */
+ public void onResetClick(View view) {
+ new AlertDialog.Builder(this)
+ .setTitle("Reset Update")
+ .setMessage("Do you really want to cancel running update"
+ + " and restore old version?")
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
+ uiReset();
+ resetUpdate();
+ })
+ .setNegativeButton(android.R.string.cancel, null).show();
+ }
+
+ /**
+ * Invoked when anything changes. The value of {@code status} will
+ * be one of the values from {@link UpdateEngine.UpdateStatusConstants},
+ * and {@code percent} will be from {@code 0.0} to {@code 1.0}.
+ */
+ private void onStatusUpdate(int status, float percent) {
+ mProgressBar.setProgress((int) (100 * percent));
+ if (mUpdateEngineStatus.get() != status) {
+ mUpdateEngineStatus.set(status);
+ runOnUiThread(() -> {
+ Log.e("UpdateEngine", "StatusUpdate - status="
+ + UpdateEngineStatuses.getStatusText(status)
+ + "/" + status);
+ setUiStatus(status);
+ Toast.makeText(this, "Update Status changed", Toast.LENGTH_LONG)
+ .show();
+ });
+ }
+ }
+
+ /**
+ * Invoked when the payload has been applied, whether successfully or
+ * unsuccessfully. The value of {@code errorCode} will be one of the
+ * values from {@link UpdateEngine.ErrorCodeConstants}.
+ */
+ private void onPayloadApplicationComplete(int errorCode) {
+ runOnUiThread(() -> {
+ final String state = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode)
+ ? "SUCCESS"
+ : "FAILURE";
+ Log.i("UpdateEngine",
+ "Completed - errorCode="
+ + UpdateEngineErrorCodes.getCodeName(errorCode) + "/" + errorCode
+ + " " + state);
+ Toast.makeText(this, "Update completed", Toast.LENGTH_LONG).show();
+ });
+ }
+
+ /** resets ui */
+ private void uiReset() {
+ mTextViewBuild.setText(Build.DISPLAY);
+ mSpinnerConfigs.setEnabled(true);
+ mButtonReload.setEnabled(true);
+ mButtonApplyConfig.setEnabled(true);
+ mButtonStop.setEnabled(false);
+ mButtonReset.setEnabled(false);
+ mProgressBar.setProgress(0);
+ mProgressBar.setEnabled(false);
+ mProgressBar.setVisibility(ProgressBar.INVISIBLE);
+ mTextViewStatus.setText(R.string.unknown);
+ }
+
+ /** sets ui updating mode */
+ private void uiSetUpdating() {
+ mTextViewBuild.setText(Build.DISPLAY);
+ mSpinnerConfigs.setEnabled(false);
+ mButtonReload.setEnabled(false);
+ mButtonApplyConfig.setEnabled(false);
+ mButtonStop.setEnabled(true);
+ mProgressBar.setEnabled(true);
+ mButtonReset.setEnabled(true);
+ mProgressBar.setVisibility(ProgressBar.VISIBLE);
+ }
+
+ /**
+ * loads json configurations from configs dir that is defined in {@link UpdateConfigs}.
+ */
+ private void loadUpdateConfigs() {
+ mConfigs = UpdateConfigs.getUpdateConfigs(this);
+ loadConfigsToSpinner(mConfigs);
+ }
+
+ /**
+ * @param status update engine status code
+ */
+ private void setUiStatus(int status) {
+ String statusText = UpdateEngineStatuses.getStatusText(status);
+ mTextViewStatus.setText(statusText);
+ }
+
+ private void loadConfigsToSpinner(List<UpdateConfig> configs) {
+ String[] spinnerArray = UpdateConfigs.configsToNames(configs);
+ ArrayAdapter<String> spinnerArrayAdapter = new ArrayAdapter<>(this,
+ android.R.layout.simple_spinner_item,
+ spinnerArray);
+ spinnerArrayAdapter.setDropDownViewResource(android.R.layout
+ .simple_spinner_dropdown_item);
+ mSpinnerConfigs.setAdapter(spinnerArrayAdapter);
+ }
+
+ private UpdateConfig getSelectedConfig() {
+ return mConfigs.get(mSpinnerConfigs.getSelectedItemPosition());
+ }
+
+ /**
+ * Applies the given update
+ */
+ private void applyUpdate(UpdateConfig config) {
+ if (config.getInstallType() == UpdateConfig.TYPE_NON_STREAMING) {
+ AbNonStreamingUpdate update = new AbNonStreamingUpdate(mUpdateEngine, config);
+ try {
+ update.execute();
+ } catch (Exception e) {
+ Log.e("MainActivity", "Error applying the update", e);
+ Toast.makeText(this, "Error applying the update", Toast.LENGTH_SHORT)
+ .show();
+ }
+ } else {
+ Toast.makeText(this, "Streaming is not implemented", Toast.LENGTH_SHORT)
+ .show();
+ }
+ }
+
+ /**
+ * Requests update engine to stop any ongoing update. If an update has been applied,
+ * leave it as is.
+ */
+ private void stopRunningUpdate() {
+ Toast.makeText(this,
+ "stopRunningUpdate is not implemented",
+ Toast.LENGTH_SHORT).show();
+
+ }
+
+ /**
+ * Resets update engine to IDLE state. Requests to cancel any onging update, or to revert if an
+ * update has been applied.
+ */
+ private void resetUpdate() {
+ Toast.makeText(this,
+ "resetUpdate is not implemented",
+ Toast.LENGTH_SHORT).show();
+ }
+
+ /**
+ * Helper class to delegate UpdateEngine callbacks to MainActivity
+ */
+ class UpdateEngineCallbackImpl extends UpdateEngineCallback {
+ @Override
+ public void onStatusUpdate(int status, float percent) {
+ MainActivity.this.onStatusUpdate(status, percent);
+ }
+
+ @Override
+ public void onPayloadApplicationComplete(int errorCode) {
+ MainActivity.this.onPayloadApplicationComplete(errorCode);
+ }
+ }
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/updates/AbNonStreamingUpdate.java b/updater_sample/src/com/example/android/systemupdatersample/updates/AbNonStreamingUpdate.java
new file mode 100644
index 000000000..1b91a1ac3
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/updates/AbNonStreamingUpdate.java
@@ -0,0 +1,52 @@
+/*
+ * 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.updates;
+
+import android.os.UpdateEngine;
+
+import com.example.android.systemupdatersample.PayloadSpec;
+import com.example.android.systemupdatersample.UpdateConfig;
+import com.example.android.systemupdatersample.util.PayloadSpecs;
+
+/**
+ * Applies A/B (seamless) non-streaming update.
+ */
+public class AbNonStreamingUpdate {
+
+ private final UpdateEngine mUpdateEngine;
+ private final UpdateConfig mUpdateConfig;
+
+ public AbNonStreamingUpdate(UpdateEngine updateEngine, UpdateConfig config) {
+ this.mUpdateEngine = updateEngine;
+ this.mUpdateConfig = config;
+ }
+
+ /**
+ * Start applying the update. This method doesn't wait until end of the update.
+ * {@code update_engine} works asynchronously.
+ */
+ public void execute() throws Exception {
+ PayloadSpec payload = PayloadSpecs.forNonStreaming(mUpdateConfig.getUpdatePackageFile());
+
+ mUpdateEngine.applyPayload(
+ payload.getUrl(),
+ payload.getOffset(),
+ payload.getSize(),
+ payload.getProperties().toArray(new String[0]));
+ }
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/PackagePropertyFiles.java b/updater_sample/src/com/example/android/systemupdatersample/util/PackagePropertyFiles.java
new file mode 100644
index 000000000..3988b5928
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/PackagePropertyFiles.java
@@ -0,0 +1,42 @@
+/*
+ * 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.util;
+
+/** Utility class for property files in a package. */
+public final class PackagePropertyFiles {
+
+ public static final String PAYLOAD_BINARY_FILE_NAME = "payload.bin";
+
+ public static final String PAYLOAD_HEADER_FILE_NAME = "payload_header.bin";
+
+ public static final String PAYLOAD_METADATA_FILE_NAME = "payload_metadata.bin";
+
+ public static final String PAYLOAD_PROPERTIES_FILE_NAME = "payload_properties.txt";
+
+ /** The zip entry in an A/B OTA package, which will be used by update_verifier. */
+ public static final String CARE_MAP_FILE_NAME = "care_map.txt";
+
+ public static final String METADATA_FILE_NAME = "metadata";
+
+ /**
+ * The zip file that claims the compatibility of the update package to check against the Android
+ * framework to ensure that the package can be installed on the device.
+ */
+ public static final String COMPATIBILITY_ZIP_FILE_NAME = "compatibility.zip";
+
+ private PackagePropertyFiles() {}
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java b/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java
new file mode 100644
index 000000000..43c8d75e2
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java
@@ -0,0 +1,117 @@
+/*
+ * 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.util;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+
+import com.example.android.systemupdatersample.PayloadSpec;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/** The helper class that creates {@link PayloadSpec}. */
+@TargetApi(Build.VERSION_CODES.N)
+public final class PayloadSpecs {
+
+ /**
+ * The payload PAYLOAD_ENTRY is stored in the zip package to comply with the Android OTA package
+ * format. We want to find out the offset of the entry, so that we can pass it over to the A/B
+ * updater without making an extra copy of the payload.
+ *
+ * <p>According to Android docs, the entries are listed in the order in which they appear in the
+ * zip file. So we enumerate the entries to identify the offset of the payload file.
+ * http://developer.android.com/reference/java/util/zip/ZipFile.html#entries()
+ */
+ public static PayloadSpec forNonStreaming(File packageFile) throws IOException {
+ boolean payloadFound = false;
+ long payloadOffset = 0;
+ long payloadSize = 0;
+
+ List<String> properties = new ArrayList<>();
+ try (ZipFile zip = new ZipFile(packageFile)) {
+ Enumeration<? extends ZipEntry> entries = zip.entries();
+ long offset = 0;
+ while (entries.hasMoreElements()) {
+ ZipEntry entry = entries.nextElement();
+ String name = entry.getName();
+ // Zip local file header has 30 bytes + filename + sizeof extra field.
+ // https://en.wikipedia.org/wiki/Zip_(file_format)
+ long extraSize = entry.getExtra() == null ? 0 : entry.getExtra().length;
+ offset += 30 + name.length() + extraSize;
+
+ if (entry.isDirectory()) {
+ continue;
+ }
+
+ long length = entry.getCompressedSize();
+ if (PackagePropertyFiles.PAYLOAD_BINARY_FILE_NAME.equals(name)) {
+ if (entry.getMethod() != ZipEntry.STORED) {
+ throw new IOException("Invalid compression method.");
+ }
+ payloadFound = true;
+ payloadOffset = offset;
+ payloadSize = length;
+ } else if (PackagePropertyFiles.PAYLOAD_PROPERTIES_FILE_NAME.equals(name)) {
+ InputStream inputStream = zip.getInputStream(entry);
+ if (inputStream != null) {
+ BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
+ String line;
+ while ((line = br.readLine()) != null) {
+ properties.add(line);
+ }
+ }
+ }
+ offset += length;
+ }
+ }
+
+ if (!payloadFound) {
+ throw new IOException("Failed to find payload entry in the given package.");
+ }
+ return PayloadSpec.newBuilder()
+ .url("file://" + packageFile.getAbsolutePath())
+ .offset(payloadOffset)
+ .size(payloadSize)
+ .properties(properties)
+ .build();
+ }
+
+ /**
+ * Converts an {@link PayloadSpec} to a string.
+ */
+ public static String toString(PayloadSpec payloadSpec) {
+ return "<PayloadSpec url=" + payloadSpec.getUrl()
+ + ", offset=" + payloadSpec.getOffset()
+ + ", size=" + payloadSpec.getSize()
+ + ", properties=" + Arrays.toString(
+ payloadSpec.getProperties().toArray(new String[0]))
+ + ">";
+ }
+
+ private PayloadSpecs() {}
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java
new file mode 100644
index 000000000..089f8b2f2
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java
@@ -0,0 +1,82 @@
+/*
+ * 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.util;
+
+import android.content.Context;
+
+import com.example.android.systemupdatersample.UpdateConfig;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class for working with json update configurations.
+ */
+public final class UpdateConfigs {
+
+ private static final String UPDATE_CONFIGS_ROOT = "configs/";
+
+ /**
+ * @param configs update configs
+ * @return list of names
+ */
+ public static String[] configsToNames(List<UpdateConfig> configs) {
+ return configs.stream().map(UpdateConfig::getName).toArray(String[]::new);
+ }
+
+ /**
+ * @param context app context
+ * @return configs root directory
+ */
+ public static String getConfigsRoot(Context context) {
+ 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}
+ */
+ public static List<UpdateConfig> getUpdateConfigs(Context context) {
+ File root = new File(getConfigsRoot(context));
+ ArrayList<UpdateConfig> configs = new ArrayList<>();
+ if (!root.exists()) {
+ return configs;
+ }
+ for (final File f : root.listFiles()) {
+ if (!f.isDirectory() && f.getName().endsWith(".json")) {
+ try {
+ String json = new String(Files.readAllBytes(f.toPath()),
+ StandardCharsets.UTF_8);
+ configs.add(UpdateConfig.fromJson(json));
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Can't read/parse config file " + f.getName(), e);
+ }
+ }
+ }
+ return configs;
+ }
+
+ private UpdateConfigs() {}
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java
new file mode 100644
index 000000000..e63da6298
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java
@@ -0,0 +1,84 @@
+/*
+ * 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.util;
+
+import android.os.UpdateEngine;
+import android.util.SparseArray;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Helper class to work with update_engine's error codes.
+ * Many error codes are defined in {@link UpdateEngine.ErrorCodeConstants},
+ * but you can find more in system/update_engine/common/error_code.h.
+ */
+public final class UpdateEngineErrorCodes {
+
+ /**
+ * Error code from the update engine. Values must agree with the ones in
+ * system/update_engine/common/error_code.h.
+ */
+ public static final int UPDATED_BUT_NOT_ACTIVE = 52;
+
+ private static final SparseArray<String> CODE_TO_NAME_MAP = new SparseArray<>();
+
+ static {
+ CODE_TO_NAME_MAP.put(0, "SUCCESS");
+ CODE_TO_NAME_MAP.put(1, "ERROR");
+ CODE_TO_NAME_MAP.put(4, "FILESYSTEM_COPIER_ERROR");
+ CODE_TO_NAME_MAP.put(5, "POST_INSTALL_RUNNER_ERROR");
+ CODE_TO_NAME_MAP.put(6, "PAYLOAD_MISMATCHED_TYPE_ERROR");
+ CODE_TO_NAME_MAP.put(7, "INSTALL_DEVICE_OPEN_ERROR");
+ CODE_TO_NAME_MAP.put(8, "KERNEL_DEVICE_OPEN_ERROR");
+ CODE_TO_NAME_MAP.put(9, "DOWNLOAD_TRANSFER_ERROR");
+ CODE_TO_NAME_MAP.put(10, "PAYLOAD_HASH_MISMATCH_ERROR");
+ CODE_TO_NAME_MAP.put(11, "PAYLOAD_SIZE_MISMATCH_ERROR");
+ CODE_TO_NAME_MAP.put(12, "DOWNLOAD_PAYLOAD_VERIFICATION_ERROR");
+ CODE_TO_NAME_MAP.put(20, "DOWNLOAD_STATE_INITIALIZATION_ERROR");
+ CODE_TO_NAME_MAP.put(48, "USER_CANCELLED");
+ CODE_TO_NAME_MAP.put(52, "UPDATED_BUT_NOT_ACTIVE");
+ }
+
+ /**
+ * Completion codes returned by update engine indicating that the update
+ * was successfully applied.
+ */
+ private static final Set<Integer> SUCCEEDED_COMPLETION_CODES = new HashSet<Integer>(
+ Arrays.asList(UpdateEngine.ErrorCodeConstants.SUCCESS,
+ // UPDATED_BUT_NOT_ACTIVE is returned when the payload is
+ // successfully applied but the
+ // device won't switch to the new slot after the next boot.
+ UPDATED_BUT_NOT_ACTIVE));
+
+ /**
+ * checks if update succeeded using errorCode
+ */
+ public static boolean isUpdateSucceeded(int errorCode) {
+ return SUCCEEDED_COMPLETION_CODES.contains(errorCode);
+ }
+
+ /**
+ * converts error code to error name
+ */
+ public static String getCodeName(int errorCode) {
+ return CODE_TO_NAME_MAP.get(errorCode);
+ }
+
+ private UpdateEngineErrorCodes() {}
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineStatuses.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineStatuses.java
new file mode 100644
index 000000000..6203b201a
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineStatuses.java
@@ -0,0 +1,51 @@
+/*
+ * 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.util;
+
+import android.util.SparseArray;
+
+/**
+ * Helper class to work with update_engine's error codes.
+ * Many error codes are defined in {@link UpdateEngine.UpdateStatusConstants},
+ * but you can find more in system/update_engine/common/error_code.h.
+ */
+public final class UpdateEngineStatuses {
+
+ private static final SparseArray<String> STATUS_MAP = new SparseArray<>();
+
+ static {
+ STATUS_MAP.put(0, "IDLE");
+ STATUS_MAP.put(1, "CHECKING_FOR_UPDATE");
+ STATUS_MAP.put(2, "UPDATE_AVAILABLE");
+ STATUS_MAP.put(3, "DOWNLOADING");
+ STATUS_MAP.put(4, "VERIFYING");
+ STATUS_MAP.put(5, "FINALIZING");
+ STATUS_MAP.put(6, "UPDATED_NEED_REBOOT");
+ STATUS_MAP.put(7, "REPORTING_ERROR_EVENT");
+ STATUS_MAP.put(8, "ATTEMPTING_ROLLBACK");
+ STATUS_MAP.put(9, "DISABLED");
+ }
+
+ /**
+ * converts status code to status name
+ */
+ public static String getStatusText(int status) {
+ return STATUS_MAP.get(status);
+ }
+
+ private UpdateEngineStatuses() {}
+}