1 /* <lambda>null2 * Copyright (C) 2023 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.keyguard.ui.binder 18 19 import android.util.Log 20 import android.view.LayoutInflater 21 import android.view.View 22 import android.view.ViewGroup 23 import android.view.WindowManager 24 import android.window.OnBackInvokedCallback 25 import android.window.OnBackInvokedDispatcher 26 import androidx.constraintlayout.widget.ConstraintLayout 27 import androidx.constraintlayout.widget.ConstraintSet 28 import androidx.lifecycle.Lifecycle 29 import androidx.lifecycle.repeatOnLifecycle 30 import com.android.app.tracing.coroutines.launchTraced as launch 31 import com.android.systemui.CoreStartable 32 import com.android.systemui.dagger.SysUISingleton 33 import com.android.systemui.dagger.qualifiers.Application 34 import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder 35 import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay 36 import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel 37 import com.android.systemui.keyguard.ui.view.AlternateBouncerWindowViewLayoutParams 38 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView 39 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies 40 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel 41 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerWindowViewModel 42 import com.android.systemui.lifecycle.repeatWhenAttached 43 import com.android.systemui.log.TouchHandlingViewLogger 44 import com.android.systemui.res.R 45 import com.android.systemui.scene.shared.flag.SceneContainerFlag 46 import com.android.systemui.scrim.ScrimView 47 import dagger.Lazy 48 import javax.inject.Inject 49 import kotlinx.coroutines.CoroutineScope 50 51 /** 52 * When necessary, adds the alternate bouncer window above most other windows (including the 53 * notification shade, system UI dialogs) but below the UDFPS touch overlay and SideFPS indicator. 54 * Also binds the alternate bouncer view to its view-model. 55 * 56 * For devices that support UDFPS, this view includes a UDFPS view. 57 */ 58 @SysUISingleton 59 class AlternateBouncerViewBinder 60 @Inject 61 constructor( 62 @Application private val applicationScope: CoroutineScope, 63 private val alternateBouncerWindowViewModel: Lazy<AlternateBouncerWindowViewModel>, 64 private val alternateBouncerDependencies: Lazy<AlternateBouncerDependencies>, 65 private val windowManager: Lazy<WindowManager>, 66 private val layoutInflater: Lazy<LayoutInflater>, 67 ) : CoreStartable { 68 69 private var alternateBouncerView: ConstraintLayout? = null 70 71 override fun start() { 72 if (SceneContainerFlag.isEnabled) { 73 return 74 } 75 76 applicationScope.launch("$TAG#alternateBouncerWindowViewModel") { 77 alternateBouncerWindowViewModel.get().alternateBouncerWindowRequired.collect { 78 addAlternateBouncerWindowView -> 79 Log.d(TAG, "alternateBouncerWindowRequired=$addAlternateBouncerWindowView") 80 if (addAlternateBouncerWindowView) { 81 addViewToWindowManager() 82 val scrim: ScrimView = 83 alternateBouncerView!!.requireViewById(R.id.alternate_bouncer_scrim) 84 scrim.viewAlpha = 0f 85 bind(alternateBouncerView!!, alternateBouncerDependencies.get()) 86 } else { 87 removeViewFromWindowManager() 88 alternateBouncerDependencies.get().viewModel.onRemovedFromWindow() 89 } 90 } 91 } 92 } 93 94 private fun removeViewFromWindowManager() { 95 alternateBouncerView?.let { 96 alternateBouncerView = null 97 if (it.isAttachedToWindow) { 98 it.removeOnAttachStateChangeListener(onAttachAddBackGestureHandler) 99 Log.d(TAG, "Removing alternate bouncer view immediately") 100 windowManager.get().removeView(it) 101 } else { 102 // once the view is attached, remove it 103 it.addOnAttachStateChangeListener( 104 object : View.OnAttachStateChangeListener { 105 override fun onViewAttachedToWindow(view: View) { 106 it.removeOnAttachStateChangeListener(this) 107 it.removeOnAttachStateChangeListener(onAttachAddBackGestureHandler) 108 Log.d(TAG, "Removing alternate bouncer view on attached") 109 windowManager.get().removeView(it) 110 } 111 112 override fun onViewDetachedFromWindow(view: View) {} 113 } 114 ) 115 } 116 } 117 } 118 119 private val onAttachAddBackGestureHandler = 120 object : View.OnAttachStateChangeListener { 121 private val onBackInvokedCallback: OnBackInvokedCallback = OnBackInvokedCallback { 122 alternateBouncerDependencies.get().viewModel.onBackRequested() 123 } 124 125 override fun onViewAttachedToWindow(view: View) { 126 view 127 .findOnBackInvokedDispatcher() 128 ?.registerOnBackInvokedCallback( 129 OnBackInvokedDispatcher.PRIORITY_OVERLAY, 130 onBackInvokedCallback, 131 ) 132 } 133 134 override fun onViewDetachedFromWindow(view: View) { 135 view 136 .findOnBackInvokedDispatcher() 137 ?.unregisterOnBackInvokedCallback(onBackInvokedCallback) 138 } 139 } 140 141 private fun addViewToWindowManager() { 142 if (SceneContainerFlag.isEnabled) { 143 return 144 } 145 if (alternateBouncerView != null) { 146 return 147 } 148 149 alternateBouncerView = 150 layoutInflater.get().inflate(R.layout.alternate_bouncer, null, false) 151 as ConstraintLayout 152 153 Log.d(TAG, "Adding alternate bouncer view") 154 windowManager 155 .get() 156 .addView(alternateBouncerView, AlternateBouncerWindowViewLayoutParams.layoutParams) 157 alternateBouncerView!!.addOnAttachStateChangeListener(onAttachAddBackGestureHandler) 158 } 159 160 /** Binds the view to the view-model, continuing to update the former based on the latter. */ 161 fun bind(view: ConstraintLayout, alternateBouncerDependencies: AlternateBouncerDependencies) { 162 optionallyAddUdfpsViews( 163 view = view, 164 logger = alternateBouncerDependencies.logger, 165 udfpsIconViewModel = alternateBouncerDependencies.udfpsIconViewModel, 166 udfpsA11yOverlayViewModel = 167 alternateBouncerDependencies.udfpsAccessibilityOverlayViewModel, 168 ) 169 170 AlternateBouncerMessageAreaViewBinder.bind( 171 view = view.requireViewById(R.id.alternate_bouncer_message_area), 172 viewModel = alternateBouncerDependencies.messageAreaViewModel, 173 ) 174 175 val scrim: ScrimView = view.requireViewById(R.id.alternate_bouncer_scrim) 176 val viewModel = alternateBouncerDependencies.viewModel 177 val swipeUpAnywhereGestureHandler = 178 alternateBouncerDependencies.swipeUpAnywhereGestureHandler 179 val tapGestureDetector = alternateBouncerDependencies.tapGestureDetector 180 181 view.repeatWhenAttached { 182 repeatOnLifecycle(Lifecycle.State.STARTED) { 183 launch("$TAG#viewModel.registerForDismissGestures") { 184 viewModel.registerForDismissGestures.collect { registerForDismissGestures -> 185 if (registerForDismissGestures) { 186 swipeUpAnywhereGestureHandler.addOnGestureDetectedCallback( 187 swipeTag 188 ) { _ -> 189 alternateBouncerDependencies.powerInteractor.onUserTouch() 190 viewModel.onTapped() 191 } 192 tapGestureDetector.addOnGestureDetectedCallback(tapTag) { _ -> 193 alternateBouncerDependencies.powerInteractor.onUserTouch() 194 viewModel.onTapped() 195 } 196 } else { 197 swipeUpAnywhereGestureHandler.removeOnGestureDetectedCallback( 198 swipeTag 199 ) 200 tapGestureDetector.removeOnGestureDetectedCallback(tapTag) 201 } 202 } 203 } 204 .invokeOnCompletion { 205 swipeUpAnywhereGestureHandler.removeOnGestureDetectedCallback(swipeTag) 206 tapGestureDetector.removeOnGestureDetectedCallback(tapTag) 207 } 208 209 launch("$TAG#viewModel.scrimAlpha") { 210 viewModel.scrimAlpha.collect { scrim.viewAlpha = it } 211 } 212 213 launch("$TAG#viewModel.scrimColor") { 214 viewModel.scrimColor.collect { scrim.tint = it } 215 } 216 } 217 } 218 } 219 220 private fun optionallyAddUdfpsViews( 221 view: ConstraintLayout, 222 logger: TouchHandlingViewLogger, 223 udfpsIconViewModel: AlternateBouncerUdfpsIconViewModel, 224 udfpsA11yOverlayViewModel: Lazy<AlternateBouncerUdfpsAccessibilityOverlayViewModel>, 225 ) { 226 view.repeatWhenAttached { 227 repeatOnLifecycle(Lifecycle.State.CREATED) { 228 launch("$TAG#udfpsIconViewModel.iconLocation") { 229 udfpsIconViewModel.iconLocation.collect { iconLocation -> 230 // add UDFPS a11y overlay 231 val udfpsA11yOverlayViewId = 232 R.id.alternate_bouncer_udfps_accessibility_overlay 233 var udfpsA11yOverlay = view.getViewById(udfpsA11yOverlayViewId) 234 if (udfpsA11yOverlay == null) { 235 udfpsA11yOverlay = 236 UdfpsAccessibilityOverlay(view.context).apply { 237 id = udfpsA11yOverlayViewId 238 } 239 view.addView(udfpsA11yOverlay) 240 UdfpsAccessibilityOverlayBinder.bind( 241 udfpsA11yOverlay, 242 udfpsA11yOverlayViewModel.get(), 243 ) 244 } 245 246 // add UDFPS icon view 247 val udfpsViewId = R.id.alternate_bouncer_udfps_icon_view 248 var udfpsView = view.getViewById(udfpsViewId) 249 if (udfpsView == null) { 250 udfpsView = 251 DeviceEntryIconView(view.context, null, logger = logger).apply { 252 id = udfpsViewId 253 contentDescription = 254 context.resources.getString( 255 R.string.accessibility_fingerprint_label 256 ) 257 } 258 view.addView(udfpsView) 259 AlternateBouncerUdfpsViewBinder.bind(udfpsView, udfpsIconViewModel) 260 } 261 262 val constraintSet = ConstraintSet().apply { clone(view) } 263 constraintSet.apply { 264 // udfpsView: 265 constrainWidth(udfpsViewId, iconLocation.width) 266 constrainHeight(udfpsViewId, iconLocation.height) 267 connect( 268 udfpsViewId, 269 ConstraintSet.TOP, 270 ConstraintSet.PARENT_ID, 271 ConstraintSet.TOP, 272 iconLocation.top, 273 ) 274 connect( 275 udfpsViewId, 276 ConstraintSet.START, 277 ConstraintSet.PARENT_ID, 278 ConstraintSet.START, 279 iconLocation.left, 280 ) 281 282 // udfpsA11yOverlayView: 283 constrainWidth( 284 udfpsA11yOverlayViewId, 285 ViewGroup.LayoutParams.MATCH_PARENT, 286 ) 287 constrainHeight( 288 udfpsA11yOverlayViewId, 289 ViewGroup.LayoutParams.MATCH_PARENT, 290 ) 291 } 292 constraintSet.applyTo(view) 293 } 294 } 295 } 296 } 297 } 298 299 companion object { 300 private const val TAG = "AlternateBouncerViewBinder" 301 private const val swipeTag = "AlternateBouncer-SWIPE" 302 private const val tapTag = "AlternateBouncer-TAP" 303 } 304 } 305