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

Commit 6e0baac9 authored by Coco Duan's avatar Coco Duan Committed by Android (Google) Code Review
Browse files

Merge "Persist CTA tile dismissed state" into main

parents 28cc50a1 7429825d
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