Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement native Improv discovery with frontend flow #4849

Merged
merged 1 commit into from
Nov 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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