diff options
11 files changed, 210 insertions, 16 deletions
diff --git a/updater_sample/README.md b/updater_sample/README.md index 95e57dbe9..c68c07caf 100644 --- a/updater_sample/README.md +++ b/updater_sample/README.md @@ -44,6 +44,10 @@ 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. +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 @@ which HTTP headers are supported. - [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 7a12d3474..d9e56b4b3 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 46fbfa33e..f188c233b 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 2b671ee5d..db4a5dc67 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 9bdd8b9e8..db99f7c74 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 @@ public class UpdateConfig implements Parcelable { 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 @@ public class UpdateConfig implements Parcelable { 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 @@ public class UpdateConfig implements Parcelable { /** 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 @@ public class UpdateConfig implements Parcelable { 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 @@ public class UpdateConfig implements Parcelable { return mAbStreamingMetadata; } + public AbConfig getAbConfig() { + return mAbConfig; + } + /** * @return File object for given url */ @@ -172,6 +186,7 @@ public class UpdateConfig implements Parcelable { dest.writeString(mUrl); dest.writeInt(mAbInstallType); dest.writeSerializable(mAbStreamingMetadata); + dest.writeSerializable(mAbConfig); dest.writeString(mRawJson); } @@ -185,9 +200,11 @@ public class UpdateConfig implements Parcelable { /** 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 @@ public class UpdateConfig implements Parcelable { } } -} + /** + * 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 170825635..c5a7f9556 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 @@ package com.example.android.systemupdatersample.ui; 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.services.PrepareStreamingService; 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 @@ public class MainActivity extends Activity { 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 @@ public class MainActivity extends Activity { 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 @@ public class MainActivity extends Activity { } /** + * 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 @@ public class MainActivity extends Activity { 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 @@ public class MainActivity extends Activity { + " " + 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 @@ public class MainActivity extends Activity { mProgressBar.setVisibility(ProgressBar.INVISIBLE); mTextViewStatus.setText(R.string.unknown); mTextViewCompletion.setText(R.string.unknown); + uiHideSwitchSlotInfo(); } /** sets ui updating mode */ @@ -245,6 +269,16 @@ public class MainActivity extends Activity { 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 @@ public class MainActivity extends Activity { * 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 @@ public class MainActivity extends Activity { .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 @@ public class MainActivity extends Activity { * @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 @@ public class MainActivity extends Activity { } /** + * 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 000000000..e368f14d2 --- /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 15127cf2c..be51b7c95 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 cf4469b1c..5d7874cdb 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 0975e76be..000f5663b 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 @@ package com.example.android.systemupdatersample; 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 @@ public class UpdateConfigTest { 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 @@ public class UpdateConfigTest { 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 @@ public class UpdateConfigTest { @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 4efa9f1c4..7fb64f7fc 100755 --- a/updater_sample/tools/gen_update_config.py +++ b/updater_sample/tools/gen_update_config.py @@ -46,10 +46,11 @@ class GenUpdateConfig(object): 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 @@ class GenUpdateConfig(object): '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 @@ def main(): # pylint: disable=missing-docstring 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 @@ def main(): # pylint: disable=missing-docstring 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) |