Merge "Use SettingsAlertDialog for app button dialogs"

This commit is contained in:
Chaohui Wang
2023-02-01 02:43:24 +00:00
committed by Android (Google) Code Review
6 changed files with 222 additions and 134 deletions

View File

@@ -24,14 +24,12 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settingslib.applications.AppUtils import com.android.settingslib.applications.AppUtils
import com.android.settingslib.spa.widget.button.ActionButton import com.android.settingslib.spa.widget.button.ActionButton
import com.android.settingslib.spa.widget.button.ActionButtons import com.android.settingslib.spa.widget.button.ActionButtons
import kotlinx.coroutines.flow.map
@Composable @Composable
fun AppButtons(packageInfoPresenter: PackageInfoPresenter) { fun AppButtons(packageInfoPresenter: PackageInfoPresenter) {
if (remember(packageInfoPresenter) { packageInfoPresenter.isMainlineModule() }) return if (remember(packageInfoPresenter) { packageInfoPresenter.isMainlineModule() }) return
val presenter = remember { AppButtonsPresenter(packageInfoPresenter) } val presenter = remember { AppButtonsPresenter(packageInfoPresenter) }
presenter.Dialogs() ActionButtons(actionButtons = presenter.getActionButtons())
ActionButtons(actionButtons = presenter.rememberActionsButtons().value)
} }
private fun PackageInfoPresenter.isMainlineModule(): Boolean = private fun PackageInfoPresenter.isMainlineModule(): Boolean =
@@ -47,12 +45,12 @@ private class AppButtonsPresenter(private val packageInfoPresenter: PackageInfoP
@OptIn(ExperimentalLifecycleComposeApi::class) @OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
fun rememberActionsButtons() = remember { fun getActionButtons() =
packageInfoPresenter.flow.map { packageInfo -> packageInfoPresenter.flow.collectAsStateWithLifecycle(initialValue = null).value?.let {
if (packageInfo != null) getActionButtons(packageInfo.applicationInfo) else emptyList() getActionButtons(it.applicationInfo)
} } ?: emptyList()
}.collectAsStateWithLifecycle(initialValue = emptyList())
@Composable
private fun getActionButtons(app: ApplicationInfo): List<ActionButton> = listOfNotNull( private fun getActionButtons(app: ApplicationInfo): List<ActionButton> = listOfNotNull(
appLaunchButton.getActionButton(app), appLaunchButton.getActionButton(app),
appInstallButton.getActionButton(app), appInstallButton.getActionButton(app),
@@ -61,11 +59,4 @@ private class AppButtonsPresenter(private val packageInfoPresenter: PackageInfoP
appClearButton.getActionButton(app), appClearButton.getActionButton(app),
appForceStopButton.getActionButton(app), appForceStopButton.getActionButton(app),
) )
@Composable
fun Dialogs() {
appDisableButton.DisableConfirmDialog()
appClearButton.ClearConfirmDialog()
appForceStopButton.ForceStopConfirmDialog()
}
} }

View File

@@ -19,61 +19,44 @@ package com.android.settings.spa.app.appinfo
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable 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 androidx.compose.ui.res.stringResource
import com.android.settings.R import com.android.settings.R
import com.android.settingslib.spa.widget.button.ActionButton 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( class AppClearButton(
private val packageInfoPresenter: PackageInfoPresenter, private val packageInfoPresenter: PackageInfoPresenter,
) { ) {
private val context = packageInfoPresenter.context private val context = packageInfoPresenter.context
private var openConfirmDialog by mutableStateOf(false) @Composable
fun getActionButton(app: ApplicationInfo): ActionButton? { fun getActionButton(app: ApplicationInfo): ActionButton? {
if (!app.isInstantApp) return null if (!app.isInstantApp) return null
return clearButton() return clearButton()
} }
private fun clearButton() = ActionButton( @Composable
private fun clearButton(): ActionButton {
val dialogPresenter = confirmDialogPresenter()
return ActionButton(
text = context.getString(R.string.clear_instant_app_data), text = context.getString(R.string.clear_instant_app_data),
imageVector = Icons.Outlined.Delete, imageVector = Icons.Outlined.Delete,
) { openConfirmDialog = true } onClick = dialogPresenter::open,
@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))
},
) )
} }
@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)) },
)
} }

View File

