diff --git a/app/src/main/java/com/android/developers/androidify/navigation/MainNavigation.kt b/app/src/main/java/com/android/developers/androidify/navigation/MainNavigation.kt index 2a8fbbc5..eb26faea 100644 --- a/app/src/main/java/com/android/developers/androidify/navigation/MainNavigation.kt +++ b/app/src/main/java/com/android/developers/androidify/navigation/MainNavigation.kt @@ -31,6 +31,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.IntOffset import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator @@ -184,6 +185,7 @@ fun MainNavigation() { } entry { val context = LocalContext.current + val uriHandler = LocalUriHandler.current AboutScreen( onBackPressed = { backStack.removeLastOrNull() @@ -191,6 +193,12 @@ fun MainNavigation() { onLicensesClicked = { context.startActivity(Intent(context, OssLicensesMenuActivity::class.java)) }, + onPrivacyClicked = { + uriHandler.openUri("https://policies.google.com/privacy") + }, + onTermsClicked = { + uriHandler.openUri("https://policies.google.com/terms") + } ) } }, diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/AboutScreen.kt b/feature/home/src/main/java/com/android/developers/androidify/home/AboutScreen.kt index 6d26b6f5..676f37cc 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/AboutScreen.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/AboutScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -44,18 +45,22 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.ui.LocalNavAnimatedContentScope +import androidx.xr.compose.platform.LocalSpatialCapabilities +import com.android.developers.androidify.home.xr.AboutScreenSpatial import com.android.developers.androidify.theme.AndroidifyTheme import com.android.developers.androidify.theme.LocalSharedTransitionScope import com.android.developers.androidify.theme.SharedElementContextPreview @@ -66,155 +71,211 @@ import com.android.developers.androidify.util.LargeScreensPreview import com.android.developers.androidify.util.PhonePreview import com.android.developers.androidify.util.isAtLeastMedium -@PhonePreview @Composable -private fun AboutPreviewCompact() { - SharedElementContextPreview { - AboutScreen(isMediumWindowSize = false, {}, {}) - } +fun AboutScreen( + viewModel: AboutViewModel = hiltViewModel(), + onBackPressed: () -> Unit, + onTermsClicked: () -> Unit, + onPrivacyClicked: () -> Unit, + onLicensesClicked: () -> Unit, +) { + val state by viewModel.state.collectAsStateWithLifecycle() + AboutScreenContents( + onBackPressed = onBackPressed, + onTermsClicked = onTermsClicked, + onPrivacyClicked = onPrivacyClicked, + onLicensesClicked = onLicensesClicked, + xrEnabled = state.isXrEnabled, + ) } -@LargeScreensPreview @Composable -private fun AboutPreviewLargeScreens() { - SharedElementContextPreview { - AboutScreen(isMediumWindowSize = true, {}, {}) +fun AboutScreenContents( + onBackPressed: () -> Unit, + onTermsClicked: () -> Unit, + onPrivacyClicked: () -> Unit, + onLicensesClicked: () -> Unit, + xrEnabled: Boolean = false, + isMediumWindowSize: Boolean = isAtLeastMedium(), +) { + val bottomButtons = @Composable { + FooterButtons( + modifier = Modifier, + onTermsClicked = onTermsClicked, + onPrivacyClicked = onPrivacyClicked, + onLicensesClicked = onLicensesClicked, + ) + } + when { + xrEnabled && LocalSpatialCapabilities.current.isSpatialUiEnabled -> + AboutScreenSpatial(onBackPressed = onBackPressed, bottomButtons = bottomButtons) + + isMediumWindowSize -> + AboutScreenScaffold(topBar = { BackButton(onBackPressed) }) { contentPadding -> + AboutScreenMedium( + padding = contentPadding, + bottomButtons = bottomButtons, + ) + } + + else -> + AboutScreenScaffold(topBar = { BackButton(onBackPressed) }) { contentPadding -> + AboutScreenCompact( + padding = contentPadding, + bottomButtons = bottomButtons, + ) + } } } @Composable -fun AboutScreen( - isMediumWindowSize: Boolean = isAtLeastMedium(), - onBackPressed: () -> Unit, - onLicensesClicked: () -> Unit, +fun AboutScreenScaffold( + topBar: @Composable () -> Unit, + content: @Composable (PaddingValues) -> Unit, ) { val sharedElementScope = LocalSharedTransitionScope.current val navScope = LocalNavAnimatedContentScope.current with(sharedElementScope) { Scaffold( - topBar = { - IconButton( - modifier = Modifier - .safeDrawingPadding() - .padding(16.dp), - shape = CircleShape, - colors = IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - contentColor = MaterialTheme.colorScheme.onSurface, - ), - onClick = { - onBackPressed() - }, - ) { - Icon( - rememberVectorPainter(ImageVector.vectorResource(com.android.developers.androidify.theme.R.drawable.rounded_close_24)), - contentDescription = stringResource(R.string.about_back_content_description), - ) - } - }, + topBar = topBar, containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, modifier = Modifier .sharedBoundsReveal( rememberSharedContentState(SharedElementKey.AboutKey), animatedVisibilityScope = navScope, ), - ) { padding -> - if (isMediumWindowSize) { - Box( - modifier = Modifier - .fillMaxSize() - .safeDrawingPadding() - .padding(padding) - .verticalScroll(rememberScrollState()) - .padding(start = 24.dp, end = 24.dp, top = 24.dp), - contentAlignment = Alignment.Center, - ) { - Column( - modifier = Modifier - .wrapContentHeight() - .width(700.dp), - ) { - Text( - text = stringResource(R.string.how_it_works), - style = MaterialTheme.typography.displayLarge, - ) - Spacer(Modifier.size(48.dp)) - AboutMessage(textStyle = MaterialTheme.typography.headlineMedium) - Spacer(Modifier.size(48.dp)) - Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { - BulletPoint( - "1", - stringResource(R.string.about_step1_title), - stringResource(R.string.about_step1_label), - modifier = Modifier.weight(1 / 3f), - textStyle = MaterialTheme.typography.bodySmall, - ) - BulletPoint( - "2", - stringResource(R.string.about_step2_title), - stringResource(R.string.about_step2_label), - modifier = Modifier.weight(1 / 3f), - textStyle = MaterialTheme.typography.bodySmall, - ) - BulletPoint( - "3", - stringResource(R.string.about_step3_title), - stringResource(R.string.about_step3_label), - modifier = Modifier.weight(1 / 3f), - textStyle = MaterialTheme.typography.bodySmall, - ) - } - Spacer(Modifier.size(48.dp)) - FooterButtons( - modifier = Modifier.padding(bottom = 8.dp), - onLicensesClicked, - ) - } - } - } else { - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(padding) - .padding(start = 24.dp, end = 24.dp, top = 24.dp), - ) { - Text( - text = stringResource(R.string.how_it_works), - style = MaterialTheme.typography.displayLarge, - ) - AboutMessage() - Spacer(modifier = Modifier.size(36.dp)) - BulletPoint( - "1", - stringResource(R.string.about_step1_title), - stringResource(R.string.about_step1_label), - ) - Spacer(modifier = Modifier.size(24.dp)) - BulletPoint( - "2", - stringResource(R.string.about_step2_title), - stringResource(R.string.about_step2_label), - ) - Spacer(modifier = Modifier.size(24.dp)) - BulletPoint( - "3", - stringResource(R.string.about_step3_title), - stringResource(R.string.about_step3_label), - ) - Spacer(modifier = Modifier.size(24.dp)) - FooterButtons(modifier = Modifier.padding(bottom = 8.dp), onLicensesClicked) - } + content = content, + ) + } +} + +@Composable +private fun AboutScreenCompact( + padding: PaddingValues, + bottomButtons: @Composable () -> Unit, +) { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(padding) + .padding(start = 24.dp, end = 24.dp, top = 24.dp), + ) { + Text( + text = stringResource(R.string.how_it_works), + style = MaterialTheme.typography.displayLarge, + ) + AboutMessage() + Spacer(modifier = Modifier.size(36.dp)) + BulletPoint( + "1", + stringResource(R.string.about_step1_title), + stringResource(R.string.about_step1_label), + ) + Spacer(modifier = Modifier.size(24.dp)) + BulletPoint( + "2", + stringResource(R.string.about_step2_title), + stringResource(R.string.about_step2_label), + ) + Spacer(modifier = Modifier.size(24.dp)) + BulletPoint( + "3", + stringResource(R.string.about_step3_title), + stringResource(R.string.about_step3_label), + ) + Spacer(modifier = Modifier.size(24.dp)) + bottomButtons() + Spacer(modifier = Modifier.size(8.dp)) + } +} + +@Composable +fun AboutScreenMedium( + padding: PaddingValues, + bottomButtons: (@Composable () -> Unit)?, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .safeDrawingPadding() + .padding(padding) + .verticalScroll(rememberScrollState()) + .padding(start = 24.dp, end = 24.dp, top = 24.dp), + contentAlignment = Alignment.Center, + ) { + Column( + modifier = Modifier + .wrapContentHeight() + .width(700.dp), + ) { + Text( + text = stringResource(R.string.how_it_works), + style = MaterialTheme.typography.displayLarge, + ) + Spacer(Modifier.size(48.dp)) + AboutMessage(textStyle = MaterialTheme.typography.headlineMedium) + Spacer(Modifier.size(48.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + BulletPoint( + "1", + stringResource(R.string.about_step1_title), + stringResource(R.string.about_step1_label), + modifier = Modifier.weight(1 / 3f), + textStyle = MaterialTheme.typography.bodySmall, + ) + BulletPoint( + "2", + stringResource(R.string.about_step2_title), + stringResource(R.string.about_step2_label), + modifier = Modifier.weight(1 / 3f), + textStyle = MaterialTheme.typography.bodySmall, + ) + BulletPoint( + "3", + stringResource(R.string.about_step3_title), + stringResource(R.string.about_step3_label), + modifier = Modifier.weight(1 / 3f), + textStyle = MaterialTheme.typography.bodySmall, + ) + } + if (bottomButtons != null) { + Spacer(Modifier.size(48.dp)) + bottomButtons() + Spacer(Modifier.size(8.dp)) } } } } @Composable -private fun FooterButtons( +fun BackButton(onBackPressed: () -> Unit, modifier: Modifier = Modifier) { + IconButton( + modifier = modifier + .safeDrawingPadding() + .padding(16.dp), + shape = CircleShape, + colors = IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + contentColor = MaterialTheme.colorScheme.onSurface, + ), + onClick = { + onBackPressed() + }, + ) { + Icon( + rememberVectorPainter(ImageVector.vectorResource(com.android.developers.androidify.theme.R.drawable.rounded_close_24)), + contentDescription = stringResource(R.string.about_back_content_description), + ) + } +} + +@Composable +fun FooterButtons( modifier: Modifier = Modifier, + onTermsClicked: () -> Unit, + onPrivacyClicked: () -> Unit, onLicensesClicked: () -> Unit, ) { - val uriHandler = LocalUriHandler.current FlowRow( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(16.dp), @@ -222,13 +283,13 @@ private fun FooterButtons( ) { SecondaryOutlinedButton( onClick = { - uriHandler.openUri("https://policies.google.com/terms") + onTermsClicked() }, buttonText = stringResource(R.string.terms), ) SecondaryOutlinedButton( onClick = { - uriHandler.openUri("https://policies.google.com/privacy") + onPrivacyClicked() }, buttonText = stringResource(R.string.privacy), ) @@ -242,7 +303,7 @@ private fun FooterButtons( } @Composable -private fun AboutMessage(textStyle: TextStyle = MaterialTheme.typography.bodyLarge) { +fun AboutMessage(textStyle: TextStyle = MaterialTheme.typography.bodyLarge) { Text( stringResource(R.string.about_message), style = textStyle.copy(fontWeight = FontWeight.Bold), @@ -287,6 +348,34 @@ fun BulletPoint( } } +@PhonePreview +@Composable +private fun AboutPreviewCompact() { + SharedElementContextPreview { + AboutScreenContents( + isMediumWindowSize = false, + onBackPressed = {}, + onTermsClicked = {}, + onPrivacyClicked = {}, + onLicensesClicked = {}, + ) + } +} + +@LargeScreensPreview +@Composable +private fun AboutPreviewLargeScreens() { + SharedElementContextPreview { + AboutScreenContents( + isMediumWindowSize = true, + onBackPressed = {}, + onTermsClicked = {}, + onPrivacyClicked = {}, + onLicensesClicked = {}, + ) + } +} + @Preview(showBackground = true) @Composable private fun BulletPointPreview() { diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/AboutViewModel.kt b/feature/home/src/main/java/com/android/developers/androidify/home/AboutViewModel.kt new file mode 100644 index 00000000..8eb1d870 --- /dev/null +++ b/feature/home/src/main/java/com/android/developers/androidify/home/AboutViewModel.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2025 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.android.developers.androidify.home + +import androidx.lifecycle.ViewModel +import com.android.developers.androidify.data.ConfigProvider +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@HiltViewModel +class AboutViewModel @Inject constructor(configProvider: ConfigProvider) : ViewModel() { + private val _state = MutableStateFlow( + AboutState( + isXrEnabled = configProvider.isXrEnabled(), + ), + ) + val state = _state.asStateFlow() +} + +data class AboutState( + val isXrEnabled: Boolean = false, +) diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/xr/AboutScreenSpatial.kt b/feature/home/src/main/java/com/android/developers/androidify/home/xr/AboutScreenSpatial.kt new file mode 100644 index 00000000..f49c8117 --- /dev/null +++ b/feature/home/src/main/java/com/android/developers/androidify/home/xr/AboutScreenSpatial.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2025 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.android.developers.androidify.home.xr + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.xr.compose.spatial.ContentEdge +import androidx.xr.compose.spatial.Orbiter +import androidx.xr.compose.spatial.OrbiterOffsetType +import androidx.xr.compose.subspace.SpatialPanel +import com.android.developers.androidify.home.AboutScreenMedium +import com.android.developers.androidify.home.BackButton +import com.android.developers.androidify.xr.MainPanelWorkaround +import com.android.developers.androidify.xr.SquiggleBackgroundSubspace + +@Composable +fun AboutScreenSpatial(onBackPressed: () -> Unit, bottomButtons: @Composable () -> Unit) { + SquiggleBackgroundSubspace(500.dp) { + MainPanelWorkaround() + SpatialPanel { + Orbiter( + ContentEdge.Top, + offset = 16.dp, + offsetType = OrbiterOffsetType.InnerEdge, + alignment = Alignment.Start, + ) { + BackButton(onBackPressed) + } + AboutScreenMedium( + modifier = Modifier.background(MaterialTheme.colorScheme.surfaceContainerHigh), + padding = PaddingValues(32.dp), + bottomButtons = null, + ) + Orbiter( + position = ContentEdge.Bottom, + offset = 48.dp, + offsetType = OrbiterOffsetType.InnerEdge, + elevation = 0.dp, + ) { + bottomButtons() + } + } + } +}