Merge "updater_sample: add switch slot demo"
am: 2ddc54f5fd

Change-Id: Ie29bdc24f011744e8d13ea0b5c5608d4d3851792
diff --git a/updater_sample/README.md b/updater_sample/README.md
index 95e57db..c68c07c 100644
--- a/updater_sample/README.md
+++ b/updater_sample/README.md
@@ -44,6 +44,10 @@
 with the offset and length. As `payload.bin` itself is already in compressed
 format, the size penalty is marginal.
 
+if `ab_config.force_switch_slot` set true device will boot to the
+updated partition on next reboot; otherwise button "Switch Slot" will
+become active, and user can manually set updated partition as the active slot.
+
 Config files can be generated using `tools/gen_update_config.py`.
 Running `./tools/gen_update_config.py --help` shows usage of the script.
 
@@ -85,8 +89,8 @@
 - [x] Add stop/reset the update
 - [x] Add demo for passing HTTP headers to `UpdateEngine#applyPayload`
 - [x] [Package compatibility check](https://source.android.com/devices/architecture/vintf/match-rules)
+- [x] Deferred switch slot demo
 - [ ] Add tests for `MainActivity`
-- [ ] Change partition demo
 - [ ] Verify system partition checksum for package
 - [ ] Add non-A/B updates demo
 
diff --git a/updater_sample/res/layout/activity_main.xml b/updater_sample/res/layout/activity_main.xml
index 7a12d34..d9e56b4 100644
--- a/updater_sample/res/layout/activity_main.xml
+++ b/updater_sample/res/layout/activity_main.xml
@@ -178,6 +178,23 @@
                     android:text="Reset" />
             </LinearLayout>
 
+            <TextView
+                android:id="@+id/textViewUpdateInfo"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="14dp"
+                android:textColor="#777"
+                android:textSize="10sp"
+                android:textStyle="italic"
+                android:text="@string/finish_update_info" />
+
+            <Button
+                android:id="@+id/buttonSwitchSlot"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:onClick="onSwitchSlotClick"
+                android:text="@string/switch_slot" />
+
         </LinearLayout>
 
     </ScrollView>
diff --git a/updater_sample/res/raw/sample.json b/updater_sample/res/raw/sample.json
index 46fbfa3..f188c23 100644
--- a/updater_sample/res/raw/sample.json
+++ b/updater_sample/res/raw/sample.json
@@ -20,5 +20,10 @@
             }
         ],
         "authorization": "Basic my-secret-token"
+    },
+    "ab_config": {
+        "__": "A/B (seamless) update configurations",
+        "__force_switch_slot": "if set true device will boot to a new slot, otherwise user manually switches slot on the screen",
+        "force_switch_slot": false
     }
 }
diff --git a/updater_sample/res/values/strings.xml b/updater_sample/res/values/strings.xml
index 2b671ee..db4a5dc 100644
--- a/updater_sample/res/values/strings.xml
+++ b/updater_sample/res/values/strings.xml
@@ -18,4 +18,6 @@
     <string name="action_reload">Reload</string>
     <string name="unknown">Unknown</string>
     <string name="close">CLOSE</string>
+    <string name="switch_slot">Switch Slot</string>
+    <string name="finish_update_info">To finish the update press the button below</string>
 </resources>
diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
index 9bdd8b9..db99f7c 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
@@ -71,7 +71,7 @@
             JSONObject meta = o.getJSONObject("ab_streaming_metadata");
             JSONArray propertyFilesJson = meta.getJSONArray("property_files");
             PackageFile[] propertyFiles =
-                new PackageFile[propertyFilesJson.length()];
+                    new PackageFile[propertyFilesJson.length()];
             for (int i = 0; i < propertyFilesJson.length(); i++) {
                 JSONObject p = propertyFilesJson.getJSONObject(i);
                 propertyFiles[i] = new PackageFile(
@@ -87,6 +87,12 @@
                     propertyFiles,
                     authorization);
         }
