Merge "updater_sample: update ui and README, clean-up"
diff --git a/updater_sample/README.md b/updater_sample/README.md
index ee1faaf..12f803f 100644
--- a/updater_sample/README.md
+++ b/updater_sample/README.md
@@ -30,13 +30,19 @@
 The directory can be found in logs or on the UI. In most cases it should be located at
 `/data/user/0/com.example.android.systemupdatersample/files/configs/`.
 
-SystemUpdaterSample app downloads OTA package from `url`. If `ab_install_type`
-is `NON_STREAMING` then app downloads the whole package and
-passes it to the `update_engine`. If `ab_install_type` is `STREAMING`
-then app downloads only some files to prepare the streaming update and
-`update_engine` will stream only `payload.bin`.
-To support streaming A/B (seamless) update, OTA package file must be
-an uncompressed (ZIP_STORED) zip file.
+SystemUpdaterSample app downloads OTA package from `url`. In this sample app
+`url` is expected to point to file system, e.g. `file:///data/sample-builds/ota-002.zip`.
+
+If `ab_install_type` is `NON_STREAMING` then app checks if `url` starts
+with `file://` and passes `url` to the `update_engine`.
+
+If `ab_install_type` is `STREAMING`, app downloads only the entries in need, as
+opposed to the entire package, to initiate a streaming update. The `payload.bin`
+entry, which takes up the majority of the space in an OTA package, will be
+streamed by `update_engine` directly. The ZIP entries in such a package need to be
+saved uncompressed (`ZIP_STORED`), so that their data can be downloaded directly
+with the offset and length. As `payload.bin` itself is already in compressed
+format, the size penalty is marginal.
 
 Config files can be generated using `tools/gen_update_config.py`.
 Running `./tools/gen_update_config.py --help` shows usage of the script.
@@ -44,11 +50,15 @@
 
 ## Running on a device
 
-The commands expected to be run from `$ANDROID_BUILD_TOP`.
+The commands expected to be run from `$ANDROID_BUILD_TOP` and for demo
+purpose only.
 
 1. Compile the app `$ mmma bootable/recovery/updater_sample`.
 2. Install the app to the device using `$ adb install <APK_PATH>`.
-3. Add update config files.
+3. Change permissions on `/data/ota_package/` to `0777` on the device.
+4. Set SELinux mode to permissive. See instructions below.
+5. Add update config files.
+6. Push OTA packages to the device.
 
 
 ## Development
@@ -86,13 +96,33 @@
    ```
 
 
-## Getting access to `update_engine` API and read/write access to `/data`
+## Accessing `android.os.UpdateEngine` API
 
-Run adb shell as a root, and set SELinux mode to permissive (0):
+`android.os.UpdateEngine`` APIs are marked as `@SystemApi`, meaning only system apps can access them.
+
+
+## Getting read/write access to `/data/ota_package/`
+
+Following must be included in `AndroidManifest.xml`:
+
+```xml
+    <uses-permission android:name="android.permission.ACCESS_CACHE_FILESYSTEM" />
+```
+
+Note: access to cache filesystem is granted only to system apps.
+
+
+## Setting SELinux mode to permissive (0)
 
 ```txt
-$ adb root
-$ adb shell
-# setenforce 0
-# getenforce
+local$ adb root
+local$ adb shell
+android# setenforce 0
+android# getenforce
 ```
+
+
+## License
+
+SystemUpdaterSample app is released under
+[Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0).
diff --git a/updater_sample/res/layout/activity_main.xml b/updater_sample/res/layout/activity_main.xml
index 3cd7721..7a12d34 100644
--- a/updater_sample/res/layout/activity_main.xml
+++ b/updater_sample/res/layout/activity_main.xml
@@ -114,7 +114,7 @@
                     android:id="@+id/textView"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:text="Running update status:" />
+                    android:text="Update status:" />
 
                 <TextView
                     android:id="@+id/textViewStatus"
@@ -124,6 +124,28 @@
                     android:text="@string/unknown" />
             </LinearLayout>
 
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="4dp"
+                android:orientation="horizontal">
+
+                <TextView
+                    android:id="@+id/textView2"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="Update completion:" />
+
+                <TextView
+                    android:id="@+id/textViewCompletion"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="8dp"
+                    android:text="@string/unknown" />
+            </LinearLayout>
+
+
             <ProgressBar
                 android:id="@+id/progressBar"
                 style="?android:attr/progressBarStyleHorizontal"