@@ -20,18 +20,15 @@ import android.content.pm.ApplicationInfo
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowCircleDown import androidx.compose.material.icons.outlined.ArrowCircleDown
import androidx.compose.material.icons.outlined.HideSource import androidx.compose.material.icons.outlined.HideSource
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable 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 androidx.compose.ui.res.stringResource
import com.android.settings.R import com.android.settings.R
import com.android.settings.Utils import com.android.settings.Utils
import com.android.settings.overlay.FeatureFactory import com.android.settings.overlay.FeatureFactory
import com.android.settingslib.spa.widget.button.ActionButton 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.devicePolicyManager
import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settingslib.spaprivileged.framework.common.userManager
import com.android.settingslib.spaprivileged.model.app.isDisabledUntilUsed import com.android.settingslib.spaprivileged.model.app.isDisabledUntilUsed
@@ -49,8 +46,7 @@ class AppDisableButton(
private val applicationFeatureProvider = private val applicationFeatureProvider =
FeatureFactory.getFactory(context).getApplicationFeatureProvider(context) FeatureFactory.getFactory(context).getApplicationFeatureProvider(context)
private var openConfirmDialog by mutableStateOf(false) @Composable
fun getActionButton(app: ApplicationInfo): ActionButton? { fun getActionButton(app: ApplicationInfo): ActionButton? {
if (!app.isSystemApp) return null if (!app.isSystemApp) return null
@@ -92,14 +88,19 @@ class AppDisableButton(
else -> true else -> true
} }
private fun disableButton(app: ApplicationInfo) = ActionButton( @Composable
private fun disableButton(app: ApplicationInfo): ActionButton {
val dialogPresenter = confirmDialogPresenter()
return ActionButton(
text = context.getString(R.string.disable_text), text = context.getString(R.string.disable_text),
imageVector = Icons.Outlined.HideSource, imageVector = Icons.Outlined.HideSource,
enabled = app.canBeDisabled(), enabled = app.canBeDisabled(),
) { ) {
// Currently we apply the same device policy for both the uninstallation and disable button. // Currently we apply the same device policy for both the uninstallation and disable
// button.
if (!appButtonRepository.isUninstallBlockedByAdmin(app)) { if (!appButtonRepository.isUninstallBlockedByAdmin(app)) {
openConfirmDialog = true dialogPresenter.open()
}
} }
} }
@@ -109,28 +110,13 @@ class AppDisableButton(
) { packageInfoPresenter.enable() } ) { packageInfoPresenter.enable() }
@Composable @Composable
fun DisableConfirmDialog() { private fun confirmDialogPresenter() = rememberAlertDialogPresenter(
if (!openConfirmDialog) return confirmButton = AlertDialogButton(
AlertDialog( text = stringResource(R.string.reset_app_preferences_button),
onDismissRequest = { openConfirmDialog = false }, onClick = packageInfoPresenter::disable,
confirmButton = { ),
TextButton( dismissButton = AlertDialogButton(stringResource(R.string.cancel)),
onClick = { title = stringResource(R.string.app_disable_dlg_positive),
openConfirmDialog = false text = { Text(stringResource(R.string.app_disable_dlg_text)) },
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))
},
) )
}
} }

View File

