- Android device with root permissions and supporting A/B partitioning.
update_engine
daemon is running (https://source.android.com/devices/tech/ota/ab#update-engine)
This project is derived out of https://android.googlesource.com/platform/bootable/recovery/+/master/updater_sample/ and requires access to android hidden api's, which are otherwise not available. To build this project locally.
- Download android.jar for API 30 and replace
<path to android sdk>/platforms/android-30/android.jar
For more information refer https://github.com/anggrayudi/android-hidden-api and anggrayudi/android-hidden-api#84
For the app to be installed on the target and to work. The permissions to REBOOT
, ACCESS_CACHE_FILES
, RECOVERY
are to be provided. These permissions are typically only granted to system apps. On a rooted devices, update the privapp-permissions-platoform.xml
file available at /etc/permissions
adb root
adb remount
adb pull /etc/permissions/privapp-permissions-platform.xml .
Update file with the permissions.
<!-- com.example.android.systemupdatersample -->
<privapp-permissions package="com.example.android.systemupdatersample">
<permission name="android.permission.RECOVERY"/>
<permission name="android.permission.ACCESS_CACHE_FILESYSTEM"/>
<permission name="android.permission.REBOOT"/>
<permission name="android.permission.DELETE_CACHE_FILES"/>
</privapp-permissions>
Push the file back adb push
privapp-permissions-platform.xml /etc/permissions/`
Push the built apk to /system/priv-app
adb push app/build/outputs/apk/app-debug.apk /system/priv-app`
adb reboot
Tested on Raspberry PI 4 running LineageOS 18.1 (Android 11). More info Refer https://konstakang.com/devices/rpi4/LineageOS18/
This app demonstrates how to use Android system updates APIs to install
OTA updates. It contains a
sample client for update_engine
to install A/B (seamless) updates.
A/B (seamless) update is available since Android Nougat (API 24), but this sample targets the latest android.
SystemUpdaterSample app shows list of available updates on the UI. User is allowed
to select an update and apply it to the device. App shows installation progress,
logs can be found in adb logcat
. User can stop or reset an update. Resetting
the update requests update engine to cancel any ongoing update, and revert
if the update has been applied. Stopping does not revert the applied update.
In this sample updates are defined in JSON update config files.
The structure of a config file is defined in
com.example.android.systemupdatersample.UpdateConfig
, example file is located
at res/raw/sample.json
.
In real-life update system the config files expected to be served from a server
to the app, but in this sample, the config files are stored on the device.
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
. In this sample app
url
is expected to point to file system, e.g. file:///data/my-sample-ota-builds-dir/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.
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.
UpdateEngine provides status for different stages of update application process. But it lacks of proper status codes when update fails.
This creates two problems:
-
If sample app is unbound from update_engine (MainActivity is paused, destroyed), app doesn't receive onStatusUpdate and onPayloadApplicationCompleted notifications. If app binds to update_engine after update is completed, only onStatusUpdate is called, but status becomes IDLE in most cases. And there is no way to know if update was successful or not.
-
This sample app demostrates suspend/resume using update_engins's
cancel
andapplyPayload
(which picks up from where it left). Whencancel
is called, status is set toIDLE
, which doesn't allow tracking suspended state properly.
To solve these problems sample app implements its own separate update
state - UpdaterState
. To solve the first problem, sample app persists
UpdaterState
on a device. When app is resumed, it checks if UpdaterState
matches the update_engine's status (as onStatusUpdate is guaranteed to be called).
If they doesn't match, sample app calls applyPayload
again with the same
parameters, and handles update completion properly using onPayloadApplicationCompleted
callback. The second problem is solved by adding PAUSED
updater state.
Current Build:
- shows current active build.Updater state:
- SystemUpdaterSample app state.Engine status:
- last reported update_engine status.Engine error:
- last reported payload application error.
Reload
- reloads update configs from device storage.View config
- shows selected update config.Apply
- applies selected update config.Stop
- cancel running update, callsUpdateEngine#cancel
.Reset
- reset update, callsUpdateEngine#resetStatus
, can be called only when update is not running.Suspend
- suspend running update, usesUpdateEngine#cancel
.Resume
- resumes suspended update, usesUpdateEngine#applyPayload
.Switch Slot
- ifab_config.force_switch_slot
config set true, this button will be enabled after payload is applied, to switch A/B slot on next reboot.
Sometimes OTA package server might require some HTTP headers to be present,
e.g. Authorization
header to contain valid auth token. While performing
streaming update, UpdateEngine
allows passing on certain HTTP headers;
as of writing this sample app, these headers are Authorization
and User-Agent
.
android.os.UpdateEngine#applyPayload
contains information on
which HTTP headers are supported.
Binds given callbacks to update_engine. When update_engine successfully initialized, it's guaranteed to invoke callback onStatusUpdate.
Start an update attempt to download an apply the provided payload_url
if
no other update is running. The extra key_value_pair_headers
will be
included when fetching the payload.
key_value_pair_headers
argument also accepts properties other than HTTP Headers.
List of allowed properties can be found in system/update_engine/common/constants.cc
.
Cancel the ongoing update. The update could be running or suspended, but it can't be canceled after it was done.
Reset the already applied update back to an idle state. This method can only be called when no update attempt is going on, and it will reset the status back to idle, deleting the currently applied update if any.
Called whenever the value of status
or progress
changes. For
progress
values changes, this method will be called only if it changes significantly.
At this time of writing this doc, delta for progress
is 0.005
.
onStatusUpdate
is always called when app binds to update_engine,
except when update_engine fails to initialize.
Called whenever an update attempt is completed or failed.
The commands are expected to be run from $ANDROID_BUILD_TOP
and for demo
purpose only.
- Compile the app
mmma -j bootable/recovery/updater_sample
. - Install the app to the device using
adb install <APK_PATH>
. - Change permissions on
/data/ota_package/
to0777
on the device. - Set SELinux mode to permissive. See instructions below.
- Add update config files; look above at Update Config file.
- Push OTA packages to the device.
- Run the sample app.
To run sample app as a privileged system app, it needs to be installed in /system/priv-app/
.
This directory is expected to be read-only, unless explicitly remounted.
The recommended way to run the app is to build and install it as a
privileged system app, so it's granted the required permissions to access
update_engine
service as well as OTA package files. Detailed steps are as follows:
- Prepare to build
- Add the module (SystemUpdaterSample) to the
PRODUCT_PACKAGES
list for the lunch target. e.g. add a line containingPRODUCT_PACKAGES += SystemUpdaterSample
todevice/google/marlin/device-common.mk
. - Whitelist the sample app
- Add
to<privapp-permissions package="com.example.android.systemupdatersample"> <permission name="android.permission.ACCESS_CACHE_FILESYSTEM"/> </privapp-permissions>
frameworks/base/data/etc/privapp-permissions-platform.xml
- Add
privileged: true
to SystemUpdaterSample building rule. - Build sample app
make -j SystemUpdaterSample
. - Build Android
make -j
- Flash the device
- Add update config files; look above at
## Update Config file
;adb root
might be required. - Push OTA packages to the device if there is no server to stream packages from;
changing of SELinux labels of OTA packages directory might be required
chcon -R u:object_r:ota_package_file:s0 /data/my-sample-ota-builds-dir
- Run the sample app.
- Create a UI with list of configs, current version, control buttons, progress bar and log viewer
- Add
PayloadSpec
andPayloadSpecs
for working with update zip file - Add
UpdateConfig
for working with json config files - Add applying non-streaming update
- Prepare streaming update (partially downloading package)
- Add applying streaming update
- Add stop/reset the update
- Add demo for passing HTTP headers to
UpdateEngine#applyPayload
- Package compatibility check
- Deferred switch slot demo
- Add UpdateManager; extract update logic from MainActivity
- Add Sample app update state (separate from update_engine status)
- Add smart update completion detection using onStatusUpdate
- Add pause/resume demo
- Verify system partition checksum for package
The commands are expected to be run from $ANDROID_BUILD_TOP
.
- Build
make -j SystemUpdaterSample
andmake -j SystemUpdaterSampleTests
. - Install app
adb install $OUT/system/app/SystemUpdaterSample/SystemUpdaterSample.apk
- Install tests
adb install $OUT/testcases/SystemUpdaterSampleTests/arm64/SystemUpdaterSampleTests.apk
- Run tests
adb shell am instrument -w com.example.android.systemupdatersample.tests/android.support.test.runner.AndroidJUnitRunner
- Run a test file
adb shell am instrument \ -w -e class com.example.android.systemupdatersample.UpdateManagerTest#applyUpdate_appliesPayloadToUpdateEngine \ com.example.android.systemupdatersample.tests/android.support.test.runner.AndroidJUnitRunner
android.os.UpdateEngine
APIs are marked as @SystemApi
, meaning only system
apps can access them.
Access to cache filesystem is granted only to system apps.
local$ adb root
local$ adb shell
android# setenforce 0
android# getenforce
SystemUpdaterSample app is released under Apache License 2.0.