diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index bfaa27fa6..9599ebab0 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -65,6 +65,7 @@ import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.ui.NavDisplay import com.google.samples.apps.nowinandroid.R +import com.google.samples.apps.nowinandroid.core.designsystem.component.LocalSnackbarHostState import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaGradientBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationSuiteScaffold @@ -74,7 +75,6 @@ import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColo import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors import com.google.samples.apps.nowinandroid.core.navigation.Navigator import com.google.samples.apps.nowinandroid.core.navigation.toEntries -import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.LocalSnackbarHostState import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.bookmarksEntry import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey import com.google.samples.apps.nowinandroid.feature.foryou.impl.navigation.forYouEntry diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt index 3c7610193..e9522d042 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt @@ -65,9 +65,9 @@ import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourc import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor +import com.google.samples.apps.nowinandroid.core.designsystem.component.LocalSnackbarHostState import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions -import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.LocalSnackbarHostState import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt index 75dc6baa7..7a24df05b 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt @@ -38,9 +38,9 @@ import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourc import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor +import com.google.samples.apps.nowinandroid.core.designsystem.component.LocalSnackbarHostState import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions -import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.LocalSnackbarHostState import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest diff --git a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/Snackbar.kt b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/Snackbar.kt new file mode 100644 index 000000000..b0e914037 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/Snackbar.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.core.designsystem.component + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.compositionLocalOf + +val LocalSnackbarHostState = compositionLocalOf { SnackbarHostState() } diff --git a/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt b/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt index 0b7b9f570..e7bf1728d 100644 --- a/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt +++ b/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt @@ -16,8 +16,10 @@ package com.google.samples.apps.nowinandroid.core.ui +import android.content.ActivityNotFoundException import android.content.Context import android.net.Uri +import android.util.Log import androidx.annotation.ColorInt import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent @@ -29,16 +31,20 @@ import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper +import com.google.samples.apps.nowinandroid.core.designsystem.component.LocalSnackbarHostState import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource +import kotlinx.coroutines.launch /** * An extension on [LazyListScope] defining a feed with news resources. @@ -62,6 +68,9 @@ fun LazyStaggeredGridScope.newsFeed( val context = LocalContext.current val analyticsHelper = LocalAnalyticsHelper.current val backgroundColor = MaterialTheme.colorScheme.background.toArgb() + val snackbarHostState = LocalSnackbarHostState.current + val coroutineScope = rememberCoroutineScope() + val noBrowserMessage = stringResource(R.string.core_ui_no_browser_available) NewsResourceCardExpanded( userNewsResource = userNewsResource, @@ -71,7 +80,11 @@ fun LazyStaggeredGridScope.newsFeed( analyticsHelper.logNewsResourceOpened( newsResourceId = userNewsResource.id, ) - launchCustomChromeTab(context, Uri.parse(userNewsResource.url), backgroundColor) + if (!launchCustomChromeTab(context, Uri.parse(userNewsResource.url), backgroundColor)) { + coroutineScope.launch { + snackbarHostState.showSnackbar(noBrowserMessage) + } + } onNewsResourceViewed(userNewsResource.id) }, @@ -92,14 +105,25 @@ fun LazyStaggeredGridScope.newsFeed( } } -fun launchCustomChromeTab(context: Context, uri: Uri, @ColorInt toolbarColor: Int) { +/** + * Launches a Chrome Custom Tab for the given [uri]. + * + * @return `true` if the URL was launched successfully, `false` if no browser activity was found. + */ +fun launchCustomChromeTab(context: Context, uri: Uri, @ColorInt toolbarColor: Int): Boolean { val customTabBarColor = CustomTabColorSchemeParams.Builder() .setToolbarColor(toolbarColor).build() val customTabsIntent = CustomTabsIntent.Builder() .setDefaultColorSchemeParams(customTabBarColor) .build() - customTabsIntent.launchUrl(context, uri) + return try { + customTabsIntent.launchUrl(context, uri) + true + } catch (e: ActivityNotFoundException) { + Log.e("NewsFeed", "No browser activity found to handle URI: $uri", e) + false + } } /** diff --git a/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt b/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt index ea1c09d01..32b9512b7 100644 --- a/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt +++ b/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt @@ -20,11 +20,15 @@ import android.net.Uri import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper +import com.google.samples.apps.nowinandroid.core.designsystem.component.LocalSnackbarHostState import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource +import kotlinx.coroutines.launch /** * Extension function for displaying a [List] of [NewsResourceCardExpanded] backed by a list of @@ -47,6 +51,9 @@ fun LazyListScope.userNewsResourceCardItems( val backgroundColor = MaterialTheme.colorScheme.background.toArgb() val context = LocalContext.current val analyticsHelper = LocalAnalyticsHelper.current + val snackbarHostState = LocalSnackbarHostState.current + val coroutineScope = rememberCoroutineScope() + val noBrowserMessage = stringResource(R.string.core_ui_no_browser_available) NewsResourceCardExpanded( userNewsResource = userNewsResource, @@ -57,7 +64,11 @@ fun LazyListScope.userNewsResourceCardItems( analyticsHelper.logNewsResourceOpened( newsResourceId = userNewsResource.id, ) - launchCustomChromeTab(context, resourceUrl, backgroundColor) + if (!launchCustomChromeTab(context, resourceUrl, backgroundColor)) { + coroutineScope.launch { + snackbarHostState.showSnackbar(noBrowserMessage) + } + } onNewsResourceViewed(userNewsResource.id) }, onTopicClick = onTopicClick, diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index a97746a9c..564d9c176 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -31,4 +31,5 @@ Unfollow interest Feed sharing %1$s: %2$s + No browser available to open link diff --git a/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksEntryProvider.kt b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksEntryProvider.kt index 467965651..42398edfe 100644 --- a/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksEntryProvider.kt +++ b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksEntryProvider.kt @@ -17,11 +17,10 @@ package com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation import androidx.compose.material3.SnackbarDuration.Short -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult.ActionPerformed -import androidx.compose.runtime.compositionLocalOf import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey +import com.google.samples.apps.nowinandroid.core.designsystem.component.LocalSnackbarHostState import com.google.samples.apps.nowinandroid.core.navigation.Navigator import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksNavKey import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.BookmarksScreen @@ -42,8 +41,3 @@ fun EntryProviderScope.bookmarksEntry(navigator: Navigator) { ) } } - -// TODO: Why is this here? -val LocalSnackbarHostState = compositionLocalOf { - error("SnackbarHostState state should be initialized at runtime") -} diff --git a/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreen.kt b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreen.kt index 0ae916db3..937d56a41 100644 --- a/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreen.kt +++ b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreen.kt @@ -86,6 +86,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionStatus.Denied import com.google.accompanist.permissions.rememberPermissionState import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicAsyncImage +import com.google.samples.apps.nowinandroid.core.designsystem.component.LocalSnackbarHostState import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOverlayLoadingWheel @@ -104,6 +105,7 @@ import com.google.samples.apps.nowinandroid.core.ui.UserNewsResourcePreviewParam import com.google.samples.apps.nowinandroid.core.ui.launchCustomChromeTab import com.google.samples.apps.nowinandroid.core.ui.newsFeed import com.google.samples.apps.nowinandroid.feature.foryou.api.R +import com.google.samples.apps.nowinandroid.core.ui.R as UiR @Composable fun ForYouScreen( @@ -469,16 +471,21 @@ private fun DeepLinkEffect( ) { val context = LocalContext.current val backgroundColor = MaterialTheme.colorScheme.background.toArgb() + val snackbarHostState = LocalSnackbarHostState.current + val noBrowserMessage = stringResource(UiR.string.core_ui_no_browser_available) LaunchedEffect(userNewsResource) { if (userNewsResource == null) return@LaunchedEffect if (!userNewsResource.hasBeenViewed) onDeepLinkOpened(userNewsResource.id) - launchCustomChromeTab( + val launched = launchCustomChromeTab( context = context, uri = Uri.parse(userNewsResource.url), toolbarColor = backgroundColor, ) + if (!launched) { + snackbarHostState.showSnackbar(noBrowserMessage) + } } }