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

Commit 0c9359f2 authored by William Leshner's avatar William Leshner Committed by Android (Google) Code Review
Browse files

Merge "Introduce a setting to enable/disable communal hub." into main

parents 420854c5 c4763344
Loading
Loading
Loading
Loading
+52 −15
Original line number Diff line number Diff line
@@ -16,24 +16,28 @@

package com.android.systemui.communal.data.repository

import android.content.pm.UserInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.scene.data.repository.SceneContainerRepository
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.data.repository.sceneContainerRepository
import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags
import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -44,19 +48,23 @@ import org.junit.runner.RunWith
class CommunalRepositoryImplTest : SysuiTestCase() {
    private lateinit var underTest: CommunalRepositoryImpl

    private val testDispatcher = StandardTestDispatcher()
    private val testScope = TestScope(testDispatcher)
    private lateinit var secureSettings: FakeSettings
    private lateinit var userRepository: FakeUserRepository

    private lateinit var featureFlagsClassic: FakeFeatureFlagsClassic
    private lateinit var sceneContainerRepository: SceneContainerRepository
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val sceneContainerRepository = kosmos.sceneContainerRepository

    @Before
    fun setUp() {
        val kosmos = testKosmos()
        sceneContainerRepository = kosmos.sceneContainerRepository
        featureFlagsClassic = FakeFeatureFlagsClassic()
        secureSettings = FakeSettings()
        userRepository = kosmos.fakeUserRepository

        featureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
        val listOfUserInfo = listOf(MAIN_USER_INFO)
        userRepository.setUserInfos(listOfUserInfo)

        kosmos.fakeFeatureFlagsClassic.apply { set(Flags.COMMUNAL_SERVICE_ENABLED, true) }
        mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)

        underTest = createRepositoryImpl(false)
    }
@@ -64,9 +72,13 @@ class CommunalRepositoryImplTest : SysuiTestCase() {
    private fun createRepositoryImpl(sceneContainerEnabled: Boolean): CommunalRepositoryImpl {
        return CommunalRepositoryImpl(
            testScope.backgroundScope,
            featureFlagsClassic,
            FakeSceneContainerFlags(enabled = sceneContainerEnabled),
            testScope.backgroundScope,
            kosmos.testDispatcher,
            kosmos.fakeFeatureFlagsClassic,
            kosmos.fakeSceneContainerFlags.apply { enabled = sceneContainerEnabled },
            sceneContainerRepository,
            kosmos.fakeUserRepository,
            secureSettings,
        )
    }

@@ -147,4 +159,29 @@ class CommunalRepositoryImplTest : SysuiTestCase() {
            assertThat(transitionState)
                .isEqualTo(ObservableCommunalTransitionState.Idle(CommunalSceneKey.DEFAULT))
        }

    @Test
    fun communalEnabledState_false_whenGlanceableHubSettingFalse() =
        testScope.runTest {
            userRepository.setSelectedUserInfo(MAIN_USER_INFO)
            secureSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 0, MAIN_USER_INFO.id)

            val communalEnabled by collectLastValue(underTest.communalEnabledState)
            assertThat(communalEnabled).isFalse()
        }

    @Test
    fun communalEnabledState_true_whenGlanceableHubSettingTrue() =
        testScope.runTest {
            userRepository.setSelectedUserInfo(MAIN_USER_INFO)
            secureSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 1, MAIN_USER_INFO.id)

            val communalEnabled by collectLastValue(underTest.communalEnabledState)
            assertThat(communalEnabled).isTrue()
        }

    companion object {
        private const val GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled"
        private val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
    }
}
+3 −2
Original line number Diff line number Diff line
@@ -27,10 +27,11 @@
    android:fitsSystemWindows="true">

    <!-- Placeholder for the communal UI that will be replaced if the feature is enabled. -->
    <ViewStub
    <View
        android:id="@+id/communal_ui_stub"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
        android:layout_height="match_parent"
        android:visibility="gone" />

    <com.android.systemui.scrim.ScrimView
        android:id="@+id/scrim_behind"
+61 −1
Original line number Diff line number Diff line
@@ -20,13 +20,18 @@ import com.android.systemui.Flags.communalHub
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.scene.data.repository.SceneContainerRepository
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
@@ -34,16 +39,26 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext

/** Encapsulates the state of communal mode. */
interface CommunalRepository {
    /** Whether communal features are enabled. */
    val isCommunalEnabled: Boolean

    /**
     * A {@link StateFlow} that tracks whether communal hub is enabled (it can be disabled in
     * settings).
     */
    val communalEnabledState: StateFlow<Boolean>