+
+        // TODO: parse only for A/B updates when non-A/B is implemented
+        JSONObject ab = o.getJSONObject("ab_config");
+        boolean forceSwitchSlot = ab.getBoolean("force_switch_slot");
+        c.mAbConfig = new AbConfig(forceSwitchSlot);
+
         c.mRawJson = json;
         return c;
     }
@@ -109,6 +115,9 @@
     /** metadata is required only for streaming update */
     private StreamingMetadata mAbStreamingMetadata;
 
+    /** A/B update configurations */
+    private AbConfig mAbConfig;
+
     private String mRawJson;
 
     protected UpdateConfig() {
@@ -119,6 +128,7 @@
         this.mUrl = in.readString();
         this.mAbInstallType = in.readInt();
         this.mAbStreamingMetadata = (StreamingMetadata) in.readSerializable();
+        this.mAbConfig = (AbConfig) in.readSerializable();
         this.mRawJson = in.readString();
     }
 
@@ -148,6 +158,10 @@
         return mAbStreamingMetadata;
     }
 
+    public AbConfig getAbConfig() {
+        return mAbConfig;
+    }
+
     /**
      * @return File object for given url
      */
@@ -172,6 +186,7 @@
         dest.writeString(mUrl);
         dest.writeInt(mAbInstallType);
         dest.writeSerializable(mAbStreamingMetadata);
+        dest.writeSerializable(mAbConfig);
         dest.writeString(mRawJson);
     }
 
@@ -185,9 +200,11 @@
         /** defines beginning of update data in archive */
         private PackageFile[] mPropertyFiles;
 
-        /** SystemUpdaterSample receives the authorization token from the OTA server, in addition
+        /**
+         * SystemUpdaterSample receives the authorization token from the OTA server, in addition
          * to the package URL. It passes on the info to update_engine, so that the latter can
-         * fetch the data from the package server directly with the token. */
+         * fetch the data from the package server directly with the token.
+         */
         private String mAuthorization;
 
         public StreamingMetadata(PackageFile[] propertyFiles, String authorization) {
@@ -239,4 +256,27 @@
         }
     }
 
-}
+    /**
+     * A/B (seamless) update configurations.
+     */
+    public static class AbConfig implements Serializable {
+
+        private static final long serialVersionUID = 31044L;
+
+        /**
+         * if set true device will boot to new slot, otherwise user manually
+         * switches slot on the screen.
+         */
+        private boolean mForceSwitchSlot;
+
+        public AbConfig(boolean forceSwitchSlot) {
+            this.mForceSwitchSlot = forceSwitchSlot;
+        }
+
+        public boolean getForceSwitchSlot() {
+            return mForceSwitchSlot;
+        }
+
+    }
+
+}
\ No newline at end of file
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 1708256..c5a7f95 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
@@ -18,6 +18,7 @@
 
 import android.app.Activity;
 import android.app.AlertDialog;
+import android.graphics.Color;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.UpdateEngine;
@@ -38,11 +39,13 @@
 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.UpdateEngineProperties;
 import com.example.android.systemupdatersample.util.UpdateEngineStatuses;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
@@ -66,10 +69,14 @@
     private ProgressBar mProgressBar;
     private TextView mTextViewStatus;
     private TextView mTextViewCompletion;
+    private TextView mTextViewUpdateInfo;
+    private Button mButtonSwitchSlot;
 
     private List<UpdateConfig> mConfigs;
     private AtomicInteger mUpdateEngineStatus =
             new AtomicInteger(UpdateEngine.UpdateStatusConstants.IDLE);
