From f23d154493b721653b6eac8667e8931e1585ec70 Mon Sep 17 00:00:00 2001 From: Andre Weber Date: Wed, 4 Oct 2023 14:43:02 +0200 Subject: [PATCH 1/6] 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) From 37066927cc3595ce6a6ed8129656b2410afa906a Mon Sep 17 00:00:00 2001 From: Andre Weber Date: Thu, 5 Oct 2023 12:31:02 +0200 Subject: [PATCH 2/6] docs: Adapt Architecture Documentation --- docs/kuksa-sdk_class-diagram.puml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/kuksa-sdk_class-diagram.puml b/docs/kuksa-sdk_class-diagram.puml index 250e8600..a7d064b3 100644 --- a/docs/kuksa-sdk_class-diagram.puml +++ b/docs/kuksa-sdk_class-diagram.puml @@ -26,6 +26,8 @@ package kuksa { DataBrokerConnection -down-> PropertyObserver DataBrokerConnection -down-> Property DataBrokerConnection -left-> DataBrokerException + DataBrokerConnection -up-> MultiListener + MultiListener -right-> DisconnectListener TimeoutConfig -left-* DataBrokerConnector class DataBrokerConnector { @@ -38,6 +40,7 @@ package kuksa { } class DataBrokerConnection { + + disconnectListeners: MultiListener + subscribe(List, PropertyObserver) + fetchProperty(Property): GetResponse + updateProperty(Property, Datapoint): SetResponse @@ -54,6 +57,15 @@ package kuksa { } class DataBrokerException + + abstract class MultiListener { + + register(T) + + unregister(T) + } + + interface DisconnectListener { + + onDisconnect() + } } DataBrokerConnector -up-> ManagedChannel From d03efda4fc227d195e8bb8095c1fd0ee20a2885a Mon Sep 17 00:00:00 2001 From: Andre Weber Date: Thu, 5 Oct 2023 12:39:49 +0200 Subject: [PATCH 3/6] chore: Adapt Sample Apps --- .../java/com/example/sample/JavaActivity.java | 18 +++++++++++++++++- .../com/example/sample/KotlinActivity.kt | 10 ++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/samples/src/main/java/com/example/sample/JavaActivity.java b/samples/src/main/java/com/example/sample/JavaActivity.java index 49a851d6..6abf5614 100644 --- a/samples/src/main/java/com/example/sample/JavaActivity.java +++ b/samples/src/main/java/com/example/sample/JavaActivity.java @@ -25,6 +25,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.model.Property; import org.eclipse.kuksa.proto.v1.KuksaValV1; import org.eclipse.kuksa.proto.v1.KuksaValV1.GetResponse; @@ -42,6 +43,11 @@ import io.grpc.TlsChannelCredentials; public class JavaActivity extends AppCompatActivity { + + private final DisconnectListener disconnectListener = () -> { + // connection closed manually or unexpectedly + }; + @Nullable private DataBrokerConnection dataBrokerConnection = null; @@ -54,8 +60,10 @@ public void connectInsecure(String host, int port) { connector.connect(new CoroutineCallback() { @Override public void onSuccess(DataBrokerConnection result) { - dataBrokerConnection = result; + if (result == null) return; + dataBrokerConnection = result; + dataBrokerConnection.getDisconnectListeners().register(disconnectListener); // handle result } @@ -138,4 +146,12 @@ public void onError(@NonNull Throwable error) { }); } + + public void disconnect() { + if (dataBrokerConnection == null) return; + + dataBrokerConnection.getDisconnectListeners().unregister(disconnectListener); + dataBrokerConnection.disconnect(); + dataBrokerConnection = null; + } } diff --git a/samples/src/main/kotlin/com/example/sample/KotlinActivity.kt b/samples/src/main/kotlin/com/example/sample/KotlinActivity.kt index 76e3563a..58ffbe53 100644 --- a/samples/src/main/kotlin/com/example/sample/KotlinActivity.kt +++ b/samples/src/main/kotlin/com/example/sample/KotlinActivity.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.launch 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.model.Property import org.eclipse.kuksa.proto.v1.Types.DataEntry @@ -38,6 +39,10 @@ import java.io.IOException @Suppress("UNUSED_VARIABLE", "SwallowedException") class KotlinActivity : AppCompatActivity() { + private var disconnectListener = DisconnectListener { + // connection closed manually or unexpectedly + } + private var dataBrokerConnection: DataBrokerConnection? = null fun connectInsecure(host: String, port: Int) { @@ -81,6 +86,9 @@ class KotlinActivity : AppCompatActivity() { val connector = DataBrokerConnector(managedChannel) try { dataBrokerConnection = connector.connect() + .apply { + disconnectListeners.register(disconnectListener) + } // Connection to DataBroker successfully established } catch (e: DataBrokerException) { // Connection to DataBroker failed @@ -121,6 +129,8 @@ class KotlinActivity : AppCompatActivity() { } fun disconnect() { + dataBrokerConnection?.disconnectListeners?.unregister(disconnectListener) dataBrokerConnection?.disconnect() + dataBrokerConnection = null } } From 29a19e9c7de4f37df59d0610c2be8e71c3d81b1d Mon Sep 17 00:00:00 2001 From: Andre Weber Date: Mon, 9 Oct 2023 08:27:56 +0200 Subject: [PATCH 4/6] chore: Channel was not Shutdown Properly --- .../org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt | 1 + .../kotlin/org/eclipse/kuksa/DataBrokerConnection.kt | 9 ++++++++- .../kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) 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 ab00c045..e35a1b41 100644 --- a/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt +++ b/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt @@ -74,6 +74,7 @@ class KuksaDataBrokerActivity : ComponentActivity() { private val onDisconnectListener = DisconnectListener { connectionViewModel.updateConnectionState(ConnectionViewState.DISCONNECTED) + outputViewModel.clear() outputViewModel.appendOutput("DataBroker disconnected") } 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 bc4fa62e..31280d3b 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerConnection.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerConnection.kt @@ -20,6 +20,7 @@ package org.eclipse.kuksa import android.util.Log +import io.grpc.ConnectivityState import io.grpc.ManagedChannel import io.grpc.StatusRuntimeException import io.grpc.stub.StreamObserver @@ -50,7 +51,13 @@ class DataBrokerConnection internal constructor( init { val state = managedChannel.getState(false) managedChannel.notifyWhenStateChanged(state) { + val newState = managedChannel.getState(false) + Log.d(TAG, "DataBrokerConnection state changed: $newState") + if (newState != ConnectivityState.SHUTDOWN) { + managedChannel.shutdownNow() + } val listeners = disconnectListeners.get() + listeners.forEach { listener -> listener.onDisconnect() } @@ -176,7 +183,7 @@ class DataBrokerConnection internal constructor( */ fun disconnect() { Log.d(TAG, "disconnect() called") - managedChannel.shutdown() + managedChannel.shutdownNow() } private companion object { 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 6afe6c10..c6f5cd05 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt @@ -214,7 +214,7 @@ class DataBrokerConnectionTest : BehaviorSpec({ dataBrokerConnection.disconnect() then("The Channel is shutDown") { - verify { managedChannel.shutdown() } + verify { managedChannel.shutdownNow() } } } } From 0ad492af77002c82e929230a4565413ca06c1f89 Mon Sep 17 00:00:00 2001 From: Andre Weber Date: Mon, 9 Oct 2023 09:13:16 +0200 Subject: [PATCH 5/6] chore: Make DisconnectListener work with JavaDataBrokerEngine --- .../eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) 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 e35a1b41..7ac27446 100644 --- a/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt +++ b/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt @@ -133,12 +133,6 @@ class KuksaDataBrokerActivity : ComponentActivity() { } } - override fun onPostCreate(savedInstanceState: Bundle?) { - super.onPostCreate(savedInstanceState) - - dataBrokerEngine.registerDisconnectListener(onDisconnectListener) - } - override fun onDestroy() { super.onDestroy() @@ -151,14 +145,14 @@ class KuksaDataBrokerActivity : ComponentActivity() { outputViewModel.appendOutput("Connecting to data broker - Please wait") connectionViewModel.updateConnectionState(ConnectionViewState.CONNECTING) + dataBrokerEngine.registerDisconnectListener(onDisconnectListener) dataBrokerEngine.connect(connectionInfo, dataBrokerConnectionCallback) } private fun disconnect() { Log.d(TAG, "Disconnecting from DataBroker") dataBrokerEngine.disconnect() - outputViewModel.clear() - connectionViewModel.updateConnectionState(ConnectionViewState.DISCONNECTED) + dataBrokerEngine.unregisterDisconnectListener(onDisconnectListener) } private fun fetchProperty(property: Property) { From 6fcbcf6db4da8e7e7d35926dd572eafc8cb6ef84 Mon Sep 17 00:00:00 2001 From: Andre Weber Date: Mon, 9 Oct 2023 11:05:07 +0200 Subject: [PATCH 6/6] chore: Add ListenerCollection Interface --- .../org/eclipse/kuksa/DataBrokerConnection.kt | 4 +- .../org/eclipse/kuksa/DisconnectListener.kt | 4 +- .../kuksa/pattern/listener/Listener.kt | 25 +++++ .../listener/ListenerCollection.kt} | 28 ++---- .../kuksa/pattern/listener/MultiListener.kt | 41 ++++++++ .../eclipse/kuksa/DataBrokerConnectionTest.kt | 11 --- .../pattern/listener/MultiListenerTest.kt | 95 +++++++++++++++++++ kuksa-sdk/src/test/kotlin/test/kotest/Tag.kt | 1 + 8 files changed, 174 insertions(+), 35 deletions(-) create mode 100644 kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/pattern/listener/Listener.kt rename kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/{MultiListener.kt => pattern/listener/ListenerCollection.kt} (61%) create mode 100644 kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/pattern/listener/MultiListener.kt create mode 100644 kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/pattern/listener/MultiListenerTest.kt 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 31280d3b..b272e4d5 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerConnection.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerConnection.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.eclipse.kuksa.model.Property +import org.eclipse.kuksa.pattern.listener.MultiListener import org.eclipse.kuksa.proto.v1.KuksaValV1 import org.eclipse.kuksa.proto.v1.KuksaValV1.GetResponse import org.eclipse.kuksa.proto.v1.KuksaValV1.SetResponse @@ -56,9 +57,8 @@ class DataBrokerConnection internal constructor( if (newState != ConnectivityState.SHUTDOWN) { managedChannel.shutdownNow() } - val listeners = disconnectListeners.get() - listeners.forEach { listener -> + disconnectListeners.forEach { listener -> listener.onDisconnect() } } diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DisconnectListener.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DisconnectListener.kt index bbb4b173..c61a5706 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DisconnectListener.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DisconnectListener.kt @@ -19,11 +19,13 @@ package org.eclipse.kuksa +import org.eclipse.kuksa.pattern.listener.Listener + /** * 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 { +fun interface DisconnectListener : Listener { /** * Will be triggered, when the connection to the DataBroker was closed manually or unexpectedly. */ diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/pattern/listener/Listener.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/pattern/listener/Listener.kt new file mode 100644 index 00000000..dac3578c --- /dev/null +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/pattern/listener/Listener.kt @@ -0,0 +1,25 @@ +/* + * 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.pattern.listener + +/** + * Marker Interface for generic listeners. + */ +interface Listener diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/MultiListener.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/pattern/listener/ListenerCollection.kt similarity index 61% rename from kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/MultiListener.kt rename to kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/pattern/listener/ListenerCollection.kt index 7c6dad6b..5dcaa238 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/MultiListener.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/pattern/listener/ListenerCollection.kt @@ -17,34 +17,20 @@ * */ -package org.eclipse.kuksa +package org.eclipse.kuksa.pattern.listener /** - * Generic Listener interface, to support multiple listeners. - */ -class MultiListener { - private var listeners: MutableSet = mutableSetOf() - + * The ListenerCollection interface provides methods to register and unregister multiple listeners with a generic type. + * The underlying collection decides if the same listener can be added only once or multiple times. +*/ +interface ListenerCollection : Iterable { /** * 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) - } + fun register(listener: T): Boolean /** * 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() - } + fun unregister(listener: T): Boolean } diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/pattern/listener/MultiListener.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/pattern/listener/MultiListener.kt new file mode 100644 index 00000000..98f800ba --- /dev/null +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/pattern/listener/MultiListener.kt @@ -0,0 +1,41 @@ +/* + * 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.pattern.listener + +/** + * The MultiListener supports registering and unregistering of multiple different listeners. + * The ListenerCollection is backed by a LinkedHashSet to prevent the same listener from being registered multiple + * times. The order of registered elements is kept in tact. + */ +class MultiListener : ListenerCollection { + private var listeners: MutableSet = LinkedHashSet() + + override fun register(listener: T): Boolean { + return listeners.add(listener) + } + + override fun unregister(listener: T): Boolean { + return listeners.remove(listener) + } + + override fun iterator(): Iterator { + return listeners.iterator() + } +} 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 c6f5cd05..7a13cff8 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt @@ -21,7 +21,6 @@ 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 @@ -186,16 +185,6 @@ class DataBrokerConnectionTest : BehaviorSpec({ 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() diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/pattern/listener/MultiListenerTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/pattern/listener/MultiListenerTest.kt new file mode 100644 index 00000000..8d868068 --- /dev/null +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/pattern/listener/MultiListenerTest.kt @@ -0,0 +1,95 @@ +/* + * 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.pattern.listener + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import test.kotest.Unit + +class MultiListenerTest : BehaviorSpec({ + tags(Unit) + + given("An Instance of a MultiListener with generic type TestListener") { + val classUnderTest = MultiListener() + + `when`("Trying to register a TestListener") { + val testListener = TestListener() + classUnderTest.register(testListener) + + then("The registration is successful") { + classUnderTest.count() shouldBe 1 + } + + `when`("Trying to register the same listener again") { + classUnderTest.register(testListener) + + then("The same listener should not be added a second time") { + classUnderTest.count() shouldBe 1 + } + } + + `when`("Trying to unregister the already registered listener") { + classUnderTest.unregister(testListener) + + then("It should be correctly removed") { + classUnderTest.count() shouldBe 0 + } + } + } + + and("Multiple TestListeners are already registered") { + val registeredListeners = ArrayList() // order is important + + repeat(100) { + val testListener = TestListener() + registeredListeners.add(testListener) + + classUnderTest.register(testListener) + } + + `when`("Iterating over the Collection using the iterator") { + val indexedValueIterator = classUnderTest.iterator().withIndex() + + then("The order should be left in tact") { + while (indexedValueIterator.hasNext()) { + val indexedValue = indexedValueIterator.next() + + val index = indexedValue.index + val testListener = indexedValue.value + testListener shouldBe registeredListeners[index] + } + } + } + } + } +}) + +fun MultiListener.count(): Int { + var count = 0 + + val iterator = iterator() + while (iterator.hasNext()) { + count++ + iterator.next() + } + return count +} + +class TestListener : Listener diff --git a/kuksa-sdk/src/test/kotlin/test/kotest/Tag.kt b/kuksa-sdk/src/test/kotlin/test/kotest/Tag.kt index ac2c3ec6..a8ad2a81 100644 --- a/kuksa-sdk/src/test/kotlin/test/kotest/Tag.kt +++ b/kuksa-sdk/src/test/kotlin/test/kotest/Tag.kt @@ -22,6 +22,7 @@ package test.kotest import io.kotest.core.NamedTag val Integration = NamedTag("Integration") +val Unit = NamedTag("Unit") val Secure = NamedTag("Secure") val Insecure = NamedTag("Insecure")