1 /* <lambda>null2 * Copyright (C) 2024 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 17 package com.android.systemui.communal.data.repository 18 19 import android.content.Context 20 import android.content.IntentFilter 21 import android.content.SharedPreferences 22 import android.content.pm.UserInfo 23 import com.android.systemui.backup.BackupHelper 24 import com.android.systemui.broadcast.BroadcastDispatcher 25 import com.android.systemui.dagger.SysUISingleton 26 import com.android.systemui.dagger.qualifiers.Background 27 import com.android.systemui.log.LogBuffer 28 import com.android.systemui.log.core.Logger 29 import com.android.systemui.log.dagger.CommunalLog 30 import com.android.systemui.log.dagger.CommunalTableLog 31 import com.android.systemui.log.table.TableLogBuffer 32 import com.android.systemui.log.table.logDiffsForTable 33 import com.android.systemui.settings.UserFileManager 34 import com.android.systemui.user.data.repository.UserRepository 35 import com.android.systemui.util.kotlin.SharedPreferencesExt.observe 36 import com.android.systemui.util.kotlin.emitOnStart 37 import javax.inject.Inject 38 import kotlinx.coroutines.CoroutineDispatcher 39 import kotlinx.coroutines.CoroutineScope 40 import kotlinx.coroutines.ExperimentalCoroutinesApi 41 import kotlinx.coroutines.flow.Flow 42 import kotlinx.coroutines.flow.SharingStarted 43 import kotlinx.coroutines.flow.combine 44 import kotlinx.coroutines.flow.flatMapLatest 45 import kotlinx.coroutines.flow.flowOn 46 import kotlinx.coroutines.flow.map 47 import kotlinx.coroutines.flow.onEach 48 import kotlinx.coroutines.flow.onStart 49 import kotlinx.coroutines.flow.stateIn 50 import kotlinx.coroutines.withContext 51 52 /** 53 * Stores simple preferences for the current user in communal hub. For use cases like "has the CTA 54 * tile been dismissed?" 55 */ 56 interface CommunalPrefsRepository { 57 58 /** Whether the CTA tile has been dismissed. */ 59 val isCtaDismissed: Flow<Boolean> 60 61 /** Save the CTA tile dismissed state for the current user. */ 62 suspend fun setCtaDismissedForCurrentUser() 63 } 64 65 @OptIn(ExperimentalCoroutinesApi::class) 66 @SysUISingleton 67 class CommunalPrefsRepositoryImpl 68 @Inject 69 constructor( 70 @Background private val backgroundScope: CoroutineScope, 71 @Background private val bgDispatcher: CoroutineDispatcher, 72 private val userRepository: UserRepository, 73 private val userFileManager: UserFileManager, 74 broadcastDispatcher: BroadcastDispatcher, 75 @CommunalLog logBuffer: LogBuffer, 76 @CommunalTableLog tableLogBuffer: TableLogBuffer, 77 ) : CommunalPrefsRepository { 78 79 private val logger = Logger(logBuffer, "CommunalPrefsRepositoryImpl") 80 81 /** 82 * Emits an event each time a Backup & Restore restoration job is completed. Does not emit an 83 * initial value. 84 */ 85 private val backupRestorationEvents: Flow<Unit> = 86 broadcastDispatcher.broadcastFlow( 87 filter = IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED), 88 flags = Context.RECEIVER_NOT_EXPORTED, 89 permission = BackupHelper.PERMISSION_SELF, 90 ) 91 92 override val isCtaDismissed: Flow<Boolean> = 93 combine( 94 userRepository.selectedUserInfo, 95 // Make sure combine can emit even if we never get a Backup & Restore event, 96 // which is the most common case as restoration only happens on initial device 97 // setup. <lambda>null98 backupRestorationEvents.emitOnStart().onEach { 99 logger.i("Restored state for communal preferences.") 100 }, usernull101 ) { user, _ -> 102 user 103 } 104 .flatMapLatest(::observeCtaDismissState) 105 .logDiffsForTable( 106 tableLogBuffer = tableLogBuffer, 107 columnPrefix = "", 108 columnName = "isCtaDismissed", 109 initialValue = false, 110 ) 111 .stateIn( 112 scope = backgroundScope, 113 started = SharingStarted.WhileSubscribed(), 114 initialValue = false, 115 ) 116 setCtaDismissedForCurrentUsernull117 override suspend fun setCtaDismissedForCurrentUser() = 118 withContext(bgDispatcher) { 119 getSharedPrefsForUser(userRepository.getSelectedUserInfo()) 120 .edit() 121 .putBoolean(CTA_DISMISSED_STATE, true) 122 .apply() 123 124 logger.i("Dismissed CTA tile") 125 } 126 observeCtaDismissStatenull127 private fun observeCtaDismissState(user: UserInfo): Flow<Boolean> = 128 getSharedPrefsForUser(user) 129 .observe() 130 // Emit at the start of collection to ensure we get an initial value 131 .onStart { emit(Unit) } <lambda>null132 .map { getCtaDismissedState() } 133 .flowOn(bgDispatcher) 134 getCtaDismissedStatenull135 private suspend fun getCtaDismissedState(): Boolean = 136 withContext(bgDispatcher) { 137 getSharedPrefsForUser(userRepository.getSelectedUserInfo()) 138 .getBoolean(CTA_DISMISSED_STATE, false) 139 } 140 getSharedPrefsForUsernull141 private fun getSharedPrefsForUser(user: UserInfo): SharedPreferences { 142 return userFileManager.getSharedPreferences( 143 FILE_NAME, 144 Context.MODE_PRIVATE, 145 user.id, 146 ) 147 } 148 149 companion object { 150 const val TAG = "CommunalRepository" 151 const val FILE_NAME = "communal_hub_prefs" 152 const val CTA_DISMISSED_STATE = "cta_dismissed" 153 } 154 } 155