diff options
7 files changed, 316 insertions, 5 deletions
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt index 941e7705441b..6d20c917e214 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt @@ -23,6 +23,7 @@ import com.android.settingslib.spa.framework.common.SettingsPageProviderReposito import com.android.settingslib.spa.framework.common.SpaEnvironment import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider +import com.android.settingslib.spa.gallery.dialog.AlterDialogPageProvider import com.android.settingslib.spa.gallery.home.HomePageProvider import com.android.settingslib.spa.gallery.page.ArgumentPageProvider import com.android.settingslib.spa.gallery.page.ChartPageProvider @@ -72,6 +73,7 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { ActionButtonPageProvider, ProgressBarPageProvider, ChartPageProvider, + AlterDialogPageProvider, ), rootPages = listOf( HomePageProvider.createSettingsPage(), diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/AlterDialogPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/AlterDialogPage.kt new file mode 100644 index 000000000000..a57df0f46f6a --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/AlterDialogPage.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 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 + * + * http://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.settingslib.spa.gallery.dialog + +import android.os.Bundle +import androidx.compose.material3.Text +import com.android.settingslib.spa.framework.common.SettingsEntry +import com.android.settingslib.spa.framework.common.SettingsEntryBuilder +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.common.createSettingsPage +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.widget.dialog.AlertDialogButton +import com.android.settingslib.spa.widget.dialog.rememberAlertDialogPresenter +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel + +private const val TITLE = "AlterDialogPage" + +object AlterDialogPageProvider : SettingsPageProvider { + override val name = "AlterDialogPage" + private val owner = createSettingsPage() + + override fun buildEntry(arguments: Bundle?): List<SettingsEntry> = listOf( + SettingsEntryBuilder.create("AlterDialog", owner).setUiLayoutFn { + val alertDialogPresenter = rememberAlertDialogPresenter( + confirmButton = AlertDialogButton("Ok"), + dismissButton = AlertDialogButton("Cancel"), + title = "Title", + text = { Text("Text") }, + ) + Preference(object : PreferenceModel { + override val title = "Show AlterDialog" + override val onClick = alertDialogPresenter::open + }) + }.build(), + ) + + fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner) + .setIsAllowSearch(true) + .setUiLayoutFn { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } + + override fun getTitle(arguments: Bundle?) = TITLE +} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt index 83c72c75e9f3..6d53dae5ae41 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt @@ -27,6 +27,7 @@ import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.gallery.R import com.android.settingslib.spa.gallery.SettingsPageProviderEnum import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider +import com.android.settingslib.spa.gallery.dialog.AlterDialogPageProvider import com.android.settingslib.spa.gallery.page.ArgumentPageModel import com.android.settingslib.spa.gallery.page.ArgumentPageProvider import com.android.settingslib.spa.gallery.page.ChartPageProvider @@ -58,6 +59,7 @@ object HomePageProvider : SettingsPageProvider { ActionButtonPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), ProgressBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), ChartPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), + AlterDialogPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), ) } diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle index 854359641bc0..b1d8d0dadfbd 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle +++ b/packages/SettingsLib/Spa/spa/build.gradle @@ -55,6 +55,8 @@ android { } dependencies { + String jetpack_lifecycle_version = "2.6.0-alpha03" + api "androidx.appcompat:appcompat:1.7.0-alpha01" api "androidx.slice:slice-builders:1.1.0-alpha02" api "androidx.slice:slice-core:1.1.0-alpha02" @@ -63,8 +65,9 @@ dependencies { api "androidx.compose.material:material-icons-extended:$jetpack_compose_version" api "androidx.compose.runtime:runtime-livedata:$jetpack_compose_version" api "androidx.compose.ui:ui-tooling-preview:$jetpack_compose_version" - api "androidx.lifecycle:lifecycle-livedata-ktx:2.6.0-alpha03" - api "androidx.navigation:navigation-compose:2.6.0-alpha03" + api "androidx.lifecycle:lifecycle-livedata-ktx:$jetpack_lifecycle_version" + api "androidx.lifecycle:lifecycle-runtime-compose:$jetpack_lifecycle_version" + api "androidx.navigation:navigation-compose:2.6.0-alpha04" api "com.github.PhilJay:MPAndroidChart:v3.1.0-alpha" api "com.google.android.material:material:1.7.0-alpha03" debugApi "androidx.compose.ui:ui-tooling:$jetpack_compose_version" diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt new file mode 100644 index 000000000000..d6f5328068ad --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2022 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 + * + * http://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.settingslib.spa.widget.dialog + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties + +data class AlertDialogButton( + val text: String, + val onClick: () -> Unit = {}, +) + +interface AlertDialogPresenter { + /** Opens the dialog. */ + fun open() + + /** Closes the dialog. */ + fun close() +} + +@Composable +fun rememberAlertDialogPresenter( + confirmButton: AlertDialogButton? = null, + dismissButton: AlertDialogButton? = null, + title: String? = null, + text: @Composable (() -> Unit)? = null, +): AlertDialogPresenter { + var openDialog by rememberSaveable { mutableStateOf(false) } + val alertDialogPresenter = remember { + object : AlertDialogPresenter { + override fun open() { + openDialog = true + } + + override fun close() { + openDialog = false + } + } + } + if (openDialog) { + alertDialogPresenter.SettingsAlertDialog(confirmButton, dismissButton, title, text) + } + return alertDialogPresenter +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun AlertDialogPresenter.SettingsAlertDialog( + confirmButton: AlertDialogButton?, + dismissButton: AlertDialogButton?, + title: String?, + text: @Composable (() -> Unit)?, +) { + val configuration = LocalConfiguration.current + AlertDialog( + onDismissRequest = ::close, + modifier = when (configuration.orientation) { + Configuration.ORIENTATION_LANDSCAPE -> { + Modifier.width(configuration.screenWidthDp.dp * 0.6f) + } + else -> Modifier + }, + confirmButton = { confirmButton?.let { Button(it) } }, + dismissButton = dismissButton?.let { { Button(it) } }, + title = title?.let { { Text(it) } }, + text = text?.let { + { + Column(Modifier.verticalScroll(rememberScrollState())) { + text() + } + } + }, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) +} + +@Composable +private fun AlertDialogPresenter.Button(button: AlertDialogButton) { + TextButton( + onClick = { + close() + button.onClick() + }, + ) { + Text(button.text) + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/MoreOptions.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/MoreOptions.kt index 5e201dfccbd5..84fea158175f 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/MoreOptions.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/MoreOptions.kt @@ -17,7 +17,6 @@ package com.android.settingslib.spa.widget.scaffold import androidx.appcompat.R -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material3.DropdownMenu @@ -36,7 +35,7 @@ import androidx.compose.ui.res.stringResource /** * Scope for the children of [MoreOptionsAction]. */ -interface MoreOptionsScope : ColumnScope { +interface MoreOptionsScope { fun dismiss() @Composable @@ -61,7 +60,7 @@ fun MoreOptionsAction( val onDismiss = { expanded = false } DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) { val moreOptionsScope = remember(this) { - object : MoreOptionsScope, ColumnScope by this { + object : MoreOptionsScope { override fun dismiss() { onDismiss() } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogTest.kt new file mode 100644 index 000000000000..9468f95a094e --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogTest.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2022 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 + * + * http://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.settingslib.spa.widget.dialog + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.testutils.onDialogText +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SettingsAlertDialogTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun title_whenDialogNotOpen_notDisplayed() { + composeTestRule.setContent { + rememberAlertDialogPresenter(title = TITLE) + } + + composeTestRule.onDialogText(TITLE).assertDoesNotExist() + } + + @Test + fun title_displayed() { + setAndOpenDialog { + rememberAlertDialogPresenter(title = TITLE) + } + + composeTestRule.onDialogText(TITLE).assertIsDisplayed() + } + + @Test + fun text_displayed() { + setAndOpenDialog { + rememberAlertDialogPresenter(text = { Text(TEXT) }) + } + + composeTestRule.onDialogText(TEXT).assertIsDisplayed() + } + + @Test + fun confirmButton_displayed() { + setAndOpenDialog { + rememberAlertDialogPresenter(confirmButton = AlertDialogButton(CONFIRM_TEXT)) + } + + composeTestRule.onDialogText(CONFIRM_TEXT).assertIsDisplayed() + } + + @Test + fun confirmButton_clickable() { + var confirmButtonClicked = false + setAndOpenDialog { + rememberAlertDialogPresenter(confirmButton = AlertDialogButton(CONFIRM_TEXT) { + confirmButtonClicked = true + }) + } + + composeTestRule.onDialogText(CONFIRM_TEXT).performClick() + + assertThat(confirmButtonClicked).isTrue() + } + + @Test + fun dismissButton_displayed() { + setAndOpenDialog { + rememberAlertDialogPresenter(dismissButton = AlertDialogButton(DISMISS_TEXT)) + } + + composeTestRule.onDialogText(DISMISS_TEXT).assertIsDisplayed() + } + + @Test + fun dismissButton_clickable() { + var dismissButtonClicked = false + setAndOpenDialog { + rememberAlertDialogPresenter(dismissButton = AlertDialogButton(DISMISS_TEXT) { + dismissButtonClicked = true + }) + } + + composeTestRule.onDialogText(DISMISS_TEXT).performClick() + + assertThat(dismissButtonClicked).isTrue() + } + + private fun setAndOpenDialog(dialog: @Composable () -> AlertDialogPresenter) { + composeTestRule.setContent { + val dialogPresenter = dialog() + LaunchedEffect(Unit) { + dialogPresenter.open() + } + } + } + + private companion object { + const val CONFIRM_TEXT = "Confirm" + const val DISMISS_TEXT = "Dismiss" + const val TITLE = "Title" + const val TEXT = "Text" + } +} |