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