Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f1d9e1ff authored by Po-Chien Hsueh's avatar Po-Chien Hsueh Committed by Gerrit Code Review
Browse files

Merge changes from topic "zip_and_sparse"

* changes:
  DSU to support zip files
  Create a SparseInputstream
parents 39083517 99d18ddd
Loading
Loading
Loading
Loading
+41 −26
Original line number Diff line number Diff line
@@ -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");
+306 −112
Original line number Diff line number Diff line
@@ -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<String, Long, Throwable> {
class InstallationAsyncTask extends AsyncTask<String, InstallationAsyncTask.Progress, Throwable> {

    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<String> UNSUPPORTED_PARTITIONS =
            Arrays.asList("vbmeta", "boot", "userdata", "dtbo", "super_empty", "system_other");

    private class UnsupportedUrlException extends RuntimeException {
        private UnsupportedUrlException(String message) {
            super(message);
        }
    }

    private class UnsupportedFormatException extends RuntimeException {
        private UnsupportedFormatException(String message) {
            super(message);
        }
    }

    /** Not completed, including being cancelled */
    static final int NO_RESULT = 0;
    /** 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<String, Long, Throwable> {
    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,48 +116,132 @@ class InstallationAsyncTask extends AsyncTask<String, Long, Throwable> {
        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();

            Thread thread =
                    new Thread(
                            () -> {
            mDynSystem.startInstallation();
                                mDynSystem.createPartition("userdata", mUserdataSize, false);
                                mInstallationSession =
                                        mDynSystem.createPartition("system", mSystemSize, true);

            installUserdata();
            if (isCancelled()) {
                mDynSystem.remove();
                return null;
            }

            installImages();
            if (isCancelled()) {
                mDynSystem.remove();
                return null;
            }

            mDynSystem.finishInstallation();
        } 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);

        if (mDynSystem.abort()) {
            Log.d(TAG, "Installation aborted");
        } else {
            Log.w(TAG, "DynamicSystemManager.abort() returned false");
        }

        mListener.onResult(RESULT_CANCELLED, null);
    }

    @Override
    protected void onProgressUpdate(Progress... values) {
        Progress progress = values[0];
        mListener.onProgressUpdate(progress);
    }

    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 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()) {
                    boolean aborted = mDynSystem.abort();
                    Log.d(TAG, "Called DynamicSystemManager.abort(), result = " + aborted);
                    return null;
                return;
            }

                GsiProgress progress = mDynSystem.getInstallationProgress();
                installedSize = progress.bytes_processed;
            installedSize = mDynSystem.getInstallationProgress().bytes_processed;

                if (installedSize > reportedInstalledSize + minStepToReport) {
                    publishProgress(installedSize);
                    reportedInstalledSize = installedSize;
            if (installedSize > progress.mInstalledSize + MIN_PROGRESS_TO_PUBLISH) {
                progress.mInstalledSize = installedSize;
                publishProgress(progress);
            }

            Thread.sleep(10);
@@ -130,85 +249,160 @@ class InstallationAsyncTask extends AsyncTask<String, Long, Throwable> {

        if (mInstallationSession == null) {
            throw new IOException(
                        "Failed to start installation with requested size: "
                                + (mSystemSize + mUserdataSize));
                    "Failed to start installation with requested size: " + mUserdataSize);
        }
    }

            installedSize = 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++;
            }

            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;
    private void installLocalZipUpdate() throws IOException, InterruptedException {
        Log.d(TAG, "To install a local ZIP update");

        Enumeration<? extends ZipEntry> entries = mZipFile.entries();
        int numInstalledPartitions = 1;

        while (entries.hasMoreElements()) {
            ZipEntry entry = entries.nextElement();
            if (installImageFromAnEntry(
                    entry, mZipFile.getInputStream(entry), numInstalledPartitions)) {
                numInstalledPartitions++;
            }

                if (installedSize > reportedInstalledSize + minStepToReport) {
                    publishProgress(installedSize);
                    reportedInstalledSize = installedSize;
            if (isCancelled()) {
                break;
            }
        }
    }
            mDynSystem.finishInstallation();
            return null;

        } catch (Exception e) {
            e.printStackTrace();
            return e;
        } finally {
            close();
    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;
        }

    @Override
    protected void onCancelled() {
        Log.d(TAG, "onCancelled(), URL: " + mUrl);
        long uncompressedSize = entry.getSize();

        mListener.onCancelled();
        installImage(partitionName, uncompressedSize, is, numInstalledPartitions);

        return true;
    }

    @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;
    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 {
            mResult = RESULT_ERROR_EXCEPTION;
            throw new IOException("Cannot get raw size for " + partitionName);
        }

        Log.d(TAG, "onPostExecute(), URL: " + mUrl + ", result: " + mResult);
        Thread thread = new Thread(() -> {
            mInstallationSession =
                    mDynSystem.createPartition(partitionName, partitionSize, true);
        });

        Log.d(TAG, "Start creating partition: " + partitionName);
        thread.start();

        mListener.onResult(mResult, detail);
        while (thread.isAlive()) {
            if (isCancelled()) {
                return;
            }

    @Override
    protected void onProgressUpdate(Long... values) {
        long progress = values[0];
        mListener.onProgressUpdate(progress);
            Thread.sleep(10);
        }

    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)));
        } else {
            throw new InvalidImageUrlException(
                    String.format(Locale.US, "Unsupported file source: %s", mUrl));
        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<String, Long, Throwable> {
                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);
    }
}
+199 −0

File added.

Preview size limit exceeded, changes collapsed.