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