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