Skip to content
Open
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 @@ -44,6 +44,11 @@ private fun LazyListScope.textStyles() {
style = HATextStyle.Headline,
modifier = Modifier.fillMaxWidth(),
)
Text(
text = "HeadlineMedium",
style = HATextStyle.HeadlineMedium,
modifier = Modifier.fillMaxWidth(),
)
Text(
text = "Body",
style = HATextStyle.Body,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ private fun OneServerFound(
) {
Text(
text = serverDiscovered.name,
style = HATextStyle.Headline,
style = HATextStyle.HeadlineMedium,
)
Icon(
imageVector = Icons.Default.Storage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.DpSize
Expand Down Expand Up @@ -259,6 +260,13 @@ class WebViewActivity :
private var playerLeft = mutableStateOf(0.dp)
private var statusBarColor = mutableStateOf<Color?>(null)
private var backgroundColor = mutableStateOf<Color?>(null)

/**
* Flag to know when the webview has been fully initialized (loadUrl called).
* It is important to know to avoid opening a full screen dialog that
* could prevent the loading of the webview.
*/
private var webViewInitialized = mutableStateOf(false)
private var failedConnection = "external"
private var clearHistory = false
private var moreInfoEntity = ""
Expand Down Expand Up @@ -309,6 +317,7 @@ class WebViewActivity :
}

setContent {
val coroutineScope = rememberCoroutineScope()
val player by remember { exoPlayer }
val playerSize by remember { playerSize }
val playerTop by remember { playerTop }
Expand All @@ -319,9 +328,12 @@ class WebViewActivity :
val backgroundColor by remember { backgroundColor }
var nightModeTheme by remember { mutableStateOf<NightModeTheme?>(null) }
val snackbarHostState = remember { snackbarHostState }
var webViewInitialized by remember { webViewInitialized }
var shouldAskNotificationPermission by remember { mutableStateOf(false) }

LaunchedEffect(Unit) {
nightModeTheme = nightModeManager.getCurrentNightMode()
shouldAskNotificationPermission = presenter.shouldAskNotificationPermission()
}

WebViewContentScreen(
Expand All @@ -333,13 +345,22 @@ class WebViewActivity :
playerLeft = playerLeft,
currentAppLocked,
customViewFromWebView,
shouldAskNotificationPermission = shouldAskNotificationPermission,
webViewInitialized = webViewInitialized,
nightModeTheme = nightModeTheme,
statusBarColor = statusBarColor,
backgroundColor = backgroundColor,
) { isFullScreen ->
isExoFullScreen = isFullScreen
if (isFullScreen) hideSystemUI() else showSystemUI()
}
onFullscreenClicked = { isFullScreen ->
isExoFullScreen = isFullScreen
if (isFullScreen) hideSystemUI() else showSystemUI()
},
onDiscardNotificationPermission = {
coroutineScope.launch {
presenter.discardNotificationPermission()
shouldAskNotificationPermission = false
}
},
)
}

authenticator = Authenticator(this, this, ::authenticationResult)
Expand Down Expand Up @@ -401,6 +422,7 @@ class WebViewActivity :
}

