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