Merge "Create AppForceStopRepository" into main

This commit is contained in:
Chaohui Wang
2024-05-22 05:53:39 +00:00
committed by Android (Google) Code Review
4 changed files with 288 additions and 55 deletions

View File

@@ -35,18 +35,14 @@ import com.android.settingslib.spa.widget.button.ActionButton
import com.android.settingslib.spa.widget.dialog.AlertDialogButton import com.android.settingslib.spa.widget.dialog.AlertDialogButton
import com.android.settingslib.spa.widget.dialog.AlertDialogPresenter import com.android.settingslib.spa.widget.dialog.AlertDialogPresenter
import com.android.settingslib.spa.widget.dialog.rememberAlertDialogPresenter 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 import com.android.settingslib.spaprivileged.model.app.userId
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
class AppForceStopButton( class AppForceStopButton(
private val packageInfoPresenter: PackageInfoPresenter, private val packageInfoPresenter: PackageInfoPresenter,
private val appForceStopRepository: AppForceStopRepository =
AppForceStopRepository(packageInfoPresenter),
) { ) {
private val context = packageInfoPresenter.context private val context = packageInfoPresenter.context
private val appButtonRepository = AppButtonRepository(context)
private val packageManager = context.packageManager private val packageManager = context.packageManager
@Composable @Composable
@@ -55,27 +51,11 @@ class AppForceStopButton(
return ActionButton( return ActionButton(
text = stringResource(R.string.force_stop), text = stringResource(R.string.force_stop),
imageVector = Icons.Outlined.Report, imageVector = Icons.Outlined.Report,
enabled = remember(app) { enabled = remember(app) { appForceStopRepository.canForceStopFlow() }
flow { .collectAsStateWithLifecycle(false).value,
emit(isForceStopButtonEnable(app))
}.flowOn(Dispatchers.Default)
}.collectAsStateWithLifecycle(false).value,
) { onForceStopButtonClicked(app, dialogPresenter) } ) { onForceStopButtonClicked(app, dialogPresenter) }
} }
/**
* Gets whether a package can be force stopped.
*/
private fun isForceStopButtonEnable(app: ApplicationInfo): Boolean = when {
// User can't force stop device admin.
app.isActiveAdmin(context) -> false
appButtonRepository.isDisallowControl(app) -> false
// If the app isn't explicitly stopped, then always show the force stop button.
else -> !app.hasFlag(ApplicationInfo.FLAG_STOPPED)
}
private fun onForceStopButtonClicked( private fun onForceStopButtonClicked(
app: ApplicationInfo, app: ApplicationInfo,
dialogPresenter: AlertDialogPresenter, dialogPresenter: AlertDialogPresenter,

View File

@@ -0,0 +1,115 @@
/*
* Copyright (C) 2024 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.Manifest
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.net.Uri
import android.os.UserHandle
import android.util.Log
import com.android.settingslib.spaprivileged.model.app.hasFlag
import com.android.settingslib.spaprivileged.model.app.isActiveAdmin
import com.android.settingslib.spaprivileged.model.app.userId
import kotlin.coroutines.resume
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.suspendCancellableCoroutine
class AppForceStopRepository(
private val packageInfoPresenter: PackageInfoPresenter,
private val appButtonRepository: AppButtonRepository =
AppButtonRepository(packageInfoPresenter.context),
) {
private val context = packageInfoPresenter.context
/**
* Flow of whether a package can be force stopped.
*/
fun canForceStopFlow(): Flow<Boolean> = packageInfoPresenter.flow
.map { packageInfo ->
val app = packageInfo?.applicationInfo ?: return@map false
canForceStop(app)
}
.conflate()
.onEach { Log.d(TAG, "canForceStopFlow: $it") }
.flowOn(Dispatchers.Default)
/**
* Gets whether a package can be force stopped.
*/
private suspend fun canForceStop(app: ApplicationInfo): Boolean = when {
// User can't force stop device admin.
app.isActiveAdmin(context) -> false
appButtonRepository.isDisallowControl(app) -> false
// If the app isn't explicitly stopped, then always show the force stop button.
!app.hasFlag(ApplicationInfo.FLAG_STOPPED) -> true
else -> queryAppRestart(app)
}
/**
* Queries if app has restarted.
*
* @return true means app can be force stop again.
*/
private suspend fun queryAppRestart(app: ApplicationInfo): Boolean {
val packageName = app.packageName
val intent = Intent(
Intent.ACTION_QUERY_PACKAGE_RESTART,
Uri.fromParts("package", packageName, null)
).apply {
putExtra(Intent.EXTRA_PACKAGES, arrayOf(packageName))
putExtra(Intent.EXTRA_UID, app.uid)
putExtra(Intent.EXTRA_USER_HANDLE, app.userId)
}
Log.d(TAG, "Sending broadcast to query restart status for $packageName")
return suspendCancellableCoroutine { continuation ->
val receiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val enabled = resultCode != Activity.RESULT_CANCELED
Log.d(TAG, "Got broadcast response: Restart status for $packageName $enabled")
continuation.resume(enabled)
}
}
context.sendOrderedBroadcastAsUser(
intent,
UserHandle.CURRENT,
Manifest.permission.HANDLE_QUERY_PACKAGE_RESTART,
receiver,
null,
Activity.RESULT_CANCELED,
null,
null,
)
}
}
private companion object {
private const val TAG = "AppForceStopRepository"
}
}