diff --git a/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java b/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java
index 90c5637..ce88338 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java
@@ -18,12 +18,15 @@
 
 import android.os.UpdateEngine;
 
+import java.io.Serializable;
 import java.util.List;
 
 /**
  * Payload that will be given to {@link UpdateEngine#applyPayload)}.
  */
-public class PayloadSpec {
+public class PayloadSpec implements Serializable {
+
+    private static final long serialVersionUID = 41043L;
 
     /**
      * Creates a payload spec {@link Builder}
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 9f1e5d1..d6a6ce3 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
@@ -31,13 +31,15 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+import com.example.android.systemupdatersample.PayloadSpec;
 import com.example.android.systemupdatersample.R;
 import com.example.android.systemupdatersample.UpdateConfig;
-import com.example.android.systemupdatersample.updates.AbNonStreamingUpdate;
+import com.example.android.systemupdatersample.util.PayloadSpecs;
 import com.example.android.systemupdatersample.util.UpdateConfigs;
 import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes;
 import com.example.android.systemupdatersample.util.UpdateEngineStatuses;
 
+import java.io.IOException;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -46,6 +48,8 @@
  */
 public class MainActivity extends Activity {
 
+    private static final String TAG = "MainActivity";
+
     private TextView mTextViewBuild;
     private Spinner mSpinnerConfigs;
     private TextView mTextViewConfigsDirHint;
@@ -55,17 +59,19 @@
     private Button mButtonReset;
     private ProgressBar mProgressBar;
     private TextView mTextViewStatus;
+    private TextView mTextViewCompletion;
 
     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();
 
+    private final UpdateEngine mUpdateEngine = new UpdateEngine();
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -80,14 +86,14 @@
         this.mButtonReset = findViewById(R.id.buttonReset);
         this.mProgressBar = findViewById(R.id.progressBar);
         this.mTextViewStatus = findViewById(R.id.textViewStatus);
-
-        this.mUpdateEngine.bind(mUpdateEngineCallback);
+        this.mTextViewCompletion = findViewById(R.id.textViewCompletion);
 
         this.mTextViewConfigsDirHint.setText(UpdateConfigs.getConfigsRoot(this));
 
         uiReset();
-
         loadUpdateConfigs();
+
+        this.mUpdateEngine.bind(mUpdateEngineCallback);
     }
 
     @Override
@@ -140,7 +146,6 @@
                 .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();
@@ -156,7 +161,6 @@
                         + " 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();
@@ -178,6 +182,13 @@
                 setUiStatus(status);
                 Toast.makeText(this, "Update Status changed", Toast.LENGTH_LONG)
                         .show();
+                if (status != UpdateEngine.UpdateStatusConstants.IDLE) {
+                    Log.d(TAG, "status changed, setting ui to updating mode");
+                    uiSetUpdating();
+                } else {
+                    Log.d(TAG, "status changed, resetting ui");
+                    uiReset();
+                }
             });
         }
     }
@@ -188,15 +199,16 @@
      * values from {@link UpdateEngine.ErrorCodeConstants}.
      */
     private void onPayloadApplicationComplete(int errorCode) {
+        final String state = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode)
+                ? "SUCCESS"
+                : "FAILURE";
         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();
+            setUiCompletion(errorCode);
         });
     }
 
@@ -212,6 +224,7 @@
         mProgressBar.setEnabled(false);
         mProgressBar.setVisibility(ProgressBar.INVISIBLE);
         mTextViewStatus.setText(R.string.unknown);
+        mTextViewCompletion.setText(R.string.unknown);
     }
 
     /** sets ui updating mode */
