1 /*
2  * Copyright (C) 2021 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.bedstead.nene.packages;
18 
19 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
20 import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
21 import static android.content.pm.PermissionInfo.PROTECTION_FLAG_DEVELOPMENT;
22 
23 import static com.google.common.truth.Truth.assertWithMessage;
24 
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.content.pm.PackageManager;
28 import android.content.pm.PermissionInfo;
29 
30 import androidx.annotation.Nullable;
31 
32 import com.android.bedstead.nene.TestApis;
33 import com.android.bedstead.nene.annotations.Experimental;
34 import com.android.bedstead.nene.exceptions.AdbException;
35 import com.android.bedstead.nene.exceptions.NeneException;
36 import com.android.bedstead.nene.permissions.PermissionContext;
37 import com.android.bedstead.nene.users.UserReference;
38 import com.android.bedstead.nene.utils.ShellCommand;
39 import com.android.compatibility.common.util.BlockingBroadcastReceiver;
40 
41 import java.io.File;
42 import java.util.Arrays;
43 import java.util.Set;
44 import java.util.stream.Collectors;
45 
46 /**
47  * A representation of a package on device which may or may not exist.
48  *
49  * <p>To resolve the package into a {@link Package}, see {@link #resolve()}.
50  */
51 public abstract class PackageReference {
52     private final TestApis mTestApis;
53     private final String mPackageName;
54 
55     private static final int PIDS_PER_USER_ID = 100000;
56 
57     private final PackageManager mPackageManager;
58 
PackageReference(TestApis testApis, String packageName)59     PackageReference(TestApis testApis, String packageName) {
60         mTestApis = testApis;
61         mPackageManager = mTestApis.context().instrumentedContext().getPackageManager();
62         mPackageName = packageName;
63     }
64 
65     /** Return the package's name. */
packageName()66     public String packageName() {
67         return mPackageName;
68     }
69 
70     /**
71      * Get the current state of the {@link Package} from the device, or {@code null} if the package
72      * does not exist.
73      */
74     @Nullable
resolve()75     public Package resolve() {
76         return mTestApis.packages().fetchPackage(mPackageName);
77     }
78 
79     /**
80      * Install the package on the given user.
81      *
82      * <p>If you wish to install a package which is not already installed on another user, see
83      * {@link Packages#install(UserReference, File)}.
84      */
install(UserReference user)85     public PackageReference install(UserReference user) {
86         if (user == null) {
87             throw new NullPointerException();
88         }
89         try {
90             // Expected output "Package X installed for user: Y"
91             ShellCommand.builderForUser(user, "cmd package install-existing")
92                     .addOperand(mPackageName)
93                     .validate(
94                             (output) -> output.contains("installed for user"))
95                     .execute();
96             return this;
97         } catch (AdbException e) {
98             throw new NeneException("Could not install-existing package " + this, e);
99         }
100     }
101 
102     /**
103      * Uninstall the package for all users.
104      *
105      * <p>The package will no longer {@link #resolve()}.
106      */
uninstallFromAllUsers()107     public PackageReference uninstallFromAllUsers() {
108         Package pkg = resolve();
109         if (pkg == null) {
110             return this;
111         }
112 
113         for (UserReference user : pkg.installedOnUsers()) {
114             pkg.uninstall(user);
115         }
116 
117         return this;
118     }
119 
120     /**
121      * Uninstall the package for the given user.
122      *
123      * <p>If this is the last user which has this package installed, then the package will no
124      * longer {@link #resolve()}.
125      *
126      * <p>If the package is not installed for the given user, nothing will happen.
127      */
uninstall(UserReference user)128     public PackageReference uninstall(UserReference user) {
129         if (user == null) {
130             throw new NullPointerException();
131         }
132 
133         IntentFilter packageRemovedIntentFilter =
134                 new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
135         packageRemovedIntentFilter.addDataScheme("package");
136 
137         BlockingBroadcastReceiver broadcastReceiver = BlockingBroadcastReceiver.create(
138                 mTestApis.context().androidContextAsUser(user),
139                 packageRemovedIntentFilter);
140 
141         try {
142             try (PermissionContext p = mTestApis.permissions().withPermission(
143                     INTERACT_ACROSS_USERS_FULL)) {
144                 broadcastReceiver.register();
145             }
146 
147             // Expected output "Success"
148             String output = ShellCommand.builderForUser(user, "pm uninstall")
149                     .addOperand(mPackageName)
150                     .validate((o) -> {
151                         o = o.toUpperCase();
152                         return o.startsWith("SUCCESS") || o.contains("NOT INSTALLED FOR");
153                     })
154                     .execute();
155 
156             if (output.toUpperCase().startsWith("SUCCESS")) {
157                 broadcastReceiver.awaitForBroadcastOrFail();
158             }
159 
160             return this;
161         } catch (AdbException e) {
162             throw new NeneException("Could not uninstall package " + this, e);
163         } finally {
164             broadcastReceiver.unregisterQuietly();
165         }
166     }
167 
168     /**
169      * Enable this package for the given {@link UserReference}.
170      */
171     @Experimental
enable(UserReference user)172     public PackageReference enable(UserReference user) {
173         try {
174             ShellCommand.builderForUser(user, "pm enable")
175                     .addOperand(mPackageName)
176                     .validate(o -> o.contains("new state"))
177                     .execute();
178         } catch (AdbException e) {
179             throw new NeneException("Error enabling package " + this + " for user " + user, e);
180         }
181         return this;
182     }
183 
184     /**
185      * Enable this package on the instrumented user.
186      */
187     @Experimental
enable()188     public PackageReference enable() {
189         return enable(mTestApis.users().instrumented());
190     }
191 
192     /**
193      * Disable this package for the given {@link UserReference}.
194      */
195     @Experimental
disable(UserReference user)196     public PackageReference disable(UserReference user) {
197         try {
198             ShellCommand.builderForUser(user, "pm disable")
199                     .addOperand(mPackageName)
200                     .validate(o -> o.contains("new state"))
201                     .execute();
202         } catch (AdbException e) {
203             throw new NeneException("Error disabling package " + this + " for user " + user, e);
204         }
205         return this;
206     }
207 
208     /**
209      * Disable this package on the instrumented user.
210      */
211     @Experimental
disable()212     public PackageReference disable() {
213         return disable(mTestApis.users().instrumented());
214     }
215 
216     /**
217      * Get a reference to the given {@code componentName} within this package.
218      *
219      * <p>This does not guarantee that the component exists.
220      */
221     @Experimental
component(String componentName)222     public ComponentReference component(String componentName) {
223         return new ComponentReference(mTestApis, this, componentName);
224     }
225 
226     /**
227      * Grant a permission for the package on the given user.
228      *
229      * <p>The package must be installed on the user, must request the given permission, and the
230      * permission must be a runtime permission.
231      */
grantPermission(UserReference user, String permission)232     public PackageReference grantPermission(UserReference user, String permission) {
233         // There is no readable output upon failure so we need to check ourselves
234         checkCanGrantOrRevokePermission(user, permission);
235 
236         try {
237             ShellCommand.builderForUser(user, "pm grant")
238                     .addOperand(packageName())
239                     .addOperand(permission)
240                     .allowEmptyOutput(true)
241                     .validate(String::isEmpty)
242                     .execute();
243 
244             assertWithMessage("Error granting permission " + permission
245                     + " to package " + this + " on user " + user
246                     + ". Command appeared successful but not set.")
247                     .that(resolve().grantedPermissions(user)).contains(permission);
248 
249             return this;
250         } catch (AdbException e) {
251             throw new NeneException("Error granting permission " + permission + " to package "
252                     + this + " on user " + user, e);
253         }
254     }
255 
256     /**
257      * Deny a permission for the package on the given user.
258      *
259      * <p>The package must be installed on the user, must request the given permission, and the
260      * permission must be a runtime permission.
261      *
262      * <p>You can not deny permissions for the current package on the current user.
263      */
denyPermission(UserReference user, String permission)264     public PackageReference denyPermission(UserReference user, String permission) {
265         // There is no readable output upon failure so we need to check ourselves
266         checkCanGrantOrRevokePermission(user, permission);
267 
268         if (packageName().equals(mTestApis.context().instrumentedContext().getPackageName())
269                 && user.equals(mTestApis.users().instrumented())) {
270             Package resolved = resolve();
271             if (!resolved.grantedPermissions(user).contains(permission)) {
272                 return this; // Already denied
273             }
274             throw new NeneException("Cannot deny permission from current package");
275         }
276 
277         try {
278             ShellCommand.builderForUser(user, "pm revoke")
279                     .addOperand(packageName())
280                     .addOperand(permission)
281                     .allowEmptyOutput(true)
282                     .validate(String::isEmpty)
283                     .execute();
284 
285             assertWithMessage("Error denying permission " + permission
286                     + " to package " + this + " on user " + user
287                     + ". Command appeared successful but not set.")
288                     .that(resolve().grantedPermissions(user)).doesNotContain(permission);
289 
290             return this;
291         } catch (AdbException e) {
292             throw new NeneException("Error denying permission " + permission + " to package "
293                     + this + " on user " + user, e);
294         }
295     }
296 
checkCanGrantOrRevokePermission(UserReference user, String permission)297     private void checkCanGrantOrRevokePermission(UserReference user, String permission) {
298         Package resolved = resolve();
299         if (resolved == null || !resolved.installedOnUsers().contains(user)) {
300             throw new NeneException("Attempting to grant " + permission + " to " + this
301                     + " on user " + user + ". But it is not installed");
302         }
303 
304         try {
305             PermissionInfo permissionInfo =
306                     mPackageManager.getPermissionInfo(permission, /* flags= */ 0);
307 
308             if (!protectionIsDangerous(permissionInfo.protectionLevel)
309                     && !protectionIsDevelopment(permissionInfo.protectionLevel)) {
310                 throw new NeneException("Cannot grant non-runtime permission "
311                         + permission + ", protection level is " + permissionInfo.protectionLevel);
312             }
313 
314             if (!resolved.requestedPermissions().contains(permission)) {
315                 throw new NeneException("Cannot grant permission "
316                         + permission + " which was not requested by package " + packageName());
317             }
318         } catch (PackageManager.NameNotFoundException e) {
319             throw new NeneException("Permission does not exist: " + permission);
320         }
321     }
322 
protectionIsDangerous(int protectionLevel)323     private boolean protectionIsDangerous(int protectionLevel) {
324         return (protectionLevel & PROTECTION_DANGEROUS) != 0;
325     }
326 
protectionIsDevelopment(int protectionLevel)327     private boolean protectionIsDevelopment(int protectionLevel) {
328         return (protectionLevel & PROTECTION_FLAG_DEVELOPMENT) != 0;
329     }
330 
331     @Experimental
runningProcesses()332     public Set<ProcessReference> runningProcesses() {
333         // TODO(scottjonathan): See if this can be remade using
334         //  ActivityManager#getRunningappProcesses
335         try {
336             return ShellCommand.builder("ps")
337                     .addOperand("-A")
338                     .addOperand("-n")
339                     .executeAndParseOutput(o -> parsePsOutput(o).stream()
340                     .filter(p -> p.mPackageName.equals(mPackageName))
341                     .map(p -> new ProcessReference(this, p.mPid, mTestApis.users().find(p.mUserId))))
342                     .collect(Collectors.toSet());
343         } catch (AdbException e) {
344             throw new NeneException("Error getting running processes ", e);
345         }
346     }
347 
parsePsOutput(String psOutput)348     private Set<ProcessInfo> parsePsOutput(String psOutput) {
349         return Arrays.stream(psOutput.split("\n"))
350                 .skip(1) // Skip the title line
351                 .map(s -> s.split("\\s+"))
352                 .map(m -> new ProcessInfo(
353                         m[8], Integer.parseInt(m[1]), Integer.parseInt(m[0]) / PIDS_PER_USER_ID))
354                 .collect(Collectors.toSet());
355     }
356 
357     private static final class ProcessInfo {
358         public final String mPackageName;
359         public final int mPid;
360         public final int mUserId;
361 
ProcessInfo(String packageName, int pid, int userId)362         public ProcessInfo(String packageName, int pid, int userId) {
363             if (packageName == null) {
364                 throw new NullPointerException();
365             }
366             mPackageName = packageName;
367             mPid = pid;
368             mUserId = userId;
369         }
370 
371         @Override
toString()372         public String toString() {
373             return "ProcessInfo{packageName=" + mPackageName + ", pid="
374                     + mPid + ", userId=" + mUserId + "}";
375         }
376     }
377 
378     @Experimental
379     @Nullable
runningProcess(UserReference user)380     public ProcessReference runningProcess(UserReference user) {
381         return runningProcesses().stream().filter(
382                 i -> i.user().equals(user))
383                 .findAny()
384                 .orElse(null);
385     }
386 
387 
388     @Override
hashCode()389     public int hashCode() {
390         return mPackageName.hashCode();
391     }
392 
393     @Override
equals(Object obj)394     public boolean equals(Object obj) {
395         if (!(obj instanceof PackageReference)) {
396             return false;
397         }
398 
399         PackageReference other = (PackageReference) obj;
400         return other.mPackageName.equals(mPackageName);
401     }
402 
403     @Override
toString()404     public String toString() {
405         StringBuilder stringBuilder = new StringBuilder("PackageReference{");
406         stringBuilder.append("packageName=" + mPackageName);
407         stringBuilder.append("}");
408         return stringBuilder.toString();
409     }
410 }
411