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

Commit 12e2998c authored by Rubin Xu's avatar Rubin Xu
Browse files

Migrate setPackagesSuspended

The underlying package suspension mechanism (PackageManager
.setPackagesSuspendedByAdmin) changes package suspension
state on a per-package basis so we can't simply "push" the
current list of suspended package to PackageManager from
PolicyEnforcerCallback. DPM.setPackagesSuspended() also
returns a list of failed packages which is complex to calculate
under coexistence. As a result, we simply use PolicyEngine
to store the package suspension state, and keep the enforcing
logic in DPMS.

Test: FrameworksServicesTests:SuspendPackagesTest
      android.suspendapps.cts.SuspendPackagesTest
      android.devicepolicy.cts.PackagesTest
      android.devicepolicy.cts.PackageSuspensionTest
      android.devicepolicy.cts.ApplicationExemptionsTest
      MixedDeviceOwnerTest#testSuspendPackage
      MixedManagedProfileOwnerTest#testSuspendPackage
      MixedDeviceOwnerTest#testSuspendPackageWithPackageManager
      MixedManagedProfileOwnerTest#testSuspendPackageWithPackageManager
Bug: 335624297
Change-Id: I9c287d39593d53d69e16ea2ec520df4d12ebcdaa
parent 101b2f5e
Loading
Loading
Loading
Loading
+16 −7
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.server.devicepolicy;

