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

Commit 5e0bbde5 authored by Chaohui Wang's avatar Chaohui Wang
Browse files

Refactor app access permission page

To enable second toggle coexists on the same page in the future cl.

Bug: 309677007
Test: manual - on Settings special access pages
Test: unit test
Change-Id: I7aace4f537820573971d5b05550fa01743582bef
parent b3550b9e
Loading
Loading
Loading
Loading
+39 −24
Original line number Diff line number Diff line
@@ -16,9 +16,7 @@

package com.android.settingslib.spaprivileged.template.app

import android.app.AppOpsManager.MODE_ALLOWED
import android.app.AppOpsManager.MODE_DEFAULT
import android.app.AppOpsManager.MODE_ERRORED
import android.app.AppOpsManager
import android.content.Context
import android.content.pm.ApplicationInfo
import androidx.compose.runtime.Composable
@@ -64,7 +62,7 @@ abstract class AppOpPermissionListModel(
     */
    open val permissionHasAppOpFlag: Boolean = true

    open val modeForNotAllowed: Int = MODE_ERRORED
    open val modeForNotAllowed: Int = AppOpsManager.MODE_ERRORED

    /**
     * Use AppOpsManager#setUidMode() instead of AppOpsManager#setMode() when set allowed.
@@ -130,34 +128,51 @@ abstract class AppOpPermissionListModel(
    override fun filter(userIdFlow: Flow<Int>, recordListFlow: Flow<List<AppOpPermissionRecord>>) =
        recordListFlow.filterItem(::isChangeable)

    @Composable
    override fun isAllowed(record: AppOpPermissionRecord): () -> Boolean? =
        isAllowed(
            record = record,
            appOpsController = record.appOpsController,
            permission = permission,
            packageManagers = packageManagers,
        )

    override fun isChangeable(record: AppOpPermissionRecord) =
        record.hasRequestPermission &&
            !record.hasRequestBroaderPermission &&
            record.app.packageName !in notChangeablePackages

    override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
        record.appOpsController.setAllowed(newAllowed)
    }
}

/**
 * Defining the default behavior as permissible as long as the package requested this permission
 * (This means pre-M gets approval during install time; M apps gets approval during runtime).
 */
@Composable
    override fun isAllowed(record: AppOpPermissionRecord): () -> Boolean? {
internal fun isAllowed(
    record: AppOpPermissionRecord,
    appOpsController: IAppOpsController,
    permission: String,
    packageManagers: IPackageManagers = PackageManagers,
): () -> Boolean? {
    if (record.hasRequestBroaderPermission) {
        // Broader permission trumps the specific permission.
        return { true }
    }

        val mode = record.appOpsController.mode.observeAsState()
    val mode = appOpsController.mode.observeAsState()
    return {
        when (mode.value) {
            null -> null
                MODE_ALLOWED -> true
                MODE_DEFAULT -> with(packageManagers) { record.app.hasGrantPermission(permission) }
                else -> false
            }
        }
            AppOpsManager.MODE_ALLOWED -> true
            AppOpsManager.MODE_DEFAULT -> {
                with(packageManagers) { record.app.hasGrantPermission(permission) }
            }

    override fun isChangeable(record: AppOpPermissionRecord) =
        record.hasRequestPermission &&
            !record.hasRequestBroaderPermission &&
            record.app.packageName !in notChangeablePackages

    override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
        record.appOpsController.setAllowed(newAllowed)
            else -> false
        }
    }
}
+28 −35
Original line number Diff line number Diff line
@@ -16,19 +16,16 @@

package com.android.settingslib.spaprivileged.template.app

import android.content.Context
import android.content.pm.ApplicationInfo
import android.os.Bundle
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.os.bundleOf
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.android.settingslib.spa.framework.common.SettingsEntry
@@ -49,6 +46,8 @@ import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProvid
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreference
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn

