/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.android.systemui.keyguard import android.content.ContentProvider import android.content.ContentValues import android.content.Context import android.content.Intent import android.content.UriMatcher import android.content.pm.PackageManager import android.content.pm.ProviderInfo import android.database.Cursor import android.database.MatrixCursor import android.net.Uri import android.os.Binder import android.os.Bundle import android.util.Log import com.android.app.tracing.coroutines.runBlocking import com.android.systemui.SystemUIAppComponentFactoryBase import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCallback import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor import com.android.systemui.keyguard.ui.preview.KeyguardRemotePreviewManager import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher class CustomizationProvider : ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer { @Inject lateinit var interactor: KeyguardQuickAffordanceInteractor @Inject lateinit var previewManager: KeyguardRemotePreviewManager @Inject @Main lateinit var mainDispatcher: CoroutineDispatcher private lateinit var contextAvailableCallback: ContextAvailableCallback private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply { addURI( Contract.AUTHORITY, Contract.LockScreenQuickAffordances.qualifiedTablePath( Contract.LockScreenQuickAffordances.SlotTable.TABLE_NAME, ), MATCH_CODE_ALL_SLOTS, ) addURI( Contract.AUTHORITY, Contract.LockScreenQuickAffordances.qualifiedTablePath( Contract.LockScreenQuickAffordances.AffordanceTable.TABLE_NAME, ), MATCH_CODE_ALL_AFFORDANCES, ) addURI( Contract.AUTHORITY, Contract.LockScreenQuickAffordances.qualifiedTablePath( Contract.LockScreenQuickAffordances.SelectionTable.TABLE_NAME, ), MATCH_CODE_ALL_SELECTIONS, ) addURI( Contract.AUTHORITY, Contract.FlagsTable.TABLE_NAME, MATCH_CODE_ALL_FLAGS, ) } override fun onCreate(): Boolean { return true } override fun attachInfo(context: Context?, info: ProviderInfo?) { contextAvailableCallback.onContextAvailable(checkNotNull(context)) super.attachInfo(context, info) } override fun setContextAvailableCallback(callback: ContextAvailableCallback) { contextAvailableCallback = callback } override fun getType(uri: Uri): String? { val prefix = when (uriMatcher.match(uri)) { MATCH_CODE_ALL_SLOTS, MATCH_CODE_ALL_AFFORDANCES, MATCH_CODE_ALL_FLAGS, MATCH_CODE_ALL_SELECTIONS -> "vnd.android.cursor.dir/vnd." else -> null } val tableName = when (uriMatcher.match(uri)) { MATCH_CODE_ALL_SLOTS -> Contract.LockScreenQuickAffordances.qualifiedTablePath( Contract.LockScreenQuickAffordances.SlotTable.TABLE_NAME, ) MATCH_CODE_ALL_AFFORDANCES -> Contract.LockScreenQuickAffordances.qualifiedTablePath( Contract.LockScreenQuickAffordances.AffordanceTable.TABLE_NAME, ) MATCH_CODE_ALL_SELECTIONS -> Contract.LockScreenQuickAffordances.qualifiedTablePath( Contract.LockScreenQuickAffordances.SelectionTable.TABLE_NAME, ) MATCH_CODE_ALL_FLAGS -> Contract.FlagsTable.TABLE_NAME else -> null } if (prefix == null || tableName == null) { return null } return "$prefix${Contract.AUTHORITY}.$tableName" } override fun insert(uri: Uri, values: ContentValues?): Uri? { if (uriMatcher.match(uri) != MATCH_CODE_ALL_SELECTIONS) { throw UnsupportedOperationException() } return runBlocking("$TAG#insert", mainDispatcher) { insertSelection(values) } } override fun query( uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?, ): Cursor? { return runBlocking("$TAG#query", mainDispatcher) { when (uriMatcher.match(uri)) { MATCH_CODE_ALL_AFFORDANCES -> queryAffordances() MATCH_CODE_ALL_SLOTS -> querySlots() MATCH_CODE_ALL_SELECTIONS -> querySelections() MATCH_CODE_ALL_FLAGS -> queryFlags() else -> null } } } override fun update( uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?, ): Int { Log.e(TAG, "Update is not supported!") return 0 } override fun delete( uri: Uri, selection: String?, selectionArgs: Array?, ): Int { if (uriMatcher.match(uri) != MATCH_CODE_ALL_SELECTIONS) { throw UnsupportedOperationException() } return runBlocking("$TAG#delete", mainDispatcher) { deleteSelection(uri, selectionArgs) } } override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { return if ( requireContext() .checkPermission( android.Manifest.permission.BIND_WALLPAPER, Binder.getCallingPid(), Binder.getCallingUid(), ) == PackageManager.PERMISSION_GRANTED ) { previewManager.preview(extras) } else { null } } private suspend fun insertSelection(values: ContentValues?): Uri? { if (values == null) { throw IllegalArgumentException("Cannot insert selection, no values passed in!") } if ( !values.containsKey(Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID) ) { throw IllegalArgumentException( "Cannot insert selection, " + "\"${Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID}\"" + " not specified!" ) } if ( !values.containsKey( Contract.LockScreenQuickAffordances.SelectionTable.Columns.AFFORDANCE_ID ) ) { throw IllegalArgumentException( "Cannot insert selection, " + "\"${Contract.LockScreenQuickAffordances .SelectionTable.Columns.AFFORDANCE_ID}\" not specified!" ) } val slotId = values.getAsString(Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID) val affordanceId = values.getAsString( Contract.LockScreenQuickAffordances.SelectionTable.Columns.AFFORDANCE_ID ) if (slotId.isNullOrEmpty()) { throw IllegalArgumentException("Cannot insert selection, slot ID was empty!") } if (affordanceId.isNullOrEmpty()) { throw IllegalArgumentException("Cannot insert selection, affordance ID was empty!") } val success = interactor.select( slotId = slotId, affordanceId = affordanceId, ) return if (success) { Log.d(TAG, "Successfully selected $affordanceId for slot $slotId") context ?.contentResolver ?.notifyChange(Contract.LockScreenQuickAffordances.SelectionTable.URI, null) Contract.LockScreenQuickAffordances.SelectionTable.URI } else { Log.d(TAG, "Failed to select $affordanceId for slot $slotId") null } } private suspend fun querySelections(): Cursor { return MatrixCursor( arrayOf( Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID, Contract.LockScreenQuickAffordances.SelectionTable.Columns.AFFORDANCE_ID, Contract.LockScreenQuickAffordances.SelectionTable.Columns.AFFORDANCE_NAME, ) ) .apply { val affordanceRepresentationsBySlotId = interactor.getSelections() affordanceRepresentationsBySlotId.entries.forEach { (slotId, affordanceRepresentations) -> affordanceRepresentations.forEach { affordanceRepresentation -> addRow( arrayOf( slotId, affordanceRepresentation.id, affordanceRepresentation.name, ) ) } } } } private suspend fun queryAffordances(): Cursor { return MatrixCursor( arrayOf( Contract.LockScreenQuickAffordances.AffordanceTable.Columns.ID, Contract.LockScreenQuickAffordances.AffordanceTable.Columns.NAME, Contract.LockScreenQuickAffordances.AffordanceTable.Columns.ICON, Contract.LockScreenQuickAffordances.AffordanceTable.Columns.IS_ENABLED, Contract.LockScreenQuickAffordances.AffordanceTable.Columns .ENABLEMENT_EXPLANATION, Contract.LockScreenQuickAffordances.AffordanceTable.Columns .ENABLEMENT_ACTION_TEXT, Contract.LockScreenQuickAffordances.AffordanceTable.Columns .ENABLEMENT_ACTION_INTENT, Contract.LockScreenQuickAffordances.AffordanceTable.Columns.CONFIGURE_INTENT, ) ) .apply { interactor.getAffordancePickerRepresentations().forEach { representation -> addRow( arrayOf( representation.id, representation.name, representation.iconResourceId, if (representation.isEnabled) 1 else 0, representation.explanation, representation.actionText, representation.actionIntent?.toUri(Intent.URI_INTENT_SCHEME), representation.configureIntent?.toUri(Intent.URI_INTENT_SCHEME), ) ) } } } private suspend fun querySlots(): Cursor { return MatrixCursor( arrayOf( Contract.LockScreenQuickAffordances.SlotTable.Columns.ID, Contract.LockScreenQuickAffordances.SlotTable.Columns.CAPACITY, ) ) .apply { interactor.getSlotPickerRepresentations().forEach { representation -> addRow( arrayOf( representation.id, representation.maxSelectedAffordances, ) ) } } } private suspend fun queryFlags(): Cursor { return MatrixCursor( arrayOf( Contract.FlagsTable.Columns.NAME, Contract.FlagsTable.Columns.VALUE, ) ) .apply { interactor.getPickerFlags().forEach { flag -> addRow( arrayOf( flag.name, if (flag.value) { 1 } else { 0 }, ) ) } } } private suspend fun deleteSelection( uri: Uri, selectionArgs: Array?, ): Int { if (selectionArgs == null) { throw IllegalArgumentException( "Cannot delete selection, selection arguments not included!" ) } val (slotId, affordanceId) = when (selectionArgs.size) { 1 -> Pair(selectionArgs[0], null) 2 -> Pair(selectionArgs[0], selectionArgs[1]) else -> throw IllegalArgumentException( "Cannot delete selection, selection arguments has wrong size, expected to" + " have 1 or 2 arguments, had ${selectionArgs.size} instead!" ) } val deleted = interactor.unselect( slotId = slotId, affordanceId = affordanceId, ) return if (deleted) { Log.d(TAG, "Successfully unselected $affordanceId for slot $slotId") context?.contentResolver?.notifyChange(uri, null) 1 } else { Log.d(TAG, "Failed to unselect $affordanceId for slot $slotId") 0 } } companion object { private const val TAG = "KeyguardQuickAffordanceProvider" private const val MATCH_CODE_ALL_SLOTS = 1 private const val MATCH_CODE_ALL_AFFORDANCES = 2 private const val MATCH_CODE_ALL_SELECTIONS = 3 private const val MATCH_CODE_ALL_FLAGS = 4 } }