• 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.shared.customization.data.content
19 
20 import android.annotation.SuppressLint
21 import android.content.ContentValues
22 import android.content.Context
23 import android.content.Intent
24 import android.database.ContentObserver
25 import android.graphics.Color
26 import android.graphics.drawable.Drawable
27 import android.net.Uri
28 import android.util.Log
29 import androidx.annotation.DrawableRes
30 import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract
31 import java.net.URISyntaxException
32 import kotlinx.coroutines.CoroutineDispatcher
33 import kotlinx.coroutines.channels.awaitClose
34 import kotlinx.coroutines.flow.Flow
35 import kotlinx.coroutines.flow.callbackFlow
36 import kotlinx.coroutines.flow.map
37 import kotlinx.coroutines.flow.onStart
38 import kotlinx.coroutines.withContext
39 
40 /** Client for using a content provider implementing the [Contract]. */
41 interface CustomizationProviderClient {
42 
43     /**
44      * Selects an affordance with the given ID for a slot on the lock screen with the given ID.
45      *
46      * Note that the maximum number of selected affordances on this slot is automatically enforced.
47      * Selecting a slot that is already full (e.g. already has a number of selected affordances at
48      * its maximum capacity) will automatically remove the oldest selected affordance before adding
49      * the one passed in this call. Additionally, selecting an affordance that's already one of the
50      * selected affordances on the slot will move the selected affordance to the newest location in
51      * the slot.
52      */
53     suspend fun insertSelection(
54         slotId: String,
55         affordanceId: String,
56     )
57 
58     /** Returns all available slots supported by the device. */
59     suspend fun querySlots(): List<Slot>
60 
61     /** Returns the list of flags. */
62     suspend fun queryFlags(): List<Flag>
63 
64     /**
65      * Returns [Flow] for observing the collection of slots.
66      *
67      * @see [querySlots]
68      */
69     fun observeSlots(): Flow<List<Slot>>
70 
71     /**
72      * Returns [Flow] for observing the collection of flags.
73      *
74      * @see [queryFlags]
75      */
76     fun observeFlags(): Flow<List<Flag>>
77 
78     /**
79      * Returns all available affordances supported by the device, regardless of current slot
80      * placement.
81      */
82     suspend fun queryAffordances(): List<Affordance>
83 
84     /**
85      * Returns [Flow] for observing the collection of affordances.
86      *
87      * @see [queryAffordances]
88      */
89     fun observeAffordances(): Flow<List<Affordance>>
90 
91     /** Returns the current slot-affordance selections. */
92     suspend fun querySelections(): List<Selection>
93 
94     /**
95      * Returns [Flow] for observing the collection of selections.
96      *
97      * @see [querySelections]
98      */
99     fun observeSelections(): Flow<List<Selection>>
100 
101     /** Unselects an affordance with the given ID from the slot with the given ID. */
102     suspend fun deleteSelection(
103         slotId: String,
104         affordanceId: String,
105     )
106 
107     /** Unselects all affordances from the slot with the given ID. */
108     suspend fun deleteAllSelections(
109         slotId: String,
110     )
111 
112     /** Returns a [Drawable] with the given ID, loaded from the system UI package. */
113     suspend fun getAffordanceIcon(
114         @DrawableRes iconResourceId: Int,
115         tintColor: Int = Color.WHITE,
116     ): Drawable
117 
118     /** Models a slot. A position that quick affordances can be positioned in. */
119     data class Slot(
120         /** Unique ID of the slot. */
121         val id: String,
122         /**
123          * The maximum number of quick affordances that are allowed to be positioned in this slot.
124          */
125         val capacity: Int,
126     )
127 
128     /**
129      * Models a quick affordance. An action that can be selected by the user to appear in one or
130      * more slots on the lock screen.
131      */
132     data class Affordance(
133         /** Unique ID of the quick affordance. */
134         val id: String,
135         /** User-facing label for this affordance. */
136         val name: String,
137         /**
138          * Resource ID for the user-facing icon for this affordance. This resource is hosted by the
139          * System UI process so it must be used with
140          * `PackageManager.getResourcesForApplication(String)`.
141          */
142         val iconResourceId: Int,
143         /**
144          * Whether the affordance is enabled. Disabled affordances should be shown on the picker but
145          * should be rendered as "disabled". When tapped, the enablement properties should be used
146          * to populate UI that would explain to the user what to do in order to re-enable this
147          * affordance.
148          */
149         val isEnabled: Boolean = true,
150         /**
151          * If the affordance is disabled, this is a set of instruction messages to be shown to the
152          * user when the disabled affordance is selected. The instructions should help the user
153          * figure out what to do in order to re-neable this affordance.
154          */
155         val enablementInstructions: List<String>? = null,
156         /**
157          * If the affordance is disabled, this is a label for a button shown together with the set
158          * of instruction messages when the disabled affordance is selected. The button should help
159          * send the user to a flow that would help them achieve the instructions and re-enable this
160          * affordance.
161          *
162          * If `null`, the button should not be shown.
163          */
164         val enablementActionText: String? = null,
165         /**
166          * If the affordance is disabled, this is a "component name" of the format
167          * `packageName/action` to be used as an `Intent` for `startActivity` when the action button
168          * (shown together with the set of instruction messages when the disabled affordance is
169          * selected) is clicked by the user. The button should help send the user to a flow that
170          * would help them achieve the instructions and re-enable this affordance.
171          *
172          * If `null`, the button should not be shown.
173          */
174         val enablementActionComponentName: String? = null,
175         /** Optional [Intent] to use to start an activity to configure this affordance. */
176         val configureIntent: Intent? = null,
177     )
178 
179     /** Models a selection of a quick affordance on a slot. */
180     data class Selection(
181         /** The unique ID of the slot. */
182         val slotId: String,
183         /** The unique ID of the quick affordance. */
184         val affordanceId: String,
185         /** The user-visible label for the quick affordance. */
186         val affordanceName: String,
187     )
188 
189     /** Models a System UI flag. */
190     data class Flag(
191         /** The name of the flag. */
192         val name: String,
193         /** The value of the flag. */
194         val value: Boolean,
195     )
196 }
197 
198 class CustomizationProviderClientImpl(
199     private val context: Context,
200     private val backgroundDispatcher: CoroutineDispatcher,
201 ) : CustomizationProviderClient {
202 
insertSelectionnull203     override suspend fun insertSelection(
204         slotId: String,
205         affordanceId: String,
206     ) {
207         withContext(backgroundDispatcher) {
208             context.contentResolver.insert(
209                 Contract.LockScreenQuickAffordances.SelectionTable.URI,
210                 ContentValues().apply {
211                     put(Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID, slotId)
212                     put(
213                         Contract.LockScreenQuickAffordances.SelectionTable.Columns.AFFORDANCE_ID,
214                         affordanceId
215                     )
216                 }
217             )
218         }
219     }
220 
querySlotsnull221     override suspend fun querySlots(): List<CustomizationProviderClient.Slot> {
222         return withContext(backgroundDispatcher) {
223             context.contentResolver
224                 .query(
225                     Contract.LockScreenQuickAffordances.SlotTable.URI,
226                     null,
227                     null,
228                     null,
229                     null,
230                 )
231                 ?.use { cursor ->
232                     buildList {
233                         val idColumnIndex =
234                             cursor.getColumnIndex(
235                                 Contract.LockScreenQuickAffordances.SlotTable.Columns.ID
236                             )
237                         val capacityColumnIndex =
238                             cursor.getColumnIndex(
239                                 Contract.LockScreenQuickAffordances.SlotTable.Columns.CAPACITY
240                             )
241                         if (idColumnIndex == -1 || capacityColumnIndex == -1) {
242                             return@buildList
243                         }
244 
245                         while (cursor.moveToNext()) {
246                             add(
247                                 CustomizationProviderClient.Slot(
248                                     id = cursor.getString(idColumnIndex),
249                                     capacity = cursor.getInt(capacityColumnIndex),
250                                 )
251                             )
252                         }
253                     }
254                 }
255         }
256             ?: emptyList()
257     }
258 
queryFlagsnull259     override suspend fun queryFlags(): List<CustomizationProviderClient.Flag> {
260         return withContext(backgroundDispatcher) {
261             context.contentResolver
262                 .query(
263                     Contract.FlagsTable.URI,
264                     null,
265                     null,
266                     null,
267                     null,
268                 )
269                 ?.use { cursor ->
270                     buildList {
271                         val nameColumnIndex =
272                             cursor.getColumnIndex(Contract.FlagsTable.Columns.NAME)
273                         val valueColumnIndex =
274                             cursor.getColumnIndex(Contract.FlagsTable.Columns.VALUE)
275                         if (nameColumnIndex == -1 || valueColumnIndex == -1) {
276                             return@buildList
277                         }
278 
279                         while (cursor.moveToNext()) {
280                             add(
281                                 CustomizationProviderClient.Flag(
282                                     name = cursor.getString(nameColumnIndex),
283                                     value = cursor.getInt(valueColumnIndex) == 1,
284                                 )
285                             )
286                         }
287                     }
288                 }
289         }
290             ?: emptyList()
291     }
292 
observeSlotsnull293     override fun observeSlots(): Flow<List<CustomizationProviderClient.Slot>> {
294         return observeUri(Contract.LockScreenQuickAffordances.SlotTable.URI).map { querySlots() }
295     }
296 
observeFlagsnull297     override fun observeFlags(): Flow<List<CustomizationProviderClient.Flag>> {
298         return observeUri(Contract.FlagsTable.URI).map { queryFlags() }
299     }
300 
queryAffordancesnull301     override suspend fun queryAffordances(): List<CustomizationProviderClient.Affordance> {
302         return withContext(backgroundDispatcher) {
303             context.contentResolver
304                 .query(
305                     Contract.LockScreenQuickAffordances.AffordanceTable.URI,
306                     null,
307                     null,
308                     null,
309                     null,
310                 )
311                 ?.use { cursor ->
312                     buildList {
313                         val idColumnIndex =
314                             cursor.getColumnIndex(
315                                 Contract.LockScreenQuickAffordances.AffordanceTable.Columns.ID
316                             )
317                         val nameColumnIndex =
318                             cursor.getColumnIndex(
319                                 Contract.LockScreenQuickAffordances.AffordanceTable.Columns.NAME
320                             )
321                         val iconColumnIndex =
322                             cursor.getColumnIndex(
323                                 Contract.LockScreenQuickAffordances.AffordanceTable.Columns.ICON
324                             )
325                         val isEnabledColumnIndex =
326                             cursor.getColumnIndex(
327                                 Contract.LockScreenQuickAffordances.AffordanceTable.Columns
328                                     .IS_ENABLED
329                             )
330                         val enablementInstructionsColumnIndex =
331                             cursor.getColumnIndex(
332                                 Contract.LockScreenQuickAffordances.AffordanceTable.Columns
333                                     .ENABLEMENT_INSTRUCTIONS
334                             )
335                         val enablementActionTextColumnIndex =
336                             cursor.getColumnIndex(
337                                 Contract.LockScreenQuickAffordances.AffordanceTable.Columns
338                                     .ENABLEMENT_ACTION_TEXT
339                             )
340                         val enablementComponentNameColumnIndex =
341                             cursor.getColumnIndex(
342                                 Contract.LockScreenQuickAffordances.AffordanceTable.Columns
343                                     .ENABLEMENT_COMPONENT_NAME
344                             )
345                         val configureIntentColumnIndex =
346                             cursor.getColumnIndex(
347                                 Contract.LockScreenQuickAffordances.AffordanceTable.Columns
348                                     .CONFIGURE_INTENT
349                             )
350                         if (
351                             idColumnIndex == -1 ||
352                                 nameColumnIndex == -1 ||
353                                 iconColumnIndex == -1 ||
354                                 isEnabledColumnIndex == -1 ||
355                                 enablementInstructionsColumnIndex == -1 ||
356                                 enablementActionTextColumnIndex == -1 ||
357                                 enablementComponentNameColumnIndex == -1 ||
358                                 configureIntentColumnIndex == -1
359                         ) {
360                             return@buildList
361                         }
362 
363                         while (cursor.moveToNext()) {
364                             val affordanceId = cursor.getString(idColumnIndex)
365                             add(
366                                 CustomizationProviderClient.Affordance(
367                                     id = affordanceId,
368                                     name = cursor.getString(nameColumnIndex),
369                                     iconResourceId = cursor.getInt(iconColumnIndex),
370                                     isEnabled = cursor.getInt(isEnabledColumnIndex) == 1,
371                                     enablementInstructions =
372                                         cursor
373                                             .getString(enablementInstructionsColumnIndex)
374                                             ?.split(
375                                                 Contract.LockScreenQuickAffordances.AffordanceTable
376                                                     .ENABLEMENT_INSTRUCTIONS_DELIMITER
377                                             ),
378                                     enablementActionText =
379                                         cursor.getString(enablementActionTextColumnIndex),
380                                     enablementActionComponentName =
381                                         cursor.getString(enablementComponentNameColumnIndex),
382                                     configureIntent =
383                                         cursor
384                                             .getString(configureIntentColumnIndex)
385                                             ?.toIntent(affordanceId = affordanceId),
386                                 )
387                             )
388                         }
389                     }
390                 }
391         }
392             ?: emptyList()
393     }
394 
observeAffordancesnull395     override fun observeAffordances(): Flow<List<CustomizationProviderClient.Affordance>> {
396         return observeUri(Contract.LockScreenQuickAffordances.AffordanceTable.URI).map {
397             queryAffordances()
398         }
399     }
400 
querySelectionsnull401     override suspend fun querySelections(): List<CustomizationProviderClient.Selection> {
402         return withContext(backgroundDispatcher) {
403             context.contentResolver
404                 .query(
405                     Contract.LockScreenQuickAffordances.SelectionTable.URI,
406                     null,
407                     null,
408                     null,
409                     null,
410                 )
411                 ?.use { cursor ->
412                     buildList {
413                         val slotIdColumnIndex =
414                             cursor.getColumnIndex(
415                                 Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID
416                             )
417                         val affordanceIdColumnIndex =
418                             cursor.getColumnIndex(
419                                 Contract.LockScreenQuickAffordances.SelectionTable.Columns
420                                     .AFFORDANCE_ID
421                             )
422                         val affordanceNameColumnIndex =
423                             cursor.getColumnIndex(
424                                 Contract.LockScreenQuickAffordances.SelectionTable.Columns
425                                     .AFFORDANCE_NAME
426                             )
427                         if (
428                             slotIdColumnIndex == -1 ||
429                                 affordanceIdColumnIndex == -1 ||
430                                 affordanceNameColumnIndex == -1
431                         ) {
432                             return@buildList
433                         }
434 
435                         while (cursor.moveToNext()) {
436                             add(
437                                 CustomizationProviderClient.Selection(
438                                     slotId = cursor.getString(slotIdColumnIndex),
439                                     affordanceId = cursor.getString(affordanceIdColumnIndex),
440                                     affordanceName = cursor.getString(affordanceNameColumnIndex),
441                                 )
442                             )
443                         }
444                     }
445                 }
446         }
447             ?: emptyList()
448     }
449 
observeSelectionsnull450     override fun observeSelections(): Flow<List<CustomizationProviderClient.Selection>> {
451         return observeUri(Contract.LockScreenQuickAffordances.SelectionTable.URI).map {
452             querySelections()
453         }
454     }
455 
deleteSelectionnull456     override suspend fun deleteSelection(
457         slotId: String,
458         affordanceId: String,
459     ) {
460         withContext(backgroundDispatcher) {
461             context.contentResolver.delete(
462                 Contract.LockScreenQuickAffordances.SelectionTable.URI,
463                 "${Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID} = ? AND" +
464                     " ${Contract.LockScreenQuickAffordances.SelectionTable.Columns.AFFORDANCE_ID}" +
465                     " = ?",
466                 arrayOf(
467                     slotId,
468                     affordanceId,
469                 ),
470             )
471         }
472     }
473 
deleteAllSelectionsnull474     override suspend fun deleteAllSelections(
475         slotId: String,
476     ) {
477         withContext(backgroundDispatcher) {
478             context.contentResolver.delete(
479                 Contract.LockScreenQuickAffordances.SelectionTable.URI,
480                 Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID,
481                 arrayOf(
482                     slotId,
483                 ),
484             )
485         }
486     }
487 
488     @SuppressLint("UseCompatLoadingForDrawables")
getAffordanceIconnull489     override suspend fun getAffordanceIcon(
490         @DrawableRes iconResourceId: Int,
491         tintColor: Int,
492     ): Drawable {
493         return withContext(backgroundDispatcher) {
494             context.packageManager
495                 .getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME)
496                 .getDrawable(iconResourceId, context.theme)
497                 .apply { setTint(tintColor) }
498         }
499     }
500 
observeUrinull501     private fun observeUri(
502         uri: Uri,
503     ): Flow<Unit> {
504         return callbackFlow {
505                 val observer =
506                     object : ContentObserver(null) {
507                         override fun onChange(selfChange: Boolean) {
508                             trySend(Unit)
509                         }
510                     }
511 
512                 context.contentResolver.registerContentObserver(
513                     uri,
514                     /* notifyForDescendants= */ true,
515                     observer,
516                 )
517 
518                 awaitClose { context.contentResolver.unregisterContentObserver(observer) }
519             }
520             .onStart { emit(Unit) }
521     }
522 
toIntentnull523     private fun String.toIntent(
524         affordanceId: String,
525     ): Intent? {
526         return try {
527             Intent.parseUri(this, 0)
528         } catch (e: URISyntaxException) {
529             Log.w(TAG, "Cannot parse Uri into Intent for affordance with ID \"$affordanceId\"!")
530             null
531         }
532     }
533 
534     companion object {
535         private const val TAG = "CustomizationProviderClient"
536         private const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui"
537     }
538 }
539