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/sample_updater/src/com/example/android/systemupdatersample/updates/AbNonStreamingUpdate.java b/sample_updater/src/com/example/android/systemupdatersample/updates/AbNonStreamingUpdate.java
new file mode 100644
index 00000000..1b91a1ac
--- /dev/null
+++ b/sample_updater/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/sample_updater/src/com/example/android/systemupdatersample/util/PackagePropertyFiles.java b/sample_updater/src/com/example/android/systemupdatersample/util/PackagePropertyFiles.java
new file mode 100644
index 00000000..3988b592
--- /dev/null
+++ b/sample_updater/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/sample_updater/src/com/example/android/systemupdatersample/util/PayloadSpecs.java b/sample_updater/src/com/example/android/systemupdatersample/util/PayloadSpecs.java
new file mode 100644
index 00000000..43c8d75e
--- /dev/null
+++ b/sample_updater/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.
+ *
+ * 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 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 "";
+ }
+
+ private PayloadSpecs() {}
+
+}
diff --git a/sample_updater/src/com/example/android/systemupdatersample/util/UpdateConfigs.java b/sample_updater/src/com/example/android/systemupdatersample/util/UpdateConfigs.java
new file mode 100644
index 00000000..089f8b2f
--- /dev/null
+++ b/sample_updater/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 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 getUpdateConfigs(Context context) {
+ File root = new File(getConfigsRoot(context));
+ ArrayList 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/sample_updater/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java b/sample_updater/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java
new file mode 100644
index 00000000..e63da629
--- /dev/null
+++ b/sample_updater/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 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 SUCCEEDED_COMPLETION_CODES = new HashSet(
+ 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/sample_updater/src/com/example/android/systemupdatersample/util/UpdateEngineStatuses.java b/sample_updater/src/com/example/android/systemupdatersample/util/UpdateEngineStatuses.java
new file mode 100644
index 00000000..6203b201
--- /dev/null
+++ b/sample_updater/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 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() {}
+}
diff --git a/sample_updater/tests/Android.mk b/sample_updater/tests/Android.mk
new file mode 100644
index 00000000..1ec68b9f
--- /dev/null
+++ b/sample_updater/tests/Android.mk
@@ -0,0 +1,32 @@
+#
+# 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := SystemUpdaterSampleTests
+LOCAL_SDK_VERSION := system_current
+LOCAL_MODULE_TAGS := tests
+LOCAL_JAVA_LIBRARIES := \
+ android.test.runner \
+ android.test.base
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
+LOCAL_INSTRUMENTATION_FOR := SystemUpdaterSample
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+include $(BUILD_PACKAGE)
diff --git a/sample_updater/tests/AndroidManifest.xml b/sample_updater/tests/AndroidManifest.xml
new file mode 100644
index 00000000..145576cc
--- /dev/null
+++ b/sample_updater/tests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sample_updater/tests/build.properties b/sample_updater/tests/build.properties
new file mode 100644
index 00000000..e0c39def
--- /dev/null
+++ b/sample_updater/tests/build.properties
@@ -0,0 +1 @@
+tested.project.dir=..
diff --git a/sample_updater/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java b/sample_updater/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java
new file mode 100644
index 00000000..87153715
--- /dev/null
+++ b/sample_updater/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link UpdateConfig}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class UpdateConfigTest {
+
+ private static final String JSON_NON_STREAMING =
+ "{\"name\": \"vip update\", \"url\": \"file:///builds/a.zip\", "
+ + " \"type\": \"NON_STREAMING\"}";
+
+ private static final String JSON_STREAMING =
+ "{\"name\": \"vip update 2\", \"url\": \"http://foo.bar/a.zip\", "
+ + "\"type\": \"STREAMING\"}";
+
+ @Rule
+ public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void fromJson_parsesJsonConfigWithoutMetadata() throws Exception {
+ UpdateConfig config = UpdateConfig.fromJson(JSON_NON_STREAMING);
+ assertEquals("name is parsed", "vip update", config.getName());
+ assertEquals("stores raw json", JSON_NON_STREAMING, config.getRawJson());
+ assertSame("type is parsed", UpdateConfig.TYPE_NON_STREAMING, config.getInstallType());
+ assertEquals("url is parsed", "file:///builds/a.zip", config.getUrl());
+ }
+
+ @Test
+ public void getUpdatePackageFile_throwsErrorIfStreaming() throws Exception {
+ UpdateConfig config = UpdateConfig.fromJson(JSON_STREAMING);
+ thrown.expect(RuntimeException.class);
+ config.getUpdatePackageFile();
+ }
+
+ @Test
+ public void getUpdatePackageFile_throwsErrorIfNotAFile() throws Exception {
+ String json = "{\"name\": \"upd\", \"url\": \"http://foo.bar\","
+ + " \"type\": \"NON_STREAMING\"}";
+ UpdateConfig config = UpdateConfig.fromJson(json);
+ thrown.expect(RuntimeException.class);
+ config.getUpdatePackageFile();
+ }
+
+ @Test
+ public void getUpdatePackageFile_works() throws Exception {
+ UpdateConfig c = UpdateConfig.fromJson(JSON_NON_STREAMING);
+ assertEquals("correct path", "/builds/a.zip", c.getUpdatePackageFile().getAbsolutePath());
+ }
+
+}
diff --git a/sample_updater/tests/src/com/example/android/systemupdatersample/ui/MainActivityTest.java b/sample_updater/tests/src/com/example/android/systemupdatersample/ui/MainActivityTest.java
new file mode 100644
index 00000000..01014168
--- /dev/null
+++ b/sample_updater/tests/src/com/example/android/systemupdatersample/ui/MainActivityTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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 static org.junit.Assert.assertNotNull;
+
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Make sure that the main launcher activity opens up properly, which will be
+ * verified by {@link #activityLaunches}.
+ */
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class MainActivityTest {
+
+ @Rule
+ public final ActivityTestRule mActivityRule =
+ new ActivityTestRule<>(MainActivity.class);
+
+ /**
+ * Verifies that the activity under test can be launched.
+ */
+ @Test
+ public void activityLaunches() {
+ assertNotNull(mActivityRule.getActivity());
+ }
+}
diff --git a/sample_updater/tests/src/com/example/android/systemupdatersample/util/PayloadSpecsTest.java b/sample_updater/tests/src/com/example/android/systemupdatersample/util/PayloadSpecsTest.java
new file mode 100644
index 00000000..6f06ca3e
--- /dev/null
+++ b/sample_updater/tests/src/com/example/android/systemupdatersample/util/PayloadSpecsTest.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 static com.example.android.systemupdatersample.util.PackagePropertyFiles.PAYLOAD_BINARY_FILE_NAME;
+import static com.example.android.systemupdatersample.util.PackagePropertyFiles.PAYLOAD_PROPERTIES_FILE_NAME;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.example.android.systemupdatersample.PayloadSpec;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+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;
+
+/**
+ * Tests if PayloadSpecs parses update package zip file correctly.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+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 mContext;
+
+ @Rule
+ public final ExpectedException thrown = ExpectedException.none();
+
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getTargetContext();
+
+ mTestDir = mContext.getFilesDir();
+ }
+
+ @Test
+ public void forNonStreaming_works() throws Exception {
+ File packageFile = createMockZipFile();
+ 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]));
+ }
+
+ @Test
+ public void forNonStreaming_IOException() throws Exception {
+ thrown.expect(IOException.class);
+ PayloadSpecs.forNonStreaming(new File("/fake/news.zip"));
+ }
+
+ /**
+ * 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;
+ }
+
+}
diff --git a/sample_updater/tests/src/com/example/android/systemupdatersample/util/UpdateConfigsTest.java b/sample_updater/tests/src/com/example/android/systemupdatersample/util/UpdateConfigsTest.java
new file mode 100644
index 00000000..4aa8c645
--- /dev/null
+++ b/sample_updater/tests/src/com/example/android/systemupdatersample/util/UpdateConfigsTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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 static org.junit.Assert.assertArrayEquals;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.example.android.systemupdatersample.UpdateConfig;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Tests for {@link UpdateConfigs}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class UpdateConfigsTest {
+
+ private Context mContext;
+
+ @Rule
+ public final ExpectedException thrown = ExpectedException.none();
+
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getTargetContext();
+ }
+
+ @Test
+ public void configsToNames_extractsNames() {
+ List configs = Arrays.asList(
+ new UpdateConfig("blah", "http://", UpdateConfig.TYPE_NON_STREAMING),
+ new UpdateConfig("blah 2", "http://", UpdateConfig.TYPE_STREAMING)
+ );
+ String[] names = UpdateConfigs.configsToNames(configs);
+ assertArrayEquals(new String[] {"blah", "blah 2"}, names);
+ }
+}