Skip to content

Commit

Permalink
Clean up device code with LiveData and Hilt
Browse files Browse the repository at this point in the history
  • Loading branch information
rien committed Jul 24, 2021
1 parent 6259356 commit e9c6fe3
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 98 deletions.
21 changes: 17 additions & 4 deletions app/src/main/java/me/vanpetegem/accentor/devices/Device.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
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
.icons
.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) {}
}


109 changes: 109 additions & 0 deletions app/src/main/java/me/vanpetegem/accentor/devices/DeviceManager.kt
Original file line number Diff line number Diff line change
@@ -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<Map<UDN, Device>>(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<RemoteDevice>()
.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"

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<UDN, Device>) {
FastScrollableGrid(devices.values.sortedBy { it.friendlyName }, { it.firstCharacter.uppercase() }) { DeviceCard(it) }
fun Devices(devicesViewModel: DevicesViewModel = hiltViewModel()) {
val devices: List<Device>? by devicesViewModel.devices().observeAsState()
FastScrollableGrid(devices ?: emptyList(), { it.firstCharacter }) { DeviceCard(it) }
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
@@ -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<List<Device>> = map(deviceManager.devices) { devices ->
devices.values.sortedWith(compareBy({ it.friendlyName }, { it.firstCharacter.uppercase() }))
}
}
36 changes: 8 additions & 28 deletions app/src/main/java/me/vanpetegem/accentor/ui/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -84,52 +84,32 @@ 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<RemoteDevice>()) {
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()
}
}

// Fix the logging integration between java.util.logging and Android internal logging
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<UDN, Device>) {
fun Content(mainViewModel: MainViewModel = viewModel()) {
val navController = rememberNavController()

val loginState by mainViewModel.loginState.observeAsState()
Expand Down Expand Up @@ -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() } }
}
}
}
Expand Down

0 comments on commit e9c6fe3

Please sign in to comment.