Merge "sample_updater: add non-streaming demo"

This commit is contained in:
Zhomart Mukhamejanov 2018-04-25 18:56:26 +00:00 committed by Gerrit Code Review
commit 4816fc1c46
26 changed files with 1723 additions and 97 deletions

9
sample_updater/.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
*~
*.bak
*.pyc
*.pyc-2.4
Thumbs.db
*.iml
.idea/
gen/
.vscode

View file

@ -15,13 +15,18 @@
#
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_PACKAGE_NAME := SystemUpdateApp
LOCAL_PACKAGE_NAME := SystemUpdaterSample
LOCAL_SDK_VERSION := system_current
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE_TAGS := samples
# TODO: enable proguard and use proguard.flags file
LOCAL_PROGUARD_ENABLED := disabled
LOCAL_SRC_FILES := $(call all-java-files-under, src)
include $(BUILD_PACKAGE)
# Use the following include to make our test apk.
include $(call all-makefiles-under,$(LOCAL_PATH))

View file

@ -15,11 +15,17 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.update">
package="com.example.android.systemupdatersample">
<application android:label="Sample Updater">
<activity android:name=".ui.SystemUpdateActivity"
android:label="Sample Updater">
<uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27" />
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round">
<activity
android:name=".ui.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@ -28,4 +34,3 @@
</application>
</manifest>

View file

