diff options
7 files changed, 151 insertions, 98 deletions
diff --git a/updater_sample/README.md b/updater_sample/README.md index ee1faaf85..12f803ff6 100644 --- a/updater_sample/README.md +++ b/updater_sample/README.md @@ -30,13 +30,19 @@ 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`. If `ab_install_type` -is `NON_STREAMING` then app downloads the whole package and -passes it to the `update_engine`. If `ab_install_type` is `STREAMING` -then app downloads only some files to prepare the streaming update and -`update_engine` will stream only `payload.bin`. -To support streaming A/B (seamless) update, OTA package file must be -an uncompressed (ZIP_STORED) zip file. +SystemUpdaterSample app downloads OTA package from `url`. In this sample app +`url` is expected to point to file system, e.g. `file:///data/sample-builds/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. Config files can be generated using `tools/gen_update_config.py`. Running `./tools/gen_update_config.py --help` shows usage of the script. @@ -44,11 +50,15 @@ Running `./tools/gen_update_config.py --help` shows usage of the script. ## Running on a device -The commands expected to be run from `$ANDROID_BUILD_TOP`. +The commands expected to be run from `$ANDROID_BUILD_TOP` and for demo +purpose only. 1. Compile the app `$ mmma bootable/recovery/updater_sample`. 2. Install the app to the device using `$ adb install <APK_PATH>`. -3. Add update config files. +3. Change permissions on `/data/ota_package/` to `0777` on the device. +4. Set SELinux mode to permissive. See instructions below. +5. Add update config files. +6. Push OTA packages to the device. ## Development @@ -86,13 +96,33 @@ The commands expected to be run from `$ANDROID_BUILD_TOP`. ``` -## Getting access to `update_engine` API and read/write access to `/data` +## Accessing `android.os.UpdateEngine` API + +`android.os.UpdateEngine`` APIs are marked as `@SystemApi`, meaning only system apps can access them. + -Run adb shell as a root, and set SELinux mode to permissive (0): +## Getting read/write access to `/data/ota_package/` + +Following must be included in `AndroidManifest.xml`: + +```xml + <uses-permission android:name="android.permission.ACCESS_CACHE_FILESYSTEM" /> +``` + +Note: access to cache filesystem is granted only to system apps. + + +## Setting SELinux mode to permissive (0) ```txt -$ adb root -$ adb shell -# setenforce 0 -# getenforce +local$ adb root +local$ adb shell +android# setenforce 0 +android# getenforce ``` + + +## License + +SystemUpdaterSample app is released under +[Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/updater_sample/res/layout/activity_main.xml b/updater_sample/res/layout/activity_main.xml index 3cd772107..7a12d3474 100644 --- a/updater_sample/res/layout/activity_main.xml +++ b/updater_sample/res/layout/activity_main.xml @@ -114,7 +114,7 @@ android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Running update status:" /> + android:text="Update status:" /> <TextView android:id="@+id/textViewStatus" @@ -124,6 +124,28 @@ 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/textView2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Update completion:" /> + + <TextView + android:id="@+id/textViewCompletion" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="8dp" + android:text="@string/unknown" /> + </LinearLayout> + + <ProgressBar android:id="@+id/progressBar" style="?android:attr/progressBarStyleHorizontal" diff --git a/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java b/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java index 90c5637ea..ce8833883 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java +++ b/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java @@ -18,12 +18,15 @@ package com.example.android.systemupdatersample; import android.os.UpdateEngine; +import java.io.Serializable; import java.util.List; /** * Payload that will be given to {@link UpdateEngine#applyPayload)}. */ -public class PayloadSpec { +public class PayloadSpec implements Serializable { + + private static final long serialVersionUID = 41043L; /** * Creates a payload spec {@link Builder} 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 9f1e5d170..d6a6ce3f5 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java +++ b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java @@ -31,13 +31,15 @@ import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; +import com.example.android.systemupdatersample.PayloadSpec; import com.example.android.systemupdatersample.R; import com.example.android.systemupdatersample.UpdateConfig; -import com.example.android.systemupdatersample.updates.AbNonStreamingUpdate; +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.UpdateEngineStatuses; +import java.io.IOException; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -46,6 +48,8 @@ import java.util.concurrent.atomic.AtomicInteger; */ public class MainActivity extends Activity { + private static final String TAG = "MainActivity"; + private TextView mTextViewBuild; private Spinner mSpinnerConfigs; private TextView mTextViewConfigsDirHint; @@ -55,17 +59,19 @@ public class MainActivity extends Activity { private Button mButtonReset; private ProgressBar mProgressBar; private TextView mTextViewStatus; + private TextView mTextViewCompletion; private List<UpdateConfig> mConfigs; private AtomicInteger mUpdateEngineStatus = new AtomicInteger(UpdateEngine.UpdateStatusConstants.IDLE); - private UpdateEngine mUpdateEngine = new UpdateEngine(); /** * Listen to {@code update_engine} events. */ private UpdateEngineCallbackImpl mUpdateEngineCallback = new UpdateEngineCallbackImpl(); + private final UpdateEngine mUpdateEngine = new UpdateEngine(); + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -80,14 +86,14 @@ public class MainActivity extends Activity { this.mButtonReset = findViewById(R.id.buttonReset); this.mProgressBar = findViewById(R.id.progressBar); this.mTextViewStatus = findViewById(R.id.textViewStatus); - - this.mUpdateEngine.bind(mUpdateEngineCallback); + this.mTextViewCompletion = findViewById(R.id.textViewCompletion); this.mTextViewConfigsDirHint.setText(UpdateConfigs.getConfigsRoot(this)); uiReset(); - loadUpdateConfigs(); + + this.mUpdateEngine.bind(mUpdateEngineCallback); } @Override @@ -140,7 +146,6 @@ public class MainActivity extends Activity { .setMessage("Do you really want to cancel running update?") .setIcon(android.R.drawable.ic_dialog_alert) .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { - uiReset(); stopRunningUpdate(); }) .setNegativeButton(android.R.string.cancel, null).show(); @@ -156,7 +161,6 @@ public class MainActivity extends Activity { + " and restore old version?") .setIcon(android.R.drawable.ic_dialog_alert) .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { - uiReset(); resetUpdate(); }) .setNegativeButton(android.R.string.cancel, null).show(); @@ -178,6 +182,13 @@ public class MainActivity extends Activity { 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 { + Log.d(TAG, "status changed, resetting ui"); + uiReset(); + } }); } } @@ -188,15 +199,16 @@ public class MainActivity extends Activity { * values from {@link UpdateEngine.ErrorCodeConstants}. */ private void onPayloadApplicationComplete(int errorCode) { + final String state = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode) + ? "SUCCESS" + : "FAILURE"; runOnUiThread(() -> { - final String state = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode) - ? "SUCCESS" - : "FAILURE"; Log.i("UpdateEngine", "Completed - errorCode=" + UpdateEngineErrorCodes.getCodeName(errorCode) + "/" + errorCode + " " + state); Toast.makeText(this, "Update completed", Toast.LENGTH_LONG).show(); + setUiCompletion(errorCode); }); } @@ -212,6 +224,7 @@ public class MainActivity extends Activity { mProgressBar.setEnabled(false); mProgressBar.setVisibility(ProgressBar.INVISIBLE); mTextViewStatus.setText(R.string.unknown); + mTextViewCompletion.setText(R.string.unknown); } /** sets ui updating mode */ @@ -239,7 +252,18 @@ public class MainActivity extends Activity { */ private void setUiStatus(int status) { String statusText = UpdateEngineStatuses.getStatusText(status); - mTextViewStatus.setText(statusText); + mTextViewStatus.setText(statusText + "/" + status); + } + + /** + * @param errorCode update engine error code + */ + private void setUiCompletion(int errorCode) { + final String state = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode) + ? "SUCCESS" + : "FAILURE"; + String errorText = UpdateEngineErrorCodes.getCodeName(errorCode); + mTextViewCompletion.setText(state + " " + errorText + "/" + errorCode); } private void loadConfigsToSpinner(List<UpdateConfig> configs) { @@ -259,19 +283,42 @@ public class MainActivity extends Activity { /** * Applies the given update */ - private void applyUpdate(UpdateConfig config) { + private void applyUpdate(final UpdateConfig config) { if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) { - AbNonStreamingUpdate update = new AbNonStreamingUpdate(mUpdateEngine, config); + PayloadSpec payload; try { - update.execute(); - } catch (Exception e) { - Log.e("MainActivity", "Error applying the update", e); - Toast.makeText(this, "Error applying the update", Toast.LENGTH_SHORT) + payload = PayloadSpecs.forNonStreaming(config.getUpdatePackageFile()); + } catch (IOException e) { + Log.e(TAG, "Error creating payload spec", e); + Toast.makeText(this, "Error creating payload spec", Toast.LENGTH_LONG) .show(); + return; } + updateEngineApplyPayload(payload); } else { - Toast.makeText(this, "Streaming is not implemented", Toast.LENGTH_SHORT) - .show(); + Log.d(TAG, "Starting PrepareStreamingService"); + } + } + + /** + * Applies given payload. + * + * UpdateEngine works asynchronously. This method doesn't wait until + * end of the update. + */ + private void updateEngineApplyPayload(PayloadSpec payloadSpec) { + try { + mUpdateEngine.applyPayload( + payloadSpec.getUrl(), + payloadSpec.getOffset(), + payloadSpec.getSize(), + payloadSpec.getProperties().toArray(new String[0])); + } catch (Exception e) { + Log.e(TAG, "UpdateEngine failed to apply the update", e); + Toast.makeText( + this, + "UpdateEngine failed to apply the update", + Toast.LENGTH_LONG).show(); } } @@ -280,10 +327,11 @@ public class MainActivity extends Activity { * leave it as is. */ private void stopRunningUpdate() { - Toast.makeText(this, - "stopRunningUpdate is not implemented", - Toast.LENGTH_SHORT).show(); - + try { + mUpdateEngine.cancel(); + } catch (Exception e) { + Log.w(TAG, "UpdateEngine failed to stop the ongoing update", e); + } } /** @@ -291,9 +339,11 @@ public class MainActivity extends Activity { * update has been applied. */ private void resetUpdate() { - Toast.makeText(this, - "resetUpdate is not implemented", - Toast.LENGTH_SHORT).show(); + try { + mUpdateEngine.resetStatus(); + } catch (Exception e) { + Log.w(TAG, "UpdateEngine failed to reset the update", e); + } } /** diff --git a/updater_sample/src/com/example/android/systemupdatersample/updates/AbNonStreamingUpdate.java b/updater_sample/src/com/example/android/systemupdatersample/updates/AbNonStreamingUpdate.java deleted file mode 100644 index 1b91a1ac3..000000000 --- a/updater_sample/src/com/example/android/systemupdatersample/updates/AbNonStreamingUpdate.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.updates; - -import android.os.UpdateEngine; - -import com.example.android.systemupdatersample.PayloadSpec; -import com.example.android.systemupdatersample.UpdateConfig; -import com.example.android.systemupdatersample.util.PayloadSpecs; - -/** - * Applies A/B (seamless) non-streaming update. - */ -public class AbNonStreamingUpdate { - - private final UpdateEngine mUpdateEngine; - private final UpdateConfig mUpdateConfig; - - public AbNonStreamingUpdate(UpdateEngine updateEngine, UpdateConfig config) { - this.mUpdateEngine = updateEngine; - this.mUpdateConfig = config; - } - - /** - * Start applying the update. This method doesn't wait until end of the update. - * {@code update_engine} works asynchronously. - */ - public void execute() throws Exception { - PayloadSpec payload = PayloadSpecs.forNonStreaming(mUpdateConfig.getUpdatePackageFile()); - - mUpdateEngine.applyPayload( - payload.getUrl(), - payload.getOffset(), - payload.getSize(), - payload.getProperties().toArray(new String[0])); - } - -} diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java index 806f17351..5c1d71117 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java +++ b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java @@ -40,7 +40,7 @@ public final class FileDownloader { private long mSize; private File mOut; - public FileDownloader(String url, long offset, long size, File out) { + public FileDownloader(String url, long offset, long size, File out) { this.mUrl = url; this.mOffset = offset; this.mSize = size; diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java index dc7ec09e1..80506ee6d 100644 --- a/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java +++ b/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java @@ -60,7 +60,7 @@ public class FileDownloaderTest { File packageFile = Paths .get(mTargetContext.getCacheDir().getAbsolutePath(), "ota.zip") .toFile(); - Files.delete(packageFile.toPath()); + Files.deleteIfExists(packageFile.toPath()); Files.copy(mTestContext.getResources().openRawResource(R.raw.ota_002_package), packageFile.toPath()); String url = "file://" + packageFile.getAbsolutePath(); @@ -68,7 +68,7 @@ public class FileDownloaderTest { File outFile = Paths .get(mTargetContext.getCacheDir().getAbsolutePath(), "care_map.txt") .toFile(); - Files.delete(outFile.toPath()); + Files.deleteIfExists(outFile.toPath()); // download a chunk of ota.zip FileDownloader downloader = new FileDownloader(url, 160, 8, outFile); downloader.download(); |