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