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

Commit af21c5a6 authored by Darrell Shi's avatar Darrell Shi
Browse files

Communal tutorial versioning and skipping

This change adds the ability to skip communal tutorials. It also
introduces the tutorial versioning so users have to complete the latest
tutorial version.

Test: atest CommunalTutorialRepositoryImplTest
Test: factory reset & enable communal flag -> do not see tutorial
Bug: 316219231
Fix: 316219231
Flag: ACONFIG com.android.systemui.communal_hub DEVELOPMENT
Change-Id: I698832a05dad21c88d76e370e41476ad2916898e
parent d39747f2
Loading
Loading
Loading
Loading
+63 −33
Original line number Diff line number Diff line
@@ -20,15 +20,16 @@ import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryImpl.Companion.CURRENT_TUTORIAL_VERSION
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.FakeLogBuffer
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.testKosmos
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.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -38,36 +39,37 @@ import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidJUnit4::class)
class CommunalTutorialRepositoryImplTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope

    private lateinit var secureSettings: FakeSettings
    private lateinit var userRepository: FakeUserRepository
    private lateinit var userTracker: FakeUserTracker
    private lateinit var logBuffer: LogBuffer

    private val testDispatcher = StandardTestDispatcher()
    private val testScope = TestScope(testDispatcher)
    private lateinit var underTest: CommunalTutorialRepositoryImpl

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)

        logBuffer = FakeLogBuffer.Factory.create()
        secureSettings = FakeSettings()
        userRepository = FakeUserRepository()
        val listOfUserInfo = listOf(MAIN_USER_INFO)
        userRepository.setUserInfos(listOfUserInfo)

        userTracker = FakeUserTracker()
        userTracker.set(
            userInfos = listOfUserInfo,
            selectedUserIndex = 0,
        underTest =
            CommunalTutorialRepositoryImpl(
                kosmos.applicationCoroutineScope,
                kosmos.testDispatcher,
                userRepository,
                secureSettings,
                logcatLogBuffer("CommunalTutorialRepositoryImplTest"),
            )
    }

    @Test
    fun tutorialSettingState_defaultToNotStarted() =
        testScope.runTest {
            val repository = initCommunalTutorialRepository()
            val tutorialSettingState = collectLastValue(repository.tutorialSettingState)()
            val tutorialSettingState by collectLastValue(underTest.tutorialSettingState)
            assertThat(tutorialSettingState)
                .isEqualTo(Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED)
        }
