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

Commit a01f3185 authored by Andreas Agvard's avatar Andreas Agvard
Browse files

Implement persistence of assistant configuration for Floaty

Only persists the preferences for a single user + assistant combination.
The goal is only to avoid issues like SysUi crashes resetting the configuration.

Bug: 405328848
Flag: com.android.systemui.shared.enable_lpp_assist_invocation_effect
Test: Unit and Manual
Change-Id: Idcab788fdb2de6cdbb83b29c34e1fea6c1019417
parent 6e31cca9
Loading
Loading
Loading
Loading
+179 −14
Original line number Diff line number Diff line
@@ -16,9 +16,13 @@

package com.android.systemui.topwindoweffects.data.repository

import android.app.role.OnRoleHoldersChangedListener
import android.app.role.RoleManager
import android.content.pm.UserInfo
import android.hardware.input.InputManager
import android.os.Bundle
import android.os.Handler
import android.os.UserHandle
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.provider.Settings.Global.POWER_BUTTON_LONG_PRESS
@@ -33,15 +37,22 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.shared.Flags
import com.android.systemui.testKosmos
import com.android.systemui.topwindoweffects.data.repository.SqueezeEffectRepositoryImpl.Companion.IS_INVOCATION_EFFECT_ENABLED_BY_ASSISTANT_DEFAULT_VALUE
import com.android.systemui.topwindoweffects.data.repository.SqueezeEffectRepositoryImpl.Companion.IS_INVOCATION_EFFECT_ENABLED_KEY
import com.android.systemui.topwindoweffects.data.repository.SqueezeEffectRepositoryImpl.Companion.SET_INVOCATION_EFFECT_PARAMETERS_ACTION
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.util.settings.FakeGlobalSettings
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executor
import kotlinx.coroutines.test.StandardTestDispatcher
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.Mockito.eq
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations

private fun createAssistantSettingBundle(enableAssistantSetting: Boolean) =
@@ -56,60 +67,66 @@ class SqueezeEffectRepositoryTest : SysuiTestCase() {

    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val globalSettings = FakeGlobalSettings(StandardTestDispatcher())
    private val mainExecutor = Executor(Runnable::run)
    private val userRepository = FakeUserRepository()

    @Mock private lateinit var bgHandler: Handler
    @Mock private lateinit var handler: Handler
    @Mock private lateinit var inputManager: InputManager
    @Mock private lateinit var roleManager: RoleManager

    private val onRoleHoldersChangedListener =
        ArgumentCaptor.forClass(OnRoleHoldersChangedListener::class.java)

    private val Kosmos.underTest by
        Kosmos.Fixture {
            SqueezeEffectRepositoryImpl(
                context = mContext,
                handler = bgHandler,
                coroutineContext = testScope.testScheduler,
                executor = Runnable::run,
                inputManager = inputManager,
                context = context,
                coroutineScope = testScope.backgroundScope,
                globalSettings = globalSettings,
                userRepository = userRepository,
                inputManager = inputManager,
                handler = handler,
                coroutineContext = testScope.testScheduler,
                roleManager = roleManager,
                executor = mainExecutor,
            )
        }

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        MockitoAnnotations.openMocks(this)
    }

    @DisableFlags(Flags.FLAG_ENABLE_LPP_ASSIST_INVOCATION_EFFECT)
    @Test
    fun testSqueezeEffectDisabled_WhenOtherwiseEnabled_FlagDisabled() =
    fun testSqueezeEffectDisabled_FlagDisabled() =
        kosmos.runTest {
            globalSettings.putInt(POWER_BUTTON_LONG_PRESS, 5)
            underTest.tryHandleSetUiHints(createAssistantSettingBundle(true))

            val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)

            assertThat(isSqueezeEffectEnabled).isFalse()
        }

    @EnableFlags(Flags.FLAG_ENABLE_LPP_ASSIST_INVOCATION_EFFECT)
    @Test
    fun testSqueezeEffectDisabled_WhenOtherwiseEnabled_GlobalSettingDisabled() =
    fun testSqueezeEffectDisabled_GlobalSettingDisabled() =
        kosmos.runTest {
            underTest.tryHandleSetUiHints(createAssistantSettingBundle(true))
            globalSettings.putInt(POWER_BUTTON_LONG_PRESS, 0)

            val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)

            assertThat(isSqueezeEffectEnabled).isFalse()
        }

    @EnableFlags(Flags.FLAG_ENABLE_LPP_ASSIST_INVOCATION_EFFECT)
    @Test
    fun testSqueezeEffectDisabled_WhenOtherwiseEnabled_AssistantSettingDisabled() =
    fun testSqueezeEffectDisabled_AssistantSettingDisabled() =
        kosmos.runTest {
            globalSettings.putInt(POWER_BUTTON_LONG_PRESS, 5)
            underTest.tryHandleSetUiHints(createAssistantSettingBundle(false))

            val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)

            assertThat(isSqueezeEffectEnabled).isFalse()
        }

