updater_sample: add updater state

- Add util.UpdaterStates - the state of SystemUpdaterSample.
    It's different from status of UpdateEngine;
    when UpdateEngine#cancel is used to suspend the update,
    UpdateEngine sets status to IDLE, which cannot be used
    to track the suspended state.
- UI: Change 'Update status' to 'Engine status'.
- UI: Change 'Update completion' to 'Engine error code'.
- UI: Add 'Updater state'.

Test: manually on the device
Test: using JUnit4
Change-Id: I9c58b5ed0eae3be7ab8b217fc01a621e8fb2f4bf
Signed-off-by: Zhomart Mukhamejanov <zhomart@google.com>
diff --git a/updater_sample/res/layout/activity_main.xml b/updater_sample/res/layout/activity_main.xml
index d9e56b4..7cde42c 100644
--- a/updater_sample/res/layout/activity_main.xml
+++ b/updater_sample/res/layout/activity_main.xml
@@ -111,19 +111,38 @@
                 android:orientation="horizontal">
 
                 <TextView
-                    android:id="@+id/textView"
+                    android:id="@+id/textView3"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:text="Update status:" />
+                    android:text="Updater state:" />
 
                 <TextView
-                    android:id="@+id/textViewStatus"
+                    android:id="@+id/textViewUpdaterState"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:layout_marginLeft="8dp"
                     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/textView"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="Engine status:" />
+
+                <TextView
+                    android:id="@+id/textViewEngineStatus"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="8dp"
+                    android:text="@string/unknown" />
+            </LinearLayout>
 
             <LinearLayout
                 android:layout_width="match_parent"
@@ -135,10 +154,10 @@
                     android:id="@+id/textView2"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:text="Update completion:" />
+                    android:text="Engine error:" />
 
                 <TextView
-                    android:id="@+id/textViewCompletion"
+                    android:id="@+id/textViewEngineErrorCode"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:layout_marginLeft="8dp"
diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java
index 9f0a04e..c370a4e 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java
@@ -25,6 +25,7 @@
 import com.example.android.systemupdatersample.util.PayloadSpecs;
 import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes;
 import com.example.android.systemupdatersample.util.UpdateEngineProperties;
+import com.example.android.systemupdatersample.util.UpdaterStates;
 import com.google.common.util.concurrent.AtomicDouble;
 
 import java.io.IOException;
