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