diff --git a/app/src/debug/kotlin/io/homeassistant/companion/android/developer/catalog/HATextAndBannersScreen.kt b/app/src/debug/kotlin/io/homeassistant/companion/android/developer/catalog/HATextAndBannersScreen.kt index 97c7e494aac..7695ff47bf2 100644 --- a/app/src/debug/kotlin/io/homeassistant/companion/android/developer/catalog/HATextAndBannersScreen.kt +++ b/app/src/debug/kotlin/io/homeassistant/companion/android/developer/catalog/HATextAndBannersScreen.kt @@ -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, diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreen.kt index b95f3d48993..39e6debbcfa 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreen.kt @@ -185,7 +185,7 @@ private fun OneServerFound( ) { Text( text = serverDiscovered.name, - style = HATextStyle.Headline, + style = HATextStyle.HeadlineMedium, ) Icon( imageVector = Icons.Default.Storage, diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt b/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt index a0939b53e33..26b50b7e3ba 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt @@ -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 @@ -259,6 +260,13 @@ class WebViewActivity : private var playerLeft = mutableStateOf(0.dp) private var statusBarColor = mutableStateOf(null) private var backgroundColor = mutableStateOf(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 = "" @@ -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 } @@ -319,9 +328,12 @@ class WebViewActivity : val backgroundColor by remember { backgroundColor } var nightModeTheme by remember { mutableStateOf(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( @@ -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) @@ -401,6 +422,7 @@ class WebViewActivity : } override fun onPageFinished(view: WebView?, url: String?) { + webViewInitialized.value = true if (clearHistory) { webView.clearHistory() clearHistory = false diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewContentScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewContentScreen.kt index 083a3a773ba..dd26ec03ac6 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewContentScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewContentScreen.kt @@ -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 @@ -22,14 +26,24 @@ 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 @@ -37,14 +51,23 @@ 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) @@ -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 @@ -107,6 +133,10 @@ internal fun WebViewContentScreen( } } } + if (webViewInitialized && shouldAskNotificationPermission && supportsNotificationPermission) { + @SuppressLint("InlinedApi") + NotificationPermission(onDiscardNotificationPermission) + } } } @@ -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), @@ -184,6 +296,10 @@ private fun WebViewContentScreenPreview() { playerTop = 0.dp, playerLeft = 0.dp, currentAppLocked = false, + shouldAskNotificationPermission = false, + webViewInitialized = true, customViewFromWebView = null, - ) { } + onFullscreenClicked = {}, + onDiscardNotificationPermission = {}, + ) } diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewPresenter.kt b/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewPresenter.kt index 313cf934f87..069d01a415e 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewPresenter.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewPresenter.kt @@ -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 } diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt b/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt index 5d3bd0bba49..9dfefc5ece0 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt @@ -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() + } } diff --git a/app/src/screenshotTest/kotlin/io/homeassistant/companion/android/webview/WebViewContentScreenScreenshotTest.kt b/app/src/screenshotTest/kotlin/io/homeassistant/companion/android/webview/WebViewContentScreenScreenshotTest.kt index 951798a2216..55437dcb459 100644 --- a/app/src/screenshotTest/kotlin/io/homeassistant/companion/android/webview/WebViewContentScreenScreenshotTest.kt +++ b/app/src/screenshotTest/kotlin/io/homeassistant/companion/android/webview/WebViewContentScreenScreenshotTest.kt @@ -22,8 +22,12 @@ class WebViewContentScreenScreenshotTest { playerTop = 0.dp, playerLeft = 0.dp, currentAppLocked = false, + shouldAskNotificationPermission = false, + webViewInitialized = true, customViewFromWebView = null, - ) { } + onFullscreenClicked = {}, + onDiscardNotificationPermission = {}, + ) } @PreviewTest @@ -38,8 +42,12 @@ class WebViewContentScreenScreenshotTest { playerTop = 0.dp, playerLeft = 0.dp, currentAppLocked = true, + shouldAskNotificationPermission = false, + webViewInitialized = true, customViewFromWebView = null, - ) { } + onFullscreenClicked = {}, + onDiscardNotificationPermission = {}, + ) } @PreviewTest @@ -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, + ) } } diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/developer/HAComposeCatalogScreenshotTest/HATextAndBannersScreen_Dark_adedabf3_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/developer/HAComposeCatalogScreenshotTest/HATextAndBannersScreen_Dark_adedabf3_0.png index 6c5affefc93..9b0c92f5d07 100644 Binary files a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/developer/HAComposeCatalogScreenshotTest/HATextAndBannersScreen_Dark_adedabf3_0.png and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/developer/HAComposeCatalogScreenshotTest/HATextAndBannersScreen_Dark_adedabf3_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/developer/HAComposeCatalogScreenshotTest/HATextAndBannersScreen_Light_fc36fde7_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/developer/HAComposeCatalogScreenshotTest/HATextAndBannersScreen_Light_fc36fde7_0.png index 367265452d5..757a775c4be 100644 Binary files a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/developer/HAComposeCatalogScreenshotTest/HATextAndBannersScreen_Light_fc36fde7_0.png and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/developer/HAComposeCatalogScreenshotTest/HATextAndBannersScreen_Light_fc36fde7_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_foldable_c908f502_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_foldable_c908f502_0.png index c8495b6b7b4..4edc491e892 100644 Binary files a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_foldable_c908f502_0.png and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_foldable_c908f502_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_phone_e05166be_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_phone_e05166be_0.png index 2c6ea40c248..926502365f8 100644 Binary files a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_phone_e05166be_0.png and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_phone_e05166be_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_phone_landscape_9e00b29d_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_phone_landscape_9e00b29d_0.png index 9256bae63c6..db1e626dc4a 100644 Binary files a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_phone_landscape_9e00b29d_0.png and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_phone_landscape_9e00b29d_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_small_phone_66e7bbf2_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_small_phone_66e7bbf2_0.png index 48ce69152f0..5ede165c485 100644 Binary files a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_small_phone_66e7bbf2_0.png and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_small_phone_66e7bbf2_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_tablet_2f22c4ea_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_tablet_2f22c4ea_0.png index 64befac6baf..ec4e1fc55ab 100644 Binary files a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_tablet_2f22c4ea_0.png and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_tablet_2f22c4ea_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_tablet_landscape_62cae397_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_tablet_landscape_62cae397_0.png index 420385f13d7..d8f7b75c61c 100644 Binary files a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_tablet_landscape_62cae397_0.png and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreenshotTest/ServerDiscoveryScreen with one server found_tablet_landscape_62cae397_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/webview/WebViewContentScreenScreenshotTest/WebView request notification permission_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/webview/WebViewContentScreenScreenshotTest/WebView request notification permission_0.png new file mode 100644 index 00000000000..d3f5c84e8ca Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/webview/WebViewContentScreenScreenshotTest/WebView request notification permission_0.png differ diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/compose/theme/HATextStyle.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/compose/theme/HATextStyle.kt index ed37373b4c4..0f32d2b99e5 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/compose/theme/HATextStyle.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/compose/theme/HATextStyle.kt @@ -12,6 +12,7 @@ import io.homeassistant.companion.android.common.compose.theme.HATextStyle.Body import io.homeassistant.companion.android.common.compose.theme.HATextStyle.BodyMedium import io.homeassistant.companion.android.common.compose.theme.HATextStyle.Button import io.homeassistant.companion.android.common.compose.theme.HATextStyle.Headline +import io.homeassistant.companion.android.common.compose.theme.HATextStyle.HeadlineMedium import io.homeassistant.companion.android.common.compose.theme.HATextStyle.UserInput /** @@ -20,6 +21,7 @@ import io.homeassistant.companion.android.common.compose.theme.HATextStyle.UserI * * Available text styles: * - [Headline]: Used for main titles and headings. + * - [HeadlineMedium]: Used for sub titles and headings. * - [Body]: Default text style for body content. * - [BodyMedium]: A variation of the Body style with medium font size. * - [UserInput]: Text style for user input fields. @@ -38,6 +40,17 @@ object HATextStyle { color = LocalHAColorScheme.current.colorTextPrimary, ) + val HeadlineMedium + @Composable + get() = TextStyle( + fontStyle = FontStyle.Normal, + fontSize = HAFontSize.X3L, + lineHeight = HAFontSize.X5L, + fontWeight = FontWeight.W500, + textAlign = TextAlign.Center, + color = LocalHAColorScheme.current.colorTextPrimary, + ) + val Body @Composable get() = TextStyle( diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/prefs/PrefsRepository.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/prefs/PrefsRepository.kt index e271d576201..eac313b5f3b 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/prefs/PrefsRepository.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/prefs/PrefsRepository.kt @@ -132,4 +132,8 @@ interface PrefsRepository { suspend fun showPrivacyHint(): Boolean suspend fun setShowPrivacyHint(showPrivacyHint: Boolean) + + suspend fun setAskNotificationPermission(shouldAsk: Boolean) + + suspend fun shouldAskNotificationPermission(): Boolean } diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/prefs/PrefsRepositoryImpl.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/prefs/PrefsRepositoryImpl.kt index 91dc1733056..9b6427d488f 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/prefs/PrefsRepositoryImpl.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/prefs/PrefsRepositoryImpl.kt @@ -44,6 +44,7 @@ private const val PREF_IMPROV_PERMISSION_DISPLAYED = "improv_permission_displaye private const val PREF_GESTURE_ACTION_PREFIX = "gesture_action" private const val PREF_CHANGE_LOG_POPUP_ENABLED = "change_log_popup_enabled" private const val PREF_SHOW_PRIVACY_HINT = "show_privacy_hint" +private const val PREF_ASK_NOTIFICATION_PERMISSION = "ask_notification_permission" /** * This class ensure that when we use the local storage in [PrefsRepositoryImpl] the migrations has been made @@ -363,4 +364,12 @@ class PrefsRepositoryImpl @Inject constructor( override suspend fun setShowPrivacyHint(showPrivacyHint: Boolean) { localStorage().putBoolean(PREF_SHOW_PRIVACY_HINT, showPrivacyHint) } + + override suspend fun setAskNotificationPermission(shouldAsk: Boolean) { + localStorage().putBoolean(PREF_ASK_NOTIFICATION_PERMISSION, shouldAsk) + } + + override suspend fun shouldAskNotificationPermission(): Boolean { + return localStorage().getBooleanOrNull(PREF_ASK_NOTIFICATION_PERMISSION) ?: true + } } diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 89590059a08..8b1697a294a 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1493,4 +1493,8 @@ Home Assistant GitHub There was an error loading Home Assistant. Please review the connection settings and try again. Back + Allow notifications? + Enable notifications and get what\'s happening in your home, from detecting leaks to doors left open. You have full control over the notifications you receive and what they tell you. + Allow notifications + Do not allow diff --git a/common/src/test/kotlin/io/homeassistant/companion/android/common/data/prefs/PrefsRepositoryImplTest.kt b/common/src/test/kotlin/io/homeassistant/companion/android/common/data/prefs/PrefsRepositoryImplTest.kt index 43085f10b5b..6be0c1e94d6 100644 --- a/common/src/test/kotlin/io/homeassistant/companion/android/common/data/prefs/PrefsRepositoryImplTest.kt +++ b/common/src/test/kotlin/io/homeassistant/companion/android/common/data/prefs/PrefsRepositoryImplTest.kt @@ -118,4 +118,37 @@ class PrefsRepositoryImplTest { // Verify no integration storage was accessed since no migration was needed coVerify(exactly = 0) { integrationStorage.getString(any()) } } + + @Test + fun `Given no preference set when checking should ask notification permission then default is true`() = runTest { + coEvery { localStorage.getBooleanOrNull("ask_notification_permission") } returns null + + val result = repository.shouldAskNotificationPermission() + + assertEquals(true, result) + } + + @Test + fun `Given user sets ask notification permission to true when retrieving then value is true`() = runTest { + coEvery { localStorage.putBoolean("ask_notification_permission", true) } returns Unit + coEvery { localStorage.getBooleanOrNull("ask_notification_permission") } returns true + repository.setAskNotificationPermission(shouldAsk = true) + + val result = repository.shouldAskNotificationPermission() + + assertEquals(true, result) + coVerify { localStorage.putBoolean("ask_notification_permission", true) } + } + + @Test + fun `Given user sets ask notification permission to false when retrieving then value is false`() = runTest { + coEvery { localStorage.putBoolean("ask_notification_permission", false) } returns Unit + coEvery { localStorage.getBooleanOrNull("ask_notification_permission") } returns false + repository.setAskNotificationPermission(shouldAsk = false) + + val result = repository.shouldAskNotificationPermission() + + assertEquals(false, result) + coVerify { localStorage.putBoolean("ask_notification_permission", false) } + } }