1 /* 2 * Copyright (C) 2019 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 android.content.pm.cts; 18 19 import static android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG; 20 21 import static com.google.common.truth.Truth.assertThat; 22 23 import static org.junit.Assert.assertEquals; 24 import static org.junit.Assert.assertFalse; 25 import static org.junit.Assert.assertNotEquals; 26 import static org.junit.Assert.assertTrue; 27 import static org.junit.Assert.fail; 28 29 import android.annotation.NonNull; 30 import android.app.UiAutomation; 31 import android.content.Context; 32 import android.content.Intent; 33 import android.content.pm.PackageManager; 34 import android.content.pm.cts.util.AbandonAllPackageSessionsRule; 35 import android.os.IBinder; 36 import android.os.ParcelFileDescriptor; 37 import android.os.Process; 38 import android.os.SystemClock; 39 import android.os.UserHandle; 40 import android.platform.test.annotations.AppModeFull; 41 import android.platform.test.annotations.AppModeNonSdkSandbox; 42 import android.platform.test.annotations.Presubmit; 43 import android.provider.DeviceConfig; 44 import android.service.dataloader.DataLoaderService; 45 import android.system.Os; 46 import android.text.TextUtils; 47 import android.util.ArrayMap; 48 import android.util.Log; 49 50 import androidx.test.InstrumentationRegistry; 51 import androidx.test.filters.FlakyTest; 52 import androidx.test.filters.LargeTest; 53 import androidx.test.runner.AndroidJUnit4; 54 55 import com.android.compatibility.common.util.PropertyUtil; 56 import com.android.incfs.install.IBlockFilter; 57 import com.android.incfs.install.IBlockTransformer; 58 import com.android.incfs.install.IncrementalInstallSession; 59 import com.android.incfs.install.PendingBlock; 60 61 import com.google.common.truth.Truth; 62 63 import libcore.io.IoUtils; 64 65 import org.apache.commons.compress.compressors.lz4.BlockLZ4CompressorOutputStream; 66 import org.junit.After; 67 import org.junit.Assert; 68 import org.junit.Assume; 69 import org.junit.Before; 70 import org.junit.Rule; 71 import org.junit.Test; 72 import org.junit.runner.RunWith; 73 74 import java.io.ByteArrayOutputStream; 75 import java.io.File; 76 import java.io.FileInputStream; 77 import java.io.FileOutputStream; 78 import java.io.IOException; 79 import java.io.InputStream; 80 import java.io.OutputStream; 81 import java.nio.ByteBuffer; 82 import java.nio.channels.Channels; 83 import java.nio.file.Paths; 84 import java.util.ArrayList; 85 import java.util.Arrays; 86 import java.util.Optional; 87 import java.util.Random; 88 import java.util.Scanner; 89 import java.util.concurrent.Callable; 90 import java.util.concurrent.CompletableFuture; 91 import java.util.concurrent.Executors; 92 import java.util.concurrent.TimeUnit; 93 import java.util.concurrent.atomic.AtomicBoolean; 94 import java.util.concurrent.atomic.AtomicLong; 95 import java.util.function.Function; 96 import java.util.stream.Collectors; 97 import java.util.stream.Stream; 98 99 @RunWith(AndroidJUnit4.class) 100 @AppModeFull 101 @AppModeNonSdkSandbox 102 @LargeTest 103 @Presubmit 104 public class PackageManagerShellCommandIncrementalTest { 105 private static final String TAG = "PackageManagerShellCommandIncrementalTest"; 106 107 private static final String CTS_PACKAGE_NAME = "android.content.cts"; 108 private static final String TEST_APP_PACKAGE = "com.example.helloworld"; 109 110 private static final String TEST_APK_PATH = "/data/local/tmp/cts/content/"; 111 private static final String TEST_APK = "HelloWorld5.apk"; 112 private static final String TEST_APK_IDSIG = "HelloWorld5.apk.idsig"; 113 private static final String TEST_APK_PROFILEABLE = "HelloWorld5Profileable.apk"; 114 private static final String TEST_APK_SYSTEM = "HelloWorldSystem.apk"; 115 private static final String TEST_APK_SPLIT0 = "HelloWorld5_mdpi-v4.apk"; 116 private static final String TEST_APK_SPLIT0_IDSIG = "HelloWorld5_mdpi-v4.apk.idsig"; 117 private static final String TEST_APK_SPLIT1 = "HelloWorld5_hdpi-v4.apk"; 118 private static final String TEST_APK_SPLIT1_IDSIG = "HelloWorld5_hdpi-v4.apk.idsig"; 119 private static final String TEST_APK_SPLIT2 = "HelloWorld5_xhdpi-v4.apk"; 120 private static final String TEST_APK_SPLIT2_IDSIG = "HelloWorld5_xhdpi-v4.apk.idsig"; 121 private static final String TEST_APK_MALFORMED = "malformed.apk"; 122 123 private static final String TEST_HW7 = "HelloWorld7.apk"; 124 private static final String TEST_HW7_IDSIG = "HelloWorld7.apk.idsig"; 125 private static final String TEST_HW7_SPLIT0 = "HelloWorld7_hdpi-v4.apk"; 126 private static final String TEST_HW7_SPLIT0_IDSIG = "HelloWorld7_hdpi-v4.apk.idsig"; 127 private static final String TEST_HW7_SPLIT1 = "HelloWorld7_mdpi-v4.apk"; 128 private static final String TEST_HW7_SPLIT1_IDSIG = "HelloWorld7_mdpi-v4.apk.idsig"; 129 private static final String TEST_HW7_SPLIT2 = "HelloWorld7_xhdpi-v4.apk"; 130 private static final String TEST_HW7_SPLIT2_IDSIG = "HelloWorld7_xhdpi-v4.apk.idsig"; 131 private static final String TEST_HW7_SPLIT3 = "HelloWorld7_xxhdpi-v4.apk"; 132 private static final String TEST_HW7_SPLIT3_IDSIG = "HelloWorld7_xxhdpi-v4.apk.idsig"; 133 private static final String TEST_HW7_SPLIT4 = "HelloWorld7_xxxhdpi-v4.apk"; 134 private static final String TEST_HW7_SPLIT4_IDSIG = "HelloWorld7_xxxhdpi-v4.apk.idsig"; 135 136 private static final boolean CHECK_BASE_APK_DIGESTION = false; 137 138 private static final long EXPECTED_READ_TIME = 1000L; 139 140 @Rule 141 public AbandonAllPackageSessionsRule mAbandonSessionsRule = new AbandonAllPackageSessionsRule(); 142 143 private IncrementalInstallSession mSession = null; 144 getUiAutomation()145 private static UiAutomation getUiAutomation() { 146 return InstrumentationRegistry.getInstrumentation().getUiAutomation(); 147 } 148 getContext()149 private static Context getContext() { 150 return InstrumentationRegistry.getInstrumentation().getContext(); 151 } 152 getPackageManager()153 private static PackageManager getPackageManager() { 154 return getContext().getPackageManager(); 155 } 156 157 @Before onBefore()158 public void onBefore() throws Exception { 159 checkIncrementalDeliveryFeature(); 160 uninstallPackageSilently(TEST_APP_PACKAGE); 161 } 162 163 @After onAfter()164 public void onAfter() throws Exception { 165 uninstallPackageSilently(TEST_APP_PACKAGE); 166 setDeviceProperty("incfs_default_timeouts", null); 167 setDeviceProperty("known_digesters_list", null); 168 setSystemProperty("debug.incremental.enforce_readlogs_max_interval_for_system_dataloaders", 169 "0"); 170 setSystemProperty("debug.incremental.readlogs_max_interval_sec", "10000"); 171 setSystemProperty("debug.incremental.always_enable_read_timeouts_for_system_dataloaders", 172 "1"); 173 IoUtils.closeQuietly(mSession); 174 mSession = null; 175 } 176 checkIncrementalDeliveryFeature()177 static void checkIncrementalDeliveryFeature() { 178 Assume.assumeTrue(getPackageManager().hasSystemFeature( 179 PackageManager.FEATURE_INCREMENTAL_DELIVERY)); 180 } 181 checkIncrementalDeliveryV2Feature()182 private static void checkIncrementalDeliveryV2Feature() throws Exception { 183 checkIncrementalDeliveryFeature(); 184 Assume.assumeTrue(getPackageManager().hasSystemFeature( 185 PackageManager.FEATURE_INCREMENTAL_DELIVERY, 2)); 186 } 187 188 @Test testAndroid12RequiresIncFsV2()189 public void testAndroid12RequiresIncFsV2() throws Exception { 190 // IncFS is a kernel feature, which is a subject to vendor freeze. That's why 191 // the test verifies the vendor API level here, not the system's one. 192 // Note: vendor API level getter returns either the frozen API level, or the current one for 193 // non-vendor-freeze devices; need to verify both the system first API level and vendor 194 // level to make the final decision. 195 final boolean v2ReqdForSystem = PropertyUtil.getFirstApiLevel() > 30; 196 final boolean v2ReqdForVendor = PropertyUtil.isVendorApiLevelNewerThan(30); 197 final boolean v2Required = v2ReqdForSystem && v2ReqdForVendor; 198 if (v2Required) { 199 Assert.assertTrue("Devices launched at API 31+ with a vendor partition of API 31+ need " 200 + "to support Incremental Delivery version 2 or higher", 201 getPackageManager().hasSystemFeature( 202 PackageManager.FEATURE_INCREMENTAL_DELIVERY, 2)); 203 } 204 } 205 206 @Test testInstallWithIdSig()207 public void testInstallWithIdSig() throws Exception { 208 installPackage(TEST_APK); 209 assertTrue(isAppInstalled(TEST_APP_PACKAGE)); 210 } 211 212 @Test testBug183952694Fixed()213 public void testBug183952694Fixed() throws Exception { 214 // first ensure the IncFS is up and running, e.g. if it's a module 215 installPackage(TEST_APK); 216 assertTrue(isAppInstalled(TEST_APP_PACKAGE)); 217 218 // the bug is fixed in the v2 version, or when the specific marker feature is present 219 final String[] validValues = {"v2", "mounter_context_for_backing_rw"}; 220 final String features = executeShellCommand("ls /sys/fs/incremental-fs/features/"); 221 assertTrue( 222 "Missing all of required IncFS features [" + TextUtils.join(",", validValues) + "]", 223 Arrays.stream(features.split("\\s+")).anyMatch( 224 f -> Arrays.stream(validValues).anyMatch(f::equals))); 225 } 226 227 @Test testBug270117845Fixed()228 public void testBug270117845Fixed() throws Exception { 229 // first ensure the IncFS is up and running, e.g. if it's a module 230 installPackage(TEST_APK); 231 assertTrue(isAppInstalled(TEST_APP_PACKAGE)); 232 233 // the bug is fixed when the specific marker feature is present 234 final String[] validValues = {"bugfix_inode_eviction"}; 235 final String features = executeShellCommand("ls /sys/fs/incremental-fs/features/"); 236 assertTrue( 237 "Missing required IncFS features [" + TextUtils.join(",", validValues) + "]", 238 Arrays.stream(features.split("\\s+")).anyMatch( 239 f -> Arrays.stream(validValues).anyMatch(f::equals))); 240 } 241 242 @LargeTest 243 @Test 244 @FlakyTest testSpaceAllocatedForPackage()245 public void testSpaceAllocatedForPackage() throws Exception { 246 final String apk = createApkPath(TEST_APK); 247 final String idsig = createApkPath(TEST_APK_IDSIG); 248 final long appFileSize = new File(apk).length(); 249 final AtomicBoolean firstTime = new AtomicBoolean(true); 250 251 getUiAutomation().adoptShellPermissionIdentity(); 252 253 final long blockSize = Os.statvfs("/data/incremental").f_bsize; 254 final long preAllocatedBlocks = Os.statvfs("/data/incremental").f_bfree; 255 256 final AtomicLong freeSpaceDifference = new AtomicLong(-1L); 257 258 mSession = 259 new IncrementalInstallSession.Builder() 260 .addApk(Paths.get(apk), Paths.get(idsig)) 261 .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME) 262 .setLogger(new IncrementalDeviceConnection.Logger()) 263 .setBlockFilter((block -> { 264 // Skip allocation check after first iteration. 265 if (!firstTime.getAndSet(false)) { 266 return true; 267 } 268 269 try { 270 final long postAllocatedBlocks = 271 Os.statvfs("/data/incremental").f_bfree; 272 freeSpaceDifference.set( 273 (preAllocatedBlocks - postAllocatedBlocks) * blockSize); 274 } catch (Exception e) { 275 Log.i(TAG, "ErrnoException: ", e); 276 throw new AssertionError(e); 277 } 278 return true; 279 })) 280 .setBlockTransformer(new CompressingBlockTransformer()) 281 .build(); 282 283 try { 284 mSession.start(Executors.newSingleThreadExecutor(), 285 IncrementalDeviceConnection.Factory.reliable()); 286 mSession.waitForInstallCompleted(30, TimeUnit.SECONDS); 287 } finally { 288 getUiAutomation().dropShellPermissionIdentity(); 289 } 290 291 assertTrue(isAppInstalled(TEST_APP_PACKAGE)); 292 293 final double freeSpaceExpectedDifference = ((appFileSize * 1.015) + blockSize * 8); 294 assertTrue(freeSpaceDifference.get() + " >= " + freeSpaceExpectedDifference, 295 freeSpaceDifference.get() >= freeSpaceExpectedDifference); 296 297 String installPath = executeShellCommand(String.format("pm path %s", TEST_APP_PACKAGE)) 298 .replaceFirst("package:", "") 299 .trim(); 300 301 // Retrieve size of APK. 302 Long apkTrimResult = Os.stat(installPath).st_size; 303 304 // Verify trim was applied. v2+ incfs version required for valid allocation results. 305 if (getPackageManager().hasSystemFeature( 306 PackageManager.FEATURE_INCREMENTAL_DELIVERY, 2)) { 307 assertTrue(apkTrimResult <= appFileSize); 308 } 309 } 310 311 @Test testSplitInstallWithIdSig()312 public void testSplitInstallWithIdSig() throws Exception { 313 // First fully install the apk. 314 { 315 installPackage(TEST_APK); 316 assertTrue(isAppInstalled(TEST_APP_PACKAGE)); 317 } 318 319 installSplit(TEST_APK_SPLIT0); 320 assertEquals("base, config.mdpi", getSplits(TEST_APP_PACKAGE)); 321 322 installSplit(TEST_APK_SPLIT1); 323 assertEquals("base, config.hdpi, config.mdpi", getSplits(TEST_APP_PACKAGE)); 324 } 325 326 @Test testSystemInstallWithIdSig()327 public void testSystemInstallWithIdSig() throws Exception { 328 final String baseName = TEST_APK_SYSTEM; 329 final File file = new File(createApkPath(baseName)); 330 assertThat(executeShellCommand("pm install-incremental -t -g " + file.getPath())) 331 .startsWith("Failure [INSTALL_FAILED_SESSION_INVALID"); 332 } 333 334 @LargeTest 335 @Test testInstallWithIdSigAndSplit()336 public void testInstallWithIdSigAndSplit() throws Exception { 337 File apkfile = new File(createApkPath(TEST_APK)); 338 File splitfile = new File(createApkPath(TEST_APK_SPLIT0)); 339 File[] files = new File[]{apkfile, splitfile}; 340 String param = Arrays.stream(files).map( 341 file -> file.getName() + ":" + file.length()).collect(Collectors.joining(" ")); 342 assertEquals("Success\n", executeShellCommand( 343 String.format("pm install-incremental -t -g -S %s %s", 344 (apkfile.length() + splitfile.length()), param), 345 files)); 346 assertTrue(isAppInstalled(TEST_APP_PACKAGE)); 347 assertEquals("base, config.mdpi", getSplits(TEST_APP_PACKAGE)); 348 } 349 350 @LargeTest 351 @Test testInstallWithStreaming()352 public void testInstallWithStreaming() throws Exception { 353 final String apk = createApkPath(TEST_APK); 354 final String idsig = createApkPath(TEST_APK_IDSIG); 355 mSession = 356 new IncrementalInstallSession.Builder() 357 .addApk(Paths.get(apk), Paths.get(idsig)) 358 .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME) 359 .setLogger(new IncrementalDeviceConnection.Logger()) 360 .build(); 361 getUiAutomation().adoptShellPermissionIdentity(); 362 try { 363 mSession.start(Executors.newSingleThreadExecutor(), 364 IncrementalDeviceConnection.Factory.reliable()); 365 mSession.waitForInstallCompleted(30, TimeUnit.SECONDS); 366 } finally { 367 getUiAutomation().dropShellPermissionIdentity(); 368 } 369 assertTrue(isAppInstalled(TEST_APP_PACKAGE)); 370 } 371 372 @LargeTest 373 @Test 374 @FlakyTest // This test is flaky by design testInstallWithMissingBlocks()375 public void testInstallWithMissingBlocks() throws Exception { 376 setDeviceProperty("incfs_default_timeouts", "0:0:0"); 377 setDeviceProperty("known_digesters_list", CTS_PACKAGE_NAME); 378 setSystemProperty("debug.incremental.always_enable_read_timeouts_for_system_dataloaders", 379 "0"); 380 381 final long randomSeed = System.currentTimeMillis(); 382 Log.i(TAG, "Randomizing missing blocks with seed: " + randomSeed); 383 final Random random = new Random(randomSeed); 384 385 // TODO: add detection of orphaned IncFS instances after failed installations 386 387 final int blockSize = 4096; 388 final int retries = 7; // 7 * 3s + leeway ~= 30secs of test timeout 389 390 final File apk = new File(createApkPath(TEST_APK)); 391 final int blocks = (int) (apk.length() / blockSize); 392 393 for (int i = 0; i < retries; ++i) { 394 final int skipBlock = random.nextInt(blocks); 395 Log.i(TAG, "skipBlock: " + skipBlock + " out of " + blocks); 396 try { 397 installWithBlockFilter((block -> block.getType() == PendingBlock.Type.SIGNATURE_TREE 398 || block.getBlockIndex() != skipBlock)); 399 if (isAppInstalled(TEST_APP_PACKAGE)) { 400 uninstallPackageSilently(TEST_APP_PACKAGE); 401 } 402 } catch (RuntimeException re) { 403 Log.i(TAG, "RuntimeException: ", re); 404 assertTrue(re.toString(), re.getCause() instanceof IOException); 405 } catch (IOException e) { 406 Log.i(TAG, "IOException: ", e); 407 throw new IOException("Skipped block: " + skipBlock + ", randomSeed: " + randomSeed, 408 e); 409 } 410 } 411 } 412 installWithBlockFilter(IBlockFilter blockFilter)413 public void installWithBlockFilter(IBlockFilter blockFilter) throws Exception { 414 final String apk = createApkPath(TEST_APK); 415 final String idsig = createApkPath(TEST_APK_IDSIG); 416 mSession = 417 new IncrementalInstallSession.Builder() 418 .addApk(Paths.get(apk), Paths.get(idsig)) 419 .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME) 420 .setLogger(new IncrementalDeviceConnection.Logger()) 421 .setBlockFilter(blockFilter) 422 .build(); 423 getUiAutomation().adoptShellPermissionIdentity(); 424 try { 425 mSession.start(Executors.newSingleThreadExecutor(), 426 IncrementalDeviceConnection.Factory.reliableExpectInstallationFailure()); 427 mSession.waitForAnyCompletion(30, TimeUnit.SECONDS); 428 } finally { 429 getUiAutomation().dropShellPermissionIdentity(); 430 } 431 } 432 433 /** 434 * Compress the data if the compressed size is < original size, otherwise return the original 435 * data. 436 */ maybeCompressPage(ByteBuffer pageData)437 private static ByteBuffer maybeCompressPage(ByteBuffer pageData) { 438 pageData.mark(); 439 ByteArrayOutputStream compressedByteStream = new ByteArrayOutputStream(); 440 try (BlockLZ4CompressorOutputStream compressor = 441 new BlockLZ4CompressorOutputStream(compressedByteStream)) { 442 Channels.newChannel(compressor).write(pageData); 443 // This is required to make sure the bytes are written to the output 444 compressor.finish(); 445 } catch (IOException impossible) { 446 throw new AssertionError(impossible); 447 } finally { 448 pageData.reset(); 449 } 450 451 byte[] compressedBytes = compressedByteStream.toByteArray(); 452 if (compressedBytes.length < pageData.remaining()) { 453 return ByteBuffer.wrap(compressedBytes); 454 } 455 return pageData; 456 } 457 458 static final class CompressedPendingBlock extends PendingBlock { 459 final ByteBuffer mPageData; 460 CompressedPendingBlock(PendingBlock block)461 CompressedPendingBlock(PendingBlock block) throws IOException { 462 super(block); 463 464 final ByteBuffer buffer = ByteBuffer.allocate(super.getBlockSize()); 465 super.readBlockData(buffer); 466 buffer.flip(); // switch to read mode 467 468 if (super.getType() == Type.APK_DATA) { 469 mPageData = maybeCompressPage(buffer); 470 } else { 471 mPageData = buffer; 472 } 473 } 474 getCompression()475 public Compression getCompression() { 476 return this.getBlockSize() < super.getBlockSize() ? Compression.LZ4 : Compression.NONE; 477 } 478 getBlockSize()479 public short getBlockSize() { 480 return (short) mPageData.remaining(); 481 } 482 readBlockData(ByteBuffer buffer)483 public void readBlockData(ByteBuffer buffer) throws IOException { 484 mPageData.mark(); 485 buffer.put(mPageData); 486 mPageData.reset(); 487 } 488 } 489 490 static final class CompressingBlockTransformer implements IBlockTransformer { 491 @Override 492 @NonNull transform(@onNull PendingBlock block)493 public PendingBlock transform(@NonNull PendingBlock block) throws IOException { 494 return new CompressedPendingBlock(block); 495 } 496 } 497 498 @LargeTest 499 @Test testInstallWithStreamingAndCompression()500 public void testInstallWithStreamingAndCompression() throws Exception { 501 final String apk = createApkPath(TEST_APK); 502 final String idsig = createApkPath(TEST_APK_IDSIG); 503 mSession = 504 new IncrementalInstallSession.Builder() 505 .addApk(Paths.get(apk), Paths.get(idsig)) 506 .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME) 507 .setLogger(new IncrementalDeviceConnection.Logger()) 508 .setBlockTransformer(new CompressingBlockTransformer()) 509 .build(); 510 getUiAutomation().adoptShellPermissionIdentity(); 511 try { 512 mSession.start(Executors.newSingleThreadExecutor(), 513 IncrementalDeviceConnection.Factory.reliable()); 514 mSession.waitForInstallCompleted(30, TimeUnit.SECONDS); 515 } finally { 516 getUiAutomation().dropShellPermissionIdentity(); 517 } 518 assertTrue(isAppInstalled(TEST_APP_PACKAGE)); 519 } 520 521 @LargeTest 522 @Test testInstallWithStreamingUnreliableConnection()523 public void testInstallWithStreamingUnreliableConnection() throws Exception { 524 final String apk = createApkPath(TEST_APK); 525 final String idsig = createApkPath(TEST_APK_IDSIG); 526 mSession = 527 new IncrementalInstallSession.Builder() 528 .addApk(Paths.get(apk), Paths.get(idsig)) 529 .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME) 530 .setLogger(new IncrementalDeviceConnection.Logger()) 531 .build(); 532 getUiAutomation().adoptShellPermissionIdentity(); 533 try { 534 mSession.start(Executors.newSingleThreadExecutor(), 535 IncrementalDeviceConnection.Factory.ureliable()); 536 mSession.waitForInstallCompleted(30, TimeUnit.SECONDS); 537 } catch (Exception ignored) { 538 // Ignore, we are looking for crashes anyway. 539 } finally { 540 getUiAutomation().dropShellPermissionIdentity(); 541 } 542 } 543 544 @Test testInstallWithIdSigInvalidLength()545 public void testInstallWithIdSigInvalidLength() throws Exception { 546 File file = new File(createApkPath(TEST_APK)); 547 Truth.assertThat( 548 executeShellCommand("pm install-incremental -t -g -S " + (file.length() - 1), 549 new File[]{file})).contains( 550 "Failure"); 551 assertFalse(isAppInstalled(TEST_APP_PACKAGE)); 552 } 553 554 @Test testInstallWithInvalidIdSig()555 public void testInstallWithInvalidIdSig() throws Exception { 556 File file = new File(createApkPath(TEST_APK_MALFORMED)); 557 Truth.assertThat( 558 executeShellCommand("pm install-incremental -t -g " + file.getPath())).contains( 559 "Failure"); 560 assertFalse(isAppInstalled(TEST_APP_PACKAGE)); 561 } 562 563 @LargeTest 564 @Test testInstallWithIdSigStreamIncompleteData()565 public void testInstallWithIdSigStreamIncompleteData() throws Exception { 566 File file = new File(createApkPath(TEST_APK)); 567 long length = file.length(); 568 // Streaming happens in blocks of 1024 bytes, new length will not stream the last block. 569 long newLength = length - (length % 1024 == 0 ? 1024 : length % 1024); 570 Truth.assertThat( 571 executeShellCommand( 572 "pm install-incremental -t -g -S " + length, 573 new File[]{file}, 574 new long[]{newLength})).contains("Failure"); 575 assertFalse(isAppInstalled(TEST_APP_PACKAGE)); 576 } 577 578 @LargeTest 579 @Test testInstallWithIdSigNoMissingPages()580 public void testInstallWithIdSigNoMissingPages() throws Exception { 581 final int installIterations = 1; 582 final int atraceDumpIterations = 3; 583 final int atraceDumpDelayMs = 1000; 584 final String missingPageReads = "|missing_page_reads: count="; 585 586 final ArrayList<String> missingPages = new ArrayList<>(); 587 588 checkSysTrace( 589 installIterations, 590 atraceDumpIterations, 591 atraceDumpDelayMs, 592 () -> { 593 // Install multiple splits so that digesters won't kick in. 594 installPackage(TEST_APK); 595 installSplit(TEST_APK_SPLIT0); 596 installSplit(TEST_APK_SPLIT1); 597 installSplit(TEST_APK_SPLIT2); 598 // Now read it as fast as we can. 599 readSplitInChunks("base.apk"); 600 readSplitInChunks("split_config.mdpi.apk"); 601 readSplitInChunks("split_config.hdpi.apk"); 602 readSplitInChunks("split_config.xhdpi.apk"); 603 return null; 604 }, 605 (stdout) -> { 606 try (Scanner scanner = new Scanner(stdout)) { 607 ReadLogEntry prevLogEntry = null; 608 while (scanner.hasNextLine()) { 609 final String line = scanner.nextLine(); 610 611 final ReadLogEntry readLogEntry = ReadLogEntry.parse(line); 612 if (readLogEntry != null) { 613 prevLogEntry = readLogEntry; 614 continue; 615 } 616 617 int missingPageIdx = line.indexOf(missingPageReads); 618 if (missingPageIdx == -1) { 619 continue; 620 } 621 String missingBlocks = line.substring( 622 missingPageIdx + missingPageReads.length()); 623 624 int prvTimestamp = prevLogEntry != null ? extractTimestamp( 625 prevLogEntry.line) : -1; 626 int curTimestamp = extractTimestamp(line); 627 if (prvTimestamp == -1 || curTimestamp == -1) { 628 missingPages.add("count=" + missingBlocks); 629 continue; 630 } 631 632 int delta = curTimestamp - prvTimestamp; 633 missingPages.add( 634 "count=" + missingBlocks + ", timestamp delta=" + delta + "ms"); 635 } 636 return false; 637 } 638 }); 639 640 assertTrue("Missing page reads found in atrace dump: " + String.join("\n", missingPages), 641 missingPages.isEmpty()); 642 } 643 644 static class ReadLogEntry { 645 public final String line; 646 public final int blockIdx; 647 public final int count; 648 public final int fileIdx; 649 public final int appId; 650 public final int userId; 651 ReadLogEntry(String line, int blockIdx, int count, int fileIdx, int appId, int userId)652 private ReadLogEntry(String line, int blockIdx, int count, int fileIdx, int appId, 653 int userId) { 654 this.line = line; 655 this.blockIdx = blockIdx; 656 this.count = count; 657 this.fileIdx = fileIdx; 658 this.appId = appId; 659 this.userId = userId; 660 } 661 toString()662 public String toString() { 663 return blockIdx + "/" + count + "/" + fileIdx + "/" + appId + "/" + userId; 664 } 665 666 static final String BLOCK_PREFIX = "|page_read: index="; 667 static final String COUNT_PREFIX = " count="; 668 static final String FILE_PREFIX = " file="; 669 static final String APP_ID_PREFIX = " appid="; 670 static final String USER_ID_PREFIX = " userid="; 671 parseInt(String line, int prefixIdx, int prefixLen, int endIdx)672 private static int parseInt(String line, int prefixIdx, int prefixLen, int endIdx) { 673 if (prefixIdx == -1) { 674 return -1; 675 } 676 final String intStr; 677 if (endIdx != -1) { 678 intStr = line.substring(prefixIdx + prefixLen, endIdx); 679 } else { 680 intStr = line.substring(prefixIdx + prefixLen); 681 } 682 683 return Integer.parseInt(intStr); 684 } 685 parse(String line)686 static ReadLogEntry parse(String line) { 687 int blockIdx = line.indexOf(BLOCK_PREFIX); 688 if (blockIdx == -1) { 689 return null; 690 } 691 int countIdx = line.indexOf(COUNT_PREFIX, blockIdx + BLOCK_PREFIX.length()); 692 if (countIdx == -1) { 693 return null; 694 } 695 int fileIdx = line.indexOf(FILE_PREFIX, countIdx + COUNT_PREFIX.length()); 696 if (fileIdx == -1) { 697 return null; 698 } 699 int appIdIdx = line.indexOf(APP_ID_PREFIX, fileIdx + FILE_PREFIX.length()); 700 final int userIdIdx; 701 if (appIdIdx != -1) { 702 userIdIdx = line.indexOf(USER_ID_PREFIX, appIdIdx + APP_ID_PREFIX.length()); 703 } else { 704 userIdIdx = -1; 705 } 706 707 return new ReadLogEntry( 708 line, 709 parseInt(line, blockIdx, BLOCK_PREFIX.length(), countIdx), 710 parseInt(line, countIdx, COUNT_PREFIX.length(), fileIdx), 711 parseInt(line, fileIdx, FILE_PREFIX.length(), appIdIdx), 712 parseInt(line, appIdIdx, APP_ID_PREFIX.length(), userIdIdx), 713 parseInt(line, userIdIdx, USER_ID_PREFIX.length(), -1)); 714 } 715 } 716 717 @Test testReadLogParser()718 public void testReadLogParser() throws Exception { 719 assertEquals(null, ReadLogEntry.parse("# tracer: nop\n")); 720 assertEquals( 721 "178/290/0/10184/0", 722 ReadLogEntry.parse( 723 "<...>-2777 ( 1639) [006] .... 2764.227110: tracing_mark_write: " 724 + "B|1639|page_read: index=178 count=290 file=0 appid=10184 " 725 + "userid=0") 726 .toString()); 727 assertEquals( 728 null, 729 ReadLogEntry.parse( 730 "<...>-2777 ( 1639) [006] .... 2764.227111: tracing_mark_write: E|1639")); 731 assertEquals( 732 "468/337/0/10184/2", 733 ReadLogEntry.parse( 734 "<...>-2777 ( 1639) [006] .... 2764.243227: tracing_mark_write: " 735 + "B|1639|page_read: index=468 count=337 file=0 appid=10184 " 736 + "userid=2") 737 .toString()); 738 assertEquals( 739 null, 740 ReadLogEntry.parse( 741 "<...>-2777 ( 1639) [006] .... 2764.243229: tracing_mark_write: E|1639")); 742 assertEquals( 743 "18/9/3/-1/-1", 744 ReadLogEntry.parse( 745 " <...>-2777 ( 1639) [006] .... 2764.227095: " 746 + "tracing_mark_write: B|1639|page_read: index=18 count=9 file=3") 747 .toString()); 748 } 749 extractTimestamp(String line)750 static int extractTimestamp(String line) { 751 final String timestampEnd = ": tracing_mark_write:"; 752 int timestampEndIdx = line.indexOf(timestampEnd); 753 if (timestampEndIdx == -1) { 754 return -1; 755 } 756 757 int timestampBegIdx = timestampEndIdx - 1; 758 for (; timestampBegIdx >= 0; --timestampBegIdx) { 759 char ch = line.charAt(timestampBegIdx); 760 if ('0' <= ch && ch <= '9' || ch == '.') { 761 continue; 762 } 763 break; 764 } 765 double timestamp = Double.parseDouble(line.substring(timestampBegIdx, timestampEndIdx)); 766 return (int) (timestamp * 1000); 767 } 768 769 @Test testExtractTimestamp()770 public void testExtractTimestamp() throws Exception { 771 assertEquals(-1, extractTimestamp("# tracer: nop\n")); 772 assertEquals(14255168, extractTimestamp( 773 "<...>-10355 ( 1636) [006] .... 14255.168694: tracing_mark_write: " 774 + "B|1636|page_read: index=1 count=16 file=0 appid=10184 userid=0")); 775 assertEquals(2764243, extractTimestamp( 776 "<...>-2777 ( 1639) [006] .... 2764.243225: tracing_mark_write: " 777 + "B|1639|missing_page_reads: count=132")); 778 assertEquals(114176, extractTimestamp( 779 "DataLoaderManag-8339 ( 1780) [004] .... 114.176342: tracing_mark_write: " 780 + "B|1780|page_read: index=1846 count=21 file=0 appid=10151 userid=0")); 781 } 782 static class AppReads { 783 public final String packageName; 784 public final int reads; 785 AppReads(String packageName, int reads)786 AppReads(String packageName, int reads) { 787 this.packageName = packageName; 788 this.reads = reads; 789 } 790 } 791 792 @LargeTest 793 @Test testInstallWithIdSigNoDigesting()794 public void testInstallWithIdSigNoDigesting() throws Exception { 795 // Overall timeout of 3secs in 100ms intervals. 796 final int installIterations = 1; 797 final int atraceDumpIterations = 30; 798 final int atraceDumpDelayMs = 100; 799 final int blockSize = 4096; 800 801 final String[] apks = 802 new String[]{TEST_HW7, TEST_HW7_SPLIT0, TEST_HW7_SPLIT1, TEST_HW7_SPLIT2, 803 TEST_HW7_SPLIT3, TEST_HW7_SPLIT4}; 804 final boolean[][] touched = new boolean[apks.length][]; 805 final int[] blocks = new int[apks.length]; 806 final AtomicLong[] totalTouchedBlocks = new AtomicLong[apks.length]; 807 for (int i = 0, size = apks.length; i < size; ++i) { 808 final String apkName = apks[i]; 809 final File apkfile = new File(createApkPath(apkName)); 810 blocks[i] = (int) ((apkfile.length() + blockSize - 1) / blockSize); 811 touched[i] = new boolean[blocks[i]]; 812 totalTouchedBlocks[i] = new AtomicLong(0); 813 } 814 815 final ArrayMap<Integer, Integer> uids = new ArrayMap<>(); 816 817 checkSysTrace( 818 installIterations, 819 atraceDumpIterations, 820 atraceDumpDelayMs, 821 () -> { 822 mSession = 823 new IncrementalInstallSession.Builder() 824 .addApk(Paths.get(createApkPath(TEST_HW7)), 825 Paths.get(createApkPath(TEST_HW7_IDSIG))) 826 .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT0)), 827 Paths.get(createApkPath(TEST_HW7_SPLIT0_IDSIG))) 828 .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT1)), 829 Paths.get(createApkPath(TEST_HW7_SPLIT1_IDSIG))) 830 .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT2)), 831 Paths.get(createApkPath(TEST_HW7_SPLIT2_IDSIG))) 832 .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT3)), 833 Paths.get(createApkPath(TEST_HW7_SPLIT3_IDSIG))) 834 .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT4)), 835 Paths.get(createApkPath(TEST_HW7_SPLIT4_IDSIG))) 836 .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME, 837 "--skip-verification") 838 .setLogger(new IncrementalDeviceConnection.Logger()) 839 .build(); 840 getUiAutomation().adoptShellPermissionIdentity(); 841 try { 842 mSession.start(Executors.newSingleThreadExecutor(), 843 IncrementalDeviceConnection.Factory.reliable()); 844 mSession.waitForInstallCompleted(30, TimeUnit.SECONDS); 845 assertEquals( 846 "base, config.hdpi, config.mdpi, config.xhdpi, config.xxhdpi, " 847 + "config.xxxhdpi", 848 getSplits(TEST_APP_PACKAGE)); 849 } finally { 850 getUiAutomation().dropShellPermissionIdentity(); 851 } 852 return null; 853 }, 854 (stdout) -> { 855 try (Scanner scanner = new Scanner(stdout)) { 856 while (scanner.hasNextLine()) { 857 String line = scanner.nextLine(); 858 final ReadLogEntry readLogEntry = ReadLogEntry.parse(line); 859 if (readLogEntry == null) { 860 continue; 861 } 862 int fileIdx = readLogEntry.fileIdx; 863 for (int i = 0, count = readLogEntry.count; i < count; ++i) { 864 int blockIdx = readLogEntry.blockIdx + i; 865 if (touched[fileIdx][blockIdx]) { 866 continue; 867 } 868 869 touched[fileIdx][blockIdx] = true; 870 871 int uid = UserHandle.getUid(readLogEntry.userId, 872 readLogEntry.appId); 873 Integer touchedByUid = uids.get(uid); 874 uids.put(uid, touchedByUid == null ? 1 : touchedByUid + 1); 875 876 long totalTouched = totalTouchedBlocks[fileIdx].incrementAndGet(); 877 if (totalTouched >= blocks[fileIdx]) { 878 return true; 879 } 880 } 881 } 882 return false; 883 } 884 }); 885 886 int firstFileIdx = CHECK_BASE_APK_DIGESTION ? 0 : 1; 887 888 boolean found = false; 889 for (int i = firstFileIdx, size = blocks.length; i < size; ++i) { 890 if (totalTouchedBlocks[i].get() >= blocks[i]) { 891 found = true; 892 break; 893 } 894 } 895 if (!found) { 896 return; 897 } 898 899 PackageManager pm = getPackageManager(); 900 901 AppReads[] appIdReads = new AppReads[uids.size()]; 902 for (int i = 0, size = uids.size(); i < size; ++i) { 903 final int uid = uids.keyAt(i); 904 final int appId = UserHandle.getAppId(uid); 905 final int userId = UserHandle.getUserId(uid); 906 907 final String packageName; 908 if (appId < Process.FIRST_APPLICATION_UID) { 909 packageName = "<system>"; 910 } else { 911 String[] packages = pm.getPackagesForUid(uid); 912 if (packages == null || packages.length == 0) { 913 packageName = "<unknown package, appId=" + appId + ", userId=" + userId + ">"; 914 } else { 915 packageName = "[" + String.join(",", packages) + "]"; 916 } 917 } 918 appIdReads[i] = new AppReads(packageName, uids.valueAt(i)); 919 } 920 Arrays.sort(appIdReads, (lhs, rhs) -> Integer.compare(rhs.reads, lhs.reads)); 921 922 final String packages = String.join("\n", Arrays.stream(appIdReads).map( 923 item -> item.packageName + " : " + item.reads + " blocks").toArray(String[]::new)); 924 fail("Digesting detected, list of packages: " + packages); 925 } 926 927 @LargeTest 928 @Test testInstallWithIdSigPerUidTimeouts()929 public void testInstallWithIdSigPerUidTimeouts() throws Exception { 930 executeShellCommand("atrace --async_start -b 1024 -c adb"); 931 try { 932 setDeviceProperty("incfs_default_timeouts", "5000000:5000000:5000000"); 933 setDeviceProperty("known_digesters_list", CTS_PACKAGE_NAME); 934 935 installPackage(TEST_APK); 936 assertTrue(isAppInstalled(TEST_APP_PACKAGE)); 937 } finally { 938 executeShellCommand("atrace --async_stop"); 939 } 940 } 941 942 @LargeTest 943 @Test testInstallWithIdSigStreamPerUidTimeoutsIncompleteData()944 public void testInstallWithIdSigStreamPerUidTimeoutsIncompleteData() throws Exception { 945 // To disable verification. 946 installNonIncremental(TEST_APK); 947 948 checkIncrementalDeliveryV2Feature(); 949 950 mSession = 951 new IncrementalInstallSession.Builder() 952 .addApk(Paths.get(createApkPath(TEST_HW7)), 953 Paths.get(createApkPath(TEST_HW7_IDSIG))) 954 .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT0)), 955 Paths.get(createApkPath(TEST_HW7_SPLIT0_IDSIG))) 956 .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT1)), 957 Paths.get(createApkPath(TEST_HW7_SPLIT1_IDSIG))) 958 .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT2)), 959 Paths.get(createApkPath(TEST_HW7_SPLIT2_IDSIG))) 960 .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT3)), 961 Paths.get(createApkPath(TEST_HW7_SPLIT3_IDSIG))) 962 .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT4)), 963 Paths.get(createApkPath(TEST_HW7_SPLIT4_IDSIG))) 964 .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME, "--skip-verification") 965 .setLogger(new IncrementalDeviceConnection.Logger()) 966 .build(); 967 968 executeShellCommand("atrace --async_start -b 10240 -c adb"); 969 try { 970 setDeviceProperty("incfs_default_timeouts", "20000000:20000000:20000000"); 971 setDeviceProperty("known_digesters_list", CTS_PACKAGE_NAME); 972 973 final int beforeReadDelayMs = 1000; 974 Thread.currentThread().sleep(beforeReadDelayMs); 975 976 // Partially install the apk+split0/1/2/3/4. 977 getUiAutomation().adoptShellPermissionIdentity(); 978 try { 979 mSession.start(Executors.newSingleThreadExecutor(), 980 IncrementalDeviceConnection.Factory.reliable()); 981 mSession.waitForInstallCompleted(30, TimeUnit.SECONDS); 982 assertEquals( 983 "base, config.hdpi, config.mdpi, config.xhdpi, config.xxhdpi, config" 984 + ".xxxhdpi", 985 getSplits(TEST_APP_PACKAGE)); 986 } finally { 987 getUiAutomation().dropShellPermissionIdentity(); 988 } 989 990 final String packagePath = getCodePath(TEST_APP_PACKAGE); 991 992 // Try to read splits and see if we are throttled at least once. 993 long maxReadTime = 0; 994 for (String splitName : new String[]{"split_config.hdpi.apk", "split_config.mdpi.apk", 995 "split_config.xhdpi.apk", "split_config.xxxhdpi.apk", 996 "split_config.xxxhdpi.apk"}) { 997 final File apkToRead = new File(packagePath, splitName); 998 final long readTime0 = readAndReportTime(apkToRead, 1000); 999 1000 if (readTime0 < EXPECTED_READ_TIME) { 1001 executeShellCommand("atrace --async_dump"); 1002 } 1003 maxReadTime = Math.max(maxReadTime, readTime0); 1004 if (maxReadTime >= EXPECTED_READ_TIME) { 1005 break; 1006 } 1007 } 1008 assertTrue("Must take longer than " + EXPECTED_READ_TIME + "ms: time0=" + maxReadTime 1009 + "ms", maxReadTime >= EXPECTED_READ_TIME); 1010 } finally { 1011 executeShellCommand("atrace --async_stop"); 1012 } 1013 } 1014 1015 @LargeTest 1016 @Test testInstallWithIdSigPerUidTimeoutsIgnored()1017 public void testInstallWithIdSigPerUidTimeoutsIgnored() throws Exception { 1018 // Timeouts would be ignored as there are no readlogs collected. 1019 final int beforeReadDelayMs = 5000; 1020 setDeviceProperty("incfs_default_timeouts", "5000000:5000000:5000000"); 1021 setDeviceProperty("known_digesters_list", CTS_PACKAGE_NAME); 1022 1023 // First fully install the apk and a split0. 1024 { 1025 installPackage(TEST_APK); 1026 assertTrue(isAppInstalled(TEST_APP_PACKAGE)); 1027 installSplit(TEST_APK_SPLIT0); 1028 assertEquals("base, config.mdpi", getSplits(TEST_APP_PACKAGE)); 1029 installSplit(TEST_APK_SPLIT1); 1030 assertEquals("base, config.hdpi, config.mdpi", getSplits(TEST_APP_PACKAGE)); 1031 } 1032 1033 // Allow IncrementalService to update the timeouts after full download. 1034 Thread.currentThread().sleep(beforeReadDelayMs); 1035 1036 // Try to read a split and see if we are throttled. 1037 final long readTime = readAndReportTime(getSplit("split_config.mdpi.apk"), 1000); 1038 assertTrue("Must take less than " + EXPECTED_READ_TIME + "ms vs " + readTime + "ms", 1039 readTime < EXPECTED_READ_TIME); 1040 } 1041 1042 @Test testInstallWithIdSigStreamIncompleteDataForSplit()1043 public void testInstallWithIdSigStreamIncompleteDataForSplit() throws Exception { 1044 File apkfile = new File(createApkPath(TEST_APK)); 1045 File splitfile = new File(createApkPath(TEST_APK_SPLIT0)); 1046 long splitLength = splitfile.length(); 1047 // Don't fully stream the split. 1048 long newSplitLength = splitLength - (splitLength % 1024 == 0 ? 1024 : splitLength % 1024); 1049 File[] files = new File[]{apkfile, splitfile}; 1050 String param = Arrays.stream(files).map( 1051 file -> file.getName() + ":" + file.length()).collect(Collectors.joining(" ")); 1052 Truth.assertThat(executeShellCommand( 1053 String.format("pm install-incremental -t -g -S %s %s", 1054 (apkfile.length() + splitfile.length()), param), 1055 files, new long[]{apkfile.length(), newSplitLength})).contains("Failure"); 1056 assertFalse(isAppInstalled(TEST_APP_PACKAGE)); 1057 } 1058 1059 static class TestDataLoaderService extends DataLoaderService { 1060 } 1061 1062 @Test testDataLoaderServiceDefaultImplementation()1063 public void testDataLoaderServiceDefaultImplementation() { 1064 DataLoaderService service = new TestDataLoaderService(); 1065 assertEquals(null, service.onCreateDataLoader(null)); 1066 IBinder binder = service.onBind(null); 1067 assertNotEquals(null, binder); 1068 assertEquals(binder, service.onBind(new Intent())); 1069 } 1070 1071 @LargeTest 1072 @Test testInstallSysTraceDebuggable()1073 public void testInstallSysTraceDebuggable() throws Exception { 1074 doTestInstallSysTrace(TEST_APK); 1075 } 1076 1077 @LargeTest 1078 @Test testInstallSysTraceProfileable()1079 public void testInstallSysTraceProfileable() throws Exception { 1080 doTestInstallSysTrace(TEST_APK_PROFILEABLE); 1081 } 1082 1083 @LargeTest 1084 @Test testInstallSysTraceNoReadlogs()1085 public void testInstallSysTraceNoReadlogs() throws Exception { 1086 setSystemProperty("debug.incremental.enforce_readlogs_max_interval_for_system_dataloaders", 1087 "1"); 1088 setSystemProperty("debug.incremental.readlogs_max_interval_sec", "0"); 1089 1090 final int atraceDumpIterations = 30; 1091 final int atraceDumpDelayMs = 100; 1092 final String expected = "|page_read:"; 1093 1094 // We don't expect any readlogs with 0sec interval. 1095 assertFalse( 1096 "Page reads (" + expected + ") were found in atrace dump", 1097 checkSysTraceForSubstring(TEST_APK, expected, atraceDumpIterations, 1098 atraceDumpDelayMs)); 1099 } 1100 checkSysTraceForSubstring(String testApk, final String expected, int atraceDumpIterations, int atraceDumpDelayMs)1101 private boolean checkSysTraceForSubstring(String testApk, final String expected, 1102 int atraceDumpIterations, int atraceDumpDelayMs) throws Exception { 1103 final int installIterations = 3; 1104 return checkSysTrace( 1105 installIterations, 1106 atraceDumpIterations, 1107 atraceDumpDelayMs, 1108 () -> installPackage(testApk), 1109 (stdout) -> stdout.contains(expected)); 1110 } 1111 checkSysTrace( int installIterations, int atraceDumpIterations, int atraceDumpDelayMs, final Callable<Void> installer, final Function<String, Boolean> checker)1112 private boolean checkSysTrace( 1113 int installIterations, 1114 int atraceDumpIterations, 1115 int atraceDumpDelayMs, 1116 final Callable<Void> installer, 1117 final Function<String, Boolean> checker) 1118 throws Exception { 1119 final int beforeReadDelayMs = 1000; 1120 1121 final CompletableFuture<Boolean> result = new CompletableFuture<>(); 1122 final Thread readFromProcess = new Thread(() -> { 1123 try { 1124 executeShellCommand("atrace --async_start -b 10240 -c adb"); 1125 try { 1126 for (int i = 0; i < atraceDumpIterations; ++i) { 1127 final String stdout = executeShellCommand("atrace --async_dump"); 1128 try { 1129 if (checker.apply(stdout)) { 1130 result.complete(true); 1131 break; 1132 } 1133 Thread.currentThread().sleep(atraceDumpDelayMs); 1134 } catch (InterruptedException ignored) { 1135 } 1136 } 1137 } finally { 1138 executeShellCommand("atrace --async_stop"); 1139 } 1140 } catch (IOException ignored) { 1141 } 1142 }); 1143 readFromProcess.start(); 1144 1145 for (int i = 0; i < installIterations && !result.isDone(); ++i) { 1146 installer.call(); 1147 assertTrue(isAppInstalled(TEST_APP_PACKAGE)); 1148 Thread.currentThread().sleep(beforeReadDelayMs); 1149 uninstallPackageSilently(TEST_APP_PACKAGE); 1150 } 1151 1152 readFromProcess.join(); 1153 return result.getNow(false); 1154 } 1155 doTestInstallSysTrace(String testApk)1156 private void doTestInstallSysTrace(String testApk) throws Exception { 1157 // Async atrace dump uses less resources but requires periodic pulls. 1158 // Overall timeout of 10secs in 100ms intervals should be enough. 1159 final int atraceDumpIterations = 100; 1160 final int atraceDumpDelayMs = 100; 1161 final String expected = "|page_read:"; 1162 1163 assertTrue( 1164 "No page reads (" + expected + ") found in atrace dump", 1165 checkSysTraceForSubstring(testApk, expected, atraceDumpIterations, 1166 atraceDumpDelayMs)); 1167 } 1168 isAppInstalled(String packageName)1169 static boolean isAppInstalled(String packageName) throws IOException { 1170 return isAppInstalledForUser(packageName, -1); 1171 } 1172 isAppInstalledForUser(String packageName, int userId)1173 static boolean isAppInstalledForUser(String packageName, int userId) throws IOException { 1174 final String command = userId < 0 ? "pm list packages " + packageName : 1175 "pm list packages --user " + userId + " " + packageName; 1176 final String commandResult = executeShellCommand(command); 1177 return Arrays.stream(commandResult.split("\\r?\\n")) 1178 .anyMatch(line -> line.equals("package:" + packageName)); 1179 } 1180 getSplits(String packageName)1181 private String getSplits(String packageName) throws IOException { 1182 final String result = parsePackageDump(packageName, " splits=["); 1183 if (TextUtils.isEmpty(result)) { 1184 return null; 1185 } 1186 return result.substring(0, result.length() - 1); 1187 } 1188 getCodePath(String packageName)1189 private String getCodePath(String packageName) throws IOException { 1190 return parsePackageDump(packageName, " codePath="); 1191 } 1192 getSplit(String splitName)1193 private File getSplit(String splitName) throws Exception { 1194 return new File(getCodePath(TEST_APP_PACKAGE), splitName); 1195 } 1196 parsePackageDump(String packageName, String prefix)1197 static String parsePackageDump(String packageName, String prefix) throws IOException { 1198 final String commandResult = executeShellCommand("pm dump-package " + packageName); 1199 final int prefixLength = prefix.length(); 1200 Optional<String> maybeSplits = Arrays.stream(commandResult.split("\\r?\\n")) 1201 .filter(line -> line.startsWith(prefix)).findFirst(); 1202 if (!maybeSplits.isPresent()) { 1203 return null; 1204 } 1205 String splits = maybeSplits.get(); 1206 return splits.substring(prefixLength); 1207 } 1208 createApkPath(String baseName)1209 private static String createApkPath(String baseName) { 1210 return TEST_APK_PATH + baseName; 1211 } 1212 installNonIncremental(String baseName)1213 static void installNonIncremental(String baseName) throws IOException { 1214 File file = new File(createApkPath(baseName)); 1215 assertEquals("Success\n", 1216 executeShellCommand("pm install -t -g " + file.getPath())); 1217 } 1218 installPackage(String baseName)1219 static Void installPackage(String baseName) throws IOException { 1220 File file = new File(createApkPath(baseName)); 1221 assertEquals("Success\n", 1222 executeShellCommand("pm install-incremental -t -g " + file.getPath())); 1223 return null; 1224 } 1225 installSplit(String splitName)1226 private void installSplit(String splitName) throws Exception { 1227 final File splitfile = new File(createApkPath(splitName)); 1228 1229 try (InputStream inputStream = executeShellCommandStream( 1230 "pm install-incremental -t -g -p " + TEST_APP_PACKAGE + " " 1231 + splitfile.getPath())) { 1232 assertEquals("Success\n", readFullStream(inputStream)); 1233 } 1234 } 1235 readSplitInChunks(String splitName)1236 private void readSplitInChunks(String splitName) throws Exception { 1237 final int chunks = 2; 1238 final int waitBetweenChunksMs = 100; 1239 final File file = getSplit(splitName); 1240 1241 assertTrue(file.toString(), file.exists()); 1242 final long totalSize = file.length(); 1243 final long chunkSize = totalSize / chunks; 1244 try (InputStream baseApkStream = new FileInputStream(file)) { 1245 final byte[] buffer = new byte[4 * 1024]; 1246 long readSoFar = 0; 1247 long maxToRead = 0; 1248 for (int i = 0; i < chunks; ++i) { 1249 maxToRead += chunkSize; 1250 int length; 1251 while ((length = baseApkStream.read(buffer)) != -1) { 1252 readSoFar += length; 1253 if (readSoFar >= maxToRead) { 1254 break; 1255 } 1256 } 1257 if (readSoFar < totalSize) { 1258 Thread.currentThread().sleep(waitBetweenChunksMs); 1259 } 1260 } 1261 } 1262 } 1263 readAndReportTime(File file, long borderTime)1264 private long readAndReportTime(File file, long borderTime) throws Exception { 1265 final long startTime = SystemClock.uptimeMillis(); 1266 assertTrue(file.toString(), file.exists()); 1267 try (InputStream baseApkStream = new FileInputStream(file)) { 1268 final byte[] buffer = new byte[128 * 1024]; 1269 while (baseApkStream.read(buffer) != -1) { 1270 long readTime = SystemClock.uptimeMillis() - startTime; 1271 if (readTime >= borderTime) { 1272 break; 1273 } 1274 } 1275 } 1276 return SystemClock.uptimeMillis() - startTime; 1277 } 1278 uninstallPackageSilently(String packageName)1279 static String uninstallPackageSilently(String packageName) throws IOException { 1280 return executeShellCommand("pm uninstall " + packageName); 1281 } 1282 1283 interface Result { await()1284 boolean await() throws Exception; 1285 } 1286 executeShellCommand(String command)1287 static String executeShellCommand(String command) throws IOException { 1288 try (InputStream inputStream = executeShellCommandStream(command)) { 1289 return readFullStream(inputStream); 1290 } 1291 } 1292 executeShellCommandStream(String command)1293 private static InputStream executeShellCommandStream(String command) throws IOException { 1294 final ParcelFileDescriptor stdout = getUiAutomation().executeShellCommand(command); 1295 return new ParcelFileDescriptor.AutoCloseInputStream(stdout); 1296 } 1297 executeShellCommand(String command, File[] inputs)1298 private static String executeShellCommand(String command, File[] inputs) 1299 throws IOException { 1300 return executeShellCommand(command, inputs, Stream.of(inputs).mapToLong( 1301 File::length).toArray()); 1302 } 1303 executeShellCommand(String command, File[] inputs, long[] expected)1304 private static String executeShellCommand(String command, File[] inputs, long[] expected) 1305 throws IOException { 1306 try (InputStream inputStream = executeShellCommandRw(command, inputs, expected)) { 1307 return readFullStream(inputStream); 1308 } 1309 } 1310 executeShellCommandRw(String command, File[] inputs, long[] expected)1311 private static InputStream executeShellCommandRw(String command, File[] inputs, long[] expected) 1312 throws IOException { 1313 assertEquals(inputs.length, expected.length); 1314 final ParcelFileDescriptor[] pfds = 1315 InstrumentationRegistry.getInstrumentation().getUiAutomation() 1316 .executeShellCommandRw(command); 1317 ParcelFileDescriptor stdout = pfds[0]; 1318 ParcelFileDescriptor stdin = pfds[1]; 1319 try (FileOutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream( 1320 stdin)) { 1321 for (int i = 0; i < inputs.length; i++) { 1322 try (FileInputStream inputStream = new FileInputStream(inputs[i])) { 1323 writeFullStream(inputStream, outputStream, expected[i]); 1324 } 1325 } 1326 } 1327 return new ParcelFileDescriptor.AutoCloseInputStream(stdout); 1328 } 1329 readFullStream(InputStream inputStream, long expected)1330 static String readFullStream(InputStream inputStream, long expected) 1331 throws IOException { 1332 ByteArrayOutputStream result = new ByteArrayOutputStream(); 1333 writeFullStream(inputStream, result, expected); 1334 return result.toString("UTF-8"); 1335 } 1336 readFullStream(InputStream inputStream)1337 static String readFullStream(InputStream inputStream) throws IOException { 1338 return readFullStream(inputStream, -1); 1339 } 1340 writeFullStream(InputStream inputStream, OutputStream outputStream, long expected)1341 static void writeFullStream(InputStream inputStream, OutputStream outputStream, 1342 long expected) 1343 throws IOException { 1344 final byte[] buffer = new byte[1024]; 1345 long total = 0; 1346 int length; 1347 while ((length = inputStream.read(buffer)) != -1 && (expected < 0 || total < expected)) { 1348 outputStream.write(buffer, 0, length); 1349 total += length; 1350 } 1351 if (expected > 0) { 1352 assertEquals(expected, total); 1353 } 1354 } 1355 setDeviceProperty(String name, String value)1356 static void setDeviceProperty(String name, String value) { 1357 getUiAutomation().adoptShellPermissionIdentity(WRITE_ALLOWLISTED_DEVICE_CONFIG); 1358 try { 1359 DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE, name, value, 1360 false); 1361 } finally { 1362 getUiAutomation().dropShellPermissionIdentity(); 1363 } 1364 } 1365 setSystemProperty(String name, String value)1366 static void setSystemProperty(String name, String value) throws Exception { 1367 executeShellCommand("setprop " + name + " " + value); 1368 } 1369 1370 } 1371 1372