/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.android.systemui.user.ui.binder import android.content.Context import android.view.Gravity import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.BaseAdapter import android.widget.ImageView import android.widget.LinearLayout import android.widget.LinearLayout.SHOW_DIVIDER_MIDDLE import android.widget.TextView import androidx.constraintlayout.helper.widget.Flow as FlowWidget import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.Gefingerpoken import com.android.systemui.res.R import com.android.systemui.classifier.FalsingCollector import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.user.UserSwitcherPopupMenu import com.android.systemui.user.UserSwitcherRootView import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.ui.viewmodel.UserActionViewModel import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel import com.android.systemui.util.children import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch /** Binds a user switcher to its view-model. */ object UserSwitcherViewBinder { private const val USER_VIEW_TAG = "user_view" /** Binds the given view to the given view-model. */ fun bind( view: ViewGroup, viewModel: UserSwitcherViewModel, layoutInflater: LayoutInflater, falsingCollector: FalsingCollector, onFinish: () -> Unit, ) { val gridContainerView: UserSwitcherRootView = view.requireViewById(R.id.user_switcher_grid_container) val flowWidget: FlowWidget = gridContainerView.requireViewById(R.id.flow) val addButton: View = view.requireViewById(R.id.add) val cancelButton: View = view.requireViewById(R.id.cancel) val popupMenuAdapter = MenuAdapter(layoutInflater) var popupMenu: UserSwitcherPopupMenu? = null gridContainerView.touchHandler = object : Gefingerpoken { override fun onTouchEvent(ev: MotionEvent?): Boolean { falsingCollector.onTouchEvent(ev) return false } } addButton.setOnClickListener { viewModel.onOpenMenuButtonClicked() } cancelButton.setOnClickListener { viewModel.onCancelButtonClicked() } view.repeatWhenAttached { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.CREATED) { launch { viewModel.isFinishRequested .filter { it } .collect { //finish requested, we want to dismiss popupmenu at the same time popupMenu?.dismiss() onFinish() viewModel.onFinished() } } } } lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { launch { viewModel.isOpenMenuButtonVisible.collect { addButton.isVisible = it } } launch { viewModel.isMenuVisible.collect { isVisible -> if (isVisible && popupMenu?.isShowing != true) { popupMenu?.dismiss() // Use post to make sure we show the popup menu *after* the activity is // ready to show one to avoid a WindowManager$BadTokenException. view.post { popupMenu = createAndShowPopupMenu( context = view.context, anchorView = addButton, adapter = popupMenuAdapter, onDismissed = viewModel::onMenuClosed, ) } } else if (!isVisible && popupMenu?.isShowing == true) { popupMenu?.dismiss() popupMenu = null } } } launch { viewModel.menu.collect { menuViewModels -> popupMenuAdapter.setItems(menuViewModels) } } launch { viewModel.maximumUserColumns.collect { maximumColumns -> flowWidget.setMaxElementsWrap(maximumColumns) } } launch { viewModel.users.collect { users -> val viewPool = gridContainerView.children .filter { it.tag == USER_VIEW_TAG } .toMutableList() viewPool.forEach { gridContainerView.removeView(it) flowWidget.removeView(it) } users.forEach { userViewModel -> val userView = if (viewPool.isNotEmpty()) { viewPool.removeAt(0) } else { val inflatedView = layoutInflater.inflate( R.layout.user_switcher_fullscreen_item, view, false, ) inflatedView.tag = USER_VIEW_TAG inflatedView } userView.id = View.generateViewId() gridContainerView.addView(userView) flowWidget.addView(userView) UserViewBinder.bind( view = userView, viewModel = userViewModel, ) } } } } } } } private fun createAndShowPopupMenu( context: Context, anchorView: View, adapter: MenuAdapter, onDismissed: () -> Unit, ): UserSwitcherPopupMenu { return UserSwitcherPopupMenu(context).apply { this.setDropDownGravity(Gravity.END) this.anchorView = anchorView setAdapter(adapter) setOnDismissListener { onDismissed() } show() } } /** Adapter for the menu that can be opened. */ private class MenuAdapter( private val layoutInflater: LayoutInflater, ) : BaseAdapter() { private var sections = listOf>() override fun getCount(): Int { return sections.size } override fun getItem(position: Int): List { return sections[position] } override fun getItemId(position: Int): Long { return position.toLong() } override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val section = getItem(position) val context = parent.context val sectionView = convertView as? LinearLayout ?: LinearLayout(context, null).apply { this.orientation = LinearLayout.VERTICAL this.background = parent.resources.getDrawable( R.drawable.bouncer_user_switcher_popup_bg, context.theme ) this.showDividers = SHOW_DIVIDER_MIDDLE this.dividerDrawable = context.getDrawable( R.drawable.fullscreen_userswitcher_menu_item_divider ) } sectionView.removeAllViewsInLayout() section.onEachIndexed { index, viewModel -> val view = layoutInflater.inflate( R.layout.user_switcher_fullscreen_popup_item, /* parent= */ null ) view .requireViewById(R.id.icon) .setImageResource(viewModel.iconResourceId) view.requireViewById(R.id.text).text = view.resources.getString(viewModel.textResourceId) view.setOnClickListener { viewModel.onClicked() } sectionView.addView(view) // Ensure that the first item in the first section gets accessibility focus. // Request for focus with a delay when view is inflated an added to the listview. if (index == 0 && position == 0) { view.postDelayed({ view.requestAccessibilityFocus() }, 200) } } return sectionView } fun setItems(items: List) { val primarySection = items.filter { it.viewKey != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong() } val secondarySection = items.filter { it.viewKey == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong() } this.sections = listOf(primarySection, secondarySection) notifyDataSetChanged() } } }