@@ -121,7 +138,155 @@ class SqueezeEffectRepositoryTest : SysuiTestCase() {
            underTest.tryHandleSetUiHints(createAssistantSettingBundle(true))

            val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)

            assertThat(isSqueezeEffectEnabled).isTrue()
        }

    private suspend fun Kosmos.initUserAndAssistant(
        userInfos: List<UserInfo>,
        userIndex: Int,
        assistantName: String,
    ) {
        underTest // "poke" class to ensure it's initialized
        userRepository.setUserInfos(userInfos)
        userRepository.setSelectedUserInfo(userInfos[userIndex])
        verify(roleManager)
            .addOnRoleHoldersChangedListenerAsUser(
                eq(mainExecutor),
                onRoleHoldersChangedListener.capture(),
                eq(UserHandle.ALL),
            )
        `when`(
                roleManager.getRoleHoldersAsUser(
                    eq(RoleManager.ROLE_ASSISTANT),
                    eq(userInfos[userIndex].userHandle),
                )
            )
            .thenReturn(listOf(assistantName))
        onRoleHoldersChangedListener.value.onRoleHoldersChanged(
            RoleManager.ROLE_ASSISTANT,
            userInfos[userIndex].userHandle,
        )
    }

    @EnableFlags(Flags.FLAG_ENABLE_LPP_ASSIST_INVOCATION_EFFECT)
    @Test
    fun testAssistantEnabledStatusIsDefault_AssistantSwitched() =
        kosmos.runTest {
            initUserAndAssistant(userInfos, 0, "a")
            globalSettings.putInt(POWER_BUTTON_LONG_PRESS, 5)
            underTest.tryHandleSetUiHints(
                createAssistantSettingBundle(
                    !IS_INVOCATION_EFFECT_ENABLED_BY_ASSISTANT_DEFAULT_VALUE
                )
            )

            `when`(
                    roleManager.getRoleHoldersAsUser(
                        eq(RoleManager.ROLE_ASSISTANT),
                        eq(userInfos[0].userHandle),
                    )
                )
                .thenReturn(listOf("b"))
            onRoleHoldersChangedListener.value.onRoleHoldersChanged(
                RoleManager.ROLE_ASSISTANT,
                userInfos[0].userHandle,
            )

            val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
            assertThat(isSqueezeEffectEnabled)
                .isEqualTo(IS_INVOCATION_EFFECT_ENABLED_BY_ASSISTANT_DEFAULT_VALUE)
        }

    @EnableFlags(Flags.FLAG_ENABLE_LPP_ASSIST_INVOCATION_EFFECT)
    @Test
    fun testAssistantEnabledStatusIsDefault_UserSwitched() =
        kosmos.runTest {
            initUserAndAssistant(userInfos, 0, "a")
            globalSettings.putInt(POWER_BUTTON_LONG_PRESS, 5)
            underTest.tryHandleSetUiHints(
                createAssistantSettingBundle(
                    !IS_INVOCATION_EFFECT_ENABLED_BY_ASSISTANT_DEFAULT_VALUE
                )
            )

            userRepository.setSelectedUserInfo(userInfos[1])

            val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
            assertThat(isSqueezeEffectEnabled)
                .isEqualTo(IS_INVOCATION_EFFECT_ENABLED_BY_ASSISTANT_DEFAULT_VALUE)
        }

    @EnableFlags(Flags.FLAG_ENABLE_LPP_ASSIST_INVOCATION_EFFECT)
    @Test
    fun testAssistantEnabledStatusIsRetained_AssistantSwitchedBackAndForth() =
        kosmos.runTest {
            initUserAndAssistant(userInfos, 0, "a")
            globalSettings.putInt(POWER_BUTTON_LONG_PRESS, 5)
            underTest.tryHandleSetUiHints(
                createAssistantSettingBundle(
                    !IS_INVOCATION_EFFECT_ENABLED_BY_ASSISTANT_DEFAULT_VALUE
                )
            )

            `when`(
                    roleManager.getRoleHoldersAsUser(
                        eq(RoleManager.ROLE_ASSISTANT),
                        eq(UserHandle.CURRENT),
                    )
                )
                .thenReturn(listOf("b"))
            onRoleHoldersChangedListener.value.onRoleHoldersChanged(
                RoleManager.ROLE_ASSISTANT,
                UserHandle.CURRENT,
            )
            `when`(
                    roleManager.getRoleHoldersAsUser(
                        eq(RoleManager.ROLE_ASSISTANT),
                        eq(UserHandle.CURRENT),
                    )
                )
                .thenReturn(listOf("a"))
            onRoleHoldersChangedListener.value.onRoleHoldersChanged(
                RoleManager.ROLE_ASSISTANT,
                UserHandle.CURRENT,
            )

            val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
            assertThat(isSqueezeEffectEnabled)
                .isEqualTo(!IS_INVOCATION_EFFECT_ENABLED_BY_ASSISTANT_DEFAULT_VALUE)
        }

    @EnableFlags(Flags.FLAG_ENABLE_LPP_ASSIST_INVOCATION_EFFECT)
    @Test
    fun testAssistantEnabledStatusIsRetained_UserSwitchedBackAndForth() =
        kosmos.runTest {
            initUserAndAssistant(userInfos, 0, "a")
            globalSettings.putInt(POWER_BUTTON_LONG_PRESS, 5)
            underTest.tryHandleSetUiHints(
                createAssistantSettingBundle(
                    !IS_INVOCATION_EFFECT_ENABLED_BY_ASSISTANT_DEFAULT_VALUE
                )
            )

            userRepository.setSelectedUserInfo(userInfos[1])
            userRepository.setSelectedUserInfo(userInfos[0])

            val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
            assertThat(isSqueezeEffectEnabled)
                .isEqualTo(!IS_INVOCATION_EFFECT_ENABLED_BY_ASSISTANT_DEFAULT_VALUE)
        }

    companion object {
        private val userInfos =
            listOf(
                UserInfo().apply {
                    id = 0
                    name = "User 0"
                },
                UserInfo().apply {
                    id = 1
                    name = "User 1"
                },
            )
    }
}
+133 −7
Original line number Diff line number Diff line
@@ -17,12 +17,15 @@
package com.android.systemui.topwindoweffects.data.repository

