diff --git a/src/com/android/settings/spa/app/appinfo/AppButtons.kt b/src/com/android/settings/spa/app/appinfo/AppButtons.kt index 5bdaf68220f..c088fec69c5 100644 --- a/src/com/android/settings/spa/app/appinfo/AppButtons.kt +++ b/src/com/android/settings/spa/app/appinfo/AppButtons.kt @@ -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 -> diff --git a/src/com/android/settings/spa/app/appinfo/AppLaunchButton.kt b/src/com/android/settings/spa/app/appinfo/AppLaunchButton.kt index 8f398c7fabd..9e82f53e146 100644 --- a/src/com/android/settings/spa/app/appinfo/AppLaunchButton.kt +++ b/src/com/android/settings/spa/app/appinfo/AppLaunchButton.kt @@ -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) } diff --git a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt index 050c0483e44..4a6f9dbc4a7 100644 --- a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt +++ b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt @@ -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 = MutableStateFlow(null) val flow: StateFlow = _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() } } diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt new file mode 100644 index 00000000000..271c0ed15ae --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt @@ -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() + } + } +} diff --git a/tests/spa_unit/src/com/android/settings/testutils/ComposeContentTestRuleExt.kt b/tests/spa_unit/src/com/android/settings/testutils/ComposeContentTestRuleExt.kt index f3eb52957d4..82df9cff4dd 100644 --- a/tests/spa_unit/src/com/android/settings/testutils/ComposeContentTestRuleExt.kt +++ b/tests/spa_unit/src/com/android/settings/testutils/ComposeContentTestRuleExt.kt @@ -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 +}