1 /*
<lambda>null2 * 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 com.android.internal.util.test.SystemPreparer
20 import com.android.tradefed.device.ITestDevice
21 import com.google.common.truth.Truth
22 import org.junit.rules.TemporaryFolder
23 import java.io.File
24 import java.io.FileOutputStream
25
26 internal fun SystemPreparer.pushApk(javaResourceName: String, partition: Partition) =
27 pushResourceFile(javaResourceName, HostUtils.makePathForApk(javaResourceName, partition)
28 .toString())
29
30 internal fun SystemPreparer.deleteApkFolders(
31 partition: Partition,
32 vararg javaResourceNames: String
33 ) = apply {
34 javaResourceNames.forEach {
35 deleteFile(partition.baseAppFolder.resolve(it.removeSuffix(".apk")).toString())
36 }
37 }
38
installJavaResourceApknull39 internal fun ITestDevice.installJavaResourceApk(
40 tempFolder: TemporaryFolder,
41 javaResource: String,
42 reinstall: Boolean = true,
43 extraArgs: Array<String> = emptyArray()
44 ): String? {
45 val file = HostUtils.copyResourceToHostFile(javaResource, tempFolder.newFile())
46 return installPackage(file, reinstall, *extraArgs)
47 }
48
uninstallPackagesnull49 internal fun ITestDevice.uninstallPackages(vararg pkgNames: String) =
50 pkgNames.forEach { uninstallPackage(it) }
51
52 /**
53 * Retry [block] a total of [maxAttempts] times, waiting [millisBetweenAttempts] milliseconds
54 * between each iteration, until a non-null result is returned, providing that result back to the
55 * caller.
56 *
57 * If an [AssertionError] is thrown by the [block] and a non-null result is never returned, that
58 * error will be re-thrown. This allows the use of [Truth.assertThat] to indicate success while
59 * providing a meaningful error message in case of failure.
60 */
retryUntilNonNullnull61 internal fun <T> retryUntilNonNull(
62 maxAttempts: Int = 10,
63 millisBetweenAttempts: Long = 1000,
64 block: () -> T?
65 ): T {
66 var attempt = 0
67 var failure: AssertionError? = null
68 while (attempt++ < maxAttempts) {
69 val result = try {
70 block()
71 } catch (e: AssertionError) {
72 failure = e
73 null
74 }
75
76 if (result != null) {
77 return result
78 } else {
79 Thread.sleep(millisBetweenAttempts)
80 }
81 }
82
83 throw failure ?: AssertionError("Never succeeded")
84 }
85
retryUntilSuccessnull86 internal fun retryUntilSuccess(block: () -> Boolean) {
87 retryUntilNonNull { block().takeIf { it } }
88 }
89
90 internal object HostUtils {
91
getDataDirnull92 fun getDataDir(device: ITestDevice, pkgName: String) =
93 device.executeShellCommand("dumpsys package $pkgName")
94 .lineSequence()
95 .map(String::trim)
96 .single { it.startsWith("dataDir=") }
97 .removePrefix("dataDir=")
98
makePathForApknull99 fun makePathForApk(fileName: String, partition: Partition) =
100 makePathForApk(File(fileName), partition)
101
102 fun makePathForApk(file: File, partition: Partition) =
103 partition.baseAppFolder
104 .resolve(file.nameWithoutExtension)
105 .resolve(file.name)
106
107 fun copyResourceToHostFile(javaResourceName: String, file: File): File {
108 javaClass.classLoader!!.getResource(javaResourceName).openStream().use { input ->
109 FileOutputStream(file).use { output ->
110 input.copyTo(output)
111 }
112 }
113 return file
114 }
115
116 /**
117 * dumpsys package and therefore device.getAppPackageInfo doesn't work immediately after reboot,
118 * so the following methods parse the package dump directly to see if the path matches.
119 */
120
121 /**
122 * Reads the pm dump for a package name starting from the Packages: metadata section until
123 * the following section.
124 */
packageSectionnull125 fun packageSection(
126 device: ITestDevice,
127 pkgName: String,
128 sectionName: String = "Packages"
129 ) = device.executeShellCommand("pm dump $pkgName")
130 .lineSequence()
131 .dropWhile { !it.startsWith(sectionName) } // Wait until the header
132 .drop(1) // Drop the header itself
<lambda>null133 .takeWhile {
134 // Until next top level header, a non-empty line that doesn't start with whitespace
135 it.isEmpty() || it.first().isWhitespace()
136 }
137 .map(String::trim)
138
getCodePathsnull139 fun getCodePaths(device: ITestDevice, pkgName: String) =
140 device.executeShellCommand("pm dump $pkgName")
141 .lineSequence()
142 .map(String::trim)
143 .filter { it.startsWith("codePath=") }
<lambda>null144 .map { it.removePrefix("codePath=") }
145 .toList()
146
userIdLineSequencenull147 private fun userIdLineSequence(device: ITestDevice, pkgName: String) =
148 packageSection(device, pkgName)
149 .filter { it.startsWith("User ") }
150
getUserIdToPkgEnabledStatenull151 fun getUserIdToPkgEnabledState(device: ITestDevice, pkgName: String) =
152 userIdLineSequence(device, pkgName).associate {
153 val userId = it.removePrefix("User ")
154 .takeWhile(Char::isDigit)
155 .toInt()
156 val enabled = it.substringAfter("enabled=")
157 .takeWhile(Char::isDigit)
158 .toInt()
159 .let {
160 when (it) {
161 0, 1 -> true
162 else -> false
163 }
164 }
165 userId to enabled
166 }
167
getUserIdToPkgInstalledStatenull168 fun getUserIdToPkgInstalledState(device: ITestDevice, pkgName: String) =
169 userIdLineSequence(device, pkgName).associate {
170 val userId = it.removePrefix("User ")
171 .takeWhile(Char::isDigit)
172 .toInt()
173 val installed = it.substringAfter("installed=")
174 .takeWhile { !it.isWhitespace() }
175 .toBoolean()
176 userId to installed
177 }
178 }
179