import android.annotation.SuppressLint
import android.app.role.OnRoleHoldersChangedListener
import android.app.role.RoleManager
import android.content.Context
import android.database.ContentObserver
import android.hardware.input.InputManager
import android.hardware.input.KeyGestureEvent
import android.os.Bundle
import android.os.Handler
import android.os.UserHandle
import android.provider.Settings.Global.POWER_BUTTON_LONG_PRESS
import android.provider.Settings.Global.POWER_BUTTON_LONG_PRESS_DURATION_MS
import android.util.DisplayUtils
@@ -30,6 +33,7 @@ import android.view.DisplayInfo
import android.view.KeyEvent
import androidx.annotation.ArrayRes
import androidx.annotation.DrawableRes
import androidx.core.content.edit
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.assist.AssistManager
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
@@ -39,30 +43,97 @@ import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.res.R
import com.android.systemui.shared.Flags
import com.android.systemui.topwindoweffects.data.entity.SqueezeEffectCornersInfo
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import java.util.concurrent.Executor
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

@SysUISingleton
class SqueezeEffectRepositoryImpl
@Inject
constructor(
    @Application private val context: Context,
    @Background private val handler: Handler?,
    @Background private val coroutineContext: CoroutineContext,
    @Background executor: Executor,
    @Background private val coroutineScope: CoroutineScope,
    private val globalSettings: GlobalSettings,
    private val userRepository: UserRepository,
    private val inputManager: InputManager,
    @Background handler: Handler?,
    @Background coroutineContext: CoroutineContext,
    roleManager: RoleManager,
    @Background executor: Executor,
) : SqueezeEffectRepository, InvocationEffectSetUiHintsHandler {

    private val sharedPreferences by lazy {
        context.getSharedPreferences(SHARED_PREFERENCES_FILE_NAME, Context.MODE_PRIVATE)
    }
    private val isInvocationEffectEnabledByAssistantFlow = MutableStateFlow<Boolean?>(null)

    private val selectedAssistantName: StateFlow<String> =
        conflatedCallbackFlow {
                val listener = OnRoleHoldersChangedListener { roleName, _ ->
                    if (roleName == RoleManager.ROLE_ASSISTANT) {
                        trySendWithFailureLogging(
                            roleManager.getCurrentAssistantFor(userRepository.selectedUserHandle),
                            TAG,
                            "updated currentlyActiveAssistantName due to role change",
                        )
                    }
                }
                roleManager.addOnRoleHoldersChangedListenerAsUser(
                    executor,
                    listener,
                    UserHandle.ALL,
                )

                launch {
                    userRepository.selectedUser.collect {
                        trySendWithFailureLogging(
                            roleManager.getCurrentAssistantFor(userRepository.selectedUserHandle),
                            TAG,
                            "updated currentlyActiveAssistantName due to user change",
                        )
                    }
                }

                awaitClose {
                    roleManager.removeOnRoleHoldersChangedListenerAsUser(listener, UserHandle.ALL)
                }
            }
            .flowOn(coroutineContext)
            .stateIn(
                scope = coroutineScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = roleManager.getCurrentAssistantFor(userRepository.selectedUserHandle),
            )

    private val selectedAssistantNameAndUserFlow =
        selectedAssistantName
            .combine(userRepository.selectedUser) { a, b -> Pair(a, b) }
            .distinctUntilChanged()

    init {
        coroutineScope.launch {
            selectedAssistantNameAndUserFlow.collect {
                // Assistant or user changed, reload enabled state
                isInvocationEffectEnabledByAssistantFlow.value =
                    loadIsInvocationEffectEnabledByAssistant()
            }
        }
    }

    private val isPowerButtonLongPressConfiguredToLaunchAssistantFlow: Flow<Boolean> =
        conflatedCallbackFlow {
                val observer =
@@ -175,14 +246,38 @@ constructor(
        return drawableResource
    }

    private val isInvocationEffectEnabledForCurrentAssistantFlow = MutableStateFlow(true)
    private fun loadIsInvocationEffectEnabledByAssistant(): Boolean {
        val persistedForUser =
            sharedPreferences.getInt(
                PERSISTED_FOR_USER_PREFERENCE,
                PERSISTED_FOR_USER_DEFAULT_VALUE,
            )

        val persistedForAssistant =
            sharedPreferences.getString(
                PERSISTED_FOR_ASSISTANT_PREFERENCE,
                PERSISTED_FOR_ASSISTANT_DEFAULT_VALUE,
            )

        return if (
            persistedForUser == userRepository.selectedUserHandle.identifier &&
                persistedForAssistant == selectedAssistantName.value
        ) {
            sharedPreferences.getBoolean(
                IS_INVOCATION_EFFECT_ENABLED_BY_ASSISTANT_PREFERENCE,
                IS_INVOCATION_EFFECT_ENABLED_BY_ASSISTANT_DEFAULT_VALUE,
            )
        } else {
            IS_INVOCATION_EFFECT_ENABLED_BY_ASSISTANT_DEFAULT_VALUE
        }
    }

    override val isSqueezeEffectEnabled: Flow<Boolean> =
        combine(
            isPowerButtonLongPressConfiguredToLaunchAssistantFlow,
            isInvocationEffectEnabledForCurrentAssistantFlow,
            isInvocationEffectEnabledByAssistantFlow,
        ) { prerequisites ->
            prerequisites.all { it } && Flags.enableLppAssistInvocationEffect()
            prerequisites.all { it ?: false } && Flags.enableLppAssistInvocationEffect()
        }

    private fun getIsPowerButtonLongPressConfiguredToLaunchAssistant() =
@@ -207,8 +302,9 @@ constructor(
        return when (hints.getString(AssistManager.ACTION_KEY)) {
            SET_INVOCATION_EFFECT_PARAMETERS_ACTION -> {
                if (hints.containsKey(IS_INVOCATION_EFFECT_ENABLED_KEY)) {
                    isInvocationEffectEnabledForCurrentAssistantFlow.value =
                    setIsInvocationEffectEnabledByAssistant(
                        hints.getBoolean(IS_INVOCATION_EFFECT_ENABLED_KEY)
                    )
                }
                true
            }
@@ -216,6 +312,17 @@ constructor(
        }
    }

    private fun setIsInvocationEffectEnabledByAssistant(enabled: Boolean) {
        coroutineScope.launch {
            isInvocationEffectEnabledByAssistantFlow.value = enabled
            sharedPreferences.edit {
                putBoolean(IS_INVOCATION_EFFECT_ENABLED_BY_ASSISTANT_PREFERENCE, enabled)
                putString(PERSISTED_FOR_ASSISTANT_PREFERENCE, selectedAssistantName.value)
                putInt(PERSISTED_FOR_USER_PREFERENCE, userRepository.selectedUserHandle.identifier)
            }
        }
    }

    companion object {
        private const val TAG = "SqueezeEffectRepository"

@@ -229,9 +336,28 @@ constructor(
         */
        @VisibleForTesting const val DEFAULT_INITIAL_DELAY_MILLIS = 150L
        @VisibleForTesting const val DEFAULT_LONG_PRESS_POWER_DURATION_MILLIS = 500L

        @VisibleForTesting
        const val SET_INVOCATION_EFFECT_PARAMETERS_ACTION = "set_invocation_effect_parameters"
        @VisibleForTesting
        const val IS_INVOCATION_EFFECT_ENABLED_KEY = "is_invocation_effect_enabled"

        @VisibleForTesting const val IS_INVOCATION_EFFECT_ENABLED_BY_ASSISTANT_DEFAULT_VALUE = true
        private const val PERSISTED_FOR_USER_DEFAULT_VALUE = Integer.MIN_VALUE
        private const val PERSISTED_FOR_ASSISTANT_DEFAULT_VALUE = ""

        @VisibleForTesting
        const val SHARED_PREFERENCES_FILE_NAME = "assistant_invocation_effect_preferences"
        @VisibleForTesting
        const val IS_INVOCATION_EFFECT_ENABLED_BY_ASSISTANT_PREFERENCE =
            "is_invocation_effect_enabled"
        private const val PERSISTED_FOR_ASSISTANT_PREFERENCE = "persisted_for_assistant"
        private const val PERSISTED_FOR_USER_PREFERENCE = "persisted_for_user"
    }
}

private val UserRepository.selectedUserHandle
    get() = selectedUser.value.userInfo.userHandle

private fun RoleManager.getCurrentAssistantFor(userHandle: UserHandle) =
    getRoleHoldersAsUser(RoleManager.ROLE_ASSISTANT, userHandle)?.firstOrNull() ?: ""