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