Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 305c452b authored by Ale Nijamkin's avatar Ale Nijamkin Committed by Android (Google) Code Review
Browse files

Merge "Cleans up UserSwitcherActivity." into tm-qpr-dev

parents 616d0b03 ac6753c0
Loading
Loading
Loading
Loading
+0 −4
Original line number Diff line number Diff line
@@ -105,10 +105,6 @@ public class Flags {
     */
    public static final UnreleasedFlag MODERN_BOUNCER = new UnreleasedFlag(208);

    /** Whether UserSwitcherActivity should use modern architecture. */
    public static final ReleasedFlag MODERN_USER_SWITCHER_ACTIVITY =
            new ReleasedFlag(209, true);

    /**
     * Whether the user interactor and repository should use `UserSwitcherController`.
     *
+9 −394
Original line number Diff line number Diff line
@@ -16,109 +16,32 @@

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.util.Log
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 android.window.OnBackInvokedCallback
import android.window.OnBackInvokedDispatcher
import androidx.activity.ComponentActivity
import androidx.constraintlayout.helper.widget.Flow
import androidx.lifecycle.ViewModelProvider
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.flags.FeatureFlags
import com.android.systemui.flags.Flags
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.BaseUserSwitcherAdapter
import com.android.systemui.statusbar.policy.UserSwitcherController
import com.android.systemui.user.data.source.UserRecord
import com.android.systemui.user.ui.binder.UserSwitcherViewBinder
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
import dagger.Lazy
import javax.inject.Inject
import kotlin.math.ceil

private const val USER_VIEW = "user_view"

/** Support a fullscreen user switcher */
open class UserSwitcherActivity
@Inject
constructor(
    private val userSwitcherController: UserSwitcherController,
    private val broadcastDispatcher: BroadcastDispatcher,
    private val falsingCollector: FalsingCollector,
    private val falsingManager: FalsingManager,
    private val userManager: UserManager,
    private val userTracker: UserTracker,
    private val flags: FeatureFlags,
    private val viewModelFactory: Lazy<UserSwitcherViewModel.Factory>,
) : 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 onBackCallback = OnBackInvokedCallback { finish() }
    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: UserAdapter by lazy { UserAdapter() }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        createActivity()
    }

    @VisibleForTesting
    fun createActivity() {
        setContentView(R.layout.user_switcher_fullscreen)
        window.decorView.systemUiVisibility =
            (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
                View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
                View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
        if (isUsingModernArchitecture()) {
            Log.d(TAG, "Using modern architecture.")
        val viewModel =
            ViewModelProvider(this, viewModelFactory.get())[UserSwitcherViewModel::class.java]
        UserSwitcherViewBinder.bind(
@@ -129,313 +52,5 @@ constructor(
            falsingCollector = falsingCollector,
            onFinish = this::finish,
        )
            return
        } else {
            Log.d(TAG, "Not using modern architecture.")
        }

        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() } }

        onBackInvokedDispatcher.registerOnBackInvokedCallback(
            OnBackInvokedDispatcher.PRIORITY_DEFAULT,
            onBackCallback
        )

        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, true) },
                { item: UserRecord ->
                    adapter.findUserIcon(item, true).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() {
        if (isUsingModernArchitecture()) {
            return super.onBackPressed()
        }

        finish()
    }

    override fun onDestroy() {
        super.onDestroy()
        if (isUsingModernArchitecture()) {
            return
        }
        destroyActivity()
    }

    @VisibleForTesting
    fun destroyActivity() {
        onBackInvokedDispatcher.unregisterOnBackInvokedCallback(onBackCallback)
        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 fun isUsingModernArchitecture(): Boolean {
        return flags.isEnabled(Flags.MODERN_USER_SWITCHER_ACTIVITY)
    }

    /** Provides views to populate the option menu. */
    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
        }
    }

    private inner class UserAdapter : BaseUserSwitcherAdapter(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)
            UserSwitcherController.setSelectableAlpha(view)
            view.setTag(USER_VIEW)
            return view
        }

        override fun getName(context: Context, item: UserRecord, isTablet: Boolean): String {
            return if (item == manageUserRecord) {
                getString(R.string.manage_users)
            } else {
                super.getName(context, item, isTablet)
            }
        }

        fun findUserIcon(item: UserRecord, isTablet: Boolean = false): Drawable {
            if (item == manageUserRecord) {
                return getDrawable(R.drawable.ic_manage_users)
            }
            if (item.info == null) {
                return getIconDrawable(this@UserSwitcherActivity, item, isTablet)
            }
            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.currentUserRecord) {
                (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()
        }
    }

    companion object {
        private const val TAG = "UserSwitcherActivity"
    }
}
+0 −11
Original line number Diff line number Diff line
@@ -22,7 +22,6 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.android.systemui.R
import com.android.systemui.user.data.source.UserRecord
import kotlin.math.ceil

/**
 * Defines utility functions for helping with legacy UI code for users.
@@ -33,16 +32,6 @@ import kotlin.math.ceil
 */
object LegacyUserUiHelper {

    /** Returns the maximum number of columns for user items in the user switcher. */
    fun getMaxUserSwitcherItemColumns(userCount: Int): Int {
        // TODO(b/243844097): remove this once we remove the old user switcher implementation.
        return if (userCount < 5) {
            4
        } else {
            ceil(userCount / 2.0).toInt()
        }
    }

