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

Commit 7429825d authored by Coco Duan's avatar Coco Duan
Browse files

Persist CTA tile dismissed state

Create a repository that stores communal hub prefs in shared preferences.
For now it is just the CTA dismissed state, so it won't show up in GH
again once dismissed.

Bug: b/313462210
Flag: ACONFIG com.android.systemui.communal_hub DEVELOPMENT
Test: atest CommunalPrefsRepositoryImplTest
Test: atest CommunalInteractorTest
Test: atest CommunalViewModelTest
Change-Id: Id0f66dbcce434eed5fdd8cd834b8706a7d58c48f
parent 006c183d
Loading
Loading
Loading
Loading
+135 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.systemui.communal.data.repository

import android.content.SharedPreferences
import android.content.pm.UserInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.CommunalPrefsRepositoryImpl.Companion.FILE_NAME
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.settings.UserFileManager
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.FakeSharedPreferences
import com.google.common.truth.Truth.assertThat
import java.io.File
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class CommunalPrefsRepositoryImplTest : SysuiTestCase() {
    private lateinit var underTest: CommunalPrefsRepositoryImpl

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope

    private lateinit var userRepository: FakeUserRepository
    private lateinit var userFileManager: UserFileManager

    @Before
    fun setUp() {
        userRepository = kosmos.fakeUserRepository
        userRepository.setUserInfos(USER_INFOS)

        userFileManager =
            FakeUserFileManager(
                mapOf(
                    USER_INFOS[0].id to FakeSharedPreferences(),
                    USER_INFOS[1].id to FakeSharedPreferences()
                )
            )
        underTest =
            CommunalPrefsRepositoryImpl(
                testScope.backgroundScope,
                kosmos.testDispatcher,
                userRepository,
                userFileManager,
            )
    }

    @Test
    fun isCtaDismissedValue_byDefault_isFalse() =
        testScope.runTest {
            val isCtaDismissed by collectLastValue(underTest.isCtaDismissed)
            assertThat(isCtaDismissed).isFalse()
        }

    @Test
    fun isCtaDismissedValue_onSet_isTrue() =
        testScope.runTest {
            val isCtaDismissed by collectLastValue(underTest.isCtaDismissed)

            underTest.setCtaDismissedForCurrentUser()
            assertThat(isCtaDismissed).isTrue()
        }

    @Test
    fun isCtaDismissedValue_whenSwitchUser() =
        testScope.runTest {
            val isCtaDismissed by collectLastValue(underTest.isCtaDismissed)
            underTest.setCtaDismissedForCurrentUser()

            // dismissed true for primary user
            assertThat(isCtaDismissed).isTrue()

            // switch to secondary user
            userRepository.setSelectedUserInfo(USER_INFOS[1])

            // dismissed is false for secondary user
            assertThat(isCtaDismissed).isFalse()

            // switch back to primary user
            userRepository.setSelectedUserInfo(USER_INFOS[0])

            // dismissed is true for primary user
            assertThat(isCtaDismissed).isTrue()
        }

    private class FakeUserFileManager(private val sharedPrefs: Map<Int, SharedPreferences>) :
        UserFileManager {
        override fun getFile(fileName: String, userId: Int): File {
            throw UnsupportedOperationException()
        }

        override fun getSharedPreferences(
            fileName: String,
            mode: Int,
            userId: Int
        ): SharedPreferences {
            if (fileName != FILE_NAME) {
                throw IllegalArgumentException("Preference files must be $FILE_NAME")
            }
            return sharedPrefs.getValue(userId)
        }
    }

    companion object {
        val USER_INFOS =
            listOf(
                UserInfo(/* id= */ 0, "zero", /* flags= */ 0),
                UserInfo(/* id= */ 1, "secondary", /* flags= */ 0),
            )
    }
}
+6 −4
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository
import com.android.systemui.communal.data.repository.FakeCommunalRepository
import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository
import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
@@ -65,6 +66,7 @@ class CommunalInteractorTest : SysuiTestCase() {
    private lateinit var widgetRepository: FakeCommunalWidgetRepository
    private lateinit var smartspaceRepository: FakeSmartspaceRepository
    private lateinit var keyguardRepository: FakeKeyguardRepository
    private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository
    private lateinit var editWidgetsActivityStarter: EditWidgetsActivityStarter

    private lateinit var underTest: CommunalInteractor
@@ -84,6 +86,7 @@ class CommunalInteractorTest : SysuiTestCase() {
        smartspaceRepository = withDeps.smartspaceRepository
        keyguardRepository = withDeps.keyguardRepository
        editWidgetsActivityStarter = withDeps.editWidgetsActivityStarter
        communalPrefsRepository = withDeps.communalPrefsRepository

        underTest = withDeps.communalInteractor
    }
@@ -331,10 +334,9 @@ class CommunalInteractorTest : SysuiTestCase() {
        }

    @Test
    fun cta_visibilityTrue_shows() =
    fun ctaTile_showsByDefault() =
        testScope.runTest {
            tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
            communalRepository.setCtaTileInViewModeVisibility(true)

            val ctaTileContent by collectLastValue(underTest.ctaTileContent)

@@ -346,10 +348,10 @@ class CommunalInteractorTest : SysuiTestCase() {
        }

    @Test
    fun ctaTile_visibilityFalse_doesNotShow() =
    fun ctaTile_afterDismiss_doesNotShow() =
        testScope.runTest {
            tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
            communalRepository.setCtaTileInViewModeVisibility(false)
            communalPrefsRepository.setCtaDismissedForCurrentUser()

            val ctaTileContent by collectLastValue(underTest.ctaTileContent)

+3 −5
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository
import com.android.systemui.communal.data.repository.FakeCommunalRepository
import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository
import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
@@ -66,6 +67,7 @@ class CommunalViewModelTest : SysuiTestCase() {
    private lateinit var widgetRepository: FakeCommunalWidgetRepository
    private lateinit var smartspaceRepository: FakeSmartspaceRepository
    private lateinit var mediaRepository: FakeCommunalMediaRepository
    private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository

    private lateinit var underTest: CommunalViewModel

@@ -82,6 +84,7 @@ class CommunalViewModelTest : SysuiTestCase() {
        widgetRepository = withDeps.widgetRepository
        smartspaceRepository = withDeps.smartspaceRepository
        mediaRepository = withDeps.mediaRepository
        communalPrefsRepository = withDeps.communalPrefsRepository

        underTest =
            CommunalViewModel(
@@ -149,9 +152,6 @@ class CommunalViewModelTest : SysuiTestCase() {
            // Media playing.
            mediaRepository.mediaActive()

            // CTA Tile not dismissed.
            communalRepository.setCtaTileInViewModeVisibility(true)

            val communalContent by collectLastValue(underTest.communalContent)

            // Order is smart space, then UMO, widget content and cta tile.
@@ -171,7 +171,6 @@ class CommunalViewModelTest : SysuiTestCase() {
    fun dismissCta_hidesCtaTileAndShowsPopup_thenHidesPopupAfterTimeout() =
        testScope.runTest {
            tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
            communalRepository.setCtaTileInViewModeVisibility(true)

            val communalContent by collectLastValue(underTest.communalContent)
            val isPopupOnDismissCtaShowing by collectLastValue(underTest.isPopupOnDismissCtaShowing)
@@ -195,7 +194,6 @@ class CommunalViewModelTest : SysuiTestCase() {
    fun popup_onDismiss_hidesImmediately() =
        testScope.runTest {
            tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
            communalRepository.setCtaTileInViewModeVisibility(true)

            val isPopupOnDismissCtaShowing by collectLastValue(underTest.isPopupOnDismissCtaShowing)

+2 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.systemui.communal.dagger

import com.android.systemui.communal.data.db.CommunalDatabaseModule
import com.android.systemui.communal.data.repository.CommunalMediaRepositoryModule
import com.android.systemui.communal.data.repository.CommunalPrefsRepositoryModule
import com.android.systemui.communal.data.repository.CommunalRepositoryModule
import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule
import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule
@@ -34,6 +35,7 @@ import dagger.Module
            CommunalTutorialRepositoryModule::class,
            CommunalWidgetRepositoryModule::class,
            CommunalDatabaseModule::class,
            CommunalPrefsRepositoryModule::class,
        ]
)
interface CommunalModule {
+108 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.systemui.communal.data.repository

import android.content.Context
import android.content.SharedPreferences
import android.content.pm.UserInfo
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserFileManagerExt.observeSharedPreferences
import com.android.systemui.user.data.repository.UserRepository
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext

/**
 * Stores simple preferences for the current user in communal hub. For use cases like "has the CTA
 * tile been dismissed?"
 */
interface CommunalPrefsRepository {

    /** Whether the CTA tile has been dismissed. */
    val isCtaDismissed: Flow<Boolean>

    /** Save the CTA tile dismissed state for the current user. */
    suspend fun setCtaDismissedForCurrentUser()
}

@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class CommunalPrefsRepositoryImpl
@Inject
constructor(
    @Background private val backgroundScope: CoroutineScope,
    @Background private val bgDispatcher: CoroutineDispatcher,
    private val userRepository: UserRepository,
    private val userFileManager: UserFileManager,
) : CommunalPrefsRepository {

    override val isCtaDismissed: Flow<Boolean> =
        userRepository.selectedUserInfo
            .flatMapLatest(::observeCtaDismissState)
            .stateIn(
                scope = backgroundScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = false,
            )

    override suspend fun setCtaDismissedForCurrentUser() =
        withContext(bgDispatcher) {
            getSharedPrefsForUser(userRepository.getSelectedUserInfo())
                .edit()
                .putBoolean(CTA_DISMISSED_STATE, true)
                .apply()
        }

    private fun observeCtaDismissState(user: UserInfo): Flow<Boolean> =
        userFileManager
            .observeSharedPreferences(FILE_NAME, Context.MODE_PRIVATE, user.id)
            // Emit at the start of collection to ensure we get an initial value
            .onStart { emit(Unit) }
            .map { getCtaDismissedState() }
            .flowOn(bgDispatcher)

    private suspend fun getCtaDismissedState(): Boolean =
        withContext(bgDispatcher) {
            getSharedPrefsForUser(userRepository.getSelectedUserInfo())
                .getBoolean(CTA_DISMISSED_STATE, false)
        }

    private fun getSharedPrefsForUser(user: UserInfo): SharedPreferences {
        return userFileManager.getSharedPreferences(
            FILE_NAME,
            Context.MODE_PRIVATE,
            user.id,
        )
    }

    companion object {
        const val TAG = "CommunalRepository"
        const val FILE_NAME = "communal_hub_prefs"
        const val CTA_DISMISSED_STATE = "cta_dismissed"
    }
}
Loading