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