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