import static android.app.admin.DevicePolicyIdentifiers.PACKAGES_SUSPENDED_POLICY;
import static android.app.admin.DevicePolicyIdentifiers.USER_CONTROL_DISABLED_PACKAGES_POLICY;
import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_TARGET_USER_ID;
import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_UPDATE_RESULT_KEY;
@@ -264,9 +265,7 @@ final class DevicePolicyEngine {
                boolean policyEnforced = Objects.equals(
                        localPolicyState.getCurrentResolvedPolicy(), value);
                // TODO(b/285532044): remove hack and handle properly
                if (!policyEnforced
                        && policyDefinition.getPolicyKey().getIdentifier().equals(
                        USER_CONTROL_DISABLED_PACKAGES_POLICY)) {
                if (!policyEnforced && shouldApplyPackageSetUnionPolicyHack(policyDefinition)) {
                    PolicyValue<Set<String>> parsedValue = (PolicyValue<Set<String>>) value;
                    PolicyValue<Set<String>> parsedResolvedValue =
                            (PolicyValue<Set<String>>) localPolicyState.getCurrentResolvedPolicy();
@@ -532,8 +531,7 @@ final class DevicePolicyEngine {
                        globalPolicyState.getCurrentResolvedPolicy(), value);
                // TODO(b/285532044): remove hack and handle properly
                if (!policyAppliedGlobally
                        && policyDefinition.getPolicyKey().getIdentifier().equals(
                        USER_CONTROL_DISABLED_PACKAGES_POLICY)) {
                        && shouldApplyPackageSetUnionPolicyHack(policyDefinition)) {
                    PolicyValue<Set<String>> parsedValue = (PolicyValue<Set<String>>) value;
                    PolicyValue<Set<String>> parsedResolvedValue =
                            (PolicyValue<Set<String>>) globalPolicyState.getCurrentResolvedPolicy();
@@ -670,8 +668,7 @@ final class DevicePolicyEngine {

            }
            // TODO(b/285532044): remove hack and handle properly
            if (policyDefinition.getPolicyKey().getIdentifier().equals(
                    USER_CONTROL_DISABLED_PACKAGES_POLICY)) {
            if (shouldApplyPackageSetUnionPolicyHack(policyDefinition)) {
                if (!Objects.equals(value, localPolicyState.getCurrentResolvedPolicy())) {
                    PolicyValue<Set<String>> parsedValue = (PolicyValue<Set<String>>) value;
                    PolicyValue<Set<String>> parsedResolvedValue =
@@ -1870,6 +1867,18 @@ final class DevicePolicyEngine {
        return false;
    }

    /**
     * For PackageSetUnion policies, we can't simply compare the resolved policy against the admin's
     * policy for equality to determine if the admin has applied the policy successfully, instead
     * the admin's policy should be considered applied successfully as long as its policy is subset
     * of the resolved policy. This method controls which policies should use this special logic.
     */
    private <V> boolean shouldApplyPackageSetUnionPolicyHack(PolicyDefinition<V> policy) {
        String policyKey =  policy.getPolicyKey().getIdentifier();
        return policyKey.equals(USER_CONTROL_DISABLED_PACKAGES_POLICY)
                || policyKey.equals(PACKAGES_SUSPENDED_POLICY);
    }

    private class DevicePoliciesReaderWriter {
        private static final String DEVICE_POLICIES_XML = "device_policy_state.xml";
        private static final String BACKUP_DIRECTORY = "device_policy_backups";
+141 −41
Original line number Diff line number Diff line
@@ -2222,32 +2222,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
        return packageNameAndSignature;
    }
    private void unsuspendAppsForQuietProfiles() {
        PackageManagerInternal pmi = mInjector.getPackageManagerInternal();
        List<UserInfo> users = mUserManagerInternal.getUsers(true /* excludeDying */);
        for (UserInfo user : users) {
            if (!user.isManagedProfile() || !user.isQuietModeEnabled()) {
                continue;
            }
            int userId = user.id;
            var suspendedByAdmin = getPackagesSuspendedByAdmin(userId);
            var packagesToUnsuspend = mInjector.getPackageManager(userId)
                    .getInstalledPackages(PackageManager.PackageInfoFlags.of(
                            MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE))
                    .stream()
                    .map(packageInfo -> packageInfo.packageName)
                    .filter(pkg -> !suspendedByAdmin.contains(pkg))
                    .toArray(String[]::new);
            Slogf.i(LOG_TAG, "Unsuspending work apps for user %d", userId);
            // When app suspension was used for quiet mode, the apps were suspended by platform
            // package, just like when admin suspends them. So although it wasn't admin who
            // suspended, this method will remove the right suspension record.
            pmi.setPackagesSuspendedByAdmin(userId, packagesToUnsuspend, false /* suspended */);
        }
    }
    private Owners makeOwners(Injector injector, PolicyPathProvider pathProvider) {
        return new Owners(
                injector.getUserManager(), injector.getUserManagerInternal(),
@@ -3511,6 +3485,39 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
        return true;
    }
    @GuardedBy("getLockObject()")
    private boolean maybeMigrateSuspendedPackagesLocked(String backupId) {
        Slog.i(LOG_TAG, "Migrating suspended packages to policy engine");
        if (!Flags.unmanagedModeMigration()) {
            return false;
        }
        if (mOwners.isSuspendedPackagesMigrated()) {
            return false;
        }
        // Create backup if none exists
        mDevicePolicyEngine.createBackup(backupId);
        try {
            iterateThroughDpcAdminsLocked((admin, enforcingAdmin) -> {
                if (admin.suspendedPackages == null || admin.suspendedPackages.size() == 0) {
                    return;
                }
                int userId = enforcingAdmin.getUserId();
                mDevicePolicyEngine.setLocalPolicy(
                        PolicyDefinition.PACKAGES_SUSPENDED,
                        enforcingAdmin,
                        new PackageSetPolicyValue(new ArraySet<>(admin.suspendedPackages)),
                        userId);
            });
        } catch (Exception e) {
            Slog.wtf(LOG_TAG, "Failed to migrate suspended packages to policy engine", e);
        }
        Slog.i(LOG_TAG, "Marking suspended packages migration complete");
        mOwners.markSuspendedPackagesMigrated();
        return true;
    }
    private void applyManagedSubscriptionsPolicyIfRequired() {
        int copeProfileUserId = getOrganizationOwnedProfileUserId();
        // This policy is relevant only for COPE devices.
@@ -13211,8 +13218,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
        return result;
    }
    @Override
    public String[] setPackagesSuspended(ComponentName who, String callerPackage,
    private String[] setPackagesSuspendedPreCoexistence(ComponentName who, String callerPackage,
            String[] packageNames, boolean suspended) {
        final CallerIdentity caller = getCallerIdentity(who, callerPackage);
        ActiveAdmin admin;
@@ -13293,6 +13299,78 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
        return result;
    }
    @Override
    public String[] setPackagesSuspended(ComponentName who, String callerPackage,
            String[] packageNames, boolean suspended) {
        if (!Flags.unmanagedModeMigration()) {
            return setPackagesSuspendedPreCoexistence(who, callerPackage, packageNames, suspended);
        }
        final CallerIdentity caller = getCallerIdentity(who, callerPackage);
        EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin(
                who,
                MANAGE_DEVICE_POLICY_PACKAGE_STATE,
                caller.getPackageName(),
                caller.getUserId());
        checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_SET_PACKAGES_SUSPENDED);
        Set<String> packages = new ArraySet<>(packageNames);
        Set<String> suspendedPackagesBefore = mDevicePolicyEngine.getResolvedPolicy(
                PolicyDefinition.PACKAGES_SUSPENDED, caller.getUserId());
        Set<String> currentPackages = mDevicePolicyEngine.getLocalPolicySetByAdmin(
                PolicyDefinition.PACKAGES_SUSPENDED,
                enforcingAdmin,
                caller.getUserId());
        if (currentPackages == null) currentPackages = new ArraySet<>();
        if (suspended) {
            currentPackages.addAll(packages);
        } else {
            currentPackages.removeAll(packages);
        }
        if (currentPackages.isEmpty()) {
            mDevicePolicyEngine.removeLocalPolicy(
                    PolicyDefinition.PACKAGES_SUSPENDED,
                    enforcingAdmin,
                    caller.getUserId());
        } else {
            mDevicePolicyEngine.setLocalPolicy(
                    PolicyDefinition.PACKAGES_SUSPENDED,
                    enforcingAdmin,
                    new PackageSetPolicyValue(currentPackages),
                    caller.getUserId());
        }
        Set<String> suspendedPackagesAfter = mDevicePolicyEngine.getResolvedPolicy(
                PolicyDefinition.PACKAGES_SUSPENDED, caller.getUserId());
        PackageSuspender suspender = new PackageSuspender(
                suspendedPackagesBefore, suspendedPackagesAfter,
                listPolicyExemptAppsUnchecked(mContext),
                mInjector.getPackageManagerInternal(), caller.getUserId());
        String[] result;
        synchronized (getLockObject()) {
            long id = mInjector.binderClearCallingIdentity();
            try {
                result = suspended ? suspender.suspend(packages) : suspender.unsuspend(packages);
            } finally {
                mInjector.binderRestoreCallingIdentity(id);
            }
        }
        DevicePolicyEventLogger
                .createEvent(DevicePolicyEnums.SET_PACKAGES_SUSPENDED)
                .setAdmin(caller.getPackageName())
                .setBoolean(/* isDelegate */ who == null)
                .setStrings(packageNames)
                .write();
        if (VERBOSE_LOG) Slogf.v(LOG_TAG, "Returning %s", Arrays.toString(result));
        return result;
    }
    /**
     * Returns an array containing the union of the given non-suspended packages and
     * exempt apps. Assumes both parameters are non-null and non-empty.
@@ -13314,7 +13392,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
    public boolean isPackageSuspended(ComponentName who, String callerPackage, String packageName) {
        final CallerIdentity caller = getCallerIdentity(who, callerPackage);
        if (isUnicornFlagEnabled()) {
        if (Flags.unmanagedModeMigration()) {
            enforcePermission(
                    MANAGE_DEVICE_POLICY_PACKAGE_STATE,
                    caller.getPackageName(),
@@ -15485,17 +15563,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
        }
    }
    private Set<String> getPackagesSuspendedByAdmin(@UserIdInt int userId) {
        synchronized (getLockObject()) {
            ActiveAdmin admin = getDeviceOrProfileOwnerAdminLocked(userId);
            if (admin == null || admin.suspendedPackages == null) {
                return Collections.emptySet();
            } else {
                return new ArraySet<>(admin.suspendedPackages);
            }
        }
    }
    /**
     * We need to update the internal state of whether a user has completed setup or a
     * device has paired once. After that, we ignore any changes that reset the
@@ -23751,7 +23818,39 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
        }
        Slog.w(LOG_TAG, "Work apps may have been paused via suspension previously.");
        unsuspendAppsForQuietProfiles();
        PackageManagerInternal pmi = mInjector.getPackageManagerInternal();
        List<UserInfo> users = mUserManagerInternal.getUsers(true /* excludeDying */);
        for (UserInfo user : users) {
            if (!user.isManagedProfile() || !user.isQuietModeEnabled()) {
                continue;
            }
            int userId = user.id;
            Set<String> suspendedByAdmin;
            synchronized (getLockObject()) {
                ActiveAdmin admin = getDeviceOrProfileOwnerAdminLocked(userId);
                // This is legacy code from Turn off Work 2.0 which is before setPackagesSuspended
                // is migrated to PolicyEngine, so we only need to query the legacy ActiveAdmin here
                if (admin == null || admin.suspendedPackages == null) {
                    suspendedByAdmin = Collections.emptySet();
                } else {
                    suspendedByAdmin = new ArraySet<>(admin.suspendedPackages);
                }
            }
            var packagesToUnsuspend = mInjector.getPackageManager(userId)
                    .getInstalledPackages(PackageManager.PackageInfoFlags.of(
                            MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE))
                    .stream()
                    .map(packageInfo -> packageInfo.packageName)
                    .filter(pkg -> !suspendedByAdmin.contains(pkg))
                    .toArray(String[]::new);
            Slogf.i(LOG_TAG, "Unsuspending work apps for user %d", userId);
            // When app suspension was used for quiet mode, the apps were suspended by platform
            // package, just like when admin suspends them. So although it wasn't admin who
            // suspended, this method will remove the right suspension record.
            pmi.setPackagesSuspendedByAdmin(userId, packagesToUnsuspend, false /* suspended */);
        }
    }
    public void setMtePolicy(int flags, String callerPackageName) {
@@ -24179,6 +24278,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
        String unmanagedBackupId = "35.1.unmanaged-mode";
        boolean migrated = false;
        migrated = migrated | maybeMigrateRequiredPasswordComplexityLocked(unmanagedBackupId);
        migrated = migrated | maybeMigrateSuspendedPackagesLocked(unmanagedBackupId);
        if (migrated) {
            Slogf.i(LOG_TAG, "Backup made: " + unmanagedBackupId);
        }
+13 −0
Original line number Diff line number Diff line
@@ -650,6 +650,19 @@ class Owners {

    }

    boolean isSuspendedPackagesMigrated() {
        synchronized (mData) {
            return mData.mSuspendedPackagesMigrated;
        }
    }

    void markSuspendedPackagesMigrated() {
        synchronized (mData) {
            mData.mSuspendedPackagesMigrated = true;
            mData.writeDeviceOwner();
        }
    }

    boolean isMigratedPostUpdate() {
        synchronized (mData) {
            return mData.mPoliciesMigratedPostUpdate;
+9 −2
Original line number Diff line number Diff line
@@ -90,6 +90,7 @@ class OwnersData {
    private static final String ATTR_SECURITY_LOG_MIGRATED = "securityLogMigrated";
    private static final String ATTR_REQUIRED_PASSWORD_COMPLEXITY_MIGRATED =
            "passwordComplexityMigrated";
    private static final String ATTR_SUSPENDED_PACKAGES_MIGRATED = "suspendedPackagesMigrated";
    private static final String ATTR_MIGRATED_POST_UPGRADE = "migratedPostUpgrade";

    // Internal state for the device owner package.
@@ -120,6 +121,7 @@ class OwnersData {
    boolean mMigratedToPolicyEngine = false;
    boolean mSecurityLoggingMigrated = false;
    boolean mRequiredPasswordComplexityMigrated = false;
    boolean mSuspendedPackagesMigrated = false;

    boolean mPoliciesMigratedPostUpdate = false;

@@ -414,6 +416,9 @@ class OwnersData {
            if (Flags.unmanagedModeMigration()) {
                out.attributeBoolean(null, ATTR_REQUIRED_PASSWORD_COMPLEXITY_MIGRATED,
                        mRequiredPasswordComplexityMigrated);
                out.attributeBoolean(null, ATTR_SUSPENDED_PACKAGES_MIGRATED,
                        mSuspendedPackagesMigrated);

            }

            out.endTag(null, TAG_POLICY_ENGINE_MIGRATION);
@@ -480,10 +485,12 @@ class OwnersData {
                            null, ATTR_MIGRATED_POST_UPGRADE, false);
                    mSecurityLoggingMigrated = Flags.securityLogV2Enabled()
                            && parser.getAttributeBoolean(null, ATTR_SECURITY_LOG_MIGRATED, false);
                    mRequiredPasswordComplexityMigrated =
                            Flags.unmanagedModeMigration()
                    mRequiredPasswordComplexityMigrated = Flags.unmanagedModeMigration()
                            && parser.getAttributeBoolean(null,
                                    ATTR_REQUIRED_PASSWORD_COMPLEXITY_MIGRATED, false);
                    mSuspendedPackagesMigrated = Flags.unmanagedModeMigration()
                            && parser.getAttributeBoolean(null,
                                    ATTR_SUSPENDED_PACKAGES_MIGRATED, false);

                    break;
                default:
+155 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.server.devicepolicy;

import static com.android.server.devicepolicy.DevicePolicyManagerService.LOG_TAG;

import android.annotation.Nullable;
import android.content.pm.PackageManagerInternal;
import android.util.ArraySet;

import com.android.server.utils.Slogf;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;

/**
 * Helper class for calling into PackageManagerInternal.setPackagesSuspendedByAdmin.
 * Two main things this class encapsulates:
 *    1. Handling of the DPM internal suspension exemption list
 *    2. Calculating the failed packages result in the context of coexistence
 *
 * 1 is handled by the two internal methods {@link #suspendWithExemption(Set)} and
 * {@link #unsuspendWithExemption(Set)} where the exemption list is taken into consideration
 * before and after calling {@link PackageManagerInternal#setPackagesSuspendedByAdmin}.
 * In order to compute 2, the resolved package suspension state before and after suspension is
 * needed as multiple admins can both suspend the same packages under coexistence.
 */
public class PackageSuspender {

    private final Set<String> mSuspendedPackageBefore;
    private final Set<String> mSuspendedPackageAfter;
    private final List<String> mExemptedPackages;
    private final PackageManagerInternal mPackageManager;
    private final int mUserId;

    public PackageSuspender(@Nullable Set<String> suspendedPackageBefore,
            @Nullable Set<String> suspendedPackageAfter, List<String> exemptedPackages,
            PackageManagerInternal pmi, int userId) {
        mSuspendedPackageBefore =
                suspendedPackageBefore != null ? suspendedPackageBefore : Collections.emptySet();
        mSuspendedPackageAfter =
                suspendedPackageAfter != null ? suspendedPackageAfter : Collections.emptySet();
        mExemptedPackages = exemptedPackages;
        mPackageManager = pmi;
        mUserId = userId;
    }

    /**
     * Suspend packages that are requested by a single admin
     *
     * @return a list of packages that the admin has requested to suspend but could not be
     * suspended, due to DPM and PackageManager exemption list.
     *
     */
    public String[] suspend(Set<String> packages) {
        // When suspending, call PM with the list of packages admin has requested, even if some
        // of these packages are already in suspension (some other admin might have already
        // suspended them). We do it this way so that we can simply return the failed list from
        // PackageManager to the caller as the accurate list of unsuspended packages.
        // This is different from the unsuspend() logic, please see below.
        //
        // For example admin A already suspended package 1, 2 and 3, but package 3 is
        // PackageManager-exempted. Now admin B wants to suspend package 2, 3 and 4 (2 and 4 are
        // suspendable). We need to return package 3 as the unsuspended package here, and we ask
        // PackageManager to suspend package 2, 3 and 4 here (who will return package 3 in the
        // failed list, and package 2 is already suspended).
        Set<String> result = suspendWithExemption(packages);
        return result.toArray(String[]::new);
    }

    /**
     * Suspend packages considering the exemption list.
     *
     * @return the list of packages that couldn't be suspended, either due to the exemption list,
     * or due to failures from PackageManagerInternal itself.
     */
    private Set<String> suspendWithExemption(Set<String> packages) {
        Set<String> packagesToSuspend = new ArraySet<>(packages);
        // Any original packages that are also in the exempted list will not be suspended and hence
        // will appear in the final result.
        Set<String> result = new ArraySet<>(mExemptedPackages);
        result.retainAll(packagesToSuspend);
        // Remove exempted packages before calling PackageManager
        packagesToSuspend.removeAll(mExemptedPackages);
        String[] failedPackages = mPackageManager.setPackagesSuspendedByAdmin(
                mUserId, packagesToSuspend.toArray(String[]::new), true);
        if (failedPackages == null) {
            Slogf.w(LOG_TAG, "PM failed to suspend packages (%s)", packages);
            return packages;
        } else {
            result.addAll(Arrays.asList(failedPackages));
            return result;
        }
    }

    /**
     * Unsuspend packages that are requested by a single admin
     *
     * @return a list of packages that the admin has requested to unsuspend but could not be
     * unsuspended, due to other amdin's policy or PackageManager restriction.
     *
     */
    public String[] unsuspend(Set<String> packages) {
        // Unlike suspend(), when unsuspending, call PackageManager with the delta of resolved
        // suspended packages list and not what the admin has requested. This is because some
        // packages might still be subject to another admin's suspension request.
        Set<String> packagesToUnsuspend = new ArraySet<>(mSuspendedPackageBefore);
        packagesToUnsuspend.removeAll(mSuspendedPackageAfter);

        // To calculate the result (which packages are not unsuspended), start with packages that
        // are still subject to another admin's suspension policy. This is calculated by
        // intersecting the packages argument with mSuspendedPackageAfter.
        Set<String> result = new ArraySet<>(packages);
        result.retainAll(mSuspendedPackageAfter);
        // Remove mExemptedPackages since they can't be suspended to start with.
        result.removeAll(mExemptedPackages);
        // Finally make the unsuspend() request and add packages that PackageManager can't unsuspend
        // to the result.
        result.addAll(unsuspendWithExemption(packagesToUnsuspend));
        return result.toArray(String[]::new);
    }

    /**
     * Unsuspend packages considering the exemption list.
     *
     * @return the list of packages that couldn't be unsuspended, either due to the exemption list,
     * or due to failures from PackageManagerInternal itself.
     */
    private Set<String> unsuspendWithExemption(Set<String> packages) {
        // when unsuspending, no need to consider exemption list since by definition they can't
        // be suspended to begin with.
        String[] failedPackages = mPackageManager.setPackagesSuspendedByAdmin(
                mUserId, packages.toArray(String[]::new), false);
        if (failedPackages == null) {
            Slogf.w(LOG_TAG, "PM failed to unsuspend packages (%s)", packages);
        }
        return new ArraySet<>(failedPackages);
    }
}
Loading