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