1 /*
<lambda>null2  * Copyright (C) 2022 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 
18 package com.android.systemui.keyguard.data.quickaffordance
19 
20 import android.content.Context
21 import android.content.IntentFilter
22 import android.content.SharedPreferences
23 import com.android.systemui.Flags
24 import com.android.systemui.backup.BackupHelper
25 import com.android.systemui.broadcast.BroadcastDispatcher
26 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
27 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
28 import com.android.systemui.dagger.SysUISingleton
29 import com.android.systemui.dagger.qualifiers.Application
30 import com.android.systemui.res.R
31 import com.android.systemui.settings.UserFileManager
32 import com.android.systemui.settings.UserTracker
33 import com.android.systemui.util.settings.SystemSettings
34 import javax.inject.Inject
35 import kotlinx.coroutines.ExperimentalCoroutinesApi
36 import kotlinx.coroutines.channels.awaitClose
37 import kotlinx.coroutines.flow.Flow
38 import kotlinx.coroutines.flow.combine
39 import kotlinx.coroutines.flow.flatMapLatest
40 import kotlinx.coroutines.flow.onStart
41 
42 /**
43  * Manages and provides access to the current "selections" of keyguard quick affordances, answering
44  * the question "which affordances should the keyguard show?" for the user associated with the
45  * System UI process.
46  */
47 @OptIn(ExperimentalCoroutinesApi::class)
48 @SysUISingleton
49 class KeyguardQuickAffordanceLocalUserSelectionManager
50 @Inject
51 constructor(
52     @Application private val context: Context,
53     private val userFileManager: UserFileManager,
54     private val userTracker: UserTracker,
55     private val systemSettings: SystemSettings,
56     broadcastDispatcher: BroadcastDispatcher,
57 ) : KeyguardQuickAffordanceSelectionManager {
58 
59     private var sharedPrefs: SharedPreferences = instantiateSharedPrefs()
60 
61     private val userId: Flow<Int> = conflatedCallbackFlow {
62         val callback =
63             object : UserTracker.Callback {
64                 override fun onUserChanged(newUser: Int, userContext: Context) {
65                     trySendWithFailureLogging(newUser, TAG)
66                 }
67             }
68 
69         userTracker.addCallback(callback) { it.run() }
70         trySendWithFailureLogging(userTracker.userId, TAG)
71 
72         awaitClose { userTracker.removeCallback(callback) }
73     }
74 
75     private val defaults: Map<String, List<String>> by lazy {
76         // Quick hack to allow testing out a lock screen shortcut to open the glanceable hub. This
77         // flag will not be rolled out and is only used for local testing.
78         // TODO(b/339667383): delete or properly implement this once a product decision is made
79         if (Flags.glanceableHubShortcutButton()) {
80             if (systemSettings.getBool("open_hub_chip_replace_home_controls", false)) {
81                 return@lazy mapOf(
82                     "bottom_start" to listOf("glanceable_hub"),
83                     "bottom_end" to listOf("create_note")
84                 )
85             } else {
86                 return@lazy mapOf(
87                     "bottom_start" to listOf("home"),
88                     "bottom_end" to listOf("glanceable_hub")
89                 )
90             }
91         }
92         context.resources
93             .getStringArray(R.array.config_keyguardQuickAffordanceDefaults)
94             .associate { item ->
95                 val splitUp = item.split(SLOT_AFFORDANCES_DELIMITER)
96                 check(splitUp.size == 2)
97                 val slotId = splitUp[0]
98                 val affordanceIds = splitUp[1].split(AFFORDANCE_DELIMITER)
99                 slotId to affordanceIds
100             }
101     }
102 
103     /**
104      * Emits an event each time a Backup & Restore restoration job is completed. Does not emit an
105      * initial value.
106      */
107     private val backupRestorationEvents: Flow<Unit> =
108         broadcastDispatcher.broadcastFlow(
109             filter = IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED),
110             flags = Context.RECEIVER_NOT_EXPORTED,
111             permission = BackupHelper.PERMISSION_SELF,
112         )
113 
114     override val selections: Flow<Map<String, List<String>>> =
115         combine(
116                 userId,
117                 backupRestorationEvents.onStart {
118                     // We emit an initial event to make sure that the combine emits at least once,
119                     // even if we never get a Backup & Restore restoration event (which is the most
120                     // common case anyway as restoration really only happens on initial device
121                     // setup).
122                     emit(Unit)
123                 }
124             ) { _, _ ->
125             }
126             .flatMapLatest {
127                 conflatedCallbackFlow {
128                     // We want to instantiate a new SharedPreferences instance each time either the
129                     // user ID changes or we have a backup & restore restoration event. The reason
130                     // is that our sharedPrefs instance needs to be replaced with a new one as it
131                     // depends on the user ID and when the B&R job completes, the backing file is
132                     // replaced but the existing instance still has a stale in-memory cache.
133                     sharedPrefs = instantiateSharedPrefs()
134 
135                     val listener =
136                         SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
137                             trySend(getSelections())
138                         }
139 
140                     sharedPrefs.registerOnSharedPreferenceChangeListener(listener)
141                     send(getSelections())
142 
143                     awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) }
144                 }
145             }
146 
147     override fun getSelections(): Map<String, List<String>> {
148         // If the custom shortcuts feature is not enabled, ignore prior selections and use defaults
149         if (!context.resources.getBoolean(R.bool.custom_lockscreen_shortcuts_enabled)) {
150             return defaults
151         }
152 
153         val slotKeys = sharedPrefs.all.keys.filter { it.startsWith(KEY_PREFIX_SLOT) }
154         val result =
155             slotKeys
156                 .associate { key ->
157                     val slotId = key.substring(KEY_PREFIX_SLOT.length)
158                     val value = sharedPrefs.getString(key, null)
159                     val affordanceIds =
160                         if (!value.isNullOrEmpty()) {
161                             value.split(AFFORDANCE_DELIMITER)
162                         } else {
163                             emptyList()
164                         }
165                     slotId to affordanceIds
166                 }
167                 .toMutableMap()
168 
169         // If the result map is missing keys, it means that the system has never set anything for
170         // those slots. This is where we need examine our defaults and see if there should be a
171         // default value for the affordances in the slot IDs that are missing from the result.
172         //
173         // Once the user makes any selection for a slot, even when they select "None", this class
174         // will persist a key for that slot ID. In the case of "None", it will have a value of the
175         // empty string. This is why this system works.
176         defaults.forEach { (slotId, affordanceIds) ->
177             if (!result.containsKey(slotId)) {
178                 result[slotId] = affordanceIds
179             }
180         }
181 
182         return result
183     }
184 
185     override fun setSelections(
186         slotId: String,
187         affordanceIds: List<String>,
188     ) {
189         val key = "$KEY_PREFIX_SLOT$slotId"
190         val value = affordanceIds.joinToString(AFFORDANCE_DELIMITER)
191         sharedPrefs.edit().putString(key, value).apply()
192     }
193 
194     private fun instantiateSharedPrefs(): SharedPreferences {
195         return userFileManager.getSharedPreferences(
196             FILE_NAME,
197             Context.MODE_PRIVATE,
198             userTracker.userId,
199         )
200     }
201 
202     companion object {
203         private const val TAG = "KeyguardQuickAffordancePrimaryUserSelectionManager"
204         const val FILE_NAME = "quick_affordance_selections"
205         private const val KEY_PREFIX_SLOT = "slot_"
206         private const val SLOT_AFFORDANCES_DELIMITER = ":"
207         private const val AFFORDANCE_DELIMITER = ","
208     }
209 }
210