diff --git a/.github/workflows/build-sample.yml b/.github/workflows/build-sample.yml index 078a4fcd38..30ee82de52 100644 --- a/.github/workflows/build-sample.yml +++ b/.github/workflows/build-sample.yml @@ -32,7 +32,9 @@ jobs: distribution: 'zulu' - name: Generate cache key - run: ./scripts/checksum.sh ${{ inputs.path }} checksum.txt + run: ./scripts/checksum.sh ${INPUTS_PATH} checksum.txt + env: + INPUTS_PATH: ${{ inputs.path }} - uses: actions/cache@v3 with: diff --git a/Reply/app/build.gradle.kts b/Reply/app/build.gradle.kts index 57836241f7..148d9e622b 100644 --- a/Reply/app/build.gradle.kts +++ b/Reply/app/build.gradle.kts @@ -102,7 +102,7 @@ dependencies { debugImplementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.compose.material3) - implementation("com.google.accompanist:accompanist-adaptive:0.26.2-beta") + implementation("androidx.compose.material3:material3-adaptive-android:1.0.0-SNAPSHOT") implementation(libs.androidx.compose.materialWindow) implementation(libs.androidx.compose.material.iconsExtended) diff --git a/Reply/app/src/main/java/com/example/reply/ui/MainActivity.kt b/Reply/app/src/main/java/com/example/reply/ui/MainActivity.kt index 62bb64a75b..a6ef3fdc8b 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/MainActivity.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/MainActivity.kt @@ -31,25 +31,20 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.example.reply.data.local.LocalEmailsDataProvider import com.example.reply.ui.theme.ReplyTheme -import com.google.accompanist.adaptive.calculateDisplayFeatures class MainActivity : ComponentActivity() { private val viewModel: ReplyHomeViewModel by viewModels() - @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ReplyTheme { - val windowSize = calculateWindowSizeClass(this) - val displayFeatures = calculateDisplayFeatures(this) val uiState by viewModel.uiState.collectAsStateWithLifecycle() ReplyApp( - windowSize = windowSize, - displayFeatures = displayFeatures, replyHomeUIState = uiState, closeDetailScreen = { viewModel.closeDetailScreen() @@ -66,67 +61,57 @@ class MainActivity : ComponentActivity() { } } -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) + @Preview(showBackground = true) @Composable fun ReplyAppPreview() { ReplyTheme { ReplyApp( replyHomeUIState = ReplyHomeUIState(emails = LocalEmailsDataProvider.allEmails), - windowSize = WindowSizeClass.calculateFromSize(DpSize(400.dp, 900.dp)), - displayFeatures = emptyList(), ) } } -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) + @Preview(showBackground = true, widthDp = 700, heightDp = 500) @Composable fun ReplyAppPreviewTablet() { ReplyTheme { ReplyApp( replyHomeUIState = ReplyHomeUIState(emails = LocalEmailsDataProvider.allEmails), - windowSize = WindowSizeClass.calculateFromSize(DpSize(700.dp, 500.dp)), - displayFeatures = emptyList(), ) } } -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) + @Preview(showBackground = true, widthDp = 500, heightDp = 700) @Composable fun ReplyAppPreviewTabletPortrait() { ReplyTheme { ReplyApp( replyHomeUIState = ReplyHomeUIState(emails = LocalEmailsDataProvider.allEmails), - windowSize = WindowSizeClass.calculateFromSize(DpSize(500.dp, 700.dp)), - displayFeatures = emptyList(), ) } } -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) + @Preview(showBackground = true, widthDp = 1100, heightDp = 600) @Composable fun ReplyAppPreviewDesktop() { ReplyTheme { ReplyApp( replyHomeUIState = ReplyHomeUIState(emails = LocalEmailsDataProvider.allEmails), - windowSize = WindowSizeClass.calculateFromSize(DpSize(1100.dp, 600.dp)), - displayFeatures = emptyList(), ) } } -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) + @Preview(showBackground = true, widthDp = 600, heightDp = 1100) @Composable fun ReplyAppPreviewDesktopPortrait() { ReplyTheme { ReplyApp( replyHomeUIState = ReplyHomeUIState(emails = LocalEmailsDataProvider.allEmails), - windowSize = WindowSizeClass.calculateFromSize(DpSize(600.dp, 1100.dp)), - displayFeatures = emptyList(), ) } } diff --git a/Reply/app/src/main/java/com/example/reply/ui/ReplyApp.kt b/Reply/app/src/main/java/com/example/reply/ui/ReplyApp.kt index c12db84785..10d8f822e8 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/ReplyApp.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/ReplyApp.kt @@ -26,6 +26,9 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.PermanentNavigationDrawer +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.Posture +import androidx.compose.material3.adaptive.calculateWindowAdaptiveInfo import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass import androidx.compose.material3.windowsizeclass.WindowSizeClass @@ -49,19 +52,14 @@ import com.example.reply.ui.navigation.ReplyNavigationActions import com.example.reply.ui.navigation.ReplyNavigationRail import com.example.reply.ui.navigation.ReplyRoute import com.example.reply.ui.navigation.ReplyTopLevelDestination -import com.example.reply.ui.utils.DevicePosture import com.example.reply.ui.utils.ReplyContentType import com.example.reply.ui.utils.ReplyNavigationContentPosition import com.example.reply.ui.utils.ReplyNavigationType -import com.example.reply.ui.utils.isBookPosture -import com.example.reply.ui.utils.isSeparating import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class) @Composable fun ReplyApp( - windowSize: WindowSizeClass, - displayFeatures: List, replyHomeUIState: ReplyHomeUIState, closeDetailScreen: () -> Unit = {}, navigateToDetail: (Long, ReplyContentType) -> Unit = { _, _ -> }, @@ -79,17 +77,9 @@ fun ReplyApp( * In the state of folding device If it's half fold in BookPosture we want to avoid content * at the crease/hinge */ - val foldingFeature = displayFeatures.filterIsInstance().firstOrNull() - - val foldingDevicePosture = when { - isBookPosture(foldingFeature) -> - DevicePosture.BookPosture(foldingFeature.bounds) - isSeparating(foldingFeature) -> - DevicePosture.Separating(foldingFeature.bounds, foldingFeature.orientation) - - else -> DevicePosture.NormalPosture - } + val windowSize = calculateWindowAdaptiveInfo().windowSizeClass + val devicePosture = calculateWindowAdaptiveInfo().posture when (windowSize.widthSizeClass) { WindowWidthSizeClass.Compact -> { @@ -98,14 +88,14 @@ fun ReplyApp( } WindowWidthSizeClass.Medium -> { navigationType = ReplyNavigationType.NAVIGATION_RAIL - contentType = if (foldingDevicePosture != DevicePosture.NormalPosture) { + contentType = if (devicePosture.hasVerticalHinge) { ReplyContentType.DUAL_PANE } else { ReplyContentType.SINGLE_PANE } } WindowWidthSizeClass.Expanded -> { - navigationType = if (foldingDevicePosture is DevicePosture.BookPosture) { + navigationType = if (devicePosture.hasVerticalHinge) { ReplyNavigationType.NAVIGATION_RAIL } else { ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER @@ -138,7 +128,6 @@ fun ReplyApp( ReplyNavigationWrapper( navigationType = navigationType, contentType = contentType, - displayFeatures = displayFeatures, navigationContentPosition = navigationContentPosition, replyHomeUIState = replyHomeUIState, closeDetailScreen = closeDetailScreen, @@ -152,7 +141,6 @@ fun ReplyApp( private fun ReplyNavigationWrapper( navigationType: ReplyNavigationType, contentType: ReplyContentType, - displayFeatures: List, navigationContentPosition: ReplyNavigationContentPosition, replyHomeUIState: ReplyHomeUIState, closeDetailScreen: () -> Unit, @@ -182,7 +170,6 @@ private fun ReplyNavigationWrapper( ReplyAppContent( navigationType = navigationType, contentType = contentType, - displayFeatures = displayFeatures, navigationContentPosition = navigationContentPosition, replyHomeUIState = replyHomeUIState, navController = navController, @@ -212,7 +199,6 @@ private fun ReplyNavigationWrapper( ReplyAppContent( navigationType = navigationType, contentType = contentType, - displayFeatures = displayFeatures, navigationContentPosition = navigationContentPosition, replyHomeUIState = replyHomeUIState, navController = navController, @@ -235,7 +221,6 @@ fun ReplyAppContent( modifier: Modifier = Modifier, navigationType: ReplyNavigationType, contentType: ReplyContentType, - displayFeatures: List, navigationContentPosition: ReplyNavigationContentPosition, replyHomeUIState: ReplyHomeUIState, navController: NavHostController, @@ -263,9 +248,7 @@ fun ReplyAppContent( ReplyNavHost( navController = navController, contentType = contentType, - displayFeatures = displayFeatures, replyHomeUIState = replyHomeUIState, - navigationType = navigationType, closeDetailScreen = closeDetailScreen, navigateToDetail = navigateToDetail, toggleSelectedEmail = toggleSelectedEmail, @@ -285,9 +268,7 @@ fun ReplyAppContent( private fun ReplyNavHost( navController: NavHostController, contentType: ReplyContentType, - displayFeatures: List, replyHomeUIState: ReplyHomeUIState, - navigationType: ReplyNavigationType, closeDetailScreen: () -> Unit, navigateToDetail: (Long, ReplyContentType) -> Unit, toggleSelectedEmail: (Long) -> Unit, @@ -299,11 +280,9 @@ private fun ReplyNavHost( startDestination = ReplyRoute.INBOX, ) { composable(ReplyRoute.INBOX) { - ReplyInboxScreen( + ReplyInboxScreenCAMAL( contentType = contentType, replyHomeUIState = replyHomeUIState, - navigationType = navigationType, - displayFeatures = displayFeatures, closeDetailScreen = closeDetailScreen, navigateToDetail = navigateToDetail, toggleSelectedEmail = toggleSelectedEmail diff --git a/Reply/app/src/main/java/com/example/reply/ui/ReplyListContent.kt b/Reply/app/src/main/java/com/example/reply/ui/ReplyListContent.kt index df26c4ed85..42148cd41e 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/ReplyListContent.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/ReplyListContent.kt @@ -27,37 +27,27 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Edit -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.LargeFloatingActionButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.ListDetailPaneScaffoldRole +import androidx.compose.material3.adaptive.rememberListDetailPaneScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.window.layout.DisplayFeature -import com.example.reply.R import com.example.reply.data.Email import com.example.reply.ui.components.EmailDetailAppBar import com.example.reply.ui.components.ReplyDockedSearchBar import com.example.reply.ui.components.ReplyEmailListItem import com.example.reply.ui.components.ReplyEmailThreadItem import com.example.reply.ui.utils.ReplyContentType -import com.example.reply.ui.utils.ReplyNavigationType -import com.google.accompanist.adaptive.HorizontalTwoPaneStrategy -import com.google.accompanist.adaptive.TwoPane -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable -fun ReplyInboxScreen( +fun ReplyInboxScreenCAMAL( contentType: ReplyContentType, replyHomeUIState: ReplyHomeUIState, - navigationType: ReplyNavigationType, - displayFeatures: List, closeDetailScreen: () -> Unit, navigateToDetail: (Long, ReplyContentType) -> Unit, toggleSelectedEmail: (Long) -> Unit, @@ -66,68 +56,44 @@ fun ReplyInboxScreen( /** * When moving from LIST_AND_DETAIL page to LIST page clear the selection and user should see LIST screen. */ + val layoutState = + rememberListDetailPaneScaffoldState(initialFocus = ListDetailPaneScaffoldRole.List) LaunchedEffect(key1 = contentType) { if (contentType == ReplyContentType.SINGLE_PANE && !replyHomeUIState.isDetailOnlyOpen) { closeDetailScreen() + } else { + layoutState. navigateTo(ListDetailPaneScaffoldRole.Detail) } } val emailLazyListState = rememberLazyListState() - // TODO: Show top app bar over full width of app when in multi-select mode - if (contentType == ReplyContentType.DUAL_PANE) { - TwoPane( - first = { - ReplyEmailList( - emails = replyHomeUIState.emails, - openedEmail = replyHomeUIState.openedEmail, - selectedEmailIds = replyHomeUIState.selectedEmails, - toggleEmailSelection = toggleSelectedEmail, - emailLazyListState = emailLazyListState, - navigateToDetail = navigateToDetail - ) - }, - second = { - ReplyEmailDetail( - email = replyHomeUIState.openedEmail ?: replyHomeUIState.emails.first(), - isFullScreen = false - ) - }, - strategy = HorizontalTwoPaneStrategy(splitFraction = 0.5f, gapWidth = 16.dp), - displayFeatures = displayFeatures - ) - } else { - Box(modifier = modifier.fillMaxSize()) { - ReplySinglePaneContent( - replyHomeUIState = replyHomeUIState, + ListDetailPaneScaffold( + layoutState = layoutState, + listPane = { + ReplyEmailList( + emails = replyHomeUIState.emails, + openedEmail = replyHomeUIState.openedEmail, + selectedEmailIds = replyHomeUIState.selectedEmails, toggleEmailSelection = toggleSelectedEmail, emailLazyListState = emailLazyListState, - modifier = Modifier.fillMaxSize(), - closeDetailScreen = closeDetailScreen, - navigateToDetail = navigateToDetail - ) - // When we have bottom navigation we show FAB at the bottom end. - if (navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) { - LargeFloatingActionButton( - onClick = { /*TODO*/ }, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(16.dp), - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer - ) { - Icon( - imageVector = Icons.Default.Edit, - contentDescription = stringResource(id = R.string.edit), - modifier = Modifier.size(28.dp) - ) + navigateToDetail = { id, contentType -> + navigateToDetail.invoke(id, contentType) + layoutState.navigateTo(ListDetailPaneScaffoldRole.Detail) } - } + ) + }, + detailPane = { + ReplyEmailDetail( + email = replyHomeUIState.openedEmail ?: replyHomeUIState.emails.first(), + isFullScreen = false + ) } - } + ) } + @Composable fun ReplySinglePaneContent( replyHomeUIState: ReplyHomeUIState, diff --git a/Reply/app/src/main/java/com/example/reply/ui/utils/WindowStateUtils.kt b/Reply/app/src/main/java/com/example/reply/ui/utils/WindowStateUtils.kt index f212254689..15c5bb6356 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/utils/WindowStateUtils.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/utils/WindowStateUtils.kt @@ -21,34 +21,7 @@ import androidx.window.layout.FoldingFeature import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract -/** - * Information about the posture of the device - */ -sealed interface DevicePosture { - object NormalPosture : DevicePosture - - data class BookPosture( - val hingePosition: Rect - ) : DevicePosture - - data class Separating( - val hingePosition: Rect, - var orientation: FoldingFeature.Orientation - ) : DevicePosture -} - -@OptIn(ExperimentalContracts::class) -fun isBookPosture(foldFeature: FoldingFeature?): Boolean { - contract { returns(true) implies (foldFeature != null) } - return foldFeature?.state == FoldingFeature.State.HALF_OPENED && - foldFeature.orientation == FoldingFeature.Orientation.VERTICAL -} -@OptIn(ExperimentalContracts::class) -fun isSeparating(foldFeature: FoldingFeature?): Boolean { - contract { returns(true) implies (foldFeature != null) } - return foldFeature?.state == FoldingFeature.State.FLAT && foldFeature.isSeparating -} /** * Different type of navigation supported by app depending on device size and state. diff --git a/Reply/gradle/libs.versions.toml b/Reply/gradle/libs.versions.toml index e57c91175c..8e4582b61c 100644 --- a/Reply/gradle/libs.versions.toml +++ b/Reply/gradle/libs.versions.toml @@ -4,7 +4,7 @@ ##### [versions] accompanist = "0.30.1" -androidGradlePlugin = "8.0.1" +androidGradlePlugin = "8.0.0-alpha11" androidx-activity-compose = "1.7.1" androidx-appcompat = "1.6.1" androidx-benchmark = "1.1.0" @@ -25,7 +25,7 @@ androidxHiltNavigationCompose = "1.0.0" androix-test-uiautomator = "2.2.0" coil = "2.2.2" # @keep -compileSdk = "33" +compileSdk = "34" compose-compiler = "1.4.0" coroutines = "1.6.4" google-maps = "18.1.0" diff --git a/Reply/settings.gradle.kts b/Reply/settings.gradle.kts index 5655826457..135b42f98b 100644 --- a/Reply/settings.gradle.kts +++ b/Reply/settings.gradle.kts @@ -22,16 +22,18 @@ pluginManagement { mavenCentral() } } + dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { snapshotVersion?.let { - println("https://androidx.dev/snapshots/builds/$it/artifacts/repository/") + println("https://androidx.dev/snapshots/builds/$it/artifacts/repository/") maven { url = uri("https://androidx.dev/snapshots/builds/$it/artifacts/repository/") } } google() mavenCentral() + maven { url = uri("https://androidx.dev/snapshots/builds/10531429/artifacts/repository") } } } rootProject.name = "Reply"