Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2437df27 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Fix PackageSetting uses-static-library and add adoptable storage tests"

parents 5f621a1f 8b83879e
Loading
Loading
Loading
Loading
+19 −25
Original line number Diff line number Diff line
@@ -306,8 +306,8 @@ public final class Settings {
    private final ArrayMap<String, KernelPackageState> mKernelMapping = new ArrayMap<>();

    // List of replaced system applications
    private final ArrayMap<String, PackageSetting> mDisabledSysPackages =
        new ArrayMap<String, PackageSetting>();
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    final ArrayMap<String, PackageSetting> mDisabledSysPackages = new ArrayMap<>();

    /** List of packages that are blocked for uninstall for specific users */
    private final SparseArray<ArraySet<String>> mBlockUninstallPackages = new SparseArray<>();
@@ -2174,13 +2174,6 @@ public final class Settings {

    void readUsesStaticLibLPw(XmlPullParser parser, PackageSetting outPs)
            throws IOException, XmlPullParserException {
        int outerDepth = parser.getDepth();
        int type;
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                continue;
            }
        String libName = parser.getAttributeValue(null, ATTR_NAME);
        String libVersionStr = parser.getAttributeValue(null, ATTR_VERSION);

@@ -2200,7 +2193,6 @@ public final class Settings {

        XmlUtils.skipCurrentTag(parser);
    }
    }

    void writeUsesStaticLibLPw(XmlSerializer serializer, String[] usesStaticLibraries,
            long[] usesStaticLibraryVersions) throws IOException {
@@ -3875,6 +3867,8 @@ public final class Settings {
                    readDomainVerificationLPw(parser, packageSetting);
                } else if (tagName.equals(TAG_MIME_GROUP)) {
                    packageSetting.mimeGroups = readMimeGroupLPw(parser, packageSetting.mimeGroups);
                } else if (tagName.equals(TAG_USES_STATIC_LIB)) {
                    readUsesStaticLibLPw(parser, packageSetting);
                } else {
                    PackageManagerService.reportSettingsProblem(Log.WARN,
                            "Unknown element under <package>: " + parser.getName());
+3 −0
Original line number Diff line number Diff line
@@ -21,12 +21,15 @@ java_test_host {
        "truth-prebuilt",
    ],
    static_libs: [
        "cts-host-utils",
        "frameworks-base-hostutils",
        "PackageManagerServiceHostTestsIntentVerifyUtils",
    ],
    test_suites: ["general-tests"],
    java_resources: [
        ":PackageManagerTestAppDeclaresStaticLibrary",
        ":PackageManagerTestAppStub",
        ":PackageManagerTestAppUsesStaticLibrary",
        ":PackageManagerTestAppVersion1",
        ":PackageManagerTestAppVersion2",
        ":PackageManagerTestAppVersion3",
+59 −8
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.server.pm.test

import com.android.internal.util.test.SystemPreparer
import com.android.tradefed.device.ITestDevice
import com.google.common.truth.Truth
import org.junit.rules.TemporaryFolder
import java.io.File
import java.io.FileOutputStream
@@ -48,6 +49,44 @@ internal fun ITestDevice.installJavaResourceApk(
internal fun ITestDevice.uninstallPackages(vararg pkgNames: String) =
        pkgNames.forEach { uninstallPackage(it) }

/**
 * Retry [block] a total of [maxAttempts] times, waiting [millisBetweenAttempts] milliseconds
 * between each iteration, until a non-null result is returned, providing that result back to the
 * caller.
 *
 * If an [AssertionError] is thrown by the [block] and a non-null result is never returned, that
 * error will be re-thrown. This allows the use of [Truth.assertThat] to indicate success while
 * providing a meaningful error message in case of failure.
 */
internal fun <T> retryUntilNonNull(
    maxAttempts: Int = 10,
    millisBetweenAttempts: Long = 1000,
    block: () -> T?
): T {
    var attempt = 0
    var failure: AssertionError? = null
    while (attempt++ < maxAttempts) {
        val result = try {
            block()
        } catch (e: AssertionError) {
            failure = e
            null
        }

        if (result != null) {
            return result
        } else {
            Thread.sleep(millisBetweenAttempts)
        }
    }

    throw failure ?: AssertionError("Never succeeded")
}

internal fun retryUntilSuccess(block: () -> Boolean) {
    retryUntilNonNull { block().takeIf { it } }
}

internal object HostUtils {

    fun getDataDir(device: ITestDevice, pkgName: String) =
@@ -78,6 +117,25 @@ internal object HostUtils {
     * dumpsys package and therefore device.getAppPackageInfo doesn't work immediately after reboot,
     * so the following methods parse the package dump directly to see if the path matches.
     */

    /**
     * Reads the pm dump for a package name starting from the Packages: metadata section until
     * the following section.
     */
    fun packageSection(
        device: ITestDevice,
        pkgName: String,
        sectionName: String = "Packages"
    ) = device.executeShellCommand("pm dump $pkgName")
            .lineSequence()
            .dropWhile { !it.startsWith(sectionName) } // Wait until the header
            .drop(1) // Drop the header itself
            .takeWhile {
                // Until next top level header, a non-empty line that doesn't start with whitespace
                it.isEmpty() || it.first().isWhitespace()
            }
            .map(String::trim)

    fun getCodePaths(device: ITestDevice, pkgName: String) =
            device.executeShellCommand("pm dump $pkgName")
                    .lineSequence()
@@ -87,14 +145,7 @@ internal object HostUtils {
                    .toList()

    private fun userIdLineSequence(device: ITestDevice, pkgName: String) =
            device.executeShellCommand("pm dump $pkgName")
                    .lineSequence()
                    .dropWhile { !it.startsWith("Packages:") }
                    .takeWhile {
                        !it.startsWith("Hidden system packages:") &&
                                !it.startsWith("Queries:")
                    }
                    .map(String::trim)
            packageSection(device, pkgName)
                    .filter { it.startsWith("User ") }

    fun getUserIdToPkgEnabledState(device: ITestDevice, pkgName: String) =
+255 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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 com.android.server.pm.test

import android.cts.host.utils.DeviceJUnit4ClassRunnerWithParameters
import android.cts.host.utils.DeviceJUnit4Parameterized
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.io.File
import java.util.regex.Pattern

/**
 * Verifies PackageManagerService behavior when an app is moved to an adoptable storage device.
 *
 * Also has the effect of verifying system behavior when the PackageSetting for a package has no
 * corresponding AndroidPackage which can be parsed from the APK on disk. This is done by removing
 * the storage device and causing a reboot, at which point PMS will read PackageSettings from disk
 * and fail to find the package path.
 */
@RunWith(DeviceJUnit4Parameterized::class)
@Parameterized.UseParametersRunnerFactory(
        DeviceJUnit4ClassRunnerWithParameters.RunnerFactory::class)
class SdCardEjectionTests : BaseHostJUnit4Test() {

    companion object {
        private const val VERSION_DECLARES = "PackageManagerTestAppDeclaresStaticLibrary.apk"
        private const val VERSION_DECLARES_PKG_NAME =
                "com.android.server.pm.test.test_app_declares_static_library"
        private const val VERSION_USES = "PackageManagerTestAppUsesStaticLibrary.apk"
        private const val VERSION_USES_PKG_NAME =
                "com.android.server.pm.test.test_app_uses_static_library"

        // TODO(chiuwinson): Use the HostUtils constants when merged
        private const val TEST_PKG_NAME = "com.android.server.pm.test.test_app"
        private const val VERSION_ONE = "PackageManagerTestAppVersion1.apk"

        @Parameterized.Parameters(name = "reboot={0}")
        @JvmStatic
        fun parameters() = arrayOf(false, true)

        data class Volume(
            val diskId: String,
            val fsUuid: String
        )
    }

    @Rule
    @JvmField
    val tempFolder = TemporaryFolder()

    @Parameterized.Parameter(0)
    @JvmField
    var reboot: Boolean = false

    @Before
    @After
    fun removePackagesAndDeleteVirtualDisk() {
        device.uninstallPackages(VERSION_ONE, VERSION_USES_PKG_NAME, VERSION_DECLARES_PKG_NAME)
        removeVirtualDisk()
        device.reboot()
    }

    @Test
    fun launchActivity() {
        val hostApkFile = HostUtils.copyResourceToHostFile(VERSION_ONE, tempFolder.newFile())
        assertThat(device.installPackage(hostApkFile, true)).isNull()

        val errorRegex = Pattern.compile("error", Pattern.CASE_INSENSITIVE)
        fun assertStartResponse(launched: Boolean) {
            val response = device.executeShellCommand("am start -n $TEST_PKG_NAME/.TestActivity")
            if (launched) {
                assertThat(response).doesNotContainMatch(errorRegex)
            } else {
                assertThat(response).containsMatch(errorRegex)
            }
        }

        assertStartResponse(launched = true)

        val volume = initializeVirtualDisk()

        movePackage(TEST_PKG_NAME, volume)
        assertStartResponse(launched = true)

        unmount(volume, TEST_PKG_NAME)
        assertStartResponse(launched = false)

        remount(volume, hostApkFile, TEST_PKG_NAME)
        assertStartResponse(launched = true)
    }

    @Test
    fun uninstallStaticLibraryInUse() {
        assertThat(device.installJavaResourceApk(tempFolder, VERSION_DECLARES)).isNull()

        val usesApkFile = HostUtils.copyResourceToHostFile(VERSION_USES, tempFolder.newFile())
        assertThat(device.installPackage(usesApkFile, true)).isNull()

        fun assertUninstallFails() = assertThat(device.uninstallPackage(VERSION_DECLARES_PKG_NAME))
                .isEqualTo("DELETE_FAILED_USED_SHARED_LIBRARY")

        assertUninstallFails()

        val volume = initializeVirtualDisk()

        movePackage(VERSION_USES_PKG_NAME, volume)
        assertUninstallFails()

        unmount(volume, VERSION_USES_PKG_NAME)
        assertUninstallFails()

        remount(volume, usesApkFile, VERSION_USES_PKG_NAME)
        assertUninstallFails()

        // Check that install in the correct order (uses first) passes
        assertThat(device.uninstallPackage(VERSION_USES_PKG_NAME)).isNull()
        assertThat(device.uninstallPackage(VERSION_DECLARES_PKG_NAME)).isNull()
    }

    private fun initializeVirtualDisk(): Volume {
        // Rather than making any assumption about what disks/volumes exist on the device,
        // save the existing disks/volumes to compare and see when a new one pops up, assuming
        // it was created as the result of the calls in this test.
        val existingDisks = device.executeShellCommand("sm list-disks adoptable").lines()
        val existingVolumes = device.executeShellCommand("sm list-volumes private").lines()
        device.executeShellCommand("sm set-virtual-disk true")

        val diskId = retryUntilNonNull {
            device.executeShellCommand("sm list-disks adoptable")
                    .lines()
                    .filterNot(existingDisks::contains)
                    .filterNot(String::isEmpty)
                    .firstOrNull()
        }

        device.executeShellCommand("sm partition $diskId private")

        return retrieveNewVolume(existingVolumes)
    }

    private fun retrieveNewVolume(existingVolumes: List<String>): Volume {
        val newVolume = retryUntilNonNull {
            device.executeShellCommand("sm list-volumes private")
                    .lines()
                    .toMutableList()
                    .apply { removeAll(existingVolumes) }
                    .firstOrNull()
                    ?.takeIf { it.isNotEmpty() }
        }

        val sections = newVolume.split(" ")
        return Volume(diskId = sections.first(), fsUuid = sections.last()).also {
            assertThat(it.diskId).isNotEmpty()
            assertThat(it.fsUuid).isNotEmpty()
        }
    }

    private fun removeVirtualDisk() {
        device.executeShellCommand("sm set-virtual-disk false")
        retryUntilSuccess {
            !device.executeShellCommand("sm list-volumes").contains("ejecting")
        }
    }

    private fun movePackage(pkgName: String, volume: Volume) {
        // TODO(b/167241596): oat dir must exist for a move install
        val codePath = HostUtils.getCodePaths(device, pkgName).first()
        device.executeShellCommand("mkdir $codePath/oat")
        assertThat(device.executeShellCommand(
                "pm move-package $pkgName ${volume.fsUuid}").trim())
                .isEqualTo("Success")
    }

    private fun unmount(volume: Volume, pkgName: String) {
        assertThat(device.executeShellCommand("sm unmount ${volume.diskId}")).isEmpty()
        if (reboot) {
            // The system automatically mounts the virtual disk on startup, which would mean the
            // app files are available to the system. To prevent this, disable the disk entirely.
            // TODO: There must be a better way to prevent it from auto-mounting.
            removeVirtualDisk()
            device.reboot()
        } else {
            // Because PackageManager unmount scan is asynchronous, need to retry until the package
            // has been unloaded. This only has to be done in the non-reboot case. Reboot will
            // clear the data structure by its nature.
            retryUntilSuccess {
                // The compiler section will print the state of the physical APK
                HostUtils.packageSection(device, pkgName, sectionName = "Compiler stats")
                        .any { it.contains("Unable to find package: $pkgName") }
            }
        }
    }

    private fun remount(volume: Volume, hostApkFile: File, pkgName: String) {
        if (reboot) {
            // Because the disk was destroyed when unmounting, it now has to be rebuilt manually.
            // This enables a new virtual disk, unmounts it, mutates its UUID to match the previous
            // partition's, remounts it, and pushes the base.apk back onto the device. This
            // simulates the same disk being re-inserted. This is very hacky.
            val newVolume = initializeVirtualDisk()
            val mountPoint = device.executeShellCommand("mount")
                    .lineSequence()
                    .first { it.contains(newVolume.fsUuid) }
                    .takeWhile { !it.isWhitespace() }

            device.executeShellCommand("sm unmount ${newVolume.diskId}")

            // Save without renamed UUID to compare and see when the renamed pops up
            val existingVolumes = device.executeShellCommand("sm list-volumes private").lines()

            device.executeShellCommand("make_f2fs -U ${volume.fsUuid} $mountPoint")
            device.executeShellCommand("sm mount ${newVolume.diskId}")

            val reparsedVolume = retrieveNewVolume(existingVolumes)
            assertThat(reparsedVolume.fsUuid).isEqualTo(volume.fsUuid)

            val codePath = HostUtils.getCodePaths(device, pkgName).first()
            device.pushFile(hostApkFile, "$codePath/base.apk")

            // Unmount so following remount will re-kick package scan
            device.executeShellCommand("sm unmount ${newVolume.diskId}")
        }

        device.executeShellCommand("sm mount ${volume.diskId}")

        // Because PackageManager remount scan is asynchronous, need to retry until the package
        // has been loaded and added to the internal structures. Otherwise resolution will fail.
        retryUntilSuccess {
            // The compiler section will print the state of the physical APK
            HostUtils.packageSection(device, pkgName, sectionName = "Compiler stats")
                    .none { it.contains("Unable to find package: $pkgName") }
        }
    }
}
Loading