No show AppButtons for system modules

To try:
1. adb shell am start -n com.android.settings/.spa.SpaActivity
2. Go to Apps -> All apps (Show system) -> Bluetooth

Bug: 236346018
Test: Unit test & Manual with Settings App
Change-Id: Ibdf5f1ec9f69beefe47fb7a046b0192a73e71b27
This commit is contained in:
Chaohui Wang
2022-10-28 15:26:02 +08:00
parent c972af0ca7
commit 56c9bfed08
5 changed files with 165 additions and 10 deletions

View File

@@ -20,18 +20,25 @@ import android.content.pm.PackageInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import com.android.settingslib.applications.AppUtils
import com.android.settingslib.spa.framework.compose.collectAsStateWithLifecycle
import com.android.settingslib.spa.widget.button.ActionButton
import com.android.settingslib.spa.widget.button.ActionButtons
import com.android.settingslib.spaprivileged.model.app.isSystemModule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
@Composable
fun AppButtons(packageInfoPresenter: PackageInfoPresenter) {
val appButtonsHolder = remember { AppButtonsHolder(packageInfoPresenter) }
appButtonsHolder.Dialogs()
ActionButtons(actionButtons = appButtonsHolder.rememberActionsButtons().value)
val presenter = remember { AppButtonsPresenter(packageInfoPresenter) }
if (!presenter.isAvailableFlow.collectAsStateWithLifecycle(initialValue = false).value) return
presenter.Dialogs()
ActionButtons(actionButtons = presenter.rememberActionsButtons().value)
}
private class AppButtonsHolder(private val packageInfoPresenter: PackageInfoPresenter) {
private class AppButtonsPresenter(private val packageInfoPresenter: PackageInfoPresenter) {
private val appLaunchButton = AppLaunchButton(packageInfoPresenter)
private val appInstallButton = AppInstallButton(packageInfoPresenter)
private val appDisableButton = AppDisableButton(packageInfoPresenter)
@@ -39,6 +46,15 @@ private class AppButtonsHolder(private val packageInfoPresenter: PackageInfoPres
private val appClearButton = AppClearButton(packageInfoPresenter)
private val appForceStopButton = AppForceStopButton(packageInfoPresenter)
val isAvailableFlow = flow { emit(isAvailable()) }
private suspend fun isAvailable(): Boolean = withContext(Dispatchers.IO) {
!packageInfoPresenter.userPackageManager.isSystemModule(packageInfoPresenter.packageName) &&
!AppUtils.isMainlineModule(
packageInfoPresenter.userPackageManager, packageInfoPresenter.packageName
)
}
@Composable
fun rememberActionsButtons() = remember {
packageInfoPresenter.flow.map { packageInfo ->

View File

@@ -27,10 +27,10 @@ import com.android.settingslib.spaprivileged.model.app.userHandle
class AppLaunchButton(packageInfoPresenter: PackageInfoPresenter) {
private val context = packageInfoPresenter.context
private val packageManagerAsUser = packageInfoPresenter.packageManagerAsUser
private val userPackageManager = packageInfoPresenter.userPackageManager
fun getActionButton(packageInfo: PackageInfo): ActionButton? =
packageManagerAsUser.getLaunchIntentForPackage(packageInfo.packageName)?.let { intent ->
userPackageManager.getLaunchIntentForPackage(packageInfo.packageName)?.let { intent ->
launchButton(intent, packageInfo.applicationInfo)
}

View File

@@ -51,7 +51,7 @@ class PackageInfoPresenter(
) {
private val metricsFeatureProvider = FeatureFactory.getFactory(context).metricsFeatureProvider
val userContext by lazy { context.asUser(UserHandle.of(userId)) }
val packageManagerAsUser: PackageManager by lazy { userContext.packageManager }
val userPackageManager: PackageManager by lazy { userContext.packageManager }
private val _flow: MutableStateFlow<PackageInfo?> = MutableStateFlow(null)
val flow: StateFlow<PackageInfo?> = _flow
@@ -92,7 +92,7 @@ class PackageInfoPresenter(
fun enable() {
logAction(SettingsEnums.ACTION_SETTINGS_ENABLE_APP)
coroutineScope.launch(Dispatchers.IO) {
packageManagerAsUser.setApplicationEnabledSetting(
userPackageManager.setApplicationEnabledSetting(
packageName, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, 0
)
notifyChange()
@@ -103,7 +103,7 @@ class PackageInfoPresenter(
fun disable() {
logAction(SettingsEnums.ACTION_SETTINGS_DISABLE_APP)
coroutineScope.launch(Dispatchers.IO) {
packageManagerAsUser.setApplicationEnabledSetting(
userPackageManager.setApplicationEnabledSetting(
packageName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0
)
notifyChange()
@@ -124,7 +124,7 @@ class PackageInfoPresenter(
fun clearInstantApp() {
logAction(SettingsEnums.ACTION_SETTINGS_CLEAR_INSTANT_APP)
coroutineScope.launch(Dispatchers.IO) {
packageManagerAsUser.deletePackageAsUser(packageName, null, 0, userId)
userPackageManager.deletePackageAsUser(packageName, null, 0, userId)
notifyChange()
}
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright (C) 2022 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.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.ModuleInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.NameNotFoundException
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onRoot
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.dx.mockito.inline.extended.ExtendedMockito
import com.android.settings.testutils.delay
import com.android.settingslib.applications.AppUtils
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.doThrow
import org.mockito.MockitoSession
import org.mockito.Spy
import org.mockito.quality.Strictness
import org.mockito.Mockito.`when` as whenever
@RunWith(AndroidJUnit4::class)
class AppButtonsTest {
@get:Rule
val composeTestRule = createComposeRule()
private lateinit var mockSession: MockitoSession
@Spy
private val context: Context = ApplicationProvider.getApplicationContext()
@Mock
private lateinit var packageInfoPresenter: PackageInfoPresenter
@Mock
private lateinit var packageManager: PackageManager
@Before
fun setUp() {
mockSession = ExtendedMockito.mockitoSession()
.initMocks(this)
.mockStatic(AppUtils::class.java)
.strictness(Strictness.LENIENT)
.startMocking()
whenever(packageInfoPresenter.context).thenReturn(context)
whenever(packageInfoPresenter.packageName).thenReturn(PACKAGE_NAME)
whenever(packageInfoPresenter.userPackageManager).thenReturn(packageManager)
doThrow(NameNotFoundException()).`when`(packageManager).getModuleInfo(PACKAGE_NAME, 0)
whenever(packageManager.getPackageInfo(PACKAGE_NAME, 0)).thenReturn(PACKAGE_INFO)
whenever(AppUtils.isMainlineModule(packageManager, PACKAGE_NAME)).thenReturn(false)
}
@After
fun tearDown() {
mockSession.finishMocking()
}
@Test
fun isSystemModule_notDisplayed() {
doReturn(ModuleInfo()).`when`(packageManager).getModuleInfo(PACKAGE_NAME, 0)
setContent()
composeTestRule.onRoot().assertIsNotDisplayed()
}
@Test
fun isMainlineModule_notDisplayed() {
whenever(AppUtils.isMainlineModule(packageManager, PACKAGE_NAME)).thenReturn(true)
setContent()
composeTestRule.onRoot().assertIsNotDisplayed()
}
@Test
fun isNormalApp_displayed() {
setContent()
composeTestRule.onRoot().assertIsDisplayed()
}
private fun setContent() {
composeTestRule.setContent {
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
whenever(packageInfoPresenter.flow).thenReturn(flowOf(PACKAGE_INFO).stateIn(scope))
}
AppButtons(packageInfoPresenter)
}
composeTestRule.delay()
}
private companion object {
const val PACKAGE_NAME = "package.name"
val PACKAGE_INFO = PackageInfo().apply {
applicationInfo = ApplicationInfo()
}
}
}

View File

@@ -16,6 +16,7 @@
package com.android.settings.testutils
import androidx.compose.ui.test.ComposeTimeoutException
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.junit4.ComposeContentTestRule
@@ -23,3 +24,10 @@ import androidx.compose.ui.test.junit4.ComposeContentTestRule
fun ComposeContentTestRule.waitUntilExists(matcher: SemanticsMatcher) = waitUntil {
onAllNodes(matcher).fetchSemanticsNodes().isNotEmpty()
}
/** Blocks until the timeout is reached. */
fun ComposeContentTestRule.delay(timeoutMillis: Long = 1_000) = try {
waitUntil(timeoutMillis) { false }
} catch (_: ComposeTimeoutException) {
// Expected
}