Merge "updater_sample: add switch slot demo"
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)