@@ -21,19 +21,17 @@ import android.content.pm.ApplicationInfo
import android.os.UserManager import android.os.UserManager
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.WarningAmber import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable 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 androidx.compose.ui.res.stringResource
import com.android.settings.R import com.android.settings.R
import com.android.settingslib.RestrictedLockUtils import com.android.settingslib.RestrictedLockUtils
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
import com.android.settingslib.RestrictedLockUtilsInternal import com.android.settingslib.RestrictedLockUtilsInternal
import com.android.settingslib.spa.widget.button.ActionButton 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.hasFlag
import com.android.settingslib.spaprivileged.model.app.isActiveAdmin import com.android.settingslib.spaprivileged.model.app.isActiveAdmin
import com.android.settingslib.spaprivileged.model.app.userId import com.android.settingslib.spaprivileged.model.app.userId
@@ -45,14 +43,14 @@ class AppForceStopButton(
private val appButtonRepository = AppButtonRepository(context) private val appButtonRepository = AppButtonRepository(context)
private val packageManager = context.packageManager private val packageManager = context.packageManager
private var openConfirmDialog by mutableStateOf(false) @Composable
fun getActionButton(app: ApplicationInfo): ActionButton { fun getActionButton(app: ApplicationInfo): ActionButton {
val dialogPresenter = confirmDialogPresenter()
return ActionButton( return ActionButton(
text = context.getString(R.string.force_stop), text = context.getString(R.string.force_stop),
imageVector = Icons.Outlined.WarningAmber, imageVector = Icons.Outlined.WarningAmber,
enabled = isForceStopButtonEnable(app), enabled = isForceStopButtonEnable(app),
) { onForceStopButtonClicked(app) } ) { onForceStopButtonClicked(app, dialogPresenter) }
} }
/** /**
@@ -68,13 +66,16 @@ class AppForceStopButton(
else -> !app.hasFlag(ApplicationInfo.FLAG_STOPPED) 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) packageInfoPresenter.logAction(SettingsEnums.ACTION_APP_INFO_FORCE_STOP)
getAdminRestriction(app)?.let { admin -> getAdminRestriction(app)?.let { admin ->
RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, admin) RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, admin)
return return
} }
openConfirmDialog = true dialogPresenter.open()
} }
private fun getAdminRestriction(app: ApplicationInfo): EnforcedAdmin? = when { private fun getAdminRestriction(app: ApplicationInfo): EnforcedAdmin? = when {
@@ -88,31 +89,13 @@ class AppForceStopButton(
} }
@Composable @Composable
fun ForceStopConfirmDialog() { private fun confirmDialogPresenter() = rememberAlertDialogPresenter(
if (!openConfirmDialog) return confirmButton = AlertDialogButton(
AlertDialog( text = stringResource(R.string.okay),
onDismissRequest = { openConfirmDialog = false }, onClick = packageInfoPresenter::forceStop,
confirmButton = { ),
TextButton( dismissButton = AlertDialogButton(stringResource(R.string.cancel)),
onClick = { title = stringResource(R.string.force_stop_dlg_title),
openConfirmDialog = false text = { Text(stringResource(R.string.force_stop_dlg_text)) },
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))
},
) )
}
} }

View File

@@ -21,16 +21,19 @@ import android.content.Context
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.UserManager import android.os.UserManager
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.dx.mockito.inline.extended.ExtendedMockito
import com.android.settings.Utils import com.android.settings.Utils
import com.android.settings.testutils.FakeFeatureFactory 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.devicePolicyManager
import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settingslib.spaprivileged.framework.common.userManager
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.Mock import org.mockito.Mock
@@ -42,6 +45,8 @@ import org.mockito.Mockito.`when` as whenever
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class AppDisableButtonTest { class AppDisableButtonTest {
@get:Rule
val composeTestRule = createComposeRule()
private lateinit var mockSession: MockitoSession private lateinit var mockSession: MockitoSession
@@ -97,7 +102,7 @@ class AppDisableButtonTest {
privateFlags = privateFlags or ApplicationInfo.PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY privateFlags = privateFlags or ApplicationInfo.PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY
} }
val actionButton = appDisableButton.getActionButton(app)!! val actionButton = setDisableButton(app)
assertThat(actionButton.enabled).isFalse() assertThat(actionButton.enabled).isFalse()
} }
@@ -108,7 +113,7 @@ class AppDisableButtonTest {
privateFlags = privateFlags or ApplicationInfo.PRIVATE_FLAG_IS_RESOURCE_OVERLAY privateFlags = privateFlags or ApplicationInfo.PRIVATE_FLAG_IS_RESOURCE_OVERLAY
} }
val actionButton = appDisableButton.getActionButton(app)!! val actionButton = setDisableButton(app)
assertThat(actionButton.enabled).isFalse() assertThat(actionButton.enabled).isFalse()
} }
@@ -118,7 +123,7 @@ class AppDisableButtonTest {
whenever(appFeatureProvider.keepEnabledPackages).thenReturn(setOf(PACKAGE_NAME)) whenever(appFeatureProvider.keepEnabledPackages).thenReturn(setOf(PACKAGE_NAME))
val app = enabledSystemApp() val app = enabledSystemApp()
val actionButton = appDisableButton.getActionButton(app)!! val actionButton = setDisableButton(app)
assertThat(actionButton.enabled).isFalse() assertThat(actionButton.enabled).isFalse()
} }
@@ -130,7 +135,7 @@ class AppDisableButtonTest {
).thenReturn(true) ).thenReturn(true)
val app = enabledSystemApp() val app = enabledSystemApp()
val actionButton = appDisableButton.getActionButton(app)!! val actionButton = setDisableButton(app)
assertThat(actionButton.enabled).isFalse() assertThat(actionButton.enabled).isFalse()
} }
@@ -141,7 +146,7 @@ class AppDisableButtonTest {
.thenReturn(true) .thenReturn(true)
val app = enabledSystemApp() val app = enabledSystemApp()
val actionButton = appDisableButton.getActionButton(app)!! val actionButton = setDisableButton(app)
assertThat(actionButton.enabled).isFalse() assertThat(actionButton.enabled).isFalse()
} }
@@ -150,11 +155,19 @@ class AppDisableButtonTest {
fun getActionButton_regularEnabledSystemApp_canDisable() { fun getActionButton_regularEnabledSystemApp_canDisable() {
val app = enabledSystemApp() val app = enabledSystemApp()
val actionButton = appDisableButton.getActionButton(app)!! val actionButton = setDisableButton(app)
assertThat(actionButton.enabled).isTrue() 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 = {}) = private fun enabledSystemApp(builder: ApplicationInfo.() -> Unit = {}) =
ApplicationInfo().apply { ApplicationInfo().apply {
packageName = PACKAGE_NAME packageName = PACKAGE_NAME

View File

@@ -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"
}
}