    @JvmStatic
    @DrawableRes
    fun getUserSwitcherActionIconResourceId(
+11 −2
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
import javax.inject.Inject
import kotlin.math.ceil
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
@@ -52,8 +53,7 @@ private constructor(
        userInteractor.users.map { models -> models.map { user -> toViewModel(user) } }

    /** The maximum number of columns that the user selection grid should use. */
    val maximumUserColumns: Flow<Int> =
        users.map { LegacyUserUiHelper.getMaxUserSwitcherItemColumns(it.size) }
    val maximumUserColumns: Flow<Int> = users.map { getMaxUserSwitcherItemColumns(it.size) }

    private val _isMenuVisible = MutableStateFlow(false)
    /**
@@ -118,6 +118,15 @@ private constructor(
        _isMenuVisible.value = false
    }

    /** Returns the maximum number of columns for user items in the user switcher. */
    private fun getMaxUserSwitcherItemColumns(userCount: Int): Int {
        return if (userCount < 5) {
            4
        } else {
            ceil(userCount / 2.0).toInt()
        }
    }

    private fun createFinishRequestedFlow(): Flow<Boolean> {
        var mostRecentSelectedUserId: Int? = null
        var mostRecentIsInteractive: Boolean? = null
+0 −152
Original line number Diff line number Diff line
/*
 * 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.app.Application
import android.os.UserManager
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import android.view.LayoutInflater
import android.view.View
import android.view.Window
import android.window.OnBackInvokedCallback
import android.window.OnBackInvokedDispatcher
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.UserSwitcherController
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.eq
import org.mockito.Mockito.mock
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import java.util.concurrent.Executor

@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper(setAsMainLooper = true)
class UserSwitcherActivityTest : SysuiTestCase() {
    @Mock
    private lateinit var activity: UserSwitcherActivity
    @Mock
    private lateinit var userSwitcherController: UserSwitcherController
    @Mock
    private lateinit var broadcastDispatcher: BroadcastDispatcher
    @Mock
    private lateinit var layoutInflater: LayoutInflater
    @Mock
    private lateinit var falsingCollector: FalsingCollector
    @Mock
    private lateinit var falsingManager: FalsingManager
    @Mock
    private lateinit var userManager: UserManager
    @Mock
    private lateinit var userTracker: UserTracker
    @Mock
    private lateinit var flags: FeatureFlags
    @Mock
    private lateinit var viewModelFactoryLazy: dagger.Lazy<UserSwitcherViewModel.Factory>
    @Mock
    private lateinit var onBackDispatcher: OnBackInvokedDispatcher
    @Mock
    private lateinit var decorView: View
    @Mock
    private lateinit var window: Window
    @Mock
    private lateinit var userSwitcherRootView: UserSwitcherRootView
    @Captor
    private lateinit var onBackInvokedCallback: ArgumentCaptor<OnBackInvokedCallback>
    var isFinished = false

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        activity = spy(object : UserSwitcherActivity(
            userSwitcherController,
            broadcastDispatcher,
            falsingCollector,
            falsingManager,
            userManager,
            userTracker,
            flags,
            viewModelFactoryLazy,
        ) {
            override fun getOnBackInvokedDispatcher() = onBackDispatcher
            override fun getMainExecutor(): Executor = FakeExecutor(FakeSystemClock())
            override fun finish() {
                isFinished = true
            }
        })
        `when`(activity.window).thenReturn(window)
        `when`(window.decorView).thenReturn(decorView)
        `when`(activity.findViewById<UserSwitcherRootView>(R.id.user_switcher_root))
                .thenReturn(userSwitcherRootView)
        `when`(activity.findViewById<View>(R.id.cancel)).thenReturn(mock(View::class.java))
        `when`(activity.findViewById<View>(R.id.add)).thenReturn(mock(View::class.java))
        `when`(activity.application).thenReturn(mock(Application::class.java))
        doNothing().`when`(activity).setContentView(anyInt())
    }

    @Test
    fun testMaxColumns() {
        assertThat(activity.getMaxColumns(3)).isEqualTo(4)
        assertThat(activity.getMaxColumns(4)).isEqualTo(4)
        assertThat(activity.getMaxColumns(5)).isEqualTo(3)
        assertThat(activity.getMaxColumns(6)).isEqualTo(3)
        assertThat(activity.getMaxColumns(7)).isEqualTo(4)
        assertThat(activity.getMaxColumns(9)).isEqualTo(5)
    }

    @Test
    fun onCreate_callbackRegistration() {
        activity.createActivity()
        verify(onBackDispatcher).registerOnBackInvokedCallback(
                eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT), any())

        activity.destroyActivity()
        verify(onBackDispatcher).unregisterOnBackInvokedCallback(any())
    }

    @Test
    fun onBackInvokedCallback_finishesActivity() {
        activity.createActivity()
        verify(onBackDispatcher).registerOnBackInvokedCallback(
                eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT), onBackInvokedCallback.capture())

        onBackInvokedCallback.value.onBackInvoked()
        assertThat(isFinished).isTrue()
    }
}