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.systemui.user.ui.binder
19 
20 import android.content.Context
21 import android.view.Gravity
22 import android.view.LayoutInflater
23 import android.view.MotionEvent
24 import android.view.View
25 import android.view.ViewGroup
26 import android.widget.BaseAdapter
27 import android.widget.ImageView
28 import android.widget.LinearLayout
29 import android.widget.LinearLayout.SHOW_DIVIDER_MIDDLE
30 import android.widget.TextView
31 import androidx.constraintlayout.helper.widget.Flow as FlowWidget
32 import androidx.core.view.isVisible
33 import androidx.lifecycle.Lifecycle
34 import androidx.lifecycle.lifecycleScope
35 import androidx.lifecycle.repeatOnLifecycle
36 import com.android.systemui.Gefingerpoken
37 import com.android.systemui.res.R
38 import com.android.systemui.classifier.FalsingCollector
39 import com.android.systemui.lifecycle.repeatWhenAttached
40 import com.android.systemui.user.UserSwitcherPopupMenu
41 import com.android.systemui.user.UserSwitcherRootView
42 import com.android.systemui.user.shared.model.UserActionModel
43 import com.android.systemui.user.ui.viewmodel.UserActionViewModel
44 import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
45 import com.android.systemui.util.children
46 import kotlinx.coroutines.flow.filter
47 import kotlinx.coroutines.launch
48 
49 /** Binds a user switcher to its view-model. */
50 object UserSwitcherViewBinder {
51 
52     private const val USER_VIEW_TAG = "user_view"
53 
54     /** Binds the given view to the given view-model. */
55     fun bind(
56         view: ViewGroup,
57         viewModel: UserSwitcherViewModel,
58         layoutInflater: LayoutInflater,
59         falsingCollector: FalsingCollector,
60         onFinish: () -> Unit,
61     ) {
62         val gridContainerView: UserSwitcherRootView =
63             view.requireViewById(R.id.user_switcher_grid_container)
64         val flowWidget: FlowWidget = gridContainerView.requireViewById(R.id.flow)
65         val addButton: View = view.requireViewById(R.id.add)
66         val cancelButton: View = view.requireViewById(R.id.cancel)
67         val popupMenuAdapter = MenuAdapter(layoutInflater)
68         var popupMenu: UserSwitcherPopupMenu? = null
69 
70         gridContainerView.touchHandler =
71             object : Gefingerpoken {
72                 override fun onTouchEvent(ev: MotionEvent?): Boolean {
73                     falsingCollector.onTouchEvent(ev)
74                     return false
75                 }
76             }
77         addButton.setOnClickListener { viewModel.onOpenMenuButtonClicked() }
78         cancelButton.setOnClickListener { viewModel.onCancelButtonClicked() }
79 
80         view.repeatWhenAttached {
81             lifecycleScope.launch {
82                 repeatOnLifecycle(Lifecycle.State.CREATED) {
83                     launch {
84                         viewModel.isFinishRequested
85                             .filter { it }
86                             .collect {
87                                 //finish requested, we want to dismiss popupmenu at the same time
88                                 popupMenu?.dismiss()
89                                 onFinish()
90                                 viewModel.onFinished()
91                             }
92                     }
93                 }
94             }
95 
96             lifecycleScope.launch {
97                 repeatOnLifecycle(Lifecycle.State.STARTED) {
98                     launch { viewModel.isOpenMenuButtonVisible.collect { addButton.isVisible = it } }
99 
100                     launch {
101                         viewModel.isMenuVisible.collect { isVisible ->
102                             if (isVisible && popupMenu?.isShowing != true) {
103                                 popupMenu?.dismiss()
104                                 // Use post to make sure we show the popup menu *after* the activity is
105                                 // ready to show one to avoid a WindowManager$BadTokenException.
106                                 view.post {
107                                     popupMenu =
108                                         createAndShowPopupMenu(
109                                             context = view.context,
110                                             anchorView = addButton,
111                                             adapter = popupMenuAdapter,
112                                             onDismissed = viewModel::onMenuClosed,
113                                         )
114                                 }
115                             } else if (!isVisible && popupMenu?.isShowing == true) {
116                                 popupMenu?.dismiss()
117                                 popupMenu = null
118                             }
119                         }
120                     }
121 
122                     launch {
123                         viewModel.menu.collect { menuViewModels ->
124                             popupMenuAdapter.setItems(menuViewModels)
125                         }
126                     }
127 
128                     launch {
129                         viewModel.maximumUserColumns.collect { maximumColumns ->
130                             flowWidget.setMaxElementsWrap(maximumColumns)
131                         }
132                     }
133 
134                     launch {
135                         viewModel.users.collect { users ->
136                             val viewPool =
137                                 gridContainerView.children
138                                     .filter { it.tag == USER_VIEW_TAG }
139                                     .toMutableList()
140                             viewPool.forEach {
141                                 gridContainerView.removeView(it)
142                                 flowWidget.removeView(it)
143                             }
144                             users.forEach { userViewModel ->
145                                 val userView =
146                                     if (viewPool.isNotEmpty()) {
147                                         viewPool.removeAt(0)
148                                     } else {
149                                         val inflatedView =
150                                             layoutInflater.inflate(
151                                                 R.layout.user_switcher_fullscreen_item,
152                                                 view,
153                                                 false,
154                                             )
155                                         inflatedView.tag = USER_VIEW_TAG
156                                         inflatedView
157                                     }
158                                 userView.id = View.generateViewId()
159                                 gridContainerView.addView(userView)
160                                 flowWidget.addView(userView)
161                                 UserViewBinder.bind(
162                                     view = userView,
163                                     viewModel = userViewModel,
164                                 )
165                             }
166                         }
167                     }
168                 }
169             }
170         }
171     }
172 
173     private fun createAndShowPopupMenu(
174         context: Context,
175         anchorView: View,
176         adapter: MenuAdapter,
177         onDismissed: () -> Unit,
178     ): UserSwitcherPopupMenu {
179         return UserSwitcherPopupMenu(context).apply {
180             this.setDropDownGravity(Gravity.END)
181             this.anchorView = anchorView
182             setAdapter(adapter)
183             setOnDismissListener { onDismissed() }
184             show()
185         }
186     }
187 
188     /** Adapter for the menu that can be opened. */
189     private class MenuAdapter(
190         private val layoutInflater: LayoutInflater,
191     ) : BaseAdapter() {
192 
193         private var sections = listOf<List<UserActionViewModel>>()
194 
195         override fun getCount(): Int {
196             return sections.size
197         }
198 
199         override fun getItem(position: Int): List<UserActionViewModel> {
200             return sections[position]
201         }
202 
203         override fun getItemId(position: Int): Long {
204             return position.toLong()
205         }
206 
207         override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
208             val section = getItem(position)
209             val context = parent.context
210             val sectionView =
211                 convertView as? LinearLayout
212                     ?: LinearLayout(context, null).apply {
213                         this.orientation = LinearLayout.VERTICAL
214                         this.background =
215                             parent.resources.getDrawable(
216                                 R.drawable.bouncer_user_switcher_popup_bg,
217                                 context.theme
218                             )
219                         this.showDividers = SHOW_DIVIDER_MIDDLE
220                         this.dividerDrawable =
221                             context.getDrawable(
222                                 R.drawable.fullscreen_userswitcher_menu_item_divider
223                             )
224                     }
225             sectionView.removeAllViewsInLayout()
226 
227             section.onEachIndexed { index, viewModel ->
228                 val view =
229                     layoutInflater.inflate(
230                         R.layout.user_switcher_fullscreen_popup_item,
231                         /* parent= */ null
232                     )
233                 view
234                     .requireViewById<ImageView>(R.id.icon)
235                     .setImageResource(viewModel.iconResourceId)
236                 view.requireViewById<TextView>(R.id.text).text =
237                     view.resources.getString(viewModel.textResourceId)
238                 view.setOnClickListener { viewModel.onClicked() }
239                 sectionView.addView(view)
240                 // Ensure that the first item in the first section gets accessibility focus.
241                 // Request for focus with a delay when view is inflated an added to the listview.
242                 if (index == 0 && position == 0) {
243                     view.postDelayed({
244                         view.requestAccessibilityFocus()
245                     }, 200)
246                 }
247             }
248             return sectionView
249         }
250 
251         fun setItems(items: List<UserActionViewModel>) {
252             val primarySection =
253                 items.filter {
254                     it.viewKey != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong()
255                 }
256             val secondarySection =
257                 items.filter {
258                     it.viewKey == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong()
259                 }
260             this.sections = listOf(primarySection, secondarySection)
261             notifyDataSetChanged()
262         }
263     }
264 }
265