@@ -37,7 +38,8 @@
 import java.util.function.IntConsumer;
 
 /**
- * Manages the update flow. Asynchronously interacts with the {@link UpdateEngine}.
+ * Manages the update flow. It has its own state (in memory), separate from
+ * {@link UpdateEngine}'s state. Asynchronously interacts with the {@link UpdateEngine}.
  */
 public class UpdateManager {
 
@@ -55,12 +57,15 @@
     private AtomicInteger mEngineErrorCode = new AtomicInteger(UpdateEngineErrorCodes.UNKNOWN);
     private AtomicDouble mProgress = new AtomicDouble(0);
 
+    private AtomicInteger mState = new AtomicInteger(UpdaterStates.IDLE);
+
     private final UpdateManager.UpdateEngineCallbackImpl
             mUpdateEngineCallback = new UpdateManager.UpdateEngineCallbackImpl();
 
     private PayloadSpec mLastPayloadSpec;
     private AtomicBoolean mManualSwitchSlotRequired = new AtomicBoolean(true);
 
+    private IntConsumer mOnStateChangeCallback = null;
     private IntConsumer mOnEngineStatusUpdateCallback = null;
     private DoubleConsumer mOnProgressUpdateCallback = null;
     private IntConsumer mOnEngineCompleteCallback = null;
@@ -97,11 +102,31 @@
      * Returns true if manual switching slot is required. Value depends on
      * the update config {@code ab_config.force_switch_slot}.
      */
-    public boolean manualSwitchSlotRequired() {
+    public boolean isManualSwitchSlotRequired() {
         return mManualSwitchSlotRequired.get();
     }
 
     /**
+     * Sets SystemUpdaterSample app state change callback. Value of {@code state} will be one
+     * of the values from {@link UpdaterStates}.
+     *
+     * @param onStateChangeCallback a callback with parameter {@code state}.
+     */
+    public void setOnStateChangeCallback(IntConsumer onStateChangeCallback) {
+        synchronized (mLock) {
+            this.mOnStateChangeCallback = onStateChangeCallback;
+        }
+    }
+
+    private Optional<IntConsumer> getOnStateChangeCallback() {
+        synchronized (mLock) {
+            return mOnStateChangeCallback == null
+                    ? Optional.empty()
+                    : Optional.of(mOnStateChangeCallback);
+        }
+    }
+
+    /**
      * Sets update engine status update callback. Value of {@code status} will
      * be one of the values from {@link UpdateEngine.UpdateStatusConstants}.
      *
@@ -161,6 +186,18 @@
     }
 
     /**
+     * Updates {@link this.mState} and if state is changed,
+     * it also notifies {@link this.mOnStateChangeCallback}.
+     */
+    private void setUpdaterState(int updaterState) {
+        int previousState = mState.get();
+        mState.set(updaterState);
+        if (previousState != updaterState) {
+            getOnStateChangeCallback().ifPresent(callback -> callback.accept(updaterState));
+        }
+    }
+
+    /**
      * Requests update engine to stop any ongoing update. If an update has been applied,
      * leave it as is.
      *
@@ -171,6 +208,7 @@
     public void cancelRunningUpdate() {
         try {
             mUpdateEngine.cancel();
+            setUpdaterState(UpdaterStates.IDLE);
         } catch (Exception e) {
             Log.w(TAG, "UpdateEngine failed to stop the ongoing update", e);
         }
@@ -186,6 +224,7 @@
     public void resetUpdate() {
         try {
             mUpdateEngine.resetStatus();
+            setUpdaterState(UpdaterStates.IDLE);
         } catch (Exception e) {
             Log.w(TAG, "UpdateEngine failed to reset the update", e);
         }
@@ -199,6 +238,7 @@
      */
     public void applyUpdate(Context context, UpdateConfig config) {
         mEngineErrorCode.set(UpdateEngineErrorCodes.UNKNOWN);
+        setUpdaterState(UpdaterStates.RUNNING);
 
         if (!config.getAbConfig().getForceSwitchSlot()) {
             mManualSwitchSlotRequired.set(true);
@@ -221,6 +261,7 @@
             payload = mPayloadSpecs.forNonStreaming(config.getUpdatePackageFile());
         } catch (IOException e) {
             Log.e(TAG, "Error creating payload spec", e);
+            setUpdaterState(UpdaterStates.ERROR);
             return;
         }
         updateEngineApplyPayload(payload, extraProperties);
@@ -239,6 +280,7 @@
                 updateEngineApplyPayload(payloadSpec, extraProperties);
             } else {
                 Log.e(TAG, "PrepareStreamingService failed, result code is " + code);
+                setUpdaterState(UpdaterStates.ERROR);
             }
         });
     }
@@ -282,6 +324,7 @@
                     properties.toArray(new String[0]));
         } catch (Exception e) {
             Log.e(TAG, "UpdateEngine failed to apply the update", e);
+            setUpdaterState(UpdaterStates.ERROR);
         }
     }
 
@@ -322,6 +365,12 @@
     private void onPayloadApplicationComplete(int errorCode) {
         Log.d(TAG, "onPayloadApplicationComplete invoked, errorCode=" + errorCode);
         mEngineErrorCode.set(errorCode);
+        if (errorCode == UpdateEngine.ErrorCodeConstants.SUCCESS
+                || errorCode == UpdateEngineErrorCodes.UPDATED_BUT_NOT_ACTIVE) {
+            setUpdaterState(UpdaterStates.FINISHED);
+        } else if (errorCode != UpdateEngineErrorCodes.USER_CANCELLED) {
+            setUpdaterState(UpdaterStates.ERROR);
+        }
 
         getOnEngineCompleteCallback()
                 .ifPresent(callback -> callback.accept(errorCode));
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 9237bc7..9983fe3 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
@@ -29,7 +29,6 @@
 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;
@@ -38,6 +37,7 @@
 import com.example.android.systemupdatersample.util.UpdateConfigs;
 import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes;
 import com.example.android.systemupdatersample.util.UpdateEngineStatuses;
+import com.example.android.systemupdatersample.util.UpdaterStates;
 
 import java.util.List;
 
@@ -56,8 +56,9 @@
     private Button mButtonStop;
     private Button mButtonReset;
     private ProgressBar mProgressBar;
-    private TextView mTextViewStatus;
-    private TextView mTextViewCompletion;
+    private TextView mTextViewUpdaterState;
+    private TextView mTextViewEngineStatus;
+    private TextView mTextViewEngineErrorCode;
     private TextView mTextViewUpdateInfo;
     private Button mButtonSwitchSlot;
 
@@ -79,8 +80,9 @@
         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.mTextViewCompletion = findViewById(R.id.textViewCompletion);
+        this.mTextViewUpdaterState = findViewById(R.id.textViewUpdaterState);
+        this.mTextViewEngineStatus = findViewById(R.id.textViewEngineStatus);
+        this.mTextViewEngineErrorCode = findViewById(R.id.textViewEngineErrorCode);
         this.mTextViewUpdateInfo = findViewById(R.id.textViewUpdateInfo);
         this.mButtonSwitchSlot = findViewById(R.id.buttonSwitchSlot);
 
@@ -89,9 +91,10 @@
         uiReset();
         loadUpdateConfigs();
 
-        this.mUpdateManager.setOnEngineStatusUpdateCallback(this::onStatusUpdate);
+        this.mUpdateManager.setOnStateChangeCallback(this::onUpdaterStateChange);
+        this.mUpdateManager.setOnEngineStatusUpdateCallback(this::onEngineStatusUpdate);
+        this.mUpdateManager.setOnEngineCompleteCallback(this::onEnginePayloadApplicationComplete);
         this.mUpdateManager.setOnProgressUpdateCallback(this::onProgressUpdate);
-        this.mUpdateManager.setOnEngineCompleteCallback(this::onPayloadApplicationComplete);
     }
 
     @Override