@ -1 +1,72 @@
# System update sample app.
# SystemUpdaterSample
This app demonstrates how to use Android system updates APIs to install
[OTA updates](https://source.android.com/devices/tech/ota/). It contains a sample
client for `update_engine` to install A/B (seamless) updates and a sample of
applying non-A/B updates using `recovery`.
A/B (seamless) update is available since Android Nougat (API 24), but this sample
targets the latest android.
## Running on a device
The commands expected to be run from `$ANDROID_BUILD_TOP`.
1. Compile the app `$ mmma bootable/recovery/sample_updater`.
2. Install the app to the device using `$ adb install <APK_PATH>`.
3. Add update config files.
## Update Config file
Directory can be found in logs or on UI. Usually json config files are located in
`/data/user/0/com.example.android.systemupdatersample/files/configs/`. Example file
is located at `res/raw/sample.json`.
## Development
- [x] Create a UI with list of configs, current version,
control buttons, progress bar and log viewer
- [x] Add `PayloadSpec` and `PayloadSpecs` for working with
update zip file
- [x] Add `UpdateConfig` for working with json config files
- [x] Add applying non-streaming update
- [ ] Add applying streaming update
- [ ] Prepare streaming update (partially downloading package)
- [ ] Add tests for `MainActivity`
- [ ] Add stop/reset the update
- [ ] Verify system partition checksum for package
- [ ] HAL compatibility check
- [ ] Change partition demo
- [ ] Add non-A/B updates demo
## Running tests
1. Build `$ mmma bootable/recovery/sample_updater/`
2. Install app
`$ adb install $OUT/system/app/SystemUpdaterSample/SystemUpdaterSample.apk`
3. Install tests
`$ adb install $OUT/testcases/SystemUpdaterSampleTests/SystemUpdaterSampleTests.apk`
4. Run tests
`$ adb shell am instrument -w com.example.android.systemupdatersample.tests/android.support.test.runner.AndroidJUnitRunner`
5. Run a test file
```
$ adb shell am instrument \
-w com.example.android.systemupdatersample.tests/android.support.test.runner.AndroidJUnitRunner \
-c com.example.android.systemupdatersample.util.PayloadSpecsTest
```
## Getting access to `update_engine` API and read/write access to `/data`
Run adb shell as a root, and set SELinux mode to permissive (0):
```txt
$ adb root
$ adb shell
# setenforce 0
# getenforce
```

View file

@ -1,5 +1,6 @@
<!--
Copyright (C) 2018 The Android Open Source Project
<?xml version="1.0" encoding="utf-8"?>
<!-- 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
@ -14,7 +15,149 @@
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent">
android:orientation="vertical"
android:padding="4dip"
android:gravity="center_horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="vertical">
<TextView
android:id="@+id/textViewBuildtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Current Build:" />
<TextView
android:id="@+id/textViewBuild"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/unknown" />
<Space
android:layout_width="match_parent"
android:layout_height="40dp" />
<TextView
android:id="@+id/textView4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Apply an update" />
<TextView
android:id="@+id/textViewConfigsDirHint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Config files located in NULL"
android:textColor="#777"
android:textSize="10sp"
android:textStyle="italic" />
<Spinner
android:id="@+id/spinnerConfigs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<Button
android:id="@+id/buttonReload"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="onReloadClick"
android:text="Reload" />
<Button
android:id="@+id/buttonViewConfig"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="onViewConfigClick"
android:text="View config" />
<Button
android:id="@+id/buttonApplyConfig"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="onApplyConfigClick"
android:text="Apply" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:orientation="horizontal">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Running update status:" />
<TextView
android:id="@+id/textViewStatus"
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"
android:layout_marginTop="8dp"
android:min="0"
android:max="100"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal">
<Button
android:id="@+id/buttonStop"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="onStopClick"
android:text="Stop" />
<Button
android:id="@+id/buttonReset"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="onResetClick"
android:text="Reset" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -0,0 +1,22 @@
{
"__name": "name will be visible on UI",
"__url": "https:// or file:// uri to update file (zip, xz, ...)",
"__type": "NON_STREAMING (from local file) OR STREAMING (on the fly)",
"name": "SAMPLE-cake-release BUILD-12345",
"url": "file:///data/builds/android-update.zip",
"type": "NON_STREAMING",
"streaming_metadata": {
"__": "streaming_metadata is required only for streaming update",
"__property_files": "name, offset and size of files",
"property_files": [
{
"__filename": "payload.bin and payload_properties.txt are required",
"__offset": "defines beginning of update data in archive",
"__size": "size of the update data in archive",
"filename": "payload.bin",
"offset": 531,
"size": 5012323
}
]
}
}

View file

@ -0,0 +1,21 @@
<!-- 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.
-->
<resources>
<string name="app_name">SystemUpdaterSample</string>
<string name="action_reload">Reload</string>
<string name="unknown">Unknown</string>
<string name="close">CLOSE</string>
</resources>

View file

@ -1,68 +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.android.update.ui;
import android.app.Activity;
import android.os.UpdateEngine;
import android.os.UpdateEngineCallback;
/** Main update activity. */
public class SystemUpdateActivity extends Activity {
private UpdateEngine updateEngine;
private UpdateEngineCallbackImpl updateEngineCallbackImpl = new UpdateEngineCallbackImpl(this);
@Override
public void onResume() {
super.onResume();
updateEngine = new UpdateEngine();
updateEngine.bind(updateEngineCallbackImpl);
}
@Override
public void onPause() {
updateEngine.unbind();
super.onPause();
}
void onStatusUpdate(int i, float v) {
// Handle update engine status update
}
void onPayloadApplicationComplete(int i) {
// Handle apply payload completion
}
private static class UpdateEngineCallbackImpl extends UpdateEngineCallback {
private final SystemUpdateActivity activity;
public UpdateEngineCallbackImpl(SystemUpdateActivity activity) {
this.activity = activity;
}
@Override
public void onStatusUpdate(int i, float v) {
activity.onStatusUpdate(i, v);
}
@Override
public void onPayloadApplicationComplete(int i) {
activity.onPayloadApplicationComplete(i);
}
}
}

View file

@ -0,0 +1,122 @@
/*
* 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;
import android.os.UpdateEngine;
import java.util.List;
/**
* Payload that will be given to {@link UpdateEngine#applyPayload)}.
*/
public class PayloadSpec {
/**
* Creates a payload spec {@link Builder}
*/
public static Builder newBuilder() {
return new Builder();
}
private String mUrl;
private long mOffset;
private long mSize;
private List<String> mProperties;
public PayloadSpec(Builder b) {
this.mUrl = b.mUrl;
this.mOffset = b.mOffset;
this.mSize = b.mSize;
this.mProperties = b.mProperties;
}
public String getUrl() {
return mUrl;
}
public long getOffset() {
return mOffset;
}
public long getSize() {
return mSize;
}
public List<String> getProperties() {
return mProperties;
}
/**
* payload spec builder.
*
* <p>Usage:</p>
*
* {@code
* PayloadSpec spec = PayloadSpec.newBuilder()
* .url("url")
* .build();
* }
*/
public static class Builder {
private String mUrl;
private long mOffset;
private long mSize;
private List<String> mProperties;
public Builder() {
}
/**
* set url
*/
public Builder url(String url) {
this.mUrl = url;
return this;
}
/**
* set offset
*/
public Builder offset(long offset) {
this.mOffset = offset;
return this;
}
/**
* set size
*/
public Builder size(long size) {
this.mSize = size;
return this;
}
/**
* set properties
*/
public Builder properties(List<String> properties) {
this.mProperties = properties;
return this;
}
/**
* build {@link PayloadSpec}
*/
public PayloadSpec build() {
return new PayloadSpec(this);
}
}
}

View file

@ -0,0 +1,183 @@
/*
* 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;
import android.os.Parcel;
import android.os.Parcelable;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.Serializable;
/**
* UpdateConfig describes an update. It will be parsed from JSON, which is intended to
* be sent from server to the update app, but in this sample app it will be stored on the device.
*/
public class UpdateConfig implements Parcelable {
public static final int TYPE_NON_STREAMING = 0;
public static final int TYPE_STREAMING = 1;
public static final Parcelable.Creator<UpdateConfig> CREATOR =
new Parcelable.Creator<UpdateConfig>() {
@Override
public UpdateConfig createFromParcel(Parcel source) {
return new UpdateConfig(source);
}
@Override
public UpdateConfig[] newArray(int size) {
return new UpdateConfig[size];
}
};
/** parse update config from json */
public static UpdateConfig fromJson(String json) throws JSONException {
UpdateConfig c = new UpdateConfig();
JSONObject o = new JSONObject(json);
c.mName = o.getString("name");
c.mUrl = o.getString("url");
if (TYPE_NON_STREAMING_JSON.equals(o.getString("type"))) {
c.mInstallType = TYPE_NON_STREAMING;
} else if (TYPE_STREAMING_JSON.equals(o.getString("type"))) {
c.mInstallType = TYPE_STREAMING;
} else {
throw new JSONException("Invalid type, expected either "
+ "NON_STREAMING or STREAMING, got " + o.getString("type"));
}
if (o.has("metadata")) {
c.mMetadata = new Metadata(
o.getJSONObject("metadata").getInt("offset"),
o.getJSONObject("metadata").getInt("size"));
}
c.mRawJson = json;
return c;
}
/**
* these strings are represent types in JSON config files
*/
private static final String TYPE_NON_STREAMING_JSON = "NON_STREAMING";
private static final String TYPE_STREAMING_JSON = "STREAMING";
/** name will be visible on UI */
private String mName;
/** update zip file URI, can be https:// or file:// */
private String mUrl;
/** non-streaming (first saves locally) OR streaming (on the fly) */
private int mInstallType;
/** metadata is required only for streaming update */
private Metadata mMetadata;
private String mRawJson;
protected UpdateConfig() {
}
protected UpdateConfig(Parcel in) {
this.mName = in.readString();
this.mUrl = in.readString();
this.mInstallType = in.readInt();
this.mMetadata = (Metadata) in.readSerializable();
this.mRawJson = in.readString();
}
public UpdateConfig(String name, String url, int installType) {
this.mName = name;
this.mUrl = url;
this.mInstallType = installType;
}
public String getName() {
return mName;
}
public String getUrl() {
return mUrl;
}
public String getRawJson() {
return mRawJson;
}
public int getInstallType() {
return mInstallType;
}
/**
* "url" must be the file located on the device.
*
* @return File object for given url
*/
public File getUpdatePackageFile() {
if (mInstallType != TYPE_NON_STREAMING) {
throw new RuntimeException("Expected non-streaming install type");
}
if (!mUrl.startsWith("file://")) {
throw new RuntimeException("url is expected to start with file://");
}
return new File(mUrl.substring(7, mUrl.length()));
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mName);
dest.writeString(mUrl);
dest.writeInt(mInstallType);
dest.writeSerializable(mMetadata);
dest.writeString(mRawJson);
}
/**
* Metadata for STREAMING update
*/
public static class Metadata implements Serializable {
private static final long serialVersionUID = 31042L;
/** defines beginning of update data in archive */
private long mOffset;
/** size of the update data in archive */
private long mSize;
public Metadata(long offset, long size) {
this.mOffset = offset;
this.mSize = size;
}
public long getOffset() {
return mOffset;
}
public long getSize() {
return mSize;
}
}
}