+    private PayloadSpec mLastPayloadSpec;
+    private AtomicBoolean mManualSwitchSlotRequired = new AtomicBoolean(true);
 
     /**
      * Listen to {@code update_engine} events.
@@ -93,6 +100,8 @@
         this.mProgressBar = findViewById(R.id.progressBar);
         this.mTextViewStatus = findViewById(R.id.textViewStatus);
         this.mTextViewCompletion = findViewById(R.id.textViewCompletion);
+        this.mTextViewUpdateInfo = findViewById(R.id.textViewUpdateInfo);
+        this.mButtonSwitchSlot = findViewById(R.id.buttonSwitchSlot);
 
         this.mTextViewConfigsDirHint.setText(UpdateConfigs.getConfigsRoot(this));
 
@@ -173,6 +182,13 @@
     }
 
     /**
+     * switch slot button clicked
+     */
+    public void onSwitchSlotClick(View view) {
+        setSwitchSlotOnReboot();
+    }
+
+    /**
      * 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}.
@@ -185,16 +201,16 @@
                 Log.e("UpdateEngine", "StatusUpdate - status="
                         + UpdateEngineStatuses.getStatusText(status)
                         + "/" + status);
-                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 {
+                if (status == UpdateEngine.UpdateStatusConstants.IDLE) {
                     Log.d(TAG, "status changed, resetting ui");
                     uiReset();
+                } else {
+                    Log.d(TAG, "status changed, setting ui to updating mode");
+                    uiSetUpdating();
                 }
+                setUiStatus(status);
             });
         }
     }
@@ -215,6 +231,13 @@
                     + " " + state);
             Toast.makeText(this, "Update completed", Toast.LENGTH_LONG).show();
             setUiCompletion(errorCode);
+            if (errorCode == UpdateEngineErrorCodes.UPDATED_BUT_NOT_ACTIVE) {
+                // if update was successfully applied.
+                if (mManualSwitchSlotRequired.get()) {
+                    // Show "Switch Slot" button.
+                    uiShowSwitchSlotInfo();
+                }
+            }
         });
     }
 
@@ -231,6 +254,7 @@
         mProgressBar.setVisibility(ProgressBar.INVISIBLE);
         mTextViewStatus.setText(R.string.unknown);
         mTextViewCompletion.setText(R.string.unknown);
+        uiHideSwitchSlotInfo();
     }
 
     /** sets ui updating mode */
@@ -245,6 +269,16 @@
         mProgressBar.setVisibility(ProgressBar.VISIBLE);
     }
 
+    private void uiShowSwitchSlotInfo() {
+        mButtonSwitchSlot.setEnabled(true);
+        mTextViewUpdateInfo.setTextColor(Color.parseColor("#777777"));
+    }
+
+    private void uiHideSwitchSlotInfo() {
+        mTextViewUpdateInfo.setTextColor(Color.parseColor("#AAAAAA"));
+        mButtonSwitchSlot.setEnabled(false);
+    }
+
     /**
      * loads json configurations from configs dir that is defined in {@link UpdateConfigs}.
      */
