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