View file

@ -0,0 +1,314 @@
/*
* 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.ui;
import android.app.Activity;
import android.app.AlertDialog;
import android.os.Build;
import android.os.Bundle;
import android.os.UpdateEngine;
import android.os.UpdateEngineCallback;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
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.UpdateConfigs;
import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes;
import com.example.android.systemupdatersample.util.UpdateEngineStatuses;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* UI for SystemUpdaterSample app.
*/
public class MainActivity extends Activity {
private TextView mTextViewBuild;
private Spinner mSpinnerConfigs;
private TextView mTextViewConfigsDirHint;
private Button mButtonReload;
private Button mButtonApplyConfig;
private Button mButtonStop;
private Button mButtonReset;
private ProgressBar mProgressBar;
private TextView mTextViewStatus;
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();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.mTextViewBuild = findViewById(R.id.textViewBuild);
this.mSpinnerConfigs = findViewById(R.id.spinnerConfigs);
this.mTextViewConfigsDirHint = findViewById(R.id.textViewConfigsDirHint);
this.mButtonReload = findViewById(R.id.buttonReload);
this.mButtonApplyConfig = findViewById(R.id.buttonApplyConfig);
this.mButtonStop = findViewById(R.id.buttonStop);
this.mButtonReset = findViewById(R.id.buttonReset);
this.mProgressBar = findViewById(R.id.progressBar);
this.mTextViewStatus = findViewById(R.id.textViewStatus);
this.mUpdateEngine.bind(mUpdateEngineCallback);
this.mTextViewConfigsDirHint.setText(UpdateConfigs.getConfigsRoot(this));
uiReset();
loadUpdateConfigs();
}
@Override
protected void onDestroy() {
this.mUpdateEngine.unbind();
super.onDestroy();
}
/**
* reload button is clicked
*/
public void onReloadClick(View view) {
loadUpdateConfigs();
}
/**
* view config button is clicked
*/
public void onViewConfigClick(View view) {
UpdateConfig config = mConfigs.get(mSpinnerConfigs.getSelectedItemPosition());
new AlertDialog.Builder(this)
.setTitle(config.getName())
.setMessage(config.getRawJson())
.setPositiveButton(R.string.close, (dialog, id) -> dialog.dismiss())
.show();
}
/**
* apply config button is clicked
*/
public void onApplyConfigClick(View view) {
new AlertDialog.Builder(this)
.setTitle("Apply Update")
.setMessage("Do you really want to apply this update?")
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
uiSetUpdating();
applyUpdate(getSelectedConfig());
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
/**
* stop button clicked
*/
public void onStopClick(View view) {
new AlertDialog.Builder(this)
.setTitle("Stop Update")
.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();
}
/**
* reset button clicked
*/
public void onResetClick(View view) {
new AlertDialog.Builder(this)
.setTitle("Reset Update")
.setMessage("Do you really want to cancel running update"
+ " 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();
}
/**
* 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}.
*/
private void onStatusUpdate(int status, float percent) {
mProgressBar.setProgress((int) (100 * percent));
if (mUpdateEngineStatus.get() != status) {
mUpdateEngineStatus.set(status);
runOnUiThread(() -> {
Log.e("UpdateEngine", "StatusUpdate - status="
+ UpdateEngineStatuses.getStatusText(status)
+ "/" + status);
setUiStatus(status);
Toast.makeText(this, "Update Status changed", Toast.LENGTH_LONG)
.show();
});
}
}
/**
* Invoked when the payload has been applied, whether successfully or
* unsuccessfully. The value of {@code errorCode} will be one of the
* values from {@link UpdateEngine.ErrorCodeConstants}.
*/
private void onPayloadApplicationComplete(int errorCode) {
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();
});
}
/** resets ui */
private void uiReset() {
mTextViewBuild.setText(Build.DISPLAY);
mSpinnerConfigs.setEnabled(true);
mButtonReload.setEnabled(true);
mButtonApplyConfig.setEnabled(true);
mButtonStop.setEnabled(false);
mButtonReset.setEnabled(false);
mProgressBar.setProgress(0);
mProgressBar.setEnabled(false);
mProgressBar.setVisibility(ProgressBar.INVISIBLE);
mTextViewStatus.setText(R.string.unknown);
}
/** sets ui updating mode */
private void uiSetUpdating() {
mTextViewBuild.setText(Build.DISPLAY);
mSpinnerConfigs.setEnabled(false);
mButtonReload.setEnabled(false);
mButtonApplyConfig.setEnabled(false);
mButtonStop.setEnabled(true);
mProgressBar.setEnabled(true);
mButtonReset.setEnabled(true);
mProgressBar.setVisibility(ProgressBar.VISIBLE);
}
/**
* loads json configurations from configs dir that is defined in {@link UpdateConfigs}.
*/
private void loadUpdateConfigs() {
mConfigs = UpdateConfigs.getUpdateConfigs(this);
loadConfigsToSpinner(mConfigs);
}
/**
* @param status update engine status code
*/
private void setUiStatus(int status) {
String statusText = UpdateEngineStatuses.getStatusText(status);
mTextViewStatus.setText(statusText);
}
private void loadConfigsToSpinner(List<UpdateConfig> configs) {
String[] spinnerArray = UpdateConfigs.configsToNames(configs);
ArrayAdapter<String> spinnerArrayAdapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item,
spinnerArray);
spinnerArrayAdapter.setDropDownViewResource(android.R.layout
.simple_spinner_dropdown_item);
mSpinnerConfigs.setAdapter(spinnerArrayAdapter);
}
private UpdateConfig getSelectedConfig() {
return mConfigs.get(mSpinnerConfigs.getSelectedItemPosition());
}
/**
* Applies the given update
*/
private void applyUpdate(UpdateConfig config) {
if (config.getInstallType() == UpdateConfig.TYPE_NON_STREAMING) {
AbNonStreamingUpdate update = new AbNonStreamingUpdate(mUpdateEngine, config);
try {
update.execute();
} catch (Exception e) {
Log.e("MainActivity", "Error applying the update", e);
Toast.makeText(this, "Error applying the update", Toast.LENGTH_SHORT)
.show();
}
} else {
Toast.makeText(this, "Streaming is not implemented", Toast.LENGTH_SHORT)
.show();
}
}
/**
* Requests update engine to stop any ongoing update. If an update has been applied,
* leave it as is.
*/
private void stopRunningUpdate() {
Toast.makeText(this,
"stopRunningUpdate is not implemented",
Toast.LENGTH_SHORT).show();
}
/**
* Resets update engine to IDLE state. Requests to cancel any onging update, or to revert if an
* update has been applied.
*/
private void resetUpdate() {
Toast.makeText(this,
"resetUpdate is not implemented",
Toast.LENGTH_SHORT).show();
}
/**
* Helper class to delegate UpdateEngine callbacks to MainActivity
*/
class UpdateEngineCallbackImpl extends UpdateEngineCallback {
@Override
public void onStatusUpdate(int status, float percent) {
MainActivity.this.onStatusUpdate(status, percent);
}
@Override
public void onPayloadApplicationComplete(int errorCode) {
MainActivity.this.onPayloadApplicationComplete(errorCode);
}
}
}

