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