From 750c6072b9e3b960ecf02ed630f1ff5bdc3cb89c Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 31 Jan 2023 17:42:18 +0800 Subject: [PATCH] Use SettingsAlertDialog for app button dialogs Use the spa standard widget SettingsAlertDialog to unify the dialog style. Bug: 236346018 Test: Manually with Settings Test: Unit test Change-Id: Idb231600e38ec7b0244baa5101da912ed2b9fd3c --- .../settings/spa/app/appinfo/AppButtons.kt | 21 +-- .../spa/app/appinfo/AppClearButton.kt | 57 +++----- .../spa/app/appinfo/AppDisableButton.kt | 64 ++++----- .../spa/app/appinfo/AppForceStopButton.kt | 57 +++----- .../spa/app/appinfo/AppDisableButtonTest.kt | 25 +++- .../spa/app/appinfo/AppForceStopButtonTest.kt | 132 ++++++++++++++++++ 6 files changed, 222 insertions(+), 134 deletions(-) create mode 100644 tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppForceStopButtonTest.kt diff --git a/src/com/android/settings/spa/app/appinfo/AppButtons.kt b/src/com/android/settings/spa/app/appinfo/AppButtons.kt index 831149132f2..e43c673324b 100644 --- a/src/com/android/settings/spa/app/appinfo/AppButtons.kt +++ b/src/com/android/settings/spa/app/appinfo/AppButtons.kt @@ -24,14 +24,12 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.settingslib.applications.AppUtils import com.android.settingslib.spa.widget.button.ActionButton import com.android.settingslib.spa.widget.button.ActionButtons -import kotlinx.coroutines.flow.map @Composable fun AppButtons(packageInfoPresenter: PackageInfoPresenter) { if (remember(packageInfoPresenter) { packageInfoPresenter.isMainlineModule() }) return val presenter = remember { AppButtonsPresenter(packageInfoPresenter) } - presenter.Dialogs() - ActionButtons(actionButtons = presenter.rememberActionsButtons().value) + ActionButtons(actionButtons = presenter.getActionButtons()) } private fun PackageInfoPresenter.isMainlineModule(): Boolean = @@ -47,12 +45,12 @@ private class AppButtonsPresenter(private val packageInfoPresenter: PackageInfoP @OptIn(ExperimentalLifecycleComposeApi::class) @Composable - fun rememberActionsButtons() = remember { - packageInfoPresenter.flow.map { packageInfo -> - if (packageInfo != null) getActionButtons(packageInfo.applicationInfo) else emptyList() - } - }.collectAsStateWithLifecycle(initialValue = emptyList()) + fun getActionButtons() = + packageInfoPresenter.flow.collectAsStateWithLifecycle(initialValue = null).value?.let { + getActionButtons(it.applicationInfo) + } ?: emptyList() + @Composable private fun getActionButtons(app: ApplicationInfo): List = listOfNotNull( appLaunchButton.getActionButton(app), appInstallButton.getActionButton(app), @@ -61,11 +59,4 @@ private class AppButtonsPresenter(private val packageInfoPresenter: PackageInfoP appClearButton.getActionButton(app), appForceStopButton.getActionButton(app), ) - - @Composable - fun Dialogs() { - appDisableButton.DisableConfirmDialog() - appClearButton.ClearConfirmDialog() - appForceStopButton.ForceStopConfirmDialog() - } } diff --git a/src/com/android/settings/spa/app/appinfo/AppClearButton.kt b/src/com/android/settings/spa/app/appinfo/AppClearButton.kt index ce00b73eed2..22a93c1dcdb 100644 --- a/src/com/android/settings/spa/app/appinfo/AppClearButton.kt +++ b/src/com/android/settings/spa/app/appinfo/AppClearButton.kt @@ -19,61 +19,44 @@ package com.android.settings.spa.app.appinfo import android.content.pm.ApplicationInfo import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Delete -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.setValue import androidx.compose.ui.res.stringResource import com.android.settings.R import com.android.settingslib.spa.widget.button.ActionButton +import com.android.settingslib.spa.widget.dialog.AlertDialogButton +import com.android.settingslib.spa.widget.dialog.rememberAlertDialogPresenter class AppClearButton( private val packageInfoPresenter: PackageInfoPresenter, ) { private val context = packageInfoPresenter.context - private var openConfirmDialog by mutableStateOf(false) - + @Composable fun getActionButton(app: ApplicationInfo): ActionButton? { if (!app.isInstantApp) return null return clearButton() } - private fun clearButton() = ActionButton( - text = context.getString(R.string.clear_instant_app_data), - imageVector = Icons.Outlined.Delete, - ) { openConfirmDialog = true } - @Composable - fun ClearConfirmDialog() { - if (!openConfirmDialog) return - AlertDialog( - onDismissRequest = { openConfirmDialog = false }, - confirmButton = { - TextButton( - onClick = { - openConfirmDialog = false - packageInfoPresenter.clearInstantApp() - }, - ) { - Text(stringResource(R.string.clear_instant_app_data)) - } - }, - dismissButton = { - TextButton(onClick = { openConfirmDialog = false }) { - Text(stringResource(R.string.cancel)) - } - }, - title = { - Text(stringResource(R.string.clear_instant_app_data)) - }, - text = { - Text(stringResource(R.string.clear_instant_app_confirmation)) - }, + private fun clearButton(): ActionButton { + val dialogPresenter = confirmDialogPresenter() + return ActionButton( + text = context.getString(R.string.clear_instant_app_data), + imageVector = Icons.Outlined.Delete, + onClick = dialogPresenter::open, ) } + + @Composable + private fun confirmDialogPresenter() = rememberAlertDialogPresenter( + confirmButton = AlertDialogButton( + text = stringResource(R.string.clear_instant_app_data), + onClick = packageInfoPresenter::clearInstantApp, + ), + dismissButton = AlertDialogButton(stringResource(R.string.cancel)), + title = stringResource(R.string.clear_instant_app_data), + text = { Text(stringResource(R.string.clear_instant_app_confirmation)) }, + ) } diff --git a/src/com/android/settings/spa/app/appinfo/AppDisableButton.kt b/src/com/android/settings/spa/app/appinfo/AppDisableButton.kt index 05b9706e7d4..450f984b8b9 100644 --- a/src/com/android/settings/spa/app/appinfo/AppDisableButton.kt +++ b/src/com/android/settings/spa/app/appinfo/AppDisableButton.kt @@ -20,18 +20,15 @@ import android.content.pm.ApplicationInfo import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ArrowCircleDown import androidx.compose.material.icons.outlined.HideSource -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.setValue import androidx.compose.ui.res.stringResource import com.android.settings.R import com.android.settings.Utils import com.android.settings.overlay.FeatureFactory import com.android.settingslib.spa.widget.button.ActionButton +import com.android.settingslib.spa.widget.dialog.AlertDialogButton +import com.android.settingslib.spa.widget.dialog.rememberAlertDialogPresenter import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settingslib.spaprivileged.model.app.isDisabledUntilUsed @@ -49,8 +46,7 @@ class AppDisableButton( private val applicationFeatureProvider = FeatureFactory.getFactory(context).getApplicationFeatureProvider(context) - private var openConfirmDialog by mutableStateOf(false) - + @Composable fun getActionButton(app: ApplicationInfo): ActionButton? { if (!app.isSystemApp) return null @@ -92,14 +88,19 @@ class AppDisableButton( else -> true } - private fun disableButton(app: ApplicationInfo) = ActionButton( - text = context.getString(R.string.disable_text), - imageVector = Icons.Outlined.HideSource, - enabled = app.canBeDisabled(), - ) { - // Currently we apply the same device policy for both the uninstallation and disable button. - if (!appButtonRepository.isUninstallBlockedByAdmin(app)) { - openConfirmDialog = true + @Composable + private fun disableButton(app: ApplicationInfo): ActionButton { + val dialogPresenter = confirmDialogPresenter() + return ActionButton( + text = context.getString(R.string.disable_text), + imageVector = Icons.Outlined.HideSource, + enabled = app.canBeDisabled(), + ) { + // Currently we apply the same device policy for both the uninstallation and disable + // button. + if (!appButtonRepository.isUninstallBlockedByAdmin(app)) { + dialogPresenter.open() + } } } @@ -109,28 +110,13 @@ class AppDisableButton( ) { packageInfoPresenter.enable() } @Composable - fun DisableConfirmDialog() { - if (!openConfirmDialog) return - AlertDialog( - onDismissRequest = { openConfirmDialog = false }, - confirmButton = { - TextButton( - onClick = { - openConfirmDialog = false - packageInfoPresenter.disable() - }, - ) { - Text(stringResource(R.string.app_disable_dlg_positive)) - } - }, - dismissButton = { - TextButton(onClick = { openConfirmDialog = false }) { - Text(stringResource(R.string.cancel)) - } - }, - text = { - Text(stringResource(R.string.app_disable_dlg_text)) - }, - ) - } + private fun confirmDialogPresenter() = rememberAlertDialogPresenter( + confirmButton = AlertDialogButton( + text = stringResource(R.string.reset_app_preferences_button), + onClick = packageInfoPresenter::disable, + ), + dismissButton = AlertDialogButton(stringResource(R.string.cancel)), + title = stringResource(R.string.app_disable_dlg_positive), + text = { Text(stringResource(R.string.app_disable_dlg_text)) }, + ) } diff --git a/src/com/android/settings/spa/app/appinfo/AppForceStopButton.kt b/src/com/android/settings/spa/app/appinfo/AppForceStopButton.kt index d05c8321074..086f59e82de 100644 --- a/src/com/android/settings/spa/app/appinfo/AppForceStopButton.kt +++ b/src/com/android/settings/spa/app/appinfo/AppForceStopButton.kt @@ -21,19 +21,17 @@ import android.content.pm.ApplicationInfo import android.os.UserManager import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.WarningAmber -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.setValue import androidx.compose.ui.res.stringResource import com.android.settings.R import com.android.settingslib.RestrictedLockUtils import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin import com.android.settingslib.RestrictedLockUtilsInternal import com.android.settingslib.spa.widget.button.ActionButton +import com.android.settingslib.spa.widget.dialog.AlertDialogButton +import com.android.settingslib.spa.widget.dialog.AlertDialogPresenter +import com.android.settingslib.spa.widget.dialog.rememberAlertDialogPresenter import com.android.settingslib.spaprivileged.model.app.hasFlag import com.android.settingslib.spaprivileged.model.app.isActiveAdmin import com.android.settingslib.spaprivileged.model.app.userId @@ -45,14 +43,14 @@ class AppForceStopButton( private val appButtonRepository = AppButtonRepository(context) private val packageManager = context.packageManager - private var openConfirmDialog by mutableStateOf(false) - + @Composable fun getActionButton(app: ApplicationInfo): ActionButton { + val dialogPresenter = confirmDialogPresenter() return ActionButton( text = context.getString(R.string.force_stop), imageVector = Icons.Outlined.WarningAmber, enabled = isForceStopButtonEnable(app), - ) { onForceStopButtonClicked(app) } + ) { onForceStopButtonClicked(app, dialogPresenter) } } /** @@ -68,13 +66,16 @@ class AppForceStopButton( else -> !app.hasFlag(ApplicationInfo.FLAG_STOPPED) } - private fun onForceStopButtonClicked(app: ApplicationInfo) { + private fun onForceStopButtonClicked( + app: ApplicationInfo, + dialogPresenter: AlertDialogPresenter, + ) { packageInfoPresenter.logAction(SettingsEnums.ACTION_APP_INFO_FORCE_STOP) getAdminRestriction(app)?.let { admin -> RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, admin) return } - openConfirmDialog = true + dialogPresenter.open() } private fun getAdminRestriction(app: ApplicationInfo): EnforcedAdmin? = when { @@ -88,31 +89,13 @@ class AppForceStopButton( } @Composable - fun ForceStopConfirmDialog() { - if (!openConfirmDialog) return - AlertDialog( - onDismissRequest = { openConfirmDialog = false }, - confirmButton = { - TextButton( - onClick = { - openConfirmDialog = false - packageInfoPresenter.forceStop() - }, - ) { - Text(stringResource(R.string.okay)) - } - }, - dismissButton = { - TextButton(onClick = { openConfirmDialog = false }) { - Text(stringResource(R.string.cancel)) - } - }, - title = { - Text(stringResource(R.string.force_stop_dlg_title)) - }, - text = { - Text(stringResource(R.string.force_stop_dlg_text)) - }, - ) - } + private fun confirmDialogPresenter() = rememberAlertDialogPresenter( + confirmButton = AlertDialogButton( + text = stringResource(R.string.okay), + onClick = packageInfoPresenter::forceStop, + ), + dismissButton = AlertDialogButton(stringResource(R.string.cancel)), + title = stringResource(R.string.force_stop_dlg_title), + text = { Text(stringResource(R.string.force_stop_dlg_text)) }, + ) } diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppDisableButtonTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppDisableButtonTest.kt index beb6abc592d..f599baeef83 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppDisableButtonTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppDisableButtonTest.kt @@ -21,16 +21,19 @@ import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.os.UserManager +import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.settings.Utils import com.android.settings.testutils.FakeFeatureFactory +import com.android.settingslib.spa.widget.button.ActionButton import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager import com.android.settingslib.spaprivileged.framework.common.userManager import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock @@ -42,6 +45,8 @@ import org.mockito.Mockito.`when` as whenever @RunWith(AndroidJUnit4::class) class AppDisableButtonTest { + @get:Rule + val composeTestRule = createComposeRule() private lateinit var mockSession: MockitoSession @@ -97,7 +102,7 @@ class AppDisableButtonTest { privateFlags = privateFlags or ApplicationInfo.PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY } - val actionButton = appDisableButton.getActionButton(app)!! + val actionButton = setDisableButton(app) assertThat(actionButton.enabled).isFalse() } @@ -108,7 +113,7 @@ class AppDisableButtonTest { privateFlags = privateFlags or ApplicationInfo.PRIVATE_FLAG_IS_RESOURCE_OVERLAY } - val actionButton = appDisableButton.getActionButton(app)!! + val actionButton = setDisableButton(app) assertThat(actionButton.enabled).isFalse() } @@ -118,7 +123,7 @@ class AppDisableButtonTest { whenever(appFeatureProvider.keepEnabledPackages).thenReturn(setOf(PACKAGE_NAME)) val app = enabledSystemApp() - val actionButton = appDisableButton.getActionButton(app)!! + val actionButton = setDisableButton(app) assertThat(actionButton.enabled).isFalse() } @@ -130,7 +135,7 @@ class AppDisableButtonTest { ).thenReturn(true) val app = enabledSystemApp() - val actionButton = appDisableButton.getActionButton(app)!! + val actionButton = setDisableButton(app) assertThat(actionButton.enabled).isFalse() } @@ -141,7 +146,7 @@ class AppDisableButtonTest { .thenReturn(true) val app = enabledSystemApp() - val actionButton = appDisableButton.getActionButton(app)!! + val actionButton = setDisableButton(app) assertThat(actionButton.enabled).isFalse() } @@ -150,11 +155,19 @@ class AppDisableButtonTest { fun getActionButton_regularEnabledSystemApp_canDisable() { val app = enabledSystemApp() - val actionButton = appDisableButton.getActionButton(app)!! + val actionButton = setDisableButton(app) assertThat(actionButton.enabled).isTrue() } + private fun setDisableButton(app: ApplicationInfo): ActionButton { + lateinit var actionButton: ActionButton + composeTestRule.setContent { + actionButton = appDisableButton.getActionButton(app)!! + } + return actionButton + } + private fun enabledSystemApp(builder: ApplicationInfo.() -> Unit = {}) = ApplicationInfo().apply { packageName = PACKAGE_NAME diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppForceStopButtonTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppForceStopButtonTest.kt new file mode 100644 index 00000000000..c093863bcb1 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppForceStopButtonTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2023 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.settings.spa.app.appinfo + +import android.app.admin.DevicePolicyManager +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.widget.button.ActionButton +import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager +import com.android.settingslib.spaprivileged.model.app.userId +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.Mockito.`when` as whenever + +@RunWith(AndroidJUnit4::class) +class AppForceStopButtonTest { + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val mockito: MockitoRule = MockitoJUnit.rule() + + @Spy + private val context: Context = ApplicationProvider.getApplicationContext() + + @Mock + private lateinit var packageInfoPresenter: PackageInfoPresenter + + @Mock + private lateinit var packageManager: PackageManager + + @Mock + private lateinit var devicePolicyManager: DevicePolicyManager + + private lateinit var appForceStopButton: AppForceStopButton + + @Before + fun setUp() { + whenever(packageInfoPresenter.context).thenReturn(context) + whenever(context.packageManager).thenReturn(packageManager) + whenever(context.devicePolicyManager).thenReturn(devicePolicyManager) + appForceStopButton = AppForceStopButton(packageInfoPresenter) + } + + @Test + fun getActionButton() { + } + + @Test + fun getActionButton_isActiveAdmin_buttonDisabled() { + val app = createApp() + whenever(devicePolicyManager.packageHasActiveAdmins(PACKAGE_NAME, app.userId)) + .thenReturn(true) + + val actionButton = setForceStopButton(app) + + assertThat(actionButton.enabled).isFalse() + } + + @Test + fun getActionButton_isUninstallInQueue_buttonDisabled() { + val app = createApp() + whenever(devicePolicyManager.isUninstallInQueue(PACKAGE_NAME)).thenReturn(true) + + val actionButton = setForceStopButton(app) + + assertThat(actionButton.enabled).isFalse() + } + + @Test + fun getActionButton_isStopped_buttonDisabled() { + val app = createApp { + flags = ApplicationInfo.FLAG_STOPPED + } + + val actionButton = setForceStopButton(app) + + assertThat(actionButton.enabled).isFalse() + } + + @Test + fun getActionButton_regularApp_buttonEnabled() { + val app = createApp() + + val actionButton = setForceStopButton(app) + + assertThat(actionButton.enabled).isTrue() + } + + private fun setForceStopButton(app: ApplicationInfo): ActionButton { + lateinit var actionButton: ActionButton + composeTestRule.setContent { + actionButton = appForceStopButton.getActionButton(app) + } + return actionButton + } + + private fun createApp(builder: ApplicationInfo.() -> Unit = {}) = + ApplicationInfo().apply { + packageName = PACKAGE_NAME + enabled = true + }.apply(builder) + + private companion object { + const val PACKAGE_NAME = "package.name" + } +} \ No newline at end of file