1 /*
<lambda>null2  * Copyright (C) 2022 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.customization.picker.quickaffordance.ui.binder
19 
20 import android.app.Dialog
21 import android.content.Context
22 import android.view.View
23 import android.view.ViewGroup
24 import android.view.accessibility.AccessibilityEvent
25 import android.widget.ImageView
26 import androidx.core.view.AccessibilityDelegateCompat
27 import androidx.core.view.ViewCompat
28 import androidx.lifecycle.Lifecycle
29 import androidx.lifecycle.LifecycleOwner
30 import androidx.lifecycle.lifecycleScope
31 import androidx.lifecycle.repeatOnLifecycle
32 import androidx.recyclerview.widget.LinearLayoutManager
33 import androidx.recyclerview.widget.RecyclerView
34 import com.android.customization.picker.common.ui.view.ItemSpacing
35 import com.android.customization.picker.quickaffordance.ui.adapter.SlotTabAdapter
36 import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordancePickerViewModel
37 import com.android.themepicker.R
38 import com.android.wallpaper.picker.common.dialog.ui.viewbinder.DialogViewBinder
39 import com.android.wallpaper.picker.common.dialog.ui.viewmodel.DialogViewModel
40 import com.android.wallpaper.picker.common.icon.ui.viewbinder.IconViewBinder
41 import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon
42 import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter
43 import kotlinx.coroutines.ExperimentalCoroutinesApi
44 import kotlinx.coroutines.flow.collectIndexed
45 import kotlinx.coroutines.flow.combine
46 import kotlinx.coroutines.flow.distinctUntilChanged
47 import kotlinx.coroutines.flow.flatMapLatest
48 import kotlinx.coroutines.flow.map
49 import kotlinx.coroutines.launch
50 
51 @OptIn(ExperimentalCoroutinesApi::class)
52 object KeyguardQuickAffordancePickerBinder {
53 
54     /** Binds view with view-model for a lock screen quick affordance picker experience. */
55     @JvmStatic
56     fun bind(
57         view: View,
58         viewModel: KeyguardQuickAffordancePickerViewModel,
59         lifecycleOwner: LifecycleOwner,
60     ) {
61         val slotTabView: RecyclerView = view.requireViewById(R.id.slot_tabs)
62         val affordancesView: RecyclerView = view.requireViewById(R.id.affordances)
63 
64         val slotTabAdapter = SlotTabAdapter()
65         slotTabView.adapter = slotTabAdapter
66         slotTabView.layoutManager =
67             LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
68         slotTabView.addItemDecoration(ItemSpacing(ItemSpacing.TAB_ITEM_SPACING_DP))
69 
70         // Setting a custom accessibility delegate so that the default content descriptions
71         // for items in a list aren't announced (for left & right shortcuts). We populate
72         // the content description for these shortcuts later on with the right (expected)
73         // values.
74         val slotTabViewDelegate: AccessibilityDelegateCompat =
75             object : AccessibilityDelegateCompat() {
76                 override fun onRequestSendAccessibilityEvent(
77                     host: ViewGroup,
78                     child: View,
79                     event: AccessibilityEvent
80                 ): Boolean {
81                     if (event.eventType != AccessibilityEvent.TYPE_VIEW_FOCUSED) {
82                         child.contentDescription = null
83                     }
84                     return super.onRequestSendAccessibilityEvent(host, child, event)
85                 }
86             }
87 
88         ViewCompat.setAccessibilityDelegate(slotTabView, slotTabViewDelegate)
89         val affordancesAdapter =
90             OptionItemAdapter(
91                 layoutResourceId = R.layout.keyguard_quick_affordance,
92                 lifecycleOwner = lifecycleOwner,
93                 bindIcon = { foregroundView: View, gridIcon: Icon ->
94                     val imageView = foregroundView as? ImageView
95                     imageView?.let { IconViewBinder.bind(imageView, gridIcon) }
96                 }
97             )
98         affordancesView.adapter = affordancesAdapter
99         affordancesView.layoutManager =
100             LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
101         affordancesView.addItemDecoration(ItemSpacing(ItemSpacing.ITEM_SPACING_DP))
102 
103         var dialog: Dialog? = null
104 
105         lifecycleOwner.lifecycleScope.launch {
106             lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
107                 launch {
108                     viewModel.slots
109                         .map { slotById -> slotById.values }
110                         .collect { slots -> slotTabAdapter.setItems(slots.toList()) }
111                 }
112 
113                 launch {
114                     viewModel.quickAffordances.collect { affordances ->
115                         affordancesAdapter.setItems(affordances)
116                     }
117                 }
118 
119                 launch {
120                     viewModel.quickAffordances
121                         .flatMapLatest { affordances ->
122                             combine(affordances.map { affordance -> affordance.isSelected }) {
123                                 selectedFlags ->
124                                 selectedFlags.indexOfFirst { it }
125                             }
126                         }
127                         .collectIndexed { index, selectedPosition ->
128                             // Scroll the view to show the first selected affordance.
129                             if (selectedPosition != -1) {
130                                 // We use "post" because we need to give the adapter item a pass to
131                                 // update the view.
132                                 affordancesView.post {
133                                     if (index == 0) {
134                                         // don't animate on initial collection
135                                         affordancesView.scrollToPosition(selectedPosition)
136                                     } else {
137                                         affordancesView.smoothScrollToPosition(selectedPosition)
138                                     }
139                                 }
140                             }
141                         }
142                 }
143 
144                 launch {
145                     viewModel.dialog.distinctUntilChanged().collect { dialogRequest ->
146                         dialog?.dismiss()
147                         dialog =
148                             if (dialogRequest != null) {
149                                 showDialog(
150                                     context = view.context,
151                                     request = dialogRequest,
152                                     onDismissed = viewModel::onDialogDismissed
153                                 )
154                             } else {
155                                 null
156                             }
157                     }
158                 }
159 
160                 launch {
161                     viewModel.activityStartRequests.collect { intent ->
162                         if (intent != null) {
163                             view.context.startActivity(intent)
164                             viewModel.onActivityStarted()
165                         }
166                     }
167                 }
168             }
169         }
170     }
171 
172     private fun showDialog(
173         context: Context,
174         request: DialogViewModel,
175         onDismissed: () -> Unit,
176     ): Dialog {
177         return DialogViewBinder.show(
178             context = context,
179             viewModel = request,
180             onDismissed = onDismissed,
181         )
182     }
183 }
184