View file

@ -0,0 +1,52 @@
/*
* 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]));
}
}

View file

@ -0,0 +1,42 @@
/*
* 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 property files in a package. */
public final class PackagePropertyFiles {
public static final String PAYLOAD_BINARY_FILE_NAME = "payload.bin";
public static final String PAYLOAD_HEADER_FILE_NAME = "payload_header.bin";
public static final String PAYLOAD_METADATA_FILE_NAME = "payload_metadata.bin";
public static final String PAYLOAD_PROPERTIES_FILE_NAME = "payload_properties.txt";
/** The zip entry in an A/B OTA package, which will be used by update_verifier. */
public static final String CARE_MAP_FILE_NAME = "care_map.txt";
public static final String METADATA_FILE_NAME = "metadata";
/**
* The zip file that claims the compatibility of the update package to check against the Android
* framework to ensure that the package can be installed on the device.
*/
public static final String COMPATIBILITY_ZIP_FILE_NAME = "compatibility.zip";
private PackagePropertyFiles() {}
}

View file

@ -0,0 +1,117 @@
/*
* 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;
import android.annotation.TargetApi;
import android.os.Build;
import com.example.android.systemupdatersample.PayloadSpec;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/** The helper class that creates {@link PayloadSpec}. */
@TargetApi(Build.VERSION_CODES.N)
public final class PayloadSpecs {
/**
* The payload PAYLOAD_ENTRY is stored in the zip package to comply with the Android OTA package
* format. We want to find out the offset of the entry, so that we can pass it over to the A/B
* updater without making an extra copy of the payload.
*
* <p>According to Android docs, the entries are listed in the order in which they appear in the
* zip file. So we enumerate the entries to identify the offset of the payload file.
* http://developer.android.com/reference/java/util/zip/ZipFile.html#entries()
*/
public static PayloadSpec forNonStreaming(File packageFile) throws IOException {
boolean payloadFound = false;
long payloadOffset = 0;
long payloadSize = 0;
List<String> properties = new ArrayList<>();
try (ZipFile zip = new ZipFile(packageFile)) {
Enumeration<? extends ZipEntry> entries = zip.entries();
long offset = 0;
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String name = entry.getName();
// Zip local file header has 30 bytes + filename + sizeof extra field.
// https://en.wikipedia.org/wiki/Zip_(file_format)
long extraSize = entry.getExtra() == null ? 0 : entry.getExtra().length;
offset += 30 + name.length() + extraSize;
if (entry.isDirectory()) {
continue;
}
long length = entry.getCompressedSize();
if (PackagePropertyFiles.PAYLOAD_BINARY_FILE_NAME.equals(name)) {
if (entry.getMethod() != ZipEntry.STORED) {
throw new IOException("Invalid compression method.");
}
payloadFound = true;
payloadOffset = offset;
payloadSize = length;
} else if (PackagePropertyFiles.PAYLOAD_PROPERTIES_FILE_NAME.equals(name)) {
InputStream inputStream = zip.getInputStream(entry);
if (inputStream != null) {
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = br.readLine()) != null) {
properties.add(line);
}
}
}
offset += length;
}
}
if (!payloadFound) {
throw new IOException("Failed to find payload entry in the given package.");
}
return PayloadSpec.newBuilder()
.url("file://" + packageFile.getAbsolutePath())
.offset(payloadOffset)
.size(payloadSize)
.properties(properties)
.build();
}
/**
* Converts an {@link PayloadSpec} to a string.
*/
public static String toString(PayloadSpec payloadSpec) {
return "<PayloadSpec url=" + payloadSpec.getUrl()
+ ", offset=" + payloadSpec.getOffset()
+ ", size=" + payloadSpec.getSize()
+ ", properties=" + Arrays.toString(
payloadSpec.getProperties().toArray(new String[0]))
+ ">";
}
private PayloadSpecs() {}
}

