Skip to content

Commit

Permalink
Implement native Improv discovery with frontend flow (#4849)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
jpelgrom authored Nov 30, 2024
1 parent 60541a9 commit 51ae8b9
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -127,4 +164,16 @@ class ImprovSetupDialog : BottomSheetDialogFragment() {
}
}
}

private fun notifyFrontend() {
lifecycleScope.launch {
externalBusRepository.send(
ExternalBusMessage(
id = -1,
type = "command",
command = "improv/device_setup_done"
)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ data class ImprovSheetState(
val devices: List<ImprovDevice>,
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,16 @@ fun ImprovSheetView(
onRestart: () -> Unit,
onDismiss: () -> Unit
) {
var selectedName by rememberSaveable { mutableStateOf<String?>(null) }
var selectedAddress by rememberSaveable { mutableStateOf<String?>(null) }
var selectedName by rememberSaveable(screenState.initialDeviceName) { mutableStateOf<String?>(screenState.initialDeviceName) }
var selectedAddress by rememberSaveable(screenState.initialDeviceAddress) { mutableStateOf<String?>(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)
}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,4 @@ interface WebView {
fun unlockAppIfNeeded()

fun showError(errorType: ErrorType = ErrorType.TIMEOUT_GENERAL, error: SslError? = null, description: String? = null)

fun showImprovAvailable()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
}
}
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 ->
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down
2 changes: 0 additions & 2 deletions common/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@
<string name="complication_entity_state_label">Entity state</string>
<string name="complication_entity_state_preview">Preview</string>
<string name="config">Configuration</string>
<string name="configure">Configure</string>
<string name="configure_action">Configure action</string>
<string name="configure_widget_label">Widget label</string>
<string name="confirm_delete_all_notification_message">Are you sure? This cannot be undone</string>
Expand Down Expand Up @@ -333,7 +332,6 @@
<string name="improv_error_unable_to_connect">Unable to connect</string>
<string name="improv_error_unknown_command">Unknown command</string>
<string name="improv_error_unknown">Unknown error, please try again</string>
<string name="improv_hint">Improv Wi-Fi devices are available to set up</string>
<string name="improv_list_title">Devices ready to set up</string>
<string name="improv_permission_title">Search devices</string>
<string name="improv_permission_text">The Home Assistant app can find devices using Bluetooth of this device. Allow access for the following permissions.</string>
Expand Down

0 comments on commit 51ae8b9

Please sign in to comment.