sample_updater: add non-streaming demo

SampleUpdater app was tested manually on a device.
There are unit tests for utility classes.

SampleUpdater app demonstrates how to use Android Update Engine to
apply A/B (seamless) update.
This CL contains demo of non-stream update using async update_engine,
which is accessed directly from an activity.
This app also shows logs from update_engine on the UI.
Instructions can be found in `README.md`.

- Create a UI with list of configs, current version, control buttons and a progress bar
- Add PayloadSpec and PayloadSpecs for working with update zip file
- Add UpdateConfig for working with json config files
- Add applying non-streaming update

Test: tested manually and unit tests for utilities
Change-Id: I05d4a46ad9cf8b334c9c60c7dd4da486dac0400a
Signed-off-by: Zhomart Mukhamejanov <zhomart@google.com>
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 0000000..3988b59
--- /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 0000000..43c8d75
--- /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.
+     *
+     * <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/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 0000000..089f8b2
--- /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<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/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 0000000..e63da62
--- /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<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/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 0000000..6203b20
--- /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<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() {}
+}