View File

@@ -29,6 +29,7 @@ import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.util.trace
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.settings.R import com.android.settings.R
@@ -36,10 +37,10 @@ import com.android.settingslib.spa.testutils.delay
import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager
import com.android.settingslib.spaprivileged.model.app.userId import com.android.settingslib.spaprivileged.model.app.userId
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
import org.junit.Rule 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.kotlin.any
import org.mockito.kotlin.doReturn import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import org.mockito.kotlin.spy import org.mockito.kotlin.spy
@@ -54,51 +55,30 @@ class AppForceStopButtonTest {
private val mockDevicePolicyManager = mock<DevicePolicyManager>() private val mockDevicePolicyManager = mock<DevicePolicyManager>()
private val mockUserManager = mock<UserManager> {
on { getUserRestrictionSources(any(), any()) } doReturn emptyList()
}
private val context: Context = spy(ApplicationProvider.getApplicationContext()) { private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
on { packageManager } doReturn mockPackageManager on { packageManager } doReturn mockPackageManager
on { devicePolicyManager } doReturn mockDevicePolicyManager on { devicePolicyManager } doReturn mockDevicePolicyManager
on { getSystemService(Context.DEVICE_POLICY_SERVICE) } doReturn mockDevicePolicyManager on { getSystemService(Context.DEVICE_POLICY_SERVICE) } doReturn mockDevicePolicyManager
on { getSystemService(Context.USER_SERVICE) } doReturn mockUserManager
} }
private val packageInfoPresenter = mock<PackageInfoPresenter> { private val packageInfoPresenter = mock<PackageInfoPresenter> {
on { context } doReturn context on { context } doReturn context
} }
private val appForceStopButton = AppForceStopButton(packageInfoPresenter) private val mockAppForceStopRepository = mock<AppForceStopRepository> {
on { canForceStopFlow() } doReturn flowOf(false)
@Test
fun getActionButton_isActiveAdmin_buttonDisabled() {
val app = createApp()
mockDevicePolicyManager.stub {
on { packageHasActiveAdmins(PACKAGE_NAME, app.userId) } doReturn true
}
setForceStopButton(app)
composeTestRule.onNodeWithText(context.getString(R.string.force_stop)).assertIsNotEnabled()
} }
@Test private val appForceStopButton = AppForceStopButton(
fun getActionButton_isUninstallInQueue_buttonDisabled() { packageInfoPresenter = packageInfoPresenter,
val app = createApp() appForceStopRepository = mockAppForceStopRepository,
mockDevicePolicyManager.stub { )
on { isUninstallInQueue(PACKAGE_NAME) } doReturn true
}
setForceStopButton(app)
composeTestRule.onNodeWithText(context.getString(R.string.force_stop)).assertIsNotEnabled()
}
@Test @Test
fun getActionButton_isStopped_buttonDisabled() { fun getActionButton_isStopped_buttonDisabled() {
val app = createApp { val app = createApp()
flags = ApplicationInfo.FLAG_STOPPED mockAppForceStopRepository.stub {
on { canForceStopFlow() } doReturn flowOf(false)
} }
setForceStopButton(app) setForceStopButton(app)
@@ -109,6 +89,9 @@ class AppForceStopButtonTest {
@Test @Test
fun getActionButton_regularApp_buttonEnabled() { fun getActionButton_regularApp_buttonEnabled() {
val app = createApp() val app = createApp()
mockAppForceStopRepository.stub {
on { canForceStopFlow() } doReturn flowOf(true)
}
setForceStopButton(app) setForceStopButton(app)

View File

@@ -0,0 +1,155 @@
/*
* Copyright (C) 2024 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.Manifest
import android.app.Activity
import android.app.admin.DevicePolicyManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.os.UserHandle
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.argThat
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.isNull
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.stub
@RunWith(AndroidJUnit4::class)
class AppForceStopRepositoryTest {
private val mockDevicePolicyManager = mock<DevicePolicyManager>()
private var resultCode = Activity.RESULT_CANCELED
private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
on { devicePolicyManager } doReturn mockDevicePolicyManager
onGeneric {
sendOrderedBroadcastAsUser(
argThat { action == Intent.ACTION_QUERY_PACKAGE_RESTART },
eq(UserHandle.CURRENT),
eq(Manifest.permission.HANDLE_QUERY_PACKAGE_RESTART),
any(),
isNull(),
eq(Activity.RESULT_CANCELED),
isNull(),
isNull(),
)
} doAnswer {
val broadcastReceiver = spy(it.arguments[3] as BroadcastReceiver) {
on { resultCode } doReturn resultCode
}
broadcastReceiver.onReceive(mock, it.arguments[0] as Intent)
}
}
private val packageInfoPresenter = mock<PackageInfoPresenter> {
on { context } doReturn context
}
private val repository = AppForceStopRepository(packageInfoPresenter)
@Test
fun getActionButton_isActiveAdmin_returnFalse() = runBlocking {
val app = mockApp {}
mockDevicePolicyManager.stub {
on { packageHasActiveAdmins(PACKAGE_NAME, app.userId) } doReturn true
}
val canForceStop = repository.canForceStopFlow().firstWithTimeoutOrNull()
assertThat(canForceStop).isFalse()
}
@Test
fun getActionButton_isUninstallInQueue_returnFalse() = runBlocking {
mockApp {}
mockDevicePolicyManager.stub {
on { isUninstallInQueue(PACKAGE_NAME) } doReturn true
}
val canForceStop = repository.canForceStopFlow().firstWithTimeoutOrNull()
assertThat(canForceStop).isFalse()
}
@Test
fun canForceStopFlow_notStopped_returnTrue() = runBlocking {
mockApp { flags = 0 }
val canForceStop = repository.canForceStopFlow().firstWithTimeoutOrNull()
assertThat(canForceStop).isTrue()
}
@Test
fun canForceStopFlow_isStoppedAndQueryReturnCancel_returnFalse() = runBlocking {
mockApp {
flags = ApplicationInfo.FLAG_STOPPED
}
resultCode = Activity.RESULT_CANCELED
val canForceStop = repository.canForceStopFlow().firstWithTimeoutOrNull()
assertThat(canForceStop).isFalse()
}
@Test
fun canForceStopFlow_isStoppedAndQueryReturnOk_returnTrue() = runBlocking {
mockApp {
flags = ApplicationInfo.FLAG_STOPPED
}
resultCode = Activity.RESULT_OK
val canForceStop = repository.canForceStopFlow().firstWithTimeoutOrNull()
assertThat(canForceStop).isTrue()
}
private fun mockApp(builder: ApplicationInfo.() -> Unit = {}) = packageInfoPresenter.stub {
on { flow } doReturn MutableStateFlow(PackageInfo().apply {
applicationInfo = createApp(builder)
})
}
private fun createApp(builder: ApplicationInfo.() -> Unit = {}) =
ApplicationInfo().apply {
packageName = PACKAGE_NAME
uid = UID
enabled = true
}.apply(builder)
private companion object {
const val PACKAGE_NAME = "package.name"
const val UID = 10000
}
}