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