Merge "updater_sample: add streaming support"
am: c23a53c272

Change-Id: I4aa8a41a4dd3afd4b1c5fb3d6955abea45c16fc4
diff --git a/updater_sample/AndroidManifest.xml b/updater_sample/AndroidManifest.xml
index 5bbb21c..4b44484 100644
--- a/updater_sample/AndroidManifest.xml
+++ b/updater_sample/AndroidManifest.xml
@@ -31,6 +31,7 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+        <service android:name=".services.PrepareStreamingService"/>
     </application>
 
 </manifest>
diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
index 23510e4..1851724 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
@@ -69,11 +69,11 @@
         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"));
@@ -176,17 +176,17 @@
         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];
+            mPropertyFiles = new PackageFile[0];
         }
 
-        public StreamingMetadata(InnerFile[] propertyFiles) {
+        public StreamingMetadata(PackageFile[] propertyFiles) {
             this.mPropertyFiles = propertyFiles;
         }
 
-        public InnerFile[] getPropertyFiles() {
+        public PackageFile[] getPropertyFiles() {
             return mPropertyFiles;
         }
     }
@@ -194,7 +194,7 @@
     /**
      * 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 +207,7 @@
         /** 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;
diff --git a/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java
new file mode 100644
index 0000000..840a6d6
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.services;
+
+import static com.example.android.systemupdatersample.util.PackageFiles.OTA_PACKAGE_DIR;
+import static com.example.android.systemupdatersample.util.PackageFiles.PAYLOAD_BINARY_FILE_NAME;
+import static com.example.android.systemupdatersample.util.PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME;
+
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import com.example.android.systemupdatersample.PayloadSpec;
+import com.example.android.systemupdatersample.UpdateConfig;
+import com.example.android.systemupdatersample.util.FileDownloader;
+import com.example.android.systemupdatersample.util.PackageFiles;
+import com.example.android.systemupdatersample.util.PayloadSpecs;
+import com.example.android.systemupdatersample.util.UpdateConfigs;
+import com.google.common.collect.ImmutableSet;
+
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.Optional;
+
+/**
+ * This IntentService will download/extract the necessary files from the package zip
+ * without downloading the whole package. And it constructs {@link PayloadSpec}.
+ * All this work required to install streaming A/B updates.
+ *
+ * PrepareStreamingService runs on it's own thread. It will notify activity
+ * using interface {@link UpdateResultCallback} when update is ready to install.
+ */
+public class PrepareStreamingService extends IntentService {
+
+    /**
+     * UpdateResultCallback result codes.
+     */
+    public static final int RESULT_CODE_SUCCESS = 0;
+    public static final int RESULT_CODE_ERROR = 1;
+
+    /**
+     * This interface is used to send results from {@link PrepareStreamingService} to
+     * {@code MainActivity}.
+     */
+    public interface UpdateResultCallback {
+
+        /**
+         * Invoked when files are downloaded and payload spec is constructed.
+         *
+         * @param resultCode result code, values are defined in {@link PrepareStreamingService}
+         * @param payloadSpec prepared payload spec for streaming update
+         */
+        void onReceiveResult(int resultCode, PayloadSpec payloadSpec);
+    }
+
+    /**
+     * Starts PrepareStreamingService.
+     *
+     * @param context application context
+     * @param config update config
+     * @param resultCallback callback that will be called when the update is ready to be installed
+     */
+    public static void startService(Context context,
+            UpdateConfig config,
+            UpdateResultCallback resultCallback) {
+        Log.d(TAG, "Starting PrepareStreamingService");
+        ResultReceiver receiver = new CallbackResultReceiver(new Handler(), resultCallback);
+        Intent intent = new Intent(context, PrepareStreamingService.class);
+        intent.putExtra(EXTRA_PARAM_CONFIG, config);
+        intent.putExtra(EXTRA_PARAM_RESULT_RECEIVER, receiver);
+        context.startService(intent);
+    }
+
+    public PrepareStreamingService() {
+        super(TAG);
+    }
+
+    private static final String TAG = "PrepareStreamingService";
+
+    /**
+     * Extra params that will be sent from Activity to IntentService.
+     */
+    private static final String EXTRA_PARAM_CONFIG = "config";
+    private static final String EXTRA_PARAM_RESULT_RECEIVER = "result-receiver";
+
+    /**
+     * The files that should be downloaded before streaming.
+     */
+    private static final ImmutableSet<String> PRE_STREAMING_FILES_SET =
+            ImmutableSet.of(
+                PackageFiles.CARE_MAP_FILE_NAME,
+                PackageFiles.COMPATIBILITY_ZIP_FILE_NAME,
+                PackageFiles.METADATA_FILE_NAME,
+                PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME
+            );
+
+    @Override
+    protected void onHandleIntent(Intent intent) {
+        Log.d(TAG, "On handle intent is called");
+        UpdateConfig config = intent.getParcelableExtra(EXTRA_PARAM_CONFIG);
+        ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_PARAM_RESULT_RECEIVER);
+
+        try {
+            downloadPreStreamingFiles(config, OTA_PACKAGE_DIR);
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to download pre-streaming files", e);
+            resultReceiver.send(RESULT_CODE_ERROR, null);
+            return;
+        }
+
+        Optional<UpdateConfig.PackageFile> payloadBinary =
+                UpdateConfigs.getPropertyFile(PAYLOAD_BINARY_FILE_NAME, config);
+
+        if (!payloadBinary.isPresent()) {
+            Log.e(TAG, "Failed to find " + PAYLOAD_BINARY_FILE_NAME + " in config");
+            resultReceiver.send(RESULT_CODE_ERROR, null);
+            return;
+        }
+
+        Optional<UpdateConfig.PackageFile> properties =
+                UpdateConfigs.getPropertyFile(PAYLOAD_PROPERTIES_FILE_NAME, config);
+
+        if (!properties.isPresent()) {
+            Log.e(TAG, "Failed to find " + PAYLOAD_PROPERTIES_FILE_NAME + " in config");
+            resultReceiver.send(RESULT_CODE_ERROR, null);
+            return;
+        }
+
+        PayloadSpec spec;
+        try {
+            spec = PayloadSpecs.forStreaming(config.getUrl(),
+                    payloadBinary.get().getOffset(),
+                    payloadBinary.get().getSize(),
+                    Paths.get(OTA_PACKAGE_DIR, properties.get().getFilename()).toFile()
+            );
+        } catch (IOException e) {
+            Log.e(TAG, "PayloadSpecs failed to create PayloadSpec for streaming", e);
+            resultReceiver.send(RESULT_CODE_ERROR, null);
+            return;
+        }
+
+        resultReceiver.send(RESULT_CODE_SUCCESS, CallbackResultReceiver.createBundle(spec));
+    }
+
+    /**
+     * Downloads files defined in {@link UpdateConfig#getStreamingMetadata()}
+     * and exists in {@code PRE_STREAMING_FILES_SET}, and put them
+     * in directory {@code dir}.
+     * @throws IOException when can't download a file
+     */
+    private static void downloadPreStreamingFiles(UpdateConfig config, String dir)
+            throws IOException {
+        Log.d(TAG, "Downloading files to " + dir);
+        for (UpdateConfig.PackageFile file : config.getStreamingMetadata().getPropertyFiles()) {
+            if (PRE_STREAMING_FILES_SET.contains(file.getFilename())) {
+                Log.d(TAG, "Downloading file " + file.getFilename());
+                FileDownloader downloader = new FileDownloader(
+                        config.getUrl(),
+                        file.getOffset(),
+                        file.getSize(),
+                        Paths.get(dir, file.getFilename()).toFile());
+                downloader.download();
+            }
+        }
+    }
+
+    /**
+     * Used by {@link PrepareStreamingService} to pass {@link PayloadSpec}
+     * to {@link UpdateResultCallback#onReceiveResult}.
+     */
+    private static class CallbackResultReceiver extends ResultReceiver {
+
+        static Bundle createBundle(PayloadSpec payloadSpec) {
+            Bundle b = new Bundle();
+            b.putSerializable(BUNDLE_PARAM_PAYLOAD_SPEC, payloadSpec);
+            return b;
+        }
+
+        private static final String BUNDLE_PARAM_PAYLOAD_SPEC = "payload-spec";
+
+        private UpdateResultCallback mUpdateResultCallback;
+
+        CallbackResultReceiver(Handler handler, UpdateResultCallback updateResultCallback) {
+            super(handler);
+            this.mUpdateResultCallback = updateResultCallback;
+        }
+
+        @Override
+        protected void onReceiveResult(int resultCode, Bundle resultData) {
+            PayloadSpec payloadSpec = null;
+            if (resultCode == RESULT_CODE_SUCCESS) {
+                payloadSpec = (PayloadSpec) resultData.getSerializable(BUNDLE_PARAM_PAYLOAD_SPEC);
+            }
+            mUpdateResultCallback.onReceiveResult(resultCode, payloadSpec);
+        }
+    }
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
index d6a6ce3..359e2b1 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
@@ -34,6 +34,7 @@
 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;
@@ -297,6 +298,17 @@
             updateEngineApplyPayload(payload);
         } else {
             Log.d(TAG, "Starting PrepareStreamingService");
+            PrepareStreamingService.startService(this, config, (code, payloadSpec) -> {
+                if (code == PrepareStreamingService.RESULT_CODE_SUCCESS) {
+                    updateEngineApplyPayload(payloadSpec);
+                } else {
+                    Log.e(TAG, "PrepareStreamingService failed, result code is " + code);
+                    Toast.makeText(
+                            MainActivity.this,
+                            "PrepareStreamingService failed, result code is " + code,
+                            Toast.LENGTH_LONG).show();
+                }
+            });
         }
     }
 
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java
index 5c1d711..ddd0919 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java
@@ -38,22 +38,23 @@
     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 @@
 
         // 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 "
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java
index 71d4df8..5080cb6 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java
@@ -26,14 +26,16 @@
 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 @@
      * @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 @@
         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() {}
 }