/* * 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 android.content.Context; import android.gsi.GsiProgress; import android.net.Uri; import android.os.AsyncTask; import android.os.image.DynamicSystemManager; import android.util.Log; import android.webkit.URLUtil; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Arrays; import java.util.Locale; import java.util.zip.GZIPInputStream; class InstallationAsyncTask extends AsyncTask { private static final String TAG = "InstallationAsyncTask"; private static final int READ_BUFFER_SIZE = 1 << 13; private class InvalidImageUrlException extends RuntimeException { private InvalidImageUrlException(String message) { super(message); } } /** Not completed, including being cancelled */ static final int NO_RESULT = 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_ERROR_EXCEPTION = 6; interface InstallStatusListener { void onProgressUpdate(long installedSize); void onResult(int resultCode, Throwable detail); void onCancelled(); } private final String mUrl; private final long mSystemSize; private final long mUserdataSize; private final Context mContext; private final DynamicSystemManager mDynSystem; private final InstallStatusListener mListener; private DynamicSystemManager.Session mInstallationSession; private int mResult = NO_RESULT; private InputStream mStream; InstallationAsyncTask(String url, long systemSize, long userdataSize, Context context, DynamicSystemManager dynSystem, InstallStatusListener listener) { mUrl = url; mSystemSize = systemSize; mUserdataSize = userdataSize; mContext = context; mDynSystem = dynSystem; 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; long minStepToReport = (mSystemSize + mUserdataSize) / 100; // init input stream before calling startInstallation(), which takes 90 seconds. initInputStream(); Thread thread = new Thread(() -> { mInstallationSession = mDynSystem.startInstallation(mSystemSize, mUserdataSize); }); 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); } if (mInstallationSession == null) { throw new IOException("Failed to start installation with requested size: " + (mSystemSize + mUserdataSize)); } installedSize = mUserdataSize; byte[] bytes = new byte[READ_BUFFER_SIZE]; int numBytesRead; Log.d(TAG, "Start installation loop"); while ((numBytesRead = mStream.read(bytes, 0, READ_BUFFER_SIZE)) != -1) { if (isCancelled()) { break; } byte[] writeBuffer = numBytesRead == READ_BUFFER_SIZE ? bytes : Arrays.copyOf(bytes, numBytesRead); if (!mInstallationSession.write(writeBuffer)) { throw new IOException("Failed write() to DynamicSystem"); } installedSize += numBytesRead; if (installedSize > reportedInstalledSize + minStepToReport) { publishProgress(installedSize); reportedInstalledSize = installedSize; } } return null; } catch (Exception e) { e.printStackTrace(); return e; } finally { close(); } } @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; } else { mResult = RESULT_ERROR_EXCEPTION; } Log.d(TAG, "onPostExecute(), URL: " + mUrl + ", result: " + mResult); mListener.onResult(mResult, detail); } @Override protected void onProgressUpdate(Long... values) { long 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))); } else { throw new InvalidImageUrlException( String.format(Locale.US, "Unsupported file source: %s", mUrl)); } } private void close() { try { if (mStream != null) { mStream.close(); mStream = null; } } catch (IOException e) { // ignore } } int getResult() { return mResult; } boolean commit() { if (mInstallationSession == null) { return false; } return mInstallationSession.commit(); } }