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.MainThread
20 import android.app.Dialog
21 import android.app.PendingIntent
22 import android.content.Context
23 import android.content.Intent
24 import android.content.pm.PackageManager
25 import android.content.pm.ResolveInfo
26 import android.os.VibrationEffect
27 import android.os.Vibrator
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.BroadcastDispatcher
36 import com.android.systemui.controls.ControlsMetricsLogger
37 import com.android.systemui.dagger.SysUISingleton
38 import com.android.systemui.dagger.qualifiers.Main
39 import com.android.systemui.globalactions.GlobalActionsComponent
40 import com.android.systemui.plugins.ActivityStarter
41 import com.android.systemui.statusbar.policy.KeyguardStateController
42 import com.android.systemui.util.concurrency.DelayableExecutor
43 import com.android.wm.shell.TaskViewFactory
44 import dagger.Lazy
45 import java.util.Optional
46 import javax.inject.Inject
47 
48 @SysUISingleton
49 class ControlActionCoordinatorImpl @Inject constructor(
50     private val context: Context,
51     private val bgExecutor: DelayableExecutor,
52     @Main private val uiExecutor: DelayableExecutor,
53     private val activityStarter: ActivityStarter,
54     private val keyguardStateController: KeyguardStateController,
55     private val globalActionsComponent: GlobalActionsComponent,
56     private val taskViewFactory: Optional<TaskViewFactory>,
57     private val broadcastDispatcher: BroadcastDispatcher,
58     private val lazyUiController: Lazy<ControlsUiController>,
59     private val controlsMetricsLogger: ControlsMetricsLogger
60 ) : ControlActionCoordinator {
61     private var dialog: Dialog? = null
62     private val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
63     private var pendingAction: Action? = null
64     private var actionsInProgress = mutableSetOf<String>()
65     private val isLocked: Boolean
66         get() = !keyguardStateController.isUnlocked()
67     override lateinit var activityContext: Context
68 
69     companion object {
70         private const val RESPONSE_TIMEOUT_IN_MILLIS = 3000L
71     }
72 
73     override fun closeDialogs() {
74         dialog?.dismiss()
75         dialog = null
76     }
77 
78     override fun toggle(cvh: ControlViewHolder, templateId: String, isChecked: Boolean) {
79         controlsMetricsLogger.touch(cvh, isLocked)
80         bouncerOrRun(createAction(cvh.cws.ci.controlId, {
81             cvh.layout.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
82             cvh.action(BooleanAction(templateId, !isChecked))
83         }, true /* blockable */))
84     }
85 
86     override fun touch(cvh: ControlViewHolder, templateId: String, control: Control) {
87         controlsMetricsLogger.touch(cvh, isLocked)
88         val blockable = cvh.usePanel()
89         bouncerOrRun(createAction(cvh.cws.ci.controlId, {
90             cvh.layout.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
91             if (cvh.usePanel()) {
92                 showDetail(cvh, control.getAppIntent())
93             } else {
94                 cvh.action(CommandAction(templateId))
95             }
96         }, blockable))
97     }
98 
99     override fun drag(isEdge: Boolean) {
100         if (isEdge) {
101             vibrate(Vibrations.rangeEdgeEffect)
102         } else {
103             vibrate(Vibrations.rangeMiddleEffect)
104         }
105     }
106 
107     override fun setValue(cvh: ControlViewHolder, templateId: String, newValue: Float) {
108         controlsMetricsLogger.drag(cvh, isLocked)
109         bouncerOrRun(createAction(cvh.cws.ci.controlId, {
110             cvh.action(FloatAction(templateId, newValue))
111         }, false /* blockable */))
112     }
113 
114     override fun longPress(cvh: ControlViewHolder) {
115         controlsMetricsLogger.longPress(cvh, isLocked)
116         bouncerOrRun(createAction(cvh.cws.ci.controlId, {
117             // Long press snould only be called when there is valid control state, otherwise ignore
118             cvh.cws.control?.let {
119                 cvh.layout.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
120                 showDetail(cvh, it.getAppIntent())
121             }
122         }, false /* blockable */))
123     }
124 
125     override fun runPendingAction(controlId: String) {
126         if (isLocked) return
127         if (pendingAction?.controlId == controlId) {
128             pendingAction?.invoke()
129             pendingAction = null
130         }
131     }
132 
133     @MainThread
134     override fun enableActionOnTouch(controlId: String) {
135         actionsInProgress.remove(controlId)
136     }
137 
138     private fun shouldRunAction(controlId: String) =
139         if (actionsInProgress.add(controlId)) {
140             uiExecutor.executeDelayed({
141                 actionsInProgress.remove(controlId)
142             }, RESPONSE_TIMEOUT_IN_MILLIS)
143             true
144         } else {
145             false
146         }
147 
148     @VisibleForTesting
149     fun bouncerOrRun(action: Action) {
150         if (keyguardStateController.isShowing()) {
151             if (isLocked) {
152                 context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
153 
154                 // pending actions will only run after the control state has been refreshed
155                 pendingAction = action
156             }
157             activityStarter.dismissKeyguardThenExecute({
158                 Log.d(ControlsUiController.TAG, "Device unlocked, invoking controls action")
159                 action.invoke()
160                 true
161             }, { pendingAction = null }, true /* afterKeyguardGone */)
162         } else {
163             action.invoke()
164         }
165     }
166 
167     private fun vibrate(effect: VibrationEffect) {
168         bgExecutor.execute { vibrator.vibrate(effect) }
169     }
170 
171     private fun showDetail(cvh: ControlViewHolder, pendingIntent: PendingIntent) {
172         bgExecutor.execute {
173             val activities: List<ResolveInfo> = context.packageManager.queryIntentActivities(
174                 pendingIntent.getIntent(),
175                 PackageManager.MATCH_DEFAULT_ONLY
176             )
177 
178             uiExecutor.execute {
179                 // make sure the intent is valid before attempting to open the dialog
180                 if (activities.isNotEmpty() && taskViewFactory.isPresent) {
181                     taskViewFactory.get().create(context, uiExecutor, {
182                         dialog = DetailDialog(activityContext, it, pendingIntent, cvh).also {
183                             it.setOnDismissListener { _ -> dialog = null }
184                             it.show()
185                         }
186                     })
187                 } else {
188                     cvh.setErrorStatus()
189                 }
190             }
191         }
192     }
193 
194     @VisibleForTesting
195     fun createAction(controlId: String, f: () -> Unit, blockable: Boolean) =
196         Action(controlId, f, blockable)
197 
198     inner class Action(val controlId: String, val f: () -> Unit, val blockable: Boolean) {
199         fun invoke() {
200             if (!blockable || shouldRunAction(controlId)) {
201                 f.invoke()
202             }
203         }
204     }
205 }
206