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.companion.AssociationInfo
20 import android.companion.CompanionDeviceService
21 import android.companion.DevicePresenceEvent
22 import android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED
23 import android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED
24 import android.content.Intent
25 import android.os.Handler
26 import android.os.ParcelUuid
27 import android.util.Log
28 import java.util.Collections.synchronizedMap
29 import java.util.Collections.synchronizedSet
30 import kotlin.time.Duration
31 import kotlin.time.Duration.Companion.seconds
32 
33 sealed class CompanionService<T : CompanionService<T>>(
34     private val instanceHolder: InstanceHolder<T>
35 ) : CompanionDeviceService() {
36     @Volatile var isBound: Boolean = false
37         private set(isBound) {
38             Log.d(TAG, "$this.isBound=$isBound")
39             if (!isBound && !connectedDevices.isEmpty()) {
40                 error("Unbinding while there are connected devices")
41             }
42             field = isBound
43         }
44 
45     var currentEvent: Int = -2
46 
47     val connectedDevices: Collection<AssociationInfo>
48         get() = _connectedDevices.values
49 
50     val associationIdsForConnectedDevices: Collection<Int>
51         get() = _connectedDevices.keys
52 
53     val connectedUuidDevices: MutableSet<ParcelUuid?> = synchronizedSet(mutableSetOf())
54 
55     val associationIdsForBtBondDevices: MutableSet<Int> = synchronizedSet(mutableSetOf())
56 
57     private val _connectedDevices: MutableMap<Int, AssociationInfo> =
58             synchronizedMap(mutableMapOf())
59 
onCreatenull60     override fun onCreate() {
61         Log.d(TAG, "$this.onCreate()")
62         super.onCreate()
63         instanceHolder.instance = this as T
64     }
65 
onBindCompanionDeviceServicenull66     override fun onBindCompanionDeviceService(intent: Intent) {
67         Log.d(TAG, "$this.onBindCompanionDeviceService()")
68         isBound = true
69     }
70 
onDeviceAppearednull71     override fun onDeviceAppeared(associationInfo: AssociationInfo) {
72         Log.d(TAG, "$this.onDevice_Appeared(), association=$associationInfo")
73         _connectedDevices[associationInfo.id] = associationInfo
74 
75         super.onDeviceAppeared(associationInfo)
76     }
77 
onDeviceDisappearednull78     override fun onDeviceDisappeared(associationInfo: AssociationInfo) {
79         Log.d(TAG, "$this.onDevice_Disappeared(), association=$associationInfo")
80         _connectedDevices.remove(associationInfo.id)
81 
82         super.onDeviceDisappeared(associationInfo)
83     }
84 
onDevicePresenceEventnull85     override fun onDevicePresenceEvent(devicePresenceEvent: DevicePresenceEvent) {
86         val event = devicePresenceEvent.event
87         currentEvent = event
88 
89         if (devicePresenceEvent.uuid == null) {
90             Log.i(
91                 TAG,
92                 "$this.onDevicePresenceEvent(), " +
93                         "association id=${devicePresenceEvent.associationId}" + "event is: $event"
94             )
95 
96             var associationId: Int = devicePresenceEvent.associationId
97             if (event == EVENT_BT_CONNECTED) {
98                 associationIdsForBtBondDevices.add(associationId)
99             } else if (event == EVENT_BT_DISCONNECTED) {
100                 associationIdsForBtBondDevices.remove(associationId)
101                     ?: error("onDeviceDisconnected() has not been called for association with id " +
102                             "${devicePresenceEvent.associationId}")
103             }
104         } else {
105             val uuid: ParcelUuid? = devicePresenceEvent.uuid
106             Log.i(TAG, "$this.onDeviceEvent(), ParcelUuid=$uuid event is: $event")
107             if (event == EVENT_BT_CONNECTED) {
108                 connectedUuidDevices.add(uuid)
109             } else if (event == EVENT_BT_DISCONNECTED) {
110                 if (!connectedUuidDevices.remove(uuid)) {
111                     error(
112                         "onDeviceEvent() with event " +
113                                 "$EVENT_BT_CONNECTED has not been called"
114                     )
115                 }
116             }
117         }
118 
119         super.onDevicePresenceEvent(devicePresenceEvent)
120     }
121 
122     // For now, we need to "post" a Runnable that sets isBound to false to the Main Thread's
123     // Handler, because this may be called between invocations of
124     // CompanionDeviceService.Stub.onDeviceAppeared() and the "real"
125     // CompanionDeviceService.onDeviceAppeared(), which would cause an error() in isBound setter.
onUnbindnull126     override fun onUnbind(intent: Intent?) = super.onUnbind(intent)
127             .also {
128                 Log.d(TAG, "$this.onUnbind()")
129                 Handler.getMain().post { isBound = false }
130             }
131 
onDestroynull132     override fun onDestroy() {
133         Log.d(TAG, "$this.onDestroy()")
134         instanceHolder.instance = null
135         super.onDestroy()
136     }
137 
removeConnectedDevicenull138     fun removeConnectedDevice(associationId: Int) {
139         _connectedDevices.remove(associationId)
140     }
141 
clearConnectedDevicesnull142     fun clearConnectedDevices() {
143         _connectedDevices.clear()
144     }
145 }
146 
147 sealed class InstanceHolder<T : CompanionService<T>> {
148     // Need synchronization, because the setter will be called from the Main thread, while the
149     // getter is expected to be called mostly from the instrumentation thread.
150     var instance: T? = null
151         @Synchronized internal set
152 
153         @Synchronized get
154 
155     val isBound: Boolean
156         get() = instance?.isBound ?: false
157 
158     val connectedDevices: Collection<AssociationInfo>
159         get() = instance?.connectedDevices ?: emptySet()
160 
161     val connectedBtBondDevices: Collection<AssociationInfo>
162         get() = instance?.connectedDevices ?: emptySet()
163 
164     val connectedUuidBondDevices: Collection<ParcelUuid?>
165         get() = instance?.connectedUuidDevices ?: emptySet()
166 
167     val associationIdsForConnectedDevices: Collection<Int>
168         get() = instance?.associationIdsForConnectedDevices ?: emptySet()
169 
170     val associationIdsForBtBondDevices: Collection<Int>
171         get() = instance?.associationIdsForBtBondDevices ?: emptySet()
172 
waitForBindnull173     fun waitForBind(timeout: Duration = 1.seconds) {
174         if (!waitFor(timeout) { isBound }) {
175             throw AssertionError("Service hasn't been bound")
176         }
177     }
178 
waitForUnbindnull179     fun waitForUnbind(timeout: Duration) {
180         if (!waitFor(timeout) { !isBound }) {
181             throw AssertionError("Service hasn't been unbound")
182         }
183     }
184 
waitAssociationToAppearnull185     fun waitAssociationToAppear(associationId: Int, timeout: Duration = 1.seconds) {
186         val appeared = waitFor(timeout) {
187             associationIdsForConnectedDevices.contains(associationId)
188         }
189         if (!appeared) {
190             throw AssertionError("""Association with $associationId hasn't "appeared"""")
191         }
192     }
193 
194     fun waitAssociationToDisappear(associationId: Int, timeout: Duration = 1.seconds) {
195         val gone = waitFor(timeout) {
196             !associationIdsForConnectedDevices.contains(associationId)
197         }
198         if (!gone) throw AssertionError("""Association with $associationId hasn't "disappeared"""")
199     }
200 
201     fun waitAssociationToBtConnect(associationId: Int, timeout: Duration = 1.seconds) {
202         val appeared = waitFor(timeout) {
203             associationIdsForBtBondDevices.contains(associationId)
204         }
205         if (!appeared) {
206             throw AssertionError("""Association with$associationId hasn't "connected"""")
207         }
208     }
209 
210     fun waitAssociationToBtDisconnect(associationId: Int, timeout: Duration = 1.seconds) {
211         val gone = waitFor(timeout) {
212             !associationIdsForBtBondDevices.contains(associationId)
213         }
214         if (!gone) {
215             throw AssertionError("""Association with $associationId hasn't "disconnected"""")
216         }
217     }
218 
219     fun waitDeviceUuidConnect(uuid: ParcelUuid, timeout: Duration = 1.seconds) {
220         val appeared = waitFor(timeout) {
221             connectedUuidBondDevices.contains(uuid)
222         }
223         if (!appeared) {
224             throw AssertionError("""Uuid $uuid hasn't "connected"""")
225         }
226     }
227 
228     fun waitDeviceUuidDisconnect(uuid: ParcelUuid, timeout: Duration = 1.seconds) {
229         val gone = waitFor(timeout) {
230             !connectedUuidBondDevices.contains(uuid)
231         }
232         if (!gone) {
233             throw AssertionError("""Uuid $uuid hasn't "disconnected"""")
234         }
235     }
236 
237     // This is a useful function to use to conveniently "forget" that a device is currently present.
238     // Use to bypass the "unbinding while there are connected devices" for simulated devices.
239     // (Don't worry! they would have removed themselves after 1 minute anyways!)
240     fun forgetDevicePresence(associationId: Int) {
241         instance?.removeConnectedDevice(associationId)
242     }
243 
244     fun clearDeviceUuidPresence() {
245         instance?.connectedUuidDevices?.clear()
246     }
247 
248     fun clearConnectedDevices() {
249         instance?.clearConnectedDevices()
250     }
251 
252     fun getCurrentEvent(): Int? {
253         return instance?.currentEvent
254     }
255 }
256 
257 class PrimaryCompanionService : CompanionService<PrimaryCompanionService>(Companion) {
258     companion object : InstanceHolder<PrimaryCompanionService>()
259 }
260 
261 class SecondaryCompanionService : CompanionService<SecondaryCompanionService>(Companion) {
262     companion object : InstanceHolder<SecondaryCompanionService>()
263 }
264 
265 class MissingPermissionCompanionService : CompanionService<
266         MissingPermissionCompanionService>(Companion) {
267     companion object : InstanceHolder<MissingPermissionCompanionService>()
268 }
269 
270 class MissingIntentFilterActionCompanionService : CompanionService<
271         MissingIntentFilterActionCompanionService>(Companion) {
272     companion object : InstanceHolder<MissingIntentFilterActionCompanionService>()
273 }
274