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 
18 package com.android.systemui.keyguard.ui.view.layout.sections
19 
20 import android.content.Context
21 import android.graphics.Point
22 import android.graphics.Rect
23 import android.util.DisplayMetrics
24 import android.util.Log
25 import android.view.View
26 import android.view.WindowManager
27 import androidx.annotation.VisibleForTesting
28 import androidx.constraintlayout.widget.ConstraintLayout
29 import androidx.constraintlayout.widget.ConstraintSet
30 import com.android.keyguard.LockIconView
31 import com.android.keyguard.LockIconViewController
32 import com.android.systemui.biometrics.AuthController
33 import com.android.systemui.dagger.qualifiers.Application
34 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
35 import com.android.systemui.flags.FeatureFlags
36 import com.android.systemui.flags.Flags
37 import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
38 import com.android.systemui.keyguard.MigrateClocksToBlueprint
39 import com.android.systemui.keyguard.shared.model.KeyguardSection
40 import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder
41 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
42 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryBackgroundViewModel
43 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel
44 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
45 import com.android.systemui.plugins.FalsingManager
46 import com.android.systemui.res.R
47 import com.android.systemui.shade.NotificationPanelView
48 import com.android.systemui.statusbar.VibratorHelper
49 import dagger.Lazy
50 import javax.inject.Inject
51 import kotlinx.coroutines.CoroutineScope
52 import kotlinx.coroutines.ExperimentalCoroutinesApi
53 
54 /** Includes the device entry icon. */
55 @ExperimentalCoroutinesApi
56 class DefaultDeviceEntrySection
57 @Inject
58 constructor(
59     @Application private val applicationScope: CoroutineScope,
60     private val authController: AuthController,
61     private val windowManager: WindowManager,
62     private val context: Context,
63     private val notificationPanelView: NotificationPanelView,
64     private val featureFlags: FeatureFlags,
65     private val lockIconViewController: Lazy<LockIconViewController>,
66     private val deviceEntryIconViewModel: Lazy<DeviceEntryIconViewModel>,
67     private val deviceEntryForegroundViewModel: Lazy<DeviceEntryForegroundViewModel>,
68     private val deviceEntryBackgroundViewModel: Lazy<DeviceEntryBackgroundViewModel>,
69     private val falsingManager: Lazy<FalsingManager>,
70     private val vibratorHelper: Lazy<VibratorHelper>,
71 ) : KeyguardSection() {
72     private val deviceEntryIconViewId = R.id.device_entry_icon_view
73 
74     override fun addViews(constraintLayout: ConstraintLayout) {
75         if (
76             !KeyguardBottomAreaRefactor.isEnabled &&
77                 !MigrateClocksToBlueprint.isEnabled &&
78                 !DeviceEntryUdfpsRefactor.isEnabled
79         ) {
80             return
81         }
82 
83         notificationPanelView.findViewById<View>(R.id.lock_icon_view).let {
84             notificationPanelView.removeView(it)
85         }
86 
87         val view =
88             if (DeviceEntryUdfpsRefactor.isEnabled) {
89                 DeviceEntryIconView(context, null).apply { id = deviceEntryIconViewId }
90             } else {
91                 // KeyguardBottomAreaRefactor.isEnabled or MigrateClocksToBlueprint.isEnabled
92                 LockIconView(context, null).apply { id = R.id.lock_icon_view }
93             }
94         constraintLayout.addView(view)
95     }
96 
97     override fun bindData(constraintLayout: ConstraintLayout) {
98         if (DeviceEntryUdfpsRefactor.isEnabled) {
99             constraintLayout.findViewById<DeviceEntryIconView?>(deviceEntryIconViewId)?.let {
100                 DeviceEntryIconViewBinder.bind(
101                     applicationScope,
102                     it,
103                     deviceEntryIconViewModel.get(),
104                     deviceEntryForegroundViewModel.get(),
105                     deviceEntryBackgroundViewModel.get(),
106                     falsingManager.get(),
107                     vibratorHelper.get(),
108                 )
109             }
110         } else {
111             constraintLayout.findViewById<LockIconView?>(R.id.lock_icon_view)?.let {
112                 lockIconViewController.get().setLockIconView(it)
113             }
114         }
115     }
116 
117     override fun applyConstraints(constraintSet: ConstraintSet) {
118         val isUdfpsSupported =
119             if (DeviceEntryUdfpsRefactor.isEnabled) {
120                 Log.d(
121                     "DefaultDeviceEntrySection",
122                     "isUdfpsSupported=${deviceEntryIconViewModel.get().isUdfpsSupported.value}"
123                 )
124                 deviceEntryIconViewModel.get().isUdfpsSupported.value
125             } else {
126                 authController.isUdfpsSupported
127             }
128         val scaleFactor: Float = authController.scaleFactor
129         val mBottomPaddingPx =
130             context.resources.getDimensionPixelSize(R.dimen.lock_icon_margin_bottom)
131         val bounds = windowManager.currentWindowMetrics.bounds
132         var widthPixels = bounds.right.toFloat()
133         if (featureFlags.isEnabled(Flags.LOCKSCREEN_ENABLE_LANDSCAPE)) {
134             // Assumed to be initially neglected as there are no left or right insets in portrait.
135             // However, on landscape, these insets need to included when calculating the midpoint.
136             val insets = windowManager.currentWindowMetrics.windowInsets
137             widthPixels -= (insets.systemWindowInsetLeft + insets.systemWindowInsetRight).toFloat()
138         }
139         val heightPixels = bounds.bottom.toFloat()
140         val defaultDensity =
141             DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() /
142                 DisplayMetrics.DENSITY_DEFAULT.toFloat()
143         val iconRadiusPx = (defaultDensity * 36).toInt()
144 
145         if (isUdfpsSupported) {
146             if (DeviceEntryUdfpsRefactor.isEnabled) {
147                 deviceEntryIconViewModel.get().udfpsLocation.value?.let { udfpsLocation ->
148                     Log.d(
149                         "DeviceEntrySection",
150                         "udfpsLocation=$udfpsLocation, " +
151                             "scaledLocation=(${udfpsLocation.centerX},${udfpsLocation.centerY}), " +
152                             "unusedAuthController=${authController.udfpsLocation}"
153                     )
154                     centerIcon(
155                         Point(udfpsLocation.centerX.toInt(), udfpsLocation.centerY.toInt()),
156                         udfpsLocation.radius,
157                         constraintSet
158                     )
159                 }
160             } else {
161                 authController.udfpsLocation?.let { udfpsLocation ->
162                     Log.d("DeviceEntrySection", "udfpsLocation=$udfpsLocation")
163                     centerIcon(udfpsLocation, authController.udfpsRadius, constraintSet)
164                 }
165             }
166         } else {
167             centerIcon(
168                 Point(
169                     (widthPixels / 2).toInt(),
170                     (heightPixels - ((mBottomPaddingPx + iconRadiusPx) * scaleFactor)).toInt()
171                 ),
172                 iconRadiusPx * scaleFactor,
173                 constraintSet,
174             )
175         }
176     }
177 
178     override fun removeViews(constraintLayout: ConstraintLayout) {
179         if (DeviceEntryUdfpsRefactor.isEnabled) {
180             constraintLayout.removeView(deviceEntryIconViewId)
181         } else {
182             constraintLayout.removeView(R.id.lock_icon_view)
183         }
184     }
185 
186     @VisibleForTesting
187     internal fun centerIcon(center: Point, radius: Float, constraintSet: ConstraintSet) {
188         val sensorRect =
189             Rect().apply {
190                 set(
191                     center.x - radius.toInt(),
192                     center.y - radius.toInt(),
193                     center.x + radius.toInt(),
194                     center.y + radius.toInt(),
195                 )
196             }
197 
198         val iconId =
199             if (DeviceEntryUdfpsRefactor.isEnabled) {
200                 deviceEntryIconViewId
201             } else {
202                 R.id.lock_icon_view
203             }
204 
205         constraintSet.apply {
206             constrainWidth(iconId, sensorRect.right - sensorRect.left)
207             constrainHeight(iconId, sensorRect.bottom - sensorRect.top)
208             connect(
209                 iconId,
210                 ConstraintSet.TOP,
211                 ConstraintSet.PARENT_ID,
212                 ConstraintSet.TOP,
213                 sensorRect.top
214             )
215             connect(
216                 iconId,
217                 ConstraintSet.START,
218                 ConstraintSet.PARENT_ID,
219                 ConstraintSet.START,
220                 sensorRect.left
221             )
222         }
223 
224         // This is only intended to be here until the KeyguardBottomAreaRefactor flag is enabled
225         // Without this logic, the lock icon location changes but the KeyguardBottomAreaView is not
226         // updated and visible ui layout jank occurs. This is due to AmbientIndicationContainer
227         // being in NPVC and laying out prior to the KeyguardRootView.
228         // Remove when both DeviceEntryUdfpsRefactor and KeyguardBottomAreaRefactor are enabled.
229         if (DeviceEntryUdfpsRefactor.isEnabled && !KeyguardBottomAreaRefactor.isEnabled) {
230             with(notificationPanelView) {
231                 val isUdfpsSupported = deviceEntryIconViewModel.get().isUdfpsSupported.value
232                 val bottomAreaViewRight = findViewById<View>(R.id.keyguard_bottom_area)?.right ?: 0
233                 findViewById<View>(R.id.ambient_indication_container)?.let {
234                     val (ambientLeft, ambientTop) = it.locationOnScreen
235                     if (isUdfpsSupported) {
236                         // make top of ambient indication view the bottom of the lock icon
237                         it.layout(
238                             ambientLeft,
239                             sensorRect.bottom,
240                             bottomAreaViewRight - ambientLeft,
241                             ambientTop + it.measuredHeight
242                         )
243                     } else {
244                         // make bottom of ambient indication view the top of the lock icon
245                         it.layout(
246                             ambientLeft,
247                             sensorRect.top - it.measuredHeight,
248                             bottomAreaViewRight - ambientLeft,
249                             sensorRect.top
250                         )
251                     }
252                 }
253             }
254         }
255     }
256 }
257