From 47e378bdacbe37e2c4b497a30872418244bc3537 Mon Sep 17 00:00:00 2001 From: Andre Weber Date: Wed, 4 Oct 2023 14:43:02 +0200 Subject: [PATCH] feature: Notify about DataBroker Disconnect Closes: #12 Signed-Off-By: Andre Weber --- .../databroker/JavaDataBrokerEngine.java | 27 ++++++++++ .../kuksa/testapp/KuksaDataBrokerActivity.kt | 23 ++++++++- .../testapp/databroker/DataBrokerEngine.kt | 5 ++ .../databroker/KotlinDataBrokerEngine.kt | 18 +++++++ .../org/eclipse/kuksa/DataBrokerConnection.kt | 11 ++++ .../org/eclipse/kuksa/DisconnectListener.kt | 31 ++++++++++++ .../kotlin/org/eclipse/kuksa/MultiListener.kt | 50 +++++++++++++++++++ .../eclipse/kuksa/DataBrokerConnectionTest.kt | 27 ++++++++++ 8 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DisconnectListener.kt create mode 100644 kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/MultiListener.kt diff --git a/app/src/main/java/org/eclipse/kuksa/testapp/databroker/JavaDataBrokerEngine.java b/app/src/main/java/org/eclipse/kuksa/testapp/databroker/JavaDataBrokerEngine.java index 4cbcad1b..3849d042 100644 --- a/app/src/main/java/org/eclipse/kuksa/testapp/databroker/JavaDataBrokerEngine.java +++ b/app/src/main/java/org/eclipse/kuksa/testapp/databroker/JavaDataBrokerEngine.java @@ -27,6 +27,7 @@ import org.eclipse.kuksa.CoroutineCallback; import org.eclipse.kuksa.DataBrokerConnection; import org.eclipse.kuksa.DataBrokerConnector; +import org.eclipse.kuksa.DisconnectListener; import org.eclipse.kuksa.PropertyObserver; import org.eclipse.kuksa.TimeoutConfig; import org.eclipse.kuksa.model.Property; @@ -41,7 +42,9 @@ import java.io.IOException; import java.io.InputStream; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; @@ -62,6 +65,8 @@ public class JavaDataBrokerEngine implements DataBrokerEngine { @Nullable private DataBrokerConnection dataBrokerConnection = null; + private final Set disconnectListeners = new HashSet<>(); + public JavaDataBrokerEngine(@NonNull AssetManager assetManager) { this.assetManager = assetManager; } @@ -135,7 +140,12 @@ private void connect( connector.connect(new CoroutineCallback<>() { @Override public void onSuccess(@Nullable DataBrokerConnection result) { + if (result == null) return; + JavaDataBrokerEngine.this.dataBrokerConnection = result; + for (DisconnectListener listener : disconnectListeners) { + result.getDisconnectListeners().register(listener); + } callback.onSuccess(result); } @@ -186,6 +196,7 @@ public void disconnect() { } dataBrokerConnection.disconnect(); + dataBrokerConnection = null; } @Nullable @@ -198,4 +209,20 @@ public DataBrokerConnection getDataBrokerConnection() { public void setDataBrokerConnection(@Nullable DataBrokerConnection dataBrokerConnection) { this.dataBrokerConnection = dataBrokerConnection; } + + @Override + public void registerDisconnectListener(@NonNull DisconnectListener listener) { + disconnectListeners.add(listener); + if (dataBrokerConnection != null) { + dataBrokerConnection.getDisconnectListeners().register(listener); + } + } + + @Override + public void unregisterDisconnectListener(@NonNull DisconnectListener listener) { + disconnectListeners.remove(listener); + if (dataBrokerConnection != null) { + dataBrokerConnection.getDisconnectListeners().unregister(listener); + } + } } diff --git a/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt b/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt index 7dd17429..ab00c045 100644 --- a/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt +++ b/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import org.eclipse.kuksa.CoroutineCallback import org.eclipse.kuksa.DataBrokerConnection +import org.eclipse.kuksa.DisconnectListener import org.eclipse.kuksa.PropertyObserver import org.eclipse.kuksa.extension.metadata import org.eclipse.kuksa.model.Property @@ -61,20 +62,26 @@ class KuksaDataBrokerActivity : ComponentActivity() { private val dataBrokerConnectionCallback = object : CoroutineCallback() { override fun onSuccess(result: DataBrokerConnection?) { - outputViewModel.appendOutput("Connection to data broker was successful") + outputViewModel.appendOutput("Connection to DataBroker successful established") connectionViewModel.updateConnectionState(ConnectionViewState.CONNECTED) } override fun onError(error: Throwable) { - outputViewModel.appendOutput("Connection to data broker failed: ${error.message}") + outputViewModel.appendOutput("Connection to DataBroker failed: ${error.message}") connectionViewModel.updateConnectionState(ConnectionViewState.DISCONNECTED) } } + private val onDisconnectListener = DisconnectListener { + connectionViewModel.updateConnectionState(ConnectionViewState.DISCONNECTED) + outputViewModel.appendOutput("DataBroker disconnected") + } + private lateinit var dataBrokerEngine: DataBrokerEngine private val kotlinDataBrokerEngine by lazy { KotlinDataBrokerEngine(lifecycleScope, assets) } + private val javaDataBrokerEngine by lazy { JavaDataBrokerEngine(assets) } @@ -125,6 +132,18 @@ class KuksaDataBrokerActivity : ComponentActivity() { } } + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + + dataBrokerEngine.registerDisconnectListener(onDisconnectListener) + } + + override fun onDestroy() { + super.onDestroy() + + dataBrokerEngine.unregisterDisconnectListener(onDisconnectListener) + } + private fun connect(connectionInfo: ConnectionInfo) { Log.d(TAG, "Connecting to DataBroker: $connectionInfo") diff --git a/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/DataBrokerEngine.kt b/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/DataBrokerEngine.kt index 680b9f33..5729e613 100644 --- a/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/DataBrokerEngine.kt +++ b/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/DataBrokerEngine.kt @@ -21,6 +21,7 @@ package org.eclipse.kuksa.testapp.databroker import org.eclipse.kuksa.CoroutineCallback import org.eclipse.kuksa.DataBrokerConnection +import org.eclipse.kuksa.DisconnectListener import org.eclipse.kuksa.PropertyObserver import org.eclipse.kuksa.model.Property import org.eclipse.kuksa.proto.v1.KuksaValV1 @@ -41,4 +42,8 @@ interface DataBrokerEngine { fun subscribe(property: Property, propertyObserver: PropertyObserver) fun disconnect() + + fun registerDisconnectListener(listener: DisconnectListener) + + fun unregisterDisconnectListener(listener: DisconnectListener) } diff --git a/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/KotlinDataBrokerEngine.kt b/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/KotlinDataBrokerEngine.kt index 13ed58d8..a65cb604 100644 --- a/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/KotlinDataBrokerEngine.kt +++ b/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/KotlinDataBrokerEngine.kt @@ -31,6 +31,7 @@ import org.eclipse.kuksa.CoroutineCallback import org.eclipse.kuksa.DataBrokerConnection import org.eclipse.kuksa.DataBrokerConnector import org.eclipse.kuksa.DataBrokerException +import org.eclipse.kuksa.DisconnectListener import org.eclipse.kuksa.PropertyObserver import org.eclipse.kuksa.TimeoutConfig import org.eclipse.kuksa.model.Property @@ -47,6 +48,8 @@ class KotlinDataBrokerEngine( ) : DataBrokerEngine { override var dataBrokerConnection: DataBrokerConnection? = null + private val disconnectListeners = mutableSetOf() + override fun connect( connectionInfo: ConnectionInfo, callback: CoroutineCallback, @@ -118,6 +121,10 @@ class KotlinDataBrokerEngine( lifecycleScope.launch { try { dataBrokerConnection = connector.connect() + .also { connection -> + disconnectListeners.forEach { listener -> connection.disconnectListeners.register(listener) } + } + callback.onSuccess(dataBrokerConnection) } catch (e: DataBrokerException) { callback.onError(e) @@ -158,6 +165,17 @@ class KotlinDataBrokerEngine( override fun disconnect() { dataBrokerConnection?.disconnect() + dataBrokerConnection = null + } + + override fun registerDisconnectListener(listener: DisconnectListener) { + disconnectListeners.add(listener) + dataBrokerConnection?.disconnectListeners?.register(listener) + } + + override fun unregisterDisconnectListener(listener: DisconnectListener) { + disconnectListeners.remove(listener) + dataBrokerConnection?.disconnectListeners?.unregister(listener) } companion object { diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerConnection.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerConnection.kt index 4a934143..bc4fa62e 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerConnection.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerConnection.kt @@ -45,6 +45,17 @@ class DataBrokerConnection internal constructor( private val managedChannel: ManagedChannel, private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default, ) { + val disconnectListeners = MultiListener() + + init { + val state = managedChannel.getState(false) + managedChannel.notifyWhenStateChanged(state) { + val listeners = disconnectListeners.get() + listeners.forEach { listener -> + listener.onDisconnect() + } + } + } /** * Subscribes to the specified vssPath with the provided propertyObserver. Once subscribed the application will be diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DisconnectListener.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DisconnectListener.kt new file mode 100644 index 00000000..bbb4b173 --- /dev/null +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DisconnectListener.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.eclipse.kuksa + +/** + * The [DisconnectListener] can be registered to [DataBrokerConnection.disconnectListeners] + * When registered it will notify about manual or unexpected connection disconnects from the DataBroker. + */ +fun interface DisconnectListener { + /** + * Will be triggered, when the connection to the DataBroker was closed manually or unexpectedly. + */ + fun onDisconnect() +} diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/MultiListener.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/MultiListener.kt new file mode 100644 index 00000000..7c6dad6b --- /dev/null +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/MultiListener.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.eclipse.kuksa + +/** + * Generic Listener interface, to support multiple listeners. + */ +class MultiListener { + private var listeners: MutableSet = mutableSetOf() + + /** + * Adds a new [listener] and returns true if the [listener] was successfully added, returns false otherwise. + * A [listener] can only be added once. + */ + fun register(listener: T): Boolean { + return listeners.add(listener) + } + + /** + * Removes a [listener] and returns true if the [listener] was successfully removed, returns false otherwise. + */ + fun unregister(listener: T): Boolean { + return listeners.remove(listener) + } + + /** + * Retrieves a defensive copy of the underlying list of listeners. + */ + @JvmSynthetic + internal fun get(): List { + return listeners.toList() + } +} diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt index 03a9ef98..6afe6c10 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt @@ -21,6 +21,7 @@ package org.eclipse.kuksa import io.grpc.ManagedChannel import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe import io.mockk.clearMocks import io.mockk.mockk import io.mockk.slot @@ -178,6 +179,32 @@ class DataBrokerConnectionTest : BehaviorSpec({ } } } + + // this test closes the connection, the connection can't be used afterward anymore + `when`("A DisconnectListener is registered successfully") { + val disconnectListener = mockk() + val disconnectListeners = dataBrokerConnection.disconnectListeners + disconnectListeners.register(disconnectListener) + + then("The number of registered DisconnectListeners should be 1") { + disconnectListeners.get().size shouldBe 1 + } + `when`("Trying to register the same listener again") { + disconnectListeners.register(disconnectListener) + + then("It is not added multiple times") { + disconnectListeners.get().size shouldBe 1 + } + } + `when`("The Connection is closed manually") { + dataBrokerConnection.disconnect() + + then("The DisconnectListener is triggered") { + verify { disconnectListener.onDisconnect() } + } + } + } + // connection is closed at this point } given("A DataBrokerConnection with a mocked ManagedChannel") { val managedChannel = mockk(relaxed = true)