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

Commit 20c05288 authored by Philip P. Moltmann's avatar Philip P. Moltmann
Browse files

Factor our staging step to own activity.

To make the Package Installer safe against acivity lifecyle I need to
make each installation step a separate activity. Also the activity's
with progress (staging, installation-progress) have to be carefully
crafted to work correctly with the activity lifecycle.

There will be further changed dealing with the other steps.

Test: Installed from content-URI
      Forced staging to fail and checked that dialog works
      Canceled staging via cancel button
Change-Id: I914fad6898f9ed9c71f18370811dca1986040561
parent bf586567
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -65,6 +65,10 @@
                android:theme="@style/Theme.DialogWhenLargeNoAnimation"
                android:exported="false" />

        <activity android:name=".InstallStaging"
                android:theme="@style/Theme.DialogWhenLargeNoAnimation"
                android:exported="false" />

        <activity android:name=".UninstallerActivity"
                android:configChanges="orientation|keyboardHidden|screenSize"
                android:excludeFromRecents="true"
+95 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  Copyright (C) 2016 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.
  -->

<!--
  Defines the layout of the splash screen that displays the security
  settings required for an application and requests the confirmation of the
  user before it is installed.
-->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

    <!-- title bar -->
    <LinearLayout android:id="@+id/app_snippet"
            android:layout_width="match_parent"
            android:layout_height="?android:attr/actionBarSize"
            android:background="?android:attr/colorPrimary"
            android:elevation="4dp"
            android:gravity="center_vertical"
            android:orientation="horizontal">

        <ImageView android:layout_width="24dp"
                android:layout_height="24dp"
                android:layout_marginLeft="16dp"
                android:scaleType="fitCenter"
                android:src="@drawable/ic_file_download"
                android:tint="?android:attr/colorControlNormal" />

        <TextView android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="32dp"
                android:layout_marginRight="16dp"
                android:ellipsize="end"
                android:singleLine="true"
                android:text="@string/app_name_unknown"
                android:textAppearance="?android:attr/titleTextStyle" />

    </LinearLayout>

    <!-- header comment -->
    <TextView android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="?android:attr/colorPrimary"
            android:elevation="4dp"
            android:paddingBottom="16dp"
            android:paddingLeft="16dp"
            android:paddingRight="16dp"
            android:text="@string/message_staging"
            android:textAppearance="?android:attr/textAppearanceMedium" />

    <!-- content -->
    <View android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_weight="1" />

    <!-- Bottom buttons -->
    <LinearLayout style="?android:attr/buttonBarStyle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:padding="8dp">

        <!-- spacer to push button to the right -->
        <View android:layout_width="0dp"
                android:layout_height="0dp"
                android:layout_weight="1" />

        <Button android:id="@+id/cancel_button"
                style="?android:attr/buttonBarButtonStyle"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:maxLines="2"
                android:text="@string/cancel" />

    </LinearLayout>

</LinearLayout>

+6 −10
Original line number Diff line number Diff line
@@ -165,7 +165,10 @@ public class InstallInstalling extends Activity {
    private void launchSuccess() {
        Intent successIntent = new Intent(getIntent());
        successIntent.setClass(this, InstallSuccess.class);
        startActivityForResult(successIntent, 0);
        successIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);

        startActivity(successIntent);
        finish();
    }

    /**
@@ -181,18 +184,11 @@ public class InstallInstalling extends Activity {

        Intent failureIntent = new Intent(getIntent());
        failureIntent.setClass(this, InstallFailed.class);
        failureIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
        failureIntent.putExtra(PackageInstaller.EXTRA_STATUS, statusCode);
        failureIntent.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, statusMessage);

        startActivityForResult(failureIntent, 0);
    }


    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        // Pass on result
        setResult(resultCode, data);

        startActivity(failureIntent);
        finish();
    }

+220 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.packageinstaller;

import android.annotation.Nullable;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * If a package gets installed from an content URI this step loads the package and turns it into
 * and installation from a file. Then it re-starts the installation as usual.
 */
public class InstallStaging extends Activity {
    private static final String LOG_TAG = InstallStaging.class.getSimpleName();

    private static final String STAGED_FILE = "STAGED_FILE";

    /** Currently running task that loads the file from the content URI into a file */
    private @Nullable StagingAsyncTask mStagingTask;

