1 /*
<lambda>null2  * Copyright (C) 2020 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.controls.ui
18 
19 import android.annotation.AnyThread
20 import android.annotation.MainThread
21 import android.app.Activity
22 import android.app.Dialog
23 import android.app.PendingIntent
24 import android.content.Context
25 import android.content.pm.PackageManager
26 import android.content.pm.ResolveInfo
27 import android.os.VibrationEffect
28 import android.service.controls.Control
29 import android.service.controls.actions.BooleanAction
30 import android.service.controls.actions.CommandAction
31 import android.service.controls.actions.FloatAction
32 import android.util.Log
33 import android.view.HapticFeedbackConstants
34 import com.android.internal.annotations.VisibleForTesting
35 import com.android.systemui.broadcast.BroadcastSender
36 import com.android.systemui.controls.ControlsMetricsLogger
37 import com.android.systemui.controls.settings.ControlsSettingsRepository
38 import com.android.systemui.dagger.SysUISingleton
39 import com.android.systemui.dagger.qualifiers.Background
40 import com.android.systemui.dagger.qualifiers.Main
41 import com.android.systemui.plugins.ActivityStarter
42 import com.android.systemui.statusbar.VibratorHelper
43 import com.android.systemui.statusbar.policy.KeyguardStateController
44 import com.android.systemui.util.concurrency.DelayableExecutor
45 import com.android.wm.shell.taskview.TaskViewFactory
46 import java.util.Optional
47 import javax.inject.Inject
48 
49 @SysUISingleton
50 class ControlActionCoordinatorImpl @Inject constructor(
51     private val context: Context,
52     @Background private val bgExecutor: DelayableExecutor,
53     @Main private val uiExecutor: DelayableExecutor,
54     private val activityStarter: ActivityStarter,
55     private val broadcastSender: BroadcastSender,
56     private val keyguardStateController: KeyguardStateController,
57     private val taskViewFactory: Optional<TaskViewFactory>,
58     private val controlsMetricsLogger: ControlsMetricsLogger,
59     private val vibrator: VibratorHelper,
60     private val controlsSettingsRepository: ControlsSettingsRepository,
61 ) : ControlActionCoordinator {
62     private var dialog: Dialog? = null
63     private var pendingAction: Action? = null
64     private var actionsInProgress = mutableSetOf<String>()
65     private val isLocked: Boolean
66         get() = !keyguardStateController.isUnlocked()
67     private val allowTrivialControls: Boolean
68         get() = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value
69     override lateinit var activityContext: Context
70 
71     companion object {
72         private const val RESPONSE_TIMEOUT_IN_MILLIS = 3000L
73     }
74 
75     override fun closeDialogs() {
76         val isActivityFinishing =
77             (activityContext as? Activity)?.let { it.isFinishing || it.isDestroyed }
78         if (isActivityFinishing == true) {
79             dialog = null
80             return
81         }
82         if (dialog?.isShowing == true) {
83             dialog?.dismiss()
84             dialog = null
85         }
86     }
87 
88     override fun toggle(cvh: ControlViewHolder, templateId: String, isChecked: Boolean) {
89         controlsMetricsLogger.touch(cvh, isLocked)
90         bouncerOrRun(
91             createAction(
92                 cvh.cws.ci.controlId,
93                 {
94                     cvh.layout.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
95                     cvh.action(BooleanAction(templateId, !isChecked))
96                 },
97                 true /* blockable */,
98                 cvh.cws.control?.isAuthRequired ?: true /* authIsRequired */
99             )
100         )
101     }
102 
103     override fun touch(cvh: ControlViewHolder, templateId: String, control: Control) {
104         controlsMetricsLogger.touch(cvh, isLocked)
105         val blockable = cvh.usePanel()
106         bouncerOrRun(
107             createAction(
108                 cvh.cws.ci.controlId,
109                 {
110                     cvh.layout.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
111                     if (cvh.usePanel()) {
112                         showDetail(cvh, control.getAppIntent())
113                     } else {
114                         cvh.action(CommandAction(templateId))
115                     }
116                 },
117                 blockable /* blockable */,
118                 cvh.cws.control?.isAuthRequired ?: true /* authIsRequired */
119             )
120         )
121     }
122 
123     override fun drag(cvh: ControlViewHolder, isEdge: Boolean) {
124         val constant =
125             if (isEdge)
126                 HapticFeedbackConstants.SEGMENT_TICK
127             else
128                 HapticFeedbackConstants.SEGMENT_FREQUENT_TICK
129         vibrator.performHapticFeedback(cvh.layout, constant)
130     }
131 
132     override fun setValue(cvh: ControlViewHolder, templateId: String, newValue: Float) {
133         controlsMetricsLogger.drag(cvh, isLocked)
134         bouncerOrRun(
135             createAction(
136                 cvh.cws.ci.controlId,
137                 { cvh.action(FloatAction(templateId, newValue)) },
138                 false /* blockable */,
139                 cvh.cws.control?.isAuthRequired ?: true /* authIsRequired */
140             )
141         )
142     }
143 
144     override fun longPress(cvh: ControlViewHolder) {
145         controlsMetricsLogger.longPress(cvh, isLocked)
146         bouncerOrRun(
147             createAction(
148                 cvh.cws.ci.controlId,
149                 {
150                     // Long press snould only be called when there is valid control state,
151                     // otherwise ignore
152                     cvh.cws.control?.let {
153                         cvh.layout.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
154                         showDetail(cvh, it.getAppIntent())
155                     }
156                 },
157                 false /* blockable */,
158                 cvh.cws.control?.isAuthRequired ?: true /* authIsRequired */
159             )
160         )
161     }
162 
163     override fun runPendingAction(controlId: String) {
164         if (isLocked) return
165         if (pendingAction?.controlId == controlId) {
166             pendingAction?.invoke()
167             pendingAction = null
168         }
169     }
170 
171     @MainThread
172     override fun enableActionOnTouch(controlId: String) {
173         actionsInProgress.remove(controlId)
174     }
175 
176     private fun shouldRunAction(controlId: String) =
177         if (actionsInProgress.add(controlId)) {
178             uiExecutor.executeDelayed({
179                 actionsInProgress.remove(controlId)
180             }, RESPONSE_TIMEOUT_IN_MILLIS)
181             true
182         } else {
183             false
184         }
185 
186     @AnyThread
187     @VisibleForTesting
188     fun bouncerOrRun(action: Action) {
189         val authRequired = action.authIsRequired || !allowTrivialControls
190 
191         if (keyguardStateController.isShowing() && authRequired) {
192             if (isLocked) {
193                 broadcastSender.closeSystemDialogs()
194 
195                 // pending actions will only run after the control state has been refreshed
196                 pendingAction = action
197             }
198             activityStarter.dismissKeyguardThenExecute({
199                 Log.d(ControlsUiController.TAG, "Device unlocked, invoking controls action")
200                 action.invoke()
201                 true
202             }, { pendingAction = null }, true /* afterKeyguardGone */)
203         } else {
204             action.invoke()
205         }
206     }
207 
208     private fun vibrate(effect: VibrationEffect) {
209         vibrator.vibrate(effect)
210     }
211 
212     private fun showDetail(cvh: ControlViewHolder, pendingIntent: PendingIntent) {
213         bgExecutor.execute {
214             val activities: List<ResolveInfo> = context.packageManager.queryIntentActivities(
215                 pendingIntent.getIntent(),
216                 PackageManager.MATCH_DEFAULT_ONLY
217             )
218 
219             uiExecutor.execute {
220                 // make sure the intent is valid before attempting to open the dialog
221                 if (activities.isNotEmpty() && taskViewFactory.isPresent) {
222                     taskViewFactory.get().create(context, uiExecutor, {
223                         dialog = DetailDialog(
224                             activityContext, broadcastSender,
225                             it, pendingIntent, cvh, keyguardStateController, activityStarter
226                         ).also {
227                             it.setOnDismissListener { _ -> dialog = null }
228                             it.show()
229                         }
230                     })
231                 } else {
232                     cvh.setErrorStatus()
233                 }
234             }
235         }
236     }
237 
238     @VisibleForTesting
239     fun createAction(
240         controlId: String,
241         f: () -> Unit,
242         blockable: Boolean,
243         authIsRequired: Boolean
244     ) = Action(controlId, f, blockable, authIsRequired)
245 
246     inner class Action(
247         val controlId: String,
248         val f: () -> Unit,
249         val blockable: Boolean,
250         val authIsRequired: Boolean
251     ) {
252         fun invoke() {
253             if (!blockable || shouldRunAction(controlId)) {
254                 f.invoke()
255             }
256         }
257     }
258 }
259