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