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