@@ -75,30 +77,54 @@ class CommunalTutorialRepositoryImplTest : SysuiTestCase() {
    @Test
    fun tutorialSettingState_whenTutorialSettingsUpdatedToStarted() =
        testScope.runTest {
            val repository = initCommunalTutorialRepository()
            setTutorialStateSetting(Settings.Secure.HUB_MODE_TUTORIAL_STARTED)
            val tutorialSettingState = collectLastValue(repository.tutorialSettingState)()
            underTest.setTutorialState(Settings.Secure.HUB_MODE_TUTORIAL_STARTED)
            val tutorialSettingState by collectLastValue(underTest.tutorialSettingState)
            assertThat(tutorialSettingState).isEqualTo(Settings.Secure.HUB_MODE_TUTORIAL_STARTED)
        }

    @Test
    fun tutorialSettingState_whenTutorialSettingsUpdatedToCompleted() =
        testScope.runTest {
            val repository = initCommunalTutorialRepository()
            setTutorialStateSetting(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
            val tutorialSettingState = collectLastValue(repository.tutorialSettingState)()
            underTest.setTutorialState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
            val tutorialSettingState by collectLastValue(underTest.tutorialSettingState)
            assertThat(tutorialSettingState).isEqualTo(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
        }

    private fun initCommunalTutorialRepository(): CommunalTutorialRepositoryImpl {
        return CommunalTutorialRepositoryImpl(
            testScope.backgroundScope,
            testDispatcher,
            userRepository,
            secureSettings,
            userTracker,
            logBuffer
        )
    @Test
    fun tutorialVersion_userCompletedCurrentVersion_stateCompleted() =
        testScope.runTest {
            // User completed the current version.
            setTutorialStateSetting(CURRENT_TUTORIAL_VERSION)

            // Verify tutorial state is completed.
            val tutorialSettingState by collectLastValue(underTest.tutorialSettingState)
            assertThat(tutorialSettingState).isEqualTo(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
        }

    @Test
    fun tutorialVersion_userCompletedPreviousVersion_stateNotStarted() =
        testScope.runTest {
            // User completed the previous version.
            setTutorialStateSetting(CURRENT_TUTORIAL_VERSION - 1)

            // Verify tutorial state is not started.
            val tutorialSettingState by collectLastValue(underTest.tutorialSettingState)
            assertThat(tutorialSettingState)
                .isEqualTo(Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED)
        }

    @Test
    fun tutorialVersion_uponTutorialCompletion_writeCurrentVersion() =
        testScope.runTest {
            // Tutorial not started.
            setTutorialStateSetting(Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED)

            // Tutorial completed.
            underTest.setTutorialState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)

            // Verify tutorial setting state is updated to current version.
            val settingState = getTutorialStateSetting()
            assertThat(settingState).isEqualTo(CURRENT_TUTORIAL_VERSION)
        }

    private fun setTutorialStateSetting(
@@ -108,6 +134,10 @@ class CommunalTutorialRepositoryImplTest : SysuiTestCase() {
        secureSettings.putIntForUser(Settings.Secure.HUB_MODE_TUTORIAL_STATE, state, user.id)
    }

    private fun getTutorialStateSetting(user: UserInfo = MAIN_USER_INFO): Int {
        return secureSettings.getIntForUser(Settings.Secure.HUB_MODE_TUTORIAL_STATE, user.id)
    }

    companion object {
        private val MAIN_USER_INFO =
            UserInfo(/* id= */ 0, /* name= */ "primary", /* flags= */ UserInfo.FLAG_MAIN)
+52 −18
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@
package com.android.systemui.communal.data.repository

import android.provider.Settings
import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED
import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED
import android.provider.Settings.Secure.HubModeTutorialState
import com.android.systemui.dagger.SysUISingleton
@@ -24,7 +25,6 @@ import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
import com.android.systemui.settings.UserTracker
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
@@ -35,6 +35,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
@@ -62,14 +63,19 @@ class CommunalTutorialRepositoryImpl
constructor(
    @Application private val applicationScope: CoroutineScope,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    userRepository: UserRepository,
    private val userRepository: UserRepository,
    private val secureSettings: SecureSettings,
    private val userTracker: UserTracker,
    @CommunalLog logBuffer: LogBuffer,
) : CommunalTutorialRepository {

    companion object {
        private const val TAG = "CommunalTutorialRepository"

        const val MIN_TUTORIAL_VERSION = HUB_MODE_TUTORIAL_COMPLETED

        // A version number which ensures that users, regardless of their completion of previous
        // versions, see the updated tutorial when this number is bumped.
        const val CURRENT_TUTORIAL_VERSION = MIN_TUTORIAL_VERSION + 1
    }

    private data class SettingsState(
@@ -80,7 +86,7 @@ constructor(

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

    /** Emits the state of tutorial state in settings */
@@ -91,31 +97,37 @@ constructor(
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = HUB_MODE_TUTORIAL_NOT_STARTED
                initialValue = HUB_MODE_TUTORIAL_NOT_STARTED,
            )

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

    private suspend fun readFromSettings(): SettingsState =
    private suspend fun readFromSettings(userId: Int): SettingsState =
        withContext(backgroundDispatcher) {
            val userId = userTracker.userId
            val hubModeTutorialState =
            var hubModeTutorialState =
                secureSettings.getIntForUser(
                    Settings.Secure.HUB_MODE_TUTORIAL_STATE,
                    HUB_MODE_TUTORIAL_NOT_STARTED,
                    userId,
                )

            if (hubModeTutorialState >= CURRENT_TUTORIAL_VERSION) {
                // Tutorial is considered "completed" if the user has completed the current or a
                // newer version.
                hubModeTutorialState = HUB_MODE_TUTORIAL_COMPLETED
            } else if (hubModeTutorialState >= MIN_TUTORIAL_VERSION) {
                // Tutorial is considered "not started" if the user completed a version older than
                // the current.
                hubModeTutorialState = HUB_MODE_TUTORIAL_NOT_STARTED
            }
            val settingsState = SettingsState(hubModeTutorialState)
            logger.d({ "Communal tutorial state for user $int1 in settings: $str1" }) {
                int1 = userId
@@ -127,18 +139,40 @@ constructor(

    override suspend fun setTutorialState(state: Int): Unit =
        withContext(backgroundDispatcher) {
            val userId = userTracker.userId
            val userId = userRepository.getSelectedUserInfo().id
            if (tutorialSettingState.value == state) {
                return@withContext
            }
            val newState =
                if (state == HUB_MODE_TUTORIAL_COMPLETED) CURRENT_TUTORIAL_VERSION else state
            logger.d({ "Update communal tutorial state to $int1 for user $int2" }) {
                int1 = state
                int1 = newState
                int2 = userId
            }
            secureSettings.putIntForUser(
                Settings.Secure.HUB_MODE_TUTORIAL_STATE,
                state,
                newState,
                userId,
            )
        }
}

// TODO(b/320769333): delete me and use the real repo above when tutorial is ready.
@SysUISingleton
class CommunalTutorialDisabledRepositoryImpl
@Inject
constructor(
    @Application private val applicationScope: CoroutineScope,
) : CommunalTutorialRepository {
    override val tutorialSettingState: StateFlow<Int> =
        emptyFlow<Int>()
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = HUB_MODE_TUTORIAL_COMPLETED,
            )

    override suspend fun setTutorialState(state: Int) {
        // Do nothing
    }
}
+4 −1
Original line number Diff line number Diff line
@@ -22,6 +22,9 @@ import dagger.Module

@Module
interface CommunalTutorialRepositoryModule {
    // TODO(b/320769333): use [CommunalTutorialRepositoryImpl] when tutorial is ready.
    @Binds
    fun communalTutorialRepository(impl: CommunalTutorialRepositoryImpl): CommunalTutorialRepository
    fun communalTutorialRepository(
        impl: CommunalTutorialDisabledRepositoryImpl
    ): CommunalTutorialRepository
}