1 /*
2 * Copyright (C) 2021 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 android.companion.cts.common
18
19 import android.Manifest
20 import android.annotation.CallSuper
21 import android.app.Instrumentation
22 import android.app.UiAutomation
23 import android.companion.AssociationInfo
24 import android.companion.AssociationRequest
25 import android.companion.CompanionDeviceManager
26 import android.content.Context
27 import android.content.pm.PackageManager
28 import android.location.LocationManager
29 import android.net.MacAddress
30 import android.os.Process
31 import android.os.SystemClock.sleep
32 import android.os.SystemClock.uptimeMillis
33 import android.os.UserHandle
34 import android.util.Log
35 import androidx.test.platform.app.InstrumentationRegistry
36 import com.android.compatibility.common.util.SystemUtil
37 import java.io.IOException
38 import java.util.Locale
39 import kotlin.test.assertContains
40 import kotlin.test.assertContentEquals
41 import kotlin.test.assertEquals
42 import kotlin.test.assertFalse
43 import kotlin.test.assertIs
44 import kotlin.test.assertTrue
45 import kotlin.time.Duration
46 import kotlin.time.Duration.Companion.milliseconds
47 import kotlin.time.Duration.Companion.seconds
48 import org.junit.After
49 import org.junit.Assume.assumeTrue
50 import org.junit.AssumptionViolatedException
51 import org.junit.Before
52
53 /**
54 * A base class for CompanionDeviceManager [Tests][org.junit.Test] to extend.
55 */
56 abstract class TestBase {
57 protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
58 protected val uiAutomation: UiAutomation = instrumentation.uiAutomation
59
60 protected val context: Context = instrumentation.context
61 protected val userId = context.userId
62 protected val targetPackageName = instrumentation.targetContext.packageName
63 protected val targetUserId = instrumentation.targetContext.userId
64
65 protected val targetApp = AppHelper(instrumentation, userId, targetPackageName)
66
<lambda>null67 protected val pm: PackageManager by lazy { context.packageManager!! }
<lambda>null68 private val hasCompanionDeviceSetupFeature by lazy {
69 pm.hasSystemFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP)
70 }
71
<lambda>null72 protected val cdm: CompanionDeviceManager by lazy {
73 context.getSystemService(CompanionDeviceManager::class.java)!!
74 }
75
76 private val locationManager = context.getSystemService(LocationManager::class.java)!!
77
78 // CDM discovery requires location is enabled, enable the location if it was disabled.
79 private var locationWasEnabled: Boolean = false
80 private var userHandle: UserHandle = Process.myUserHandle()
81
82 @Before
base_setUpnull83 fun base_setUp() {
84 assumeTrue(hasCompanionDeviceSetupFeature)
85
86 // Remove all existing associations (for the user).
87 assertEmpty(withShellPermissionIdentity {
88 cdm.disassociateAll()
89 cdm.allAssociations
90 })
91
92 // Make sure CompanionDeviceServices are not bound.
93 assertValidCompanionDeviceServicesUnbind()
94 // Enable location if it was disabled.
95 enableLocation()
96 setUp()
97 }
98
99 @After
base_tearDownnull100 fun base_tearDown() {
101 if (!hasCompanionDeviceSetupFeature) return
102
103 tearDown()
104
105 // Remove all existing associations (for the user).
106 withShellPermissionIdentity { cdm.disassociateAll() }
107 // Disable the location if it was disabled.
108 disableLocation()
109 }
110
111 @CallSuper
setUpnull112 protected open fun setUp() {}
113
114 @CallSuper
tearDownnull115 protected open fun tearDown() {}
116
withShellPermissionIdentitynull117 protected fun <T> withShellPermissionIdentity(
118 vararg permissions: String,
119 block: () -> T
120 ): T {
121 if (permissions.isNotEmpty()) {
122 uiAutomation.adoptShellPermissionIdentity(*permissions)
123 } else {
124 uiAutomation.adoptShellPermissionIdentity()
125 }
126
127 try {
128 return block()
129 } finally {
130 uiAutomation.dropShellPermissionIdentity()
131 }
132 }
133
createSelfManagedAssociationnull134 protected fun createSelfManagedAssociation(
135 displayName: String,
136 onAssociationCreatedAction: ((AssociationInfo) -> Unit)? = null
137 ): Int {
138 val callback = RecordingCallback(onAssociationCreatedAction = onAssociationCreatedAction)
139 val request: AssociationRequest = AssociationRequest.Builder()
140 .setSelfManaged(true)
141 .setDisplayName(displayName)
142 .build()
143 callback.assertInvokedByActions {
144 withShellPermissionIdentity(Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) {
145 cdm.associate(request, SIMPLE_EXECUTOR, callback)
146 }
147 }
148
149 val callbackInvocation = callback.invocations.first()
150 assertIs<RecordingCallback.OnAssociationCreated>(callbackInvocation)
151 return callbackInvocation.associationInfo.id
152 }
153
runShellCommandnull154 protected fun runShellCommand(cmd: String) = instrumentation.runShellCommand(cmd)
155
156 private fun CompanionDeviceManager.disassociateAll() =
157 allAssociations.forEach { disassociate(it.id) }
158
setSystemPropertyDurationnull159 protected fun setSystemPropertyDuration(duration: Duration, systemPropertyTag: String) =
160 instrumentation.setSystemProp(
161 systemPropertyTag,
162 duration.inWholeMilliseconds.toString()
163 )
164
165 private fun enableLocation() {
166 locationWasEnabled = locationManager.isLocationEnabledForUser(userHandle)
167 if (!locationWasEnabled) {
168 withShellPermissionIdentity {
169 locationManager.setLocationEnabledForUser(true, userHandle)
170 }
171 }
172 }
173
disableLocationnull174 private fun disableLocation() {
175 if (!locationWasEnabled) {
176 withShellPermissionIdentity {
177 locationManager.setLocationEnabledForUser(false, userHandle)
178 }
179 }
180 }
181 }
182
183 const val TAG = "CtsCompanionDeviceManagerTestCases"
184
185 /** See [com.android.server.companion.CompanionDeviceServiceConnector.UNBIND_POST_DELAY_MS]. */
186 private val UNBIND_DELAY_DURATION = 5.seconds
187
assumeThatnull188 fun <T> assumeThat(message: String, obj: T, assumption: (T) -> Boolean) {
189 if (!assumption(obj)) throw AssumptionViolatedException(message)
190 }
191
assertApplicationBindsnull192 fun assertApplicationBinds(cdm: CompanionDeviceManager) {
193 assertTrue {
194 waitFor(timeout = 1.seconds, interval = 100.milliseconds) {
195 cdm.isCompanionApplicationBound
196 }
197 }
198 }
199
assertApplicationUnbindsnull200 fun assertApplicationUnbinds(cdm: CompanionDeviceManager) {
201 assertTrue {
202 waitFor(timeout = 1.seconds.plus(UNBIND_DELAY_DURATION), interval = 100.milliseconds) {
203 !cdm.isCompanionApplicationBound
204 }
205 }
206 }
207
assertApplicationRemainsBoundnull208 fun assertApplicationRemainsBound(cdm: CompanionDeviceManager) {
209 assertFalse {
210 waitFor(timeout = 3.seconds.plus(UNBIND_DELAY_DURATION), interval = 100.milliseconds) {
211 !cdm.isCompanionApplicationBound
212 }
213 }
214 }
215
<lambda>null216 fun <T> assertEmpty(list: Collection<T>) = assertTrue("Collection is not empty") { list.isEmpty() }
217
assertAssociationsnull218 fun assertAssociations(
219 actual: List<AssociationInfo>,
220 expected: Set<Pair<String, MacAddress?>>
221 ) = assertEquals(actual = actual.map { it.packageName to it.deviceMacAddress }.toSet(),
222 expected = expected)
223
assertSelfManagedAssociationsnull224 fun assertSelfManagedAssociations(
225 actual: List<AssociationInfo>,
226 expected: Set<Pair<String, Int>>
227 ) = assertEquals(actual = actual.map { it.packageName to it.id }.toSet(),
228 expected = expected)
229
230 /**
231 * Assert that CDM binds valid CompanionDeviceServices, both primary and secondary.
232 * Use when services are expected to switch its state to "bound".
233 */
assertValidCompanionDeviceServicesBindnull234 fun assertValidCompanionDeviceServicesBind() =
235 assertTrue("Both valid CompanionDeviceServices - Primary and Secondary - should bind") {
236 waitFor(timeout = 1.seconds, interval = 100.milliseconds) {
237 PrimaryCompanionService.isBound && SecondaryCompanionService.isBound
238 }
239 }
240
241 /**
242 * Assert both primary and secondary CompanionDeviceServices stay bound.
243 * Use when services are expected to be in "bound" state already.
244 */
assertValidCompanionDeviceServicesRemainBoundnull245 fun assertValidCompanionDeviceServicesRemainBound() =
246 assertFalse("Both valid CompanionDeviceServices should stay bound") {
247 waitFor(timeout = 3.seconds.plus(UNBIND_DELAY_DURATION), interval = 100.milliseconds) {
248 !PrimaryCompanionService.isBound || !SecondaryCompanionService.isBound
249 }
250 }
251
252 /**
253 * Assert that CDM unbinds valid CompanionDeviceServices, both primary and secondary.
254 * Use when services are expected to switch its state to "unbound".
255 */
assertValidCompanionDeviceServicesUnbindnull256 fun assertValidCompanionDeviceServicesUnbind() =
257 assertTrue("CompanionDeviceServices should not bind") {
258 waitFor(timeout = 1.seconds.plus(UNBIND_DELAY_DURATION), interval = 100.milliseconds) {
259 !PrimaryCompanionService.isBound && !SecondaryCompanionService.isBound
260 }
261 }
262
263 /**
264 * Assert that neither primary nor secondary CompanionDeviceService is bound.
265 * Use when services are expected to be in "unbound" state already.
266 */
assertValidCompanionDeviceServicesRemainUnboundnull267 fun assertValidCompanionDeviceServicesRemainUnbound() =
268 assertFalse("CompanionDeviceServices should not be bound") {
269 waitFor(timeout = 3.seconds, interval = 100.milliseconds) {
270 PrimaryCompanionService.isBound || SecondaryCompanionService.isBound
271 }
272 }
273
274 /**
275 * Assert that CDM did not bind invalid CompanionDeviceServices
276 * (i.e. missing permission or intent-filter).
277 */
assertInvalidCompanionDeviceServicesNotBoundnull278 fun assertInvalidCompanionDeviceServicesNotBound() =
279 assertFalse("CompanionDeviceServices that do not require " +
280 "BIND_COMPANION_DEVICE_SERVICE permission or do not declare an intent-filter for " +
281 "\"android.companion.CompanionDeviceService\" action should not be bound") {
282 MissingPermissionCompanionService.isBound ||
283 MissingIntentFilterActionCompanionService.isBound
284 }
285
286 /**
287 * Assert that device (dis)appearance detection callback is only triggered for the primary
288 * CompanionDeviceService and not on any of the non-primary or invalid CompanionDeviceServices.
289 */
assertOnlyPrimaryCompanionDeviceServiceNotifiednull290 fun assertOnlyPrimaryCompanionDeviceServiceNotified(associationId: Int, appeared: Boolean) {
291 val snapshotSecondary = HashSet(SecondaryCompanionService.connectedDevices)
292 val snapshotUnauthorized = HashSet(MissingPermissionCompanionService.connectedDevices)
293 val snapshotInvalid = HashSet(MissingIntentFilterActionCompanionService.connectedDevices)
294
295 // Check that the primary CompanionDeviceService received onDevice(Dis)Appeared() callback
296 if (appeared) {
297 PrimaryCompanionService.waitAssociationToAppear(associationId)
298 assertContains(PrimaryCompanionService.associationIdsForConnectedDevices, associationId)
299 } else {
300 PrimaryCompanionService.waitAssociationToDisappear(associationId)
301 assertFalse(PrimaryCompanionService.associationIdsForConnectedDevices
302 .contains(associationId))
303 }
304
305 // ... while neither the non-primary nor incorrectly defined CompanionDeviceServices -
306 // have NOT. (Give it 1 more second.)
307 sleepFor(1.seconds)
308 assertContentEquals(snapshotSecondary, SecondaryCompanionService.connectedDevices)
309 assertContentEquals(snapshotUnauthorized, MissingPermissionCompanionService.connectedDevices)
310 assertContentEquals(snapshotInvalid, MissingIntentFilterActionCompanionService.connectedDevices)
311 }
312
313 /**
314 * @return whether the condition was met before time ran out.
315 */
waitFornull316 fun waitFor(
317 timeout: Duration = 10.seconds,
318 interval: Duration = 1.seconds,
319 condition: () -> Boolean
320 ): Boolean {
321 val startTime = uptimeMillis()
322 while (!condition()) {
323 if (uptimeMillis() - startTime > timeout.inWholeMilliseconds) return false
324 sleep(interval.inWholeMilliseconds)
325 }
326 return true
327 }
328
waitForResultnull329 fun <R> waitForResult(
330 timeout: Duration = 10.seconds,
331 interval: Duration = 1.seconds,
332 block: () -> R
333 ): R? {
334 val startTime = uptimeMillis()
335 while (true) {
336 val result: R = block()
337 if (result != null) return result
338 sleep(interval.inWholeMilliseconds)
339 if (uptimeMillis() - startTime > timeout.inWholeMilliseconds) return null
340 }
341 }
342
runShellCommandnull343 fun Instrumentation.runShellCommand(cmd: String): String {
344 Log.i(TAG, "Running shell command: '$cmd'")
345 try {
346 val out = SystemUtil.runShellCommand(this, cmd)
347 Log.i(TAG, "Out:\n$out")
348 return out
349 } catch (e: IOException) {
350 Log.e(TAG, "Error running shell command: $cmd")
351 throw e
352 }
353 }
354
setSystemPropnull355 fun Instrumentation.setSystemProp(name: String, value: String) =
356 runShellCommand("setprop $name $value")
357
358 fun MacAddress.toUpperCaseString() = toString().uppercase(Locale.ROOT)
359
360 fun sleepFor(duration: Duration) = sleep(duration.inWholeMilliseconds)
361
362 fun killProcess(name: String) {
363 val pids = SystemUtil.runShellCommand("pgrep $name").trim().split("\\s+".toRegex())
364 for (pid: String in pids) {
365 // Make sure that it is the intended process before killing it.
366 val process = SystemUtil.runShellCommand("ps $pid")
367 if (process.contains("android.companion.cts.multiprocess")) {
368 Process.killProcess(Integer.valueOf(pid))
369 }
370 }
371 }
372