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