    /** Whether the communal hub is showing. */
    val isCommunalHubShowing: Flow<Boolean>

@@ -72,13 +87,36 @@ interface CommunalRepository {
class CommunalRepositoryImpl
@Inject
constructor(
    @Application private val applicationScope: CoroutineScope,
    @Background backgroundScope: CoroutineScope,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    private val featureFlagsClassic: FeatureFlagsClassic,
    sceneContainerFlags: SceneContainerFlags,
    sceneContainerRepository: SceneContainerRepository,
    userRepository: UserRepository,
    private val secureSettings: SecureSettings
) : CommunalRepository {

    private val communalEnabledSettingState: Flow<Boolean> =
        userRepository.selectedUserInfo
            .flatMapLatest { userInfo -> observeSettings(userInfo.id) }
            .shareIn(scope = applicationScope, started = SharingStarted.WhileSubscribed())

    override val communalEnabledState: StateFlow<Boolean> =
        if (featureFlagsClassic.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) && communalHub()) {
            communalEnabledSettingState
                .filterNotNull()
                .stateIn(
                    scope = applicationScope,
                    started = SharingStarted.Eagerly,
                    initialValue = true
                )
        } else {
            MutableStateFlow(false)
        }

    override val isCommunalEnabled: Boolean
        get() = featureFlagsClassic.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) && communalHub()
        get() = communalEnabledState.value

    private val _desiredScene: MutableStateFlow<CommunalSceneKey> =
        MutableStateFlow(CommunalSceneKey.DEFAULT)
@@ -115,4 +153,26 @@ constructor(
        } else {
            desiredScene.map { sceneKey -> sceneKey == CommunalSceneKey.Communal }
        }

    private fun observeSettings(userId: Int): Flow<Boolean> =
        secureSettings
            .observerFlow(
                userId = userId,
                names =
                    arrayOf(
                        GLANCEABLE_HUB_ENABLED,
                    )
            )
            // Force an update
            .onStart { emit(Unit) }
            .map { readFromSettings(userId) }

    private suspend fun readFromSettings(userId: Int): Boolean =
        withContext(backgroundDispatcher) {
            secureSettings.getIntForUser(GLANCEABLE_HUB_ENABLED, 1, userId) == 1
        }