internal class TogglePermissionAppInfoPageProvider(
    private val appListTemplate: TogglePermissionAppListTemplate,
@@ -132,7 +131,7 @@ internal fun <T : AppRecord> TogglePermissionAppListModel<T>.TogglePermissionApp

@VisibleForTesting
@Composable
internal fun TogglePermissionAppListModel<out AppRecord>.TogglePermissionAppInfoPage(
internal fun <T : AppRecord> TogglePermissionAppListModel<T>.TogglePermissionAppInfoPage(
    packageName: String,
    userId: Int,
    packageManagers: IPackageManagers = PackageManagers,
@@ -145,40 +144,34 @@ internal fun TogglePermissionAppListModel<out AppRecord>.TogglePermissionAppInfo
        footerContent = { AnnotatedText(footerResId) },
        packageManagers = packageManagers,
    ) {
        val model = createSwitchModel(checkNotNull(applicationInfo))
        val app = applicationInfo ?: return@AppInfoPage
        val record = rememberRecord(app).value ?: return@AppInfoPage
        val isAllowed = isAllowed(record)
        val isChangeable by rememberIsChangeable(record)
        val switchModel = object : SwitchPreferenceModel {
            override val title = stringResource(switchTitleResId)
            override val checked = isAllowed
            override val changeable = { isChangeable }
            override val onCheckedChange: (Boolean) -> Unit = { setAllowed(record, it) }
        }
        val restrictions = Restrictions(userId, switchRestrictionKeys)
        RestrictedSwitchPreference(model, restrictions, restrictionsProviderFactory)
        RestrictedSwitchPreference(switchModel, restrictions, restrictionsProviderFactory)
    }
}

@Composable
private fun <T : AppRecord> TogglePermissionAppListModel<T>.createSwitchModel(
    app: ApplicationInfo,
): TogglePermissionSwitchModel<T> {
    val context = LocalContext.current
    val record = remember(app) { transformItem(app) }
    val isAllowed = isAllowed(record)
    return remember(record) { TogglePermissionSwitchModel(context, this, record, isAllowed) }
        .also { model -> LaunchedEffect(model, Dispatchers.IO) { model.initState() } }
}
private fun <T : AppRecord> TogglePermissionAppListModel<T>.rememberRecord(app: ApplicationInfo) =
    remember(app) {
        flow {
            emit(transformItem(app))
        }.flowOn(Dispatchers.Default)
    }.collectAsStateWithLifecycle(initialValue = null)

private class TogglePermissionSwitchModel<T : AppRecord>(
    context: Context,
    private val listModel: TogglePermissionAppListModel<T>,
    private val record: T,
    isAllowed: () -> Boolean?,
) : SwitchPreferenceModel {
    private var appChangeable by mutableStateOf(true)

    override val title: String = context.getString(listModel.switchTitleResId)
    override val checked = isAllowed
    override val changeable = { appChangeable }

    fun initState() {
        appChangeable = listModel.isChangeable(record)
    }

    override val onCheckedChange: (Boolean) -> Unit = { newChecked ->
        listModel.setAllowed(record, newChecked)
    }
}
@Composable
private fun <T : AppRecord> TogglePermissionAppListModel<T>.rememberIsChangeable(record: T) =
    remember(record) {
        flow {
            emit(isChangeable(record))
        }.flowOn(Dispatchers.Default)
    }.collectAsStateWithLifecycle(initialValue = false)
+15 −22
Original line number Diff line number Diff line
@@ -32,44 +32,37 @@ import com.android.settingslib.spaprivileged.test.R
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Spy
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.kotlin.any
import org.mockito.kotlin.doNothing
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@RunWith(AndroidJUnit4::class)
class AppOpPermissionAppListTest {
    @get:Rule val mockito: MockitoRule = MockitoJUnit.rule()
    @get:Rule
    val composeTestRule = createComposeRule()

    @get:Rule val composeTestRule = createComposeRule()
    private val packageManagers = mock<IPackageManagers>()

    @Spy private val context: Context = ApplicationProvider.getApplicationContext()
    private val appOpsManager = mock<AppOpsManager>()

    @Mock private lateinit var packageManagers: IPackageManagers

    @Mock private lateinit var appOpsManager: AppOpsManager

    @Mock private lateinit var packageManager: PackageManager

    private lateinit var listModel: TestAppOpPermissionAppListModel
    private val packageManager = mock<PackageManager> {
        doNothing().whenever(mock).updatePermissionFlags(any(), any(), any(), any(), any())
    }

    @Before
    fun setUp() {
        whenever(context.appOpsManager).thenReturn(appOpsManager)
        whenever(context.packageManager).thenReturn(packageManager)
        doNothing().whenever(packageManager)
                .updatePermissionFlags(any(), any(), any(), any(), any())
        listModel = TestAppOpPermissionAppListModel()
    private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
        on { appOpsManager } doReturn appOpsManager
        on { packageManager } doReturn packageManager
    }

    private val listModel = TestAppOpPermissionAppListModel()

    @Test
    fun transformItem_recordHasCorrectApp() {
        val record = listModel.transformItem(APP)
+8 −18
Original line number Diff line number Diff line
@@ -38,44 +38,34 @@ import com.android.settingslib.spaprivileged.tests.testutils.FakeRestrictionsPro
import com.android.settingslib.spaprivileged.tests.testutils.TestTogglePermissionAppListModel
import com.android.settingslib.spaprivileged.tests.testutils.TestTogglePermissionAppListProvider
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.kotlin.whenever
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock

@RunWith(AndroidJUnit4::class)
class TogglePermissionAppInfoPageTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    @get:Rule
    val mockito: MockitoRule = MockitoJUnit.rule()

    private val context: Context = ApplicationProvider.getApplicationContext()

    @Mock
    private lateinit var packageManagers: IPackageManagers
    private val packageManagers = mock<IPackageManagers> {
        on { getPackageInfoAsUser(PACKAGE_NAME, USER_ID) } doReturn PACKAGE_INFO
    }

    private val fakeNavControllerWrapper = FakeNavControllerWrapper()

    private val fakeRestrictionsProvider = FakeRestrictionsProvider()
    private val fakeRestrictionsProvider = FakeRestrictionsProvider().apply {
        restrictedMode = NoRestricted
    }

    private val appListTemplate =
        TogglePermissionAppListTemplate(listOf(TestTogglePermissionAppListProvider))

    private val appInfoPageProvider = TogglePermissionAppInfoPageProvider(appListTemplate)

    @Before
    fun setUp() {
        fakeRestrictionsProvider.restrictedMode = NoRestricted
        whenever(packageManagers.getPackageInfoAsUser(PACKAGE_NAME, USER_ID))
            .thenReturn(PACKAGE_INFO)
    }

    @Test
    fun buildEntry() {
        val entryList = appInfoPageProvider.buildEntry(null)