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