1 /* <lambda>null2 * Copyright (C) 2021 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 package com.android.systemui.qs.external 18 19 import android.app.Dialog 20 import android.app.IUriGrantsManager 21 import android.app.StatusBarManager 22 import android.content.ComponentName 23 import android.content.DialogInterface 24 import android.graphics.drawable.Icon 25 import android.os.RemoteException 26 import android.util.Log 27 import androidx.annotation.VisibleForTesting 28 import com.android.internal.statusbar.IAddTileResultCallback 29 import com.android.systemui.res.R 30 import com.android.systemui.dagger.SysUISingleton 31 import com.android.systemui.qs.QSHost 32 import com.android.systemui.statusbar.CommandQueue 33 import com.android.systemui.statusbar.commandline.Command 34 import com.android.systemui.statusbar.commandline.CommandRegistry 35 import com.android.systemui.statusbar.phone.SystemUIDialog 36 import java.io.PrintWriter 37 import java.util.concurrent.atomic.AtomicBoolean 38 import java.util.function.Consumer 39 import javax.inject.Inject 40 41 private const val TAG = "TileServiceRequestController" 42 43 /** 44 * Controller to interface between [TileRequestDialog] and [QSHost]. 45 */ 46 class TileServiceRequestController( 47 private val qsHost: QSHost, 48 private val commandQueue: CommandQueue, 49 private val commandRegistry: CommandRegistry, 50 private val eventLogger: TileRequestDialogEventLogger, 51 private val iUriGrantsManager: IUriGrantsManager, 52 private val dialogCreator: () -> TileRequestDialog = { TileRequestDialog(qsHost.context) } 53 ) { 54 55 companion object { 56 internal const val ADD_TILE = StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED 57 internal const val DONT_ADD_TILE = StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED 58 internal const val TILE_ALREADY_ADDED = 59 StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED 60 internal const val DISMISSED = StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED 61 } 62 63 private var dialogCanceller: ((String) -> Unit)? = null 64 65 private val commandQueueCallback = object : CommandQueue.Callbacks { requestAddTilenull66 override fun requestAddTile( 67 callingUid: Int, 68 componentName: ComponentName, 69 appName: CharSequence, 70 label: CharSequence, 71 icon: Icon, 72 callback: IAddTileResultCallback 73 ) { 74 requestTileAdd(callingUid, componentName, appName, label, icon) { 75 try { 76 callback.onTileRequest(it) 77 } catch (e: RemoteException) { 78 Log.e(TAG, "Couldn't respond to request", e) 79 } 80 } 81 } 82 cancelRequestAddTilenull83 override fun cancelRequestAddTile(packageName: String) { 84 dialogCanceller?.invoke(packageName) 85 } 86 } 87 initnull88 fun init() { 89 commandRegistry.registerCommand("tile-service-add") { TileServiceRequestCommand() } 90 commandQueue.addCallback(commandQueueCallback) 91 } 92 destroynull93 fun destroy() { 94 commandRegistry.unregisterCommand("tile-service-add") 95 commandQueue.removeCallback(commandQueueCallback) 96 } 97 addTilenull98 private fun addTile(componentName: ComponentName) { 99 qsHost.addTile(componentName, true) 100 } 101 102 @VisibleForTesting requestTileAddnull103 internal fun requestTileAdd( 104 callingUid: Int, 105 componentName: ComponentName, 106 appName: CharSequence, 107 label: CharSequence, 108 icon: Icon?, 109 callback: Consumer<Int> 110 ) { 111 val instanceId = eventLogger.newInstanceId() 112 val packageName = componentName.packageName 113 if (isTileAlreadyAdded(componentName)) { 114 callback.accept(TILE_ALREADY_ADDED) 115 eventLogger.logTileAlreadyAdded(packageName, instanceId) 116 return 117 } 118 val dialogResponse = SingleShotConsumer<Int> { response -> 119 if (response == ADD_TILE) { 120 addTile(componentName) 121 } 122 dialogCanceller = null 123 eventLogger.logUserResponse(response, packageName, instanceId) 124 callback.accept(response) 125 } 126 val tileData = TileRequestDialog.TileData( 127 callingUid, 128 appName, 129 label, 130 icon, 131 componentName.packageName, 132 ) 133 createDialog(tileData, dialogResponse).also { dialog -> 134 dialogCanceller = { 135 if (packageName == it) { 136 dialog.cancel() 137 } 138 dialogCanceller = null 139 } 140 }.show() 141 eventLogger.logDialogShown(packageName, instanceId) 142 } 143 createDialognull144 private fun createDialog( 145 tileData: TileRequestDialog.TileData, 146 responseHandler: SingleShotConsumer<Int> 147 ): SystemUIDialog { 148 val dialogClickListener = DialogInterface.OnClickListener { _, which -> 149 if (which == Dialog.BUTTON_POSITIVE) { 150 responseHandler.accept(ADD_TILE) 151 } else { 152 responseHandler.accept(DONT_ADD_TILE) 153 } 154 } 155 return dialogCreator().apply { 156 setTileData(tileData, iUriGrantsManager) 157 setShowForAllUsers(true) 158 setCanceledOnTouchOutside(true) 159 setOnCancelListener { responseHandler.accept(DISMISSED) } 160 // We want this in case the dialog is dismissed without it being cancelled (for example 161 // by going home or locking the device). We use a SingleShotConsumer so the response 162 // is only sent once, with the first value. 163 setOnDismissListener { responseHandler.accept(DISMISSED) } 164 setPositiveButton(R.string.qs_tile_request_dialog_add, dialogClickListener) 165 setNegativeButton(R.string.qs_tile_request_dialog_not_add, dialogClickListener) 166 } 167 } 168 isTileAlreadyAddednull169 private fun isTileAlreadyAdded(componentName: ComponentName): Boolean { 170 val spec = CustomTile.toSpec(componentName) 171 return qsHost.indexOf(spec) != -1 172 } 173 174 inner class TileServiceRequestCommand : Command { executenull175 override fun execute(pw: PrintWriter, args: List<String>) { 176 val componentName: ComponentName = ComponentName.unflattenFromString(args[0]) 177 ?: run { 178 Log.w(TAG, "Malformed componentName ${args[0]}") 179 return 180 } 181 requestTileAdd(0, componentName, args[1], args[2], null) { 182 Log.d(TAG, "Response: $it") 183 } 184 } 185 helpnull186 override fun help(pw: PrintWriter) { 187 pw.println("Usage: adb shell cmd statusbar tile-service-add " + 188 "<componentName> <appName> <label>") 189 } 190 } 191 192 private class SingleShotConsumer<T>(private val consumer: Consumer<T>) : Consumer<T> { 193 private val dispatched = AtomicBoolean(false) 194 acceptnull195 override fun accept(t: T) { 196 if (dispatched.compareAndSet(false, true)) { 197 consumer.accept(t) 198 } 199 } 200 } 201 202 @SysUISingleton 203 class Builder @Inject constructor( 204 private val commandQueue: CommandQueue, 205 private val commandRegistry: CommandRegistry, 206 private val iUriGrantsManager: IUriGrantsManager, 207 ) { createnull208 fun create(qsHost: QSHost): TileServiceRequestController { 209 return TileServiceRequestController( 210 qsHost, 211 commandQueue, 212 commandRegistry, 213 TileRequestDialogEventLogger(), 214 iUriGrantsManager, 215 ) 216 } 217 } 218 } 219