override fun onPageFinished(view: WebView?, url: String?) {
webViewInitialized.value = true
if (clearHistory) {
webView.clearHistory()
clearHistory = false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package io.homeassistant.companion.android.webview

import android.Manifest
import android.annotation.SuppressLint
import android.os.Build
import android.view.View
import android.webkit.WebView
import androidx.annotation.RequiresApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
Expand All @@ -22,29 +26,48 @@ import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.rememberStandardBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.Player
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.rememberPermissionState
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import io.homeassistant.companion.android.common.R as commonR
import io.homeassistant.companion.android.common.compose.composable.HAAccentButton
import io.homeassistant.companion.android.common.compose.composable.HAModalBottomSheet
import io.homeassistant.companion.android.common.compose.composable.HAPlainButton
import io.homeassistant.companion.android.common.compose.theme.HADimens
import io.homeassistant.companion.android.common.compose.theme.HATextStyle
import io.homeassistant.companion.android.common.compose.theme.HATheme
import io.homeassistant.companion.android.common.data.prefs.NightModeTheme
import io.homeassistant.companion.android.util.compose.HomeAssistantAppTheme
import io.homeassistant.companion.android.util.compose.media.player.HAMediaPlayer
import io.homeassistant.companion.android.util.compose.webview.HAWebView
import kotlinx.coroutines.launch

@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalHazeMaterialsApi::class)
Expand All @@ -58,18 +81,21 @@ internal fun WebViewContentScreen(
playerLeft: Dp,
currentAppLocked: Boolean,
customViewFromWebView: View?,
shouldAskNotificationPermission: Boolean,
webViewInitialized: Boolean,
onFullscreenClicked: (isFullscreen: Boolean) -> Unit,
onDiscardNotificationPermission: () -> Unit,
nightModeTheme: NightModeTheme? = null,
statusBarColor: Color? = null,
backgroundColor: Color? = null,
onFullscreenClicked: (isFullscreen: Boolean) -> Unit,
supportsNotificationPermission: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU,
) {
HomeAssistantAppTheme {
HATheme {
Scaffold(
snackbarHost = {
SnackbarHost(
snackbarHostState,
modifier =
Modifier.windowInsetsPadding(WindowInsets.safeDrawing),
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing),
)
},
// Delegate the insets handling to the webview
Expand Down Expand Up @@ -107,6 +133,10 @@ internal fun WebViewContentScreen(
}
}
}
if (webViewInitialized && shouldAskNotificationPermission && supportsNotificationPermission) {
@SuppressLint("InlinedApi")
NotificationPermission(onDiscardNotificationPermission)
}
}
}

Expand Down Expand Up @@ -165,8 +195,90 @@ private fun SafeHAWebView(
}
}

@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
@Composable
private fun Color.Overlay(modifier: Modifier) {
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private fun NotificationPermission(onDiscardNotificationPermission: () -> Unit) {
val bottomSheetState = rememberStandardBottomSheetState(skipHiddenState = false)
val coroutineScope = rememberCoroutineScope()

// Track whether the bottom sheet has been dismissed to completely remove it from composition.
// This is necessary because Material 3's ModalBottomSheet creates a Dialog window that,
// even when hidden, can block touch events to underlying views like the WebView.
// By removing the composable entirely when dismissed (checking !isClosed), we ensure
// the Dialog window is destroyed and the WebView remains fully interactive.
var isClosed by remember { mutableStateOf(false) }

fun closeSheet() {
coroutineScope.launch {
bottomSheetState.hide()
isClosed = true
}
}

// By default on lower API the bottom sheet won't be displayed
val notificationPermission = rememberPermissionState(
permission = Manifest.permission.POST_NOTIFICATIONS,
previewPermissionStatus = PermissionStatus.Denied(true),
onPermissionResult = {
closeSheet()
},
)

if (notificationPermission.status is PermissionStatus.Denied && !isClosed) {
HAModalBottomSheet(
bottomSheetState = bottomSheetState,
onDismissRequest = {
closeSheet()
},
) {
Column(
modifier = Modifier
.padding(horizontal = HADimens.SPACE6)
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom))
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(HADimens.SPACE6),
) {
Text(
text = stringResource(commonR.string.notification_permission_dialog_title),
style = HATextStyle.HeadlineMedium,
)
Text(
text = stringResource(commonR.string.notification_permission_dialog_content),
style = HATextStyle.Body,
)
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(HADimens.SPACE4),
) {
HAAccentButton(
text = stringResource(commonR.string.notification_permission_dialog_allow),
onClick = {
notificationPermission.launchPermissionRequest()
},
modifier = Modifier.fillMaxWidth(),
)

HAPlainButton(
text = stringResource(commonR.string.notification_permission_dialog_deny),
onClick = {
onDiscardNotificationPermission()
closeSheet()
},
modifier = Modifier
.fillMaxWidth()
.padding(bottom = HADimens.SPACE6),
)
}
}
}
}
}

