• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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