From 265f8bfd99becca62c2bc0d1b2d0d8cc59ca77a9 Mon Sep 17 00:00:00 2001 From: Howard Chen Date: Tue, 20 Aug 2019 13:00:49 +0800 Subject: [PATCH 1/2] Create a SparseInputstream SparseInputStream read from upstream and detects the data format. If the upstream is a valid sparse data, it will unsparse it on the fly. Otherwise, it just passthrough as is. Bug: 139510436 Test: \ java com.android.dynsystem.SparseInputStream system.img system.raw simg2img system.img system.raw.golden diff system.raw system.raw.golden Change-Id: Ibc5c130127a455392484467fe32d638be642afa8 --- .../android/dynsystem/SparseInputStream.java | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 packages/DynamicSystemInstallationService/src/com/android/dynsystem/SparseInputStream.java diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/SparseInputStream.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/SparseInputStream.java new file mode 100644 index 0000000000000..72230b4062e11 --- /dev/null +++ b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/SparseInputStream.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2019 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.dynsystem; + +import static java.lang.Math.min; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +/** + * SparseInputStream read from upstream and detects the data format. If the upstream is a valid + * sparse data, it will unsparse it on the fly. Otherwise, it just passthrough as is. + */ +public class SparseInputStream extends InputStream { + static final int FILE_HDR_SIZE = 28; + static final int CHUNK_HDR_SIZE = 12; + + /** + * This class represents a chunk in the Android sparse image. + * + * @see system/core/libsparse/sparse_format.h + */ + private class SparseChunk { + static final short RAW = (short) 0xCAC1; + static final short FILL = (short) 0xCAC2; + static final short DONTCARE = (short) 0xCAC3; + public short mChunkType; + public int mChunkSize; + public int mTotalSize; + public byte[] fill; + public String toString() { + return String.format( + "type: %x, chunk_size: %d, total_size: %d", mChunkType, mChunkSize, mTotalSize); + } + } + + private byte[] readFull(InputStream in, int size) throws IOException { + byte[] buf = new byte[size]; + for (int done = 0, n = 0; done < size; done += n) { + if ((n = in.read(buf, done, size - done)) < 0) { + throw new IOException("Failed to readFull"); + } + } + return buf; + } + + private ByteBuffer readBuffer(InputStream in, int size) throws IOException { + return ByteBuffer.wrap(readFull(in, size)).order(ByteOrder.LITTLE_ENDIAN); + } + + private SparseChunk readChunk(InputStream in) throws IOException { + SparseChunk chunk = new SparseChunk(); + ByteBuffer buf = readBuffer(in, CHUNK_HDR_SIZE); + chunk.mChunkType = buf.getShort(); + buf.getShort(); + chunk.mChunkSize = buf.getInt(); + chunk.mTotalSize = buf.getInt(); + return chunk; + } + + private BufferedInputStream mIn; + private boolean mIsSparse; + private long mBlockSize; + private long mTotalBlocks; + private long mTotalChunks; + private SparseChunk mCur; + private long mLeft; + private int mCurChunks; + + public SparseInputStream(BufferedInputStream in) throws IOException { + mIn = in; + in.mark(FILE_HDR_SIZE * 2); + ByteBuffer buf = readBuffer(mIn, FILE_HDR_SIZE); + mIsSparse = (buf.getInt() == 0xed26ff3a); + if (!mIsSparse) { + mIn.reset(); + return; + } + int major = buf.getShort(); + int minor = buf.getShort(); + + if (major > 0x1 || minor > 0x0) { + throw new IOException("Unsupported sparse version: " + major + "." + minor); + } + + if (buf.getShort() != FILE_HDR_SIZE) { + throw new IOException("Illegal file header size"); + } + if (buf.getShort() != CHUNK_HDR_SIZE) { + throw new IOException("Illegal chunk header size"); + } + mBlockSize = buf.getInt(); + if ((mBlockSize & 0x3) != 0) { + throw new IOException("Illegal block size, must be a multiple of 4"); + } + mTotalBlocks = buf.getInt(); + mTotalChunks = buf.getInt(); + mLeft = mCurChunks = 0; + } + + /** + * Check if it needs to open a new chunk. + * + * @return true if it's EOF + */ + private boolean prepareChunk() throws IOException { + if (mCur == null || mLeft <= 0) { + if (++mCurChunks > mTotalChunks) return true; + mCur = readChunk(mIn); + if (mCur.mChunkType == SparseChunk.FILL) { + mCur.fill = readFull(mIn, 4); + } + mLeft = mCur.mChunkSize * mBlockSize; + } + return mLeft == 0; + } + + /** + * It overrides the InputStream.read(byte[] buf) + */ + public int read(byte[] buf) throws IOException { + if (!mIsSparse) { + return mIn.read(buf); + } + if (prepareChunk()) return -1; + int n = -1; + switch (mCur.mChunkType) { + case SparseChunk.RAW: + n = mIn.read(buf, 0, (int) min(mLeft, buf.length)); + mLeft -= n; + return n; + case SparseChunk.DONTCARE: + n = (int) min(mLeft, buf.length); + Arrays.fill(buf, 0, n - 1, (byte) 0); + mLeft -= n; + return n; + case SparseChunk.FILL: + // The FILL type is rarely used, so use a simple implmentation. + return super.read(buf); + default: + throw new IOException("Unsupported Chunk:" + mCur.toString()); + } + } + + /** + * It overrides the InputStream.read() + */ + public int read() throws IOException { + if (!mIsSparse) { + return mIn.read(); + } + if (prepareChunk()) return -1; + int ret = -1; + switch (mCur.mChunkType) { + case SparseChunk.RAW: + ret = mIn.read(); + break; + case SparseChunk.DONTCARE: + ret = 0; + break; + case SparseChunk.FILL: + ret = mCur.fill[(4 - ((int) mLeft & 0x3)) & 0x3]; + break; + default: + throw new IOException("Unsupported Chunk:" + mCur.toString()); + } + mLeft--; + return ret; + } + + /** + * Get the unsparse size + * @return -1 if unknown + */ + public long getUnsparseSize() { + if (!mIsSparse) { + return -1; + } + return mBlockSize * mTotalBlocks; + } +} From 99d18dddd27634c1e0085041989ab01489d97cda Mon Sep 17 00:00:00 2001 From: Po-Chien Hsueh Date: Wed, 30 Oct 2019 16:48:15 +0800 Subject: [PATCH 2/2] DSU to support zip files DSU previously only supports gzipped system images. This CL enables DSU to also read zipped files. If there are multiple images in the zipped release, DSU will install them all. Bug: 134353973 Test: adb shell am start-activity \ -n com.android.dynsystem/com.android.dynsystem.VerificationActivity \ -a android.os.image.action.START_INSTALL \ -d https://dl.google.com/developers/android/qt/images/gsi/aosp_arm64-QP1A.190771.020-5800535.zip \ --el KEY_USERDATA_SIZE 8589934592 Test: adb shell am start-activity \ -n com.android.dynsystem/com.android.dynsystem.VerificationActivity \ -a android.os.image.action.START_INSTALL \ -d file:///storage/emulated/0/Download/aosp_crosshatch-img-eng.pchsueh.zip \ --el KEY_USERDATA_SIZE 8589934592 Change-Id: I9c2137d4d81398a9c6153df63a29c16eb8339795 --- .../DynamicSystemInstallationService.java | 67 +-- .../dynsystem/InstallationAsyncTask.java | 424 +++++++++++++----- 2 files changed, 350 insertions(+), 141 deletions(-) diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java index 142078e1b77cc..9e49826f70c3d 100644 --- a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java +++ b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java @@ -32,9 +32,11 @@ import static android.os.image.DynamicSystemClient.STATUS_IN_USE; import static android.os.image.DynamicSystemClient.STATUS_NOT_STARTED; import static android.os.image.DynamicSystemClient.STATUS_READY; +import static com.android.dynsystem.InstallationAsyncTask.RESULT_CANCELLED; import static com.android.dynsystem.InstallationAsyncTask.RESULT_ERROR_EXCEPTION; -import static com.android.dynsystem.InstallationAsyncTask.RESULT_ERROR_INVALID_URL; import static com.android.dynsystem.InstallationAsyncTask.RESULT_ERROR_IO; +import static com.android.dynsystem.InstallationAsyncTask.RESULT_ERROR_UNSUPPORTED_FORMAT; +import static com.android.dynsystem.InstallationAsyncTask.RESULT_ERROR_UNSUPPORTED_URL; import static com.android.dynsystem.InstallationAsyncTask.RESULT_OK; import android.app.Notification; @@ -66,11 +68,10 @@ import java.util.ArrayList; * cancel and confirm commnands. */ public class DynamicSystemInstallationService extends Service - implements InstallationAsyncTask.InstallStatusListener { + implements InstallationAsyncTask.ProgressListener { private static final String TAG = "DynSystemInstallationService"; - // TODO (b/131866826): This is currently for test only. Will move this to System API. static final String KEY_ENABLE_WHEN_COMPLETED = "KEY_ENABLE_WHEN_COMPLETED"; @@ -121,9 +122,12 @@ public class DynamicSystemInstallationService extends Service private DynamicSystemManager mDynSystem; private NotificationManager mNM; - private long mSystemSize; - private long mUserdataSize; - private long mInstalledSize; + private int mNumInstalledPartitions; + + private String mCurrentPartitionName; + private long mCurrentPartitionSize; + private long mCurrentPartitionInstalledSize; + private boolean mJustCancelledByUser; // This is for testing only now @@ -176,8 +180,12 @@ public class DynamicSystemInstallationService extends Service } @Override - public void onProgressUpdate(long installedSize) { - mInstalledSize = installedSize; + public void onProgressUpdate(InstallationAsyncTask.Progress progress) { + mCurrentPartitionName = progress.mPartitionName; + mCurrentPartitionSize = progress.mPartitionSize; + mCurrentPartitionInstalledSize = progress.mInstalledSize; + mNumInstalledPartitions = progress.mNumInstalledPartitions; + postStatus(STATUS_IN_PROGRESS, CAUSE_NOT_SPECIFIED, null); } @@ -197,11 +205,16 @@ public class DynamicSystemInstallationService extends Service resetTaskAndStop(); switch (result) { + case RESULT_CANCELLED: + postStatus(STATUS_NOT_STARTED, CAUSE_INSTALL_CANCELLED, null); + break; + case RESULT_ERROR_IO: postStatus(STATUS_NOT_STARTED, CAUSE_ERROR_IO, detail); break; - case RESULT_ERROR_INVALID_URL: + case RESULT_ERROR_UNSUPPORTED_URL: + case RESULT_ERROR_UNSUPPORTED_FORMAT: postStatus(STATUS_NOT_STARTED, CAUSE_ERROR_INVALID_URL, detail); break; @@ -211,12 +224,6 @@ public class DynamicSystemInstallationService extends Service } } - @Override - public void onCancelled() { - resetTaskAndStop(); - postStatus(STATUS_NOT_STARTED, CAUSE_INSTALL_CANCELLED, null); - } - private void executeInstallCommand(Intent intent) { if (!verifyRequest(intent)) { Log.e(TAG, "Verification failed. Did you use VerificationActivity?"); @@ -234,12 +241,13 @@ public class DynamicSystemInstallationService extends Service } String url = intent.getDataString(); - mSystemSize = intent.getLongExtra(DynamicSystemClient.KEY_SYSTEM_SIZE, 0); - mUserdataSize = intent.getLongExtra(DynamicSystemClient.KEY_USERDATA_SIZE, 0); + long systemSize = intent.getLongExtra(DynamicSystemClient.KEY_SYSTEM_SIZE, 0); + long userdataSize = intent.getLongExtra(DynamicSystemClient.KEY_USERDATA_SIZE, 0); mEnableWhenCompleted = intent.getBooleanExtra(KEY_ENABLE_WHEN_COMPLETED, false); + // TODO: better constructor or builder mInstallTask = new InstallationAsyncTask( - url, mSystemSize, mUserdataSize, this, mDynSystem, this); + url, systemSize, userdataSize, this, mDynSystem, this); mInstallTask.execute(); @@ -257,7 +265,7 @@ public class DynamicSystemInstallationService extends Service mJustCancelledByUser = true; if (mInstallTask.cancel(false)) { - // Will cleanup and post status in onCancelled() + // Will cleanup and post status in onResult() Log.d(TAG, "Cancel request filed successfully"); } else { Log.e(TAG, "Trying to cancel installation while it's already completed."); @@ -288,7 +296,7 @@ public class DynamicSystemInstallationService extends Service private void executeRebootToDynSystemCommand() { boolean enabled = false; - if (mInstallTask != null && mInstallTask.getResult() == RESULT_OK) { + if (mInstallTask != null && mInstallTask.isCompleted()) { enabled = mInstallTask.commit(); } else if (isDynamicSystemInstalled()) { enabled = mDynSystem.setEnable(true, true); @@ -380,8 +388,16 @@ public class DynamicSystemInstallationService extends Service case STATUS_IN_PROGRESS: builder.setContentText(getString(R.string.notification_install_inprogress)); - int max = (int) Math.max((mSystemSize + mUserdataSize) >> 20, 1); - int progress = (int) (mInstalledSize >> 20); + int max = 1024; + int progress = 0; + + int currentMax = max >> (mNumInstalledPartitions + 1); + progress = max - currentMax * 2; + + long currentProgress = (mCurrentPartitionInstalledSize >> 20) * currentMax + / Math.max(mCurrentPartitionSize >> 20, 1); + + progress += (int) currentProgress; builder.setProgress(max, progress, false); @@ -464,7 +480,8 @@ public class DynamicSystemInstallationService extends Service throws RemoteException { Bundle bundle = new Bundle(); - bundle.putLong(DynamicSystemClient.KEY_INSTALLED_SIZE, mInstalledSize); + // TODO: send more info to the clients + bundle.putLong(DynamicSystemClient.KEY_INSTALLED_SIZE, mCurrentPartitionInstalledSize); if (detail != null) { bundle.putSerializable(DynamicSystemClient.KEY_EXCEPTION_DETAIL, @@ -492,9 +509,7 @@ public class DynamicSystemInstallationService extends Service return STATUS_IN_PROGRESS; case FINISHED: - int result = mInstallTask.getResult(); - - if (result == RESULT_OK) { + if (mInstallTask.isCompleted()) { return STATUS_READY; } else { throw new IllegalStateException("A failed InstallationTask is not reset"); diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/InstallationAsyncTask.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/InstallationAsyncTask.java index 19ae97070188a..b206a1fccba40 100644 --- a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/InstallationAsyncTask.java +++ b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/InstallationAsyncTask.java @@ -17,7 +17,6 @@ package com.android.dynsystem; import android.content.Context; -import android.gsi.GsiProgress; import android.net.Uri; import android.os.AsyncTask; import android.os.MemoryFile; @@ -27,35 +26,70 @@ import android.util.Log; import android.webkit.URLUtil; import java.io.BufferedInputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; import java.util.Locale; import java.util.zip.GZIPInputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; -class InstallationAsyncTask extends AsyncTask { +class InstallationAsyncTask extends AsyncTask { private static final String TAG = "InstallationAsyncTask"; private static final int READ_BUFFER_SIZE = 1 << 13; + private static final long MIN_PROGRESS_TO_PUBLISH = 1 << 27; - private class InvalidImageUrlException extends RuntimeException { - private InvalidImageUrlException(String message) { + private static final List UNSUPPORTED_PARTITIONS = + Arrays.asList("vbmeta", "boot", "userdata", "dtbo", "super_empty", "system_other"); + + private class UnsupportedUrlException extends RuntimeException { + private UnsupportedUrlException(String message) { super(message); } } - /** Not completed, including being cancelled */ - static final int NO_RESULT = 0; + private class UnsupportedFormatException extends RuntimeException { + private UnsupportedFormatException(String message) { + super(message); + } + } + + /** UNSET means the installation is not completed */ + static final int RESULT_UNSET = 0; static final int RESULT_OK = 1; - static final int RESULT_ERROR_IO = 2; - static final int RESULT_ERROR_INVALID_URL = 3; + static final int RESULT_CANCELLED = 2; + static final int RESULT_ERROR_IO = 3; + static final int RESULT_ERROR_UNSUPPORTED_URL = 4; + static final int RESULT_ERROR_UNSUPPORTED_FORMAT = 5; static final int RESULT_ERROR_EXCEPTION = 6; - interface InstallStatusListener { - void onProgressUpdate(long installedSize); + class Progress { + String mPartitionName; + long mPartitionSize; + long mInstalledSize; + + int mNumInstalledPartitions; + + Progress(String partitionName, long partitionSize, long installedSize, + int numInstalled) { + mPartitionName = partitionName; + mPartitionSize = partitionSize; + mInstalledSize = installedSize; + + mNumInstalledPartitions = numInstalled; + } + } + + interface ProgressListener { + void onProgressUpdate(Progress progress); void onResult(int resultCode, Throwable detail); - void onCancelled(); } private final String mUrl; @@ -63,16 +97,17 @@ class InstallationAsyncTask extends AsyncTask { private final long mUserdataSize; private final Context mContext; private final DynamicSystemManager mDynSystem; - private final InstallStatusListener mListener; + private final ProgressListener mListener; private DynamicSystemManager.Session mInstallationSession; - private int mResult = NO_RESULT; + private boolean mIsZip; + private boolean mIsCompleted; private InputStream mStream; - + private ZipFile mZipFile; InstallationAsyncTask(String url, long systemSize, long userdataSize, Context context, - DynamicSystemManager dynSystem, InstallStatusListener listener) { + DynamicSystemManager dynSystem, ProgressListener listener) { mUrl = url; mSystemSize = systemSize; mUserdataSize = userdataSize; @@ -81,134 +116,293 @@ class InstallationAsyncTask extends AsyncTask { mListener = listener; } - @Override - protected void onPreExecute() { - mListener.onProgressUpdate(0); - } - @Override protected Throwable doInBackground(String... voids) { Log.d(TAG, "Start doInBackground(), URL: " + mUrl); try { - long installedSize = 0; - long reportedInstalledSize = 0; + // call DynamicSystemManager to cleanup stuff + mDynSystem.remove(); - long minStepToReport = (mSystemSize + mUserdataSize) / 100; + verifyAndPrepare(); - // init input stream before calling startInstallation(), which takes 90 seconds. - initInputStream(); + mDynSystem.startInstallation(); - Thread thread = - new Thread( - () -> { - mDynSystem.startInstallation(); - mDynSystem.createPartition("userdata", mUserdataSize, false); - mInstallationSession = - mDynSystem.createPartition("system", mSystemSize, true); - }); - - thread.start(); - - while (thread.isAlive()) { - if (isCancelled()) { - boolean aborted = mDynSystem.abort(); - Log.d(TAG, "Called DynamicSystemManager.abort(), result = " + aborted); - return null; - } - - GsiProgress progress = mDynSystem.getInstallationProgress(); - installedSize = progress.bytes_processed; - - if (installedSize > reportedInstalledSize + minStepToReport) { - publishProgress(installedSize); - reportedInstalledSize = installedSize; - } - - Thread.sleep(10); + installUserdata(); + if (isCancelled()) { + mDynSystem.remove(); + return null; } - if (mInstallationSession == null) { - throw new IOException( - "Failed to start installation with requested size: " - + (mSystemSize + mUserdataSize)); + installImages(); + if (isCancelled()) { + mDynSystem.remove(); + return null; } - installedSize = mUserdataSize; - - MemoryFile memoryFile = new MemoryFile("dsu", READ_BUFFER_SIZE); - byte[] bytes = new byte[READ_BUFFER_SIZE]; - mInstallationSession.setAshmem( - new ParcelFileDescriptor(memoryFile.getFileDescriptor()), READ_BUFFER_SIZE); - int numBytesRead; - Log.d(TAG, "Start installation loop"); - while ((numBytesRead = mStream.read(bytes, 0, READ_BUFFER_SIZE)) != -1) { - memoryFile.writeBytes(bytes, 0, 0, numBytesRead); - if (isCancelled()) { - break; - } - if (!mInstallationSession.submitFromAshmem(numBytesRead)) { - throw new IOException("Failed write() to DynamicSystem"); - } - - installedSize += numBytesRead; - - if (installedSize > reportedInstalledSize + minStepToReport) { - publishProgress(installedSize); - reportedInstalledSize = installedSize; - } - } mDynSystem.finishInstallation(); - return null; - } catch (Exception e) { e.printStackTrace(); + mDynSystem.remove(); return e; } finally { close(); } + + return null; + } + + @Override + protected void onPostExecute(Throwable detail) { + int result = RESULT_UNSET; + + if (detail == null) { + result = RESULT_OK; + mIsCompleted = true; + } else if (detail instanceof IOException) { + result = RESULT_ERROR_IO; + } else if (detail instanceof UnsupportedUrlException) { + result = RESULT_ERROR_UNSUPPORTED_URL; + } else if (detail instanceof UnsupportedFormatException) { + result = RESULT_ERROR_UNSUPPORTED_FORMAT; + } else { + result = RESULT_ERROR_EXCEPTION; + } + + Log.d(TAG, "onPostExecute(), URL: " + mUrl + ", result: " + result); + + mListener.onResult(result, detail); } @Override protected void onCancelled() { Log.d(TAG, "onCancelled(), URL: " + mUrl); - mListener.onCancelled(); - } - - @Override - protected void onPostExecute(Throwable detail) { - if (detail == null) { - mResult = RESULT_OK; - } else if (detail instanceof IOException) { - mResult = RESULT_ERROR_IO; - } else if (detail instanceof InvalidImageUrlException) { - mResult = RESULT_ERROR_INVALID_URL; + if (mDynSystem.abort()) { + Log.d(TAG, "Installation aborted"); } else { - mResult = RESULT_ERROR_EXCEPTION; + Log.w(TAG, "DynamicSystemManager.abort() returned false"); } - Log.d(TAG, "onPostExecute(), URL: " + mUrl + ", result: " + mResult); - - mListener.onResult(mResult, detail); + mListener.onResult(RESULT_CANCELLED, null); } @Override - protected void onProgressUpdate(Long... values) { - long progress = values[0]; + protected void onProgressUpdate(Progress... values) { + Progress progress = values[0]; mListener.onProgressUpdate(progress); } - private void initInputStream() throws IOException, InvalidImageUrlException { - if (URLUtil.isNetworkUrl(mUrl) || URLUtil.isFileUrl(mUrl)) { - mStream = new BufferedInputStream(new GZIPInputStream(new URL(mUrl).openStream())); - } else if (URLUtil.isContentUrl(mUrl)) { - Uri uri = Uri.parse(mUrl); - mStream = new BufferedInputStream(new GZIPInputStream( - mContext.getContentResolver().openInputStream(uri))); + private void verifyAndPrepare() throws Exception { + String extension = mUrl.substring(mUrl.lastIndexOf('.') + 1); + + if ("gz".equals(extension) || "gzip".equals(extension)) { + mIsZip = false; + } else if ("zip".equals(extension)) { + mIsZip = true; } else { - throw new InvalidImageUrlException( - String.format(Locale.US, "Unsupported file source: %s", mUrl)); + throw new UnsupportedFormatException( + String.format(Locale.US, "Unsupported file format: %s", mUrl)); + } + + if (URLUtil.isNetworkUrl(mUrl)) { + mStream = new URL(mUrl).openStream(); + } else if (URLUtil.isFileUrl(mUrl)) { + if (mIsZip) { + mZipFile = new ZipFile(new File(new URL(mUrl).toURI())); + } else { + mStream = new URL(mUrl).openStream(); + } + } else if (URLUtil.isContentUrl(mUrl)) { + mStream = mContext.getContentResolver().openInputStream(Uri.parse(mUrl)); + } else { + throw new UnsupportedUrlException( + String.format(Locale.US, "Unsupported URL: %s", mUrl)); + } + } + + private void installUserdata() throws Exception { + Thread thread = new Thread(() -> { + mInstallationSession = mDynSystem.createPartition("userdata", mUserdataSize, false); + }); + + Log.d(TAG, "Creating partition: userdata"); + thread.start(); + + long installedSize = 0; + Progress progress = new Progress("userdata", mUserdataSize, installedSize, 0); + + while (thread.isAlive()) { + if (isCancelled()) { + return; + } + + installedSize = mDynSystem.getInstallationProgress().bytes_processed; + + if (installedSize > progress.mInstalledSize + MIN_PROGRESS_TO_PUBLISH) { + progress.mInstalledSize = installedSize; + publishProgress(progress); + } + + Thread.sleep(10); + } + + if (mInstallationSession == null) { + throw new IOException( + "Failed to start installation with requested size: " + mUserdataSize); + } + } + + private void installImages() throws IOException, InterruptedException { + if (mStream != null) { + if (mIsZip) { + installStreamingZipUpdate(); + } else { + installStreamingGzUpdate(); + } + } else { + installLocalZipUpdate(); + } + } + + private void installStreamingGzUpdate() throws IOException, InterruptedException { + Log.d(TAG, "To install a streaming GZ update"); + installImage("system", mSystemSize, new GZIPInputStream(mStream), 1); + } + + private void installStreamingZipUpdate() throws IOException, InterruptedException { + Log.d(TAG, "To install a streaming ZIP update"); + + ZipInputStream zis = new ZipInputStream(mStream); + ZipEntry zipEntry = null; + + int numInstalledPartitions = 1; + + while ((zipEntry = zis.getNextEntry()) != null) { + if (installImageFromAnEntry(zipEntry, zis, numInstalledPartitions)) { + numInstalledPartitions++; + } + + if (isCancelled()) { + break; + } + } + } + + private void installLocalZipUpdate() throws IOException, InterruptedException { + Log.d(TAG, "To install a local ZIP update"); + + Enumeration entries = mZipFile.entries(); + int numInstalledPartitions = 1; + + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (installImageFromAnEntry( + entry, mZipFile.getInputStream(entry), numInstalledPartitions)) { + numInstalledPartitions++; + } + + if (isCancelled()) { + break; + } + } + } + + private boolean installImageFromAnEntry(ZipEntry entry, InputStream is, + int numInstalledPartitions) throws IOException, InterruptedException { + String name = entry.getName(); + + Log.d(TAG, "ZipEntry: " + name); + + if (!name.endsWith(".img")) { + return false; + } + + String partitionName = name.substring(0, name.length() - 4); + + if (UNSUPPORTED_PARTITIONS.contains(partitionName)) { + Log.d(TAG, name + " installation is not supported, skip it."); + return false; + } + + long uncompressedSize = entry.getSize(); + + installImage(partitionName, uncompressedSize, is, numInstalledPartitions); + + return true; + } + + private void installImage(String partitionName, long uncompressedSize, InputStream is, + int numInstalledPartitions) throws IOException, InterruptedException { + + SparseInputStream sis = new SparseInputStream(new BufferedInputStream(is)); + + long unsparseSize = sis.getUnsparseSize(); + + final long partitionSize; + + if (unsparseSize != -1) { + partitionSize = unsparseSize; + Log.d(TAG, partitionName + " is sparse, raw size = " + unsparseSize); + } else if (uncompressedSize != -1) { + partitionSize = uncompressedSize; + Log.d(TAG, partitionName + " is already unsparse, raw size = " + uncompressedSize); + } else { + throw new IOException("Cannot get raw size for " + partitionName); + } + + Thread thread = new Thread(() -> { + mInstallationSession = + mDynSystem.createPartition(partitionName, partitionSize, true); + }); + + Log.d(TAG, "Start creating partition: " + partitionName); + thread.start(); + + while (thread.isAlive()) { + if (isCancelled()) { + return; + } + + Thread.sleep(10); + } + + if (mInstallationSession == null) { + throw new IOException( + "Failed to start installation with requested size: " + partitionSize); + } + + Log.d(TAG, "Start installing: " + partitionName); + + MemoryFile memoryFile = new MemoryFile("dsu_" + partitionName, READ_BUFFER_SIZE); + ParcelFileDescriptor pfd = new ParcelFileDescriptor(memoryFile.getFileDescriptor()); + + mInstallationSession.setAshmem(pfd, READ_BUFFER_SIZE); + + long installedSize = 0; + Progress progress = new Progress( + partitionName, partitionSize, installedSize, numInstalledPartitions); + + byte[] bytes = new byte[READ_BUFFER_SIZE]; + int numBytesRead; + + while ((numBytesRead = sis.read(bytes, 0, READ_BUFFER_SIZE)) != -1) { + if (isCancelled()) { + return; + } + + memoryFile.writeBytes(bytes, 0, 0, numBytesRead); + + if (!mInstallationSession.submitFromAshmem(numBytesRead)) { + throw new IOException("Failed write() to DynamicSystem"); + } + + installedSize += numBytesRead; + + if (installedSize > progress.mInstalledSize + MIN_PROGRESS_TO_PUBLISH) { + progress.mInstalledSize = installedSize; + publishProgress(progress); + } } } @@ -218,20 +412,20 @@ class InstallationAsyncTask extends AsyncTask { mStream.close(); mStream = null; } + if (mZipFile != null) { + mZipFile.close(); + mZipFile = null; + } } catch (IOException e) { // ignore } } - int getResult() { - return mResult; + boolean isCompleted() { + return mIsCompleted; } boolean commit() { - if (mInstallationSession == null) { - return false; - } - - return mInstallationSession.commit(); + return mDynSystem.setEnable(true, true); } }