@@ -45,7 +45,10 @@ import androidx.compose.ui.res.stringResource
4545import androidx.compose.ui.unit.LayoutDirection
4646import androidx.compose.ui.viewinterop.AndroidView
4747import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
48+ import androidx.lifecycle.Lifecycle
49+ import androidx.lifecycle.compose.LocalLifecycleOwner
4850import androidx.lifecycle.compose.collectAsStateWithLifecycle
51+ import androidx.lifecycle.repeatOnLifecycle
4952import io.homeassistant.companion.android.common.R as commonR
5053import io.homeassistant.companion.android.common.compose.composable.HAAccentButton
5154import io.homeassistant.companion.android.common.compose.composable.HAPlainButton
@@ -60,12 +63,11 @@ import io.homeassistant.companion.android.frontend.error.FrontendConnectionError
6063import io.homeassistant.companion.android.frontend.error.FrontendConnectionErrorStateProvider
6164import io.homeassistant.companion.android.frontend.filechooser.FileChooserEffect
6265import io.homeassistant.companion.android.frontend.filechooser.FileChooserRequest
66+ import io.homeassistant.companion.android.frontend.improv.ui.ImprovOverlay
6367import io.homeassistant.companion.android.frontend.js.FrontendJsBridge
6468import io.homeassistant.companion.android.frontend.js.FrontendJsCallback
65- import io.homeassistant.companion.android.frontend.permissions.MultiplePermissionsEffect
66- import io.homeassistant.companion.android.frontend.permissions.NotificationPermissionPrompt
69+ import io.homeassistant.companion.android.frontend.permissions.PendingPermissionHandler
6770import io.homeassistant.companion.android.frontend.permissions.PermissionRequest
68- import io.homeassistant.companion.android.frontend.permissions.SinglePermissionEffect
6971import io.homeassistant.companion.android.launch.PipReadiness
7072import io.homeassistant.companion.android.loading.LoadingScreen
7173import io.homeassistant.companion.android.onboarding.locationforsecureconnection.LocationForSecureConnectionScreen
@@ -124,6 +126,7 @@ internal fun FrontendScreen(
124126 val pendingDialog by viewModel.pendingDialog.collectAsStateWithLifecycle()
125127 val pendingFileChooser by viewModel.pendingFileChooser.collectAsStateWithLifecycle()
126128 val autoPlayVideoEnabled by viewModel.autoPlayVideoEnabled.collectAsStateWithLifecycle()
129+ val improvScanRequested by viewModel.improvScanRequested.collectAsStateWithLifecycle()
127130
128131 // The fullscreen View handed over by the WebView is Activity-scoped. Keep it in screen
129132 // state so it does not leak across configuration changes via the ViewModel.
@@ -172,8 +175,13 @@ internal fun FrontendScreen(
172175 webViewActions = viewModel.webViewActions,
173176 onGesture = viewModel::onGesture,
174177 onExoPlayerFullscreenChanged = viewModel::onExoPlayerFullscreenChanged,
178+ onImprovConnectDevice = viewModel::onImprovConnectDevice,
179+ onImprovRestart = viewModel::onImprovRestart,
180+ onImprovDismiss = viewModel::onImprovSheetDismissed,
175181 autoPlayVideoEnabled = autoPlayVideoEnabled,
176182 onPipReadinessChanged = onPipReadinessChanged,
183+ improvScanRequested = improvScanRequested,
184+ processImprovScanRequests = viewModel::processImprovScanRequests,
177185 modifier = modifier,
178186 )
179187}
@@ -209,28 +217,26 @@ internal fun FrontendScreenContent(
209217 onGesture : (GestureDirection , Int ) -> Unit = { _, _ -> },
210218 onExoPlayerFullscreenChanged : (Boolean ) -> Unit = {},
211219 onPipReadinessChanged : (PipReadiness ? ) -> Unit = {},
220+ onImprovConnectDevice : (ssid: String , password: String ) -> Unit = { _, _ -> },
221+ onImprovRestart : () -> Unit = {},
222+ onImprovDismiss : () -> Unit = {},
223+ improvScanRequested : Boolean = false,
224+ processImprovScanRequests : suspend () -> Unit = {},
212225) {
213226 var webView by remember { mutableStateOf<WebView ?>(null ) }
214227
215- WebViewEffects (
228+ FrontendScreenEffects (
216229 webView = webView,
217230 url = viewState.url,
218231 frontendJsCallback = frontendJsCallback,
219232 webViewActions = webViewActions,
233+ pendingFileChooser = pendingFileChooser,
220234 autoPlayVideoEnabled = autoPlayVideoEnabled,
235+ improvScanRequested = improvScanRequested,
236+ processImprovScanRequests = processImprovScanRequests,
221237 )
222238
223- PendingPermissionHandler (
224- pendingRequest = pendingPermissionRequest,
225- )
226-
227- PendingDialogHandler (
228- pendingDialog = pendingDialog,
229- )
230-
231- FileChooserEffect (
232- pendingRequest = pendingFileChooser,
233- )
239+ FrontendScreenHandlers (pendingPermissionRequest = pendingPermissionRequest, pendingDialog = pendingDialog)
234240
235241 Box (modifier = modifier.fillMaxSize()) {
236242 // Always render WebView at base layer
@@ -253,6 +259,13 @@ internal fun FrontendScreenContent(
253259 onPipReadinessChanged = onPipReadinessChanged,
254260 )
255261
262+ ImprovOverlay (
263+ state = (viewState as ? FrontendViewState .Content )?.improvUiState,
264+ onConnectDevice = onImprovConnectDevice,
265+ onRestart = onImprovRestart,
266+ onDismiss = onImprovDismiss,
267+ )
268+
256269 StateOverlay (
257270 viewState = viewState,
258271 errorStateProvider = errorStateProvider,
@@ -271,6 +284,46 @@ internal fun FrontendScreenContent(
271284 }
272285}
273286
287+ @Composable
288+ private fun FrontendScreenHandlers (pendingPermissionRequest : PermissionRequest ? , pendingDialog : FrontendDialog ? ) {
289+ PendingPermissionHandler (
290+ pendingRequest = pendingPermissionRequest,
291+ )
292+
293+ PendingDialogHandler (
294+ pendingDialog = pendingDialog,
295+ )
296+ }
297+
298+ @Composable
299+ private fun FrontendScreenEffects (
300+ webView : WebView ? ,
301+ url : String ,
302+ frontendJsCallback : FrontendJsCallback ,
303+ webViewActions : Flow <WebViewAction >,
304+ pendingFileChooser : FileChooserRequest ? ,
305+ autoPlayVideoEnabled : Boolean ,
306+ improvScanRequested : Boolean ,
307+ processImprovScanRequests : suspend () -> Unit ,
308+ ) {
309+ ImprovScanLifecycleEffect (
310+ scanRequested = improvScanRequested,
311+ processImprovScanRequests = processImprovScanRequests,
312+ )
313+
314+ WebViewEffects (
315+ webView = webView,
316+ url = url,
317+ frontendJsCallback = frontendJsCallback,
318+ webViewActions = webViewActions,
319+ autoPlayVideoEnabled = autoPlayVideoEnabled,
320+ )
321+
322+ FileChooserEffect (
323+ pendingRequest = pendingFileChooser,
324+ )
325+ }
326+
274327/* *
275328 * Renders the appropriate overlay based on the current view state.
276329 */
@@ -566,48 +619,6 @@ private fun WebView.configureForFrontend(
566619 )
567620}
568621
569- /* *
570- * Routes a [PermissionRequest] to the appropriate UI and delivers the result back through the
571- * request's own callback. The slot is freed automatically by the manager once the callback is
572- * invoked, so this composable doesn't have to clear anything itself.
573- *
574- * Types with custom UI (e.g. [PermissionRequest.Notification] bottom sheet) are matched first.
575- * Remaining types fall through to the system dialog based on their category:
576- * [PermissionRequest.MultiplePermissions] or [PermissionRequest.SinglePermission].
577- *
578- * Adding a new permission type that uses the system dialog requires no changes here.
579- */
580- @Composable
581- private fun PendingPermissionHandler (pendingRequest : PermissionRequest ? ) {
582- when (pendingRequest) {
583- is PermissionRequest .Notification -> {
584- @SuppressLint(" InlinedApi" )
585- NotificationPermissionPrompt (
586- onPermissionResult = pendingRequest.onResult,
587- onDismiss = pendingRequest.onDismiss,
588- )
589- }
590-
591- is PermissionRequest .MultiplePermissions -> {
592- MultiplePermissionsEffect (
593- pendingRequest = pendingRequest,
594- onPermissionResult = pendingRequest.onResult,
595- )
596- }
597-
598- is PermissionRequest .SinglePermission -> {
599- SinglePermissionEffect (
600- pendingRequest = pendingRequest,
601- onPermissionResult = pendingRequest.onResult,
602- )
603- }
604-
605- null -> {
606- /* No pending permission */
607- }
608- }
609- }
610-
611622/* *
612623 * Handles WebView side effects: URL loading, [WebViewAction] dispatch, and reapplying the
613624 * "Autoplay video" preference (which requires a [WebView.reload] to take effect on the loaded page).
@@ -645,6 +656,18 @@ private fun WebViewEffects(
645656 }
646657}
647658
659+ @Composable
660+ private fun ImprovScanLifecycleEffect (scanRequested : Boolean , processImprovScanRequests : suspend () -> Unit ) {
661+ if (scanRequested) {
662+ val lifecycle = LocalLifecycleOwner .current.lifecycle
663+ LaunchedEffect (lifecycle) {
664+ lifecycle.repeatOnLifecycle(Lifecycle .State .RESUMED ) {
665+ processImprovScanRequests()
666+ }
667+ }
668+ }
669+ }
670+
648671/* *
649672 * Renders PiP-eligible overlays and reports their combined [PipReadiness] to the host.
650673 */
0 commit comments