@Composable
private fun Color.Overlay(modifier: Modifier = Modifier) {
Spacer(
modifier = modifier
.background(this),
Expand All @@ -184,6 +296,10 @@ private fun WebViewContentScreenPreview() {
playerTop = 0.dp,
playerLeft = 0.dp,
currentAppLocked = false,
shouldAskNotificationPermission = false,
webViewInitialized = true,
customViewFromWebView = null,
) { }
onFullscreenClicked = {},
onDiscardNotificationPermission = {},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,7 @@ interface WebViewPresenter {
/** @return `true` if the app tried starting scanning or `false` if it was missing permissions */
fun startScanningForImprov(): Boolean
fun stopScanningForImprov(force: Boolean)

suspend fun discardNotificationPermission()
suspend fun shouldAskNotificationPermission(): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -581,4 +581,12 @@ class WebViewPresenterImpl @Inject constructor(
improvJob?.cancel()
}
}

override suspend fun discardNotificationPermission() {
prefsRepository.setAskNotificationPermission(false)
}

override suspend fun shouldAskNotificationPermission(): Boolean {
return prefsRepository.shouldAskNotificationPermission()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ class WebViewContentScreenScreenshotTest {
playerTop = 0.dp,
playerLeft = 0.dp,
currentAppLocked = false,
shouldAskNotificationPermission = false,
webViewInitialized = true,
customViewFromWebView = null,
) { }
onFullscreenClicked = {},
onDiscardNotificationPermission = {},
)
}

@PreviewTest
Expand All @@ -38,8 +42,12 @@ class WebViewContentScreenScreenshotTest {
playerTop = 0.dp,
playerLeft = 0.dp,
currentAppLocked = true,
shouldAskNotificationPermission = false,
webViewInitialized = true,
customViewFromWebView = null,
) { }
onFullscreenClicked = {},
onDiscardNotificationPermission = {},
)
}

@PreviewTest
Expand All @@ -54,7 +62,32 @@ class WebViewContentScreenScreenshotTest {
playerTop = 50.dp,
playerLeft = 10.dp,
currentAppLocked = false,
shouldAskNotificationPermission = false,
webViewInitialized = true,
customViewFromWebView = null,
) { }
onFullscreenClicked = {},
onDiscardNotificationPermission = {},
)
}

@PreviewTest
@Preview
@Composable
fun `WebView request notification permission`() {
WebViewContentScreen(
webView = null,
player = null,
snackbarHostState = SnackbarHostState(),
playerSize = null,
playerTop = 0.dp,
playerLeft = 0.dp,
currentAppLocked = false,
shouldAskNotificationPermission = true,
webViewInitialized = true,
customViewFromWebView = null,
onFullscreenClicked = {},
onDiscardNotificationPermission = {},
supportsNotificationPermission = true,
)
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design feedback:

  • Very large title (we already have other bottom sheets like the server chooser which consistently use a smaller title, which I think also matches M3 guidelines and is closer to this iOS screenshot)
  • The padding between the buttons looks really large compared to the padding between the main text and first button

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've fixed the padding to look like the other onboarding screens. For the large title I think it's something for another task. Since it's the same as the bottom sheet discovery title.

I need to take the time to make proper typography objects. I've been discussing this with the design team some time ago, but we didn't reach yet a conclusion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the large title I think it's something for another task. Since it's the same as the bottom sheet discovery title.

It didn't stand out as much to me with the onboarding discovery but you're correct. Still, the other style is more common and on a quick glance also more like what iOS is doing for these sheets. If there are not yet "proper" typography objects it isn't too hard to introduce another temporary one which fits in better with smaller UI components like these?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added HeadlineMedium

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think it's quite big but it's an improvement :) Did you get this style from somewhere or did you come up with it yourself?

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading