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

Commit 6357035a authored by Jakob Schneider's avatar Jakob Schneider
Browse files

Store the standard icon during the archive call.

Test: PackageArchiverServiceTest, CTS test will follow once we have a
public API to return the icon.
Bug: 290776948

Change-Id: I34ff05c3c1f610975561d82dff82b7f129e08f97
parent 6a6eeaad
Loading
Loading
Loading
Loading
+154 −13
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.server.pm;

import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
import static android.content.pm.PackageManager.DELETE_KEEP_DATA;
import static android.os.PowerExemptionManager.REASON_PACKAGE_UNARCHIVE;
import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
@@ -24,6 +25,7 @@ import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.UserIdInt;
import android.app.AppOpsManager;
import android.app.BroadcastOptions;
import android.content.Context;
@@ -33,24 +35,37 @@ import android.content.pm.IPackageArchiverService;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherApps;
import android.content.pm.PackageArchiver;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.VersionedPackage;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Binder;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelableException;
import android.os.SELinux;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.pm.pkg.ArchiveState;
import com.android.server.pm.pkg.ArchiveState.ArchiveActivityInfo;
import com.android.server.pm.pkg.PackageStateInternal;
import com.android.server.pm.pkg.PackageUserStateInternal;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;

/**
 * Responsible archiving apps and returning information about archived apps.
@@ -61,6 +76,8 @@ import java.util.Objects;
 */
public class PackageArchiverService extends IPackageArchiverService.Stub {

    private static final String TAG = "PackageArchiverService";

    /**
     * The maximum time granted for an app store to start a foreground service when unarchival
     * is requested.
@@ -68,6 +85,8 @@ public class PackageArchiverService extends IPackageArchiverService.Stub {
    // TODO(b/297358628) Make this configurable through a flag.
    private static final int DEFAULT_UNARCHIVE_FOREGROUND_TIMEOUT_MS = 120 * 1000;

    private static final String ARCHIVE_ICONS_DIR = "package_archiver";

    private final Context mContext;
    private final PackageManagerService mPm;

@@ -97,25 +116,44 @@ public class PackageArchiverService extends IPackageArchiverService.Stub {
        snapshot.enforceCrossUserPermission(binderUid, userId, true, true,
                "archiveApp");
        verifyCaller(providedUid, binderUid);
        ArchiveState archiveState;
        CompletableFuture<ArchiveState> archiveStateFuture;
        try {
            archiveState = createArchiveState(packageName, userId);
            archiveStateFuture = createArchiveState(packageName, userId);
        } catch (PackageManager.NameNotFoundException e) {
            Slog.d(TAG, TextUtils.formatSimple("Failed to archive %s with message %s",
                    packageName, e.getMessage()));
            throw new ParcelableException(e);
        }

        archiveStateFuture
                .thenAccept(
                        archiveState -> {
                            // TODO(b/282952870) Should be reverted if uninstall fails/cancels
                            try {
                                storeArchiveState(packageName, archiveState, userId);
                            } catch (PackageManager.NameNotFoundException e) {
            throw new ParcelableException(e);
                                sendFailureStatus(intentSender, packageName, e.getMessage());
                                return;
                            }

                            // TODO(b/278553670) Add special strings for the delete dialog
                            mPm.mInstallerService.uninstall(
                new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST),
                callerPackageName, DELETE_KEEP_DATA, intentSender, userId);
                                    new VersionedPackage(packageName,
                                            PackageManager.VERSION_CODE_HIGHEST),
                                    callerPackageName, DELETE_KEEP_DATA, intentSender, userId,
                                    binderUid);
                        })
                .exceptionally(
                        e -> {
                            sendFailureStatus(intentSender, packageName, e.getMessage());
                            return null;
                        });
    }

    /**
     * Creates archived state for the package and user.
     */
    public ArchiveState createArchiveState(String packageName, int userId)
    public CompletableFuture<ArchiveState> createArchiveState(String packageName, int userId)
            throws PackageManager.NameNotFoundException {
        PackageStateInternal ps = getPackageState(packageName, mPm.snapshotComputer(),
                Binder.getCallingUid(), userId);
@@ -123,16 +161,56 @@ public class PackageArchiverService extends IPackageArchiverService.Stub {
        verifyInstaller(responsibleInstallerPackage);

        List<LauncherActivityInfo> mainActivities = getLauncherActivityInfos(ps, userId);
        final CompletableFuture<ArchiveState> archiveState = new CompletableFuture<>();
        mPm.mHandler.post(() -> {
            try {
                archiveState.complete(
                        createArchiveStateInternal(packageName, userId, mainActivities,
                                responsibleInstallerPackage));
            } catch (IOException e) {
                archiveState.completeExceptionally(e);
            }
        });
        return archiveState;
    }

    private ArchiveState createArchiveStateInternal(String packageName, int userId,
            List<LauncherActivityInfo> mainActivities, String installerPackage)
            throws IOException {
        List<ArchiveActivityInfo> archiveActivityInfos = new ArrayList<>();
        for (int i = 0; i < mainActivities.size(); i++) {
            // TODO(b/278553670) Extract and store launcher icons
            LauncherActivityInfo mainActivity = mainActivities.get(i);
            Path iconPath = storeIcon(packageName, mainActivity, userId);
            ArchiveActivityInfo activityInfo = new ArchiveActivityInfo(
                    mainActivities.get(i).getLabel().toString(),
                    Path.of("/TODO"), null);
                    mainActivity.getLabel().toString(), iconPath, null);
            archiveActivityInfos.add(activityInfo);
        }

        return new ArchiveState(archiveActivityInfos, responsibleInstallerPackage);
        return new ArchiveState(archiveActivityInfos, installerPackage);
    }

    // TODO(b/298452477) Handle monochrome icons.
    @VisibleForTesting
    Path storeIcon(String packageName, LauncherActivityInfo mainActivity,
            @UserIdInt int userId)
            throws IOException {
        int iconResourceId = mainActivity.getActivityInfo().getIconResource();
        if (iconResourceId == 0) {
            // The app doesn't define an icon. No need to store anything.
            return null;
        }
        File iconsDir = createIconsDir(userId);
        File iconFile = new File(iconsDir, packageName + "-" + mainActivity.getName() + ".png");
        Bitmap icon = drawableToBitmap(mainActivity.getIcon(/* density= */ 0));
        try (FileOutputStream out = new FileOutputStream(iconFile)) {
            // Note: Quality is ignored for PNGs.
            if (!icon.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, out)) {
                throw new IOException(TextUtils.formatSimple("Failure to store icon file %s",
                        iconFile.getName()));
            }
            out.flush();
        }
        return iconFile.toPath();
    }

    private void verifyInstaller(String installerPackage)
@@ -313,6 +391,29 @@ public class PackageArchiverService extends IPackageArchiverService.Stub {
        return ps;
    }

    private void sendFailureStatus(IntentSender statusReceiver, String packageName,
            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);
        try {
            final BroadcastOptions options = BroadcastOptions.makeBasic();
            options.setPendingIntentBackgroundActivityStartMode(
                    MODE_BACKGROUND_ACTIVITY_START_DENIED);
            statusReceiver.sendIntent(mContext, 0, fillIn, /* 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",
                            packageName, message),
                    e);
        }
    }

    private static void verifyCaller(int providedUid, int binderUid) {
        if (providedUid != binderUid) {
            throw new SecurityException(
@@ -323,4 +424,44 @@ public class PackageArchiverService extends IPackageArchiverService.Stub {
                            binderUid));
        }
    }

    private File createIconsDir(@UserIdInt int userId) throws IOException {
        File iconsDir = getIconsDir(userId);
        if (!iconsDir.isDirectory()) {
            iconsDir.delete();
            iconsDir.mkdirs();
            if (!iconsDir.isDirectory()) {
                throw new IOException("Unable to create directory " + iconsDir);
            }
        }
        SELinux.restorecon(iconsDir);
        return iconsDir;
    }

    private File getIconsDir(int userId) {
        return new File(Environment.getDataSystemCeDirectory(userId), ARCHIVE_ICONS_DIR);
    }

    private static Bitmap drawableToBitmap(Drawable drawable) {
        if (drawable instanceof BitmapDrawable) {
            return ((BitmapDrawable) drawable).getBitmap();

        }

        Bitmap bitmap;
        if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
            // Needed for drawables that are just a single color.
            bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
        } else {
            bitmap =
                    Bitmap.createBitmap(
                            drawable.getIntrinsicWidth(),
                            drawable.getIntrinsicHeight(),
                            Bitmap.Config.ARGB_8888);
        }
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);
        return bitmap;
    }
}
+12 −2
Original line number Diff line number Diff line
@@ -1240,8 +1240,18 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements
    @Override
    public void uninstall(VersionedPackage versionedPackage, String callerPackageName, int flags,
                IntentSender statusReceiver, int userId) {
        uninstall(
                versionedPackage,
                callerPackageName,
                flags,
                statusReceiver,
                userId,
                Binder.getCallingUid());
    }

    void uninstall(VersionedPackage versionedPackage, String callerPackageName, int flags,
            IntentSender statusReceiver, int userId, int callingUid) {
        final Computer snapshot = mPm.snapshotComputer();
        final int callingUid = Binder.getCallingUid();
        snapshot.enforceCrossUserPermission(callingUid, userId, true, true, "uninstall");
        if (!PackageManagerServiceUtils.isRootOrShell(callingUid)) {
            mAppOps.checkPackage(callingUid, callerPackageName);
@@ -1257,7 +1267,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements
        final PackageDeleteObserverAdapter adapter = new PackageDeleteObserverAdapter(mContext,
                statusReceiver, versionedPackage.getPackageName(),
                canSilentlyInstallPackage, userId);
        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DELETE_PACKAGES)
        if (mContext.checkCallingOrSelfPermission(Manifest.permission.DELETE_PACKAGES)
                    == PackageManager.PERMISSION_GRANTED) {
            // Sweet, call straight through!
            mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags);
+10 −12
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ import android.content.pm.overlay.OverlayPaths;
import android.os.UserHandle;
import android.os.incremental.IncrementalManager;
import android.service.pm.PackageProto;
import android.service.pm.PackageProto.UserInfoProto.ArchiveState.ArchiveActivityInfo;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -1161,18 +1162,15 @@ public class PackageSetting extends SettingBase implements PackageStateInternal
        for (ArchiveState.ArchiveActivityInfo activityInfo : archiveState.getActivityInfos()) {
            long activityInfoToken = proto.start(
                    PackageProto.UserInfoProto.ArchiveState.ACTIVITY_INFOS);
            proto.write(PackageProto.UserInfoProto.ArchiveState.ArchiveActivityInfo.TITLE,
                    activityInfo.getTitle());
            proto.write(
                    PackageProto.UserInfoProto.ArchiveState.ArchiveActivityInfo.ICON_BITMAP_PATH,
            proto.write(ArchiveActivityInfo.TITLE, activityInfo.getTitle());
            if (activityInfo.getIconBitmap() != null) {
                proto.write(ArchiveActivityInfo.ICON_BITMAP_PATH,
                        activityInfo.getIconBitmap().toAbsolutePath().toString());
            proto.write(
                    PackageProto
                            .UserInfoProto
                            .ArchiveState
                            .ArchiveActivityInfo
                            .MONOCHROME_ICON_BITMAP_PATH,
            }
            if (activityInfo.getMonochromeIconBitmap() != null) {
                proto.write(ArchiveActivityInfo.MONOCHROME_ICON_BITMAP_PATH,
                        activityInfo.getMonochromeIconBitmap().toAbsolutePath().toString());
            }
            proto.end(activityInfoToken);
        }

+7 −4
Original line number Diff line number Diff line
@@ -2062,8 +2062,9 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile
            if (tagName.equals(TAG_ARCHIVE_ACTIVITY_INFO)) {
                String title = parser.getAttributeValue(null,
                        ATTR_ARCHIVE_ACTIVITY_TITLE);
                Path iconPath = Path.of(parser.getAttributeValue(null,
                        ATTR_ARCHIVE_ICON_PATH));
                String iconAttribute = parser.getAttributeValue(null,
                        ATTR_ARCHIVE_ICON_PATH);
                Path iconPath = iconAttribute == null ? null : Path.of(iconAttribute);
                String monochromeAttribute = parser.getAttributeValue(null,
                        ATTR_ARCHIVE_MONOCHROME_ICON_PATH);
                Path monochromeIconPath = monochromeAttribute == null ? null : Path.of(
@@ -2447,8 +2448,10 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile
        for (ArchiveState.ArchiveActivityInfo activityInfo : archiveState.getActivityInfos()) {
            serializer.startTag(null, TAG_ARCHIVE_ACTIVITY_INFO);
            serializer.attribute(null, ATTR_ARCHIVE_ACTIVITY_TITLE, activityInfo.getTitle());
            if (activityInfo.getIconBitmap() != null) {
                serializer.attribute(null, ATTR_ARCHIVE_ICON_PATH,
                        activityInfo.getIconBitmap().toAbsolutePath().toString());
            }
            if (activityInfo.getMonochromeIconBitmap() != null) {
                serializer.attribute(null, ATTR_ARCHIVE_MONOCHROME_ICON_PATH,
                        activityInfo.getMonochromeIconBitmap().toAbsolutePath().toString());
+14 −11
Original line number Diff line number Diff line
@@ -56,8 +56,11 @@ public class ArchiveState {
        @NonNull
        private final String mTitle;

        /** The path to the stored icon of the activity in the app's locale. */
        @NonNull
        /**
         * The path to the stored icon of the activity in the app's locale. Null if the app does
         * not define any icon (default icon would be shown on the launcher).
         */
        @Nullable
        private final Path mIconBitmap;

        /** See {@link #mIconBitmap}. Only set if the app defined a monochrome icon. */
@@ -85,21 +88,20 @@ public class ArchiveState {
         * @param title
         *   Corresponds to the activity's android:label in the app's locale.
         * @param iconBitmap
         *   The path to the stored icon of the activity in the app's locale.
         *   The path to the stored icon of the activity in the app's locale. Null if the app does
         *   not define any icon (default icon would be shown on the launcher).
         * @param monochromeIconBitmap
         *   See {@link #mIconBitmap}. Only set if the app defined a monochrome icon.
         */
        @DataClass.Generated.Member
        public ArchiveActivityInfo(
                @NonNull String title,
                @NonNull Path iconBitmap,
                @Nullable Path iconBitmap,
                @Nullable Path monochromeIconBitmap) {
            this.mTitle = title;
            com.android.internal.util.AnnotationValidations.validate(
                    NonNull.class, null, mTitle);
            this.mIconBitmap = iconBitmap;
            com.android.internal.util.AnnotationValidations.validate(
                    NonNull.class, null, mIconBitmap);
            this.mMonochromeIconBitmap = monochromeIconBitmap;

            // onConstructed(); // You can define this method to get a callback
@@ -114,10 +116,11 @@ public class ArchiveState {
        }

        /**
         * The path to the stored icon of the activity in the app's locale.
         * The path to the stored icon of the activity in the app's locale. Null if the app does
         * not define any icon (default icon would be shown on the launcher).
         */
        @DataClass.Generated.Member
        public @NonNull Path getIconBitmap() {
        public @Nullable Path getIconBitmap() {
            return mIconBitmap;
        }

@@ -174,10 +177,10 @@ public class ArchiveState {
        }

        @DataClass.Generated(
                time = 1689169065133L,
                time = 1693590309015L,
                codegenVersion = "1.0.23",
                sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/ArchiveState.java",
                inputSignatures = "private final @android.annotation.NonNull java.lang.String mTitle\nprivate final @android.annotation.NonNull java.nio.file.Path mIconBitmap\nprivate final @android.annotation.Nullable java.nio.file.Path mMonochromeIconBitmap\nclass ArchiveActivityInfo extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true)")
                inputSignatures = "private final @android.annotation.NonNull java.lang.String mTitle\nprivate final @android.annotation.Nullable java.nio.file.Path mIconBitmap\nprivate final @android.annotation.Nullable java.nio.file.Path mMonochromeIconBitmap\nclass ArchiveActivityInfo extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true)")
        @Deprecated
        private void __metadata() {}

@@ -292,7 +295,7 @@ public class ArchiveState {
    }

    @DataClass.Generated(
            time = 1689169065144L,
            time = 1693590309027L,
            codegenVersion = "1.0.23",
            sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/ArchiveState.java",
            inputSignatures = "private final @android.annotation.NonNull java.util.List<com.android.server.pm.pkg.ArchiveActivityInfo> mActivityInfos\nprivate final @android.annotation.NonNull java.lang.String mInstallerTitle\nclass ArchiveState extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true)")
Loading