@@ -290,6 +324,17 @@
      * Applies the given update
      */
     private void applyUpdate(final UpdateConfig config) {
+        List<String> extraProperties = new ArrayList<>();
+
+        if (!config.getAbConfig().getForceSwitchSlot()) {
+            // Disable switch slot on reboot, which is enabled by default.
+            // User will enable it manually by clicking "Switch Slot" button on the screen.
+            extraProperties.add(UpdateEngineProperties.PROPERTY_DISABLE_SWITCH_SLOT_ON_REBOOT);
+            mManualSwitchSlotRequired.set(true);
+        } else {
+            mManualSwitchSlotRequired.set(false);
+        }
+
         if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) {
             PayloadSpec payload;
             try {
@@ -300,12 +345,11 @@
                         .show();
                 return;
             }
-            updateEngineApplyPayload(payload, null);
+            updateEngineApplyPayload(payload, extraProperties);
         } else {
             Log.d(TAG, "Starting PrepareStreamingService");
             PrepareStreamingService.startService(this, config, (code, payloadSpec) -> {
                 if (code == PrepareStreamingService.RESULT_CODE_SUCCESS) {
-                    List<String> extraProperties = new ArrayList<>();
                     extraProperties.add("USER_AGENT=" + HTTP_USER_AGENT);
                     config.getStreamingMetadata()
                             .getAuthorization()
@@ -332,6 +376,8 @@
      * @param extraProperties additional properties to pass to {@link UpdateEngine#applyPayload}
      */
     private void updateEngineApplyPayload(PayloadSpec payloadSpec, List<String> extraProperties) {
+        mLastPayloadSpec = payloadSpec;
+
         ArrayList<String> properties = new ArrayList<>(payloadSpec.getProperties());
         if (extraProperties != null) {
             properties.addAll(extraProperties);
@@ -352,6 +398,29 @@
     }
 
     /**
+     * Sets the new slot that has the updated partitions as the active slot,
+     * which device will boot into next time.
+     * This method is only supposed to be called after the payload is applied.
+     *
+     * Invoking {@link UpdateEngine#applyPayload} with the same payload url, offset, size
+     * and payload metadata headers doesn't trigger new update. It can be used to just switch
+     * active A/B slot.
+     *
+     * {@link UpdateEngine#applyPayload} might take several seconds to finish, and it will
+     * invoke callbacks {@link this#onStatusUpdate} and {@link this#onPayloadApplicationComplete)}.
+     */
+    private void setSwitchSlotOnReboot() {
+        Log.d(TAG, "setSwitchSlotOnReboot invoked");
+        List<String> extraProperties = new ArrayList<>();
+        // PROPERTY_SKIP_POST_INSTALL should be passed on to skip post-installation hooks.
+        extraProperties.add(UpdateEngineProperties.PROPERTY_SKIP_POST_INSTALL);
+        // It sets property SWITCH_SLOT_ON_REBOOT=1 by default.
+        // HTTP headers are not required, UpdateEngine is not expected to stream payload.
+        updateEngineApplyPayload(mLastPayloadSpec, extraProperties);
+        uiHideSwitchSlotInfo();
+    }
+
+    /**
      * Requests update engine to stop any ongoing update. If an update has been applied,
      * leave it as is.
      */
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineProperties.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineProperties.java
new file mode 100644
index 0000000..e368f14
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineProperties.java
@@ -0,0 +1,37 @@
+/*
+ * 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 properties that will be passed to {@code UpdateEngine#applyPayload}.
+ */
+public final class UpdateEngineProperties {
+
+    /**
+     * The property indicating that the update engine should not switch slot
+     * when the device reboots.
+     */
+    public static final String PROPERTY_DISABLE_SWITCH_SLOT_ON_REBOOT = "SWITCH_SLOT_ON_REBOOT=0";
+
+    /**
+     * The property to skip post-installation.
+     * https://source.android.com/devices/tech/ota/ab/#post-installation
+     */
+    public static final String PROPERTY_SKIP_POST_INSTALL = "RUN_POST_INSTALL=0";
+
+    private UpdateEngineProperties() {}
+}
diff --git a/updater_sample/tests/res/raw/update_config_stream_001.json b/updater_sample/tests/res/raw/update_config_stream_001.json
index 15127cf..be51b7c 100644
--- a/updater_sample/tests/res/raw/update_config_stream_001.json
+++ b/updater_sample/tests/res/raw/update_config_stream_001.json
@@ -10,5 +10,8 @@
                 "size": 8
             }
         ]
+    },
+    "ab_config": {
+        "force_switch_slot": true
     }
 }
diff --git a/updater_sample/tests/res/raw/update_config_stream_002.json b/updater_sample/tests/res/raw/update_config_stream_002.json
index cf4469b..5d7874c 100644
--- a/updater_sample/tests/res/raw/update_config_stream_002.json
+++ b/updater_sample/tests/res/raw/update_config_stream_002.json
@@ -1,5 +1,8 @@
 {
     "__": "*** Generated using tools/gen_update_config.py ***",
+    "ab_config": {
+        "force_switch_slot": false
+    },
     "ab_install_type": "STREAMING",
     "ab_streaming_metadata": {
         "property_files": [
diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java
index 0975e76..000f566 100644
--- a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java
@@ -18,6 +18,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
 import android.support.test.InstrumentationRegistry;
@@ -45,7 +46,8 @@
 
     private static final String JSON_NON_STREAMING =
             "{\"name\": \"vip update\", \"url\": \"file:///builds/a.zip\", "
-            + " \"ab_install_type\": \"NON_STREAMING\"}";
+                    + " \"ab_install_type\": \"NON_STREAMING\","
+                    + " \"ab_config\": { \"force_switch_slot\": false } }";
 
     @Rule
     public final ExpectedException thrown = ExpectedException.none();
@@ -82,6 +84,7 @@
                 config.getStreamingMetadata().getPropertyFiles()[0].getFilename());
         assertEquals(195, config.getStreamingMetadata().getPropertyFiles()[0].getOffset());
         assertEquals(8, config.getStreamingMetadata().getPropertyFiles()[0].getSize());
+        assertTrue(config.getAbConfig().getForceSwitchSlot());
     }
 
     @Test
@@ -94,7 +97,8 @@
     @Test
     public void getUpdatePackageFile_throwsErrorIfNotAFile() throws Exception {
         String json = "{\"name\": \"upd\", \"url\": \"http://foo.bar\","
-                + " \"ab_install_type\": \"NON_STREAMING\"}";
+                + " \"ab_install_type\": \"NON_STREAMING\","
+                + " \"ab_config\": { \"force_switch_slot\": false } }";
         UpdateConfig config = UpdateConfig.fromJson(json);
         thrown.expect(RuntimeException.class);
         config.getUpdatePackageFile();
diff --git a/updater_sample/tools/gen_update_config.py b/updater_sample/tools/gen_update_config.py
index 4efa9f1..7fb64f7 100755
--- a/updater_sample/tools/gen_update_config.py
+++ b/updater_sample/tools/gen_update_config.py
@@ -46,10 +46,11 @@
     AB_INSTALL_TYPE_STREAMING = 'STREAMING'
     AB_INSTALL_TYPE_NON_STREAMING = 'NON_STREAMING'
 
-    def __init__(self, package, url, ab_install_type):
+    def __init__(self, package, url, ab_install_type, ab_force_switch_slot):
         self.package = package
         self.url = url
         self.ab_install_type = ab_install_type
+        self.ab_force_switch_slot = ab_force_switch_slot
         self.streaming_required = (
             # payload.bin and payload_properties.txt must exist.
             'payload.bin',
@@ -80,6 +81,9 @@
             'url': self.url,
             'ab_streaming_metadata': streaming_metadata,
             'ab_install_type': self.ab_install_type,
+            'ab_config': {
+                'force_switch_slot': self.ab_force_switch_slot,
+            }
         }
 
     def _gen_ab_streaming_metadata(self):
@@ -126,6 +130,11 @@
                         default=GenUpdateConfig.AB_INSTALL_TYPE_NON_STREAMING,
                         choices=ab_install_type_choices,
                         help='A/B update installation type')
+    parser.add_argument('--ab_force_switch_slot',
+                        type=bool,
+                        default=False,
+                        help='if set true device will boot to a new slot, otherwise user manually '
+                             'switches slot on the screen')
     parser.add_argument('package',
                         type=str,
                         help='OTA package zip file')
@@ -144,7 +153,8 @@
     gen = GenUpdateConfig(
         package=args.package,
         url=args.url,
-        ab_install_type=args.ab_install_type)
+        ab_install_type=args.ab_install_type,
+        ab_force_switch_slot=args.ab_force_switch_slot)
     gen.run()
     gen.write(args.out)
     print('Config is written to ' + args.out)