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