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