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

Commit 01dd3870 authored by Jakob Schneider's avatar Jakob Schneider Committed by Android (Google) Code Review
Browse files

Merge "Add confirmation dialog for unarchival if app only possesses weak permissions." into main

parents 51c6d52b 83dd0af2
Loading
Loading
Loading
Loading
+12 −0
Original line number Original line Diff line number Diff line
@@ -181,6 +181,18 @@


        <receiver android:name="androidx.profileinstaller.ProfileInstallReceiver"
        <receiver android:name="androidx.profileinstaller.ProfileInstallReceiver"
            tools:node="remove" />
            tools:node="remove" />

        <activity android:name=".UnarchiveActivity"
                  android:configChanges="orientation|keyboardHidden|screenSize"
                  android:theme="@style/Theme.AlertDialogActivity.NoActionBar"
                  android:excludeFromRecents="true"
                  android:noHistory="true"
                  android:exported="true">
            <intent-filter android:priority="1">
                <action android:name="android.intent.action.UNARCHIVE_DIALOG" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
    </application>
    </application>


</manifest>
</manifest>
+10 −0
Original line number Original line Diff line number Diff line
@@ -257,4 +257,14 @@
    <!-- Notification shown in status bar when an application is successfully installed.
    <!-- Notification shown in status bar when an application is successfully installed.
         [CHAR LIMIT=50] -->
         [CHAR LIMIT=50] -->
    <string name="notification_installation_success_status">Successfully installed \u201c<xliff:g id="appname" example="Package Installer">%1$s</xliff:g>\u201d</string>
    <string name="notification_installation_success_status">Successfully installed \u201c<xliff:g id="appname" example="Package Installer">%1$s</xliff:g>\u201d</string>

    <!-- The title of a dialog which asks the user to restore (i.e. re-install, re-download) an app
         after parts of the app have been previously moved into the cloud for temporary storage.
         "installername" is the app that will facilitate the download of the app. [CHAR LIMIT=50] -->
    <string name="unarchive_application_title">Restore <xliff:g id="appname" example="Bird Game">%1$s</xliff:g> from <xliff:g id="installername" example="App Store">%1$s</xliff:g>?</string>
    <!-- After the user confirms the dialog, a download will start. [CHAR LIMIT=none] -->
    <string name="unarchive_body_text">This app will begin to download in the background</string>
    <!-- The action to restore (i.e. re-install, re-download) an app after parts of the app have been previously moved
         into the cloud for temporary storage. [CHAR LIMIT=15] -->
    <string name="restore">Restore</string>
</resources>
</resources>
+151 −0
Original line number Original line 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
 *
 *      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 static android.Manifest.permission;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.content.pm.PackageManager.MATCH_ARCHIVED_PACKAGES;

import android.app.Activity;
import android.app.DialogFragment;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.content.IntentSender;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Process;
import android.util.Log;

import androidx.annotation.Nullable;

import java.io.IOException;
import java.util.Arrays;
import java.util.Objects;

public class UnarchiveActivity extends Activity {

    public static final String EXTRA_UNARCHIVE_INTENT_SENDER =
            "android.content.pm.extra.UNARCHIVE_INTENT_SENDER";
    static final String APP_TITLE = "com.android.packageinstaller.unarchive.app_title";
    static final String INSTALLER_TITLE = "com.android.packageinstaller.unarchive.installer_title";

    private static final String TAG = "UnarchiveActivity";

    private String mPackageName;
    private IntentSender mIntentSender;

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(null);

        int callingUid = getLaunchedFromUid();
        if (callingUid == Process.INVALID_UID) {
            // Cannot reach Package/ActivityManager. Aborting uninstall.
            Log.e(TAG, "Could not determine the launching uid.");

            setResult(Activity.RESULT_FIRST_USER);
            finish();
            return;
        }

        String callingPackage = getPackageNameForUid(callingUid);
        if (callingPackage == null) {
            Log.e(TAG, "Package not found for originating uid " + callingUid);
            setResult(Activity.RESULT_FIRST_USER);
            finish();
            return;
        }