    companion object {
        private const val GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled"
    }
}
+6 −1
Original line number Diff line number Diff line
@@ -72,7 +72,12 @@ constructor(
    val isCommunalEnabled: Boolean
        get() = communalRepository.isCommunalEnabled

    val isCommunalAvailable =
    /** A {@link StateFlow} that tracks whether communal features are enabled. */
    val communalEnabledState: StateFlow<Boolean>
        get() = communalRepository.communalEnabledState

    /** Whether communal features are enabled and available. */
    val isCommunalAvailable: StateFlow<Boolean> =
        flowOf(isCommunalEnabled)
            .flatMapLatest { enabled ->
                if (enabled)
+37 −29
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.os.SystemClock
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
@@ -33,6 +34,7 @@ import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.util.kotlin.collectFlow
import javax.inject.Inject
import kotlinx.coroutines.flow.StateFlow

/**
 * Controller that's responsible for the glanceable hub container view and its touch handling.
@@ -49,7 +51,7 @@ constructor(
    private val powerManager: PowerManager,
) {
    /** The container view for the hub. This will not be initialized until [initView] is called. */
    private lateinit var communalContainerView: View
    private var communalContainerView: View? = null

    /**
     * The width of the area in which a right edge swipe can open the hub, in pixels. Read from
@@ -108,6 +110,11 @@ constructor(
        return communalInteractor.isCommunalEnabled && isComposeAvailable()
    }

    /** Returns a {@link StateFlow} that tracks whether communal hub is enabled. */
    fun enabledState(): StateFlow<Boolean> {
        return communalInteractor.communalEnabledState
    }

    /**
     * Creates the container view containing the glanceable hub UI.
     *
@@ -125,42 +132,44 @@ constructor(
        if (!isEnabled()) {
            throw RuntimeException("Glanceable hub is not enabled")
        }
        if (::communalContainerView.isInitialized) {
        if (communalContainerView != null) {
            throw RuntimeException("Communal view has already been initialized")
        }

        communalContainerView = containerView

        rightEdgeSwipeRegionWidth =
            communalContainerView.resources.getDimensionPixelSize(
            containerView.resources.getDimensionPixelSize(
                R.dimen.communal_right_edge_swipe_region_width
            )
        topEdgeSwipeRegionWidth =
            communalContainerView.resources.getDimensionPixelSize(
            containerView.resources.getDimensionPixelSize(
                R.dimen.communal_top_edge_swipe_region_height
            )
        bottomEdgeSwipeRegionWidth =
            communalContainerView.resources.getDimensionPixelSize(
            containerView.resources.getDimensionPixelSize(
                R.dimen.communal_bottom_edge_swipe_region_height
            )

        collectFlow(
            communalContainerView,
            containerView,
            keyguardTransitionInteractor.isFinishedInStateWhere(KeyguardState::isBouncerState),
            { anyBouncerShowing = it }
        )
        collectFlow(
            communalContainerView,
            communalInteractor.isCommunalShowing,
            { hubShowing = it }
        )
        collectFlow(
            communalContainerView,
            shadeInteractor.isAnyFullyExpanded,
            { shadeShowing = it }
        )
        collectFlow(containerView, communalInteractor.isCommunalShowing, { hubShowing = it })
        collectFlow(containerView, shadeInteractor.isAnyFullyExpanded, { shadeShowing = it })

        return communalContainerView
        communalContainerView = containerView

        return containerView
    }

    /** Removes the container view from its parent. */
    fun disposeView() {
        communalContainerView?.let {
            (it.parent as ViewGroup).removeView(it)
            communalContainerView = null
        }
    }

    /**
@@ -173,10 +182,10 @@ constructor(
     * to be fully in control of its own touch handling.
     */
    fun onTouchEvent(ev: MotionEvent): Boolean {
        if (!::communalContainerView.isInitialized) {
            return false
        return communalContainerView?.let { handleTouchEventOnCommunalView(it, ev) } ?: false
    }

    private fun handleTouchEventOnCommunalView(view: View, ev: MotionEvent): Boolean {
        val isDown = ev.actionMasked == MotionEvent.ACTION_DOWN
        val isUp = ev.actionMasked == MotionEvent.ACTION_UP
        val isCancel = ev.actionMasked == MotionEvent.ACTION_CANCEL
@@ -190,7 +199,7 @@ constructor(
        if (hubShowing && isDown) {
            val y = ev.rawY
            val topSwipe: Boolean = y <= topEdgeSwipeRegionWidth
            val bottomSwipe = y >= communalContainerView.height - bottomEdgeSwipeRegionWidth
            val bottomSwipe = y >= view.height - bottomEdgeSwipeRegionWidth

            if (topSwipe || bottomSwipe) {
                // Don't intercept touches at the top/bottom edge so that swipes can open the
@@ -200,7 +209,7 @@ constructor(

            if (!hubOccluded) {
                isTrackingHubTouch = true
                dispatchTouchEvent(ev)
                dispatchTouchEvent(view, ev)
                // Return true regardless of dispatch result as some touches at the start of a
                // gesture may return false from dispatchTouchEvent.
                return true
@@ -209,7 +218,7 @@ constructor(
            if (isUp || isCancel) {
                isTrackingHubTouch = false
            }
            dispatchTouchEvent(ev)
            dispatchTouchEvent(view, ev)
            // Return true regardless of dispatch result as some touches at the start of a gesture
            // may return false from dispatchTouchEvent.
            return true
@@ -223,11 +232,10 @@ constructor(

        if (!isTrackingOpenGesture && isDown) {
            val x = ev.rawX
            val inOpeningSwipeRegion: Boolean =
                x >= communalContainerView.width - rightEdgeSwipeRegionWidth
            val inOpeningSwipeRegion: Boolean = x >= view.width - rightEdgeSwipeRegionWidth
            if (inOpeningSwipeRegion && !hubOccluded) {
                isTrackingOpenGesture = true
                dispatchTouchEvent(ev)
                dispatchTouchEvent(view, ev)
                // Return true regardless of dispatch result as some touches at the start of a
                // gesture may return false from dispatchTouchEvent.
                return true
@@ -236,7 +244,7 @@ constructor(
            if (isUp || isCancel) {
                isTrackingOpenGesture = false
            }
            dispatchTouchEvent(ev)
            dispatchTouchEvent(view, ev)
            // Return true regardless of dispatch result as some touches at the start of a gesture
            // may return false from dispatchTouchEvent.
            return true
@@ -249,8 +257,8 @@ constructor(
     * Dispatches the touch event to the communal container and sends a user activity event to reset
     * the screen timeout.
     */
    private fun dispatchTouchEvent(ev: MotionEvent) {
        communalContainerView.dispatchTouchEvent(ev)
    private fun dispatchTouchEvent(view: View, ev: MotionEvent) {
        view.dispatchTouchEvent(ev)
        powerManager.userActivity(
            SystemClock.uptimeMillis(),
            PowerManager.USER_ACTIVITY_EVENT_TOUCH,
Loading