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

Commit 9a922ab3 authored by Fabián Kozynski's avatar Fabián Kozynski
Browse files

Fix icon loading in controls

This introduces a SafeIconLoader that uses the safe loading call from
Icon to make sure that the icon can be accessed by the uid of the
current controls app + user.

Test: manual, using POC apk
Test: atest com.android.systemui.controls
Flag: EXEMPT bugfix
Bug: 322818950

Change-Id: I9419a11466545b9373bd3d1f59c81931a81b2939
parent 8a131dbc
Loading
Loading
Loading
Loading
+7 −9
Original line number Diff line number Diff line
@@ -12,13 +12,14 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.ControlsMetricsLogger
import com.android.systemui.controls.controller.ControlInfo
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.res.R
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import com.android.systemui.utils.SafeIconLoader
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -32,6 +33,7 @@ class TemperatureControlBehaviorTest : SysuiTestCase() {
    @Mock lateinit var controlsMetricsLogger: ControlsMetricsLogger
    @Mock lateinit var controlActionCoordinator: ControlActionCoordinator
    @Mock lateinit var controlsController: ControlsController
    @Mock lateinit var safeIconLoader: SafeIconLoader

    private val fakeSystemClock = FakeSystemClock()
    private val underTest = TemperatureControlBehavior()
@@ -53,6 +55,7 @@ class TemperatureControlBehaviorTest : SysuiTestCase() {
                controlsMetricsLogger,
                0,
                0,
                safeIconLoader,
            )
    }

