blob: 15bbb00aba18d8a15c7d6d7b494c33ca98c1727c [file] [log] [blame]
/*
* Copyright (C) 2023 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 com.android.adblib.ddmlibcompatibility
import com.android.adblib.AdbSession
import com.android.adblib.ConnectedDevice
import com.android.adblib.DeviceState
import com.android.adblib.adbLogger
import com.android.adblib.connectedDevicesTracker
import com.android.adblib.ddmlibcompatibility.debugging.AdbLibDeviceClientManager
import com.android.adblib.ddmlibcompatibility.debugging.AdblibIDeviceWrapper
import com.android.adblib.scope
import com.android.adblib.utils.createChildScope
import com.android.ddmlib.AndroidDebugBridge
import com.android.ddmlib.IDevice
import com.android.ddmlib.idevicemanager.IDeviceManager
import com.android.ddmlib.idevicemanager.IDeviceManagerListener
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import java.util.IdentityHashMap
import java.util.concurrent.atomic.AtomicReference
internal class AdbLibIDeviceManager(
private val session: AdbSession,
private val bridge: AndroidDebugBridge,
private val iDeviceManagerListener: IDeviceManagerListener
) : IDeviceManager {
private val logger = adbLogger(session)
private val scope = session.scope.createChildScope(isSupervisor = true)
// Using `IdentityHashMap` as `connectedDevicesTracker.connectedDevices` guarantees
// to return the same instance for the same device
private val deviceMap = IdentityHashMap<ConnectedDevice, AdblibIDeviceWrapper>()
private val deviceList = AtomicReference<List<IDevice>>(emptyList())
private val ddmlibEventQueue =
AdbLibDeviceClientManager.DdmlibEventQueue(logger, "DeviceUpdates")
private var initialDeviceListDone = false
init {
scope.launch {
ddmlibEventQueue.runDispatcher()
}
scope.launch {
session.connectedDevicesTracker.connectedDevices.collect { value ->
run {
// Process added devices
val added = value.filter { !deviceMap.containsKey(it) }
val addedIDevices = mutableListOf<IDevice>()
for (key in added) {
val iDevice = AdblibIDeviceWrapper(key, bridge)
deviceMap[key] = iDevice
addedIDevices.add(iDevice)
}
// Process removed devices
val removed = deviceMap.keys.filter { !value.contains(it)}
val removedIDevices = mutableListOf<IDevice>()
for (key in removed) {
deviceMap.remove(key)?.also {
it.deviceState = DeviceState.DISCONNECTED
removedIDevices.add(it)
}
}
deviceList.set(deviceMap.values.toList())
// flag the fact that we have build the list at least once
initialDeviceListDone = true
if (addedIDevices.isNotEmpty()) {
//Set current deviceState before triggering `iDeviceManagerListener.addedDevices`
for (addedConnectedDevice in added) {
val iDevice = deviceMap.getValue(addedConnectedDevice)
iDevice.deviceState =
addedConnectedDevice.deviceInfoFlow.value.deviceState
}
postAndWaitForCompletion(scope, "devices added") {
iDeviceManagerListener.addedDevices(addedIDevices)
}
for (addedConnectedDevice in added) {
val iDevice = deviceMap.getValue(addedConnectedDevice)
addedConnectedDevice.scope.launch {
addedConnectedDevice.deviceInfoFlow.map { it.deviceState }.collect {
if (iDevice.deviceState != it) {
iDevice.deviceState = it
// Match ddmlib behavior by not triggering device state
// change event for a `DISCONNECTED` device state value.
if (it != DeviceState.DISCONNECTED) {
postAndWaitForCompletion(
scope,
"device state changed"
) {
iDeviceManagerListener.deviceStateChanged(iDevice)
}
}
}
}
}
}
}
if (removedIDevices.isNotEmpty()) {
postAndWaitForCompletion(scope, "devices removed") {
iDeviceManagerListener.removedDevices(removedIDevices)
}
}
}
}
}
}
override fun close() {
scope.cancel("${this::class.simpleName} has been closed")
}
override fun getDevices(): MutableList<IDevice> {
return deviceList.get().toMutableList()
}
override fun hasInitialDeviceList(): Boolean {
return initialDeviceListDone
}
private suspend fun postAndWaitForCompletion(scope: CoroutineScope, name: String, handler: () -> Unit) {
val processed = CompletableDeferred<Unit>(scope.coroutineContext.job)
ddmlibEventQueue.post(scope, name) {
try {
handler()
} finally {
processed.complete(Unit)
}
}
processed.await()
}
}