Merge "updater_sample: update ui and README, clean-up"
am: 9c544a2bc6
Change-Id: I01da369a194bdb0b4d6d698dd835ce3bf9904756
This commit is contained in:
+45
-15
@@ -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
|
||||
|
||||
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
|
||||
$ 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).
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
-52
@@ -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 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;
|
||||
|
||||
+2
-2
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user