View file

@ -0,0 +1,82 @@
/*
* 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;
import android.content.Context;
import com.example.android.systemupdatersample.UpdateConfig;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
/**
* Utility class for working with json update configurations.
*/
public final class UpdateConfigs {
private static final String UPDATE_CONFIGS_ROOT = "configs/";
/**
* @param configs update configs
* @return list of names
*/
public static String[] configsToNames(List<UpdateConfig> configs) {
return configs.stream().map(UpdateConfig::getName).toArray(String[]::new);
}
/**
* @param context app context
* @return configs root directory
*/
public static String getConfigsRoot(Context context) {
return Paths.get(context.getFilesDir().toString(),
UPDATE_CONFIGS_ROOT).toString();
}
/**
* It parses only {@code .json} files.
*
* @param context application context
* @return list of configs from directory {@link UpdateConfigs#getConfigsRoot}
*/
public static List<UpdateConfig> getUpdateConfigs(Context context) {
File root = new File(getConfigsRoot(context));
ArrayList<UpdateConfig> configs = new ArrayList<>();
if (!root.exists()) {
return configs;
}
for (final File f : root.listFiles()) {
if (!f.isDirectory() && f.getName().endsWith(".json")) {
try {
String json = new String(Files.readAllBytes(f.toPath()),
StandardCharsets.UTF_8);
configs.add(UpdateConfig.fromJson(json));
} catch (Exception e) {
throw new RuntimeException(
"Can't read/parse config file " + f.getName(), e);
}
}
}
return configs;
}
private UpdateConfigs() {}
}

