Create AppForceStopRepository

Use Intent.ACTION_QUERY_PACKAGE_RESTART for more accurate app state.

Fix: 340150349
Test: manual - on App info
Test: unit test
Change-Id: I1fb634b3fbee200f2251afa37ef2cba88ff58fe0
This commit is contained in:
Chaohui Wang
2024-05-13 16:06:19 +08:00
parent 7b4b26b3af
commit 298b738171
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.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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
class AppForceStopButton(
private val packageInfoPresenter: PackageInfoPresenter,
private val appForceStopRepository: AppForceStopRepository =
AppForceStopRepository(packageInfoPresenter),
) {
private val context = packageInfoPresenter.context
private val appButtonRepository = AppButtonRepository(context)
private val packageManager = context.packageManager
@Composable
@@ -55,27 +51,11 @@ class AppForceStopButton(
return ActionButton(
text = stringResource(R.string.force_stop),
imageVector = Icons.Outlined.Report,
enabled = remember(app) {
flow {
emit(isForceStopButtonEnable(app))
}.flowOn(Dispatchers.Default)
}.collectAsStateWithLifecycle(false).value,
enabled = remember(app) { appForceStopRepository.canForceStopFlow() }
.collectAsStateWithLifecycle(false).value,
) { 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(
app: ApplicationInfo,
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.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.util.trace
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
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.model.app.userId
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
@@ -54,51 +55,30 @@ class AppForceStopButtonTest {
private val mockDevicePolicyManager = mock<DevicePolicyManager>()
private val mockUserManager = mock<UserManager> {
on { getUserRestrictionSources(any(), any()) } doReturn emptyList()
}
private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
on { packageManager } doReturn mockPackageManager
on { devicePolicyManager } doReturn mockDevicePolicyManager
on { getSystemService(Context.DEVICE_POLICY_SERVICE) } doReturn mockDevicePolicyManager
on { getSystemService(Context.USER_SERVICE) } doReturn mockUserManager
}
private val packageInfoPresenter = mock<PackageInfoPresenter> {
on { context } doReturn context
}
private val appForceStopButton = AppForceStopButton(packageInfoPresenter)
@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()
private val mockAppForceStopRepository = mock<AppForceStopRepository> {
on { canForceStopFlow() } doReturn flowOf(false)
}
@Test
fun getActionButton_isUninstallInQueue_buttonDisabled() {
val app = createApp()
mockDevicePolicyManager.stub {
on { isUninstallInQueue(PACKAGE_NAME) } doReturn true
}
setForceStopButton(app)
composeTestRule.onNodeWithText(context.getString(R.string.force_stop)).assertIsNotEnabled()
}
private val appForceStopButton = AppForceStopButton(
packageInfoPresenter = packageInfoPresenter,
appForceStopRepository = mockAppForceStopRepository,
)
@Test
fun getActionButton_isStopped_buttonDisabled() {
val app = createApp {
flags = ApplicationInfo.FLAG_STOPPED
val app = createApp()
mockAppForceStopRepository.stub {
on { canForceStopFlow() } doReturn flowOf(false)
}
setForceStopButton(app)
@@ -109,6 +89,9 @@ class AppForceStopButtonTest {
@Test
fun getActionButton_regularApp_buttonEnabled() {
val app = createApp()
mockAppForceStopRepository.stub {
on { canForceStopFlow() } doReturn flowOf(true)
}
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
}
}