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

Commit 83dd0af2 authored by Jakob Schneider's avatar Jakob Schneider
Browse files

Add confirmation dialog for unarchival if app only possesses weak

permissions.

Test: PackageInstallerArchiveTest
Bug: 305902395

Change-Id: I9f3bb5bf1ba6c0ed5164ac8be644287ee95251d9
parent cb62207e
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -181,6 +181,18 @@

        <receiver android:name="androidx.profileinstaller.ProfileInstallReceiver"
            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>

</manifest>
+10 −0
Original line number Diff line number Diff line
@@ -257,4 +257,14 @@
    <!-- Notification shown in status bar when an application is successfully installed.
         [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>

    <!-- 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>
+151 −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
 *
 *      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 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 Diff line number Diff line
@@ -95,6 +95,9 @@ public class PackageArchiver {

    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
     * is requested.
@@ -104,6 +107,8 @@ public class PackageArchiver {

    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 PackageManagerService mPm;

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

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

        // TODO(b/305902395) Introduce a confirmation dialog if the requestor only holds
        // REQUEST_INSTALL permission.
        boolean hasInstallPackages = mContext.checkCallingOrSelfPermission(
                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;
        try {
            draftSessionId = createDraftSession(packageName, installerPackage, statusReceiver,
                    userId);
            draftSessionId = Binder.withCleanCallingIdentity(() ->
                    createDraftSession(packageName, installerPackage, statusReceiver, userId));
        } catch (RuntimeException e) {
            if (e.getCause() instanceof IOException) {
                throw ExceptionUtils.wrap((IOException) e.getCause());
@@ -438,15 +464,17 @@ public class PackageArchiver {
                () -> unarchiveInternal(packageName, userHandle, installerPackage, draftSessionId));
    }

    private void verifyInstallPermissions() {
        if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES)
                != PackageManager.PERMISSION_GRANTED && mContext.checkCallingOrSelfPermission(
                Manifest.permission.REQUEST_INSTALL_PACKAGES)
                != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException("You need the com.android.permission.INSTALL_PACKAGES "
                    + "or com.android.permission.REQUEST_INSTALL_PACKAGES permission to request "
                    + "an unarchival.");
        }
    private void requestUnarchiveConfirmation(String packageName, IntentSender statusReceiver) {
        final Intent dialogIntent = new Intent(ACTION_UNARCHIVE_DIALOG);
        dialogIntent.putExtra(EXTRA_UNARCHIVE_INTENT_SENDER, statusReceiver);
        dialogIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName);

        final Intent broadcastIntent = new Intent();
        broadcastIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName);
        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() {
@@ -461,7 +489,7 @@ public class PackageArchiver {
    }

    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.MODE_FULL_INSTALL);
        sessionParams.setAppPackageName(packageName);
@@ -477,12 +505,11 @@ public class PackageArchiver {
            return existingSessionId;
        }

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

    private void sendIntent(IntentSender statusReceiver, String packageName, String message,
            Intent intent) {
        try {
            final BroadcastOptions options = BroadcastOptions.makeBasic();
            options.setPendingIntentBackgroundActivityStartMode(
                    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());
        } catch (IntentSender.SendIntentException e) {
            Slog.e(
                    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),
                    e);
        }
Loading