• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
19 
20 import android.content.ContentProvider
21 import android.content.ContentValues
22 import android.content.Context
23 import android.content.UriMatcher
24 import android.content.pm.PackageManager
25 import android.content.pm.ProviderInfo
26 import android.database.Cursor
27 import android.database.MatrixCursor
28 import android.net.Uri
29 import android.os.Binder
30 import android.os.Bundle
31 import android.util.Log
32 import com.android.systemui.SystemUIAppComponentFactoryBase
33 import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCallback
34 import com.android.systemui.dagger.qualifiers.Main
35 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
36 import com.android.systemui.keyguard.ui.preview.KeyguardRemotePreviewManager
37 import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract
38 import javax.inject.Inject
39 import kotlinx.coroutines.CoroutineDispatcher
40 import kotlinx.coroutines.runBlocking
41 
42 class CustomizationProvider :
43     ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer {
44 
45     @Inject lateinit var interactor: KeyguardQuickAffordanceInteractor
46     @Inject lateinit var previewManager: KeyguardRemotePreviewManager
47     @Inject @Main lateinit var mainDispatcher: CoroutineDispatcher
48 
49     private lateinit var contextAvailableCallback: ContextAvailableCallback
50 
51     private val uriMatcher =
52         UriMatcher(UriMatcher.NO_MATCH).apply {
53             addURI(
54                 Contract.AUTHORITY,
55                 Contract.LockScreenQuickAffordances.qualifiedTablePath(
56                     Contract.LockScreenQuickAffordances.SlotTable.TABLE_NAME,
57                 ),
58                 MATCH_CODE_ALL_SLOTS,
59             )
60             addURI(
61                 Contract.AUTHORITY,
62                 Contract.LockScreenQuickAffordances.qualifiedTablePath(
63                     Contract.LockScreenQuickAffordances.AffordanceTable.TABLE_NAME,
64                 ),
65                 MATCH_CODE_ALL_AFFORDANCES,
66             )
67             addURI(
68                 Contract.AUTHORITY,
69                 Contract.LockScreenQuickAffordances.qualifiedTablePath(
70                     Contract.LockScreenQuickAffordances.SelectionTable.TABLE_NAME,
71                 ),
72                 MATCH_CODE_ALL_SELECTIONS,
73             )
74             addURI(
75                 Contract.AUTHORITY,
76                 Contract.FlagsTable.TABLE_NAME,
77                 MATCH_CODE_ALL_FLAGS,
78             )
79         }
80 
81     override fun onCreate(): Boolean {
82         return true
83     }
84 
85     override fun attachInfo(context: Context?, info: ProviderInfo?) {
86         contextAvailableCallback.onContextAvailable(checkNotNull(context))
87         super.attachInfo(context, info)
88     }
89 
90     override fun setContextAvailableCallback(callback: ContextAvailableCallback) {
91         contextAvailableCallback = callback
92     }
93 
94     override fun getType(uri: Uri): String? {
95         val prefix =
96             when (uriMatcher.match(uri)) {
97                 MATCH_CODE_ALL_SLOTS,
98                 MATCH_CODE_ALL_AFFORDANCES,
99                 MATCH_CODE_ALL_FLAGS,
100                 MATCH_CODE_ALL_SELECTIONS -> "vnd.android.cursor.dir/vnd."
101                 else -> null
102             }
103 
104         val tableName =
105             when (uriMatcher.match(uri)) {
106                 MATCH_CODE_ALL_SLOTS ->
107                     Contract.LockScreenQuickAffordances.qualifiedTablePath(
108                         Contract.LockScreenQuickAffordances.SlotTable.TABLE_NAME,
109                     )
110                 MATCH_CODE_ALL_AFFORDANCES ->
111                     Contract.LockScreenQuickAffordances.qualifiedTablePath(
112                         Contract.LockScreenQuickAffordances.AffordanceTable.TABLE_NAME,
113                     )
114                 MATCH_CODE_ALL_SELECTIONS ->
115                     Contract.LockScreenQuickAffordances.qualifiedTablePath(
116                         Contract.LockScreenQuickAffordances.SelectionTable.TABLE_NAME,
117                     )
118                 MATCH_CODE_ALL_FLAGS -> Contract.FlagsTable.TABLE_NAME
119                 else -> null
120             }
121 
122         if (prefix == null || tableName == null) {
123             return null
124         }
125 
126         return "$prefix${Contract.AUTHORITY}.$tableName"
127     }
128 
129     override fun insert(uri: Uri, values: ContentValues?): Uri? {
130         if (uriMatcher.match(uri) != MATCH_CODE_ALL_SELECTIONS) {
131             throw UnsupportedOperationException()
132         }
133 
134         return runBlocking(mainDispatcher) { insertSelection(values) }
135     }
136 
137     override fun query(
138         uri: Uri,
139         projection: Array<out String>?,
140         selection: String?,
141         selectionArgs: Array<out String>?,
142         sortOrder: String?,
143     ): Cursor? {
144         return runBlocking(mainDispatcher) {
145             when (uriMatcher.match(uri)) {
146                 MATCH_CODE_ALL_AFFORDANCES -> queryAffordances()
147                 MATCH_CODE_ALL_SLOTS -> querySlots()
148                 MATCH_CODE_ALL_SELECTIONS -> querySelections()
149                 MATCH_CODE_ALL_FLAGS -> queryFlags()
150                 else -> null
151             }
152         }
153     }
154 
155     override fun update(
156         uri: Uri,
157         values: ContentValues?,
158         selection: String?,
159         selectionArgs: Array<out String>?,
160     ): Int {
161         Log.e(TAG, "Update is not supported!")
162         return 0
163     }
164 
165     override fun delete(
166         uri: Uri,
167         selection: String?,
168         selectionArgs: Array<out String>?,
169     ): Int {
170         if (uriMatcher.match(uri) != MATCH_CODE_ALL_SELECTIONS) {
171             throw UnsupportedOperationException()
172         }
173 
174         return runBlocking(mainDispatcher) { deleteSelection(uri, selectionArgs) }
175     }
176 
177     override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
178         return if (
179             requireContext()
180                 .checkPermission(
181                     android.Manifest.permission.BIND_WALLPAPER,
182                     Binder.getCallingPid(),
183                     Binder.getCallingUid(),
184                 ) == PackageManager.PERMISSION_GRANTED
185         ) {
186             previewManager.preview(extras)
187         } else {
188             null
189         }
190     }
191 
192     private suspend fun insertSelection(values: ContentValues?): Uri? {
193         if (values == null) {
194             throw IllegalArgumentException("Cannot insert selection, no values passed in!")
195         }
196 
197         if (
198             !values.containsKey(Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID)
199         ) {
200             throw IllegalArgumentException(
201                 "Cannot insert selection, " +
202                     "\"${Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID}\"" +
203                     " not specified!"
204             )
205         }
206 
207         if (
208             !values.containsKey(
209                 Contract.LockScreenQuickAffordances.SelectionTable.Columns.AFFORDANCE_ID
210             )
211         ) {
212             throw IllegalArgumentException(
213                 "Cannot insert selection, " +
214                     "\"${Contract.LockScreenQuickAffordances
215                         .SelectionTable.Columns.AFFORDANCE_ID}\" not specified!"
216             )
217         }
218 
219         val slotId =
220             values.getAsString(Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID)
221         val affordanceId =
222             values.getAsString(
223                 Contract.LockScreenQuickAffordances.SelectionTable.Columns.AFFORDANCE_ID
224             )
225 
226         if (slotId.isNullOrEmpty()) {
227             throw IllegalArgumentException("Cannot insert selection, slot ID was empty!")
228         }
229 
230         if (affordanceId.isNullOrEmpty()) {
231             throw IllegalArgumentException("Cannot insert selection, affordance ID was empty!")
232         }
233 
234         val success =
235             interactor.select(
236                 slotId = slotId,
237                 affordanceId = affordanceId,
238             )
239 
240         return if (success) {
241             Log.d(TAG, "Successfully selected $affordanceId for slot $slotId")
242             context
243                 ?.contentResolver
244                 ?.notifyChange(Contract.LockScreenQuickAffordances.SelectionTable.URI, null)
245             Contract.LockScreenQuickAffordances.SelectionTable.URI
246         } else {
247             Log.d(TAG, "Failed to select $affordanceId for slot $slotId")
248             null
249         }
250     }
251 
252     private suspend fun querySelections(): Cursor {
253         return MatrixCursor(
254                 arrayOf(
255                     Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID,
256                     Contract.LockScreenQuickAffordances.SelectionTable.Columns.AFFORDANCE_ID,
257                     Contract.LockScreenQuickAffordances.SelectionTable.Columns.AFFORDANCE_NAME,
258                 )
259             )
260             .apply {
261                 val affordanceRepresentationsBySlotId = interactor.getSelections()
262                 affordanceRepresentationsBySlotId.entries.forEach {
263                     (slotId, affordanceRepresentations) ->
264                     affordanceRepresentations.forEach { affordanceRepresentation ->
265                         addRow(
266                             arrayOf(
267                                 slotId,
268                                 affordanceRepresentation.id,
269                                 affordanceRepresentation.name,
270                             )
271                         )
272                     }
273                 }
274             }
275     }
276 
277     private suspend fun queryAffordances(): Cursor {
278         return MatrixCursor(
279                 arrayOf(
280                     Contract.LockScreenQuickAffordances.AffordanceTable.Columns.ID,
281                     Contract.LockScreenQuickAffordances.AffordanceTable.Columns.NAME,
282                     Contract.LockScreenQuickAffordances.AffordanceTable.Columns.ICON,
283                     Contract.LockScreenQuickAffordances.AffordanceTable.Columns.IS_ENABLED,
284                     Contract.LockScreenQuickAffordances.AffordanceTable.Columns
285                         .ENABLEMENT_INSTRUCTIONS,
286                     Contract.LockScreenQuickAffordances.AffordanceTable.Columns
287                         .ENABLEMENT_ACTION_TEXT,
288                     Contract.LockScreenQuickAffordances.AffordanceTable.Columns
289                         .ENABLEMENT_COMPONENT_NAME,
290                     Contract.LockScreenQuickAffordances.AffordanceTable.Columns.CONFIGURE_INTENT,
291                 )
292             )
293             .apply {
294                 interactor.getAffordancePickerRepresentations().forEach { representation ->
295                     addRow(
296                         arrayOf(
297                             representation.id,
298                             representation.name,
299                             representation.iconResourceId,
300                             if (representation.isEnabled) 1 else 0,
301                             representation.instructions?.joinToString(
302                                 Contract.LockScreenQuickAffordances.AffordanceTable
303                                     .ENABLEMENT_INSTRUCTIONS_DELIMITER
304                             ),
305                             representation.actionText,
306                             representation.actionComponentName,
307                             representation.configureIntent?.toUri(0),
308                         )
309                     )
310                 }
311             }
312     }
313 
314     private suspend fun querySlots(): Cursor {
315         return MatrixCursor(
316                 arrayOf(
317                     Contract.LockScreenQuickAffordances.SlotTable.Columns.ID,
318                     Contract.LockScreenQuickAffordances.SlotTable.Columns.CAPACITY,
319                 )
320             )
321             .apply {
322                 interactor.getSlotPickerRepresentations().forEach { representation ->
323                     addRow(
324                         arrayOf(
325                             representation.id,
326                             representation.maxSelectedAffordances,
327                         )
328                     )
329                 }
330             }
331     }
332 
333     private suspend fun queryFlags(): Cursor {
334         return MatrixCursor(
335                 arrayOf(
336                     Contract.FlagsTable.Columns.NAME,
337                     Contract.FlagsTable.Columns.VALUE,
338                 )
339             )
340             .apply {
341                 interactor.getPickerFlags().forEach { flag ->
342                     addRow(
343                         arrayOf(
344                             flag.name,
345                             if (flag.value) {
346                                 1
347                             } else {
348                                 0
349                             },
350                         )
351                     )
352                 }
353             }
354     }
355 
356     private suspend fun deleteSelection(
357         uri: Uri,
358         selectionArgs: Array<out String>?,
359     ): Int {
360         if (selectionArgs == null) {
361             throw IllegalArgumentException(
362                 "Cannot delete selection, selection arguments not included!"
363             )
364         }
365 
366         val (slotId, affordanceId) =
367             when (selectionArgs.size) {
368                 1 -> Pair(selectionArgs[0], null)
369                 2 -> Pair(selectionArgs[0], selectionArgs[1])
370                 else ->
371                     throw IllegalArgumentException(
372                         "Cannot delete selection, selection arguments has wrong size, expected to" +
373                             " have 1 or 2 arguments, had ${selectionArgs.size} instead!"
374                     )
375             }
376 
377         val deleted =
378             interactor.unselect(
379                 slotId = slotId,
380                 affordanceId = affordanceId,
381             )
382 
383         return if (deleted) {
384             Log.d(TAG, "Successfully unselected $affordanceId for slot $slotId")
385             context?.contentResolver?.notifyChange(uri, null)
386             1
387         } else {
388             Log.d(TAG, "Failed to unselect $affordanceId for slot $slotId")
389             0
390         }
391     }
392 
393     companion object {
394         private const val TAG = "KeyguardQuickAffordanceProvider"
395         private const val MATCH_CODE_ALL_SLOTS = 1
396         private const val MATCH_CODE_ALL_AFFORDANCES = 2
397         private const val MATCH_CODE_ALL_SELECTIONS = 3
398         private const val MATCH_CODE_ALL_FLAGS = 4
399     }
400 }
401