    /** The file the package is in */
    private @Nullable File mStagedFile;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.install_staging);

        if (savedInstanceState != null) {
            mStagedFile = new File(savedInstanceState.getString(STAGED_FILE));

            if (!mStagedFile.exists()) {
                mStagedFile = null;
            }
        }

        findViewById(R.id.cancel_button).setOnClickListener(view -> {
            if (mStagingTask != null) {
                mStagingTask.cancel(true);
            }
            setResult(RESULT_CANCELED);
            finish();
        });
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        setResult(resultCode, data);

        if (mStagedFile != null) {
            mStagedFile.delete();
        }

        // This is executed before onResume but after the mStagingTask completed, hence no need
        // to deal with the task.
        finish();
    }

    @Override
    protected void onResume() {
        super.onResume();

        // This is the first onResume in a single life of the activity
        if (mStagingTask == null) {
            // File does not exist, or became invalid
            if (mStagedFile == null) {
                // Create file delayed to be able to show error
                try {
                    mStagedFile = File.createTempFile("package", ".apk", getCacheDir());
                } catch (IOException e) {
                    showError();
                    return;
                }
            }

            mStagingTask = new StagingAsyncTask();
            mStagingTask.execute(getIntent().getData());
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        outState.putString(STAGED_FILE, mStagedFile.getPath());
    }

    @Override
    protected void onDestroy() {
        if (mStagingTask != null) {
            mStagingTask.cancel(true);
        }

        super.onDestroy();
    }

    /**
     * Show an error message and set result as error.
     */
    private void showError() {
        (new ErrorDialog()).showAllowingStateLoss(getFragmentManager(), "error");

        Intent result = new Intent();
        result.putExtra(Intent.EXTRA_INSTALL_RESULT,
                PackageManager.INSTALL_FAILED_INVALID_APK);
        setResult(RESULT_FIRST_USER, result);
    }

    /**
     * Dialog for errors while staging.
     */
    public static class ErrorDialog extends DialogFragment {
        private Activity mActivity;

        @Override
        public void onAttach(Context context) {
            super.onAttach(context);

            mActivity = (Activity) context;
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            AlertDialog alertDialog = new AlertDialog.Builder(mActivity)
                    .setMessage(R.string.Parse_error_dlg_text)
                    .setPositiveButton(R.string.ok,
                            (dialog, which) -> mActivity.finish())
                    .create();
            alertDialog.setCanceledOnTouchOutside(false);

            return alertDialog;
        }

        @Override
        public void onCancel(DialogInterface dialog) {
            super.onCancel(dialog);

            mActivity.finish();
        }
    }

    private final class StagingAsyncTask extends AsyncTask<Uri, Void, Boolean> {
        @Override
        protected Boolean doInBackground(Uri... params) {
            if (params == null || params.length <= 0) {
                return false;
            }
            Uri packageUri = params[0];
            try (InputStream in = getContentResolver().openInputStream(packageUri)) {
                // Despite the comments in ContentResolver#openInputStream the returned stream can
                // be null.
                if (in == null) {
                    return false;
                }

                try (OutputStream out = new FileOutputStream(mStagedFile)) {
                    byte[] buffer = new byte[4096];
                    int bytesRead;
                    while ((bytesRead = in.read(buffer)) >= 0) {
                        // Be nice and respond to a cancellation
                        if (isCancelled()) {
                            return false;
                        }
                        out.write(buffer, 0, bytesRead);
                    }
                }
            } catch (IOException | SecurityException e) {
                Log.w(LOG_TAG, "Error staging apk from content URI", e);
                return false;
            }
            return true;
        }

        @Override
        protected void onPostExecute(Boolean success) {
            if (success) {
                // Now start the installation again from a file
                Intent installIntent = new Intent(getIntent());
                installIntent.setClass(InstallStaging.this, PackageInstallerActivity.class);
                installIntent.setData(Uri.fromFile(mStagedFile));
                installIntent
                        .setFlags(installIntent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT);
                startActivityForResult(installIntent, 0);
            } else {
                showError();
            }
        }
    }
}
+36 −134
Original line number Diff line number Diff line
@@ -32,9 +32,7 @@ import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageParser;
import android.content.pm.PackageUserState;
import android.content.pm.VerificationParams;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Process;
@@ -48,16 +46,10 @@ import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AppSecurityPermissions;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TabHost;
import android.widget.TextView;
import com.android.packageinstaller.permission.utils.Utils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/*
 * This activity is launched when a new application is installed via side loading
@@ -78,14 +70,13 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen
    private static final String SCHEME_CONTENT = "content";
    private static final String SCHEME_PACKAGE = "package";

    private static final String EXTRA_ORIGINAL_SOURCE_INFO = "EXTRA_ORIGINAL_SOURCE_INFO";

    private int mSessionId = -1;
    private Uri mPackageURI;
    private Uri mOriginatingURI;
    private Uri mReferrerURI;
    private int mOriginatingUid = VerificationParams.NO_UID;
    private File mContentUriApkStagingFile;

    private AsyncTask<Uri, Void, File> mStagingAsynTask;

    private boolean localLOGV = false;
    PackageManager mPm;
@@ -307,7 +298,7 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen
                    .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int which) {
                            setResult(RESULT_OK);
                            clearCachedApkIfNeededAndFinish();
                            finish();
                        }
                    })
                    .setOnCancelListener(this)
@@ -329,7 +320,7 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen
        if (request == REQUEST_ENABLE_UNKNOWN_SOURCES && result == RESULT_OK) {
            checkIfAllowedAndInitiateInstall(true);
        } else {
            clearCachedApkIfNeededAndFinish();
            finish();
        }
    }

@@ -337,8 +328,6 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen
        String callerPackage = getCallingPackage();
        if (callerPackage != null && intent.getBooleanExtra(
                Intent.EXTRA_NOT_UNKNOWN_SOURCE, false)) {
            try {
                mSourceInfo = mPm.getApplicationInfo(callerPackage, 0);
            if (mSourceInfo != null) {
                if ((mSourceInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED)
                        != 0) {
@@ -346,8 +335,6 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen
                    return false;
                }
            }
            } catch (NameNotFoundException e) {
            }
        }

        return true;
@@ -411,6 +398,15 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen
        mUserManager = (UserManager) getSystemService(Context.USER_SERVICE);

        final Intent intent = getIntent();

        // This activity might have been started by InstallStaging. In this case recover
        // the info from the app that initiated the install request
        if (getPackageName().equals(getCallingPackage())) {
            mSourceInfo = getIntent().getParcelableExtra(EXTRA_ORIGINAL_SOURCE_INFO);
        } else {
            mSourceInfo = getSourceInfo();
        }

        mOriginatingUid = getOriginatingUid(intent);

        final Uri packageUri;
@@ -492,7 +488,7 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen
                }
            } else {
                startActivity(new Intent(Settings.ACTION_SHOW_ADMIN_SUPPORT_DETAILS));
                clearCachedApkIfNeededAndFinish();
                finish();
            }
        } else if (!isUnknownSourcesEnabled() && isManagedProfile) {
            showDialogInner(DLG_ADMIN_RESTRICTS_UNKNOWN_SOURCES);
@@ -510,10 +506,6 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen

    @Override
    protected void onDestroy() {
        if (mStagingAsynTask != null) {
            mStagingAsynTask.cancel(true);
            mStagingAsynTask = null;
        }
        super.onDestroy();
    }

@@ -567,15 +559,24 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen
            } break;

            case SCHEME_CONTENT: {
                mStagingAsynTask = new StagingAsyncTask();
                mStagingAsynTask.execute(packageUri);
                Intent installStaging = new Intent(getIntent());
                installStaging.setClass(this, InstallStaging.class);

                // Store UID which might not be set in original intent
                installStaging.putExtra(Intent.EXTRA_ORIGINATING_UID, mOriginatingUid);

                // Store source info as when called back the source is the packageinstaller
                installStaging.putExtra(EXTRA_ORIGINAL_SOURCE_INFO, mSourceInfo);
                installStaging.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
                startActivity(installStaging);
                finish();
                return false;
            }

            default: {
                Log.w(TAG, "Unsupported scheme " + scheme);
                setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI);
                clearCachedApkIfNeededAndFinish();
                finish();
                return false;
            }
        }
@@ -609,8 +610,7 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen
        // Get the source info from the calling package, if available. This will be the
        // definitive calling package, but it only works if the intent was started using
        // startActivityForResult,
        ApplicationInfo sourceInfo = getSourceInfo();
        if (sourceInfo != null) {
        if (mSourceInfo != null) {
            if (uidFromIntent != VerificationParams.NO_UID &&
                    (mSourceInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0) {
                return uidFromIntent;
@@ -618,7 +618,7 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen
            }
            // We either didn't get a uid in the intent, or we don't trust it. Use the
            // uid of the calling package instead.
            return sourceInfo.uid;
            return mSourceInfo.uid;
        }

        // We couldn't get the specific calling package. Let's get the uid instead
@@ -667,7 +667,7 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen

    // Generic handling when pressing back key
    public void onCancel(DialogInterface dialog) {
        clearCachedApkIfNeededAndFinish();
        finish();
    }

    public void onClick(View v) {
@@ -675,7 +675,7 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen
            if (mOkCanInstall || mScrollView == null) {
                if (mSessionId != -1) {
                    mInstaller.setPermissionsResult(mSessionId, true);
                    clearCachedApkIfNeededAndFinish();
                    finish();
                } else {
                    startInstall();
                }
@@ -688,7 +688,7 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen
            if (mSessionId != -1) {
                mInstaller.setPermissionsResult(mSessionId, false);
            }
            clearCachedApkIfNeededAndFinish();
            finish();
        }
    }

@@ -722,102 +722,4 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen
        startActivity(newIntent);
        finish();
    }

    private void clearCachedApkIfNeededAndFinish() {
        if (mContentUriApkStagingFile != null) {
            mContentUriApkStagingFile.delete();
            mContentUriApkStagingFile = null;
        }
        finish();
    }

    private final class StagingAsyncTask extends AsyncTask<Uri, Void, File> {
        private static final long SHOW_EMPTY_STATE_DELAY_MILLIS = 300;

        private final Runnable mEmptyStateRunnable = new Runnable() {
            @Override
            public void run() {
                ((TextView) findViewById(R.id.app_name)).setText(R.string.app_name_unknown);
                ((TextView) findViewById(R.id.install_confirm_question))
                        .setText(R.string.message_staging);
                mInstallConfirm.setVisibility(View.VISIBLE);
                findViewById(android.R.id.tabhost).setVisibility(View.INVISIBLE);
                findViewById(R.id.spacer).setVisibility(View.VISIBLE);
                findViewById(R.id.ok_button).setEnabled(false);
                Drawable icon = getDrawable(R.drawable.ic_file_download);
                Utils.applyTint(PackageInstallerActivity.this,
                        icon, android.R.attr.colorControlNormal);
                ((ImageView) findViewById(R.id.app_icon)).setImageDrawable(icon);
            }
        };

        @Override
        protected void onPreExecute() {
            getWindow().getDecorView().postDelayed(mEmptyStateRunnable,
                    SHOW_EMPTY_STATE_DELAY_MILLIS);
        }

        @Override
        protected File doInBackground(Uri... params) {
            if (params == null || params.length <= 0) {
                return null;
            }
            Uri packageUri = params[0];
            File sourceFile = null;
            try {
                sourceFile = File.createTempFile("package", ".apk", getCacheDir());
                try (
                    InputStream in = getContentResolver().openInputStream(packageUri);
                    OutputStream out = (in != null) ? new FileOutputStream(
                            sourceFile) : null;
                ) {
                    // Despite the comments in ContentResolver#openInputStream
                    // the returned stream can be null.
                    if (in == null) {
                        return null;
                    }
                    byte[] buffer = new byte[4096];
                    int bytesRead;
                    while ((bytesRead = in.read(buffer)) >= 0) {
                        // Be nice and respond to a cancellation
                        if (isCancelled()) {
                            return null;
                        }
                        out.write(buffer, 0, bytesRead);
                    }
                }
            } catch (IOException | SecurityException e) {
                Log.w(TAG, "Error staging apk from content URI", e);
                if (sourceFile != null) {
                    sourceFile.delete();
                }
            }
            return sourceFile;
        }

        @Override
        protected void onPostExecute(File file) {
            getWindow().getDecorView().removeCallbacks(mEmptyStateRunnable);
            if (isFinishing() || isDestroyed()) {
                return;
            }
            if (file == null) {
                showDialogInner(DLG_PACKAGE_ERROR);
                setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK);
                return;
            }
            mContentUriApkStagingFile = file;
            Uri fileUri = Uri.fromFile(file);

            boolean wasSetUp = processPackageUri(fileUri);
            if (wasSetUp) {
                checkIfAllowedAndInitiateInstall(false);
            }
        }

        @Override
        protected void onCancelled(File file) {
            getWindow().getDecorView().removeCallbacks(mEmptyStateRunnable);
        }
    };
}