1 /* 2 * Copyright (C) 2020 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 com.android.server.pm.test 18 19 import android.cts.host.utils.DeviceJUnit4ClassRunnerWithParameters 20 import android.cts.host.utils.DeviceJUnit4Parameterized 21 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test 22 import com.google.common.truth.Truth.assertThat 23 import org.junit.After 24 import org.junit.Before 25 import org.junit.Ignore 26 import org.junit.Rule 27 import org.junit.Test 28 import org.junit.rules.TemporaryFolder 29 import org.junit.runner.RunWith 30 import org.junit.runners.Parameterized 31 import java.io.File 32 import java.util.regex.Pattern 33 34 /** 35 * Verifies PackageManagerService behavior when an app is moved to an adoptable storage device. 36 * 37 * Also has the effect of verifying system behavior when the PackageSetting for a package has no 38 * corresponding AndroidPackage which can be parsed from the APK on disk. This is done by removing 39 * the storage device and causing a reboot, at which point PMS will read PackageSettings from disk 40 * and fail to find the package path. 41 */ 42 @RunWith(DeviceJUnit4Parameterized::class) 43 @Parameterized.UseParametersRunnerFactory( 44 DeviceJUnit4ClassRunnerWithParameters.RunnerFactory::class) 45 @Ignore("b/275403538") 46 class SdCardEjectionTests : BaseHostJUnit4Test() { 47 48 companion object { 49 private const val VERSION_DECLARES = "PackageManagerTestAppDeclaresStaticLibrary.apk" 50 private const val VERSION_DECLARES_PKG_NAME = 51 "com.android.server.pm.test.test_app_declares_static_library" 52 private const val VERSION_USES = "PackageManagerTestAppUsesStaticLibrary.apk" 53 private const val VERSION_USES_PKG_NAME = 54 "com.android.server.pm.test.test_app_uses_static_library" 55 56 // TODO(chiuwinson): Use the HostUtils constants when merged 57 private const val TEST_PKG_NAME = "com.android.server.pm.test.test_app" 58 private const val VERSION_ONE = "PackageManagerTestAppVersion1.apk" 59 60 @Parameterized.Parameters(name = "reboot={0}") 61 @JvmStatic 62 // TODO(b/275403538): re-enable non-reboot scenarios with better tracking of APK removal 63 fun parameters() = arrayOf(/*false, */true) 64 65 data class Volume( 66 val diskId: String, 67 val fsUuid: String 68 ) 69 } 70 71 @Rule 72 @JvmField 73 val tempFolder = TemporaryFolder() 74 75 @Parameterized.Parameter(0) 76 @JvmField 77 var reboot: Boolean = false 78 79 @Before 80 @After 81 fun removePackagesAndDeleteVirtualDisk() { 82 device.uninstallPackages(VERSION_ONE, VERSION_USES_PKG_NAME, VERSION_DECLARES_PKG_NAME) 83 removeVirtualDisk() 84 device.reboot() 85 } 86 87 @Test 88 fun launchActivity() { 89 val hostApkFile = HostUtils.copyResourceToHostFile(VERSION_ONE, tempFolder.newFile()) 90 assertThat(device.installPackage(hostApkFile, true)).isNull() 91 92 val errorRegex = Pattern.compile("error", Pattern.CASE_INSENSITIVE) 93 fun assertStartResponse(launched: Boolean) { 94 val response = device.executeShellCommand("am start -n $TEST_PKG_NAME/.TestActivity") 95 if (launched) { 96 assertThat(response).doesNotContainMatch(errorRegex) 97 } else { 98 assertThat(response).containsMatch(errorRegex) 99 } 100 } 101 102 assertStartResponse(launched = true) 103 104 val volume = initializeVirtualDisk() 105 106 movePackage(TEST_PKG_NAME, volume) 107 assertStartResponse(launched = true) 108 109 unmount(volume, TEST_PKG_NAME) 110 assertStartResponse(launched = false) 111 112 remount(volume, hostApkFile, TEST_PKG_NAME) 113 assertStartResponse(launched = true) 114 } 115 116 @Test 117 fun uninstallStaticLibraryInUse() { 118 assertThat(device.installJavaResourceApk(tempFolder, VERSION_DECLARES)).isNull() 119 120 val usesApkFile = HostUtils.copyResourceToHostFile(VERSION_USES, tempFolder.newFile()) 121 assertThat(device.installPackage(usesApkFile, true)).isNull() 122 123 fun assertUninstallFails() = assertThat(device.uninstallPackage(VERSION_DECLARES_PKG_NAME)) 124 .isEqualTo("DELETE_FAILED_USED_SHARED_LIBRARY") 125 126 assertUninstallFails() 127 128 val volume = initializeVirtualDisk() 129 130 movePackage(VERSION_USES_PKG_NAME, volume) 131 assertUninstallFails() 132 133 unmount(volume, VERSION_USES_PKG_NAME) 134 assertUninstallFails() 135 136 remount(volume, usesApkFile, VERSION_USES_PKG_NAME) 137 assertUninstallFails() 138 139 // Check that install in the correct order (uses first) passes 140 assertThat(device.uninstallPackage(VERSION_USES_PKG_NAME)).isNull() 141 assertThat(device.uninstallPackage(VERSION_DECLARES_PKG_NAME)).isNull() 142 } 143 144 private fun initializeVirtualDisk(): Volume { 145 // Rather than making any assumption about what disks/volumes exist on the device, 146 // save the existing disks/volumes to compare and see when a new one pops up, assuming 147 // it was created as the result of the calls in this test. 148 val existingDisks = device.executeShellCommand("sm list-disks adoptable").lines() 149 val existingVolumes = device.executeShellCommand("sm list-volumes private").lines() 150 device.executeShellCommand("sm set-virtual-disk true") 151 152 val diskId = retryUntilNonNull { 153 device.executeShellCommand("sm list-disks adoptable") 154 .lines() 155 .filterNot(existingDisks::contains) 156 .filterNot(String::isEmpty) 157 .firstOrNull() 158 } 159 160 device.executeShellCommand("sm partition $diskId private") 161 162 return retrieveNewVolume(existingVolumes) 163 } 164 165 private fun retrieveNewVolume(existingVolumes: List<String>): Volume { 166 val newVolume = retryUntilNonNull { 167 device.executeShellCommand("sm list-volumes private") 168 .lines() 169 .toMutableList() 170 .apply { removeAll(existingVolumes) } 171 .firstOrNull() 172 ?.takeIf { it.isNotEmpty() } 173 } 174 175 val sections = newVolume.split(" ") 176 return Volume(diskId = sections.first(), fsUuid = sections.last()).also { 177 assertThat(it.diskId).isNotEmpty() 178 assertThat(it.fsUuid).isNotEmpty() 179 } 180 } 181 182 private fun removeVirtualDisk() { 183 device.executeShellCommand("sm set-virtual-disk false") 184 retryUntilSuccess { 185 !device.executeShellCommand("sm list-volumes").contains("ejecting") 186 } 187 } 188 189 private fun movePackage(pkgName: String, volume: Volume) { 190 // TODO(b/167241596): oat dir must exist for a move install 191 val codePath = HostUtils.getCodePaths(device, pkgName).first() 192 device.executeShellCommand("mkdir $codePath/oat") 193 assertThat(device.executeShellCommand( 194 "pm move-package $pkgName ${volume.fsUuid}").trim()) 195 .isEqualTo("Success") 196 } 197 198 private fun unmount(volume: Volume, pkgName: String) { 199 assertThat(device.executeShellCommand("sm unmount ${volume.diskId}")).isEmpty() 200 if (reboot) { 201 // The system automatically mounts the virtual disk on startup, which would mean the 202 // app files are available to the system. To prevent this, disable the disk entirely. 203 // TODO: There must be a better way to prevent it from auto-mounting. 204 removeVirtualDisk() 205 device.reboot() 206 } 207 } 208 209 private fun remount(volume: Volume, hostApkFile: File, pkgName: String) { 210 if (reboot) { 211 // Because the disk was destroyed when unmounting, it now has to be rebuilt manually. 212 // This enables a new virtual disk, unmounts it, mutates its UUID to match the previous 213 // partition's, remounts it, and pushes the base.apk back onto the device. This 214 // simulates the same disk being re-inserted. This is very hacky. 215 val newVolume = initializeVirtualDisk() 216 val mountPoint = device.executeShellCommand("mount") 217 .lineSequence() 218 .first { it.contains(newVolume.fsUuid) } 219 .takeWhile { !it.isWhitespace() } 220 221 device.executeShellCommand("sm unmount ${newVolume.diskId}") 222 223 // Save without renamed UUID to compare and see when the renamed pops up 224 val existingVolumes = device.executeShellCommand("sm list-volumes private").lines() 225 226 device.executeShellCommand("make_f2fs -U ${volume.fsUuid} $mountPoint") 227 device.executeShellCommand("sm mount ${newVolume.diskId}") 228 229 val reparsedVolume = retrieveNewVolume(existingVolumes) 230 assertThat(reparsedVolume.fsUuid).isEqualTo(volume.fsUuid) 231 232 val codePath = HostUtils.getCodePaths(device, pkgName).first() 233 device.pushFile(hostApkFile, "$codePath/base.apk") 234 235 // Unmount so following remount will re-kick package scan 236 device.executeShellCommand("sm unmount ${newVolume.diskId}") 237 } 238 239 device.executeShellCommand("sm mount ${volume.diskId}") 240 241 // Because PackageManager remount scan is asynchronous, need to retry until the package 242 // has been loaded and added to the internal structures. Otherwise resolution will fail. 243 retryUntilSuccess { 244 // The compiler section will print the state of the physical APK 245 HostUtils.packageSection(device, pkgName, sectionName = "Compiler stats") 246 .none { it.contains("Unable to find package: $pkgName") } 247 } 248 } 249 } 250