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

Commit a79235cb authored by Sumedh Sen's avatar Sumedh Sen
Browse files

Stage file for install

If the caller used intents to install an app, stage the supplied APK for install. Show the staging progress on UI.

Bug: 182205982
Test: builds successfully
Test: No CTS Tests. Flag to use new app is turned off by default

Change-Id: I9e8436a2403d549095a131067cfab9abc2c9931f
parent 1b8522ca
Loading
Loading
Loading
Loading
+154 −3
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.packageinstaller.v2.model;

import static com.android.packageinstaller.v2.model.PackageUtil.canPackageQuery;
import static com.android.packageinstaller.v2.model.PackageUtil.isCallerSessionOwner;
import static com.android.packageinstaller.v2.model.PackageUtil.isInstallPermissionGrantedOrRequested;
import static com.android.packageinstaller.v2.model.PackageUtil.isPermissionGranted;
@@ -23,31 +24,42 @@ import static com.android.packageinstaller.v2.model.installstagedata.InstallAbor
import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_POLICY;

import android.Manifest;
import android.app.Activity;
import android.app.admin.DevicePolicyManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.EventLog;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.MutableLiveData;
import com.android.packageinstaller.v2.model.installstagedata.InstallAborted;
import com.android.packageinstaller.v2.model.installstagedata.InstallReady;
import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
import com.android.packageinstaller.v2.model.installstagedata.InstallStaging;
import java.io.IOException;

public class InstallRepository {

