| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.content.pm.cts; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotEquals; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| import android.annotation.NonNull; |
| import android.app.UiAutomation; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.os.IBinder; |
| import android.os.ParcelFileDescriptor; |
| import android.os.Process; |
| import android.os.SystemClock; |
| import android.os.UserHandle; |
| import android.platform.test.annotations.AppModeFull; |
| import android.platform.test.annotations.Presubmit; |
| import android.provider.DeviceConfig; |
| import android.service.dataloader.DataLoaderService; |
| import android.system.Os; |
| import android.text.TextUtils; |
| import android.util.ArrayMap; |
| import android.util.Log; |
| |
| import androidx.test.InstrumentationRegistry; |
| import androidx.test.filters.LargeTest; |
| import androidx.test.runner.AndroidJUnit4; |
| |
| import com.android.compatibility.common.util.PropertyUtil; |
| import com.android.incfs.install.IBlockFilter; |
| import com.android.incfs.install.IBlockTransformer; |
| import com.android.incfs.install.IncrementalInstallSession; |
| import com.android.incfs.install.PendingBlock; |
| |
| import com.google.common.truth.Truth; |
| |
| import libcore.io.IoUtils; |
| |
| import org.apache.commons.compress.compressors.lz4.BlockLZ4CompressorOutputStream; |
| import org.junit.After; |
| import org.junit.Assert; |
| import org.junit.Assume; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.nio.ByteBuffer; |
| import java.nio.channels.Channels; |
| import java.nio.file.Paths; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Optional; |
| import java.util.Random; |
| import java.util.Scanner; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.CompletableFuture; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicLong; |
| import java.util.function.Function; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| |
| @RunWith(AndroidJUnit4.class) |
| @AppModeFull |
| @LargeTest |
| @Presubmit |
| public class PackageManagerShellCommandIncrementalTest { |
| private static final String TAG = "PackageManagerShellCommandIncrementalTest"; |
| |
| private static final String CTS_PACKAGE_NAME = "android.content.cts"; |
| private static final String TEST_APP_PACKAGE = "com.example.helloworld"; |
| |
| private static final String TEST_APK_PATH = "/data/local/tmp/cts/content/"; |
| private static final String TEST_APK = "HelloWorld5.apk"; |
| private static final String TEST_APK_IDSIG = "HelloWorld5.apk.idsig"; |
| private static final String TEST_APK_PROFILEABLE = "HelloWorld5Profileable.apk"; |
| private static final String TEST_APK_SHELL = "HelloWorldShell.apk"; |
| private static final String TEST_APK_SPLIT0 = "HelloWorld5_mdpi-v4.apk"; |
| private static final String TEST_APK_SPLIT0_IDSIG = "HelloWorld5_mdpi-v4.apk.idsig"; |
| private static final String TEST_APK_SPLIT1 = "HelloWorld5_hdpi-v4.apk"; |
| private static final String TEST_APK_SPLIT1_IDSIG = "HelloWorld5_hdpi-v4.apk.idsig"; |
| private static final String TEST_APK_SPLIT2 = "HelloWorld5_xhdpi-v4.apk"; |
| private static final String TEST_APK_SPLIT2_IDSIG = "HelloWorld5_xhdpi-v4.apk.idsig"; |
| private static final String TEST_APK_MALFORMED = "malformed.apk"; |
| |
| private static final String TEST_HW7 = "HelloWorld7.apk"; |
| private static final String TEST_HW7_IDSIG = "HelloWorld7.apk.idsig"; |
| private static final String TEST_HW7_SPLIT0 = "HelloWorld7_hdpi-v4.apk"; |
| private static final String TEST_HW7_SPLIT0_IDSIG = "HelloWorld7_hdpi-v4.apk.idsig"; |
| private static final String TEST_HW7_SPLIT1 = "HelloWorld7_mdpi-v4.apk"; |
| private static final String TEST_HW7_SPLIT1_IDSIG = "HelloWorld7_mdpi-v4.apk.idsig"; |
| private static final String TEST_HW7_SPLIT2 = "HelloWorld7_xhdpi-v4.apk"; |
| private static final String TEST_HW7_SPLIT2_IDSIG = "HelloWorld7_xhdpi-v4.apk.idsig"; |
| private static final String TEST_HW7_SPLIT3 = "HelloWorld7_xxhdpi-v4.apk"; |
| private static final String TEST_HW7_SPLIT3_IDSIG = "HelloWorld7_xxhdpi-v4.apk.idsig"; |
| private static final String TEST_HW7_SPLIT4 = "HelloWorld7_xxxhdpi-v4.apk"; |
| private static final String TEST_HW7_SPLIT4_IDSIG = "HelloWorld7_xxxhdpi-v4.apk.idsig"; |
| |
| private static final boolean CHECK_BASE_APK_DIGESTION = false; |
| |
| private static final long EXPECTED_READ_TIME = 1000L; |
| |
| private IncrementalInstallSession mSession = null; |
| private String mPackageVerifier = null; |
| |
| private static UiAutomation getUiAutomation() { |
| return InstrumentationRegistry.getInstrumentation().getUiAutomation(); |
| } |
| |
| private static Context getContext() { |
| return InstrumentationRegistry.getInstrumentation().getContext(); |
| } |
| |
| private static PackageManager getPackageManager() { |
| return getContext().getPackageManager(); |
| } |
| |
| @Before |
| public void onBefore() throws Exception { |
| checkIncrementalDeliveryFeature(); |
| cleanup(); |
| |
| // Disable the package verifier to avoid the dialog when installing an app. |
| mPackageVerifier = executeShellCommand("settings get global verifier_verify_adb_installs"); |
| executeShellCommand("settings put global verifier_verify_adb_installs 0"); |
| } |
| |
| @After |
| public void onAfter() throws Exception { |
| cleanup(); |
| |
| // Reset the package verifier setting to its original value. |
| executeShellCommand("settings put global verifier_verify_adb_installs " + mPackageVerifier); |
| } |
| |
| static void checkIncrementalDeliveryFeature() { |
| Assume.assumeTrue(getPackageManager().hasSystemFeature( |
| PackageManager.FEATURE_INCREMENTAL_DELIVERY)); |
| } |
| |
| private static void checkIncrementalDeliveryV2Feature() throws Exception { |
| checkIncrementalDeliveryFeature(); |
| Assume.assumeTrue(getPackageManager().hasSystemFeature( |
| PackageManager.FEATURE_INCREMENTAL_DELIVERY, 2)); |
| } |
| |
| @Test |
| public void testAndroid12RequiresIncFsV2() throws Exception { |
| // IncFS is a kernel feature, which is a subject to vendor freeze. That's why |
| // the test verifies the vendor API level here, not the system's one. |
| // Note: vendor API level getter returns either the frozen API level, or the current one for |
| // non-vendor-freeze devices; need to verify both the system first API level and vendor |
| // level to make the final decision. |
| final boolean v2ReqdForSystem = PropertyUtil.getFirstApiLevel() > 30; |
| final boolean v2ReqdForVendor = PropertyUtil.isVendorApiLevelNewerThan(30); |
| final boolean v2Required = v2ReqdForSystem && v2ReqdForVendor; |
| if (v2Required) { |
| Assert.assertTrue("Devices launched at API 31+ with a vendor partition of API 31+ need " |
| + "to support Incremental Delivery version 2 or higher", |
| getPackageManager().hasSystemFeature( |
| PackageManager.FEATURE_INCREMENTAL_DELIVERY, 2)); |
| } |
| } |
| |
| @Test |
| public void testInstallWithIdSig() throws Exception { |
| installPackage(TEST_APK); |
| assertTrue(isAppInstalled(TEST_APP_PACKAGE)); |
| } |
| |
| @Test |
| public void testBug183952694Fixed() throws Exception { |
| // first ensure the IncFS is up and running, e.g. if it's a module |
| installPackage(TEST_APK); |
| assertTrue(isAppInstalled(TEST_APP_PACKAGE)); |
| |
| // the bug is fixed in the v2 version, or when the specific marker feature is present |
| final String[] validValues = {"v2", "mounter_context_for_backing_rw"}; |
| final String features = executeShellCommand("ls /sys/fs/incremental-fs/features/"); |
| assertTrue( |
| "Missing all of required IncFS features [" + TextUtils.join(",", validValues) + "]", |
| Arrays.stream(features.split("\\s+")).anyMatch( |
| f -> Arrays.stream(validValues).anyMatch(f::equals))); |
| } |
| |
| @LargeTest |
| @Test |
| public void testSpaceAllocatedForPackage() throws Exception { |
| final String apk = createApkPath(TEST_APK); |
| final String idsig = createApkPath(TEST_APK_IDSIG); |
| final long appFileSize = new File(apk).length(); |
| final AtomicBoolean firstTime = new AtomicBoolean(true); |
| |
| getUiAutomation().adoptShellPermissionIdentity(); |
| |
| final long blockSize = Os.statvfs("/data/incremental").f_bsize; |
| final long preAllocatedBlocks = Os.statvfs("/data/incremental").f_bfree; |
| |
| final AtomicLong freeSpaceDifference = new AtomicLong(-1L); |
| |
| mSession = |
| new IncrementalInstallSession.Builder() |
| .addApk(Paths.get(apk), Paths.get(idsig)) |
| .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME) |
| .setLogger(new IncrementalDeviceConnection.Logger()) |
| .setBlockFilter((block -> { |
| // Skip allocation check after first iteration. |
| if (!firstTime.getAndSet(false)) { |
| return true; |
| } |
| |
| try { |
| final long postAllocatedBlocks = |
| Os.statvfs("/data/incremental").f_bfree; |
| freeSpaceDifference.set( |
| (preAllocatedBlocks - postAllocatedBlocks) * blockSize); |
| } catch (Exception e) { |
| Log.i(TAG, "ErrnoException: ", e); |
| throw new AssertionError(e); |
| } |
| return true; |
| })) |
| .setBlockTransformer(new CompressingBlockTransformer()) |
| .build(); |
| |
| try { |
| mSession.start(Executors.newSingleThreadExecutor(), |
| IncrementalDeviceConnection.Factory.reliable()); |
| mSession.waitForInstallCompleted(30, TimeUnit.SECONDS); |
| } finally { |
| getUiAutomation().dropShellPermissionIdentity(); |
| } |
| |
| assertTrue(isAppInstalled(TEST_APP_PACKAGE)); |
| |
| final double freeSpaceExpectedDifference = ((appFileSize * 1.015) + blockSize * 8); |
| assertTrue(freeSpaceDifference.get() + " >= " + freeSpaceExpectedDifference, |
| freeSpaceDifference.get() >= freeSpaceExpectedDifference); |
| |
| String installPath = executeShellCommand(String.format("pm path %s", TEST_APP_PACKAGE)) |
| .replaceFirst("package:", "") |
| .trim(); |
| |
| // Retrieve size of APK. |
| Long apkTrimResult = Os.stat(installPath).st_size; |
| |
| // Verify trim was applied. v2+ incfs version required for valid allocation results. |
| if (getPackageManager().hasSystemFeature( |
| PackageManager.FEATURE_INCREMENTAL_DELIVERY, 2)) { |
| assertTrue(apkTrimResult <= appFileSize); |
| } |
| } |
| |
| @Test |
| public void testSplitInstallWithIdSig() throws Exception { |
| // First fully install the apk. |
| { |
| installPackage(TEST_APK); |
| assertTrue(isAppInstalled(TEST_APP_PACKAGE)); |
| } |
| |
| installSplit(TEST_APK_SPLIT0); |
| assertEquals("base, config.mdpi", getSplits(TEST_APP_PACKAGE)); |
| |
| installSplit(TEST_APK_SPLIT1); |
| assertEquals("base, config.hdpi, config.mdpi", getSplits(TEST_APP_PACKAGE)); |
| } |
| |
| @Test |
| public void testSystemInstallWithIdSig() throws Exception { |
| final String baseName = TEST_APK_SHELL; |
| final File file = new File(createApkPath(baseName)); |
| assertEquals( |
| "Failure [INSTALL_FAILED_SESSION_INVALID: Incremental installation of this " |
| + "package is not allowed.]\n", |
| executeShellCommand("pm install-incremental -t -g " + file.getPath())); |
| } |
| |
| @LargeTest |
| @Test |
| public void testInstallWithIdSigAndSplit() throws Exception { |
| File apkfile = new File(createApkPath(TEST_APK)); |
| File splitfile = new File(createApkPath(TEST_APK_SPLIT0)); |
| File[] files = new File[]{apkfile, splitfile}; |
| String param = Arrays.stream(files).map( |
| file -> file.getName() + ":" + file.length()).collect(Collectors.joining(" ")); |
| assertEquals("Success\n", executeShellCommand( |
| String.format("pm install-incremental -t -g -S %s %s", |
| (apkfile.length() + splitfile.length()), param), |
| files)); |
| assertTrue(isAppInstalled(TEST_APP_PACKAGE)); |
| assertEquals("base, config.mdpi", getSplits(TEST_APP_PACKAGE)); |
| } |
| |
| @LargeTest |
| @Test |
| public void testInstallWithStreaming() throws Exception { |
| final String apk = createApkPath(TEST_APK); |
| final String idsig = createApkPath(TEST_APK_IDSIG); |
| mSession = |
| new IncrementalInstallSession.Builder() |
| .addApk(Paths.get(apk), Paths.get(idsig)) |
| .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME) |
| .setLogger(new IncrementalDeviceConnection.Logger()) |
| .build(); |
| getUiAutomation().adoptShellPermissionIdentity(); |
| try { |
| mSession.start(Executors.newSingleThreadExecutor(), |
| IncrementalDeviceConnection.Factory.reliable()); |
| mSession.waitForInstallCompleted(30, TimeUnit.SECONDS); |
| } finally { |
| getUiAutomation().dropShellPermissionIdentity(); |
| } |
| assertTrue(isAppInstalled(TEST_APP_PACKAGE)); |
| } |
| |
| @LargeTest |
| @Test |
| public void testInstallWithMissingBlocks() throws Exception { |
| setDeviceProperty("incfs_default_timeouts", "0:0:0"); |
| setDeviceProperty("known_digesters_list", CTS_PACKAGE_NAME); |
| setSystemProperty("debug.incremental.always_enable_read_timeouts_for_system_dataloaders", |
| "0"); |
| |
| final long randomSeed = System.currentTimeMillis(); |
| Log.i(TAG, "Randomizing missing blocks with seed: " + randomSeed); |
| final Random random = new Random(randomSeed); |
| |
| // TODO: add detection of orphaned IncFS instances after failed installations |
| |
| final int blockSize = 4096; |
| final int retries = 7; // 7 * 3s + leeway ~= 30secs of test timeout |
| |
| final File apk = new File(createApkPath(TEST_APK)); |
| final int blocks = (int) (apk.length() / blockSize); |
| |
| for (int i = 0; i < retries; ++i) { |
| final int skipBlock = random.nextInt(blocks); |
| Log.i(TAG, "skipBlock: " + skipBlock + " out of " + blocks); |
| try { |
| installWithBlockFilter((block -> block.getType() == PendingBlock.Type.SIGNATURE_TREE |
| || block.getBlockIndex() != skipBlock)); |
| if (isAppInstalled(TEST_APP_PACKAGE)) { |
| uninstallPackageSilently(TEST_APP_PACKAGE); |
| } |
| } catch (RuntimeException re) { |
| Log.i(TAG, "RuntimeException: ", re); |
| assertTrue(re.toString(), re.getCause() instanceof IOException); |
| } catch (IOException e) { |
| Log.i(TAG, "IOException: ", e); |
| throw new IOException("Skipped block: " + skipBlock + ", randomSeed: " + randomSeed, |
| e); |
| } |
| } |
| } |
| |
| public void installWithBlockFilter(IBlockFilter blockFilter) throws Exception { |
| final String apk = createApkPath(TEST_APK); |
| final String idsig = createApkPath(TEST_APK_IDSIG); |
| mSession = |
| new IncrementalInstallSession.Builder() |
| .addApk(Paths.get(apk), Paths.get(idsig)) |
| .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME) |
| .setLogger(new IncrementalDeviceConnection.Logger()) |
| .setBlockFilter(blockFilter) |
| .build(); |
| getUiAutomation().adoptShellPermissionIdentity(); |
| try { |
| mSession.start(Executors.newSingleThreadExecutor(), |
| IncrementalDeviceConnection.Factory.reliableExpectInstallationFailure()); |
| mSession.waitForAnyCompletion(3, TimeUnit.SECONDS); |
| } finally { |
| getUiAutomation().dropShellPermissionIdentity(); |
| } |
| } |
| |
| /** |
| * Compress the data if the compressed size is < original size, otherwise return the original |
| * data. |
| */ |
| private static ByteBuffer maybeCompressPage(ByteBuffer pageData) { |
| pageData.mark(); |
| ByteArrayOutputStream compressedByteStream = new ByteArrayOutputStream(); |
| try (BlockLZ4CompressorOutputStream compressor = |
| new BlockLZ4CompressorOutputStream(compressedByteStream)) { |
| Channels.newChannel(compressor).write(pageData); |
| // This is required to make sure the bytes are written to the output |
| compressor.finish(); |
| } catch (IOException impossible) { |
| throw new AssertionError(impossible); |
| } finally { |
| pageData.reset(); |
| } |
| |
| byte[] compressedBytes = compressedByteStream.toByteArray(); |
| if (compressedBytes.length < pageData.remaining()) { |
| return ByteBuffer.wrap(compressedBytes); |
| } |
| return pageData; |
| } |
| |
| static final class CompressedPendingBlock extends PendingBlock { |
| final ByteBuffer mPageData; |
| |
| CompressedPendingBlock(PendingBlock block) throws IOException { |
| super(block); |
| |
| final ByteBuffer buffer = ByteBuffer.allocate(super.getBlockSize()); |
| super.readBlockData(buffer); |
| buffer.flip(); // switch to read mode |
| |
| if (super.getType() == Type.APK_DATA) { |
| mPageData = maybeCompressPage(buffer); |
| } else { |
| mPageData = buffer; |
| } |
| } |
| |
| public Compression getCompression() { |
| return this.getBlockSize() < super.getBlockSize() ? Compression.LZ4 : Compression.NONE; |
| } |
| |
| public short getBlockSize() { |
| return (short) mPageData.remaining(); |
| } |
| |
| public void readBlockData(ByteBuffer buffer) throws IOException { |
| mPageData.mark(); |
| buffer.put(mPageData); |
| mPageData.reset(); |
| } |
| } |
| |
| static final class CompressingBlockTransformer implements IBlockTransformer { |
| @Override |
| @NonNull |
| public PendingBlock transform(@NonNull PendingBlock block) throws IOException { |
| return new CompressedPendingBlock(block); |
| } |
| } |
| |
| @LargeTest |
| @Test |
| public void testInstallWithStreamingAndCompression() throws Exception { |
| final String apk = createApkPath(TEST_APK); |
| final String idsig = createApkPath(TEST_APK_IDSIG); |
| mSession = |
| new IncrementalInstallSession.Builder() |
| .addApk(Paths.get(apk), Paths.get(idsig)) |
| .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME) |
| .setLogger(new IncrementalDeviceConnection.Logger()) |
| .setBlockTransformer(new CompressingBlockTransformer()) |
| .build(); |
| getUiAutomation().adoptShellPermissionIdentity(); |
| try { |
| mSession.start(Executors.newSingleThreadExecutor(), |
| IncrementalDeviceConnection.Factory.reliable()); |
| mSession.waitForInstallCompleted(30, TimeUnit.SECONDS); |
| } finally { |
| getUiAutomation().dropShellPermissionIdentity(); |
| } |
| assertTrue(isAppInstalled(TEST_APP_PACKAGE)); |
| } |
| |
| @LargeTest |
| @Test |
| public void testInstallWithStreamingUnreliableConnection() throws Exception { |
| final String apk = createApkPath(TEST_APK); |
| final String idsig = createApkPath(TEST_APK_IDSIG); |
| mSession = |
| new IncrementalInstallSession.Builder() |
| .addApk(Paths.get(apk), Paths.get(idsig)) |
| .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME) |
| .setLogger(new IncrementalDeviceConnection.Logger()) |
| .build(); |
| getUiAutomation().adoptShellPermissionIdentity(); |
| try { |
| mSession.start(Executors.newSingleThreadExecutor(), |
| IncrementalDeviceConnection.Factory.ureliable()); |
| mSession.waitForInstallCompleted(30, TimeUnit.SECONDS); |
| } catch (Exception ignored) { |
| // Ignore, we are looking for crashes anyway. |
| } finally { |
| getUiAutomation().dropShellPermissionIdentity(); |
| } |
| } |
| |
| @Test |
| public void testInstallWithIdSigInvalidLength() throws Exception { |
| File file = new File(createApkPath(TEST_APK)); |
| Truth.assertThat( |
| executeShellCommand("pm install-incremental -t -g -S " + (file.length() - 1), |
| new File[]{file})).contains( |
| "Failure"); |
| assertFalse(isAppInstalled(TEST_APP_PACKAGE)); |
| } |
| |
| @Test |
| public void testInstallWithInvalidIdSig() throws Exception { |
| File file = new File(createApkPath(TEST_APK_MALFORMED)); |
| Truth.assertThat( |
| executeShellCommand("pm install-incremental -t -g " + file.getPath())).contains( |
| "Failure"); |
| assertFalse(isAppInstalled(TEST_APP_PACKAGE)); |
| } |
| |
| @LargeTest |
| @Test |
| public void testInstallWithIdSigStreamIncompleteData() throws Exception { |
| File file = new File(createApkPath(TEST_APK)); |
| long length = file.length(); |
| // Streaming happens in blocks of 1024 bytes, new length will not stream the last block. |
| long newLength = length - (length % 1024 == 0 ? 1024 : length % 1024); |
| Truth.assertThat( |
| executeShellCommand( |
| "pm install-incremental -t -g -S " + length, |
| new File[]{file}, |
| new long[]{newLength})).contains("Failure"); |
| assertFalse(isAppInstalled(TEST_APP_PACKAGE)); |
| } |
| |
| @LargeTest |
| @Test |
| public void testInstallWithIdSigNoMissingPages() throws Exception { |
| final int installIterations = 1; |
| final int atraceDumpIterations = 3; |
| final int atraceDumpDelayMs = 1000; |
| final String missingPageReads = "|missing_page_reads: count="; |
| |
| final ArrayList<String> missingPages = new ArrayList<>(); |
| |
| checkSysTrace( |
| installIterations, |
| atraceDumpIterations, |
| atraceDumpDelayMs, |
| () -> { |
| // Install multiple splits so that digesters won't kick in. |
| installPackage(TEST_APK); |
| installSplit(TEST_APK_SPLIT0); |
| installSplit(TEST_APK_SPLIT1); |
| installSplit(TEST_APK_SPLIT2); |
| // Now read it as fast as we can. |
| readSplitInChunks("base.apk"); |
| readSplitInChunks("split_config.mdpi.apk"); |
| readSplitInChunks("split_config.hdpi.apk"); |
| readSplitInChunks("split_config.xhdpi.apk"); |
| return null; |
| }, |
| (stdout) -> { |
| try (Scanner scanner = new Scanner(stdout)) { |
| ReadLogEntry prevLogEntry = null; |
| while (scanner.hasNextLine()) { |
| final String line = scanner.nextLine(); |
| |
| final ReadLogEntry readLogEntry = ReadLogEntry.parse(line); |
| if (readLogEntry != null) { |
| prevLogEntry = readLogEntry; |
| continue; |
| } |
| |
| int missingPageIdx = line.indexOf(missingPageReads); |
| if (missingPageIdx == -1) { |
| continue; |
| } |
| String missingBlocks = line.substring( |
| missingPageIdx + missingPageReads.length()); |
| |
| int prvTimestamp = prevLogEntry != null ? extractTimestamp( |
| prevLogEntry.line) : -1; |
| int curTimestamp = extractTimestamp(line); |
| if (prvTimestamp == -1 || curTimestamp == -1) { |
| missingPages.add("count=" + missingBlocks); |
| continue; |
| } |
| |
| int delta = curTimestamp - prvTimestamp; |
| missingPages.add( |
| "count=" + missingBlocks + ", timestamp delta=" + delta + "ms"); |
| } |
| return false; |
| } |
| }); |
| |
| assertTrue("Missing page reads found in atrace dump: " + String.join("\n", missingPages), |
| missingPages.isEmpty()); |
| } |
| |
| static class ReadLogEntry { |
| public final String line; |
| public final int blockIdx; |
| public final int count; |
| public final int fileIdx; |
| public final int appId; |
| public final int userId; |
| |
| private ReadLogEntry(String line, int blockIdx, int count, int fileIdx, int appId, |
| int userId) { |
| this.line = line; |
| this.blockIdx = blockIdx; |
| this.count = count; |
| this.fileIdx = fileIdx; |
| this.appId = appId; |
| this.userId = userId; |
| } |
| |
| public String toString() { |
| return blockIdx + "/" + count + "/" + fileIdx + "/" + appId + "/" + userId; |
| } |
| |
| static final String BLOCK_PREFIX = "|page_read: index="; |
| static final String COUNT_PREFIX = " count="; |
| static final String FILE_PREFIX = " file="; |
| static final String APP_ID_PREFIX = " appid="; |
| static final String USER_ID_PREFIX = " userid="; |
| |
| private static int parseInt(String line, int prefixIdx, int prefixLen, int endIdx) { |
| if (prefixIdx == -1) { |
| return -1; |
| } |
| final String intStr; |
| if (endIdx != -1) { |
| intStr = line.substring(prefixIdx + prefixLen, endIdx); |
| } else { |
| intStr = line.substring(prefixIdx + prefixLen); |
| } |
| |
| return Integer.parseInt(intStr); |
| } |
| |
| static ReadLogEntry parse(String line) { |
| int blockIdx = line.indexOf(BLOCK_PREFIX); |
| if (blockIdx == -1) { |
| return null; |
| } |
| int countIdx = line.indexOf(COUNT_PREFIX, blockIdx + BLOCK_PREFIX.length()); |
| if (countIdx == -1) { |
| return null; |
| } |
| int fileIdx = line.indexOf(FILE_PREFIX, countIdx + COUNT_PREFIX.length()); |
| if (fileIdx == -1) { |
| return null; |
| } |
| int appIdIdx = line.indexOf(APP_ID_PREFIX, fileIdx + FILE_PREFIX.length()); |
| final int userIdIdx; |
| if (appIdIdx != -1) { |
| userIdIdx = line.indexOf(USER_ID_PREFIX, appIdIdx + APP_ID_PREFIX.length()); |
| } else { |
| userIdIdx = -1; |
| } |
| |
| return new ReadLogEntry( |
| line, |
| parseInt(line, blockIdx, BLOCK_PREFIX.length(), countIdx), |
| parseInt(line, countIdx, COUNT_PREFIX.length(), fileIdx), |
| parseInt(line, fileIdx, FILE_PREFIX.length(), appIdIdx), |
| parseInt(line, appIdIdx, APP_ID_PREFIX.length(), userIdIdx), |
| parseInt(line, userIdIdx, USER_ID_PREFIX.length(), -1)); |
| } |
| } |
| |
| @Test |
| public void testReadLogParser() throws Exception { |
| assertEquals(null, ReadLogEntry.parse("# tracer: nop\n")); |
| assertEquals( |
| "178/290/0/10184/0", |
| ReadLogEntry.parse( |
| "<...>-2777 ( 1639) [006] .... 2764.227110: tracing_mark_write: " |
| + "B|1639|page_read: index=178 count=290 file=0 appid=10184 " |
| + "userid=0") |
| .toString()); |
| assertEquals( |
| null, |
| ReadLogEntry.parse( |
| "<...>-2777 ( 1639) [006] .... 2764.227111: tracing_mark_write: E|1639")); |
| assertEquals( |
| "468/337/0/10184/2", |
| ReadLogEntry.parse( |
| "<...>-2777 ( 1639) [006] .... 2764.243227: tracing_mark_write: " |
| + "B|1639|page_read: index=468 count=337 file=0 appid=10184 " |
| + "userid=2") |
| .toString()); |
| assertEquals( |
| null, |
| ReadLogEntry.parse( |
| "<...>-2777 ( 1639) [006] .... 2764.243229: tracing_mark_write: E|1639")); |
| assertEquals( |
| "18/9/3/-1/-1", |
| ReadLogEntry.parse( |
| " <...>-2777 ( 1639) [006] .... 2764.227095: " |
| + "tracing_mark_write: B|1639|page_read: index=18 count=9 file=3") |
| .toString()); |
| } |
| |
| static int extractTimestamp(String line) { |
| final String timestampEnd = ": tracing_mark_write:"; |
| int timestampEndIdx = line.indexOf(timestampEnd); |
| if (timestampEndIdx == -1) { |
| return -1; |
| } |
| |
| int timestampBegIdx = timestampEndIdx - 1; |
| for (; timestampBegIdx >= 0; --timestampBegIdx) { |
| char ch = line.charAt(timestampBegIdx); |
| if ('0' <= ch && ch <= '9' || ch == '.') { |
| continue; |
| } |
| break; |
| } |
| double timestamp = Double.parseDouble(line.substring(timestampBegIdx, timestampEndIdx)); |
| return (int) (timestamp * 1000); |
| } |
| |
| @Test |
| public void testExtractTimestamp() throws Exception { |
| assertEquals(-1, extractTimestamp("# tracer: nop\n")); |
| assertEquals(14255168, extractTimestamp( |
| "<...>-10355 ( 1636) [006] .... 14255.168694: tracing_mark_write: " |
| + "B|1636|page_read: index=1 count=16 file=0 appid=10184 userid=0")); |
| assertEquals(2764243, extractTimestamp( |
| "<...>-2777 ( 1639) [006] .... 2764.243225: tracing_mark_write: " |
| + "B|1639|missing_page_reads: count=132")); |
| assertEquals(114176, extractTimestamp( |
| "DataLoaderManag-8339 ( 1780) [004] .... 114.176342: tracing_mark_write: " |
| + "B|1780|page_read: index=1846 count=21 file=0 appid=10151 userid=0")); |
| } |
| static class AppReads { |
| public final String packageName; |
| public final int reads; |
| |
| AppReads(String packageName, int reads) { |
| this.packageName = packageName; |
| this.reads = reads; |
| } |
| } |
| |
| @LargeTest |
| @Test |
| public void testInstallWithIdSigNoDigesting() throws Exception { |
| // Overall timeout of 3secs in 100ms intervals. |
| final int installIterations = 1; |
| final int atraceDumpIterations = 30; |
| final int atraceDumpDelayMs = 100; |
| final int blockSize = 4096; |
| |
| final String[] apks = |
| new String[]{TEST_HW7, TEST_HW7_SPLIT0, TEST_HW7_SPLIT1, TEST_HW7_SPLIT2, |
| TEST_HW7_SPLIT3, TEST_HW7_SPLIT4}; |
| final boolean[][] touched = new boolean[apks.length][]; |
| final int[] blocks = new int[apks.length]; |
| final AtomicLong[] totalTouchedBlocks = new AtomicLong[apks.length]; |
| for (int i = 0, size = apks.length; i < size; ++i) { |
| final String apkName = apks[i]; |
| final File apkfile = new File(createApkPath(apkName)); |
| blocks[i] = (int) ((apkfile.length() + blockSize - 1) / blockSize); |
| touched[i] = new boolean[blocks[i]]; |
| totalTouchedBlocks[i] = new AtomicLong(0); |
| } |
| |
| final ArrayMap<Integer, Integer> uids = new ArrayMap<>(); |
| |
| checkSysTrace( |
| installIterations, |
| atraceDumpIterations, |
| atraceDumpDelayMs, |
| () -> { |
| mSession = |
| new IncrementalInstallSession.Builder() |
| .addApk(Paths.get(createApkPath(TEST_HW7)), |
| Paths.get(createApkPath(TEST_HW7_IDSIG))) |
| .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT0)), |
| Paths.get(createApkPath(TEST_HW7_SPLIT0_IDSIG))) |
| .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT1)), |
| Paths.get(createApkPath(TEST_HW7_SPLIT1_IDSIG))) |
| .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT2)), |
| Paths.get(createApkPath(TEST_HW7_SPLIT2_IDSIG))) |
| .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT3)), |
| Paths.get(createApkPath(TEST_HW7_SPLIT3_IDSIG))) |
| .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT4)), |
| Paths.get(createApkPath(TEST_HW7_SPLIT4_IDSIG))) |
| .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME, |
| "--skip-verification") |
| .setLogger(new IncrementalDeviceConnection.Logger()) |
| .build(); |
| getUiAutomation().adoptShellPermissionIdentity(); |
| try { |
| mSession.start(Executors.newSingleThreadExecutor(), |
| IncrementalDeviceConnection.Factory.reliable()); |
| mSession.waitForInstallCompleted(30, TimeUnit.SECONDS); |
| assertEquals( |
| "base, config.hdpi, config.mdpi, config.xhdpi, config.xxhdpi, " |
| + "config.xxxhdpi", |
| getSplits(TEST_APP_PACKAGE)); |
| } finally { |
| getUiAutomation().dropShellPermissionIdentity(); |
| } |
| return null; |
| }, |
| (stdout) -> { |
| try (Scanner scanner = new Scanner(stdout)) { |
| while (scanner.hasNextLine()) { |
| String line = scanner.nextLine(); |
| final ReadLogEntry readLogEntry = ReadLogEntry.parse(line); |
| if (readLogEntry == null) { |
| continue; |
| } |
| int fileIdx = readLogEntry.fileIdx; |
| for (int i = 0, count = readLogEntry.count; i < count; ++i) { |
| int blockIdx = readLogEntry.blockIdx + i; |
| if (touched[fileIdx][blockIdx]) { |
| continue; |
| } |
| |
| touched[fileIdx][blockIdx] = true; |
| |
| int uid = UserHandle.getUid(readLogEntry.userId, |
| readLogEntry.appId); |
| Integer touchedByUid = uids.get(uid); |
| uids.put(uid, touchedByUid == null ? 1 : touchedByUid + 1); |
| |
| long totalTouched = totalTouchedBlocks[fileIdx].incrementAndGet(); |
| if (totalTouched >= blocks[fileIdx]) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| }); |
| |
| int firstFileIdx = CHECK_BASE_APK_DIGESTION ? 0 : 1; |
| |
| boolean found = false; |
| for (int i = firstFileIdx, size = blocks.length; i < size; ++i) { |
| if (totalTouchedBlocks[i].get() >= blocks[i]) { |
| found = true; |
| break; |
| } |
| } |
| if (!found) { |
| return; |
| } |
| |
| PackageManager pm = getPackageManager(); |
| |
| AppReads[] appIdReads = new AppReads[uids.size()]; |
| for (int i = 0, size = uids.size(); i < size; ++i) { |
| final int uid = uids.keyAt(i); |
| final int appId = UserHandle.getAppId(uid); |
| final int userId = UserHandle.getUserId(uid); |
| |
| final String packageName; |
| if (appId < Process.FIRST_APPLICATION_UID) { |
| packageName = "<system>"; |
| } else { |
| String[] packages = pm.getPackagesForUid(uid); |
| if (packages == null || packages.length == 0) { |
| packageName = "<unknown package, appId=" + appId + ", userId=" + userId + ">"; |
| } else { |
| packageName = "[" + String.join(",", packages) + "]"; |
| } |
| } |
| appIdReads[i] = new AppReads(packageName, uids.valueAt(i)); |
| } |
| Arrays.sort(appIdReads, (lhs, rhs) -> Integer.compare(rhs.reads, lhs.reads)); |
| |
| final String packages = String.join("\n", Arrays.stream(appIdReads).map( |
| item -> item.packageName + " : " + item.reads + " blocks").toArray(String[]::new)); |
| fail("Digesting detected, list of packages: " + packages); |
| } |
| |
| @LargeTest |
| @Test |
| public void testInstallWithIdSigPerUidTimeouts() throws Exception { |
| executeShellCommand("atrace --async_start -b 1024 -c adb"); |
| try { |
| setDeviceProperty("incfs_default_timeouts", "5000000:5000000:5000000"); |
| setDeviceProperty("known_digesters_list", CTS_PACKAGE_NAME); |
| |
| installPackage(TEST_APK); |
| assertTrue(isAppInstalled(TEST_APP_PACKAGE)); |
| } finally { |
| executeShellCommand("atrace --async_stop"); |
| } |
| } |
| |
| @LargeTest |
| @Test |
| public void testInstallWithIdSigStreamPerUidTimeoutsIncompleteData() throws Exception { |
| // To disable verification. |
| installNonIncremental(TEST_APK); |
| |
| checkIncrementalDeliveryV2Feature(); |
| |
| mSession = |
| new IncrementalInstallSession.Builder() |
| .addApk(Paths.get(createApkPath(TEST_HW7)), |
| Paths.get(createApkPath(TEST_HW7_IDSIG))) |
| .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT0)), |
| Paths.get(createApkPath(TEST_HW7_SPLIT0_IDSIG))) |
| .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT1)), |
| Paths.get(createApkPath(TEST_HW7_SPLIT1_IDSIG))) |
| .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT2)), |
| Paths.get(createApkPath(TEST_HW7_SPLIT2_IDSIG))) |
| .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT3)), |
| Paths.get(createApkPath(TEST_HW7_SPLIT3_IDSIG))) |
| .addApk(Paths.get(createApkPath(TEST_HW7_SPLIT4)), |
| Paths.get(createApkPath(TEST_HW7_SPLIT4_IDSIG))) |
| .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME, "--skip-verification") |
| .setLogger(new IncrementalDeviceConnection.Logger()) |
| .build(); |
| |
| executeShellCommand("atrace --async_start -b 10240 -c adb"); |
| try { |
| setDeviceProperty("incfs_default_timeouts", "20000000:20000000:20000000"); |
| setDeviceProperty("known_digesters_list", CTS_PACKAGE_NAME); |
| |
| final int beforeReadDelayMs = 1000; |
| Thread.currentThread().sleep(beforeReadDelayMs); |
| |
| // Partially install the apk+split0/1/2/3/4. |
| getUiAutomation().adoptShellPermissionIdentity(); |
| try { |
| mSession.start(Executors.newSingleThreadExecutor(), |
| IncrementalDeviceConnection.Factory.reliable()); |
| mSession.waitForInstallCompleted(30, TimeUnit.SECONDS); |
| assertEquals( |
| "base, config.hdpi, config.mdpi, config.xhdpi, config.xxhdpi, config" |
| + ".xxxhdpi", |
| getSplits(TEST_APP_PACKAGE)); |
| } finally { |
| getUiAutomation().dropShellPermissionIdentity(); |
| } |
| |
| final String packagePath = getCodePath(TEST_APP_PACKAGE); |
| |
| // Try to read splits and see if we are throttled at least once. |
| long maxReadTime = 0; |
| for (String splitName : new String[]{"split_config.hdpi.apk", "split_config.mdpi.apk", |
| "split_config.xhdpi.apk", "split_config.xxxhdpi.apk", |
| "split_config.xxxhdpi.apk"}) { |
| final File apkToRead = new File(packagePath, splitName); |
| final long readTime0 = readAndReportTime(apkToRead, 1000); |
| |
| if (readTime0 < EXPECTED_READ_TIME) { |
| executeShellCommand("atrace --async_dump"); |
| } |
| maxReadTime = Math.max(maxReadTime, readTime0); |
| if (maxReadTime >= EXPECTED_READ_TIME) { |
| break; |
| } |
| } |
| assertTrue("Must take longer than " + EXPECTED_READ_TIME + "ms: time0=" + maxReadTime |
| + "ms", maxReadTime >= EXPECTED_READ_TIME); |
| } finally { |
| executeShellCommand("atrace --async_stop"); |
| } |
| } |
| |
| @LargeTest |
| @Test |
| public void testInstallWithIdSigPerUidTimeoutsIgnored() throws Exception { |
| // Timeouts would be ignored as there are no readlogs collected. |
| final int beforeReadDelayMs = 5000; |
| setDeviceProperty("incfs_default_timeouts", "5000000:5000000:5000000"); |
| setDeviceProperty("known_digesters_list", CTS_PACKAGE_NAME); |
| |
| // First fully install the apk and a split0. |
| { |
| installPackage(TEST_APK); |
| assertTrue(isAppInstalled(TEST_APP_PACKAGE)); |
| installSplit(TEST_APK_SPLIT0); |
| assertEquals("base, config.mdpi", getSplits(TEST_APP_PACKAGE)); |
| installSplit(TEST_APK_SPLIT1); |
| assertEquals("base, config.hdpi, config.mdpi", getSplits(TEST_APP_PACKAGE)); |
| } |
| |
| // Allow IncrementalService to update the timeouts after full download. |
| Thread.currentThread().sleep(beforeReadDelayMs); |
| |
| // Try to read a split and see if we are throttled. |
| final long readTime = readAndReportTime(getSplit("split_config.mdpi.apk"), 1000); |
| assertTrue("Must take less than " + EXPECTED_READ_TIME + "ms vs " + readTime + "ms", |
| readTime < EXPECTED_READ_TIME); |
| } |
| |
| @Test |
| public void testInstallWithIdSigStreamIncompleteDataForSplit() throws Exception { |
| File apkfile = new File(createApkPath(TEST_APK)); |
| File splitfile = new File(createApkPath(TEST_APK_SPLIT0)); |
| long splitLength = splitfile.length(); |
| // Don't fully stream the split. |
| long newSplitLength = splitLength - (splitLength % 1024 == 0 ? 1024 : splitLength % 1024); |
| File[] files = new File[]{apkfile, splitfile}; |
| String param = Arrays.stream(files).map( |
| file -> file.getName() + ":" + file.length()).collect(Collectors.joining(" ")); |
| Truth.assertThat(executeShellCommand( |
| String.format("pm install-incremental -t -g -S %s %s", |
| (apkfile.length() + splitfile.length()), param), |
| files, new long[]{apkfile.length(), newSplitLength})).contains("Failure"); |
| assertFalse(isAppInstalled(TEST_APP_PACKAGE)); |
| } |
| |
| static class TestDataLoaderService extends DataLoaderService { |
| } |
| |
| @Test |
| public void testDataLoaderServiceDefaultImplementation() { |
| DataLoaderService service = new TestDataLoaderService(); |
| assertEquals(null, service.onCreateDataLoader(null)); |
| IBinder binder = service.onBind(null); |
| assertNotEquals(null, binder); |
| assertEquals(binder, service.onBind(new Intent())); |
| } |
| |
| @LargeTest |
| @Test |
| public void testInstallSysTraceDebuggable() throws Exception { |
| doTestInstallSysTrace(TEST_APK); |
| } |
| |
| @LargeTest |
| @Test |
| public void testInstallSysTraceProfileable() throws Exception { |
| doTestInstallSysTrace(TEST_APK_PROFILEABLE); |
| } |
| |
| @LargeTest |
| @Test |
| public void testInstallSysTraceNoReadlogs() throws Exception { |
| setSystemProperty("debug.incremental.enforce_readlogs_max_interval_for_system_dataloaders", |
| "1"); |
| setSystemProperty("debug.incremental.readlogs_max_interval_sec", "0"); |
| |
| final int atraceDumpIterations = 30; |
| final int atraceDumpDelayMs = 100; |
| final String expected = "|page_read:"; |
| |
| // We don't expect any readlogs with 0sec interval. |
| assertFalse( |
| "Page reads (" + expected + ") were found in atrace dump", |
| checkSysTraceForSubstring(TEST_APK, expected, atraceDumpIterations, |
| atraceDumpDelayMs)); |
| } |
| |
| private boolean checkSysTraceForSubstring(String testApk, final String expected, |
| int atraceDumpIterations, int atraceDumpDelayMs) throws Exception { |
| final int installIterations = 3; |
| return checkSysTrace( |
| installIterations, |
| atraceDumpIterations, |
| atraceDumpDelayMs, |
| () -> installPackage(testApk), |
| (stdout) -> stdout.contains(expected)); |
| } |
| |
| private boolean checkSysTrace( |
| int installIterations, |
| int atraceDumpIterations, |
| int atraceDumpDelayMs, |
| final Callable<Void> installer, |
| final Function<String, Boolean> checker) |
| throws Exception { |
| final int beforeReadDelayMs = 1000; |
| |
| final CompletableFuture<Boolean> result = new CompletableFuture<>(); |
| final Thread readFromProcess = new Thread(() -> { |
| try { |
| executeShellCommand("atrace --async_start -b 10240 -c adb"); |
| try { |
| for (int i = 0; i < atraceDumpIterations; ++i) { |
| final String stdout = executeShellCommand("atrace --async_dump"); |
| try { |
| if (checker.apply(stdout)) { |
| result.complete(true); |
| break; |
| } |
| Thread.currentThread().sleep(atraceDumpDelayMs); |
| } catch (InterruptedException ignored) { |
| } |
| } |
| } finally { |
| executeShellCommand("atrace --async_stop"); |
| } |
| } catch (IOException ignored) { |
| } |
| }); |
| readFromProcess.start(); |
| |
| for (int i = 0; i < installIterations; ++i) { |
| installer.call(); |
| assertTrue(isAppInstalled(TEST_APP_PACKAGE)); |
| Thread.currentThread().sleep(beforeReadDelayMs); |
| uninstallPackageSilently(TEST_APP_PACKAGE); |
| } |
| |
| readFromProcess.join(); |
| return result.getNow(false); |
| } |
| |
| private void doTestInstallSysTrace(String testApk) throws Exception { |
| // Async atrace dump uses less resources but requires periodic pulls. |
| // Overall timeout of 10secs in 100ms intervals should be enough. |
| final int atraceDumpIterations = 100; |
| final int atraceDumpDelayMs = 100; |
| final String expected = "|page_read:"; |
| |
| assertTrue( |
| "No page reads (" + expected + ") found in atrace dump", |
| checkSysTraceForSubstring(testApk, expected, atraceDumpIterations, |
| atraceDumpDelayMs)); |
| } |
| |
| static boolean isAppInstalled(String packageName) throws IOException { |
| return isAppInstalledForUser(packageName, -1); |
| } |
| |
| static boolean isAppInstalledForUser(String packageName, int userId) throws IOException { |
| final String command = userId < 0 ? "pm list packages " + packageName : |
| "pm list packages --user " + userId + " " + packageName; |
| final String commandResult = executeShellCommand(command); |
| return Arrays.stream(commandResult.split("\\r?\\n")) |
| .anyMatch(line -> line.equals("package:" + packageName)); |
| } |
| |
| private String getSplits(String packageName) throws IOException { |
| final String result = parsePackageDump(packageName, " splits=["); |
| if (TextUtils.isEmpty(result)) { |
| return null; |
| } |
| return result.substring(0, result.length() - 1); |
| } |
| |
| private String getCodePath(String packageName) throws IOException { |
| return parsePackageDump(packageName, " codePath="); |
| } |
| |
| private File getSplit(String splitName) throws Exception { |
| return new File(getCodePath(TEST_APP_PACKAGE), splitName); |
| } |
| |
| private String parsePackageDump(String packageName, String prefix) throws IOException { |
| final String commandResult = executeShellCommand("pm dump " + packageName); |
| final int prefixLength = prefix.length(); |
| Optional<String> maybeSplits = Arrays.stream(commandResult.split("\\r?\\n")) |
| .filter(line -> line.startsWith(prefix)).findFirst(); |
| if (!maybeSplits.isPresent()) { |
| return null; |
| } |
| String splits = maybeSplits.get(); |
| return splits.substring(prefixLength); |
| } |
| |
| private static String createApkPath(String baseName) { |
| return TEST_APK_PATH + baseName; |
| } |
| |
| static void installNonIncremental(String baseName) throws IOException { |
| File file = new File(createApkPath(baseName)); |
| assertEquals("Success\n", |
| executeShellCommand("pm install -t -g " + file.getPath())); |
| } |
| |
| static Void installPackage(String baseName) throws IOException { |
| File file = new File(createApkPath(baseName)); |
| assertEquals("Success\n", |
| executeShellCommand("pm install-incremental -t -g " + file.getPath())); |
| return null; |
| } |
| |
| private void installSplit(String splitName) throws Exception { |
| final File splitfile = new File(createApkPath(splitName)); |
| |
| try (InputStream inputStream = executeShellCommandStream( |
| "pm install-incremental -t -g -p " + TEST_APP_PACKAGE + " " |
| + splitfile.getPath())) { |
| assertEquals("Success\n", readFullStream(inputStream)); |
| } |
| } |
| |
| private void readSplitInChunks(String splitName) throws Exception { |
| final int chunks = 2; |
| final int waitBetweenChunksMs = 100; |
| final File file = getSplit(splitName); |
| |
| assertTrue(file.toString(), file.exists()); |
| final long totalSize = file.length(); |
| final long chunkSize = totalSize / chunks; |
| try (InputStream baseApkStream = new FileInputStream(file)) { |
| final byte[] buffer = new byte[4 * 1024]; |
| long readSoFar = 0; |
| long maxToRead = 0; |
| for (int i = 0; i < chunks; ++i) { |
| maxToRead += chunkSize; |
| int length; |
| while ((length = baseApkStream.read(buffer)) != -1) { |
| readSoFar += length; |
| if (readSoFar >= maxToRead) { |
| break; |
| } |
| } |
| if (readSoFar < totalSize) { |
| Thread.currentThread().sleep(waitBetweenChunksMs); |
| } |
| } |
| } |
| } |
| |
| private long readAndReportTime(File file, long borderTime) throws Exception { |
| final long startTime = SystemClock.uptimeMillis(); |
| assertTrue(file.toString(), file.exists()); |
| try (InputStream baseApkStream = new FileInputStream(file)) { |
| final byte[] buffer = new byte[128 * 1024]; |
| while (baseApkStream.read(buffer) != -1) { |
| long readTime = SystemClock.uptimeMillis() - startTime; |
| if (readTime >= borderTime) { |
| break; |
| } |
| } |
| } |
| return SystemClock.uptimeMillis() - startTime; |
| } |
| |
| static String uninstallPackageSilently(String packageName) throws IOException { |
| return executeShellCommand("pm uninstall " + packageName); |
| } |
| |
| interface Result { |
| boolean await() throws Exception; |
| } |
| |
| static String executeShellCommand(String command) throws IOException { |
| try (InputStream inputStream = executeShellCommandStream(command)) { |
| return readFullStream(inputStream); |
| } |
| } |
| |
| private static InputStream executeShellCommandStream(String command) throws IOException { |
| final ParcelFileDescriptor stdout = getUiAutomation().executeShellCommand(command); |
| return new ParcelFileDescriptor.AutoCloseInputStream(stdout); |
| } |
| |
| private static String executeShellCommand(String command, File[] inputs) |
| throws IOException { |
| return executeShellCommand(command, inputs, Stream.of(inputs).mapToLong( |
| File::length).toArray()); |
| } |
| |
| private static String executeShellCommand(String command, File[] inputs, long[] expected) |
| throws IOException { |
| try (InputStream inputStream = executeShellCommandRw(command, inputs, expected)) { |
| return readFullStream(inputStream); |
| } |
| } |
| |
| private static InputStream executeShellCommandRw(String command, File[] inputs, long[] expected) |
| throws IOException { |
| assertEquals(inputs.length, expected.length); |
| final ParcelFileDescriptor[] pfds = |
| InstrumentationRegistry.getInstrumentation().getUiAutomation() |
| .executeShellCommandRw(command); |
| ParcelFileDescriptor stdout = pfds[0]; |
| ParcelFileDescriptor stdin = pfds[1]; |
| try (FileOutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream( |
| stdin)) { |
| for (int i = 0; i < inputs.length; i++) { |
| try (FileInputStream inputStream = new FileInputStream(inputs[i])) { |
| writeFullStream(inputStream, outputStream, expected[i]); |
| } |
| } |
| } |
| return new ParcelFileDescriptor.AutoCloseInputStream(stdout); |
| } |
| |
| static String readFullStream(InputStream inputStream, long expected) |
| throws IOException { |
| ByteArrayOutputStream result = new ByteArrayOutputStream(); |
| writeFullStream(inputStream, result, expected); |
| return result.toString("UTF-8"); |
| } |
| |
| static String readFullStream(InputStream inputStream) throws IOException { |
| return readFullStream(inputStream, -1); |
| } |
| |
| static void writeFullStream(InputStream inputStream, OutputStream outputStream, |
| long expected) |
| throws IOException { |
| final byte[] buffer = new byte[1024]; |
| long total = 0; |
| int length; |
| while ((length = inputStream.read(buffer)) != -1 && (expected < 0 || total < expected)) { |
| outputStream.write(buffer, 0, length); |
| total += length; |
| } |
| if (expected > 0) { |
| assertEquals(expected, total); |
| } |
| } |
| |
| private void cleanup() throws Exception { |
| uninstallPackageSilently(TEST_APP_PACKAGE); |
| assertFalse(isAppInstalled(TEST_APP_PACKAGE)); |
| assertEquals(null, getSplits(TEST_APP_PACKAGE)); |
| setDeviceProperty("incfs_default_timeouts", null); |
| setDeviceProperty("known_digesters_list", null); |
| setSystemProperty("debug.incremental.enforce_readlogs_max_interval_for_system_dataloaders", |
| "0"); |
| setSystemProperty("debug.incremental.readlogs_max_interval_sec", "10000"); |
| setSystemProperty("debug.incremental.always_enable_read_timeouts_for_system_dataloaders", |
| "1"); |
| IoUtils.closeQuietly(mSession); |
| mSession = null; |
| } |
| |
| static void setDeviceProperty(String name, String value) { |
| getUiAutomation().adoptShellPermissionIdentity(); |
| try { |
| DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE, name, value, |
| false); |
| } finally { |
| getUiAutomation().dropShellPermissionIdentity(); |
| } |
| } |
| |
| static void setSystemProperty(String name, String value) throws Exception { |
| executeShellCommand("setprop " + name + " " + value); |
| } |
| |
| } |
| |