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