blob: 80c55c0bad07382aa537a6a8b7c165916ee53f91 [file] [log] [blame]
/*
* 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
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.LayerDrawable
import android.os.Bundle
import android.os.UserManager
import android.provider.Settings
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.constraintlayout.helper.widget.Flow
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.util.UserIcons
import com.android.settingslib.Utils
import com.android.systemui.Gefingerpoken
import com.android.systemui.R
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.UserSwitcherController
import com.android.systemui.statusbar.policy.UserSwitcherController.BaseUserAdapter
import com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_DISABLED_ALPHA
import com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_ENABLED_ALPHA
import com.android.systemui.statusbar.policy.UserSwitcherController.UserRecord
import javax.inject.Inject
import kotlin.math.ceil
private const val USER_VIEW = "user_view"
/**
* Support a fullscreen user switcher
*/
class UserSwitcherActivity @Inject constructor(
private val userSwitcherController: UserSwitcherController,
private val broadcastDispatcher: BroadcastDispatcher,
private val layoutInflater: LayoutInflater,
private val falsingCollector: FalsingCollector,
private val falsingManager: FalsingManager,
private val userManager: UserManager,
private val userTracker: UserTracker
) : ComponentActivity() {
private lateinit var parent: UserSwitcherRootView
private lateinit var broadcastReceiver: BroadcastReceiver
private var popupMenu: UserSwitcherPopupMenu? = null
private lateinit var addButton: View
private var addUserRecords = mutableListOf<UserRecord>()
private val userSwitchedCallback: UserTracker.Callback = object : UserTracker.Callback {
override fun onUserChanged(newUser: Int, userContext: Context) {
finish()
}
}
// When the add users options become available, insert another option to manage users
private val manageUserRecord = UserRecord(
null /* info */,
null /* picture */,
false /* isGuest */,
false /* isCurrent */,
false /* isAddUser */,
false /* isRestricted */,
false /* isSwitchToEnabled */,
false /* isAddSupervisedUser */
)
private val adapter = object : BaseUserAdapter(userSwitcherController) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val item = getItem(position)
var view = convertView as ViewGroup?
if (view == null) {
view = layoutInflater.inflate(
R.layout.user_switcher_fullscreen_item,
parent,
false
) as ViewGroup
}
(view.getChildAt(0) as ImageView).apply {
setImageDrawable(getDrawable(item))
}
(view.getChildAt(1) as TextView).apply {
setText(getName(getContext(), item))
}
view.setEnabled(item.isSwitchToEnabled)
view.setAlpha(
if (view.isEnabled()) {
USER_SWITCH_ENABLED_ALPHA
} else {
USER_SWITCH_DISABLED_ALPHA
}
)
view.setTag(USER_VIEW)
return view
}
override fun getName(context: Context, item: UserRecord): String {
return if (item == manageUserRecord) {
getString(R.string.manage_users)
} else {
super.getName(context, item)
}
}
fun findUserIcon(item: UserRecord): Drawable {
if (item == manageUserRecord) {
return getDrawable(R.drawable.ic_manage_users)
}
if (item.info == null) {
return getIconDrawable(this@UserSwitcherActivity, item)
}
val userIcon = userManager.getUserIcon(item.info.id)
if (userIcon != null) {
return BitmapDrawable(userIcon)
}
return UserIcons.getDefaultUserIcon(resources, item.info.id, false)
}
fun getTotalUserViews(): Int {
return users.count { item ->
!doNotRenderUserView(item)
}
}
fun doNotRenderUserView(item: UserRecord): Boolean {
return item.isAddUser ||
item.isAddSupervisedUser ||
item.isGuest && item.info == null
}
private fun getDrawable(item: UserRecord): Drawable {
var drawable = if (item.isGuest) {
getDrawable(R.drawable.ic_account_circle)
} else {
findUserIcon(item)
}
drawable.mutate()
if (!item.isCurrent && !item.isSwitchToEnabled) {
drawable.setTint(
resources.getColor(
R.color.kg_user_switcher_restricted_avatar_icon_color,
getTheme()
)
)
}
val ld = getDrawable(R.drawable.user_switcher_icon_large).mutate()
as LayerDrawable
if (item == userSwitcherController.getCurrentUserRecord()) {
(ld.findDrawableByLayerId(R.id.ring) as GradientDrawable).apply {
val stroke = resources
.getDimensionPixelSize(R.dimen.user_switcher_icon_selected_width)
val color = Utils.getColorAttrDefaultColor(
this@UserSwitcherActivity,
com.android.internal.R.attr.colorAccentPrimary
)
setStroke(stroke, color)
}
}
ld.setDrawableByLayerId(R.id.user_avatar, drawable)
return ld
}
override fun notifyDataSetChanged() {
super.notifyDataSetChanged()
buildUserViews()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.user_switcher_fullscreen)
window.decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
parent = requireViewById<UserSwitcherRootView>(R.id.user_switcher_root)
parent.touchHandler = object : Gefingerpoken {
override fun onTouchEvent(ev: MotionEvent?): Boolean {
falsingCollector.onTouchEvent(ev)
return false
}
}
requireViewById<View>(R.id.cancel).apply {
setOnClickListener {
_ -> finish()
}
}
addButton = requireViewById<View>(R.id.add).apply {
setOnClickListener {
_ -> showPopupMenu()
}
}
userSwitcherController.init(parent)
initBroadcastReceiver()
parent.post { buildUserViews() }
userTracker.addCallback(userSwitchedCallback, mainExecutor)
}
private fun showPopupMenu() {
val items = mutableListOf<UserRecord>()
addUserRecords.forEach { items.add(it) }
var popupMenuAdapter = ItemAdapter(
this,
R.layout.user_switcher_fullscreen_popup_item,
layoutInflater,
{ item: UserRecord -> adapter.getName(this@UserSwitcherActivity, item) },
{ item: UserRecord -> adapter.findUserIcon(item).mutate().apply {
setTint(resources.getColor(
R.color.user_switcher_fullscreen_popup_item_tint,
getTheme()
))
} }
)
popupMenuAdapter.addAll(items)
popupMenu = UserSwitcherPopupMenu(this).apply {
setAnchorView(addButton)
setAdapter(popupMenuAdapter)
setOnItemClickListener {
parent: AdapterView<*>, view: View, pos: Int, id: Long ->
if (falsingManager.isFalseTap(LOW_PENALTY) || !view.isEnabled()) {
return@setOnItemClickListener
}
// -1 for the header
val item = popupMenuAdapter.getItem(pos - 1)
if (item == manageUserRecord) {
val i = Intent().setAction(Settings.ACTION_USER_SETTINGS)
this@UserSwitcherActivity.startActivity(i)
} else {
adapter.onUserListItemClicked(item)
}
dismiss()
popupMenu = null
if (!item.isAddUser) {
this@UserSwitcherActivity.finish()
}
}
show()
}
}
private fun buildUserViews() {
var count = 0
var start = 0
for (i in 0 until parent.getChildCount()) {
if (parent.getChildAt(i).getTag() == USER_VIEW) {
if (count == 0) start = i
count++
}
}
parent.removeViews(start, count)
addUserRecords.clear()
val flow = requireViewById<Flow>(R.id.flow)
val totalWidth = parent.width
val userViewCount = adapter.getTotalUserViews()
val maxColumns = getMaxColumns(userViewCount)
val horizontalGap = resources
.getDimensionPixelSize(R.dimen.user_switcher_fullscreen_horizontal_gap)
val totalWidthOfHorizontalGap = (maxColumns - 1) * horizontalGap
val maxWidgetDiameter = (totalWidth - totalWidthOfHorizontalGap) / maxColumns
flow.setMaxElementsWrap(maxColumns)
for (i in 0 until adapter.getCount()) {
val item = adapter.getItem(i)
if (adapter.doNotRenderUserView(item)) {
addUserRecords.add(item)
} else {
val userView = adapter.getView(i, null, parent)
userView.requireViewById<ImageView>(R.id.user_switcher_icon).apply {
val lp = layoutParams
if (maxWidgetDiameter < lp.width) {
lp.width = maxWidgetDiameter
lp.height = maxWidgetDiameter
layoutParams = lp
}
}
userView.setId(View.generateViewId())
parent.addView(userView)
// Views must have an id and a parent in order for Flow to lay them out
flow.addView(userView)
userView.setOnClickListener { v ->
if (falsingManager.isFalseTap(LOW_PENALTY) || !v.isEnabled()) {
return@setOnClickListener
}
if (!item.isCurrent || item.isGuest) {
adapter.onUserListItemClicked(item)
}
}
}
}
if (!addUserRecords.isEmpty()) {
addUserRecords.add(manageUserRecord)
addButton.visibility = View.VISIBLE
} else {
addButton.visibility = View.GONE
}
}
override fun onBackPressed() {
finish()
}
override fun onDestroy() {
super.onDestroy()
broadcastDispatcher.unregisterReceiver(broadcastReceiver)
userTracker.removeCallback(userSwitchedCallback)
}
private fun initBroadcastReceiver() {
broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.getAction()
if (Intent.ACTION_SCREEN_OFF.equals(action)) {
finish()
}
}
}
val filter = IntentFilter()
filter.addAction(Intent.ACTION_SCREEN_OFF)
broadcastDispatcher.registerReceiver(broadcastReceiver, filter)
}
@VisibleForTesting
fun getMaxColumns(userCount: Int): Int {
return if (userCount < 5) 4 else ceil(userCount / 2.0).toInt()
}
private class ItemAdapter(
val parentContext: Context,
val resource: Int,
val layoutInflater: LayoutInflater,
val textGetter: (UserRecord) -> String,
val iconGetter: (UserRecord) -> Drawable
) : ArrayAdapter<UserRecord>(parentContext, resource) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val item = getItem(position)
val view = convertView ?: layoutInflater.inflate(resource, parent, false)
view.requireViewById<ImageView>(R.id.icon).apply {
setImageDrawable(iconGetter(item))
}
view.requireViewById<TextView>(R.id.text).apply {
setText(textGetter(item))
}
return view
}
}
}