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