View file

@ -0,0 +1,84 @@
/*
* 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;
import android.os.UpdateEngine;
import android.util.SparseArray;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* Helper class to work with update_engine's error codes.
* Many error codes are defined in {@link UpdateEngine.ErrorCodeConstants},
* but you can find more in system/update_engine/common/error_code.h.
*/
public final class UpdateEngineErrorCodes {
/**
* Error code from the update engine. Values must agree with the ones in
* system/update_engine/common/error_code.h.
*/
public static final int UPDATED_BUT_NOT_ACTIVE = 52;
private static final SparseArray<String> CODE_TO_NAME_MAP = new SparseArray<>();
static {
CODE_TO_NAME_MAP.put(0, "SUCCESS");
CODE_TO_NAME_MAP.put(1, "ERROR");
CODE_TO_NAME_MAP.put(4, "FILESYSTEM_COPIER_ERROR");
CODE_TO_NAME_MAP.put(5, "POST_INSTALL_RUNNER_ERROR");
CODE_TO_NAME_MAP.put(6, "PAYLOAD_MISMATCHED_TYPE_ERROR");
CODE_TO_NAME_MAP.put(7, "INSTALL_DEVICE_OPEN_ERROR");
CODE_TO_NAME_MAP.put(8, "KERNEL_DEVICE_OPEN_ERROR");
CODE_TO_NAME_MAP.put(9, "DOWNLOAD_TRANSFER_ERROR");
CODE_TO_NAME_MAP.put(10, "PAYLOAD_HASH_MISMATCH_ERROR");
CODE_TO_NAME_MAP.put(11, "PAYLOAD_SIZE_MISMATCH_ERROR");
CODE_TO_NAME_MAP.put(12, "DOWNLOAD_PAYLOAD_VERIFICATION_ERROR");
CODE_TO_NAME_MAP.put(20, "DOWNLOAD_STATE_INITIALIZATION_ERROR");
CODE_TO_NAME_MAP.put(48, "USER_CANCELLED");
CODE_TO_NAME_MAP.put(52, "UPDATED_BUT_NOT_ACTIVE");
}
/**
* Completion codes returned by update engine indicating that the update
* was successfully applied.
*/
private static final Set<Integer> SUCCEEDED_COMPLETION_CODES = new HashSet<Integer>(
Arrays.asList(UpdateEngine.ErrorCodeConstants.SUCCESS,
// UPDATED_BUT_NOT_ACTIVE is returned when the payload is
// successfully applied but the
// device won't switch to the new slot after the next boot.
UPDATED_BUT_NOT_ACTIVE));
/**
* checks if update succeeded using errorCode
*/
public static boolean isUpdateSucceeded(int errorCode) {
return SUCCEEDED_COMPLETION_CODES.contains(errorCode);
}
/**
* converts error code to error name
*/
public static String getCodeName(int errorCode) {
return CODE_TO_NAME_MAP.get(errorCode);
}
private UpdateEngineErrorCodes() {}
}

