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