blob: 4d093331792e8d3ac6a31bfb85bfbf7830595985 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.companion.cts.common
import android.companion.AssociationInfo
import android.companion.CompanionDeviceService
import android.content.Intent
import android.os.Handler
import android.util.Log
import java.util.Collections.synchronizedMap
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
sealed class CompanionService<T : CompanionService<T>>(
private val instanceHolder: InstanceHolder<T>
) : CompanionDeviceService() {
@Volatile var isBound: Boolean = false
private set(isBound) {
Log.d(TAG, "$this.isBound=$isBound")
if (!isBound && !connectedDevices.isEmpty())
error("Unbinding while there are connected devices")
field = isBound
}
val connectedDevices: Collection<AssociationInfo>
get() = _connectedDevices.values
val associationIdsForConnectedDevices: Collection<Int>
get() = _connectedDevices.keys
private val _connectedDevices: MutableMap<Int, AssociationInfo> =
synchronizedMap(mutableMapOf())
override fun onCreate() {
Log.d(TAG, "$this.onCreate()")
super.onCreate()
instanceHolder.instance = this as T
}
override fun onBindCompanionDeviceService(intent: Intent) {
Log.d(TAG, "$this.onBindCompanionDeviceService()")
isBound = true
}
override fun onDeviceAppeared(associationInfo: AssociationInfo) {
Log.d(TAG, "$this.onDevice_Appeared(), association=$associationInfo")
_connectedDevices[associationInfo.id] = associationInfo
super.onDeviceAppeared(associationInfo)
}
override fun onDeviceDisappeared(associationInfo: AssociationInfo) {
Log.d(TAG, "$this.onDevice_Disappeared(), association=$associationInfo")
_connectedDevices.remove(associationInfo.id)
?: error("onDeviceAppeared() has not been called for association with id " +
"${associationInfo.id}")
super.onDeviceDisappeared(associationInfo)
}
// For now, we need to "post" a Runnable that sets isBound to false to the Main Thread's
// Handler, because this may be called between invocations of
// CompanionDeviceService.Stub.onDeviceAppeared() and the "real"
// CompanionDeviceService.onDeviceAppeared(), which would cause an error() in isBound setter.
override fun onUnbind(intent: Intent?) = super.onUnbind(intent)
.also {
Log.d(TAG, "$this.onUnbind()")
Handler.getMain().post { isBound = false }
}
override fun onDestroy() {
Log.d(TAG, "$this.onDestroy()")
instanceHolder.instance = null
super.onDestroy()
}
}
sealed class InstanceHolder<T : CompanionService<T>> {
// Need synchronization, because the setter will be called from the Main thread, while the
// getter is expected to be called mostly from the instrumentation thread.
var instance: T? = null
@Synchronized internal set
@Synchronized get
val isBound: Boolean
get() = instance?.isBound ?: false
val connectedDevices: Collection<AssociationInfo>
get() = instance?.connectedDevices ?: emptySet()
val associationIdsForConnectedDevices: Collection<Int>
get() = instance?.associationIdsForConnectedDevices ?: emptySet()
fun waitForBind(timeout: Duration = 1.seconds) {
if (!waitFor(timeout) { isBound })
throw AssertionError("Service hasn't been bound")
}
fun waitForUnbind(timeout: Duration) {
if (!waitFor(timeout) { !isBound })
throw AssertionError("Service hasn't been unbound")
}
fun waitAssociationToAppear(associationId: Int, timeout: Duration = 1.seconds) {
val appeared = waitFor(timeout) {
associationIdsForConnectedDevices.contains(associationId)
}
if (!appeared) throw AssertionError("""Association with $associationId hasn't "appeared"""")
}
fun waitAssociationToDisappear(associationId: Int, timeout: Duration = 1.seconds) {
val gone = waitFor(timeout) {
!associationIdsForConnectedDevices.contains(associationId)
}
if (!gone) throw AssertionError("""Association with $associationId hasn't "disappeared"""")
}
}
class PrimaryCompanionService : CompanionService<PrimaryCompanionService>(Companion) {
companion object : InstanceHolder<PrimaryCompanionService>()
}
class SecondaryCompanionService : CompanionService<SecondaryCompanionService>(Companion) {
companion object : InstanceHolder<SecondaryCompanionService>()
}
class MissingPermissionCompanionService
: CompanionService<MissingPermissionCompanionService>(Companion) {
companion object : InstanceHolder<MissingPermissionCompanionService>()
}
class MissingIntentFilterActionCompanionService
: CompanionService<MissingIntentFilterActionCompanionService>(Companion) {
companion object : InstanceHolder<MissingIntentFilterActionCompanionService>()
}