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