        // We don't check the AppOpsManager here for REQUEST_INSTALL_PACKAGES because the requester
        // is not the source of the installation.
        boolean hasRequestInstallPermission = Arrays.asList(getRequestedPermissions(callingPackage))
                .contains(permission.REQUEST_INSTALL_PACKAGES);
        boolean hasInstallPermission = getBaseContext().checkPermission(permission.INSTALL_PACKAGES,
                0 /* random value for pid */, callingUid) != PackageManager.PERMISSION_GRANTED;
        if (!hasRequestInstallPermission && !hasInstallPermission) {
            Log.e(TAG, "Uid " + callingUid + " does not have "
                    + permission.REQUEST_INSTALL_PACKAGES + " or "
                    + permission.INSTALL_PACKAGES);
            setResult(Activity.RESULT_FIRST_USER);
            finish();
            return;
        }

        Bundle extras = getIntent().getExtras();
        mPackageName = extras.getString(PackageInstaller.EXTRA_PACKAGE_NAME);
        mIntentSender = extras.getParcelable(EXTRA_UNARCHIVE_INTENT_SENDER, IntentSender.class);
        Objects.requireNonNull(mPackageName);
        Objects.requireNonNull(mIntentSender);

        PackageManager pm = getPackageManager();
        try {
            String appTitle = pm.getApplicationInfo(mPackageName,
                    PackageManager.ApplicationInfoFlags.of(
                            MATCH_ARCHIVED_PACKAGES)).loadLabel(pm).toString();
            // TODO(ag/25387215) Get the real installer title here after fixing getInstallSource for
            //  archived apps.
            showDialogFragment(appTitle, "installerTitle");
        } catch (PackageManager.NameNotFoundException e) {
            Log.e(TAG, "Invalid packageName: " + e.getMessage());
        }
    }

    @Nullable
    private String[] getRequestedPermissions(String callingPackage) {
        String[] requestedPermissions = null;
        try {
            requestedPermissions = getPackageManager()
                    .getPackageInfo(callingPackage, GET_PERMISSIONS).requestedPermissions;
        } catch (PackageManager.NameNotFoundException e) {
            // Should be unreachable because we've just fetched the packageName above.
            Log.e(TAG, "Package not found for " + callingPackage);
        }
        return requestedPermissions;
    }

    void startUnarchive() {
        try {
            getPackageManager().getPackageInstaller().requestUnarchive(mPackageName, mIntentSender);
        } catch (PackageManager.NameNotFoundException | IOException e) {
            Log.e(TAG, "RequestUnarchive failed with %s." + e.getMessage());
        }
    }

    private void showDialogFragment(String appTitle, String installerAppTitle) {
        FragmentTransaction ft = getFragmentManager().beginTransaction();
        Fragment prev = getFragmentManager().findFragmentByTag("dialog");
        if (prev != null) {
            ft.remove(prev);
        }

        Bundle args = new Bundle();
        args.putString(APP_TITLE, appTitle);
        args.putString(INSTALLER_TITLE, installerAppTitle);
        DialogFragment fragment = new UnarchiveFragment();
        fragment.setArguments(args);
        fragment.show(ft, "dialog");
    }

    private String getPackageNameForUid(int sourceUid) {
        String[] packagesForUid = getPackageManager().getPackagesForUid(sourceUid);
        if (packagesForUid == null) {
            return null;
        }
        return packagesForUid[0];
    }
}
+59 −0
Original line number Original line 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
 *
 *      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.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.os.Bundle;

