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