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