• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.ControlsSettingsDialogManager
38 import com.android.systemui.controls.settings.ControlsSettingsRepository
39 import com.android.systemui.dagger.SysUISingleton
40 import com.android.systemui.dagger.qualifiers.Main
41 import com.android.systemui.flags.FeatureFlags
42 import com.android.systemui.flags.Flags
43 import com.android.systemui.plugins.ActivityStarter
44 import com.android.systemui.statusbar.VibratorHelper
45 import com.android.systemui.statusbar.policy.KeyguardStateController
46 import com.android.systemui.util.concurrency.DelayableExecutor
47 import com.android.wm.shell.TaskViewFactory
48 import java.util.Optional
49 import javax.inject.Inject
50 
51 @SysUISingleton
52 class ControlActionCoordinatorImpl @Inject constructor(
53         private val context: Context,
54         private val bgExecutor: DelayableExecutor,
55         @Main private val uiExecutor: DelayableExecutor,
56         private val activityStarter: ActivityStarter,
57         private val broadcastSender: BroadcastSender,
58         private val keyguardStateController: KeyguardStateController,
59         private val taskViewFactory: Optional<TaskViewFactory>,
60         private val controlsMetricsLogger: ControlsMetricsLogger,
61         private val vibrator: VibratorHelper,
62         private val controlsSettingsRepository: ControlsSettingsRepository,
63         private val controlsSettingsDialogManager: ControlsSettingsDialogManager,
64         private val featureFlags: FeatureFlags,
65 ) : ControlActionCoordinator {
66     private var dialog: Dialog? = null
67     private var pendingAction: Action? = null
68     private var actionsInProgress = mutableSetOf<String>()
69     private val isLocked: Boolean
70         get() = !keyguardStateController.isUnlocked()
71     private val allowTrivialControls: Boolean
72         get() = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value
73     override lateinit var activityContext: Context
74 
75     companion object {
76         private const val RESPONSE_TIMEOUT_IN_MILLIS = 3000L
77     }
78 
79     override fun closeDialogs() {
80         if (!featureFlags.isEnabled(Flags.USE_APP_PANELS)) {
81             controlsSettingsDialogManager.closeDialog()
82         }
83         val isActivityFinishing =
84             (activityContext as? Activity)?.let { it.isFinishing || it.isDestroyed }
85         if (isActivityFinishing == true) {
86             dialog = null
87             return
88         }
89         if (dialog?.isShowing == true) {
90             dialog?.dismiss()
91             dialog = null
92         }
93     }
94 
95     override fun toggle(cvh: ControlViewHolder, templateId: String, isChecked: Boolean) {
96         controlsMetricsLogger.touch(cvh, isLocked)
97         bouncerOrRun(
98             createAction(
99                 cvh.cws.ci.controlId,
100                 {
101                     cvh.layout.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
102                     cvh.action(BooleanAction(templateId, !isChecked))
103                 },
104                 true /* blockable */,
105                 cvh.cws.control?.isAuthRequired ?: true /* authIsRequired */
106             )
107         )
108     }
109 
110     override fun touch(cvh: ControlViewHolder, templateId: String, control: Control) {
111         controlsMetricsLogger.touch(cvh, isLocked)
112         val blockable = cvh.usePanel()
113         bouncerOrRun(
114             createAction(
115                 cvh.cws.ci.controlId,
116                 {
117                     cvh.layout.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
118                     if (cvh.usePanel()) {
119                         showDetail(cvh, control.getAppIntent())
120                     } else {
121                         cvh.action(CommandAction(templateId))
122                     }
123                 },
124                 blockable /* blockable */,
125                 cvh.cws.control?.isAuthRequired ?: true /* authIsRequired */
126             )
127         )
128     }
129 
130     override fun drag(isEdge: Boolean) {
131         if (isEdge) {
132             vibrate(Vibrations.rangeEdgeEffect)
133         } else {
134             vibrate(Vibrations.rangeMiddleEffect)
135         }
136     }
137 
138     override fun setValue(cvh: ControlViewHolder, templateId: String, newValue: Float) {
139         controlsMetricsLogger.drag(cvh, isLocked)
140         bouncerOrRun(
141             createAction(
142                 cvh.cws.ci.controlId,
143                 { cvh.action(FloatAction(templateId, newValue)) },
144                 false /* blockable */,
145                 cvh.cws.control?.isAuthRequired ?: true /* authIsRequired */
146             )
147         )
148     }
149 
150     override fun longPress(cvh: ControlViewHolder) {
151         controlsMetricsLogger.longPress(cvh, isLocked)
152         bouncerOrRun(
153             createAction(
154                 cvh.cws.ci.controlId,
155                 {
156                     // Long press snould only be called when there is valid control state,
157                     // otherwise ignore
158                     cvh.cws.control?.let {
159                         cvh.layout.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
160                         showDetail(cvh, it.getAppIntent())
161                     }
162                 },
163                 false /* blockable */,
164                 cvh.cws.control?.isAuthRequired ?: true /* authIsRequired */
165             )
166         )
167     }
168 
169     override fun runPendingAction(controlId: String) {
170         if (isLocked) return
171         if (pendingAction?.controlId == controlId) {
172             showSettingsDialogIfNeeded(pendingAction!!)
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             showSettingsDialogIfNeeded(action)
212             action.invoke()
213         }
214     }
215 
216     private fun vibrate(effect: VibrationEffect) {
217         vibrator.vibrate(effect)
218     }
219 
220     private fun showDetail(cvh: ControlViewHolder, pendingIntent: PendingIntent) {
221         bgExecutor.execute {
222             val activities: List<ResolveInfo> = context.packageManager.queryIntentActivities(
223                 pendingIntent.getIntent(),
224                 PackageManager.MATCH_DEFAULT_ONLY
225             )
226 
227             uiExecutor.execute {
228                 // make sure the intent is valid before attempting to open the dialog
229                 if (activities.isNotEmpty() && taskViewFactory.isPresent) {
230                     taskViewFactory.get().create(context, uiExecutor, {
231                         dialog = DetailDialog(
232                             activityContext, broadcastSender,
233                             it, pendingIntent, cvh, keyguardStateController, activityStarter
234                         ).also {
235                             it.setOnDismissListener { _ -> dialog = null }
236                             it.show()
237                         }
238                     })
239                 } else {
240                     cvh.setErrorStatus()
241                 }
242             }
243         }
244     }
245 
246     private fun showSettingsDialogIfNeeded(action: Action) {
247         if (action.authIsRequired) {
248             return
249         }
250         if (!featureFlags.isEnabled(Flags.USE_APP_PANELS)) {
251             controlsSettingsDialogManager.maybeShowDialog(activityContext) {}
252         }
253     }
254 
255     @VisibleForTesting
256     fun createAction(
257         controlId: String,
258         f: () -> Unit,
259         blockable: Boolean,
260         authIsRequired: Boolean
261     ) = Action(controlId, f, blockable, authIsRequired)
262 
263     inner class Action(
264         val controlId: String,
265         val f: () -> Unit,
266         val blockable: Boolean,
267         val authIsRequired: Boolean
268     ) {
269         fun invoke() {
270             if (!blockable || shouldRunAction(controlId)) {
271                 f.invoke()
272             }
273         }
274     }
275 }
276