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.view
18 
19 import android.content.Context
20 import android.graphics.drawable.AnimatedStateListDrawable
21 import android.graphics.drawable.AnimatedVectorDrawable
22 import android.util.AttributeSet
23 import android.util.StateSet
24 import android.view.Gravity
25 import android.view.View
26 import android.view.ViewGroup
27 import android.view.accessibility.AccessibilityNodeInfo
28 import android.widget.FrameLayout
29 import android.widget.ImageView
30 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
31 import com.airbnb.lottie.LottieCompositionFactory
32 import com.airbnb.lottie.LottieDrawable
33 import com.android.systemui.common.ui.view.LongPressHandlingView
34 import com.android.systemui.res.R
35 
36 class DeviceEntryIconView
37 @JvmOverloads
38 constructor(
39     context: Context,
40     attrs: AttributeSet?,
41     defStyleAttrs: Int = 0,
42 ) : FrameLayout(context, attrs, defStyleAttrs) {
43     val longPressHandlingView: LongPressHandlingView = LongPressHandlingView(context, attrs)
44     val iconView: ImageView = ImageView(context, attrs).apply { id = R.id.device_entry_icon_fg }
45     val bgView: ImageView = ImageView(context, attrs).apply { id = R.id.device_entry_icon_bg }
46     val aodFpDrawable: LottieDrawable = LottieDrawable()
47     var accessibilityHintType: AccessibilityHintType = AccessibilityHintType.NONE
48 
49     private var animatedIconDrawable: AnimatedStateListDrawable = AnimatedStateListDrawable()
50 
51     init {
52         setupIconStates()
53         setupIconTransitions()
54         setupAccessibilityDelegate()
55 
56         // Ordering matters. From background to foreground we want:
57         //     bgView, iconView, longpressHandlingView overlay
58         addBgImageView()
59         addIconImageView()
60         addLongpressHandlingView()
61     }
62 
63     private fun setupAccessibilityDelegate() {
64         accessibilityDelegate =
65             object : AccessibilityDelegate() {
66                 private val accessibilityBouncerHint =
67                     AccessibilityNodeInfo.AccessibilityAction(
68                         AccessibilityNodeInfoCompat.ACTION_CLICK,
69                         resources.getString(R.string.accessibility_bouncer)
70                     )
71                 private val accessibilityEnterHint =
72                     AccessibilityNodeInfo.AccessibilityAction(
73                         AccessibilityNodeInfoCompat.ACTION_CLICK,
74                         resources.getString(R.string.accessibility_enter_hint)
75                     )
76 
77                 override fun onInitializeAccessibilityNodeInfo(
78                     v: View,
79                     info: AccessibilityNodeInfo
80                 ) {
81                     super.onInitializeAccessibilityNodeInfo(v, info)
82                     when (accessibilityHintType) {
83                         AccessibilityHintType.BOUNCER -> info.addAction(accessibilityBouncerHint)
84                         AccessibilityHintType.ENTER -> info.addAction(accessibilityEnterHint)
85                         AccessibilityHintType.NONE -> return
86                     }
87                 }
88             }
89     }
90 
91     /**
92      * Setups different icon states.
93      * - All lottie views will require a LottieOnCompositionLoadedListener to update
94      *   LottieProperties (like color) of the view.
95      * - Drawable properties can be updated using ImageView properties like imageTintList.
96      */
97     private fun setupIconStates() {
98         // Lockscreen States
99         // LOCK
100         animatedIconDrawable.addState(
101             getIconState(IconType.LOCK, false),
102             context.getDrawable(R.drawable.ic_lock)!!,
103             R.id.locked,
104         )
105         // UNLOCK
106         animatedIconDrawable.addState(
107             getIconState(IconType.UNLOCK, false),
108             context.getDrawable(R.drawable.ic_unlocked)!!,
109             R.id.unlocked,
110         )
111         // FINGERPRINT
112         animatedIconDrawable.addState(
113             getIconState(IconType.FINGERPRINT, false),
114             context.getDrawable(R.drawable.ic_fingerprint)!!,
115             R.id.locked_fp,
116         )
117 
118         // AOD states
119         // LOCK
120         animatedIconDrawable.addState(
121             getIconState(IconType.LOCK, true),
122             context.getDrawable(R.drawable.ic_lock_aod)!!,
123             R.id.locked_aod,
124         )
125         // UNLOCK
126         animatedIconDrawable.addState(
127             getIconState(IconType.UNLOCK, true),
128             context.getDrawable(R.drawable.ic_unlocked_aod)!!,
129             R.id.unlocked_aod,
130         )
131         // FINGERPRINT
132         LottieCompositionFactory.fromRawRes(mContext, R.raw.udfps_aod_fp).addListener { result ->
133             aodFpDrawable.setComposition(result)
134         }
135         animatedIconDrawable.addState(
136             getIconState(IconType.FINGERPRINT, true),
137             aodFpDrawable,
138             R.id.udfps_aod_fp,
139         )
140 
141         // WILDCARD: should always be the last state added since any states will match with this
142         // and therefore won't get matched with subsequent states.
143         animatedIconDrawable.addState(
144             StateSet.WILD_CARD,
145             context.getDrawable(R.color.transparent)!!,
146             R.id.no_icon,
147         )
148     }
149 
150     private fun setupIconTransitions() {
151         // LockscreenFp <=> LockscreenUnlocked
152         animatedIconDrawable.addTransition(
153             R.id.locked_fp,
154             R.id.unlocked,
155             context.getDrawable(R.drawable.fp_to_unlock) as AnimatedVectorDrawable,
156             /* reversible */ false,
157         )
158         animatedIconDrawable.addTransition(
159             R.id.unlocked,
160             R.id.locked_fp,
161             context.getDrawable(R.drawable.unlock_to_fp) as AnimatedVectorDrawable,
162             /* reversible */ false,
163         )
164 
165         // LockscreenLocked <=> AodLocked
166         animatedIconDrawable.addTransition(
167             R.id.locked_aod,
168             R.id.locked,
169             context.getDrawable(R.drawable.lock_aod_to_ls) as AnimatedVectorDrawable,
170             /* reversible */ false,
171         )
172         animatedIconDrawable.addTransition(
173             R.id.locked,
174             R.id.locked_aod,
175             context.getDrawable(R.drawable.lock_ls_to_aod) as AnimatedVectorDrawable,
176             /* reversible */ false,
177         )
178 
179         // LockscreenUnlocked <=> AodUnlocked
180         animatedIconDrawable.addTransition(
181             R.id.unlocked_aod,
182             R.id.unlocked,
183             context.getDrawable(R.drawable.unlocked_aod_to_ls) as AnimatedVectorDrawable,
184             /* reversible */ false,
185         )
186         animatedIconDrawable.addTransition(
187             R.id.unlocked,
188             R.id.unlocked_aod,
189             context.getDrawable(R.drawable.unlocked_ls_to_aod) as AnimatedVectorDrawable,
190             /* reversible */ false,
191         )
192 
193         // LockscreenLocked <=> LockscreenUnlocked
194         animatedIconDrawable.addTransition(
195             R.id.locked,
196             R.id.unlocked,
197             context.getDrawable(R.drawable.lock_to_unlock) as AnimatedVectorDrawable,
198             /* reversible */ false,
199         )
200         animatedIconDrawable.addTransition(
201             R.id.unlocked,
202             R.id.locked,
203             context.getDrawable(R.drawable.unlocked_to_locked) as AnimatedVectorDrawable,
204             /* reversible */ false,
205         )
206 
207         // LockscreenFingerprint => LockscreenLocked
208         animatedIconDrawable.addTransition(
209             R.id.locked_fp,
210             R.id.locked,
211             context.getDrawable(R.drawable.fp_to_locked) as AnimatedVectorDrawable,
212             /* reversible */ false,
213         )
214 
215         // LockscreenUnlocked <=> AodLocked
216         animatedIconDrawable.addTransition(
217             R.id.unlocked,
218             R.id.locked_aod,
219             context.getDrawable(R.drawable.unlocked_to_aod_lock) as AnimatedVectorDrawable,
220             /* reversible */ false,
221         )
222     }
223 
224     private fun addLongpressHandlingView() {
225         addView(longPressHandlingView)
226         val lp = longPressHandlingView.layoutParams as LayoutParams
227         lp.height = ViewGroup.LayoutParams.MATCH_PARENT
228         lp.width = ViewGroup.LayoutParams.MATCH_PARENT
229         longPressHandlingView.layoutParams = lp
230     }
231 
232     private fun addIconImageView() {
233         iconView.scaleType = ImageView.ScaleType.CENTER_CROP
234         iconView.setImageDrawable(animatedIconDrawable)
235         addView(iconView)
236         val lp = iconView.layoutParams as LayoutParams
237         lp.height = ViewGroup.LayoutParams.MATCH_PARENT
238         lp.width = ViewGroup.LayoutParams.MATCH_PARENT
239         lp.gravity = Gravity.CENTER
240         iconView.layoutParams = lp
241     }
242 
243     private fun addBgImageView() {
244         bgView.setImageDrawable(context.getDrawable(R.drawable.fingerprint_bg))
245         addView(bgView)
246         val lp = bgView.layoutParams as LayoutParams
247         lp.height = ViewGroup.LayoutParams.MATCH_PARENT
248         lp.width = ViewGroup.LayoutParams.MATCH_PARENT
249         bgView.layoutParams = lp
250     }
251 
252     fun getIconState(icon: IconType, aod: Boolean): IntArray {
253         val lockIconState = IntArray(2)
254         when (icon) {
255             IconType.LOCK -> lockIconState[0] = android.R.attr.state_first
256             IconType.UNLOCK -> lockIconState[0] = android.R.attr.state_last
257             IconType.FINGERPRINT -> lockIconState[0] = android.R.attr.state_middle
258             IconType.NONE -> return StateSet.NOTHING
259         }
260         if (aod) {
261             lockIconState[1] = android.R.attr.state_single
262         } else {
263             lockIconState[1] = -android.R.attr.state_single
264         }
265         return lockIconState
266     }
267 
268     enum class IconType(val contentDescriptionResId: Int) {
269         LOCK(R.string.accessibility_lock_icon),
270         UNLOCK(R.string.accessibility_unlock_button),
271         FINGERPRINT(R.string.accessibility_fingerprint_label),
272         NONE(-1),
273     }
274 
275     enum class AccessibilityHintType {
276         NONE,
277         BOUNCER,
278         ENTER,
279     }
280 }
281