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