    private static final String SCHEME_PACKAGE = "package";
    private static final String TAG = InstallRepository.class.getSimpleName();
    private final Context mContext;
    private final PackageManager mPackageManager;
    private final PackageInstaller mPackageInstaller;
    private final UserManager mUserManager;
    private final DevicePolicyManager mDevicePolicyManager;
    private final MutableLiveData<InstallStage> mStagingResult = new MutableLiveData<>();
    private final boolean mLocalLOGV = false;
    private Intent mIntent;
    private boolean mIsSessionInstall;
@@ -56,8 +68,13 @@ public class InstallRepository {
     * Session ID for a session created when caller uses PackageInstaller APIs
     */
    private int mSessionId;
    /**
     * Session ID for a session created by this app
     */
    private int mStagedSessionId = SessionInfo.INVALID_ID;
    private int mCallingUid;
    private String mCallingPackage;
    private SessionStager mSessionStager;

    public InstallRepository(Context context) {
        mContext = context;
@@ -71,10 +88,10 @@ public class InstallRepository {
     * Extracts information from the incoming install intent, checks caller's permission to install
     * packages, verifies that the caller is the install session owner (in case of a session based
     * install) and checks if the current user has restrictions set that prevent app installation,
     *
     * @param intent the incoming {@link Intent} object for installing a package
     * @param callerInfo {@link CallerInfo} that holds the callingUid and callingPackageName
     * @return
     * <p>{@link InstallAborted} if there are errors while performing the checks</p>
     * @return <p>{@link InstallAborted} if there are errors while performing the checks</p>
     *     <p>{@link InstallStaging} after successfully performing the checks</p>
     */
    public InstallStage performPreInstallChecks(Intent intent, CallerInfo callerInfo) {
@@ -197,6 +214,140 @@ public class InstallRepository {
        }
    }

    public void stageForInstall() {
        Uri uri = mIntent.getData();
        if (mIsSessionInstall || (uri != null && SCHEME_PACKAGE.equals(uri.getScheme()))) {
            // For a session based install or installing with a package:// URI, there is no file
            // for us to stage. Setting the mStagingResult as null will signal InstallViewModel to
            // proceed with user confirmation stage.
            mStagingResult.setValue(new InstallReady());
            return;
        }
        if (uri != null
            && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())
            && canPackageQuery(mContext, mCallingUid, uri)) {

            if (mStagedSessionId > 0) {
                final PackageInstaller.SessionInfo info =
                    mPackageInstaller.getSessionInfo(mStagedSessionId);
                if (info == null || !info.isActive() || info.getResolvedBaseApkPath() == null) {
                    Log.w(TAG, "Session " + mStagedSessionId + " in funky state; ignoring");
                    if (info != null) {
                        cleanupStagingSession();
                    }
                    mStagedSessionId = 0;
                }
            }

            // Session does not exist, or became invalid.
            if (mStagedSessionId <= 0) {
                // Create session here to be able to show error.
                try (final AssetFileDescriptor afd =
                    mContext.getContentResolver().openAssetFileDescriptor(uri, "r")) {
                    ParcelFileDescriptor pfd = afd != null ? afd.getParcelFileDescriptor() : null;
                    PackageInstaller.SessionParams params =
                        createSessionParams(mIntent, pfd, uri.toString());
                    mStagedSessionId = mPackageInstaller.createSession(params);
                } catch (IOException e) {
                    Log.w(TAG, "Failed to create a staging session", e);
                    mStagingResult.setValue(
                        new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR)
                            .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT,
                                PackageManager.INSTALL_FAILED_INVALID_APK))
                            .setActivityResultCode(Activity.RESULT_FIRST_USER)
                            .build());
                    return;
                }
            }

            SessionStageListener listener = new SessionStageListener() {
                @Override
                public void onStagingSuccess(SessionInfo info) {
                    //TODO: Verify if the returned sessionInfo should be used anywhere
                    mStagingResult.setValue(new InstallReady());
                }

                @Override
                public void onStagingFailure() {
                    cleanupStagingSession();
                    mStagingResult.setValue(
                        new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR)
                            .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT,
                                PackageManager.INSTALL_FAILED_INVALID_APK))
                            .setActivityResultCode(Activity.RESULT_FIRST_USER)
                            .build());
                }
            };
            if (mSessionStager != null) {
                mSessionStager.cancel(true);
            }
            mSessionStager = new SessionStager(mContext, uri, mStagedSessionId, listener);
            mSessionStager.execute();
        }
    }

    private void cleanupStagingSession() {
        if (mStagedSessionId > 0) {
            try {
                mPackageInstaller.abandonSession(mStagedSessionId);
            } catch (SecurityException ignored) {
            }
            mStagedSessionId = 0;
        }
    }

    private PackageInstaller.SessionParams createSessionParams(@NonNull Intent intent,
        @Nullable ParcelFileDescriptor pfd, @NonNull String debugPathName) {
        PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
            PackageInstaller.SessionParams.MODE_FULL_INSTALL);
        final Uri referrerUri = intent.getParcelableExtra(Intent.EXTRA_REFERRER, Uri.class);
        params.setPackageSource(
            referrerUri != null ? PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE
                : PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE);
        params.setInstallAsInstantApp(false);
        params.setReferrerUri(referrerUri);
        params.setOriginatingUri(
            intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI, Uri.class));
        params.setOriginatingUid(intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID,
            Process.INVALID_UID));
        params.setInstallerPackageName(intent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME));
        params.setInstallReason(PackageManager.INSTALL_REASON_USER);
        // Disable full screen intent usage by for sideloads.
        params.setPermissionState(Manifest.permission.USE_FULL_SCREEN_INTENT,
            PackageInstaller.SessionParams.PERMISSION_STATE_DENIED);

        if (pfd != null) {
            try {
                final PackageInstaller.InstallInfo result = mPackageInstaller.readInstallInfo(pfd,
                    debugPathName, 0);
                params.setAppPackageName(result.getPackageName());
                params.setInstallLocation(result.getInstallLocation());
                params.setSize(result.calculateInstalledSize(params, pfd));
            } catch (PackageInstaller.PackageParsingException e) {
                Log.e(TAG, "Cannot parse package " + debugPathName + ". Assuming defaults.", e);
                params.setSize(pfd.getStatSize());
            } catch (IOException e) {
                Log.e(TAG,
                    "Cannot calculate installed size " + debugPathName
                        + ". Try only apk size.", e);
            }
        } else {
            Log.e(TAG, "Cannot parse package " + debugPathName + ". Assuming defaults.");
        }
        return params;
    }

    public MutableLiveData<Integer> getStagingProgress() {
        if (mSessionStager != null) {
            return mSessionStager.getProgress();
        }
        return new MutableLiveData<>(0);
    }

    public MutableLiveData<InstallStage> getStagingResult() {
        return mStagingResult;
    }

    public interface SessionStageListener {

        void onStagingSuccess(SessionInfo info);
+26 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Process;
import android.util.Log;
@@ -81,6 +82,31 @@ public class PackageUtil {
        return targetSdkVersion;
    }

    public static boolean canPackageQuery(Context context, int callingUid, Uri packageUri) {
        PackageManager pm = context.getPackageManager();
        ProviderInfo info = pm.resolveContentProvider(packageUri.getAuthority(),
            PackageManager.ComponentInfoFlags.of(0));
        if (info == null) {
            return false;
        }
        String targetPackage = info.packageName;

        String[] callingPackages = pm.getPackagesForUid(callingUid);
        if (callingPackages == null) {
            return false;
        }
        for (String callingPackage : callingPackages) {
            try {
                if (pm.canPackageQuery(callingPackage, targetPackage)) {
                    return true;
                }
            } catch (PackageManager.NameNotFoundException e) {
                // no-op
            }
        }
        return false;
    }

    /**
     * @param context the {@link Context} object
     * @param permission the permission name to check
+16 −2
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.packageinstaller.v2.model.installstagedata;


import android.app.Activity;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -45,11 +46,14 @@ public class InstallAborted extends InstallStage {
     */
    @Nullable
    private final Intent mIntent;
    private final int mActivityResultCode;

    private InstallAborted(int reason, @NonNull String message, @Nullable Intent intent) {
    private InstallAborted(int reason, @NonNull String message, @Nullable Intent intent,
        int activityResultCode) {
        mAbortReason = reason;
        mMessage = message;
        mIntent = intent;
        mActivityResultCode = activityResultCode;
    }

    public int getAbortReason() {
@@ -66,6 +70,10 @@ public class InstallAborted extends InstallStage {
        return mIntent;
    }

    public int getActivityResultCode() {
        return mActivityResultCode;
    }

    @Override
    public int getStageCode() {
        return mStage;
@@ -76,6 +84,7 @@ public class InstallAborted extends InstallStage {
        private final int mAbortReason;
        private String mMessage = "";
        private Intent mIntent = null;
        private int mActivityResultCode = Activity.RESULT_CANCELED;

        public Builder(int reason) {
            mAbortReason = reason;
@@ -91,8 +100,13 @@ public class InstallAborted extends InstallStage {
            return this;
        }

        public Builder setActivityResultCode(int resultCode) {
            mActivityResultCode = resultCode;
            return this;
        }

        public InstallAborted build() {
            return new InstallAborted(mAbortReason, mMessage, mIntent);
            return new InstallAborted(mAbortReason, mMessage, mIntent, mActivityResultCode);
        }
    }
}
+27 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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
 *
 *      https://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.v2.model.installstagedata;

public class InstallReady extends InstallStage{

    private final int mStage = InstallStage.STAGE_READY;

    @Override
    public int getStageCode() {
        return mStage;
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -82,6 +82,7 @@ public class InstallLaunch extends FragmentActivity {
        if (installStage.getStageCode() == InstallStage.STAGE_STAGING) {
            InstallStagingFragment stagingDialog = new InstallStagingFragment();
            showDialogInner(stagingDialog);
            mInstallViewModel.getStagingProgress().observe(this, stagingDialog::setProgress);
        } else if (installStage.getStageCode() == InstallStage.STAGE_ABORTED) {
            InstallAborted aborted = (InstallAborted) installStage;
            switch (aborted.getAbortReason()) {
Loading