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