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.net.MacAddress
29 import android.os.SystemClock.sleep
30 import android.os.SystemClock.uptimeMillis
31 import android.util.Log
32 import androidx.test.platform.app.InstrumentationRegistry
33 import com.android.compatibility.common.util.SystemUtil
34 import org.junit.After
35 import org.junit.Assume.assumeTrue
36 import org.junit.AssumptionViolatedException
37 import org.junit.Before
38 import java.io.IOException
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
49 /**
50 * A base class for CompanionDeviceManager [Tests][org.junit.Test] to extend.
51 */
52 abstract class TestBase {
53 protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
54 protected val uiAutomation: UiAutomation = instrumentation.uiAutomation
55
56 protected val context: Context = instrumentation.context
57 protected val userId = context.userId
58 protected val targetPackageName = instrumentation.targetContext.packageName
59
60 protected val targetApp = AppHelper(instrumentation, userId, targetPackageName)
61
<lambda>null62 protected val pm: PackageManager by lazy { context.packageManager!! }
<lambda>null63 private val hasCompanionDeviceSetupFeature by lazy {
64 pm.hasSystemFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP)
65 }
66
<lambda>null67 protected val cdm: CompanionDeviceManager by lazy {
68 context.getSystemService(CompanionDeviceManager::class.java)!!
69 }
70
71 @Before
base_setUpnull72 fun base_setUp() {
73 assumeTrue(hasCompanionDeviceSetupFeature)
74
75 // Remove all existing associations (for the user).
76 assertEmpty(withShellPermissionIdentity {
77 cdm.disassociateAll()
78 cdm.allAssociations
79 })
80
81 // Make sure CompanionDeviceServices are not bound.
82 assertValidCompanionDeviceServicesUnbind()
83
84 setUp()
85 }
86
87 @After
base_tearDownnull88 fun base_tearDown() {
89 if (!hasCompanionDeviceSetupFeature) return
90
91 tearDown()
92
93 // Remove all existing associations (for the user).
94 withShellPermissionIdentity { cdm.disassociateAll() }
95 }
96
97 @CallSuper
setUpnull98 protected open fun setUp() {}
99
100 @CallSuper
tearDownnull101 protected open fun tearDown() {}
102
withShellPermissionIdentitynull103 protected fun <T> withShellPermissionIdentity(
104 vararg permissions: String,
105 block: () -> T
106 ): T {
107 if (permissions.isNotEmpty()) {
108 uiAutomation.adoptShellPermissionIdentity(*permissions)
109 } else {
110 uiAutomation.adoptShellPermissionIdentity()
111 }
112
113 try {
114 return block()
115 } finally {
116 uiAutomation.dropShellPermissionIdentity()
117 }
118 }
119
createSelfManagedAssociationnull120 protected fun createSelfManagedAssociation(
121 displayName: String,
122 onAssociationCreatedAction: ((AssociationInfo) -> Unit)? = null
123 ): Int {
124 val callback = RecordingCallback(onAssociationCreatedAction = onAssociationCreatedAction)
125 val request: AssociationRequest = AssociationRequest.Builder()
126 .setSelfManaged(true)
127 .setDisplayName(displayName)
128 .build()
129 callback.assertInvokedByActions {
130 withShellPermissionIdentity(Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) {
131 cdm.associate(request, SIMPLE_EXECUTOR, callback)
132 }
133 }
134
135 val callbackInvocation = callback.invocations.first()
136 assertIs<RecordingCallback.OnAssociationCreated>(callbackInvocation)
137 return callbackInvocation.associationInfo.id
138 }
139
runShellCommandnull140 protected fun runShellCommand(cmd: String) = instrumentation.runShellCommand(cmd)
141
142 private fun CompanionDeviceManager.disassociateAll() =
143 allAssociations.forEach { disassociate(it.id) }
144 }
145
146 const val TAG = "CtsCompanionDeviceManagerTestCases"
147
assumeThatnull148 fun <T> assumeThat(message: String, obj: T, assumption: (T) -> Boolean) {
149 if (!assumption(obj)) throw AssumptionViolatedException(message)
150 }
151
<lambda>null152 fun <T> assertEmpty(list: Collection<T>) = assertTrue("Collection is not empty") { list.isEmpty() }
153
assertAssociationsnull154 fun assertAssociations(
155 actual: List<AssociationInfo>,
156 expected: Set<Pair<String, MacAddress?>>
157 ) = assertEquals(actual = actual.map { it.packageName to it.deviceMacAddress }.toSet(),
158 expected = expected)
159
160 /**
161 * Assert that CDM binds valid CompanionDeviceServices, both primary and secondary.
162 * Use when services are expected to switch its state to "bound".
163 */
assertValidCompanionDeviceServicesBindnull164 fun assertValidCompanionDeviceServicesBind() =
165 assertTrue("Both valid CompanionDeviceServices - Primary and Secondary - should bind") {
166 waitFor(timeout = 1.seconds, interval = 100.milliseconds) {
167 PrimaryCompanionService.isBound && SecondaryCompanionService.isBound
168 }
169 }
170
171 /**
172 * Assert both primary and secondary CompanionDeviceServices stay bound.
173 * Use when services are expected to be in "bound" state already.
174 */
assertValidCompanionDeviceServicesRemainBoundnull175 fun assertValidCompanionDeviceServicesRemainBound() =
176 assertFalse("Both valid CompanionDeviceServices should stay bound") {
177 waitFor(timeout = 3.seconds, interval = 100.milliseconds) {
178 !PrimaryCompanionService.isBound || !SecondaryCompanionService.isBound
179 }
180 }
181
182 /**
183 * Assert that CDM unbinds valid CompanionDeviceServices, both primary and secondary.
184 * Use when services are expected to switch its state to "unbound".
185 */
assertValidCompanionDeviceServicesUnbindnull186 fun assertValidCompanionDeviceServicesUnbind() =
187 assertTrue("CompanionDeviceServices should not bind") {
188 waitFor(timeout = 1.seconds, interval = 100.milliseconds) {
189 !PrimaryCompanionService.isBound && !SecondaryCompanionService.isBound
190 }
191 }
192
193 /**
194 * Assert that neither primary nor secondary CompanionDeviceService is bound.
195 * Use when services are expected to be in "unbound" state already.
196 */
assertValidCompanionDeviceServicesRemainUnboundnull197 fun assertValidCompanionDeviceServicesRemainUnbound() =
198 assertFalse("CompanionDeviceServices should not be bound") {
199 waitFor(timeout = 3.seconds, interval = 100.milliseconds) {
200 PrimaryCompanionService.isBound || SecondaryCompanionService.isBound
201 }
202 }
203
204 /**
205 * Assert that CDM did not bind invalid CompanionDeviceServices
206 * (i.e. missing permission or intent-filter).
207 */
assertInvalidCompanionDeviceServicesNotBoundnull208 fun assertInvalidCompanionDeviceServicesNotBound() =
209 assertFalse("CompanionDeviceServices that do not require " +
210 "BIND_COMPANION_DEVICE_SERVICE permission or do not declare an intent-filter for " +
211 "\"android.companion.CompanionDeviceService\" action should not be bound") {
212 MissingPermissionCompanionService.isBound ||
213 MissingIntentFilterActionCompanionService.isBound
214 }
215
216 /**
217 * Assert that device (dis)appearance detection callback is only triggered for the primary
218 * CompanionDeviceService and not on any of the non-primary or invalid CompanionDeviceServices.
219 */
assertOnlyPrimaryCompanionDeviceServiceNotifiednull220 fun assertOnlyPrimaryCompanionDeviceServiceNotified(associationId: Int, appeared: Boolean) {
221 val snapshotSecondary = HashSet(SecondaryCompanionService.connectedDevices)
222 val snapshotUnauthorized = HashSet(MissingPermissionCompanionService.connectedDevices)
223 val snapshotInvalid = HashSet(MissingIntentFilterActionCompanionService.connectedDevices)
224
225 // Check that the primary CompanionDeviceService received onDevice(Dis)Appeared() callback
226 if (appeared) {
227 PrimaryCompanionService.waitAssociationToAppear(associationId)
228 assertContains(PrimaryCompanionService.associationIdsForConnectedDevices, associationId)
229 } else {
230 PrimaryCompanionService.waitAssociationToDisappear(associationId)
231 assertFalse(PrimaryCompanionService.associationIdsForConnectedDevices
232 .contains(associationId))
233 }
234
235 // ... while neither the non-primary nor incorrectly defined CompanionDeviceServices -
236 // have NOT. (Give it 1 more second.)
237 sleepFor(1.seconds)
238 assertContentEquals(snapshotSecondary, SecondaryCompanionService.connectedDevices)
239 assertContentEquals(snapshotUnauthorized, MissingPermissionCompanionService.connectedDevices)
240 assertContentEquals(snapshotInvalid, MissingIntentFilterActionCompanionService.connectedDevices)
241 }
242
243 /**
244 * @return whether the condition was met before time ran out.
245 */
waitFornull246 fun waitFor(
247 timeout: Duration = 10.seconds,
248 interval: Duration = 1.seconds,
249 condition: () -> Boolean
250 ): Boolean {
251 val startTime = uptimeMillis()
252 while (!condition()) {
253 if (uptimeMillis() - startTime > timeout.inWholeMilliseconds) return false
254 sleep(interval.inWholeMilliseconds)
255 }
256 return true
257 }
258
waitForResultnull259 fun <R> waitForResult(
260 timeout: Duration = 10.seconds,
261 interval: Duration = 1.seconds,
262 block: () -> R
263 ): R? {
264 val startTime = uptimeMillis()
265 while (true) {
266 val result: R = block()
267 if (result != null) return result
268 sleep(interval.inWholeMilliseconds)
269 if (uptimeMillis() - startTime > timeout.inWholeMilliseconds) return null
270 }
271 }
272
runShellCommandnull273 fun Instrumentation.runShellCommand(cmd: String): String {
274 Log.i(TAG, "Running shell command: '$cmd'")
275 try {
276 val out = SystemUtil.runShellCommand(this, cmd)
277 Log.i(TAG, "Out:\n$out")
278 return out
279 } catch (e: IOException) {
280 Log.e(TAG, "Error running shell command: $cmd")
281 throw e
282 }
283 }
284
setSystemPropnull285 fun Instrumentation.setSystemProp(name: String, value: String) =
286 runShellCommand("setprop $name $value")
287
288 fun MacAddress.toUpperCaseString() = toString().toUpperCase()
289
290 fun sleepFor(duration: Duration) = sleep(duration.inWholeMilliseconds)