1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.systemui.communal.data.repository 17 18 import android.provider.Settings 19 import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED 20 import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED 21 import android.provider.Settings.Secure.HubModeTutorialState 22 import com.android.systemui.dagger.SysUISingleton 23 import com.android.systemui.dagger.qualifiers.Application 24 import com.android.systemui.dagger.qualifiers.Background 25 import com.android.systemui.log.LogBuffer 26 import com.android.systemui.log.core.Logger 27 import com.android.systemui.log.dagger.CommunalLog 28 import com.android.systemui.log.dagger.CommunalTableLog 29 import com.android.systemui.log.table.TableLogBuffer 30 import com.android.systemui.log.table.logDiffsForTable 31 import com.android.systemui.user.data.repository.UserRepository 32 import com.android.systemui.util.settings.SecureSettings 33 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow 34 import javax.inject.Inject 35 import kotlinx.coroutines.CoroutineDispatcher 36 import kotlinx.coroutines.CoroutineScope 37 import kotlinx.coroutines.ExperimentalCoroutinesApi 38 import kotlinx.coroutines.flow.Flow 39 import kotlinx.coroutines.flow.SharingStarted 40 import kotlinx.coroutines.flow.StateFlow 41 import kotlinx.coroutines.flow.emptyFlow 42 import kotlinx.coroutines.flow.filterNotNull 43 import kotlinx.coroutines.flow.flatMapLatest 44 import kotlinx.coroutines.flow.map 45 import kotlinx.coroutines.flow.onStart 46 import kotlinx.coroutines.flow.shareIn 47 import kotlinx.coroutines.flow.stateIn 48 import kotlinx.coroutines.withContext 49 50 /** 51 * Repository for the current state of hub mode tutorial. Valid states are defined in 52 * [HubModeTutorialState]. 53 */ 54 interface CommunalTutorialRepository { 55 /** Emits the tutorial state stored in Settings */ 56 val tutorialSettingState: StateFlow<Int> 57 58 /** Update the tutorial state */ 59 suspend fun setTutorialState(@HubModeTutorialState state: Int) 60 } 61 62 @OptIn(ExperimentalCoroutinesApi::class) 63 @SysUISingleton 64 class CommunalTutorialRepositoryImpl 65 @Inject 66 constructor( 67 @Application private val applicationScope: CoroutineScope, 68 @Background private val backgroundDispatcher: CoroutineDispatcher, 69 private val userRepository: UserRepository, 70 private val secureSettings: SecureSettings, 71 @CommunalLog logBuffer: LogBuffer, 72 @CommunalTableLog tableLogBuffer: TableLogBuffer, 73 ) : CommunalTutorialRepository { 74 75 companion object { 76 private const val TAG = "CommunalTutorialRepository" 77 78 const val MIN_TUTORIAL_VERSION = HUB_MODE_TUTORIAL_COMPLETED 79 80 // A version number which ensures that users, regardless of their completion of previous 81 // versions, see the updated tutorial when this number is bumped. 82 const val CURRENT_TUTORIAL_VERSION = MIN_TUTORIAL_VERSION + 1 83 } 84 85 private data class SettingsState( 86 @HubModeTutorialState val hubModeTutorialState: Int? = null, 87 ) 88 89 private val logger = Logger(logBuffer, TAG) 90 91 private val settingsState: Flow<SettingsState> = 92 userRepository.selectedUserInfo userInfonull93 .flatMapLatest { userInfo -> observeSettings(userInfo.id) } 94 .shareIn(scope = applicationScope, started = SharingStarted.WhileSubscribed()) 95 96 /** Emits the state of tutorial state in settings */ 97 override val tutorialSettingState: StateFlow<Int> = 98 settingsState <lambda>null99 .map { it.hubModeTutorialState } 100 .filterNotNull() 101 .logDiffsForTable( 102 tableLogBuffer = tableLogBuffer, 103 columnPrefix = "", 104 columnName = "tutorialSettingState", 105 initialValue = HUB_MODE_TUTORIAL_NOT_STARTED, 106 ) 107 .stateIn( 108 scope = applicationScope, 109 started = SharingStarted.WhileSubscribed(), 110 initialValue = HUB_MODE_TUTORIAL_NOT_STARTED, 111 ) 112 observeSettingsnull113 private fun observeSettings(userId: Int): Flow<SettingsState> = 114 secureSettings 115 .observerFlow( 116 userId = userId, 117 names = arrayOf(Settings.Secure.HUB_MODE_TUTORIAL_STATE), 118 ) 119 // Force an update 120 .onStart { emit(Unit) } <lambda>null121 .map { readFromSettings(userId) } 122 readFromSettingsnull123 private suspend fun readFromSettings(userId: Int): SettingsState = 124 withContext(backgroundDispatcher) { 125 var hubModeTutorialState = 126 secureSettings.getIntForUser( 127 Settings.Secure.HUB_MODE_TUTORIAL_STATE, 128 HUB_MODE_TUTORIAL_NOT_STARTED, 129 userId, 130 ) 131 132 if (hubModeTutorialState >= CURRENT_TUTORIAL_VERSION) { 133 // Tutorial is considered "completed" if the user has completed the current or a 134 // newer version. 135 hubModeTutorialState = HUB_MODE_TUTORIAL_COMPLETED 136 } else if (hubModeTutorialState >= MIN_TUTORIAL_VERSION) { 137 // Tutorial is considered "not started" if the user completed a version older than 138 // the current. 139 hubModeTutorialState = HUB_MODE_TUTORIAL_NOT_STARTED 140 } 141 val settingsState = SettingsState(hubModeTutorialState) 142 logger.d({ "Communal tutorial state for user $int1 in settings: $str1" }) { 143 int1 = userId 144 str1 = settingsState.hubModeTutorialState.toString() 145 } 146 147 settingsState 148 } 149 setTutorialStatenull150 override suspend fun setTutorialState(state: Int): Unit = 151 withContext(backgroundDispatcher) { 152 val userId = userRepository.getSelectedUserInfo().id 153 if (tutorialSettingState.value == state) { 154 return@withContext 155 } 156 val newState = 157 if (state == HUB_MODE_TUTORIAL_COMPLETED) CURRENT_TUTORIAL_VERSION else state 158 logger.d({ "Update communal tutorial state to $int1 for user $int2" }) { 159 int1 = newState 160 int2 = userId 161 } 162 secureSettings.putIntForUser( 163 Settings.Secure.HUB_MODE_TUTORIAL_STATE, 164 newState, 165 userId, 166 ) 167 } 168 } 169 170 // TODO(b/320769333): delete me and use the real repo above when tutorial is ready. 171 @SysUISingleton 172 class CommunalTutorialDisabledRepositoryImpl 173 @Inject 174 constructor( 175 @Application private val applicationScope: CoroutineScope, 176 ) : CommunalTutorialRepository { 177 override val tutorialSettingState: StateFlow<Int> = 178 emptyFlow<Int>() 179 .stateIn( 180 scope = applicationScope, 181 started = SharingStarted.WhileSubscribed(), 182 initialValue = HUB_MODE_TUTORIAL_COMPLETED, 183 ) 184 setTutorialStatenull185 override suspend fun setTutorialState(state: Int) { 186 // Do nothing 187 } 188 } 189