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

Commit 9ab0eed7 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

Respects device policy to disble quick affordances

- Interactor updated to check DevicePolicyManager
- Documentation section added

Bug: 268218507
Test: Unit test added
Test: When hard-coded flag to disable all keyguard features, manually
verified that: (a) shortcuts don't show on lock screen, (b) wallpaper
picker does ot display "shortcuts" item, (c) settings app doesn't
display "shortcuts" item.

Change-Id: Ie21ad7e8213d63b6505a16a6a09d4675c7112b34
parent 83e97973
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -52,6 +52,11 @@ A picker experience may:
* Unselect an already-selected quick affordance from a slot
* Unselect all already-selected quick affordances from a slot

## Device Policy
Returning `DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL` or
`DevicePolicyManager.KEYGUARD_DISABLE_SHORTCUTS_ALL` from
`DevicePolicyManager#getKeyguardDisabledFeatures` will disable the keyguard quick affordance feature on the device.

## Testing
* Add a unit test for your implementation of `KeyguardQuickAffordanceConfig`
* Manually verify that your implementation works in multi-user environments from both the main user and a secondary user
+6 −6
Original line number Diff line number Diff line
@@ -131,7 +131,7 @@ class CustomizationProvider :
            throw UnsupportedOperationException()
        }

        return insertSelection(values)
        return runBlocking { insertSelection(values) }
    }

    override fun query(
@@ -171,7 +171,7 @@ class CustomizationProvider :
            throw UnsupportedOperationException()
        }

        return deleteSelection(uri, selectionArgs)
        return runBlocking { deleteSelection(uri, selectionArgs) }
    }

    override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
