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.annotation.UserIdInt
22 import android.app.Instrumentation
23 import android.app.UiAutomation
24 import android.companion.AssociationInfo
25 import android.companion.AssociationRequest
26 import android.companion.CompanionDeviceManager
27 import android.content.Context
28 import android.content.pm.PackageManager
29 import android.location.LocationManager
30 import android.net.MacAddress
31 import android.os.ParcelUuid
32 import android.os.Process
33 import android.os.SystemClock.sleep
34 import android.os.SystemClock.uptimeMillis
35 import android.os.UserHandle
36 import android.util.Log
37 import androidx.test.platform.app.InstrumentationRegistry
38 import com.android.compatibility.common.util.SystemUtil
39 import java.io.IOException
40 import java.util.Locale
41 import kotlin.test.assertContains
42 import kotlin.test.assertContentEquals
43 import kotlin.test.assertEquals
44 import kotlin.test.assertFalse
45 import kotlin.test.assertIs
46 import kotlin.test.assertTrue
47 import kotlin.test.fail
48 import kotlin.time.Duration
49 import kotlin.time.Duration.Companion.milliseconds
50 import kotlin.time.Duration.Companion.seconds
51 import org.junit.After
52 import org.junit.Assume.assumeTrue
53 import org.junit.AssumptionViolatedException
54 import org.junit.Before
55
56 /**
57 * A base class for CompanionDeviceManager [Tests][org.junit.Test] to extend.
58 */
59 abstract class TestBase {
60 protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
61 protected val uiAutomation: UiAutomation = instrumentation.uiAutomation
62
63 protected val context: Context = instrumentation.context
64 protected val userId = context.userId
65 protected val targetPackageName = instrumentation.targetContext.packageName
66 protected val targetUserId = instrumentation.targetContext.userId
67
68 protected val targetApp = AppHelper(instrumentation, userId, targetPackageName)
69
<lambda>null70 protected val pm: PackageManager by lazy { context.packageManager!! }
<lambda>null71 private val hasCompanionDeviceSetupFeature by lazy {
72 pm.hasSystemFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP)
73 }
74
<lambda>null75 protected val cdm: CompanionDeviceManager by lazy {
76 context.getSystemService(CompanionDeviceManager::class.java)!!
77 }
78
79 private val locationManager = context.getSystemService(LocationManager::class.java)!!
80
81 // CDM discovery requires location is enabled, enable the location if it was disabled.
82 private var locationWasEnabled: Boolean = false
83 private var userHandle: UserHandle = Process.myUserHandle()
84
85 @Before
base_setUpnull86 fun base_setUp() {
87 assumeTrue(hasCompanionDeviceSetupFeature)
88
89 // Remove all existing associations (for the user).
90 assertEmpty(withShellPermissionIdentity {
91 cdm.disassociateAll()
92 cdm.allAssociations
93 })
94
95 // Make sure CompanionDeviceServices are not bound.
96 assertValidCompanionDeviceServicesUnbind()
97 // Enable location if it was disabled.
98 enableLocation()
99 setUp()
100 }
101
102 @After
base_tearDownnull103 fun base_tearDown() {
104 if (!hasCompanionDeviceSetupFeature) return
105
106 tearDown()
107
108 // Remove all existing associations (for the user).
109 withShellPermissionIdentity { cdm.disassociateAll() }
110 // Disable the location if it was disabled.
111 disableLocation()
112 }
113
114 @CallSuper
setUpnull115 protected open fun setUp() {}
116
117 @CallSuper
tearDownnull118 protected open fun tearDown() {}
119
withShellPermissionIdentitynull120 protected fun <T> withShellPermissionIdentity(
121 vararg permissions: String,
122 block: () -> T
123 ): T {
124 if (permissions.isNotEmpty()) {
125 uiAutomation.adoptShellPermissionIdentity(*permissions)
126 } else {
127 uiAutomation.adoptShellPermissionIdentity()
128 }
129
130 try {
131 return block()
132 } finally {
133 uiAutomation.dropShellPermissionIdentity()
134 }
135 }
136
createSelfManagedAssociationnull137 protected fun createSelfManagedAssociation(
138 displayName: String,
139 onAssociationCreatedAction: ((AssociationInfo) -> Unit)? = null
140 ): Int {
141 val callback = RecordingCallback(onAssociationCreatedAction = onAssociationCreatedAction)
142 val request: AssociationRequest = AssociationRequest.Builder()
143 .setSelfManaged(true)
144 .setDisplayName(displayName)
145 .build()
146 callback.assertInvokedByActions {
147 withShellPermissionIdentity(Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) {
148 cdm.associate(request, SIMPLE_EXECUTOR, callback)
149 }
150 }
151
152 val callbackInvocation = callback.invocations.first()
153 assertIs<RecordingCallback.OnAssociationCreated>(callbackInvocation)
154 return callbackInvocation.associationInfo.id
155 }
156
runShellCommandnull157 protected fun runShellCommand(cmd: String) = instrumentation.runShellCommand(cmd)
158
159 private fun CompanionDeviceManager.disassociateAll() =
160 allAssociations.forEach { disassociate(it.id) }
161
setSystemPropertyDurationnull162 protected fun setSystemPropertyDuration(duration: Duration, systemPropertyTag: String) =
163 instrumentation.setSystemProp(
164 systemPropertyTag,
165 duration.inWholeMilliseconds.toString()
166 )
167
168 private fun enableLocation() {
169 locationWasEnabled = locationManager.isLocationEnabledForUser(userHandle)
170 if (!locationWasEnabled) {
171 withShellPermissionIdentity {
172 locationManager.setLocationEnabledForUser(true, userHandle)
173 }
174 }
175 }
176
disableLocationnull177 private fun disableLocation() {
178 if (!locationWasEnabled) {
179 withShellPermissionIdentity {
180 locationManager.setLocationEnabledForUser(false, userHandle)
181 }
182 }
183 }
184
simulateDeviceEventnull185 fun simulateDeviceEvent(associationId: Int, event: Int) =
186 runShellCommand("cmd companiondevice simulate-device-event $associationId $event")
187
188 fun simulateDeviceUuidEvent(uuid: ParcelUuid, event: Int) =
189 runShellCommand(
190 "cmd companiondevice simulate-device-uuid-event " +
191 "$uuid $targetPackageName $userId $event"
192 )
193
194 fun simulateDeviceEventDeviceLocked(associationId: Int, userId: Int, event: Int, uuid: String) {
195 runShellCommand(
196 "cmd companiondevice simulate-device-event-device-locked " +
197 "$associationId $userId $event $uuid"
198 )
199 }
200
simulateDeviceEventDeviceUnlockednull201 fun simulateDeviceEventDeviceUnlocked(userId: Int) {
202 runShellCommand("cmd companiondevice simulate-device-event-device-unlocked $userId")
203 }
204
startObservingDevicePresenceByUuidnull205 fun startObservingDevicePresenceByUuid(userId: Int, packageName: String, uuid: String) {
206 runShellCommand(
207 "cmd companiondevice start-observing-device-presence-uuid " +
208 "$userId $packageName $uuid"
209 )
210 }
211
stopObservingDevicePresenceByUuidnull212 fun stopObservingDevicePresenceByUuid(userId: Int, packageName: String, uuid: String) {
213 runShellCommand(
214 "cmd companiondevice stop-observing-device-presence-uuid " +
215 "$userId $packageName $uuid"
216 )
217 }
218 }
219
220 const val TAG = "CtsCompanionDeviceManagerTestCases"
221
222 /** See [com.android.server.companion.CompanionDeviceServiceConnector.UNBIND_POST_DELAY_MS]. */
223 private val UNBIND_DELAY_DURATION = 5.seconds
224
assumeThatnull225 fun <T> assumeThat(message: String, obj: T, assumption: (T) -> Boolean) {
226 if (!assumption(obj)) throw AssumptionViolatedException(message)
227 }
228
assertApplicationBindsnull229 fun assertApplicationBinds(cdm: CompanionDeviceManager) {
230 assertTrue {
231 waitFor(timeout = 1.seconds, interval = 100.milliseconds) {
232 cdm.isCompanionApplicationBound
233 }
234 }
235 }
236
assertApplicationUnbindsnull237 fun assertApplicationUnbinds(cdm: CompanionDeviceManager) {
238 assertTrue {
239 waitFor(timeout = 1.seconds.plus(UNBIND_DELAY_DURATION), interval = 100.milliseconds) {
240 !cdm.isCompanionApplicationBound
241 }
242 }
243 }
244
assertApplicationRemainsBoundnull245 fun assertApplicationRemainsBound(cdm: CompanionDeviceManager) {
246 assertFalse {
247 waitFor(timeout = 3.seconds.plus(UNBIND_DELAY_DURATION), interval = 100.milliseconds) {
248 !cdm.isCompanionApplicationBound
249 }
250 }
251 }
252
<lambda>null253 fun <T> assertEmpty(list: Collection<T>) = assertTrue("Collection is not empty") { list.isEmpty() }
254
assertAssociationsnull255 fun assertAssociations(
256 actual: List<AssociationInfo>,
257 expected: Set<Pair<String, MacAddress?>>
258 ) = assertEquals(actual = actual.map {
259 it.packageName to it.deviceMacAddress }.toSet(), expected = expected)
260
assertSelfManagedAssociationsnull261 fun assertSelfManagedAssociations(
262 actual: List<AssociationInfo>,
263 expected: Set<Pair<String, Int>>
264 ) = assertEquals(actual = actual.map { it.packageName to it.id }.toSet(), expected = expected)
265
266 /**
267 * Assert that CDM binds valid CompanionDeviceServices, both primary and secondary.
268 * Use when services are expected to switch its state to "bound".
269 */
assertValidCompanionDeviceServicesBindnull270 fun assertValidCompanionDeviceServicesBind() =
271 assertTrue("Both valid CompanionDeviceServices - Primary and Secondary - should bind") {
272 waitFor(timeout = 1.seconds, interval = 100.milliseconds) {
273 PrimaryCompanionService.isBound && SecondaryCompanionService.isBound
274 }
275 }
276
277 /**
278 * Assert both primary and secondary CompanionDeviceServices stay bound.
279 * Use when services are expected to be in "bound" state already.
280 */
assertValidCompanionDeviceServicesRemainBoundnull281 fun assertValidCompanionDeviceServicesRemainBound() =
282 assertFalse("Both valid CompanionDeviceServices should stay bound") {
283 waitFor(timeout = 3.seconds.plus(UNBIND_DELAY_DURATION), interval = 100.milliseconds) {
284 !PrimaryCompanionService.isBound || !SecondaryCompanionService.isBound
285 }
286 }
287
288 /**
289 * Assert that CDM unbinds valid CompanionDeviceServices, both primary and secondary.
290 * Use when services are expected to switch its state to "unbound".
291 */
assertValidCompanionDeviceServicesUnbindnull292 fun assertValidCompanionDeviceServicesUnbind() =
293 assertTrue("CompanionDeviceServices should not bind") {
294 waitFor(timeout = 1.seconds.plus(UNBIND_DELAY_DURATION), interval = 100.milliseconds) {
295 !PrimaryCompanionService.isBound && !SecondaryCompanionService.isBound
296 }
297 }
298
299 /**
300 * Assert that neither primary nor secondary CompanionDeviceService is bound.
301 * Use when services are expected to be in "unbound" state already.
302 */
assertValidCompanionDeviceServicesRemainUnboundnull303 fun assertValidCompanionDeviceServicesRemainUnbound() =
304 assertFalse("CompanionDeviceServices should not be bound") {
305 waitFor(timeout = 3.seconds, interval = 100.milliseconds) {
306 PrimaryCompanionService.isBound || SecondaryCompanionService.isBound
307 }
308 }
309
310 /**
311 * Assert that CDM did not bind invalid CompanionDeviceServices
312 * (i.e. missing permission or intent-filter).
313 */
assertInvalidCompanionDeviceServicesNotBoundnull314 fun assertInvalidCompanionDeviceServicesNotBound() =
315 assertFalse(
316 "CompanionDeviceServices that do not require " +
317 "BIND_COMPANION_DEVICE_SERVICE permission or do not declare an intent-filter for " +
318 "\"android.companion.CompanionDeviceService\" action should not be bound"
319 ) {
320 MissingPermissionCompanionService.isBound ||
321 MissingIntentFilterActionCompanionService.isBound
322 }
323
324 /**
325 * Assert that device (dis)appearance detection callback is only triggered for the primary
326 * CompanionDeviceService and not on any of the non-primary or invalid CompanionDeviceServices.
327 */
assertOnlyPrimaryCompanionDeviceServiceNotifiednull328 fun assertOnlyPrimaryCompanionDeviceServiceNotified(associationId: Int, appeared: Boolean) {
329 val snapshotSecondary = HashSet(SecondaryCompanionService.connectedDevices)
330 val snapshotUnauthorized = HashSet(MissingPermissionCompanionService.connectedDevices)
331 val snapshotInvalid = HashSet(MissingIntentFilterActionCompanionService.connectedDevices)
332
333 // Check that the primary CompanionDeviceService received onDevice(Dis)Appeared() callback
334 if (appeared) {
335 PrimaryCompanionService.waitAssociationToAppear(associationId)
336 assertContains(PrimaryCompanionService.associationIdsForConnectedDevices, associationId)
337 } else {
338 PrimaryCompanionService.waitAssociationToDisappear(associationId)
339 assertFalse(
340 PrimaryCompanionService.associationIdsForConnectedDevices.contains(associationId)
341 )
342 }
343
344 // ... while neither the non-primary nor incorrectly defined CompanionDeviceServices -
345 // have NOT. (Give it 1 more second.)
346 sleepFor(1.seconds)
347 assertContentEquals(snapshotSecondary, SecondaryCompanionService.connectedDevices)
348 assertContentEquals(snapshotUnauthorized, MissingPermissionCompanionService.connectedDevices)
349 assertContentEquals(snapshotInvalid, MissingIntentFilterActionCompanionService.connectedDevices)
350 }
351
assertDevicePresenceEventnull352 fun assertDevicePresenceEvent(expected: Int, actual: Int) {
353 assertTrue("Expected event: $expected, but actual: $actual") {
354 waitFor (timeout = 2.seconds, interval = 100.milliseconds ) {
355 actual == expected
356 }
357 }
358 }
359
360 /**
361 * @return whether the condition was met before time ran out.
362 */
waitFornull363 fun waitFor(
364 timeout: Duration = 10.seconds,
365 interval: Duration = 1.seconds,
366 condition: () -> Boolean
367 ): Boolean {
368 val startTime = uptimeMillis()
369 while (!condition()) {
370 if (uptimeMillis() - startTime > timeout.inWholeMilliseconds) return false
371 sleep(interval.inWholeMilliseconds)
372 }
373 return true
374 }
375
waitForResultnull376 fun <R> waitForResult(
377 timeout: Duration = 10.seconds,
378 interval: Duration = 1.seconds,
379 block: () -> R
380 ): R? {
381 val startTime = uptimeMillis()
382 while (true) {
383 val result: R = block()
384 if (result != null) return result
385 sleep(interval.inWholeMilliseconds)
386 if (uptimeMillis() - startTime > timeout.inWholeMilliseconds) return null
387 }
388 }
389
runShellCommandnull390 fun Instrumentation.runShellCommand(cmd: String): String {
391 Log.i(TAG, "Running shell command: '$cmd'")
392 try {
393 val out = SystemUtil.runShellCommand(this, cmd)
394 Log.i(TAG, "Out:\n$out")
395 return out
396 } catch (e: IOException) {
397 Log.e(TAG, "Error running shell command: $cmd")
398 throw e
399 }
400 }
401
setSystemPropnull402 fun Instrumentation.setSystemProp(name: String, value: String) =
403 runShellCommand("setprop $name $value")
404
405 fun MacAddress.toUpperCaseString() = toString().uppercase(Locale.ROOT)
406
407 fun sleepFor(duration: Duration) = sleep(duration.inWholeMilliseconds)
408
409 fun killProcess(name: String) {
410 val pids = SystemUtil.runShellCommand("pgrep $name").trim().split("\\s+".toRegex())
411 for (pid: String in pids) {
412 // Make sure that it is the intended process before killing it.
413 val process = SystemUtil.runShellCommand("ps $pid")
414 if (process.contains("android.companion.cts.multiprocess")) {
415 Process.killProcess(Integer.valueOf(pid))
416 }
417 }
418 }
419
getAssociationForPackagenull420 fun getAssociationForPackage(
421 @UserIdInt userId: Int,
422 packageName: String,
423 macAddress: MacAddress,
424 cdm: CompanionDeviceManager
425 ): AssociationInfo = cdm.allAssociations.find {
426 it.belongsToPackage(userId, packageName) && it.deviceMacAddress == macAddress
427 } ?: fail("Association for u$userId/$packageName linked to address $macAddress does not exist")
428