/* * Copyright (C) 2021 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.qs.external import android.app.Dialog import android.app.StatusBarManager import android.content.ComponentName import android.content.DialogInterface import android.graphics.drawable.Icon import android.os.RemoteException import android.util.Log import androidx.annotation.VisibleForTesting import com.android.internal.statusbar.IAddTileResultCallback import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.QSHost import com.android.systemui.qs.external.ui.dialog.TileRequestDialogDelegate import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.statusbar.phone.SystemUIDialog import java.io.PrintWriter import java.util.concurrent.atomic.AtomicBoolean import java.util.function.Consumer import javax.inject.Inject private const val TAG = "TileServiceRequestController" /** Controller to interface between [TileRequestDialogDelegate] and [QSHost]. */ class TileServiceRequestController( private val qsHost: QSHost, private val commandQueue: CommandQueue, private val commandRegistry: CommandRegistry, private val eventLogger: TileRequestDialogEventLogger, private val tileRequestDialogComposeDelegateFactory: TileRequestDialogDelegate.Factory, ) { companion object { const val ADD_TILE = StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED const val DONT_ADD_TILE = StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED const val TILE_ALREADY_ADDED = StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED const val DISMISSED = StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED } private var dialogCanceller: ((String) -> Unit)? = null private val commandQueueCallback = object : CommandQueue.Callbacks { override fun requestAddTile( callingUid: Int, componentName: ComponentName, appName: CharSequence, label: CharSequence, icon: Icon, callback: IAddTileResultCallback, ) { requestTileAdd(callingUid, componentName, appName, label, icon) { try { callback.onTileRequest(it) } catch (e: RemoteException) { Log.e(TAG, "Couldn't respond to request", e) } } } override fun cancelRequestAddTile(packageName: String) { dialogCanceller?.invoke(packageName) } } fun init() { commandRegistry.registerCommand("tile-service-add") { TileServiceRequestCommand() } commandQueue.addCallback(commandQueueCallback) } fun destroy() { commandRegistry.unregisterCommand("tile-service-add") commandQueue.removeCallback(commandQueueCallback) } private fun addTile(componentName: ComponentName) { qsHost.addTile(componentName, true) } @VisibleForTesting fun requestTileAdd( callingUid: Int, componentName: ComponentName, appName: CharSequence, label: CharSequence, icon: Icon?, callback: Consumer, ): SystemUIDialog? { val instanceId = eventLogger.newInstanceId() val packageName = componentName.packageName if (isTileAlreadyAdded(componentName)) { callback.accept(TILE_ALREADY_ADDED) eventLogger.logTileAlreadyAdded(packageName, instanceId) return null } val dialogResponse = SingleShotConsumer { response -> if (response == ADD_TILE) { addTile(componentName) } dialogCanceller = null eventLogger.logUserResponse(response, packageName, instanceId) callback.accept(response) } val tileData = TileData(callingUid, appName, label, icon, componentName.packageName) return createComposeDialog(tileData, dialogResponse).also { dialog -> dialogCanceller = { if (packageName == it) { dialog.cancel() } dialogCanceller = null } dialog.show() eventLogger.logDialogShown(packageName, instanceId) } } private fun createComposeDialog( tileData: TileData, responseHandler: SingleShotConsumer, ): SystemUIDialog { val dialogClickListener = DialogInterface.OnClickListener { _, which -> if (which == Dialog.BUTTON_POSITIVE) { responseHandler.accept(ADD_TILE) } else { responseHandler.accept(DONT_ADD_TILE) } } return tileRequestDialogComposeDelegateFactory .create(dialogListener = dialogClickListener, tiledata = tileData) .createDialog() .apply { setShowForAllUsers(true) setCanceledOnTouchOutside(true) setOnCancelListener { responseHandler.accept(DISMISSED) } // We want this in case the dialog is dismissed without it being cancelled (for // example // by going home or locking the device). We use a SingleShotConsumer so the response // is only sent once, with the first value. setOnDismissListener { responseHandler.accept(DISMISSED) } } } private fun isTileAlreadyAdded(componentName: ComponentName): Boolean { val spec = CustomTile.toSpec(componentName) return qsHost.indexOf(spec) != -1 } inner class TileServiceRequestCommand : Command { override fun execute(pw: PrintWriter, args: List) { val componentName: ComponentName = ComponentName.unflattenFromString(args[0]) ?: run { Log.w(TAG, "Malformed componentName ${args[0]}") return } requestTileAdd(0, componentName, args[1], args[2], null) { Log.d(TAG, "Response: $it") } } override fun help(pw: PrintWriter) { pw.println( "Usage: adb shell cmd statusbar tile-service-add " + "