From 50935de2af0aa2c411ce6328b291255467f9e0fe Mon Sep 17 00:00:00 2001 From: mueller-ma Date: Wed, 24 Jan 2024 17:48:25 +0100 Subject: [PATCH] Support UI control via Item Closes #3540 Signed-off-by: mueller-ma --- .../org/openhab/habdroid/ui/MainActivity.kt | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/MainActivity.kt b/mobile/src/main/java/org/openhab/habdroid/ui/MainActivity.kt index 1864572b426..d16b55e7db4 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/MainActivity.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/MainActivity.kt @@ -14,6 +14,7 @@ package org.openhab.habdroid.ui import android.Manifest +import android.app.Dialog import android.app.PendingIntent import android.content.ActivityNotFoundException import android.content.ComponentName @@ -61,19 +62,24 @@ import androidx.core.view.isVisible import androidx.core.widget.ContentLoadingProgressBar import androidx.drawerlayout.widget.DrawerLayout import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.navigation.NavigationView import com.google.android.material.snackbar.Snackbar import de.duenndns.ssl.MemorizingTrustManager import java.nio.charset.Charset import java.util.concurrent.CancellationException import javax.jmdns.ServiceInfo +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.json.JSONException +import org.json.JSONObject import org.openhab.habdroid.BuildConfig import org.openhab.habdroid.R import org.openhab.habdroid.background.BackgroundTasksManager @@ -169,6 +175,9 @@ class MainActivity : AbstractBaseActivity(), ConnectionFactory.UpdateListener { private var inServerSelectionMode = false private var wifiSsidDuringLastOnStart: String? = null + private var uiCommandItemJob: Job? = null + private var uiCommandItemNotification: Dialog? = null + private val permissionRequestNoActionCallback = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {} @@ -1129,6 +1138,7 @@ class MainActivity : AbstractBaseActivity(), ConnectionFactory.UpdateListener { if (action != null && executeActionIfPossible(action)) { pendingAction = null } + setupUiCommandItem() } private fun executeActionIfPossible(action: PendingAction): Boolean = when { @@ -1431,6 +1441,83 @@ class MainActivity : AbstractBaseActivity(), ConnectionFactory.UpdateListener { } } + private fun setupUiCommandItem () { + uiCommandItemJob?.cancel() + uiCommandItemJob = launch { + listenUiCommandItem() + } + } + + private suspend fun listenUiCommandItem() { + val connection = connection ?: return + val eventSubscription = connection.httpClient.makeSse( + // Support for both the "openhab" and the older "smarthome" root topic by using a wildcard + // TODO: Limit to item + connection.httpClient.buildUrl("rest/events?topics=*/items/Command/command") + ) + + try { + while (isActive) { + try { + val event = JSONObject(eventSubscription.getNextEvent()) + if (event.optString("type") == "ALIVE") { + Log.d(TAG, "Got ALIVE event") + continue + } + val topic = event.getString("topic") + val topicPath = topic.split('/') + // Possible formats: + // - openhab/items//statechanged + // - openhab/items///statechanged + // When an update for a group is sent, there's also one for the individual item. + // Therefore always take the element on index two. + if (topicPath.size !in 4..5) { + throw JSONException("Unexpected topic path $topic") + } + val state = JSONObject(event.getString("payload")).getString("value") + Log.e(TAG, "Got state by event: $state") + handleUiCommand(state) + } catch (e: JSONException) { + Log.e(TAG, "Failed parsing JSON of state change event", e) + } + } + } finally { + eventSubscription.cancel() + } + } + + private fun handleUiCommand(command: String) { + val prefix = command.substringBefore(":") + val commandContent = command.removePrefix("$prefix:") + when (prefix) { + "notification" -> { + val split = commandContent.split(":") + val message = "${split.getOrNull(0).orEmpty()}\n" + + "${split.getOrNull(2).orEmpty()}\n" + + split.getOrNull(3).orEmpty() + val closeAfter = split.getOrNull(4)?.toIntOrNull() + uiCommandItemNotification?.dismiss() + uiCommandItemNotification = MaterialAlertDialogBuilder(this) + .setTitle(split.getOrNull(1).orEmpty()) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .show() + closeAfter?.let { + launch { + delay(closeAfter.milliseconds) + uiCommandItemNotification?.dismiss() + } + } + } + "close" -> uiCommandItemNotification?.dismiss() + "back" -> onBackPressedCallback.handleOnBackPressed() + "reload" -> recreate() + else -> { + Log.d(TAG, "Command not implemented: $command") + } + } + } + private fun manageHabPanelShortcut(visible: Boolean) { manageShortcut(visible, "habpanel", ACTION_HABPANEL_SELECTED, R.string.mainmenu_openhab_habpanel, R.mipmap.ic_shortcut_habpanel,