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:
@@ -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,
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
@@ -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)
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user