From f34bd63dc91c5c4f1de72e3471c6e772bd7801f5 Mon Sep 17 00:00:00 2001 From: Sean Proctor Date: Mon, 8 Jul 2024 18:45:22 -0400 Subject: [PATCH 1/3] fix stop button in demo --- .../ivanempire/lighthouse/demo/MainActivity.kt | 2 +- .../lighthouse/demo/MainActivityViewModel.kt | 17 ++++++++++++----- .../lighthouse/core/RealLighthouseClient.kt | 5 ++++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/demo/src/main/java/com/ivanempire/lighthouse/demo/MainActivity.kt b/demo/src/main/java/com/ivanempire/lighthouse/demo/MainActivity.kt index 2854ed9..5c5d5f3 100644 --- a/demo/src/main/java/com/ivanempire/lighthouse/demo/MainActivity.kt +++ b/demo/src/main/java/com/ivanempire/lighthouse/demo/MainActivity.kt @@ -90,7 +90,7 @@ class MainActivity : ComponentActivity() { viewModel.stopDiscovery() isDiscoveryRunning.value = false }, - enabled = !isDiscoveryRunning.value + enabled = isDiscoveryRunning.value ) { Text(text = "Stop discovery") } diff --git a/demo/src/main/java/com/ivanempire/lighthouse/demo/MainActivityViewModel.kt b/demo/src/main/java/com/ivanempire/lighthouse/demo/MainActivityViewModel.kt index 39edddd..f6c5a5f 100644 --- a/demo/src/main/java/com/ivanempire/lighthouse/demo/MainActivityViewModel.kt +++ b/demo/src/main/java/com/ivanempire/lighthouse/demo/MainActivityViewModel.kt @@ -4,10 +4,13 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ivanempire.lighthouse.LighthouseClient import com.ivanempire.lighthouse.models.devices.AbridgedMediaDevice +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -21,11 +24,15 @@ class MainActivityViewModel( val discoveredDevices = backingDiscoveredDevices.asStateFlow() fun startDiscovery() { - discoveryJob = - viewModelScope.launch { - lighthouseClient.discoverDevices().collect { backingDiscoveredDevices.value = it } - } + viewModelScope.launch { + discoveryJob = + lighthouseClient.discoverDevices() + .onEach { backingDiscoveredDevices.value = it } + .launchIn(viewModelScope) + } } - fun stopDiscovery() = runBlocking { discoveryJob?.cancelAndJoin() } + fun stopDiscovery() { + discoveryJob?.cancel() + } } diff --git a/lighthouse/src/main/java/com/ivanempire/lighthouse/core/RealLighthouseClient.kt b/lighthouse/src/main/java/com/ivanempire/lighthouse/core/RealLighthouseClient.kt index b2b5c47..d83ef0c 100644 --- a/lighthouse/src/main/java/com/ivanempire/lighthouse/core/RealLighthouseClient.kt +++ b/lighthouse/src/main/java/com/ivanempire/lighthouse/core/RealLighthouseClient.kt @@ -43,8 +43,11 @@ internal class RealLighthouseClient( return merge(foundDevicesFlow, lostDevicesFlow) .distinctUntilChanged() + .onCompletion { + logger?.logStatusMessage(TAG, "Discovery stopped") + discoveryMutex.withLock { isDiscoveryRunning = false } + } .flowOn(dispatcher) - .onCompletion { discoveryMutex.withLock { isDiscoveryRunning = false } } } private companion object { From 07e1f62e62cee2dbd98a081ee2923c963116de9d Mon Sep 17 00:00:00 2001 From: Sean Proctor Date: Mon, 8 Jul 2024 22:22:05 -0400 Subject: [PATCH 2/3] remove limitation on multiple concurrent usages --- buildSrc/src/main/kotlin/Projects.kt | 8 +-- .../lighthouse/demo/MainActivityViewModel.kt | 4 +- .../ivanempire/lighthouse/LighthouseClient.kt | 19 ++--- .../lighthouse/core/DiscoveryManager.kt | 29 -------- .../lighthouse/core/LighthouseState.kt | 4 +- .../lighthouse/core/RealDiscoveryManager.kt | 61 ---------------- .../lighthouse/core/RealLighthouseClient.kt | 71 +++++++++++-------- 7 files changed, 53 insertions(+), 143 deletions(-) delete mode 100644 lighthouse/src/main/java/com/ivanempire/lighthouse/core/DiscoveryManager.kt delete mode 100644 lighthouse/src/main/java/com/ivanempire/lighthouse/core/RealDiscoveryManager.kt diff --git a/buildSrc/src/main/kotlin/Projects.kt b/buildSrc/src/main/kotlin/Projects.kt index c0089c1..068f696 100644 --- a/buildSrc/src/main/kotlin/Projects.kt +++ b/buildSrc/src/main/kotlin/Projects.kt @@ -1,10 +1,7 @@ package lighthouse -import com.android.build.api.dsl.CommonExtension -import com.android.build.api.dsl.Lint import com.android.build.gradle.BaseExtension import com.android.build.gradle.LibraryExtension -import com.android.build.gradle.TestExtension import com.android.build.gradle.internal.dsl.BaseAppModuleExtension import com.vanniktech.maven.publish.MavenPublishBaseExtension import com.vanniktech.maven.publish.SonatypeHost @@ -19,9 +16,7 @@ import org.gradle.kotlin.dsl.get import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile -/** - * - */ + fun Project.setupLibraryModule( moduleName: String, shouldBePublished: Boolean, @@ -97,6 +92,7 @@ fun Project.setupDemoModule( applicationId = name versionCode = 1 versionName = project.versionName + targetSdk = project.targetSdk } buildFeatures { compose = true diff --git a/demo/src/main/java/com/ivanempire/lighthouse/demo/MainActivityViewModel.kt b/demo/src/main/java/com/ivanempire/lighthouse/demo/MainActivityViewModel.kt index f6c5a5f..73047ec 100644 --- a/demo/src/main/java/com/ivanempire/lighthouse/demo/MainActivityViewModel.kt +++ b/demo/src/main/java/com/ivanempire/lighthouse/demo/MainActivityViewModel.kt @@ -6,13 +6,12 @@ import com.ivanempire.lighthouse.LighthouseClient import com.ivanempire.lighthouse.models.devices.AbridgedMediaDevice import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking class MainActivityViewModel( private val lighthouseClient: LighthouseClient, @@ -27,6 +26,7 @@ class MainActivityViewModel( viewModelScope.launch { discoveryJob = lighthouseClient.discoverDevices() + .flowOn(Dispatchers.IO) .onEach { backingDiscoveredDevices.value = it } .launchIn(viewModelScope) } diff --git a/lighthouse/src/main/java/com/ivanempire/lighthouse/LighthouseClient.kt b/lighthouse/src/main/java/com/ivanempire/lighthouse/LighthouseClient.kt index 2201598..db9ea7e 100644 --- a/lighthouse/src/main/java/com/ivanempire/lighthouse/LighthouseClient.kt +++ b/lighthouse/src/main/java/com/ivanempire/lighthouse/LighthouseClient.kt @@ -2,14 +2,11 @@ package com.ivanempire.lighthouse import android.content.Context import android.net.wifi.WifiManager -import com.ivanempire.lighthouse.core.LighthouseState -import com.ivanempire.lighthouse.core.RealDiscoveryManager import com.ivanempire.lighthouse.core.RealLighthouseClient import com.ivanempire.lighthouse.models.Constants.DEFAULT_SEARCH_REQUEST import com.ivanempire.lighthouse.models.devices.AbridgedMediaDevice import com.ivanempire.lighthouse.models.search.SearchRequest import com.ivanempire.lighthouse.socket.RealSocketListener -import java.lang.IllegalStateException import kotlinx.coroutines.flow.Flow /** The main entrypoint for the Lighthouse library */ @@ -20,8 +17,6 @@ interface LighthouseClient { private var retryCount = 1 - private var shouldPersist = false - private var logger: LighthouseLogger? = null private val wifiManager = @@ -38,8 +33,6 @@ interface LighthouseClient { this.retryCount += retryCount } - fun setShouldPersist(shouldPersist: Boolean) = apply { this.shouldPersist = shouldPersist } - /** * Specify a custom implementation of [LighthouseLogger] in order to log events from the * library at the consumer level @@ -51,14 +44,10 @@ interface LighthouseClient { fun build(): LighthouseClient { val socketListener = RealSocketListener(wifiManager, retryCount, logger) - val discoveryManager = - RealDiscoveryManager( - shouldPersist, - LighthouseState(logger), - socketListener, - logger, - ) - return RealLighthouseClient(discoveryManager, logger = logger) + return RealLighthouseClient( + socketListener = socketListener, + logger = logger, + ) } } diff --git a/lighthouse/src/main/java/com/ivanempire/lighthouse/core/DiscoveryManager.kt b/lighthouse/src/main/java/com/ivanempire/lighthouse/core/DiscoveryManager.kt deleted file mode 100644 index 32004dd..0000000 --- a/lighthouse/src/main/java/com/ivanempire/lighthouse/core/DiscoveryManager.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.ivanempire.lighthouse.core - -import com.ivanempire.lighthouse.models.devices.AbridgedMediaDevice -import com.ivanempire.lighthouse.models.search.SearchRequest -import kotlinx.coroutines.flow.Flow - -/** - * All discovery managers should conform to this interface in order to setup two required list - * flows: the newly-discovered devices to add, and the stale devices to remove - */ -internal interface DiscoveryManager { - - /** - * Creates a [Flow] of a list of [AbridgedMediaDevice] instances which have been built after - * receiving the proper SSDP packets from the multicast socket. - * - * @param searchRequest The [SearchRequest] to send at the start of device discovery - */ - fun createNewDeviceFlow(searchRequest: SearchRequest): Flow> - - /** - * Creates a [Flow] of a list of [AbridgedMediaDevice] instances that have not received any SSDP - * packets in the last [AbridgedMediaDevice.cache] seconds. These will be removed from the - * library's device list when this flow is combined with [createNewDeviceFlow]. - * - * @return [Flow] of a list of stale [AbridgedMediaDevice] instances - */ - fun createStaleDeviceFlow(): Flow> -} diff --git a/lighthouse/src/main/java/com/ivanempire/lighthouse/core/LighthouseState.kt b/lighthouse/src/main/java/com/ivanempire/lighthouse/core/LighthouseState.kt index c83734c..5b9562b 100644 --- a/lighthouse/src/main/java/com/ivanempire/lighthouse/core/LighthouseState.kt +++ b/lighthouse/src/main/java/com/ivanempire/lighthouse/core/LighthouseState.kt @@ -194,13 +194,13 @@ internal class LighthouseState(private val logger: LighthouseLogger? = null) { } /** - * Iterates over the entire device list and filters out any stale devices. A stale device is + * Iterates over the entire device list and removes any stale devices. A stale device is * defined as one that has not seen a media packet in the last [AbridgedMediaDevice.cache] * seconds * * @return A device list with stale root devices removed */ - fun parseStaleDevices() { + fun removeStaleDevices() { backingDeviceList.update { currentList -> currentList.filterNot { System.currentTimeMillis() - it.latestTimestamp > it.cache * 1000 diff --git a/lighthouse/src/main/java/com/ivanempire/lighthouse/core/RealDiscoveryManager.kt b/lighthouse/src/main/java/com/ivanempire/lighthouse/core/RealDiscoveryManager.kt deleted file mode 100644 index 8041e77..0000000 --- a/lighthouse/src/main/java/com/ivanempire/lighthouse/core/RealDiscoveryManager.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.ivanempire.lighthouse.core - -import com.ivanempire.lighthouse.LighthouseLogger -import com.ivanempire.lighthouse.models.devices.AbridgedMediaDevice -import com.ivanempire.lighthouse.models.search.SearchRequest -import com.ivanempire.lighthouse.parsers.DatagramPacketTransformer -import com.ivanempire.lighthouse.parsers.packets.MediaPacketParser -import com.ivanempire.lighthouse.socket.SocketListener -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.isActive - -/** - * Specific implementation of [DiscoveryManager] - * - * @param lighthouseState A library-wide instance of [LighthouseState] to keep track of discovered - * devices - * @param multicastSocketListener An implementation of [SocketListener] to send/receive packets from - * the network - */ -internal class RealDiscoveryManager( - private val shouldPersist: Boolean, - private val lighthouseState: LighthouseState, - private val multicastSocketListener: SocketListener, - private val logger: LighthouseLogger? = null, -) : DiscoveryManager { - - @OptIn(ExperimentalCoroutinesApi::class) - override fun createNewDeviceFlow( - searchRequest: SearchRequest - ): Flow> { - if (!shouldPersist) { - lighthouseState.resetDeviceList() - } - return multicastSocketListener - .listenForPackets(searchRequest) - .mapNotNull { DatagramPacketTransformer(it, logger) } - .mapNotNull { MediaPacketParser(it, logger) } - .onEach { lighthouseState.parseMediaPacket(it) } - .flatMapLatest { lighthouseState.deviceList } - .filter { it.isNotEmpty() } - } - - override fun createStaleDeviceFlow(): Flow> { - return flow { - while (currentCoroutineContext().isActive) { - delay(1000) - lighthouseState.parseStaleDevices() - emit(lighthouseState.deviceList.value) - } - } - .filter { it.isNotEmpty() } - } -} diff --git a/lighthouse/src/main/java/com/ivanempire/lighthouse/core/RealLighthouseClient.kt b/lighthouse/src/main/java/com/ivanempire/lighthouse/core/RealLighthouseClient.kt index d83ef0c..cf39aef 100644 --- a/lighthouse/src/main/java/com/ivanempire/lighthouse/core/RealLighthouseClient.kt +++ b/lighthouse/src/main/java/com/ivanempire/lighthouse/core/RealLighthouseClient.kt @@ -4,50 +4,65 @@ import com.ivanempire.lighthouse.LighthouseClient import com.ivanempire.lighthouse.LighthouseLogger import com.ivanempire.lighthouse.models.devices.AbridgedMediaDevice import com.ivanempire.lighthouse.models.search.SearchRequest -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers +import com.ivanempire.lighthouse.parsers.DatagramPacketTransformer +import com.ivanempire.lighthouse.parsers.packets.MediaPacketParser +import com.ivanempire.lighthouse.socket.SocketListener +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.isActive /** Specific implementation of [LighthouseClient] */ internal class RealLighthouseClient( - private val discoveryManager: DiscoveryManager, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO, - private val logger: LighthouseLogger? = null, + private val socketListener: SocketListener, + private val logger: LighthouseLogger?, ) : LighthouseClient { - private val discoveryMutex = Mutex() - private var isDiscoveryRunning = false - override suspend fun discoverDevices( - searchRequest: SearchRequest + searchRequest: SearchRequest, ): Flow> { - discoveryMutex.withLock { - if (isDiscoveryRunning) { - throw IllegalStateException( - "Discovery is already in progress - did you call discoverDevices() multiple times?" - ) - } - isDiscoveryRunning = true - } + val state = LighthouseState(logger) logger?.logStatusMessage(TAG, "Discovering devices with search request: $searchRequest") - val foundDevicesFlow = discoveryManager.createNewDeviceFlow(searchRequest) - val lostDevicesFlow = discoveryManager.createStaleDeviceFlow() + val foundDevicesFlow = createNewDeviceFlow(searchRequest, state) + val withoutStaleDevicesFlow = createNonStaleDeviceFlow(state) - return merge(foundDevicesFlow, lostDevicesFlow) + return merge(foundDevicesFlow, withoutStaleDevicesFlow) .distinctUntilChanged() - .onCompletion { - logger?.logStatusMessage(TAG, "Discovery stopped") - discoveryMutex.withLock { isDiscoveryRunning = false } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun createNewDeviceFlow( + searchRequest: SearchRequest, + lighthouseState: LighthouseState, + ): Flow> { + return socketListener + .listenForPackets(searchRequest) + .mapNotNull { DatagramPacketTransformer(it, logger) } + .mapNotNull { MediaPacketParser(it, logger) } + .onEach { lighthouseState.parseMediaPacket(it) } + .flatMapLatest { lighthouseState.deviceList } + .filter { it.isNotEmpty() } + } + + private fun createNonStaleDeviceFlow(lighthouseState: LighthouseState): Flow> { + return flow { + while (currentCoroutineContext().isActive) { + delay(1000) + lighthouseState.removeStaleDevices() + emit(lighthouseState.deviceList.value) } - .flowOn(dispatcher) + } + .filter { it.isNotEmpty() } } private companion object { From 2609a2c5658a68b4fe5168eccf72dd67f4ed7c0f Mon Sep 17 00:00:00 2001 From: Sean Proctor Date: Mon, 8 Jul 2024 22:44:48 -0400 Subject: [PATCH 3/3] run through ktfmt --- .../lighthouse/demo/MainActivityViewModel.kt | 3 ++- .../lighthouse/core/LighthouseState.kt | 5 ++--- .../lighthouse/core/RealLighthouseClient.kt | 17 +++++++++-------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/demo/src/main/java/com/ivanempire/lighthouse/demo/MainActivityViewModel.kt b/demo/src/main/java/com/ivanempire/lighthouse/demo/MainActivityViewModel.kt index 73047ec..e2e6040 100644 --- a/demo/src/main/java/com/ivanempire/lighthouse/demo/MainActivityViewModel.kt +++ b/demo/src/main/java/com/ivanempire/lighthouse/demo/MainActivityViewModel.kt @@ -25,7 +25,8 @@ class MainActivityViewModel( fun startDiscovery() { viewModelScope.launch { discoveryJob = - lighthouseClient.discoverDevices() + lighthouseClient + .discoverDevices() .flowOn(Dispatchers.IO) .onEach { backingDiscoveredDevices.value = it } .launchIn(viewModelScope) diff --git a/lighthouse/src/main/java/com/ivanempire/lighthouse/core/LighthouseState.kt b/lighthouse/src/main/java/com/ivanempire/lighthouse/core/LighthouseState.kt index 5b9562b..58d6d26 100644 --- a/lighthouse/src/main/java/com/ivanempire/lighthouse/core/LighthouseState.kt +++ b/lighthouse/src/main/java/com/ivanempire/lighthouse/core/LighthouseState.kt @@ -194,9 +194,8 @@ internal class LighthouseState(private val logger: LighthouseLogger? = null) { } /** - * Iterates over the entire device list and removes any stale devices. A stale device is - * defined as one that has not seen a media packet in the last [AbridgedMediaDevice.cache] - * seconds + * Iterates over the entire device list and removes any stale devices. A stale device is defined + * as one that has not seen a media packet in the last [AbridgedMediaDevice.cache] seconds * * @return A device list with stale root devices removed */ diff --git a/lighthouse/src/main/java/com/ivanempire/lighthouse/core/RealLighthouseClient.kt b/lighthouse/src/main/java/com/ivanempire/lighthouse/core/RealLighthouseClient.kt index cf39aef..65c1174 100644 --- a/lighthouse/src/main/java/com/ivanempire/lighthouse/core/RealLighthouseClient.kt +++ b/lighthouse/src/main/java/com/ivanempire/lighthouse/core/RealLighthouseClient.kt @@ -36,8 +36,7 @@ internal class RealLighthouseClient( val foundDevicesFlow = createNewDeviceFlow(searchRequest, state) val withoutStaleDevicesFlow = createNonStaleDeviceFlow(state) - return merge(foundDevicesFlow, withoutStaleDevicesFlow) - .distinctUntilChanged() + return merge(foundDevicesFlow, withoutStaleDevicesFlow).distinctUntilChanged() } @OptIn(ExperimentalCoroutinesApi::class) @@ -54,14 +53,16 @@ internal class RealLighthouseClient( .filter { it.isNotEmpty() } } - private fun createNonStaleDeviceFlow(lighthouseState: LighthouseState): Flow> { + private fun createNonStaleDeviceFlow( + lighthouseState: LighthouseState + ): Flow> { return flow { - while (currentCoroutineContext().isActive) { - delay(1000) - lighthouseState.removeStaleDevices() - emit(lighthouseState.deviceList.value) + while (currentCoroutineContext().isActive) { + delay(1000) + lighthouseState.removeStaleDevices() + emit(lighthouseState.deviceList.value) + } } - } .filter { it.isNotEmpty() } }