@@ -143,6 +146,7 @@
                 .setIcon(android.R.drawable.ic_dialog_alert)
                 .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
                     uiSetUpdating();
+                    uiResetEngineText();
                     mUpdateManager.applyUpdate(this, getSelectedConfig());
                 })
                 .setNegativeButton(android.R.string.cancel, null)
@@ -186,17 +190,26 @@
     }
 
     /**
-     * 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}.
+     * Invoked when SystemUpdaterSample app state changes.
+     * Value of {@code state} will be one of the
+     * values from {@link UpdaterStates}.
      */
-    private void onStatusUpdate(int status) {
+    private void onUpdaterStateChange(int state) {
+        Log.i(TAG, "onUpdaterStateChange invoked state=" + state);
         runOnUiThread(() -> {
-            Log.e("UpdateEngine", "StatusUpdate - status="
+            setUiUpdaterState(state);
+        });
+    }
+
+    /**
+     * Invoked when {@link UpdateEngine} status changes. Value of {@code status} will
+     * be one of the values from {@link UpdateEngine.UpdateStatusConstants}.
+     */
+    private void onEngineStatusUpdate(int status) {
+        runOnUiThread(() -> {
+            Log.e(TAG, "StatusUpdate - status="
                     + UpdateEngineStatuses.getStatusText(status)
                     + "/" + status);
-            Toast.makeText(this, "Update Status changed", Toast.LENGTH_LONG)
-                    .show();
             if (status == UpdateEngine.UpdateStatusConstants.IDLE) {
                 Log.d(TAG, "status changed, resetting ui");
                 uiReset();
@@ -204,33 +217,28 @@
                 Log.d(TAG, "status changed, setting ui to updating mode");
                 uiSetUpdating();
             }
-            setUiStatus(status);
+            setUiEngineStatus(status);
         });
     }
 
-    private void onProgressUpdate(double progress) {
-        mProgressBar.setProgress((int) (100 * progress));
-    }
-
     /**
      * 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) {
-        final String state = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode)
+    private void onEnginePayloadApplicationComplete(int errorCode) {
+        final String completionState = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode)
                 ? "SUCCESS"
                 : "FAILURE";
         runOnUiThread(() -> {
-            Log.i("UpdateEngine",
+            Log.i(TAG,
                     "Completed - errorCode="
                     + UpdateEngineErrorCodes.getCodeName(errorCode) + "/" + errorCode
-                    + " " + state);
-            Toast.makeText(this, "Update completed", Toast.LENGTH_LONG).show();
-            setUiCompletion(errorCode);
+                    + " " + completionState);
+            setUiEngineErrorCode(errorCode);
             if (errorCode == UpdateEngineErrorCodes.UPDATED_BUT_NOT_ACTIVE) {
                 // if update was successfully applied.
-                if (mUpdateManager.manualSwitchSlotRequired()) {
+                if (mUpdateManager.isManualSwitchSlotRequired()) {
                     // Show "Switch Slot" button.
                     uiShowSwitchSlotInfo();
                 }
@@ -238,6 +246,13 @@
         });
     }
 
+    /**
+     * Invoked when update progress changes.
+     */
+    private void onProgressUpdate(double progress) {
+        mProgressBar.setProgress((int) (100 * progress));
+    }
+
     /** resets ui */
     private void uiReset() {
         mTextViewBuild.setText(Build.DISPLAY);
@@ -249,11 +264,15 @@
         mProgressBar.setProgress(0);
         mProgressBar.setEnabled(false);
         mProgressBar.setVisibility(ProgressBar.INVISIBLE);
-        mTextViewStatus.setText(R.string.unknown);
-        mTextViewCompletion.setText(R.string.unknown);
         uiHideSwitchSlotInfo();
     }
 
+    private void uiResetEngineText() {
+        mTextViewEngineStatus.setText(R.string.unknown);
+        mTextViewEngineErrorCode.setText(R.string.unknown);
+        // Note: Do not reset mTextViewUpdaterState; UpdateManager notifies properly.
+    }
+
     /** sets ui updating mode */
     private void uiSetUpdating() {
         mTextViewBuild.setText(Build.DISPLAY);
@@ -287,20 +306,25 @@
     /**
      * @param status update engine status code
      */
-    private void setUiStatus(int status) {
+    private void setUiEngineStatus(int status) {
         String statusText = UpdateEngineStatuses.getStatusText(status);
-        mTextViewStatus.setText(statusText + "/" + status);
+        mTextViewEngineStatus.setText(statusText + "/" + status);
     }
 
     /**
      * @param errorCode update engine error code
      */
-    private void setUiCompletion(int errorCode) {
-        final String state = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode)
-                ? "SUCCESS"
-                : "FAILURE";
+    private void setUiEngineErrorCode(int errorCode) {
         String errorText = UpdateEngineErrorCodes.getCodeName(errorCode);
-        mTextViewCompletion.setText(state + " " + errorText + "/" + errorCode);
+        mTextViewEngineErrorCode.setText(errorText + "/" + errorCode);
+    }
+
+    /**
+     * @param state updater sample state
+     */
+    private void setUiUpdaterState(int state) {
+        String stateText = UpdaterStates.getStateText(state);
+        mTextViewUpdaterState.setText(stateText + "/" + state);
     }
 
     private void loadConfigsToSpinner(List<UpdateConfig> configs) {
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java
index f06ddf7..7d55ff8 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java
@@ -36,6 +36,7 @@
     */
     public static final int UNKNOWN = -1;
     public static final int UPDATED_BUT_NOT_ACTIVE = 52;
+    public static final int USER_CANCELLED = 48;
 
     private static final SparseArray<String> CODE_TO_NAME_MAP = new SparseArray<>();
 
@@ -61,7 +62,7 @@
      * Completion codes returned by update engine indicating that the update
      * was successfully applied.
      */
-    private static final Set<Integer> SUCCEEDED_COMPLETION_CODES = new HashSet<Integer>(
+    private static final Set<Integer> SUCCEEDED_COMPLETION_CODES = new HashSet<>(
             Arrays.asList(UpdateEngine.ErrorCodeConstants.SUCCESS,
                     // UPDATED_BUT_NOT_ACTIVE is returned when the payload is
                     // successfully applied but the
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdaterStates.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdaterStates.java
new file mode 100644
index 0000000..fc20a79
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdaterStates.java
@@ -0,0 +1,50 @@
+/*
+ * 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;
+
+/**
+ * SystemUpdaterSample app state.
+ */
+public class UpdaterStates {
+
+    public static final int IDLE = 0;
+    public static final int ERROR = 1;
+    public static final int RUNNING = 2;
+    public static final int PAUSED = 3;
+    public static final int FINISHED = 4;
+
+    private static final SparseArray<String> STATE_MAP = new SparseArray<>();
+
+    static {
+        STATE_MAP.put(0, "IDLE");
+        STATE_MAP.put(1, "ERROR");
+        STATE_MAP.put(2, "RUNNING");
+        STATE_MAP.put(3, "PAUSED");
+        STATE_MAP.put(4, "FINISHED");
+    }
+
+    /**
+     * converts status code to status name
+     */
+    public static String getStateText(int state) {
+        return STATE_MAP.get(state);
+    }
+
+    private UpdaterStates() {}
+}