diff options
Diffstat (limited to 'updater_sample')
11 files changed, 313 insertions, 47 deletions
diff --git a/updater_sample/Android.mk b/updater_sample/Android.mk index 2786de44f..056ad66be 100644 --- a/updater_sample/Android.mk +++ b/updater_sample/Android.mk @@ -26,6 +26,10 @@ LOCAL_PROGUARD_ENABLED := disabled LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_STATIC_JAVA_LIBRARIES += guava + +LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res + include $(BUILD_PACKAGE) # Use the following include to make our test apk. diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java index cbee18fcb..23510e426 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java +++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java @@ -19,6 +19,7 @@ package com.example.android.systemupdatersample; import android.os.Parcel; import android.os.Parcelable; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -26,13 +27,13 @@ import java.io.File; import java.io.Serializable; /** - * UpdateConfig describes an update. It will be parsed from JSON, which is intended to + * An update description. 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 int AB_INSTALL_TYPE_NON_STREAMING = 0; + public static final int AB_INSTALL_TYPE_STREAMING = 1; public static final Parcelable.Creator<UpdateConfig> CREATOR = new Parcelable.Creator<UpdateConfig>() { @@ -54,18 +55,30 @@ public class UpdateConfig implements Parcelable { 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")); + switch (o.getString("ab_install_type")) { + case AB_INSTALL_TYPE_NON_STREAMING_JSON: + c.mAbInstallType = AB_INSTALL_TYPE_NON_STREAMING; + break; + case AB_INSTALL_TYPE_STREAMING_JSON: + c.mAbInstallType = AB_INSTALL_TYPE_STREAMING; + break; + default: + throw new JSONException("Invalid type, expected either " + + "NON_STREAMING or STREAMING, got " + o.getString("ab_install_type")); } - if (o.has("metadata")) { - c.mMetadata = new Metadata( - o.getJSONObject("metadata").getInt("offset"), - o.getJSONObject("metadata").getInt("size")); + if (c.mAbInstallType == AB_INSTALL_TYPE_STREAMING) { + JSONObject meta = o.getJSONObject("ab_streaming_metadata"); + JSONArray propertyFilesJson = meta.getJSONArray("property_files"); + InnerFile[] propertyFiles = + new InnerFile[propertyFilesJson.length()]; + for (int i = 0; i < propertyFilesJson.length(); i++) { + JSONObject p = propertyFilesJson.getJSONObject(i); + propertyFiles[i] = new InnerFile( + p.getString("filename"), + p.getLong("offset"), + p.getLong("size")); + } + c.mAbStreamingMetadata = new StreamingMetadata(propertyFiles); } c.mRawJson = json; return c; @@ -74,8 +87,8 @@ public class UpdateConfig implements Parcelable { /** * 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"; + private static final String AB_INSTALL_TYPE_NON_STREAMING_JSON = "NON_STREAMING"; + private static final String AB_INSTALL_TYPE_STREAMING_JSON = "STREAMING"; /** name will be visible on UI */ private String mName; @@ -84,10 +97,10 @@ public class UpdateConfig implements Parcelable { private String mUrl; /** non-streaming (first saves locally) OR streaming (on the fly) */ - private int mInstallType; + private int mAbInstallType; /** metadata is required only for streaming update */ - private Metadata mMetadata; + private StreamingMetadata mAbStreamingMetadata; private String mRawJson; @@ -97,15 +110,15 @@ public class UpdateConfig implements Parcelable { protected UpdateConfig(Parcel in) { this.mName = in.readString(); this.mUrl = in.readString(); - this.mInstallType = in.readInt(); - this.mMetadata = (Metadata) in.readSerializable(); + this.mAbInstallType = in.readInt(); + this.mAbStreamingMetadata = (StreamingMetadata) in.readSerializable(); this.mRawJson = in.readString(); } public UpdateConfig(String name, String url, int installType) { this.mName = name; this.mUrl = url; - this.mInstallType = installType; + this.mAbInstallType = installType; } public String getName() { @@ -121,16 +134,18 @@ public class UpdateConfig implements Parcelable { } public int getInstallType() { - return mInstallType; + return mAbInstallType; + } + + public StreamingMetadata getStreamingMetadata() { + return mAbStreamingMetadata; } /** - * "url" must be the file located on the device. - * * @return File object for given url */ public File getUpdatePackageFile() { - if (mInstallType != TYPE_NON_STREAMING) { + if (mAbInstallType != AB_INSTALL_TYPE_NON_STREAMING) { throw new RuntimeException("Expected non-streaming install type"); } if (!mUrl.startsWith("file://")) { @@ -148,29 +163,60 @@ public class UpdateConfig implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeString(mName); dest.writeString(mUrl); - dest.writeInt(mInstallType); - dest.writeSerializable(mMetadata); + dest.writeInt(mAbInstallType); + dest.writeSerializable(mAbStreamingMetadata); dest.writeString(mRawJson); } /** - * Metadata for STREAMING update + * Metadata for streaming A/B update. */ - public static class Metadata implements Serializable { + public static class StreamingMetadata implements Serializable { private static final long serialVersionUID = 31042L; /** defines beginning of update data in archive */ + private InnerFile[] mPropertyFiles; + + public StreamingMetadata() { + mPropertyFiles = new InnerFile[0]; + } + + public StreamingMetadata(InnerFile[] propertyFiles) { + this.mPropertyFiles = propertyFiles; + } + + public InnerFile[] getPropertyFiles() { + return mPropertyFiles; + } + } + + /** + * Description of a file in an OTA package zip file. + */ + public static class InnerFile implements Serializable { + + private static final long serialVersionUID = 31043L; + + /** filename in an archive */ + private String mFilename; + + /** 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) { + public InnerFile(String filename, long offset, long size) { + this.mFilename = filename; this.mOffset = offset; this.mSize = size; } + public String getFilename() { + return mFilename; + } + public long getOffset() { return mOffset; } @@ -178,6 +224,7 @@ public class UpdateConfig implements Parcelable { public long getSize() { return mSize; } + } } 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 72e1b2469..8507a9e84 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java +++ b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java @@ -260,7 +260,7 @@ public class MainActivity extends Activity { * Applies the given update */ private void applyUpdate(UpdateConfig config) { - if (config.getInstallType() == UpdateConfig.TYPE_NON_STREAMING) { + if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) { AbNonStreamingUpdate update = new AbNonStreamingUpdate(mUpdateEngine, config); try { update.execute(); diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java new file mode 100644 index 000000000..806f17351 --- /dev/null +++ b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java @@ -0,0 +1,93 @@ +/* + * 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.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; + +/** + * Downloads chunk of a file from given url using {@code offset} and {@code size}, + * and saves to a given location. + * + * In real-life application this helper class should download from HTTP Server, + * but in this sample app it will only download from a local file. + */ +public final class FileDownloader { + + private String mUrl; + private long mOffset; + private long mSize; + private File mOut; + + public FileDownloader(String url, long offset, long size, File out) { + this.mUrl = url; + this.mOffset = offset; + this.mSize = size; + this.mOut = out; + } + + /** + * Downloads the file with given offset and size. + */ + public void download() throws IOException { + Log.d("FileDownloader", "downloading " + mOut.getName() + + " from " + mUrl + + " to " + mOut.getAbsolutePath()); + + URL url = new URL(mUrl); + URLConnection connection = url.openConnection(); + connection.connect(); + + // download the file + try (InputStream input = connection.getInputStream()) { + try (OutputStream output = new FileOutputStream(mOut)) { + long skipped = input.skip(mOffset); + if (skipped != mOffset) { + throw new IOException("Can't download file " + + mUrl + + " with given offset " + + mOffset); + } + byte[] data = new byte[4096]; + long total = 0; + while (total < mSize) { + int needToRead = (int) Math.min(4096, mSize - total); + int count = input.read(data, 0, needToRead); + if (count <= 0) { + break; + } + output.write(data, 0, count); + total += count; + } + if (total != mSize) { + throw new IOException("Can't download file " + + mUrl + + " with given size " + + mSize); + } + } + } + } + +} diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java index 089f8b2f2..71d4df8ab 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java +++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java @@ -17,6 +17,7 @@ package com.example.android.systemupdatersample.util; import android.content.Context; +import android.util.Log; import com.example.android.systemupdatersample.UpdateConfig; @@ -70,6 +71,7 @@ public final class UpdateConfigs { StandardCharsets.UTF_8); configs.add(UpdateConfig.fromJson(json)); } catch (Exception e) { + Log.e("UpdateConfigs", "Can't read/parse config file " + f.getName(), e); throw new RuntimeException( "Can't read/parse config file " + f.getName(), e); } diff --git a/updater_sample/tests/Android.mk b/updater_sample/tests/Android.mk index 83082cda6..a1a4664dc 100644 --- a/updater_sample/tests/Android.mk +++ b/updater_sample/tests/Android.mk @@ -22,11 +22,15 @@ LOCAL_SDK_VERSION := system_current LOCAL_MODULE_TAGS := tests LOCAL_JAVA_LIBRARIES := \ android.test.base.stubs \ - android.test.runner.stubs + android.test.runner.stubs \ + guava \ + mockito-target-minus-junit4 LOCAL_STATIC_JAVA_LIBRARIES := android-support-test LOCAL_INSTRUMENTATION_FOR := SystemUpdaterSample LOCAL_PROGUARD_ENABLED := disabled -LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res + +LOCAL_SRC_FILES := $(call all-java-files-under, src) include $(BUILD_PACKAGE) diff --git a/updater_sample/tests/AndroidManifest.xml b/updater_sample/tests/AndroidManifest.xml index 2392bb3af..76af5f1a9 100644 --- a/updater_sample/tests/AndroidManifest.xml +++ b/updater_sample/tests/AndroidManifest.xml @@ -17,6 +17,8 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.systemupdatersample.tests"> + <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27" /> + <!-- 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. --> diff --git a/updater_sample/tests/res/raw/update_config_stream_001.json b/updater_sample/tests/res/raw/update_config_stream_001.json index 965f737d7..a9afd331c 100644 --- a/updater_sample/tests/res/raw/update_config_stream_001.json +++ b/updater_sample/tests/res/raw/update_config_stream_001.json @@ -1,8 +1,8 @@ { "name": "streaming-001", "url": "http://foo.bar/update.zip", - "type": "STREAMING", - "streaming_metadata": { + "ab_install_type": "STREAMING", + "ab_streaming_metadata": { "property_files": [ { "filename": "payload.bin", diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java index 87153715e..0975e76be 100644 --- a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java +++ b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java @@ -19,14 +19,23 @@ package com.example.android.systemupdatersample; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; +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.tests.R; +import com.google.common.io.CharStreams; + +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.IOException; +import java.io.InputStreamReader; + /** * Tests for {@link UpdateConfig} */ @@ -36,27 +45,48 @@ 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\"}"; + + " \"ab_install_type\": \"NON_STREAMING\"}"; @Rule public final ExpectedException thrown = ExpectedException.none(); + private Context mContext; + private Context mTargetContext; + private String mJsonStreaming001; + + @Before + public void setUp() throws Exception { + mContext = InstrumentationRegistry.getContext(); + mTargetContext = InstrumentationRegistry.getTargetContext(); + mJsonStreaming001 = readResource(R.raw.update_config_stream_001); + } + @Test - public void fromJson_parsesJsonConfigWithoutMetadata() throws Exception { + public void fromJson_parsesNonStreaming() 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()); + assertSame("type is parsed", + UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING, + config.getInstallType()); assertEquals("url is parsed", "file:///builds/a.zip", config.getUrl()); } @Test + public void fromJson_parsesStreaming() throws Exception { + UpdateConfig config = UpdateConfig.fromJson(mJsonStreaming001); + assertEquals("streaming-001", config.getName()); + assertEquals("http://foo.bar/update.zip", config.getUrl()); + assertSame(UpdateConfig.AB_INSTALL_TYPE_STREAMING, config.getInstallType()); + assertEquals("payload.bin", + config.getStreamingMetadata().getPropertyFiles()[0].getFilename()); + assertEquals(195, config.getStreamingMetadata().getPropertyFiles()[0].getOffset()); + assertEquals(8, config.getStreamingMetadata().getPropertyFiles()[0].getSize()); + } + + @Test public void getUpdatePackageFile_throwsErrorIfStreaming() throws Exception { - UpdateConfig config = UpdateConfig.fromJson(JSON_STREAMING); + UpdateConfig config = UpdateConfig.fromJson(mJsonStreaming001); thrown.expect(RuntimeException.class); config.getUpdatePackageFile(); } @@ -64,7 +94,7 @@ public class UpdateConfigTest { @Test public void getUpdatePackageFile_throwsErrorIfNotAFile() throws Exception { String json = "{\"name\": \"upd\", \"url\": \"http://foo.bar\"," - + " \"type\": \"NON_STREAMING\"}"; + + " \"ab_install_type\": \"NON_STREAMING\"}"; UpdateConfig config = UpdateConfig.fromJson(json); thrown.expect(RuntimeException.class); config.getUpdatePackageFile(); @@ -73,7 +103,11 @@ public class UpdateConfigTest { @Test public void getUpdatePackageFile_works() throws Exception { UpdateConfig c = UpdateConfig.fromJson(JSON_NON_STREAMING); - assertEquals("correct path", "/builds/a.zip", c.getUpdatePackageFile().getAbsolutePath()); + assertEquals("/builds/a.zip", c.getUpdatePackageFile().getAbsolutePath()); } + private String readResource(int id) throws IOException { + return CharStreams.toString(new InputStreamReader( + mContext.getResources().openRawResource(id))); + } } 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 new file mode 100644 index 000000000..df47c8ca8 --- /dev/null +++ b/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java @@ -0,0 +1,80 @@ +/* + * 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 junit.framework.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.tests.R; + +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.nio.file.Files; +import java.nio.file.Paths; + +/** + * Tests for {@link FileDownloader} + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class FileDownloaderTest { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private Context mTestContext; + private Context mTargetContext; + + @Before + public void setUp() { + mTestContext = InstrumentationRegistry.getContext(); + mTargetContext = InstrumentationRegistry.getTargetContext(); + } + + @Test + public void download_downloadsChunkOfZip() throws Exception { + // Prepare the target file + File packageFile = Paths + .get(mTargetContext.getCacheDir().getAbsolutePath(), "ota.zip") + .toFile(); + Files.delete(packageFile.toPath()); + Files.copy(mTestContext.getResources().openRawResource(R.raw.ota_002_package), + packageFile.toPath()); + String url = "file://" + packageFile.getAbsolutePath(); + // prepare where to download + File outFile = Paths + .get(mTargetContext.getCacheDir().getAbsolutePath(), "care_map.txt") + .toFile(); + Files.delete(outFile.toPath()); + // download a chunk of ota.zip + FileDownloader downloader = new FileDownloader(url, 160, 8, outFile); + downloader.download(); + String downloadedContent = String.join("\n", Files.readAllLines(outFile.toPath())); + // Look at tools/create_test_ota.py + assertEquals("CARE_MAP", downloadedContent); + } + +} diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/util/UpdateConfigsTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/util/UpdateConfigsTest.java index 4aa8c6453..c85698c0f 100644 --- a/updater_sample/tests/src/com/example/android/systemupdatersample/util/UpdateConfigsTest.java +++ b/updater_sample/tests/src/com/example/android/systemupdatersample/util/UpdateConfigsTest.java @@ -54,8 +54,8 @@ public class UpdateConfigsTest { @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) + new UpdateConfig("blah", "http://", UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING), + new UpdateConfig("blah 2", "http://", UpdateConfig.AB_INSTALL_TYPE_STREAMING) ); String[] names = UpdateConfigs.configsToNames(configs); assertArrayEquals(new String[] {"blah", "blah 2"}, names); |