@@ -61,12 +64,7 @@ class TemperatureControlBehaviorTest : SysuiTestCase() {
        val controlWithState =
            ControlWithState(
                ComponentName("test.pkg", "TestClass"),
                ControlInfo(
                    "test_id",
                    "test title",
                    "test subtitle",
                    DeviceTypes.TYPE_AC_UNIT,
                ),
                ControlInfo("test_id", "test title", "test subtitle", DeviceTypes.TYPE_AC_UNIT),
                Control.StatefulBuilder(
                        "",
                        PendingIntent.getActivity(
@@ -87,11 +85,11 @@ class TemperatureControlBehaviorTest : SysuiTestCase() {
                            ),
                            0,
                            0,
                            0
                            0,
                        )
                    )
                    .setStatus(Control.STATUS_OK)
                    .build()
                    .build(),
            )
        viewHolder.bindData(controlWithState, false)
        underTest.initialize(viewHolder)
+26 −26
Original line number Diff line number Diff line
@@ -43,7 +43,7 @@ class AllModel(
    private val controls: List<ControlStatus>,
    initialFavoriteIds: List<String>,
    private val emptyZoneString: CharSequence,
    private val controlsModelCallback: ControlsModel.ControlsModelCallback
    private val controlsModelCallback: ControlsModel.ControlsModelCallback,
) : ControlsModel {

    private var modified = false
@@ -51,11 +51,10 @@ class AllModel(
    override val moveHelper = null

    override val favorites: List<ControlInfo>
        get() = favoriteIds.mapNotNull { id ->
        get() =
            favoriteIds.mapNotNull { id ->
                val control = controls.firstOrNull { it.control.controlId == id }?.control
            control?.let {
                ControlInfo.fromControl(it)
            }
                control?.let { ControlInfo.fromControl(it) }
            }

    private val favoriteIds = run {
@@ -66,11 +65,13 @@ class AllModel(
    override val elements: List<ElementWrapper> = createWrappers(controls)

    override fun changeFavoriteStatus(controlId: String, favorite: Boolean) {
        val toChange = elements.firstOrNull {
        val toChange =
            elements.firstOrNull {
                it is ControlStatusWrapper && it.controlStatus.control.controlId == controlId
            } as ControlStatusWrapper?
        if (favorite == toChange?.controlStatus?.favorite) return
        val changed: Boolean = if (favorite) {
        val changed: Boolean =
            if (favorite) {
                favoriteIds.add(controlId)
            } else {
                favoriteIds.remove(controlId)
@@ -82,13 +83,12 @@ class AllModel(
            }
            controlsModelCallback.onChange()
        }
        toChange?.let {
            it.controlStatus.favorite = favorite
        }
        toChange?.let { it.controlStatus.favorite = favorite }
    }

    private fun createWrappers(list: List<ControlStatus>): List<ElementWrapper> {
        val map = list.groupByTo(OrderedMap(ArrayMap<CharSequence, MutableList<ControlStatus>>())) {
        val map =
            list.groupByTo(OrderedMap(ArrayMap<CharSequence, MutableList<ControlStatus>>())) {
                it.control.zone ?: ""
            }
        val output = mutableListOf<ElementWrapper>()
+89 −76
Original line number Diff line number Diff line
@@ -36,10 +36,11 @@ import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.recyclerview.widget.RecyclerView
import com.android.systemui.res.R
import com.android.systemui.controls.ControlInterface
import com.android.systemui.controls.ui.CanUseIconPredicate
import com.android.systemui.controls.ui.RenderInfo
import com.android.systemui.res.R
import com.android.systemui.utils.SafeIconLoader

private typealias ModelFavoriteChanger = (String, Boolean) -> Unit

@@ -54,6 +55,7 @@ private typealias ModelFavoriteChanger = (String, Boolean) -> Unit
class ControlAdapter(
    private val elevation: Float,
    private val currentUserId: Int,
    private val safeIconLoader: SafeIconLoader,
) : RecyclerView.Adapter<Holder>() {

    companion object {
@@ -62,9 +64,8 @@ class ControlAdapter(
        const val TYPE_DIVIDER = 2

        /**
         * For low-dp width screens that also employ an increased font scale, adjust the
         * number of columns. This helps prevent text truncation on these devices.
         *
         * For low-dp width screens that also employ an increased font scale, adjust the number of
         * columns. This helps prevent text truncation on these devices.
         */
        @JvmStatic
        fun findMaxColumns(res: Resources): Int {
@@ -78,10 +79,12 @@ class ControlAdapter(

            val config = res.configuration
            val isPortrait = config.orientation == Configuration.ORIENTATION_PORTRAIT
            if (isPortrait &&
            if (
                isPortrait &&
                    config.screenWidthDp != Configuration.SCREEN_WIDTH_DP_UNDEFINED &&
                    config.screenWidthDp <= maxColumnsAdjustWidth &&
                    config.fontScale >= maxColumnsAdjustFontScale) {
                    config.fontScale >= maxColumnsAdjustFontScale
            ) {
                maxColumns--
            }

@@ -106,11 +109,12 @@ class ControlAdapter(
                            rightMargin = 0
                        }
                        elevation = this@ControlAdapter.elevation
                        background = parent.context.getDrawable(
                                R.drawable.control_background_ripple)
                        background =
                            parent.context.getDrawable(R.drawable.control_background_ripple)
                    },
                    currentUserId,
                    model?.moveHelper, // Indicates that position information is needed
                    safeIconLoader,
                ) { id, favorite ->
                    model?.changeFavoriteStatus(id, favorite)
                }
@@ -119,8 +123,13 @@ class ControlAdapter(
                ZoneHolder(layoutInflater.inflate(R.layout.controls_zone_header, parent, false))
            }
            TYPE_DIVIDER -> {
                DividerHolder(layoutInflater.inflate(
                        R.layout.controls_horizontal_divider_with_empty, parent, false))
                DividerHolder(
                    layoutInflater.inflate(
                        R.layout.controls_horizontal_divider_with_empty,
                        parent,
                        false,
                    )
                )
            }
            else -> throw IllegalStateException("Wrong viewType: $viewType")
        }
@@ -134,9 +143,7 @@ class ControlAdapter(
    override fun getItemCount() = model?.elements?.size ?: 0

    override fun onBindViewHolder(holder: Holder, index: Int) {
        model?.let {
            holder.bindData(it.elements[index])
        }
        model?.let { holder.bindData(it.elements[index]) }
    }

    override fun onBindViewHolder(holder: Holder, position: Int, payloads: MutableList<Any>) {
@@ -166,13 +173,12 @@ class ControlAdapter(

/**
 * Holder for binding views in the [RecyclerView]-
 *
 * @param view the [View] for this [Holder]
 */
sealed class Holder(view: View) : RecyclerView.ViewHolder(view) {

    /**
     * Bind the data from the model into the view
     */
    /** Bind the data from the model into the view */
    abstract fun bindData(wrapper: ElementWrapper)

    open fun updateFavorite(favorite: Boolean) {}
@@ -181,12 +187,13 @@ sealed class Holder(view: View) : RecyclerView.ViewHolder(view) {
/**
 * Holder for using with [DividerWrapper] to display a divider between zones.
 *
 * The divider can be shown or hidden. It also has a view the height of a control, that can
 * be toggled visible or gone.
 * The divider can be shown or hidden. It also has a view the height of a control, that can be
 * toggled visible or gone.
 */
private class DividerHolder(view: View) : Holder(view) {
    private val frame: View = itemView.requireViewById(R.id.frame)
    private val divider: View = itemView.requireViewById(R.id.divider)

    override fun bindData(wrapper: ElementWrapper) {
        wrapper as DividerWrapper
        frame.visibility = if (wrapper.showNone) View.VISIBLE else View.GONE
@@ -194,9 +201,7 @@ private class DividerHolder(view: View) : Holder(view) {
    }
}

/**
 * Holder for using with [ZoneNameWrapper] to display names of zones.
 */
/** Holder for using with [ZoneNameWrapper] to display names of zones. */
private class ZoneHolder(view: View) : Holder(view) {
    private val zone: TextView = itemView as TextView

@@ -208,15 +213,17 @@ private class ZoneHolder(view: View) : Holder(view) {

/**
 * Holder for using with [ControlStatusWrapper] to display names of zones.
 *
 * @param moveHelper a helper interface to facilitate a11y rearranging. Null indicates no
 *   rearranging
 * @param favoriteCallback this callback will be called whenever the favorite state of the
 *                         [Control] this view represents changes.
 * @param favoriteCallback this callback will be called whenever the favorite state of the [Control]
 *   this view represents changes.
 */
internal class ControlHolder(
    view: View,
    currentUserId: Int,
    val moveHelper: ControlsModel.MoveHelper?,
    val safeIconLoader: SafeIconLoader,
    val favoriteCallback: ModelFavoriteChanger,
) : Holder(view) {
    private val favoriteStateDescription =
@@ -228,15 +235,15 @@ internal class ControlHolder(
    private val title: TextView = itemView.requireViewById(R.id.title)
    private val subtitle: TextView = itemView.requireViewById(R.id.subtitle)
    private val removed: TextView = itemView.requireViewById(R.id.status)
    private val favorite: CheckBox = itemView.requireViewById<CheckBox>(R.id.favorite).apply {
        visibility = View.VISIBLE
    }
    private val favorite: CheckBox =
        itemView.requireViewById<CheckBox>(R.id.favorite).apply { visibility = View.VISIBLE }

    private val canUseIconPredicate = CanUseIconPredicate(currentUserId)
    private val accessibilityDelegate = ControlHolderAccessibilityDelegate(
    private val accessibilityDelegate =
        ControlHolderAccessibilityDelegate(
            this::stateDescription,
            this::getLayoutPosition,
        moveHelper
            moveHelper,
        )

    init {
@@ -252,7 +259,9 @@ internal class ControlHolder(
        } else {
            val position = layoutPosition + 1
            return itemView.context.getString(
                R.string.accessibility_control_favorite_position, position)
                R.string.accessibility_control_favorite_position,
                position,
            )
        }
    }

@@ -262,7 +271,8 @@ internal class ControlHolder(
        title.text = wrapper.title
        subtitle.text = wrapper.subtitle
        updateFavorite(wrapper.favorite)
        removed.text = if (wrapper.removed) {
        removed.text =
            if (wrapper.removed) {
                itemView.context.getText(R.string.controls_removed)
            } else {
                ""
@@ -282,7 +292,7 @@ internal class ControlHolder(

    private fun getRenderInfo(
        component: ComponentName,
        @DeviceTypes.DeviceType deviceType: Int
        @DeviceTypes.DeviceType deviceType: Int,
    ): RenderInfo {
        return RenderInfo.lookup(itemView.context, component, deviceType)
    }
@@ -292,11 +302,12 @@ internal class ControlHolder(
        val fg = context.getResources().getColorStateList(ri.foreground, context.getTheme())

        icon.imageTintList = null
        ci.customIcon
                ?.takeIf(canUseIconPredicate)
                ?.let {
            icon.setImageIcon(it)
        } ?: run {
        ci.customIcon?.takeIf(canUseIconPredicate)?.let {
            val drawable = safeIconLoader.load(it)
            icon.setImageDrawable(drawable)
            drawable
        }
            ?: run {
                icon.setImageDrawable(ri.icon)

                // Do not color app icons
@@ -317,14 +328,13 @@ internal class ControlHolder(
 *
 * @param stateRetriever function to determine the state description based on the favorite state
 * @param positionRetriever function to obtain the position of this control. It only has to be
 *                          correct in controls that are currently favorites (and therefore can
 *                          be moved).
 *   correct in controls that are currently favorites (and therefore can be moved).
 * @param moveHelper helper interface to determine if a control can be moved and actually move it.
 */
private class ControlHolderAccessibilityDelegate(
    val stateRetriever: (Boolean) -> CharSequence?,
    val positionRetriever: () -> Int,
    val moveHelper: ControlsModel.MoveHelper?
    val moveHelper: ControlsModel.MoveHelper?,
) : AccessibilityDelegateCompat() {

    var isFavorite = false
@@ -369,24 +379,28 @@ private class ControlHolderAccessibilityDelegate(

    private fun addClickAction(host: View, info: AccessibilityNodeInfoCompat) {
        // Change the text for the double-tap action
        val clickActionString = if (isFavorite) {
        val clickActionString =
            if (isFavorite) {
                host.context.getString(R.string.accessibility_control_change_unfavorite)
            } else {
                host.context.getString(R.string.accessibility_control_change_favorite)
            }
        val click = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
        val click =
            AccessibilityNodeInfoCompat.AccessibilityActionCompat(
                AccessibilityNodeInfo.ACTION_CLICK,
                // “favorite/unfavorite”
            clickActionString)
                clickActionString,
            )
        info.addAction(click)
    }

    private fun maybeAddMoveBeforeAction(host: View, info: AccessibilityNodeInfoCompat) {
        if (moveHelper?.canMoveBefore(positionRetriever()) ?: false) {
            val newPosition = positionRetriever() + 1 - 1
            val moveBefore = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
            val moveBefore =
                AccessibilityNodeInfoCompat.AccessibilityActionCompat(
                    MOVE_BEFORE_ID,
                host.context.getString(R.string.accessibility_control_move, newPosition)
                    host.context.getString(R.string.accessibility_control_move, newPosition),
                )
            info.addAction(moveBefore)
            info.isContextClickable = true
@@ -396,9 +410,10 @@ private class ControlHolderAccessibilityDelegate(
    private fun maybeAddMoveAfterAction(host: View, info: AccessibilityNodeInfoCompat) {
        if (moveHelper?.canMoveAfter(positionRetriever()) ?: false) {
            val newPosition = positionRetriever() + 1 + 1
            val moveAfter = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
            val moveAfter =
                AccessibilityNodeInfoCompat.AccessibilityActionCompat(
                    MOVE_AFTER_ID,
                host.context.getString(R.string.accessibility_control_move, newPosition)
                    host.context.getString(R.string.accessibility_control_move, newPosition),
                )
            info.addAction(moveAfter)
            info.isContextClickable = true
@@ -406,16 +421,14 @@ private class ControlHolderAccessibilityDelegate(
    }
}

class MarginItemDecorator(
    private val topMargin: Int,
    private val sideMargins: Int
) : RecyclerView.ItemDecoration() {
class MarginItemDecorator(private val topMargin: Int, private val sideMargins: Int) :
    RecyclerView.ItemDecoration() {

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
        state: RecyclerView.State,
    ) {
        val position = parent.getChildAdapterPosition(view)
        if (position == RecyclerView.NO_POSITION) return
+15 −1
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Process
import android.util.Log
import android.view.View
import android.view.ViewGroup
@@ -42,6 +43,7 @@ import com.android.systemui.controls.ui.ControlsActivity
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.res.R
import com.android.systemui.settings.UserTracker
import com.android.systemui.utils.SafeIconLoader
import java.util.concurrent.Executor
import javax.inject.Inject

@@ -53,6 +55,8 @@ constructor(
    private val controller: ControlsControllerImpl,
    private val userTracker: UserTracker,
    private val customIconCache: CustomIconCache,
    private val controlsListingController: ControlsListingController,
    private val safeIconLoaderFactory: SafeIconLoader.Factory,
) : ComponentActivity(), ControlsManagementActivity {

    companion object {
@@ -258,8 +262,18 @@ constructor(
        val elevation = resources.getFloat(R.dimen.control_card_elevation)
        val recyclerView = requireViewById<RecyclerView>(R.id.list)
        recyclerView.alpha = 0.0f
        val uid =
            controlsListingController
                .getCurrentServices()
                .firstOrNull { it.componentName == component }
                ?.serviceInfo
                ?.applicationInfo
                ?.uid ?: Process.INVALID_UID
        val packageName = component.packageName
        val safeIconLoader = safeIconLoaderFactory.create(uid, packageName, userTracker.userId)

        val adapter =
            ControlAdapter(elevation, userTracker.userId).apply {
            ControlAdapter(elevation, userTracker.userId, safeIconLoader).apply {
                registerAdapterDataObserver(
                    object : RecyclerView.AdapterDataObserver() {
                        var hasAnimated = false
+27 −2
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.os.Process.INVALID_UID
import android.text.TextUtils
import android.util.Log
import android.view.Gravity
@@ -47,6 +48,7 @@ import com.android.systemui.controls.ui.ControlsActivity
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.res.R
import com.android.systemui.settings.UserTracker
import com.android.systemui.utils.SafeIconLoader
import java.text.Collator
import java.util.concurrent.Executor
import javax.inject.Inject
@@ -57,6 +59,8 @@ constructor(
    @Main private val executor: Executor,
    private val controller: ControlsControllerImpl,
    private val userTracker: UserTracker,
    private val safeIconLoaderFactory: SafeIconLoader.Factory,
    private val controlsListingController: ControlsListingController,
) : ComponentActivity(), ControlsManagementActivity {

    companion object {
@@ -196,9 +200,20 @@ constructor(
                        listOfStructures = listOf(listOfStructures[structureIndex])
                    }

                    val uid =
                        controlsListingController
                            .getCurrentServices()
                            .firstOrNull { it.componentName == componentName }
                            ?.serviceInfo
                            ?.applicationInfo
                            ?.uid ?: INVALID_UID
                    val packageName = componentName.packageName
                    val safeIconLoader =
                        safeIconLoaderFactory.create(uid, packageName, userTracker.userId)

                    executor.execute {
                        structurePager.adapter =
                            StructureAdapter(listOfStructures, userTracker.userId)
                            StructureAdapter(listOfStructures, userTracker.userId, safeIconLoader)
                        structurePager.setCurrentItem(structureIndex)
                        if (error) {
                            statusText.text =
@@ -260,8 +275,18 @@ constructor(
    private fun setUpPager() {
        structurePager.alpha = 0.0f
        pageIndicator.alpha = 0.0f
        val uid =
            controlsListingController
                .getCurrentServices()
                .firstOrNull { it.componentName == component }
                ?.serviceInfo
                ?.applicationInfo
                ?.uid ?: INVALID_UID
        val packageName = componentName?.packageName ?: ""
        val safeIconLoader = safeIconLoaderFactory.create(uid, packageName, userTracker.userId)

        structurePager.apply {
            adapter = StructureAdapter(emptyList(), userTracker.userId)
            adapter = StructureAdapter(emptyList(), userTracker.userId, safeIconLoader)
            registerOnPageChangeCallback(
                object : ViewPager2.OnPageChangeCallback() {
                    override fun onPageSelected(position: Int) {
Loading