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.flowOn 37 import kotlinx.coroutines.flow.map 38 import kotlinx.coroutines.flow.onStart 39 import kotlinx.coroutines.withContext 40 41 /** Client for using a content provider implementing the [Contract]. */ 42 interface CustomizationProviderClient { 43 44 /** 45 * Selects an affordance with the given ID for a slot on the lock screen with the given ID. 46 * 47 * Note that the maximum number of selected affordances on this slot is automatically enforced. 48 * Selecting a slot that is already full (e.g. already has a number of selected affordances at 49 * its maximum capacity) will automatically remove the oldest selected affordance before adding 50 * the one passed in this call. Additionally, selecting an affordance that's already one of the 51 * selected affordances on the slot will move the selected affordance to the newest location in 52 * the slot. 53 */ 54 suspend fun insertSelection( 55 slotId: String, 56 affordanceId: String, 57 ) 58 59 /** Returns all available slots supported by the device. */ 60 suspend fun querySlots(): List<Slot> 61 62 /** Returns the list of flags. */ 63 suspend fun queryFlags(): List<Flag> 64 65 /** 66 * Returns [Flow] for observing the collection of slots. 67 * 68 * @see [querySlots] 69 */ 70 fun observeSlots(): Flow<List<Slot>> 71 72 /** 73 * Returns [Flow] for observing the collection of flags. 74 * 75 * @see [queryFlags] 76 */ 77 fun observeFlags(): Flow<List<Flag>> 78 79 /** 80 * Returns all available affordances supported by the device, regardless of current slot 81 * placement. 82 */ 83 suspend fun queryAffordances(): List<Affordance> 84 85 /** 86 * Returns [Flow] for observing the collection of affordances. 87 * 88 * @see [queryAffordances] 89 */ 90 fun observeAffordances(): Flow<List<Affordance>> 91 92 /** Returns the current slot-affordance selections. */ 93 suspend fun querySelections(): List<Selection> 94 95 /** 96 * Returns [Flow] for observing the collection of selections. 97 * 98 * @see [querySelections] 99 */ 100 fun observeSelections(): Flow<List<Selection>> 101 102 /** Unselects an affordance with the given ID from the slot with the given ID. */ 103 suspend fun deleteSelection( 104 slotId: String, 105 affordanceId: String, 106 ) 107 108 /** Unselects all affordances from the slot with the given ID. */ 109 suspend fun deleteAllSelections( 110 slotId: String, 111 ) 112 113 /** Returns a [Drawable] with the given ID, loaded from the system UI package. */ 114 suspend fun getAffordanceIcon( 115 @DrawableRes iconResourceId: Int, 116 tintColor: Int = Color.WHITE, 117 ): Drawable 118 119 /** Models a slot. A position that quick affordances can be positioned in. */ 120 data class Slot( 121 /** Unique ID of the slot. */ 122 val id: String, 123 /** 124 * The maximum number of quick affordances that are allowed to be positioned in this slot. 125 */ 126 val capacity: Int, 127 ) 128 129 /** 130 * Models a quick affordance. An action that can be selected by the user to appear in one or 131 * more slots on the lock screen. 132 */ 133 data class Affordance( 134 /** Unique ID of the quick affordance. */ 135 val id: String, 136 /** User-facing label for this affordance. */ 137 val name: String, 138 /** 139 * Resource ID for the user-facing icon for this affordance. This resource is hosted by the 140 * System UI process so it must be used with 141 * `PackageManager.getResourcesForApplication(String)`. 142 */ 143 val iconResourceId: Int, 144 /** 145 * Whether the affordance is enabled. Disabled affordances should be shown on the picker but 146 * should be rendered as "disabled". When tapped, the enablement properties should be used 147 * to populate UI that would explain to the user what to do in order to re-enable this 148 * affordance. 149 */ 150 val isEnabled: Boolean = true, 151 /** 152 * If the affordance is disabled, this is the explanation to be shown to the user when the 153 * disabled affordance is selected. The instructions should help the user figure out what to 154 * do in order to re-neable this affordance. 155 */ 156 val enablementExplanation: String? = null, 157 /** 158 * If the affordance is disabled, this is a label for a button shown together with the set 159 * of instruction messages when the disabled affordance is selected. The button should help 160 * send the user to a flow that would help them achieve the instructions and re-enable this 161 * affordance. 162 * 163 * If `null`, the button should not be shown. 164 */ 165 val enablementActionText: String? = null, 166 /** 167 * If the affordance is disabled, this is an [Intent] to be used with `startActivity` when 168 * the action button (shown together with the set of instruction messages when the disabled 169 * affordance is selected) is clicked by the user. The button should help send the user to a 170 * flow that would help them achieve the instructions and re-enable this affordance. 171 * 172 * If `null`, the button should not be shown. 173 */ 174 val enablementActionIntent: Intent? = 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 enablementExplanationColumnIndex = 331 cursor.getColumnIndex( 332 Contract.LockScreenQuickAffordances.AffordanceTable.Columns 333 .ENABLEMENT_EXPLANATION 334 ) 335 val enablementActionTextColumnIndex = 336 cursor.getColumnIndex( 337 Contract.LockScreenQuickAffordances.AffordanceTable.Columns 338 .ENABLEMENT_ACTION_TEXT 339 ) 340 val enablementActionIntentColumnIndex = 341 cursor.getColumnIndex( 342 Contract.LockScreenQuickAffordances.AffordanceTable.Columns 343 .ENABLEMENT_ACTION_INTENT 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 enablementExplanationColumnIndex == -1 || 356 enablementActionTextColumnIndex == -1 || 357 enablementActionIntentColumnIndex == -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 enablementExplanation = 372 cursor.getString(enablementExplanationColumnIndex), 373 enablementActionText = 374 cursor.getString(enablementActionTextColumnIndex), 375 enablementActionIntent = 376 cursor 377 .getString(enablementActionIntentColumnIndex) 378 ?.toIntent( 379 affordanceId = affordanceId, 380 ), 381 configureIntent = 382 cursor 383 .getString(configureIntentColumnIndex) 384 ?.toIntent( 385 affordanceId = affordanceId, 386 ), 387 ) 388 ) 389 } 390 } 391 } 392 } 393 ?: emptyList() 394 } 395 observeAffordancesnull396 override fun observeAffordances(): Flow<List<CustomizationProviderClient.Affordance>> { 397 return observeUri(Contract.LockScreenQuickAffordances.AffordanceTable.URI).map { 398 queryAffordances() 399 } 400 } 401 querySelectionsnull402 override suspend fun querySelections(): List<CustomizationProviderClient.Selection> { 403 return withContext(backgroundDispatcher) { 404 context.contentResolver 405 .query( 406 Contract.LockScreenQuickAffordances.SelectionTable.URI, 407 null, 408 null, 409 null, 410 null, 411 ) 412 ?.use { cursor -> 413 buildList { 414 val slotIdColumnIndex = 415 cursor.getColumnIndex( 416 Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID 417 ) 418 val affordanceIdColumnIndex = 419 cursor.getColumnIndex( 420 Contract.LockScreenQuickAffordances.SelectionTable.Columns 421 .AFFORDANCE_ID 422 ) 423 val affordanceNameColumnIndex = 424 cursor.getColumnIndex( 425 Contract.LockScreenQuickAffordances.SelectionTable.Columns 426 .AFFORDANCE_NAME 427 ) 428 if ( 429 slotIdColumnIndex == -1 || 430 affordanceIdColumnIndex == -1 || 431 affordanceNameColumnIndex == -1 432 ) { 433 return@buildList 434 } 435 436 while (cursor.moveToNext()) { 437 add( 438 CustomizationProviderClient.Selection( 439 slotId = cursor.getString(slotIdColumnIndex), 440 affordanceId = cursor.getString(affordanceIdColumnIndex), 441 affordanceName = cursor.getString(affordanceNameColumnIndex), 442 ) 443 ) 444 } 445 } 446 } 447 } 448 ?: emptyList() 449 } 450 observeSelectionsnull451 override fun observeSelections(): Flow<List<CustomizationProviderClient.Selection>> { 452 return observeUri(Contract.LockScreenQuickAffordances.SelectionTable.URI).map { 453 querySelections() 454 } 455 } 456 deleteSelectionnull457 override suspend fun deleteSelection( 458 slotId: String, 459 affordanceId: String, 460 ) { 461 withContext(backgroundDispatcher) { 462 context.contentResolver.delete( 463 Contract.LockScreenQuickAffordances.SelectionTable.URI, 464 "${Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID} = ? AND" + 465 " ${Contract.LockScreenQuickAffordances.SelectionTable.Columns.AFFORDANCE_ID}" + 466 " = ?", 467 arrayOf( 468 slotId, 469 affordanceId, 470 ), 471 ) 472 } 473 } 474 deleteAllSelectionsnull475 override suspend fun deleteAllSelections( 476 slotId: String, 477 ) { 478 withContext(backgroundDispatcher) { 479 context.contentResolver.delete( 480 Contract.LockScreenQuickAffordances.SelectionTable.URI, 481 Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID, 482 arrayOf( 483 slotId, 484 ), 485 ) 486 } 487 } 488 489 @SuppressLint("UseCompatLoadingForDrawables") getAffordanceIconnull490 override suspend fun getAffordanceIcon( 491 @DrawableRes iconResourceId: Int, 492 tintColor: Int, 493 ): Drawable { 494 return withContext(backgroundDispatcher) { 495 context.packageManager 496 .getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME) 497 .getDrawable(iconResourceId, context.theme) 498 .apply { setTint(tintColor) } 499 } 500 } 501 observeUrinull502 private fun observeUri( 503 uri: Uri, 504 ): Flow<Unit> { 505 return callbackFlow { 506 val observer = 507 object : ContentObserver(null) { 508 override fun onChange(selfChange: Boolean) { 509 trySend(Unit) 510 } 511 } 512 513 context.contentResolver.registerContentObserver( 514 uri, 515 /* notifyForDescendants= */ true, 516 observer, 517 ) 518 519 awaitClose { context.contentResolver.unregisterContentObserver(observer) } 520 } 521 .onStart { emit(Unit) } 522 .flowOn(backgroundDispatcher) 523 } 524 toIntentnull525 private fun String.toIntent( 526 affordanceId: String, 527 ): Intent? { 528 return try { 529 Intent.parseUri(this, Intent.URI_INTENT_SCHEME) 530 } catch (e: URISyntaxException) { 531 Log.w(TAG, "Cannot parse Uri into Intent for affordance with ID \"$affordanceId\"!") 532 null 533 } 534 } 535 536 companion object { 537 private const val TAG = "CustomizationProviderClient" 538 private const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui" 539 } 540 } 541