Merge "Create AppForceStopRepository" into main
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.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,
|
||||||
|
@@ -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.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)
|
||||||
|
|
||||||
|
@@ -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