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