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