1 /* 2 * 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.settings.UserFileManager 31 import com.android.systemui.util.kotlin.SharedPreferencesExt.observe 32 import com.android.systemui.util.kotlin.emitOnStart 33 import javax.inject.Inject 34 import kotlinx.coroutines.CoroutineDispatcher 35 import kotlinx.coroutines.flow.Flow 36 import kotlinx.coroutines.flow.flatMapLatest 37 import kotlinx.coroutines.flow.flowOn 38 import kotlinx.coroutines.flow.map 39 import kotlinx.coroutines.flow.onEach 40 import kotlinx.coroutines.withContext 41 42 /** 43 * Stores simple preferences for the current user in communal hub. For use cases like "has the CTA 44 * tile been dismissed?" 45 */ 46 interface CommunalPrefsRepository { 47 48 /** Whether the CTA tile has been dismissed. */ isCtaDismissednull49 fun isCtaDismissed(user: UserInfo): Flow<Boolean> 50 51 /** Save the CTA tile dismissed state for the current user. */ 52 suspend fun setCtaDismissed(user: UserInfo) 53 54 /** Whether hub onboarding has been dismissed. */ 55 fun isHubOnboardingDismissed(user: UserInfo): Flow<Boolean> 56 57 /** Save the hub onboarding dismissed state for the current user. */ 58 suspend fun setHubOnboardingDismissed(user: UserInfo) 59 60 /** Whether dream button tooltip has been dismissed. */ 61 fun isDreamButtonTooltipDismissed(user: UserInfo): Flow<Boolean> 62 63 /** Save the dream button tooltip dismissed state for the current user. */ 64 suspend fun setDreamButtonTooltipDismissed(user: UserInfo) 65 } 66 67 @SysUISingleton 68 class CommunalPrefsRepositoryImpl 69 @Inject 70 constructor( 71 @Background private val bgDispatcher: CoroutineDispatcher, 72 private val userFileManager: UserFileManager, 73 broadcastDispatcher: BroadcastDispatcher, 74 @CommunalLog logBuffer: LogBuffer, 75 ) : CommunalPrefsRepository { 76 private val logger by lazy { Logger(logBuffer, TAG) } 77 78 /** 79 * Emits an event each time a Backup & Restore restoration job is completed, and once at the 80 * start of collection. 81 */ 82 private val backupRestorationEvents: Flow<Unit> = 83 broadcastDispatcher 84 .broadcastFlow( 85 filter = IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED), 86 flags = Context.RECEIVER_NOT_EXPORTED, 87 permission = BackupHelper.PERMISSION_SELF, 88 ) 89 .onEach { logger.i("Restored state for communal preferences.") } 90 .emitOnStart() 91 92 override fun isCtaDismissed(user: UserInfo): Flow<Boolean> = 93 readKeyForUser(user, CTA_DISMISSED_STATE) 94 95 override suspend fun setCtaDismissed(user: UserInfo) = 96 setBooleanKeyValueForUser(user, CTA_DISMISSED_STATE, "Dismissed CTA tile") 97 98 override fun isHubOnboardingDismissed(user: UserInfo): Flow<Boolean> = 99 readKeyForUser(user, HUB_ONBOARDING_DISMISSED_STATE) 100 101 override suspend fun setHubOnboardingDismissed(user: UserInfo) = 102 setBooleanKeyValueForUser(user, HUB_ONBOARDING_DISMISSED_STATE, "Dismissed hub onboarding") 103 104 override fun isDreamButtonTooltipDismissed(user: UserInfo): Flow<Boolean> = 105 readKeyForUser(user, DREAM_BUTTON_TOOLTIP_DISMISSED_STATE) 106 107 override suspend fun setDreamButtonTooltipDismissed(user: UserInfo) = 108 setBooleanKeyValueForUser( 109 user, 110 DREAM_BUTTON_TOOLTIP_DISMISSED_STATE, 111 "Dismissed dream button tooltip", 112 ) 113 114 private fun getSharedPrefsForUser(user: UserInfo): SharedPreferences { 115 return userFileManager.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE, user.id) 116 } 117 118 private suspend fun setBooleanKeyValueForUser(user: UserInfo, key: String, logMsg: String) = 119 withContext(bgDispatcher) { 120 getSharedPrefsForUser(user).edit().putBoolean(key, true).apply() 121 logger.i(logMsg) 122 } 123 124 private fun readKeyForUser(user: UserInfo, key: String): Flow<Boolean> { 125 return backupRestorationEvents 126 .flatMapLatest { 127 val sharedPrefs = getSharedPrefsForUser(user) 128 sharedPrefs.observe().emitOnStart().map { sharedPrefs.getBoolean(key, false) } 129 } 130 .flowOn(bgDispatcher) 131 } 132 133 companion object { 134 const val TAG = "CommunalPrefsRepository" 135 const val FILE_NAME = "communal_hub_prefs" 136 const val CTA_DISMISSED_STATE = "cta_dismissed" 137 const val HUB_ONBOARDING_DISMISSED_STATE = "hub_onboarding_dismissed" 138 const val DREAM_BUTTON_TOOLTIP_DISMISSED_STATE = "dream_button_tooltip_dismissed_state" 139 } 140 } 141