Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5396d8a1 authored by Chaohui Wang's avatar Chaohui Wang Committed by Android (Google) Code Review
Browse files

Merge "No show AppButtons for system modules"

parents 4c1b702c 56c9bfed
Loading
Loading
Loading
Loading
+20 −4
Original line number Original line Diff line number Diff line
@@ -20,18 +20,25 @@ import android.content.pm.PackageInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
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.ActionButton
import com.android.settingslib.spa.widget.button.ActionButtons
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.flow.map
import kotlinx.coroutines.withContext


@Composable
@Composable
fun AppButtons(packageInfoPresenter: PackageInfoPresenter) {
fun AppButtons(packageInfoPresenter: PackageInfoPresenter) {
    val appButtonsHolder = remember { AppButtonsHolder(packageInfoPresenter) }
    val presenter = remember { AppButtonsPresenter(packageInfoPresenter) }
    appButtonsHolder.Dialogs()
    if (!presenter.isAvailableFlow.collectAsStateWithLifecycle(initialValue = false).value) return
    ActionButtons(actionButtons = appButtonsHolder.rememberActionsButtons().value)
    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 appLaunchButton = AppLaunchButton(packageInfoPresenter)
    private val appInstallButton = AppInstallButton(packageInfoPresenter)
    private val appInstallButton = AppInstallButton(packageInfoPresenter)
    private val appDisableButton = AppDisableButton(packageInfoPresenter)
    private val appDisableButton = AppDisableButton(packageInfoPresenter)
@@ -39,6 +46,15 @@ private class AppButtonsHolder(private val packageInfoPresenter: PackageInfoPres
    private val appClearButton = AppClearButton(packageInfoPresenter)
    private val appClearButton = AppClearButton(packageInfoPresenter)
    private val appForceStopButton = AppForceStopButton(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
    @Composable
    fun rememberActionsButtons() = remember {
    fun rememberActionsButtons() = remember {
        packageInfoPresenter.flow.map { packageInfo ->
        packageInfoPresenter.flow.map { packageInfo ->
+2 −2
Original line number Original line Diff line number Diff line
@@ -27,10 +27,10 @@ import com.android.settingslib.spaprivileged.model.app.userHandle


class AppLaunchButton(packageInfoPresenter: PackageInfoPresenter) {
class AppLaunchButton(packageInfoPresenter: PackageInfoPresenter) {
    private val context = packageInfoPresenter.context
    private val context = packageInfoPresenter.context
    private val packageManagerAsUser = packageInfoPresenter.packageManagerAsUser
    private val userPackageManager = packageInfoPresenter.userPackageManager


    fun getActionButton(packageInfo: PackageInfo): ActionButton? =
    fun getActionButton(packageInfo: PackageInfo): ActionButton? =
        packageManagerAsUser.getLaunchIntentForPackage(packageInfo.packageName)?.let { intent ->
        userPackageManager.getLaunchIntentForPackage(packageInfo.packageName)?.let { intent ->
            launchButton(intent, packageInfo.applicationInfo)
            launchButton(intent, packageInfo.applicationInfo)
        }
        }


+4 −4
Original line number Original line Diff line number Diff line
@@ -51,7 +51,7 @@ class PackageInfoPresenter(
) {
) {
    private val metricsFeatureProvider = FeatureFactory.getFactory(context).metricsFeatureProvider
    private val metricsFeatureProvider = FeatureFactory.getFactory(context).metricsFeatureProvider
    val userContext by lazy { context.asUser(UserHandle.of(userId)) }
    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)
    private val _flow: MutableStateFlow<PackageInfo?> = MutableStateFlow(null)


    val flow: StateFlow<PackageInfo?> = _flow
    val flow: StateFlow<PackageInfo?> = _flow
@@ -92,7 +92,7 @@ class PackageInfoPresenter(
    fun enable() {
    fun enable() {
        logAction(SettingsEnums.ACTION_SETTINGS_ENABLE_APP)
        logAction(SettingsEnums.ACTION_SETTINGS_ENABLE_APP)
        coroutineScope.launch(Dispatchers.IO) {
        coroutineScope.launch(Dispatchers.IO) {
            packageManagerAsUser.setApplicationEnabledSetting(
            userPackageManager.setApplicationEnabledSetting(
                packageName, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, 0
                packageName, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, 0
            )
            )
            notifyChange()
            notifyChange()
@@ -103,7 +103,7 @@ class PackageInfoPresenter(
    fun disable() {
    fun disable() {
        logAction(SettingsEnums.ACTION_SETTINGS_DISABLE_APP)
        logAction(SettingsEnums.ACTION_SETTINGS_DISABLE_APP)
        coroutineScope.launch(Dispatchers.IO) {
        coroutineScope.launch(Dispatchers.IO) {
            packageManagerAsUser.setApplicationEnabledSetting(
            userPackageManager.setApplicationEnabledSetting(
                packageName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0
                packageName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0
            )
            )
            notifyChange()
            notifyChange()
@@ -124,7 +124,7 @@ class PackageInfoPresenter(
    fun clearInstantApp() {
    fun clearInstantApp() {
        logAction(SettingsEnums.ACTION_SETTINGS_CLEAR_INSTANT_APP)
        logAction(SettingsEnums.ACTION_SETTINGS_CLEAR_INSTANT_APP)
        coroutineScope.launch(Dispatchers.IO) {
        coroutineScope.launch(Dispatchers.IO) {
            packageManagerAsUser.deletePackageAsUser(packageName, null, 0, userId)
            userPackageManager.deletePackageAsUser(packageName, null, 0, userId)
            notifyChange()
            notifyChange()
        }
        }
    }
    }
+131 −0
Original line number Original line Diff line number Diff line
/*
 * 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()
        }
    }
}
+8 −0
Original line number Original line Diff line number Diff line
@@ -16,6 +16,7 @@


package com.android.settings.testutils
package com.android.settings.testutils


import androidx.compose.ui.test.ComposeTimeoutException
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.ComposeContentTestRule


@@ -23,3 +24,10 @@ import androidx.compose.ui.test.junit4.ComposeContentTestRule
fun ComposeContentTestRule.waitUntilExists(matcher: SemanticsMatcher) = waitUntil {
fun ComposeContentTestRule.waitUntilExists(matcher: SemanticsMatcher) = waitUntil {
    onAllNodes(matcher).fetchSemanticsNodes().isNotEmpty()
    onAllNodes(matcher).fetchSemanticsNodes().isNotEmpty()
}
}

/** Blocks until the timeout is reached. */
fun ComposeContentTestRule.delay(timeoutMillis: Long = 1_000) = try {
    waitUntil(timeoutMillis) { false }
} catch (_: ComposeTimeoutException) {
    // Expected
}