public class UnarchiveFragment extends DialogFragment implements
        DialogInterface.OnClickListener {

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        String appTitle = getArguments().getString(UnarchiveActivity.APP_TITLE);

        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());

        dialogBuilder.setTitle(
                String.format(getContext().getString(R.string.unarchive_application_title),
                        appTitle));
        dialogBuilder.setMessage(R.string.unarchive_body_text);

        dialogBuilder.setPositiveButton(R.string.restore, this);
        dialogBuilder.setNegativeButton(android.R.string.cancel, this);

        return dialogBuilder.create();
    }

    @Override
    public void onClick(DialogInterface dialog, int which) {
        if (which == Dialog.BUTTON_POSITIVE) {
            ((UnarchiveActivity) getActivity()).startUnarchive();
        }
    }

    @Override
    public void onDismiss(DialogInterface dialog) {
        super.onDismiss(dialog);
        if (isAdded()) {
            getActivity().finish();
        }
    }
}
+59 −27
Original line number Original line Diff line number Diff line
@@ -95,6 +95,9 @@ public class PackageArchiver {


    private static final String TAG = "PackageArchiverService";
    private static final String TAG = "PackageArchiverService";


    public static final String EXTRA_UNARCHIVE_INTENT_SENDER =
            "android.content.pm.extra.UNARCHIVE_INTENT_SENDER";

    /**
    /**
     * The maximum time granted for an app store to start a foreground service when unarchival
     * The maximum time granted for an app store to start a foreground service when unarchival
     * is requested.
     * is requested.
@@ -104,6 +107,8 @@ public class PackageArchiver {


    private static final String ARCHIVE_ICONS_DIR = "package_archiver";
    private static final String ARCHIVE_ICONS_DIR = "package_archiver";


    private static final String ACTION_UNARCHIVE_DIALOG = "android.intent.action.UNARCHIVE_DIALOG";

    private final Context mContext;
    private final Context mContext;
    private final PackageManagerService mPm;
    private final PackageManagerService mPm;


@@ -403,11 +408,12 @@ public class PackageArchiver {
        }
        }
        snapshot.enforceCrossUserPermission(binderUid, userId, true, true,
        snapshot.enforceCrossUserPermission(binderUid, userId, true, true,
                "unarchiveApp");
                "unarchiveApp");
        verifyInstallPermissions();


        PackageStateInternal ps;
        PackageStateInternal ps;
        PackageStateInternal callerPs;
        try {
        try {
            ps = getPackageState(packageName, snapshot, binderUid, userId);
            ps = getPackageState(packageName, snapshot, binderUid, userId);
            callerPs = getPackageState(callerPackageName, snapshot, binderUid, userId);
            verifyArchived(ps, userId);
            verifyArchived(ps, userId);
        } catch (PackageManager.NameNotFoundException e) {
        } catch (PackageManager.NameNotFoundException e) {
            throw new ParcelableException(e);
            throw new ParcelableException(e);
@@ -420,12 +426,32 @@ public class PackageArchiver {
                                    packageName)));
                                    packageName)));
        }
        }


        // TODO(b/305902395) Introduce a confirmation dialog if the requestor only holds
        boolean hasInstallPackages = mContext.checkCallingOrSelfPermission(
        // REQUEST_INSTALL permission.
                Manifest.permission.INSTALL_PACKAGES)
                == PackageManager.PERMISSION_GRANTED;
        // We don't check the AppOpsManager here for REQUEST_INSTALL_PACKAGES because the requester
        // is not the source of the installation.
        boolean hasRequestInstallPackages = callerPs.getAndroidPackage().getRequestedPermissions()
                .contains(android.Manifest.permission.REQUEST_INSTALL_PACKAGES);
        if (!hasInstallPackages && !hasRequestInstallPackages) {
            throw new SecurityException("You need the com.android.permission.INSTALL_PACKAGES "
                    + "or com.android.permission.REQUEST_INSTALL_PACKAGES permission to request "
                    + "an unarchival.");
        }

        if (!hasInstallPackages) {
            requestUnarchiveConfirmation(packageName, statusReceiver);
            return;
        }

        // TODO(b/311709794) Check that the responsible installer has INSTALL_PACKAGES or
        // OPSTR_REQUEST_INSTALL_PACKAGES too. Edge case: In reality this should always be the case,
        // unless a user has disabled the permission after archiving an app.

        int draftSessionId;
        int draftSessionId;
        try {
        try {
            draftSessionId = createDraftSession(packageName, installerPackage, statusReceiver,
            draftSessionId = Binder.withCleanCallingIdentity(() ->
                    userId);
                    createDraftSession(packageName, installerPackage, statusReceiver, userId));
        } catch (RuntimeException e) {
        } catch (RuntimeException e) {
            if (e.getCause() instanceof IOException) {
            if (e.getCause() instanceof IOException) {
                throw ExceptionUtils.wrap((IOException) e.getCause());
                throw ExceptionUtils.wrap((IOException) e.getCause());
@@ -438,15 +464,17 @@ public class PackageArchiver {
                () -> unarchiveInternal(packageName, userHandle, installerPackage, draftSessionId));
                () -> unarchiveInternal(packageName, userHandle, installerPackage, draftSessionId));
    }
    }


    private void verifyInstallPermissions() {
    private void requestUnarchiveConfirmation(String packageName, IntentSender statusReceiver) {
        if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES)
        final Intent dialogIntent = new Intent(ACTION_UNARCHIVE_DIALOG);
                != PackageManager.PERMISSION_GRANTED && mContext.checkCallingOrSelfPermission(
        dialogIntent.putExtra(EXTRA_UNARCHIVE_INTENT_SENDER, statusReceiver);
                Manifest.permission.REQUEST_INSTALL_PACKAGES)
        dialogIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName);
                != PackageManager.PERMISSION_GRANTED) {

            throw new SecurityException("You need the com.android.permission.INSTALL_PACKAGES "
        final Intent broadcastIntent = new Intent();
                    + "or com.android.permission.REQUEST_INSTALL_PACKAGES permission to request "
        broadcastIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName);
                    + "an unarchival.");
        broadcastIntent.putExtra(PackageInstaller.EXTRA_UNARCHIVE_STATUS,
        }
                PackageInstaller.STATUS_PENDING_USER_ACTION);
        broadcastIntent.putExtra(Intent.EXTRA_INTENT, dialogIntent);
        sendIntent(statusReceiver, packageName, /* message= */ "", broadcastIntent);
    }
    }


    private void verifyUninstallPermissions() {
    private void verifyUninstallPermissions() {
@@ -461,7 +489,7 @@ public class PackageArchiver {
    }
    }


    private int createDraftSession(String packageName, String installerPackage,
    private int createDraftSession(String packageName, String installerPackage,
            IntentSender statusReceiver, int userId) {
            IntentSender statusReceiver, int userId) throws IOException {
        PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(
        PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(
                PackageInstaller.SessionParams.MODE_FULL_INSTALL);
                PackageInstaller.SessionParams.MODE_FULL_INSTALL);
        sessionParams.setAppPackageName(packageName);
        sessionParams.setAppPackageName(packageName);
@@ -477,12 +505,11 @@ public class PackageArchiver {
            return existingSessionId;
            return existingSessionId;
        }
        }


        int sessionId = Binder.withCleanCallingIdentity(
        int sessionId = mPm.mInstallerService.createSessionInternal(
                () -> mPm.mInstallerService.createSessionInternal(
                sessionParams,
                sessionParams,
                installerPackage, mContext.getAttributionTag(),
                installerPackage, mContext.getAttributionTag(),
                installerUid,
                installerUid,
                        userId));
                userId);
        // TODO(b/297358628) Also cleanup sessions upon device restart.
        // TODO(b/297358628) Also cleanup sessions upon device restart.
        mPm.mHandler.postDelayed(() -> mPm.mInstallerService.cleanupDraftIfUnclaimed(sessionId),
        mPm.mHandler.postDelayed(() -> mPm.mInstallerService.cleanupDraftIfUnclaimed(sessionId),
                getUnarchiveForegroundTimeout());
                getUnarchiveForegroundTimeout());
@@ -692,20 +719,25 @@ public class PackageArchiver {
            String message) {
            String message) {
        Slog.d(TAG, TextUtils.formatSimple("Failed to archive %s with message %s", packageName,
        Slog.d(TAG, TextUtils.formatSimple("Failed to archive %s with message %s", packageName,
                message));
                message));
        final Intent fillIn = new Intent();
        final Intent intent = new Intent();
        fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName);
        intent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName);
        fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
        intent.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
        fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, message);
        intent.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, message);
        sendIntent(statusReceiver, packageName, message, intent);
    }

    private void sendIntent(IntentSender statusReceiver, String packageName, String message,
            Intent intent) {
        try {
        try {
            final BroadcastOptions options = BroadcastOptions.makeBasic();
            final BroadcastOptions options = BroadcastOptions.makeBasic();
            options.setPendingIntentBackgroundActivityStartMode(
            options.setPendingIntentBackgroundActivityStartMode(
                    MODE_BACKGROUND_ACTIVITY_START_DENIED);
                    MODE_BACKGROUND_ACTIVITY_START_DENIED);
            statusReceiver.sendIntent(mContext, 0, fillIn, /* onFinished= */ null,
            statusReceiver.sendIntent(mContext, 0, intent, /* onFinished= */ null,
                    /* handler= */ null, /* requiredPermission= */ null, options.toBundle());
                    /* handler= */ null, /* requiredPermission= */ null, options.toBundle());
        } catch (IntentSender.SendIntentException e) {
        } catch (IntentSender.SendIntentException e) {
            Slog.e(
            Slog.e(
                    TAG,
                    TAG,
                    TextUtils.formatSimple("Failed to send failure status for %s with message %s",
                    TextUtils.formatSimple("Failed to send status for %s with message %s",
                            packageName, message),
                            packageName, message),
                    e);
                    e);
        }
        }
Loading