@@ -239,7 +252,18 @@
      */
     private void setUiStatus(int status) {
         String statusText = UpdateEngineStatuses.getStatusText(status);
-        mTextViewStatus.setText(statusText);
+        mTextViewStatus.setText(statusText + "/" + status);
+    }
+
+    /**
+     * @param errorCode update engine error code
+     */
+    private void setUiCompletion(int errorCode) {
+        final String state = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode)
+                ? "SUCCESS"
+                : "FAILURE";
+        String errorText = UpdateEngineErrorCodes.getCodeName(errorCode);
+        mTextViewCompletion.setText(state + " " + errorText + "/" + errorCode);
     }
 
     private void loadConfigsToSpinner(List<UpdateConfig> configs) {
@@ -259,19 +283,42 @@
     /**
      * Applies the given update
      */
-    private void applyUpdate(UpdateConfig config) {
+    private void applyUpdate(final UpdateConfig config) {
         if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) {
-            AbNonStreamingUpdate update = new AbNonStreamingUpdate(mUpdateEngine, config);
+            PayloadSpec payload;
             try {
-                update.execute();
-            } catch (Exception e) {
-                Log.e("MainActivity", "Error applying the update", e);
-                Toast.makeText(this, "Error applying the update", Toast.LENGTH_SHORT)
+                payload = PayloadSpecs.forNonStreaming(config.getUpdatePackageFile());
+            } catch (IOException e) {
+                Log.e(TAG, "Error creating payload spec", e);
+                Toast.makeText(this, "Error creating payload spec", Toast.LENGTH_LONG)
                         .show();
+                return;
             }
+            updateEngineApplyPayload(payload);
         } else {
-            Toast.makeText(this, "Streaming is not implemented", Toast.LENGTH_SHORT)
-                    .show();
+            Log.d(TAG, "Starting PrepareStreamingService");
+        }
+    }
+
+    /**
+     * Applies given payload.
+     *
+     * UpdateEngine works asynchronously. This method doesn't wait until
+     * end of the update.
+     */
+    private void updateEngineApplyPayload(PayloadSpec payloadSpec) {
+        try {
+            mUpdateEngine.applyPayload(
+                    payloadSpec.getUrl(),
+                    payloadSpec.getOffset(),
+                    payloadSpec.getSize(),
+                    payloadSpec.getProperties().toArray(new String[0]));
+        } catch (Exception e) {
+            Log.e(TAG, "UpdateEngine failed to apply the update", e);
+            Toast.makeText(
+                    this,
+                    "UpdateEngine failed to apply the update",
+                    Toast.LENGTH_LONG).show();
         }
     }
 
@@ -280,10 +327,11 @@
      * leave it as is.
      */
     private void stopRunningUpdate() {
-        Toast.makeText(this,
-                "stopRunningUpdate is not implemented",
-                Toast.LENGTH_SHORT).show();
-
+        try {
+            mUpdateEngine.cancel();
+        } catch (Exception e) {
+            Log.w(TAG, "UpdateEngine failed to stop the ongoing update", e);
+        }
     }
 
     /**
@@ -291,9 +339,11 @@
      * update has been applied.
      */
     private void resetUpdate() {
-        Toast.makeText(this,
-                "resetUpdate is not implemented",
-                Toast.LENGTH_SHORT).show();
+        try {
+            mUpdateEngine.resetStatus();
+        } catch (Exception e) {
+            Log.w(TAG, "UpdateEngine failed to reset the update", e);
+        }
     }
 
     /**
diff --git a/updater_sample/src/com/example/android/systemupdatersample/updates/AbNonStreamingUpdate.java b/updater_sample/src/com/example/android/systemupdatersample/updates/AbNonStreamingUpdate.java
deleted file mode 100644
index 1b91a1a..0000000
--- a/updater_sample/src/com/example/android/systemupdatersample/updates/AbNonStreamingUpdate.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * 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/FileDownloader.java b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java
index 806f173..5c1d711 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java
@@ -40,7 +40,7 @@
     private long mSize;
     private File mOut;
 
-    public FileDownloader(String url, long offset, long size, File out)  {
+    public FileDownloader(String url, long offset, long size, File out) {
         this.mUrl = url;
         this.mOffset = offset;
         this.mSize = size;
diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java
index dc7ec09..80506ee 100644
--- a/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java
@@ -60,7 +60,7 @@
         File packageFile = Paths
                 .get(mTargetContext.getCacheDir().getAbsolutePath(), "ota.zip")
                 .toFile();
-        Files.delete(packageFile.toPath());
+        Files.deleteIfExists(packageFile.toPath());
         Files.copy(mTestContext.getResources().openRawResource(R.raw.ota_002_package),
                 packageFile.toPath());
         String url = "file://" + packageFile.getAbsolutePath();
@@ -68,7 +68,7 @@
         File outFile = Paths
                 .get(mTargetContext.getCacheDir().getAbsolutePath(), "care_map.txt")
                 .toFile();
-        Files.delete(outFile.toPath());
+        Files.deleteIfExists(outFile.toPath());
         // download a chunk of ota.zip
         FileDownloader downloader = new FileDownloader(url, 160, 8, outFile);
         downloader.download();