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.os.Build.VERSION.SDK_INT;
21 
22 import static com.android.bedstead.nene.users.User.UserState.RUNNING_UNLOCKED;
23 import static com.android.compatibility.common.util.FileUtils.readInputStreamFully;
24 
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.os.Build;
30 
31 import androidx.annotation.CheckResult;
32 import androidx.annotation.Nullable;
33 import androidx.annotation.RequiresApi;
34 
35 import com.android.bedstead.nene.TestApis;
36 import com.android.bedstead.nene.annotations.Experimental;
37 import com.android.bedstead.nene.exceptions.AdbException;
38 import com.android.bedstead.nene.exceptions.AdbParseException;
39 import com.android.bedstead.nene.exceptions.NeneException;
40 import com.android.bedstead.nene.permissions.PermissionContext;
41 import com.android.bedstead.nene.users.User;
42 import com.android.bedstead.nene.users.UserReference;
43 import com.android.bedstead.nene.utils.ShellCommand;
44 import com.android.bedstead.nene.utils.ShellCommandUtils;
45 import com.android.bedstead.nene.utils.Versions;
46 import com.android.compatibility.common.util.BlockingBroadcastReceiver;
47 
48 import com.google.common.io.Files;
49 
50 import java.io.File;
51 import java.io.FileInputStream;
52 import java.io.IOException;
53 import java.io.InputStream;
54 import java.util.Collection;
55 import java.util.HashSet;
56 import java.util.Map;
57 import java.util.Objects;
58 import java.util.Set;
59 
60 /**
61  * Test APIs relating to packages.
62  */
63 public final class Packages {
64 
65     /** Reference to a Java resource. */
66     public static final class JavaResource {
67         private final String mName;
68 
JavaResource(String name)69         private JavaResource(String name) {
70             mName = name;
71         }
72 
73         /** Reference a Java resource by name. */
javaResource(String name)74         public static JavaResource javaResource(String name) {
75             if (name == null) {
76                 throw new NullPointerException();
77             }
78             return new JavaResource(name);
79         }
80 
81         @Override
toString()82         public String toString() {
83             return "JavaResource{name=" + mName + "}";
84         }
85 
86         @Override
equals(Object o)87         public boolean equals(Object o) {
88             if (this == o) return true;
89             if (!(o instanceof JavaResource)) return false;
90             JavaResource that = (JavaResource) o;
91             return mName.equals(that.mName);
92         }
93 
94         @Override
hashCode()95         public int hashCode() {
96             return Objects.hash(mName);
97         }
98     }
99 
100     /** Reference to an Android resource. */
101     public static final class AndroidResource {
102         private final String mName;
103 
AndroidResource(String name)104         private AndroidResource(String name) {
105             if (name == null) {
106                 throw new NullPointerException();
107             }
108             mName = name;
109         }
110 
111         /** Reference an Android resource by name. */
androidResource(String name)112         public static AndroidResource androidResource(String name) {
113             return new AndroidResource(name);
114         }
115 
116         @Override
toString()117         public String toString() {
118             return "AndroidResource{name=" + mName + "}";
119         }
120 
121         @Override
equals(Object o)122         public boolean equals(Object o) {
123             if (this == o) return true;
124             if (!(o instanceof AndroidResource)) return false;
125             AndroidResource that = (AndroidResource) o;
126             return mName.equals(that.mName);
127         }
128 
129         @Override
hashCode()130         public int hashCode() {
131             return Objects.hash(mName);
132         }
133     }
134 
135     private Map<String, Package> mCachedPackages = null;
136     private Set<String> mFeatures = null;
137     private final AdbPackageParser mParser;
138     final TestApis mTestApis;
139     private final Context mInstrumentedContext;
140 
141     private final IntentFilter mPackageAddedIntentFilter =
142             new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
143 
144 
Packages(TestApis testApis)145     public Packages(TestApis testApis) {
146         if (testApis == null) {
147             throw new NullPointerException();
148         }
149         mPackageAddedIntentFilter.addDataScheme("package");
150         mTestApis = testApis;
151         mParser = AdbPackageParser.get(mTestApis, SDK_INT);
152         mInstrumentedContext = mTestApis.context().instrumentedContext();
153     }
154 
155 
156     /** Get the features available on the device. */
features()157     public Set<String> features() {
158         if (mFeatures == null) {
159             fillCache();
160         }
161 
162         return mFeatures;
163     }
164 
165     /** Resolve all packages on the device. */
all()166     public Collection<PackageReference> all() {
167         return new HashSet<>(allResolved());
168     }
169 
170     /** Resolve all packages installed for a given {@link UserReference}. */
installedForUser(UserReference user)171     public Collection<PackageReference> installedForUser(UserReference user) {
172         if (user == null) {
173             throw new NullPointerException();
174         }
175         Set<PackageReference> installedForUser = new HashSet<>();
176 
177         for (Package pkg : allResolved()) {
178             if (pkg.installedOnUsers().contains(user)) {
179                 installedForUser.add(pkg);
180             }
181         }
182 
183         return installedForUser;
184     }
185 
allResolved()186     private Collection<Package> allResolved() {
187         fillCache();
188 
189         return mCachedPackages.values();
190     }
191 
192     /**
193      * Install an APK file to a given {@link UserReference}.
194      *
195      * <p>The user must be started.
196      *
197      * <p>If the package is already installed, this will replace it.
198      *
199      * <p>If the package is marked testOnly, it will still be installed.
200      */
install(UserReference user, File apkFile)201     public PackageReference install(UserReference user, File apkFile) {
202         if (user == null || apkFile == null) {
203             throw new NullPointerException();
204         }
205 
206         if (Versions.meetsMinimumSdkVersionRequirement(Build.VERSION_CODES.S)) {
207             return install(user, loadBytes(apkFile));
208         }
209 
210         User resolvedUser = user.resolve();
211 
212         if (resolvedUser == null || resolvedUser.state() != RUNNING_UNLOCKED) {
213             throw new NeneException("Packages can not be installed in non-started users "
214                     + "(Trying to install into user " + resolvedUser + ")");
215         }
216 
217         BlockingBroadcastReceiver broadcastReceiver =
218                 registerPackageInstalledBroadcastReceiver(user);
219 
220         try {
221             // Expected output "Success"
222             ShellCommand.builderForUser(user, "pm install")
223                     .addOperand("-r") // Reinstall automatically
224                     .addOperand("-t") // Allow test-only install
225                     .addOperand(apkFile.getAbsolutePath())
226                     .validate(ShellCommandUtils::startsWithSuccess)
227                     .execute();
228 
229             return waitForPackageAddedBroadcast(broadcastReceiver);
230         } catch (AdbException e) {
231             throw new NeneException("Could not install " + apkFile + " for user " + user, e);
232         } finally {
233             broadcastReceiver.unregisterQuietly();
234         }
235     }
236 
waitForPackageAddedBroadcast( BlockingBroadcastReceiver broadcastReceiver)237     private PackageReference waitForPackageAddedBroadcast(
238             BlockingBroadcastReceiver broadcastReceiver) {
239         Intent intent = broadcastReceiver.awaitForBroadcast();
240         if (intent == null) {
241             throw new NeneException(
242                     "Did not receive ACTION_PACKAGE_ADDED broadcast after installing package.");
243         }
244         // TODO(scottjonathan): Could this be flaky? what if something is added elsewhere at
245         //  the same time...
246         String installedPackageName = intent.getDataString().split(":", 2)[1];
247 
248         return mTestApis.packages().find(installedPackageName);
249     }
250 
251     // TODO: Move this somewhere reusable (in utils)
loadBytes(File file)252     private static byte[] loadBytes(File file) {
253         try (FileInputStream fis = new FileInputStream(file)) {
254             return readInputStreamFully(fis);
255         } catch (IOException e) {
256             throw new NeneException("Could not read file bytes for file " + file);
257         }
258     }
259 
260     /**
261      * Install an APK from the given byte array to a given {@link UserReference}.
262      *
263      * <p>The user must be started.
264      *
265      * <p>If the package is already installed, this will replace it.
266      *
267      * <p>If the package is marked testOnly, it will still be installed.
268      */
install(UserReference user, byte[] apkFile)269     public PackageReference install(UserReference user, byte[] apkFile) {
270         if (user == null || apkFile == null) {
271             throw new NullPointerException();
272         }
273 
274         if (!Versions.meetsMinimumSdkVersionRequirement(Build.VERSION_CODES.S)) {
275             return installPreS(user, apkFile);
276         }
277 
278         User resolvedUser = user.resolve();
279 
280         if (resolvedUser == null || resolvedUser.state() != RUNNING_UNLOCKED) {
281             throw new NeneException("Packages can not be installed in non-started users "
282                     + "(Trying to install into user " + resolvedUser + ")");
283         }
284 
285         BlockingBroadcastReceiver broadcastReceiver =
286                 registerPackageInstalledBroadcastReceiver(user);
287         try {
288             // Expected output "Success"
289             ShellCommand.builderForUser(user, "pm install")
290                     .addOption("-S", apkFile.length)
291                     .addOperand("-r")
292                     .addOperand("-t")
293                     .writeToStdIn(apkFile)
294                     .validate(ShellCommandUtils::startsWithSuccess)
295                     .execute();
296 
297             return waitForPackageAddedBroadcast(broadcastReceiver);
298         } catch (AdbException e) {
299             throw new NeneException("Could not install from bytes for user " + user, e);
300         } finally {
301             broadcastReceiver.unregisterQuietly();
302         }
303 
304         // TODO(scottjonathan): Re-enable this after we have a TestAPI which allows us to install
305         //   testOnly apks
306 //        BlockingBroadcastReceiver broadcastReceiver =
307 //                registerPackageInstalledBroadcastReceiver(user);
308 //
309 //        PackageManager packageManager =
310 //                mTestApis.context().androidContextAsUser(user).getPackageManager();
311 //        PackageInstaller packageInstaller = packageManager.getPackageInstaller();
312 //
313 //        try {
314 //            int sessionId;
315 //            try(PermissionContext p =
316 //                        mTestApis.permissions().withPermission(INTERACT_ACROSS_USERS_FULL)) {
317 //                PackageInstaller.SessionParams sessionParams =
318 //                      new PackageInstaller.SessionParams(MODE_FULL_INSTALL);
319 //                // TODO(scottjonathan): Enable installing test apps once there is a test
320 //                //  API for this
321 ////                    sessionParams.installFlags =
322 //                          sessionParams.installFlags | INSTALL_ALLOW_TEST;
323 //                sessionId = packageInstaller.createSession(sessionParams);
324 //            }
325 //
326 //            PackageInstaller.Session session = packageInstaller.openSession(sessionId);
327 //            try (OutputStream out =
328 //                         session.openWrite("NAME", 0, apkFile.length)) {
329 //                out.write(apkFile);
330 //                session.fsync(out);
331 //            }
332 //
333 //            try (BlockingIntentSender intentSender = BlockingIntentSender.create()) {
334 //                try (PermissionContext p =
335 //                             mTestApis.permissions().withPermission(INSTALL_PACKAGES)) {
336 //                    session.commit(intentSender.intentSender());
337 //                    session.close();
338 //                }
339 //
340 //                Intent intent = intentSender.await();
341 //                if (intent.getIntExtra(EXTRA_STATUS, /* defaultValue= */ STATUS_FAILURE)
342 //                        != STATUS_SUCCESS) {
343 //                    throw new NeneException("Not successful while installing package. "
344 //                            + "Got status: "
345 //                            + intent.getIntExtra(
346 //                            EXTRA_STATUS, /* defaultValue= */ STATUS_FAILURE)
347 //                            + " exta info: " + intent.getStringExtra(EXTRA_STATUS_MESSAGE));
348 //                }
349 //            }
350 //
351 //            return waitForPackageAddedBroadcast(broadcastReceiver);
352 //        } catch (IOException e) {
353 //            throw new NeneException("Could not install package", e);
354 //        } finally {
355 //            broadcastReceiver.unregisterQuietly();
356 //        }
357     }
358 
installPreS(UserReference user, byte[] apkFile)359     private PackageReference installPreS(UserReference user, byte[] apkFile) {
360         // Prior to S we cannot pass bytes to stdin so we write it to a temp file first
361         File outputDir = mTestApis.context().instrumentedContext().getCacheDir();
362         File outputFile = null;
363         try {
364             outputFile = File.createTempFile("tmp", ".apk", outputDir);
365             Files.write(apkFile, outputFile);
366             outputFile.setReadable(true, false);
367             return install(user, outputFile);
368         } catch (IOException e) {
369             throw new NeneException("Error when writing bytes to temp file", e);
370         } finally {
371             if (outputFile != null) {
372                 outputFile.delete();
373             }
374         }
375     }
376 
377     /**
378      * Install an APK stored in Android resources to the given {@link UserReference}.
379      *
380      * <p>The user must be started.
381      *
382      * <p>If the package is already installed, this will replace it.
383      *
384      * <p>If the package is marked testOnly, it will still be installed.
385      */
386     @Experimental
install(UserReference user, AndroidResource resource)387     public PackageReference install(UserReference user, AndroidResource resource) {
388         int indexId = mInstrumentedContext.getResources().getIdentifier(
389                 resource.mName, /* defType= */ null, /* defPackage= */ null);
390 
391         try (InputStream inputStream =
392                      mInstrumentedContext.getResources().openRawResource(indexId)) {
393             return install(user, readInputStreamFully(inputStream));
394         } catch (IOException e) {
395             throw new NeneException("Error reading resource " + resource, e);
396         }
397     }
398 
399     /**
400      * Install an APK stored in Java resources to the given {@link UserReference}.
401      *
402      * <p>The user must be started.
403      *
404      * <p>If the package is already installed, this will replace it.
405      *
406      * <p>If the package is marked testOnly, it will still be installed.
407      */
408     @Experimental
install(UserReference user, JavaResource resource)409     public PackageReference install(UserReference user, JavaResource resource) {
410         try (InputStream inputStream =
411                      Packages.class.getClassLoader().getResourceAsStream(resource.mName)) {
412             return install(user, readInputStreamFully(inputStream));
413         } catch (IOException e) {
414             throw new NeneException("Error reading java resource " + resource, e);
415         }
416     }
417 
registerPackageInstalledBroadcastReceiver( UserReference user)418     private BlockingBroadcastReceiver registerPackageInstalledBroadcastReceiver(
419             UserReference user) {
420         BlockingBroadcastReceiver broadcastReceiver = BlockingBroadcastReceiver.create(
421                 mTestApis.context().androidContextAsUser(user),
422                 mPackageAddedIntentFilter);
423 
424         if (user.equals(mTestApis.users().instrumented())) {
425             broadcastReceiver.register();
426         } else {
427             // TODO(scottjonathan): If this is cross-user then it needs _FULL, but older versions
428             //  cannot get full - so we'll need to poll
429             try (PermissionContext p =
430                          mTestApis.permissions().withPermission(INTERACT_ACROSS_USERS_FULL)) {
431                 broadcastReceiver.register();
432             }
433         }
434 
435         return broadcastReceiver;
436     }
437 
438     /**
439      * Set packages which will not be cleaned up by the system even if they are not installed on
440      * any user.
441      *
442      * <p>This will ensure they can still be resolved and re-installed without needing the APK
443      */
444     @RequiresApi(Build.VERSION_CODES.S)
445     @CheckResult
keepUninstalledPackages()446     public KeepUninstalledPackagesBuilder keepUninstalledPackages() {
447         Versions.requireMinimumVersion(Build.VERSION_CODES.S);
448 
449         return new KeepUninstalledPackagesBuilder(mTestApis);
450     }
451 
452     @Nullable
fetchPackage(String packageName)453     Package fetchPackage(String packageName) {
454         // TODO(scottjonathan): fillCache probably does more than we need here -
455         //  can we make it more efficient?
456         fillCache();
457 
458         return mCachedPackages.get(packageName);
459     }
460 
461     /**
462      * Get a reference to a package with the given {@code packageName}.
463      *
464      * <p>This does not guarantee that the package exists. Call {@link PackageReference#resolve()}
465      * to find specific details about the package on the device.
466      */
find(String packageName)467     public PackageReference find(String packageName) {
468         if (packageName == null) {
469             throw new NullPointerException();
470         }
471         return new UnresolvedPackage(mTestApis, packageName);
472     }
473 
474     /**
475      * Get a reference to a given {@code componentName}.
476      *
477      * <p>This does not guarantee that the component exists.
478      */
479     @Experimental
component(ComponentName componentName)480     public ComponentReference component(ComponentName componentName) {
481         if (componentName == null) {
482             throw new NullPointerException();
483         }
484 
485         return new ComponentReference(mTestApis,
486                 find(componentName.getPackageName()), componentName.getClassName());
487     }
488 
fillCache()489     private void fillCache() {
490         try {
491             // TODO: Replace use of adb on supported versions of Android
492             String packageDumpsysOutput = ShellCommand.builder("dumpsys package").execute();
493             AdbPackageParser.ParseResult result = mParser.parse(packageDumpsysOutput);
494 
495             mCachedPackages = result.mPackages;
496             mFeatures = result.mFeatures;
497         } catch (AdbException | AdbParseException e) {
498             throw new RuntimeException("Error filling cache", e);
499         }
500     }
501 }
502