From 51ae8b9fbad825d952bfcd269306b564c03c42fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joris=20Pelgr=C3=B6m?= Date: Sat, 30 Nov 2024 19:21:58 +0100 Subject: [PATCH] Implement native Improv discovery with frontend flow (#4849) - Updates how discovered Improv devices are handled to submit info to the frontend so it can show a frontend discovery box, and the frontend can directly trigger the Connect to Wi-Fi screen of the native setup. Requires core 2024.12+ - Fix permission explanation dialog showing even if all permissions are granted - Don't start Improv scanning while the app is in the background --- .../android/improv/ui/ImprovSetupDialog.kt | 49 ++++++++++++++++ .../android/improv/ui/ImprovSheetState.kt | 4 +- .../android/improv/ui/ImprovSheetView.kt | 8 ++- .../companion/android/webview/WebView.kt | 2 - .../android/webview/WebViewActivity.kt | 57 +++++++++---------- .../android/webview/WebViewPresenterImpl.kt | 17 +++++- common/src/main/res/values/strings.xml | 2 - 7 files changed, 98 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/io/homeassistant/companion/android/improv/ui/ImprovSetupDialog.kt b/app/src/main/java/io/homeassistant/companion/android/improv/ui/ImprovSetupDialog.kt index 0d0230dc5cb..9dd2ec22665 100644 --- a/app/src/main/java/io/homeassistant/companion/android/improv/ui/ImprovSetupDialog.kt +++ b/app/src/main/java/io/homeassistant/companion/android/improv/ui/ImprovSetupDialog.kt @@ -12,11 +12,14 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.wifi.improv.DeviceState import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.common.data.wifi.WifiHelper import io.homeassistant.companion.android.improv.ImprovRepository import io.homeassistant.companion.android.util.compose.HomeAssistantAppTheme import io.homeassistant.companion.android.util.setLayoutAndExpandedByDefault +import io.homeassistant.companion.android.webview.externalbus.ExternalBusMessage +import io.homeassistant.companion.android.webview.externalbus.ExternalBusRepository import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -29,14 +32,25 @@ class ImprovSetupDialog : BottomSheetDialogFragment() { @Inject lateinit var improvRepository: ImprovRepository + @Inject + lateinit var externalBusRepository: ExternalBusRepository + @Inject lateinit var wifiHelper: WifiHelper companion object { const val TAG = "ImprovSetupDialog" + private const val ARG_NAME = "name" + const val RESULT_KEY = "ImprovSetupResult" const val RESULT_DOMAIN = "domain" + + fun newInstance(deviceName: String?): ImprovSetupDialog { + return ImprovSetupDialog().apply { + arguments = bundleOf(ARG_NAME to deviceName) + } + } } private val screenState = MutableStateFlow( @@ -48,9 +62,20 @@ class ImprovSetupDialog : BottomSheetDialogFragment() { ) ) + private var initialDeviceName: String? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launch { + if (savedInstanceState == null) { + if (arguments?.containsKey(ARG_NAME) == true) { + val name = arguments?.getString(ARG_NAME, "").takeIf { !it.isNullOrBlank() } + name?.let { + initialDeviceName = it + screenState.emit(screenState.value.copy(initialDeviceName = it)) + } + } + } repeatOnLifecycle(Lifecycle.State.RESUMED) { screenState.emit(screenState.value.copy(activeSsid = wifiHelper.getWifiSsid()?.removeSurrounding("\""))) launch { @@ -61,11 +86,23 @@ class ImprovSetupDialog : BottomSheetDialogFragment() { launch { improvRepository.getDevices().collect { screenState.emit(screenState.value.copy(devices = it)) + if (initialDeviceName != null) { + it.firstOrNull { device -> device.name == initialDeviceName } + ?.let { foundDevice -> + screenState.emit( + screenState.value.copy( + initialDeviceAddress = foundDevice.address + ) + ) + initialDeviceName = null + } + } } } launch { improvRepository.getDeviceState().collect { screenState.emit(screenState.value.copy(deviceState = it)) + if (it == DeviceState.PROVISIONED) notifyFrontend() } } launch { @@ -127,4 +164,16 @@ class ImprovSetupDialog : BottomSheetDialogFragment() { } } } + + private fun notifyFrontend() { + lifecycleScope.launch { + externalBusRepository.send( + ExternalBusMessage( + id = -1, + type = "command", + command = "improv/device_setup_done" + ) + ) + } + } } diff --git a/app/src/main/java/io/homeassistant/companion/android/improv/ui/ImprovSheetState.kt b/app/src/main/java/io/homeassistant/companion/android/improv/ui/ImprovSheetState.kt index 1f20f2a1dd9..70dfacfac59 100644 --- a/app/src/main/java/io/homeassistant/companion/android/improv/ui/ImprovSheetState.kt +++ b/app/src/main/java/io/homeassistant/companion/android/improv/ui/ImprovSheetState.kt @@ -9,7 +9,9 @@ data class ImprovSheetState( val devices: List, val deviceState: DeviceState?, val errorState: ErrorState?, - val activeSsid: String? = null + val activeSsid: String? = null, + val initialDeviceName: String? = null, + val initialDeviceAddress: String? = null ) { /** @return `true` when [errorState] is not `null` or [ErrorState.NO_ERROR] */ val hasError diff --git a/app/src/main/java/io/homeassistant/companion/android/improv/ui/ImprovSheetView.kt b/app/src/main/java/io/homeassistant/companion/android/improv/ui/ImprovSheetView.kt index 79d68a2f5cd..c20efd38ad2 100644 --- a/app/src/main/java/io/homeassistant/companion/android/improv/ui/ImprovSheetView.kt +++ b/app/src/main/java/io/homeassistant/companion/android/improv/ui/ImprovSheetView.kt @@ -59,14 +59,16 @@ fun ImprovSheetView( onRestart: () -> Unit, onDismiss: () -> Unit ) { - var selectedName by rememberSaveable { mutableStateOf(null) } - var selectedAddress by rememberSaveable { mutableStateOf(null) } + var selectedName by rememberSaveable(screenState.initialDeviceName) { mutableStateOf(screenState.initialDeviceName) } + var selectedAddress by rememberSaveable(screenState.initialDeviceAddress) { mutableStateOf(screenState.initialDeviceAddress) } var submittedWifi by rememberSaveable { mutableStateOf(false) } ModalBottomSheet( title = if (screenState.scanning && screenState.deviceState == null && !screenState.hasError) { if (selectedAddress != null) { stringResource(commonR.string.improv_wifi_title) + } else if (selectedName != null) { + "" } else { stringResource(commonR.string.improv_list_title) } @@ -142,7 +144,7 @@ fun ImprovSheetView( commonR.string.improv_device_authorized } else if (screenState.deviceState == DeviceState.PROVISIONING) { commonR.string.improv_device_provisioning - } else if (selectedAddress != null) { + } else if (selectedName != null) { commonR.string.improv_device_connecting } else { commonR.string.state_unknown diff --git a/app/src/main/java/io/homeassistant/companion/android/webview/WebView.kt b/app/src/main/java/io/homeassistant/companion/android/webview/WebView.kt index c795a31a05f..ef308d9884c 100644 --- a/app/src/main/java/io/homeassistant/companion/android/webview/WebView.kt +++ b/app/src/main/java/io/homeassistant/companion/android/webview/WebView.kt @@ -29,6 +29,4 @@ interface WebView { fun unlockAppIfNeeded() fun showError(errorType: ErrorType = ErrorType.TIMEOUT_GENERAL, error: SslError? = null, description: String? = null) - - fun showImprovAvailable() } diff --git a/app/src/main/java/io/homeassistant/companion/android/webview/WebViewActivity.kt b/app/src/main/java/io/homeassistant/companion/android/webview/WebViewActivity.kt index 9301097fd99..922c902de94 100644 --- a/app/src/main/java/io/homeassistant/companion/android/webview/WebViewActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/webview/WebViewActivity.kt @@ -71,8 +71,6 @@ import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature -import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_INDEFINITE -import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.BaseActivity import io.homeassistant.companion.android.BuildConfig @@ -234,7 +232,6 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi private var downloadFileUrl = "" private var downloadFileContentDisposition = "" private var downloadFileMimetype = "" - private var snackbar: Snackbar? = null private val javascriptInterface = "externalApp" @SuppressLint("SetJavaScriptEnabled") @@ -491,10 +488,6 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi super.doUpdateVisitedHistory(view, url, isReload) onBackPressed.isEnabled = canGoBack() presenter.stopScanningForImprov(false) - snackbar?.let { - it.dismiss() - snackbar = null - } } } @@ -830,6 +823,11 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi ) } "improv/scan" -> scanForImprov() + "improv/configure_device" -> { + val payload = if (json.has("payload")) json.getJSONObject("payload") else null + if (payload?.has("name") != true) return@post + configureImprovDevice(payload.getString("name")) + } "exoplayer/play_hls" -> exoPlayHls(json) "exoplayer/stop" -> exoStopHls() "exoplayer/resize" -> exoResizeHls(json) @@ -1680,6 +1678,7 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi private fun scanForImprov() { if (!packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) return + if (!hasWindowFocus()) return lifecycleScope.launch { if (presenter.shouldShowImprovPermissions()) { supportFragmentManager.setFragmentResultListener(ImprovPermissionDialog.RESULT_KEY, this@WebViewActivity) { _, bundle -> @@ -1696,33 +1695,29 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi } } - override fun showImprovAvailable() { - snackbar = Snackbar.make(binding.root, commonR.string.improv_hint, LENGTH_INDEFINITE) - .setAction(commonR.string.configure) { - supportFragmentManager.setFragmentResultListener(ImprovSetupDialog.RESULT_KEY, this) { _, bundle -> - if (bundle.containsKey(ImprovSetupDialog.RESULT_DOMAIN)) { - bundle.getString(ImprovSetupDialog.RESULT_DOMAIN)?.let { improvDomain -> - val url = serverManager.getServer(presenter.getActiveServer())?.let url@{ - val base = it.connection.getUrl() ?: return@url null - Uri.parse(base.toString()) - .buildUpon() - .appendEncodedPath("config/integrations/dashboard/add") - .appendQueryParameter("domain", improvDomain) - .appendQueryParameter("external_auth", "1") - .build() - .toString() - } - if (url != null) { - loadUrl(url = url, keepHistory = true, openInApp = true) - } - } - supportFragmentManager.clearFragmentResultListener(ImprovSetupDialog.RESULT_KEY) + private fun configureImprovDevice(deviceName: String) { + supportFragmentManager.setFragmentResultListener(ImprovSetupDialog.RESULT_KEY, this) { _, bundle -> + if (bundle.containsKey(ImprovSetupDialog.RESULT_DOMAIN)) { + bundle.getString(ImprovSetupDialog.RESULT_DOMAIN)?.let { improvDomain -> + val url = serverManager.getServer(presenter.getActiveServer())?.let url@{ + val base = it.connection.getUrl() ?: return@url null + Uri.parse(base.toString()) + .buildUpon() + .appendEncodedPath("config/integrations/dashboard/add") + .appendQueryParameter("domain", improvDomain) + .appendQueryParameter("external_auth", "1") + .build() + .toString() + } + if (url != null) { + loadUrl(url = url, keepHistory = true, openInApp = true) } } - val dialog = ImprovSetupDialog() - dialog.show(supportFragmentManager, ImprovSetupDialog.TAG) + supportFragmentManager.clearFragmentResultListener(ImprovSetupDialog.RESULT_KEY) } - snackbar?.show() + } + val dialog = ImprovSetupDialog.newInstance(deviceName) + dialog.show(supportFragmentManager, ImprovSetupDialog.TAG) } override fun onNewIntent(intent: Intent) { diff --git a/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt b/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt index 2f6c3281f8a..359c4b2b5b5 100644 --- a/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt +++ b/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt @@ -18,6 +18,7 @@ import io.homeassistant.companion.android.matter.MatterManager import io.homeassistant.companion.android.thread.ThreadManager import io.homeassistant.companion.android.util.UrlUtil import io.homeassistant.companion.android.util.UrlUtil.baseIsEqual +import io.homeassistant.companion.android.webview.externalbus.ExternalBusMessage import io.homeassistant.companion.android.webview.externalbus.ExternalBusRepository import java.net.SocketTimeoutException import java.net.URL @@ -482,7 +483,7 @@ class WebViewPresenterImpl @Inject constructor( override suspend fun shouldShowImprovPermissions(): Boolean { return if (improvRepository.hasPermission(view as Context)) { - true + false } else { prefsRepository.getImprovPermissionDisplayedCount() < 2 } @@ -496,7 +497,19 @@ class WebViewPresenterImpl @Inject constructor( improvRepository.startScanning(view as Context) } improvRepository.getDevices().collect { - if (it.any()) view.showImprovAvailable() + it.forEach { device -> + val name = device.name ?: return@forEach + externalBusRepository.send( + ExternalBusMessage( + id = -1, + type = "command", + command = "improv/discovered_device", + payload = mapOf( + "name" to name + ) + ) + ) + } } } return true diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 16e9ada780a..0457e021125 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -153,7 +153,6 @@ Entity state Preview Configuration - Configure Configure action Widget label Are you sure? This cannot be undone @@ -333,7 +332,6 @@ Unable to connect Unknown command Unknown error, please try again - Improv Wi-Fi devices are available to set up Devices ready to set up Search devices The Home Assistant app can find devices using Bluetooth of this device. Allow access for the following permissions.