View file

@ -0,0 +1,51 @@
/*
* 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;
import android.util.SparseArray;
/**
* Helper class to work with update_engine's error codes.
* Many error codes are defined in {@link UpdateEngine.UpdateStatusConstants},
* but you can find more in system/update_engine/common/error_code.h.
*/
public final class UpdateEngineStatuses {
private static final SparseArray<String> STATUS_MAP = new SparseArray<>();
static {
STATUS_MAP.put(0, "IDLE");
STATUS_MAP.put(1, "CHECKING_FOR_UPDATE");
STATUS_MAP.put(2, "UPDATE_AVAILABLE");
STATUS_MAP.put(3, "DOWNLOADING");
STATUS_MAP.put(4, "VERIFYING");
STATUS_MAP.put(5, "FINALIZING");
STATUS_MAP.put(6, "UPDATED_NEED_REBOOT");
STATUS_MAP.put(7, "REPORTING_ERROR_EVENT");
STATUS_MAP.put(8, "ATTEMPTING_ROLLBACK");
STATUS_MAP.put(9, "DISABLED");
}
/**
* converts status code to status name
*/
public static String getStatusText(int status) {
return STATUS_MAP.get(status);
}
private UpdateEngineStatuses() {}
}

View file

@ -0,0 +1,32 @@
#
# 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.
#
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_PACKAGE_NAME := SystemUpdaterSampleTests
LOCAL_SDK_VERSION := system_current
LOCAL_MODULE_TAGS := tests
LOCAL_JAVA_LIBRARIES := \
android.test.runner \
android.test.base
LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
LOCAL_INSTRUMENTATION_FOR := SystemUpdaterSample
LOCAL_PROGUARD_ENABLED := disabled
LOCAL_SRC_FILES := $(call all-subdir-java-files)
include $(BUILD_PACKAGE)

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.systemupdatersample.tests">
<!-- We add an application tag here just so that we can indicate that
this package needs to link against the android.test library,
which is needed when building test cases. -->
<application>
<uses-library android:name="android.test.runner" />
</application>
<instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
android:targetPackage="com.example.android.systemupdatersample"
android:label="Tests for SampleUpdater."/>
</manifest>

View file

@ -0,0 +1 @@
tested.project.dir=..

View file

@ -0,0 +1,79 @@
/*
* 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;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
/**
* Tests for {@link UpdateConfig}
*/
@RunWith(AndroidJUnit4.class)
@SmallTest
public class UpdateConfigTest {
private static final String JSON_NON_STREAMING =
"{\"name\": \"vip update\", \"url\": \"file:///builds/a.zip\", "
+ " \"type\": \"NON_STREAMING\"}";
private static final String JSON_STREAMING =
"{\"name\": \"vip update 2\", \"url\": \"http://foo.bar/a.zip\", "
+ "\"type\": \"STREAMING\"}";
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Test
public void fromJson_parsesJsonConfigWithoutMetadata() throws Exception {
UpdateConfig config = UpdateConfig.fromJson(JSON_NON_STREAMING);
assertEquals("name is parsed", "vip update", config.getName());
assertEquals("stores raw json", JSON_NON_STREAMING, config.getRawJson());
assertSame("type is parsed", UpdateConfig.TYPE_NON_STREAMING, config.getInstallType());
assertEquals("url is parsed", "file:///builds/a.zip", config.getUrl());
}
@Test
public void getUpdatePackageFile_throwsErrorIfStreaming() throws Exception {
UpdateConfig config = UpdateConfig.fromJson(JSON_STREAMING);
thrown.expect(RuntimeException.class);
config.getUpdatePackageFile();
}
@Test
public void getUpdatePackageFile_throwsErrorIfNotAFile() throws Exception {
String json = "{\"name\": \"upd\", \"url\": \"http://foo.bar\","
+ " \"type\": \"NON_STREAMING\"}";
UpdateConfig config = UpdateConfig.fromJson(json);
thrown.expect(RuntimeException.class);
config.getUpdatePackageFile();
}
@Test
public void getUpdatePackageFile_works() throws Exception {
UpdateConfig c = UpdateConfig.fromJson(JSON_NON_STREAMING);
assertEquals("correct path", "/builds/a.zip", c.getUpdatePackageFile().getAbsolutePath());
}
}

