1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.devicepolicy;
18 
19 import static com.android.server.devicepolicy.DevicePolicyManagerService.LOG_TAG;
20 
21 import android.annotation.Nullable;
22 import android.content.pm.PackageManagerInternal;
23 import android.util.ArraySet;
24 
25 import com.android.server.utils.Slogf;
26 
27 import java.util.Arrays;
28 import java.util.Collections;
29 import java.util.List;
30 import java.util.Set;
31 
32 /**
33  * Helper class for calling into PackageManagerInternal.setPackagesSuspendedByAdmin.
34  * Two main things this class encapsulates:
35  *    1. Handling of the DPM internal suspension exemption list
36  *    2. Calculating the failed packages result in the context of coexistence
37  *
38  * 1 is handled by the two internal methods {@link #suspendWithExemption(Set)} and
39  * {@link #unsuspendWithExemption(Set)} where the exemption list is taken into consideration
40  * before and after calling {@link PackageManagerInternal#setPackagesSuspendedByAdmin}.
41  * In order to compute 2, the resolved package suspension state before and after suspension is
42  * needed as multiple admins can both suspend the same packages under coexistence.
43  */
44 public class PackageSuspender {
45 
46     private final Set<String> mSuspendedPackageBefore;
47     private final Set<String> mSuspendedPackageAfter;
48     private final List<String> mExemptedPackages;
49     private final PackageManagerInternal mPackageManager;
50     private final int mUserId;
51 
PackageSuspender(@ullable Set<String> suspendedPackageBefore, @Nullable Set<String> suspendedPackageAfter, List<String> exemptedPackages, PackageManagerInternal pmi, int userId)52     public PackageSuspender(@Nullable Set<String> suspendedPackageBefore,
53             @Nullable Set<String> suspendedPackageAfter, List<String> exemptedPackages,
54             PackageManagerInternal pmi, int userId) {
55         mSuspendedPackageBefore =
56                 suspendedPackageBefore != null ? suspendedPackageBefore : Collections.emptySet();
57         mSuspendedPackageAfter =
58                 suspendedPackageAfter != null ? suspendedPackageAfter : Collections.emptySet();
59         mExemptedPackages = exemptedPackages;
60         mPackageManager = pmi;
61         mUserId = userId;
62     }
63 
64     /**
65      * Suspend packages that are requested by a single admin
66      *
67      * @return a list of packages that the admin has requested to suspend but could not be
68      * suspended, due to DPM and PackageManager exemption list.
69      *
70      */
suspend(Set<String> packages)71     public String[] suspend(Set<String> packages) {
72         // When suspending, call PM with the list of packages admin has requested, even if some
73         // of these packages are already in suspension (some other admin might have already
74         // suspended them). We do it this way so that we can simply return the failed list from
75         // PackageManager to the caller as the accurate list of unsuspended packages.
76         // This is different from the unsuspend() logic, please see below.
77         //
78         // For example admin A already suspended package 1, 2 and 3, but package 3 is
79         // PackageManager-exempted. Now admin B wants to suspend package 2, 3 and 4 (2 and 4 are
80         // suspendable). We need to return package 3 as the unsuspended package here, and we ask
81         // PackageManager to suspend package 2, 3 and 4 here (who will return package 3 in the
82         // failed list, and package 2 is already suspended).
83         Set<String> result = suspendWithExemption(packages);
84         return result.toArray(String[]::new);
85     }
86 
87     /**
88      * Suspend packages considering the exemption list.
89      *
90      * @return the list of packages that couldn't be suspended, either due to the exemption list,
91      * or due to failures from PackageManagerInternal itself.
92      */
suspendWithExemption(Set<String> packages)93     private Set<String> suspendWithExemption(Set<String> packages) {
94         Set<String> packagesToSuspend = new ArraySet<>(packages);
95         // Any original packages that are also in the exempted list will not be suspended and hence
96         // will appear in the final result.
97         Set<String> result = new ArraySet<>(mExemptedPackages);
98         result.retainAll(packagesToSuspend);
99         // Remove exempted packages before calling PackageManager
100         packagesToSuspend.removeAll(mExemptedPackages);
101         String[] failedPackages = mPackageManager.setPackagesSuspendedByAdmin(
102                 mUserId, packagesToSuspend.toArray(String[]::new), true);
103         if (failedPackages == null) {
104             Slogf.w(LOG_TAG, "PM failed to suspend packages (%s)", packages);
105             return packages;
106         } else {
107             result.addAll(Arrays.asList(failedPackages));
108             return result;
109         }
110     }
111 
112     /**
113      * Unsuspend packages that are requested by a single admin
114      *
115      * @return a list of packages that the admin has requested to unsuspend but could not be
116      * unsuspended, due to other amdin's policy or PackageManager restriction.
117      *
118      */
unsuspend(Set<String> packages)119     public String[] unsuspend(Set<String> packages) {
120         // Unlike suspend(), when unsuspending, call PackageManager with the delta of resolved
121         // suspended packages list and not what the admin has requested. This is because some
122         // packages might still be subject to another admin's suspension request.
123         Set<String> packagesToUnsuspend = new ArraySet<>(mSuspendedPackageBefore);
124         packagesToUnsuspend.removeAll(mSuspendedPackageAfter);
125 
126         // To calculate the result (which packages are not unsuspended), start with packages that
127         // are still subject to another admin's suspension policy. This is calculated by
128         // intersecting the packages argument with mSuspendedPackageAfter.
129         Set<String> result = new ArraySet<>(packages);
130         result.retainAll(mSuspendedPackageAfter);
131         // Remove mExemptedPackages since they can't be suspended to start with.
132         result.removeAll(mExemptedPackages);
133         // Finally make the unsuspend() request and add packages that PackageManager can't unsuspend
134         // to the result.
135         result.addAll(unsuspendWithExemption(packagesToUnsuspend));
136         return result.toArray(String[]::new);
137     }
138 
139     /**
140      * Unsuspend packages considering the exemption list.
141      *
142      * @return the list of packages that couldn't be unsuspended, either due to the exemption list,
143      * or due to failures from PackageManagerInternal itself.
144      */
unsuspendWithExemption(Set<String> packages)145     private Set<String> unsuspendWithExemption(Set<String> packages) {
146         // when unsuspending, no need to consider exemption list since by definition they can't
147         // be suspended to begin with.
148         String[] failedPackages = mPackageManager.setPackagesSuspendedByAdmin(
149                 mUserId, packages.toArray(String[]::new), false);
150         if (failedPackages == null) {
151             Slogf.w(LOG_TAG, "PM failed to unsuspend packages (%s)", packages);
152         }
153         return new ArraySet<>(failedPackages);
154     }
155 }
156