Merge "updater_sample: update ui and README, clean-up"

This commit is contained in:
Zhomart Mukhamejanov
2018-05-03 17:47:59 +00:00
committed by Gerrit Code Review
7 changed files with 151 additions and 98 deletions
+45 -15
View File
@@ -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 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/`. `/data/user/0/com.example.android.systemupdatersample/files/configs/`.
SystemUpdaterSample app downloads OTA package from `url`. If `ab_install_type` SystemUpdaterSample app downloads OTA package from `url`. In this sample app
is `NON_STREAMING` then app downloads the whole package and `url` is expected to point to file system, e.g. `file:///data/sample-builds/ota-002.zip`.
passes it to the `update_engine`. If `ab_install_type` is `STREAMING`
then app downloads only some files to prepare the streaming update and If `ab_install_type` is `NON_STREAMING` then app checks if `url` starts
`update_engine` will stream only `payload.bin`. with `file://` and passes `url` to the `update_engine`.
To support streaming A/B (seamless) update, OTA package file must be
an uncompressed (ZIP_STORED) zip file. 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`. Config files can be generated using `tools/gen_update_config.py`.
Running `./tools/gen_update_config.py --help` shows usage of the script. 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 ## 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`. 1. Compile the app `$ mmma bootable/recovery/updater_sample`.
2. Install the app to the device using `$ adb install <APK_PATH>`. 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 ## 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
Run adb shell as a root, and set SELinux mode to permissive (0): `android.os.UpdateEngine`` APIs are marked as `@SystemApi`, meaning only system apps can access them.
## 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 ```txt
$ adb root local$ adb root
$ adb shell local$ adb shell
# setenforce 0 android# setenforce 0
# getenforce android# getenforce
``` ```
## License
SystemUpdaterSample app is released under
[Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0).
+23 -1
View File
@@ -114,7 +114,7 @@
android:id="@+id/textView" android:id="@+id/textView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Running update status:" /> android:text="Update status:" />
<TextView <TextView
android:id="@+id/textViewStatus" android:id="@+id/textViewStatus"
@@ -124,6 +124,28 @@
android:text="@string/unknown" /> android:text="@string/unknown" />
</LinearLayout> </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 <ProgressBar
android:id="@+id/progressBar" android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal" style="?android:attr/progressBarStyleHorizontal"
@@ -18,12 +18,15 @@ package com.example.android.systemupdatersample;
import android.os.UpdateEngine; import android.os.UpdateEngine;
import java.io.Serializable;
import java.util.List; import java.util.List;
/** /**
* Payload that will be given to {@link UpdateEngine#applyPayload)}. * 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} * Creates a payload spec {@link Builder}
@@ -31,13 +31,15 @@ import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.example.android.systemupdatersample.PayloadSpec;
import com.example.android.systemupdatersample.R; import com.example.android.systemupdatersample.R;
import com.example.android.systemupdatersample.UpdateConfig; 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.UpdateConfigs;
import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes; import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes;
import com.example.android.systemupdatersample.util.UpdateEngineStatuses; import com.example.android.systemupdatersample.util.UpdateEngineStatuses;
import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@@ -46,6 +48,8 @@ import java.util.concurrent.atomic.AtomicInteger;
*/ */
public class MainActivity extends Activity { public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private TextView mTextViewBuild; private TextView mTextViewBuild;
private Spinner mSpinnerConfigs; private Spinner mSpinnerConfigs;
private TextView mTextViewConfigsDirHint; private TextView mTextViewConfigsDirHint;
@@ -55,17 +59,19 @@ public class MainActivity extends Activity {
private Button mButtonReset; private Button mButtonReset;
private ProgressBar mProgressBar; private ProgressBar mProgressBar;
private TextView mTextViewStatus; private TextView mTextViewStatus;
private TextView mTextViewCompletion;
private List<UpdateConfig> mConfigs; private List<UpdateConfig> mConfigs;
private AtomicInteger mUpdateEngineStatus = private AtomicInteger mUpdateEngineStatus =
new AtomicInteger(UpdateEngine.UpdateStatusConstants.IDLE); new AtomicInteger(UpdateEngine.UpdateStatusConstants.IDLE);
private UpdateEngine mUpdateEngine = new UpdateEngine();
/** /**
* Listen to {@code update_engine} events. * Listen to {@code update_engine} events.
*/ */
private UpdateEngineCallbackImpl mUpdateEngineCallback = new UpdateEngineCallbackImpl(); private UpdateEngineCallbackImpl mUpdateEngineCallback = new UpdateEngineCallbackImpl();
private final UpdateEngine mUpdateEngine = new UpdateEngine();
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -80,14 +86,14 @@ public class MainActivity extends Activity {
this.mButtonReset = findViewById(R.id.buttonReset); this.mButtonReset = findViewById(R.id.buttonReset);
this.mProgressBar = findViewById(R.id.progressBar); this.mProgressBar = findViewById(R.id.progressBar);
this.mTextViewStatus = findViewById(R.id.textViewStatus); this.mTextViewStatus = findViewById(R.id.textViewStatus);
this.mTextViewCompletion = findViewById(R.id.textViewCompletion);
this.mUpdateEngine.bind(mUpdateEngineCallback);
this.mTextViewConfigsDirHint.setText(UpdateConfigs.getConfigsRoot(this)); this.mTextViewConfigsDirHint.setText(UpdateConfigs.getConfigsRoot(this));
uiReset(); uiReset();
loadUpdateConfigs(); loadUpdateConfigs();
this.mUpdateEngine.bind(mUpdateEngineCallback);
} }
@Override @Override
@@ -140,7 +146,6 @@ public class MainActivity extends Activity {
.setMessage("Do you really want to cancel running update?") .setMessage("Do you really want to cancel running update?")
.setIcon(android.R.drawable.ic_dialog_alert) .setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
uiReset();
stopRunningUpdate(); stopRunningUpdate();
}) })
.setNegativeButton(android.R.string.cancel, null).show(); .setNegativeButton(android.R.string.cancel, null).show();
@@ -156,7 +161,6 @@ public class MainActivity extends Activity {
+ " and restore old version?") + " and restore old version?")
.setIcon(android.R.drawable.ic_dialog_alert) .setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
uiReset();
resetUpdate(); resetUpdate();
}) })
.setNegativeButton(android.R.string.cancel, null).show(); .setNegativeButton(android.R.string.cancel, null).show();
@@ -178,6 +182,13 @@ public class MainActivity extends Activity {
setUiStatus(status); setUiStatus(status);
Toast.makeText(this, "Update Status changed", Toast.LENGTH_LONG) Toast.makeText(this, "Update Status changed", Toast.LENGTH_LONG)
.show(); .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}. * values from {@link UpdateEngine.ErrorCodeConstants}.
*/ */
private void onPayloadApplicationComplete(int errorCode) { private void onPayloadApplicationComplete(int errorCode) {
final String state = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode)
? "SUCCESS"
: "FAILURE";
runOnUiThread(() -> { runOnUiThread(() -> {
final String state = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode)
? "SUCCESS"
: "FAILURE";
Log.i("UpdateEngine", Log.i("UpdateEngine",
"Completed - errorCode=" "Completed - errorCode="
+ UpdateEngineErrorCodes.getCodeName(errorCode) + "/" + errorCode + UpdateEngineErrorCodes.getCodeName(errorCode) + "/" + errorCode
+ " " + state); + " " + state);
Toast.makeText(this, "Update completed", Toast.LENGTH_LONG).show(); Toast.makeText(this, "Update completed", Toast.LENGTH_LONG).show();
setUiCompletion(errorCode);
}); });
} }
@@ -212,6 +224,7 @@ public class MainActivity extends Activity {
mProgressBar.setEnabled(false); mProgressBar.setEnabled(false);
mProgressBar.setVisibility(ProgressBar.INVISIBLE); mProgressBar.setVisibility(ProgressBar.INVISIBLE);
mTextViewStatus.setText(R.string.unknown); mTextViewStatus.setText(R.string.unknown);
mTextViewCompletion.setText(R.string.unknown);
} }
/** sets ui updating mode */ /** sets ui updating mode */
@@ -239,7 +252,18 @@ public class MainActivity extends Activity {
*/ */
private void setUiStatus(int status) { private void setUiStatus(int status) {
String statusText = UpdateEngineStatuses.getStatusText(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) { private void loadConfigsToSpinner(List<UpdateConfig> configs) {
@@ -259,19 +283,42 @@ public class MainActivity extends Activity {
/** /**
* Applies the given update * Applies the given update
*/ */
private void applyUpdate(UpdateConfig config) { private void applyUpdate(final UpdateConfig config) {
if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) { if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) {
AbNonStreamingUpdate update = new AbNonStreamingUpdate(mUpdateEngine, config); PayloadSpec payload;
try { try {
update.execute(); payload = PayloadSpecs.forNonStreaming(config.getUpdatePackageFile());
} catch (Exception e) { } catch (IOException e) {
Log.e("MainActivity", "Error applying the update", e); Log.e(TAG, "Error creating payload spec", e);
Toast.makeText(this, "Error applying the update", Toast.LENGTH_SHORT) Toast.makeText(this, "Error creating payload spec", Toast.LENGTH_LONG)
.show(); .show();
return;
} }
updateEngineApplyPayload(payload);
} else { } else {
Toast.makeText(this, "Streaming is not implemented", Toast.LENGTH_SHORT) Log.d(TAG, "Starting PrepareStreamingService");
.show(); }
}
/**
* 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. * leave it as is.
*/ */
private void stopRunningUpdate() { private void stopRunningUpdate() {
Toast.makeText(this, try {
"stopRunningUpdate is not implemented", mUpdateEngine.cancel();
Toast.LENGTH_SHORT).show(); } 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. * update has been applied.
*/ */
private void resetUpdate() { private void resetUpdate() {
Toast.makeText(this, try {
"resetUpdate is not implemented", mUpdateEngine.resetStatus();
Toast.LENGTH_SHORT).show(); } catch (Exception e) {
Log.w(TAG, "UpdateEngine failed to reset the update", e);
}
} }
/** /**
@@ -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]));
}
}
@@ -40,7 +40,7 @@ public final class FileDownloader {
private long mSize; private long mSize;
private File mOut; 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.mUrl = url;
this.mOffset = offset; this.mOffset = offset;
this.mSize = size; this.mSize = size;
@@ -60,7 +60,7 @@ public class FileDownloaderTest {
File packageFile = Paths File packageFile = Paths
.get(mTargetContext.getCacheDir().getAbsolutePath(), "ota.zip") .get(mTargetContext.getCacheDir().getAbsolutePath(), "ota.zip")
.toFile(); .toFile();
Files.delete(packageFile.toPath()); Files.deleteIfExists(packageFile.toPath());
Files.copy(mTestContext.getResources().openRawResource(R.raw.ota_002_package), Files.copy(mTestContext.getResources().openRawResource(R.raw.ota_002_package),
packageFile.toPath()); packageFile.toPath());
String url = "file://" + packageFile.getAbsolutePath(); String url = "file://" + packageFile.getAbsolutePath();
@@ -68,7 +68,7 @@ public class FileDownloaderTest {
File outFile = Paths File outFile = Paths
.get(mTargetContext.getCacheDir().getAbsolutePath(), "care_map.txt") .get(mTargetContext.getCacheDir().getAbsolutePath(), "care_map.txt")
.toFile(); .toFile();
Files.delete(outFile.toPath()); Files.deleteIfExists(outFile.toPath());
// download a chunk of ota.zip // download a chunk of ota.zip
FileDownloader downloader = new FileDownloader(url, 160, 8, outFile); FileDownloader downloader = new FileDownloader(url, 160, 8, outFile);
downloader.download(); downloader.download();