View file

@ -0,0 +1,48 @@
/*
* 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.ui;
import static org.junit.Assert.assertNotNull;
import android.support.test.filters.MediumTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Make sure that the main launcher activity opens up properly, which will be
* verified by {@link #activityLaunches}.
*/
@RunWith(AndroidJUnit4.class)
@MediumTest
public class MainActivityTest {
@Rule
public final ActivityTestRule<MainActivity> mActivityRule =
new ActivityTestRule<>(MainActivity.class);
/**
* Verifies that the activity under test can be launched.
*/
@Test
public void activityLaunches() {
assertNotNull(mActivityRule.getActivity());
}
}

View file

@ -0,0 +1,117 @@
/*
* 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;
import static com.example.android.systemupdatersample.util.PackagePropertyFiles.PAYLOAD_BINARY_FILE_NAME;
import static com.example.android.systemupdatersample.util.PackagePropertyFiles.PAYLOAD_PROPERTIES_FILE_NAME;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import com.example.android.systemupdatersample.PayloadSpec;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* Tests if PayloadSpecs parses update package zip file correctly.
*/
@RunWith(AndroidJUnit4.class)
@SmallTest
public class PayloadSpecsTest {
private static final String PROPERTIES_CONTENTS = "k1=val1\nkey2=val2";
private static final String PAYLOAD_CONTENTS = "hello\nworld";
private static final int PAYLOAD_SIZE = PAYLOAD_CONTENTS.length();
private File mTestDir;
private Context mContext;
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Before
public void setUp() {
mContext = InstrumentationRegistry.getTargetContext();
mTestDir = mContext.getFilesDir();
}
@Test
public void forNonStreaming_works() throws Exception {
File packageFile = createMockZipFile();
PayloadSpec spec = PayloadSpecs.forNonStreaming(packageFile);
assertEquals("correct url", "file://" + packageFile.getAbsolutePath(), spec.getUrl());
assertEquals("correct payload offset",
30 + PAYLOAD_BINARY_FILE_NAME.length(), spec.getOffset());
assertEquals("correct payload size", PAYLOAD_SIZE, spec.getSize());
assertArrayEquals("correct properties",
new String[]{"k1=val1", "key2=val2"}, spec.getProperties().toArray(new String[0]));
}
@Test
public void forNonStreaming_IOException() throws Exception {
thrown.expect(IOException.class);
PayloadSpecs.forNonStreaming(new File("/fake/news.zip"));
}
/**
* Creates package zip file that contains payload.bin and payload_properties.txt
*/
private File createMockZipFile() throws IOException {
File testFile = new File(mTestDir, "test.zip");
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(testFile))) {
// Add payload.bin entry.
ZipEntry entry = new ZipEntry(PAYLOAD_BINARY_FILE_NAME);
entry.setMethod(ZipEntry.STORED);
entry.setCompressedSize(PAYLOAD_SIZE);
entry.setSize(PAYLOAD_SIZE);
CRC32 crc = new CRC32();
crc.update(PAYLOAD_CONTENTS.getBytes(StandardCharsets.UTF_8));
entry.setCrc(crc.getValue());
zos.putNextEntry(entry);
zos.write(PAYLOAD_CONTENTS.getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
// Add payload properties entry.
ZipEntry propertiesEntry = new ZipEntry(PAYLOAD_PROPERTIES_FILE_NAME);
zos.putNextEntry(propertiesEntry);
zos.write(PROPERTIES_CONTENTS.getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
}
return testFile;
}
}

View file

@ -0,0 +1,63 @@
/*
* 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;
import static org.junit.Assert.assertArrayEquals;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import com.example.android.systemupdatersample.UpdateConfig;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.List;
/**
* Tests for {@link UpdateConfigs}
*/
@RunWith(AndroidJUnit4.class)
@SmallTest
public class UpdateConfigsTest {
private Context mContext;
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Before
public void setUp() {
mContext = InstrumentationRegistry.getTargetContext();
}
@Test
public void configsToNames_extractsNames() {
List<UpdateConfig> configs = Arrays.asList(
new UpdateConfig("blah", "http://", UpdateConfig.TYPE_NON_STREAMING),
new UpdateConfig("blah 2", "http://", UpdateConfig.TYPE_STREAMING)
);
String[] names = UpdateConfigs.configsToNames(configs);
assertArrayEquals(new String[] {"blah", "blah 2"}, names);
}
}