@@ -189,7 +189,7 @@ class CustomizationProvider :
        }
    }

    private fun insertSelection(values: ContentValues?): Uri? {
    private suspend fun insertSelection(values: ContentValues?): Uri? {
        if (values == null) {
            throw IllegalArgumentException("Cannot insert selection, no values passed in!")
        }
@@ -311,7 +311,7 @@ class CustomizationProvider :
            }
    }

    private fun querySlots(): Cursor {
    private suspend fun querySlots(): Cursor {
        return MatrixCursor(
                arrayOf(
                    Contract.LockScreenQuickAffordances.SlotTable.Columns.ID,
@@ -330,7 +330,7 @@ class CustomizationProvider :
            }
    }

    private fun queryFlags(): Cursor {
    private suspend fun queryFlags(): Cursor {
        return MatrixCursor(
                arrayOf(
                    Contract.FlagsTable.Columns.NAME,
@@ -353,7 +353,7 @@ class CustomizationProvider :
            }
    }

    private fun deleteSelection(
    private suspend fun deleteSelection(
        uri: Uri,
        selectionArgs: Array<out String>?,
    ): Int {
+54 −8
Original line number Diff line number Diff line
@@ -18,12 +18,14 @@
package com.android.systemui.keyguard.domain.interactor

import android.app.AlertDialog
import android.app.admin.DevicePolicyManager
import android.content.Intent
import android.util.Log
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
@@ -41,13 +43,17 @@ import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.policy.KeyguardStateController
import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.withContext

@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class KeyguardQuickAffordanceInteractor
@Inject
@@ -61,6 +67,8 @@ constructor(
    private val featureFlags: FeatureFlags,
    private val repository: Lazy<KeyguardQuickAffordanceRepository>,
    private val launchAnimator: DialogLaunchAnimator,
    private val devicePolicyManager: DevicePolicyManager,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
) {
    private val isUsingRepository: Boolean
        get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)
@@ -74,9 +82,13 @@ constructor(
        get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)

    /** Returns an observable for the quick affordance at the given position. */
    fun quickAffordance(
    suspend fun quickAffordance(
        position: KeyguardQuickAffordancePosition
    ): Flow<KeyguardQuickAffordanceModel> {
        if (isFeatureDisabledByDevicePolicy()) {
            return flowOf(KeyguardQuickAffordanceModel.Hidden)
        }

        return combine(
            quickAffordanceAlwaysVisible(position),
            keyguardInteractor.isDozing,
@@ -148,13 +160,20 @@ constructor(
     *
     * @return `true` if the affordance was selected successfully; `false` otherwise.
     */
    fun select(slotId: String, affordanceId: String): Boolean {
    suspend fun select(slotId: String, affordanceId: String): Boolean {
        check(isUsingRepository)
        if (isFeatureDisabledByDevicePolicy()) {
            return false
        }

        val slots = repository.get().getSlotPickerRepresentations()
        val slot = slots.find { it.id == slotId } ?: return false
        val selections =
            repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).toMutableList()
            repository
                .get()
                .getCurrentSelections()
                .getOrDefault(slotId, emptyList())
                .toMutableList()
        val alreadySelected = selections.remove(affordanceId)
        if (!alreadySelected) {
            while (selections.size > 0 && selections.size >= slot.maxSelectedAffordances) {
@@ -183,8 +202,11 @@ constructor(
     * @return `true` if the affordance was successfully removed; `false` otherwise (for example, if
     * the affordance was not on the slot to begin with).
     */
    fun unselect(slotId: String, affordanceId: String?): Boolean {
    suspend fun unselect(slotId: String, affordanceId: String?): Boolean {
        check(isUsingRepository)
        if (isFeatureDisabledByDevicePolicy()) {
            return false
        }

        val slots = repository.get().getSlotPickerRepresentations()
        if (slots.find { it.id == slotId } == null) {
@@ -203,7 +225,11 @@ constructor(
        }

        val selections =
            repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).toMutableList()
            repository
                .get()
                .getCurrentSelections()
                .getOrDefault(slotId, emptyList())
                .toMutableList()
        return if (selections.remove(affordanceId)) {
            repository
                .get()
@@ -219,6 +245,10 @@ constructor(

    /** Returns affordance IDs indexed by slot ID, for all known slots. */
    suspend fun getSelections(): Map<String, List<KeyguardQuickAffordancePickerRepresentation>> {
        if (isFeatureDisabledByDevicePolicy()) {
            return emptyMap()
        }

        val slots = repository.get().getSlotPickerRepresentations()
        val selections = repository.get().getCurrentSelections()
        val affordanceById =
@@ -343,13 +373,17 @@ constructor(
        return repository.get().getAffordancePickerRepresentations()
    }

    fun getSlotPickerRepresentations(): List<KeyguardSlotPickerRepresentation> {
    suspend fun getSlotPickerRepresentations(): List<KeyguardSlotPickerRepresentation> {
        check(isUsingRepository)

        if (isFeatureDisabledByDevicePolicy()) {
            return emptyList()
        }

        return repository.get().getSlotPickerRepresentations()
    }

    fun getPickerFlags(): List<KeyguardPickerFlag> {
    suspend fun getPickerFlags(): List<KeyguardPickerFlag> {
        return listOf(
            KeyguardPickerFlag(
                name = Contract.FlagsTable.FLAG_NAME_REVAMPED_WALLPAPER_UI,
@@ -357,7 +391,9 @@ constructor(
            ),
            KeyguardPickerFlag(
                name = Contract.FlagsTable.FLAG_NAME_CUSTOM_LOCK_SCREEN_QUICK_AFFORDANCES_ENABLED,
                value = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES),
                value =
                    !isFeatureDisabledByDevicePolicy() &&
                        featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES),
            ),
            KeyguardPickerFlag(
                name = Contract.FlagsTable.FLAG_NAME_CUSTOM_CLOCKS_ENABLED,
@@ -374,6 +410,16 @@ constructor(
        )
    }

    private suspend fun isFeatureDisabledByDevicePolicy(): Boolean {
        val flags =
            withContext(backgroundDispatcher) {
                devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId)
            }
        val flagsToCheck = DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL
        // TODO(b/268218507): "or" with DevicePolicyManager.KEYGUARD_DISABLE_SHORTCUTS_ALL
        return flagsToCheck and flags != 0
    }

    companion object {
        private const val TAG = "KeyguardQuickAffordanceInteractor"
        private const val DELIMITER = "::"
+4 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@

package com.android.systemui.keyguard

import android.app.admin.DevicePolicyManager
import android.content.ContentValues
import android.content.pm.PackageManager
import android.content.pm.ProviderInfo
@@ -90,6 +91,7 @@ class CustomizationProviderTest : SysuiTestCase() {
    @Mock private lateinit var previewSurfacePackage: SurfaceControlViewHost.SurfacePackage
    @Mock private lateinit var launchAnimator: DialogLaunchAnimator
    @Mock private lateinit var commandQueue: CommandQueue
    @Mock private lateinit var devicePolicyManager: DevicePolicyManager

    private lateinit var underTest: CustomizationProvider
    private lateinit var testScope: TestScope
@@ -183,6 +185,8 @@ class CustomizationProviderTest : SysuiTestCase() {
                featureFlags = featureFlags,
                repository = { quickAffordanceRepository },
                launchAnimator = launchAnimator,
                devicePolicyManager = devicePolicyManager,
                backgroundDispatcher = testDispatcher,
            )
        underTest.previewManager =
            KeyguardRemotePreviewManager(
+55 −43
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@

package com.android.systemui.keyguard.domain.interactor

import android.app.admin.DevicePolicyManager
import android.content.Intent
import android.os.UserHandle
import androidx.test.filters.SmallTest
@@ -54,7 +55,10 @@ import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -70,6 +74,7 @@ import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(Parameterized::class)
class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
@@ -219,8 +224,10 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
    @Mock private lateinit var expandable: Expandable
    @Mock private lateinit var launchAnimator: DialogLaunchAnimator
    @Mock private lateinit var commandQueue: CommandQueue
    @Mock private lateinit var devicePolicyManager: DevicePolicyManager

    private lateinit var underTest: KeyguardQuickAffordanceInteractor
    private lateinit var testScope: TestScope

    @JvmField @Parameter(0) var needStrongAuthAfterBoot: Boolean = false
    @JvmField @Parameter(1) var canShowWhileLocked: Boolean = false
@@ -292,6 +299,8 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
                set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
                set(Flags.FACE_AUTH_REFACTOR, true)
            }
        val testDispatcher = StandardTestDispatcher()
        testScope = TestScope(testDispatcher)
        underTest =
            KeyguardQuickAffordanceInteractor(
                keyguardInteractor =
@@ -322,11 +331,14 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
                featureFlags = featureFlags,
                repository = { quickAffordanceRepository },
                launchAnimator = launchAnimator,
                devicePolicyManager = devicePolicyManager,
                backgroundDispatcher = testDispatcher,
            )
    }

    @Test
    fun onQuickAffordanceTriggered() = runBlockingTest {
    fun onQuickAffordanceTriggered() =
        testScope.runTest {
            setUpMocks(
                needStrongAuthAfterBoot = needStrongAuthAfterBoot,
                keyguardIsUnlocked = keyguardIsUnlocked,
Loading