From e9c6fe3918e04a8ef33e236895f339568d2322cf Mon Sep 17 00:00:00 2001 From: Rien Maertens Date: Sat, 24 Jul 2021 22:38:54 +0200 Subject: [PATCH] Clean up device code with LiveData and Hilt --- .../me/vanpetegem/accentor/devices/Device.kt | 21 +++- .../accentor/devices/DeviceManager.kt | 109 ++++++++++++++++++ .../devices/DeviceRegistryListener.kt | 54 --------- .../accentor/devices/DeviceService.kt | 2 - .../accentor/ui/devices/DevicesView.kt | 10 +- .../accentor/ui/devices/DevicesViewModel.kt | 21 +++- .../accentor/ui/main/MainActivity.kt | 36 ++---- 7 files changed, 155 insertions(+), 98 deletions(-) create mode 100644 app/src/main/java/me/vanpetegem/accentor/devices/DeviceManager.kt delete mode 100644 app/src/main/java/me/vanpetegem/accentor/devices/DeviceRegistryListener.kt diff --git a/app/src/main/java/me/vanpetegem/accentor/devices/Device.kt b/app/src/main/java/me/vanpetegem/accentor/devices/Device.kt index 005187d4..245a5640 100644 --- a/app/src/main/java/me/vanpetegem/accentor/devices/Device.kt +++ b/app/src/main/java/me/vanpetegem/accentor/devices/Device.kt @@ -1,15 +1,14 @@ package me.vanpetegem.accentor.devices -import org.fourthline.cling.model.meta.Device import org.fourthline.cling.model.meta.RemoteDevice +import java.lang.Exception -class Device( - private val clingDevice: RemoteDevice +sealed class Device( + protected val clingDevice: RemoteDevice ) { val friendlyName: String = clingDevice.details.friendlyName val firstCharacter: String = String(intArrayOf(friendlyName.codePointAt(0)), 0, 1) - val type: String = clingDevice.type.displayString val imageURL: String? = clingDevice @@ -17,4 +16,18 @@ class Device( .maxWithOrNull(compareBy({ it.height * it.width }, { it.mimeType.subtype == "png" })) ?.let { clingDevice.normalizeURI(it.uri).toString() } + + class Discovered(clingDevice: RemoteDevice): Device(clingDevice) { + fun failed(exception: Exception?): Failed { + return Failed(clingDevice, exception) + } + + fun ready(): Ready { + return Ready(clingDevice) + } + } + class Failed(clingDevice: RemoteDevice, val exception: Exception?): Device(clingDevice) {} + class Ready(clingDevice: RemoteDevice): Device(clingDevice) {} } + + diff --git a/app/src/main/java/me/vanpetegem/accentor/devices/DeviceManager.kt b/app/src/main/java/me/vanpetegem/accentor/devices/DeviceManager.kt new file mode 100644 index 00000000..5887dd7d --- /dev/null +++ b/app/src/main/java/me/vanpetegem/accentor/devices/DeviceManager.kt @@ -0,0 +1,109 @@ +package me.vanpetegem.accentor.devices + + +import android.content.ComponentName +import android.content.ServiceConnection +import android.os.IBinder +import android.util.Log +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import dagger.Reusable +import org.fourthline.cling.android.AndroidUpnpService +import org.fourthline.cling.model.message.header.ServiceTypeHeader +import org.fourthline.cling.model.meta.RemoteDevice +import org.fourthline.cling.model.types.ServiceType +import org.fourthline.cling.model.types.UDN +import org.fourthline.cling.registry.DefaultRegistryListener +import org.fourthline.cling.registry.Registry +import java.lang.Exception +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DeviceManager @Inject constructor() { + + val devices = MutableLiveData>(emptyMap()) + val connection = DeviceServiceConnection() + + private lateinit var upnp: AndroidUpnpService + private val isConnected = MutableLiveData(false) + private val registryListener = DeviceRegistryListener() + + fun search() { + val playerService = ServiceTypeHeader(ServiceType("schemas-upnp-org", "AVTransport", 1)) + upnp.controlPoint.search(playerService) + } + + inner class DeviceServiceConnection() : ServiceConnection { + override fun onServiceConnected(className: ComponentName?, binder: IBinder?) { + upnp = binder!! as AndroidUpnpService + isConnected.value = true + + // clear devices (if any) and collect the known remote devices into a map + devices.value = upnp.registry.devices + .filterIsInstance() + .map { it.identity.udn to Device.Ready(it) } + .toMap() + + upnp.registry.addListener(registryListener) + search() + } + + override fun onServiceDisconnected(className: ComponentName?) { + isConnected.value = false + } + } + + private inner class DeviceRegistryListener(): DefaultRegistryListener() { + + override fun remoteDeviceDiscoveryStarted(registry: Registry?, remote: RemoteDevice?) { + val udn = remote!!.identity.udn + // this will only add a new device if not yet present in the map + devices.postValue(mapOf(udn to Device.Discovered(remote)) + devices.value!!) + } + + override fun remoteDeviceDiscoveryFailed(registry: Registry?, remote: RemoteDevice?, ex: Exception?) { + val udn = remote!!.identity.udn + val known = devices.value!! + when(val dev = known[udn]) { + is Device.Discovered -> devices.postValue(known + (udn to dev.failed(ex))) + else -> Log.e(TAG, "Discovery failed of existing device", ex) + } + } + + override fun remoteDeviceUpdated(registry: Registry?, remote: RemoteDevice?) { + if (devices.value!!.contains(remote!!.identity.udn)) { + // trigger an update + devices.postValue(devices.value) + } else { + Log.e(TAG, "Non-existing device updated") + } + } + + override fun remoteDeviceAdded(registry: Registry?, remote: RemoteDevice?) { + addDevice(remote!!) + } + + override fun remoteDeviceRemoved(registry: Registry?, remote: RemoteDevice?) { + val withRemoved = devices.value!!.minus(remote!!.identity.udn) + devices.postValue(withRemoved) + } + + fun addDevice(remote: RemoteDevice) { + val udn = remote.identity.udn + val known = devices.value!! + if (udn in known) { + when (val dev = known[udn]) { + is Device.Discovered -> devices.postValue(known + (udn to dev.ready())) + else -> Log.e(TAG, "Device added twice, ignoring... ${remote.displayString} ($udn)") + } + } else { + devices.postValue(known + (udn to Device.Ready(remote))) + } + } + } +} + +const val TAG: String = "DeviceManager" diff --git a/app/src/main/java/me/vanpetegem/accentor/devices/DeviceRegistryListener.kt b/app/src/main/java/me/vanpetegem/accentor/devices/DeviceRegistryListener.kt deleted file mode 100644 index 47103ce7..00000000 --- a/app/src/main/java/me/vanpetegem/accentor/devices/DeviceRegistryListener.kt +++ /dev/null @@ -1,54 +0,0 @@ -package me.vanpetegem.accentor.devices - - -import android.util.Log -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.snapshots.SnapshotStateList -import androidx.compose.runtime.snapshots.SnapshotStateMap -import androidx.lifecycle.LiveData -import org.fourthline.cling.model.meta.LocalDevice -import org.fourthline.cling.model.meta.RemoteDevice -import org.fourthline.cling.model.meta.Service -import org.fourthline.cling.model.types.UDN -import org.fourthline.cling.registry.DefaultRegistryListener -import org.fourthline.cling.registry.Registry -import java.lang.Exception -import org.fourthline.cling.model.meta.Device as ClingDevice - -class DeviceRegistryListener: DefaultRegistryListener() { - - val devices: SnapshotStateMap = mutableStateMapOf() - - override fun remoteDeviceDiscoveryStarted(registry: Registry?, remote: RemoteDevice?) { - // TODO - } - - override fun remoteDeviceDiscoveryFailed(registry: Registry?, remote: RemoteDevice?, ex: Exception?) { - // TODO - } - - override fun remoteDeviceUpdated(registry: Registry?, device: RemoteDevice?) { - // TODO - } - - override fun remoteDeviceAdded(registry: Registry?, remote: RemoteDevice?) { - addDevice(remote) - } - - override fun remoteDeviceRemoved(registry: Registry?, remote: RemoteDevice?) { - removeDevice(remote) - } - - fun addDevice(remote: RemoteDevice?) { - val device = Device(remote!!) - devices[remote.identity.udn] = device - } - - fun removeDevice(remote: RemoteDevice?) { - devices.remove(remote!!.identity.udn) - } - - -} diff --git a/app/src/main/java/me/vanpetegem/accentor/devices/DeviceService.kt b/app/src/main/java/me/vanpetegem/accentor/devices/DeviceService.kt index fdf690f7..f883409c 100644 --- a/app/src/main/java/me/vanpetegem/accentor/devices/DeviceService.kt +++ b/app/src/main/java/me/vanpetegem/accentor/devices/DeviceService.kt @@ -4,11 +4,9 @@ import org.fourthline.cling.UpnpServiceConfiguration import org.fourthline.cling.android.AndroidUpnpServiceConfiguration import org.fourthline.cling.android.AndroidUpnpServiceImpl import org.fourthline.cling.binding.xml.ServiceDescriptorBinder -import org.fourthline.cling.binding.xml.UDA10DeviceDescriptorBinderImpl import org.fourthline.cling.binding.xml.UDA10ServiceDescriptorBinderImpl class DeviceService: AndroidUpnpServiceImpl() { - override fun createConfiguration(): UpnpServiceConfiguration { return object: AndroidUpnpServiceConfiguration() { // This override fixes the XML parser diff --git a/app/src/main/java/me/vanpetegem/accentor/ui/devices/DevicesView.kt b/app/src/main/java/me/vanpetegem/accentor/ui/devices/DevicesView.kt index d6772251..f54e681c 100644 --- a/app/src/main/java/me/vanpetegem/accentor/ui/devices/DevicesView.kt +++ b/app/src/main/java/me/vanpetegem/accentor/ui/devices/DevicesView.kt @@ -9,21 +9,23 @@ import androidx.compose.material.Card import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import coil.compose.rememberImagePainter import me.vanpetegem.accentor.R import me.vanpetegem.accentor.devices.Device import me.vanpetegem.accentor.ui.util.FastScrollableGrid -import org.fourthline.cling.model.types.UDN @Composable -fun Devices(devices: SnapshotStateMap) { - FastScrollableGrid(devices.values.sortedBy { it.friendlyName }, { it.firstCharacter.uppercase() }) { DeviceCard(it) } +fun Devices(devicesViewModel: DevicesViewModel = hiltViewModel()) { + val devices: List? by devicesViewModel.devices().observeAsState() + FastScrollableGrid(devices ?: emptyList(), { it.firstCharacter }) { DeviceCard(it) } } @Composable diff --git a/app/src/main/java/me/vanpetegem/accentor/ui/devices/DevicesViewModel.kt b/app/src/main/java/me/vanpetegem/accentor/ui/devices/DevicesViewModel.kt index 5e8ca7d0..98b3bd3a 100644 --- a/app/src/main/java/me/vanpetegem/accentor/ui/devices/DevicesViewModel.kt +++ b/app/src/main/java/me/vanpetegem/accentor/ui/devices/DevicesViewModel.kt @@ -1,12 +1,21 @@ package me.vanpetegem.accentor.ui.devices import android.app.Application -import android.content.ComponentName -import android.content.ServiceConnection -import android.os.IBinder import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations.map +import dagger.hilt.android.lifecycle.HiltViewModel import me.vanpetegem.accentor.devices.Device -import me.vanpetegem.accentor.devices.DeviceRegistryListener -import org.fourthline.cling.android.AndroidUpnpService +import me.vanpetegem.accentor.devices.DeviceManager +import javax.inject.Inject -class DevicesViewModel(application: Application) : AndroidViewModel(application) {} +@HiltViewModel +class DevicesViewModel @Inject constructor( + application: Application, + private val deviceManager: DeviceManager, +) : AndroidViewModel(application) { + + fun devices(): LiveData> = map(deviceManager.devices) { devices -> + devices.values.sortedWith(compareBy({ it.friendlyName }, { it.firstCharacter.uppercase() })) + } +} diff --git a/app/src/main/java/me/vanpetegem/accentor/ui/main/MainActivity.kt b/app/src/main/java/me/vanpetegem/accentor/ui/main/MainActivity.kt index 7c5a7e53..9e6c1ea9 100644 --- a/app/src/main/java/me/vanpetegem/accentor/ui/main/MainActivity.kt +++ b/app/src/main/java/me/vanpetegem/accentor/ui/main/MainActivity.kt @@ -63,7 +63,7 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import me.vanpetegem.accentor.R import me.vanpetegem.accentor.devices.Device -import me.vanpetegem.accentor.devices.DeviceRegistryListener +import me.vanpetegem.accentor.devices.DeviceManager import me.vanpetegem.accentor.devices.DeviceService import me.vanpetegem.accentor.ui.AccentorTheme import me.vanpetegem.accentor.ui.albums.AlbumGrid @@ -84,39 +84,19 @@ import org.fourthline.cling.model.meta.RemoteDevice import org.fourthline.cling.model.types.ServiceType import org.fourthline.cling.model.types.UDN import org.seamless.util.logging.LoggingUtil +import javax.inject.Inject @AndroidEntryPoint class MainActivity : ComponentActivity() { - - private lateinit var deviceService: AndroidUpnpService - private var isServiceConnected = false - private val registryListener = DeviceRegistryListener() - - private val deviceConnection = object : ServiceConnection { - override fun onServiceConnected(className: ComponentName?, service: IBinder?) { - deviceService = service!! as AndroidUpnpService - isServiceConnected = true - - deviceService.registry.addListener(registryListener) - for (device in deviceService.registry.devices.filterIsInstance()) { - registryListener.addDevice(device) - } - - val playerService = ServiceTypeHeader(ServiceType("schemas-upnp-org", "AVTransport", 1)) - deviceService.controlPoint.search(playerService) - } - - override fun onServiceDisconnected(className: ComponentName?) { - isServiceConnected = false - } - } + @Inject + lateinit var deviceManager: DeviceManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { AccentorTheme() { - Content(devices = registryListener.devices) + Content() } } @@ -124,12 +104,12 @@ class MainActivity : ComponentActivity() { LoggingUtil.resetRootHandler(FixedAndroidLogHandler()) //Logger.getLogger("org.fourthline.cling").level = Level.FINE - applicationContext.bindService(Intent(this, DeviceService::class.java), deviceConnection, Context.BIND_AUTO_CREATE) + applicationContext.bindService(Intent(this, DeviceService::class.java), deviceManager.connection, Context.BIND_AUTO_CREATE) } } @Composable -fun Content(mainViewModel: MainViewModel = viewModel(), devices: SnapshotStateMap) { +fun Content(mainViewModel: MainViewModel = viewModel()) { val navController = rememberNavController() val loginState by mainViewModel.loginState.observeAsState() @@ -162,7 +142,7 @@ fun Content(mainViewModel: MainViewModel = viewModel(), devices: SnapshotStateMa AlbumView(entry.arguments!!.getInt("albumId"), navController) } } - composable("devices") { Base(navController, mainViewModel) { Devices(devices = devices) } } + composable("devices") { Base(navController, mainViewModel) { Devices() } } } } }