summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/TemperatureControlBehaviorTest.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt52
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt165
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/management/StructureAdapter.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt324
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt600
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/ui/ThumbnailBehavior.kt45
-rw-r--r--packages/SystemUI/src/com/android/systemui/utils/SafeIconLoader.kt68
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsEditingActivityTest.kt48
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt46
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlViewHolderTest.kt106
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt24
14 files changed, 965 insertions, 609 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/TemperatureControlBehaviorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/TemperatureControlBehaviorTest.kt
index b3f458821cba..70570f9323f1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/TemperatureControlBehaviorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/TemperatureControlBehaviorTest.kt
@@ -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)
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt b/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt
index be428a84da2f..9e9e9998a82e 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt
@@ -25,14 +25,14 @@ import com.android.systemui.controls.controller.ControlInfo
* This model is used to show controls separated by zones.
*
* The model will sort the controls and zones in the following manner:
- * * The zones will be sorted in a first seen basis
- * * The controls in each zone will be sorted in a first seen basis.
+ * * The zones will be sorted in a first seen basis
+ * * The controls in each zone will be sorted in a first seen basis.
*
- * The controls passed should belong to the same structure, as an instance of this model will be
- * created for each structure.
+ * The controls passed should belong to the same structure, as an instance of this model will be
+ * created for each structure.
*
- * The list of favorite ids can contain ids for controls not passed to this model. Those will be
- * filtered out.
+ * The list of favorite ids can contain ids for controls not passed to this model. Those will be
+ * filtered out.
*
* @property controls List of controls as returned by loading
* @property initialFavoriteIds sorted ids of favorite controls.
@@ -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,12 +51,11 @@ class AllModel(
override val moveHelper = null
override val favorites: List<ControlInfo>
- get() = favoriteIds.mapNotNull { id ->
- val control = controls.firstOrNull { it.control.controlId == id }?.control
- control?.let {
- ControlInfo.fromControl(it)
+ get() =
+ favoriteIds.mapNotNull { id ->
+ val control = controls.firstOrNull { it.control.controlId == id }?.control
+ control?.let { ControlInfo.fromControl(it) }
}
- }
private val favoriteIds = run {
val ids = controls.mapTo(HashSet()) { it.control.controlId }
@@ -66,15 +65,17 @@ class AllModel(
override val elements: List<ElementWrapper> = createWrappers(controls)
override fun changeFavoriteStatus(controlId: String, favorite: Boolean) {
- val toChange = elements.firstOrNull {
- it is ControlStatusWrapper && it.controlStatus.control.controlId == controlId
- } as ControlStatusWrapper?
+ 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) {
- favoriteIds.add(controlId)
- } else {
- favoriteIds.remove(controlId)
- }
+ val changed: Boolean =
+ if (favorite) {
+ favoriteIds.add(controlId)
+ } else {
+ favoriteIds.remove(controlId)
+ }
if (changed) {
if (!modified) {
modified = true
@@ -82,15 +83,14 @@ 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>>())) {
- it.control.zone ?: ""
- }
+ val map =
+ list.groupByTo(OrderedMap(ArrayMap<CharSequence, MutableList<ControlStatus>>())) {
+ it.control.zone ?: ""
+ }
val output = mutableListOf<ElementWrapper>()
var emptyZoneValues: Sequence<ControlStatusWrapper>? = null
for (zoneName in map.orderedKeys) {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
index f034851e5d80..3ea415675bdf 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
@@ -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,15 +64,14 @@ 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 {
var maxColumns = res.getInteger(R.integer.controls_max_columns)
val maxColumnsAdjustWidth =
- res.getInteger(R.integer.controls_max_columns_adjust_below_width_dp)
+ res.getInteger(R.integer.controls_max_columns_adjust_below_width_dp)
val outValue = TypedValue()
res.getValue(R.dimen.controls_max_columns_adjust_above_font_scale, outValue, true)
@@ -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.
+ * rearranging
+ * @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,16 +235,16 @@ 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(
- this::stateDescription,
- this::getLayoutPosition,
- moveHelper
- )
+ private val accessibilityDelegate =
+ ControlHolderAccessibilityDelegate(
+ this::stateDescription,
+ this::getLayoutPosition,
+ moveHelper,
+ )
init {
ViewCompat.setAccessibilityDelegate(itemView, accessibilityDelegate)
@@ -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,11 +271,12 @@ internal class ControlHolder(
title.text = wrapper.title
subtitle.text = wrapper.subtitle
updateFavorite(wrapper.favorite)
- removed.text = if (wrapper.removed) {
- itemView.context.getText(R.string.controls_removed)
- } else {
- ""
- }
+ removed.text =
+ if (wrapper.removed) {
+ itemView.context.getText(R.string.controls_removed)
+ } else {
+ ""
+ }
itemView.setOnClickListener {
updateFavorite(!favorite.isChecked)
favoriteCallback(wrapper.controlId, favorite.isChecked)
@@ -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,18 +302,19 @@ internal class ControlHolder(
val fg = context.getResources().getColorStateList(ri.foreground, context.getTheme())
icon.imageTintList = null
- ci.customIcon
- ?.takeIf(canUseIconPredicate)
- ?.let {
- icon.setImageIcon(it)
- } ?: run {
- icon.setImageDrawable(ri.icon)
-
- // Do not color app icons
- if (ci.deviceType != DeviceTypes.TYPE_ROUTINE) {
- icon.setImageTintList(fg)
- }
+ ci.customIcon?.takeIf(canUseIconPredicate)?.let {
+ val drawable = safeIconLoader.load(it)
+ icon.setImageDrawable(drawable)
+ drawable
}
+ ?: run {
+ icon.setImageDrawable(ri.icon)
+
+ // Do not color app icons
+ if (ci.deviceType != DeviceTypes.TYPE_ROUTINE) {
+ icon.setImageTintList(fg)
+ }
+ }
}
}
@@ -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,25 +379,29 @@ private class ControlHolderAccessibilityDelegate(
private fun addClickAction(host: View, info: AccessibilityNodeInfoCompat) {
// Change the text for the double-tap action
- 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(
- AccessibilityNodeInfo.ACTION_CLICK,
- // “favorite/unfavorite”
- clickActionString)
+ 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(
+ AccessibilityNodeInfo.ACTION_CLICK,
+ // “favorite/unfavorite”
+ 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(
- MOVE_BEFORE_ID,
- host.context.getString(R.string.accessibility_control_move, newPosition)
- )
+ val moveBefore =
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ MOVE_BEFORE_ID,
+ host.context.getString(R.string.accessibility_control_move, newPosition),
+ )
info.addAction(moveBefore)
info.isContextClickable = true
}
@@ -396,26 +410,25 @@ private class ControlHolderAccessibilityDelegate(
private fun maybeAddMoveAfterAction(host: View, info: AccessibilityNodeInfoCompat) {
if (moveHelper?.canMoveAfter(positionRetriever()) ?: false) {
val newPosition = positionRetriever() + 1 + 1
- val moveAfter = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
- MOVE_AFTER_ID,
- host.context.getString(R.string.accessibility_control_move, newPosition)
- )
+ val moveAfter =
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ MOVE_AFTER_ID,
+ host.context.getString(R.string.accessibility_control_move, newPosition),
+ )
info.addAction(moveAfter)
info.isContextClickable = true
}
}
}
-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
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt
index 740e011b3d20..e4913cb59768 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt
@@ -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
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
index ab55c5326b55..80c9d0bb8858 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
@@ -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) {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/StructureAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/StructureAdapter.kt
index 7e56077dec29..dc6f3c7a514b 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/StructureAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/StructureAdapter.kt
@@ -22,10 +22,12 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.systemui.res.R
+import com.android.systemui.utils.SafeIconLoader
class StructureAdapter(
private val models: List<StructureContainer>,
private val currentUserId: Int,
+ private val safeIconLoader: SafeIconLoader,
) : RecyclerView.Adapter<StructureAdapter.StructureHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, p1: Int): StructureHolder {
@@ -33,6 +35,7 @@ class StructureAdapter(
return StructureHolder(
layoutInflater.inflate(R.layout.controls_structure_page, parent, false),
currentUserId,
+ safeIconLoader,
)
}
@@ -42,8 +45,8 @@ class StructureAdapter(
holder.bind(models[index].model)
}
- class StructureHolder(view: View, currentUserId: Int) :
- RecyclerView.ViewHolder(view) {
+ class StructureHolder(view: View, currentUserId: Int, safeIconLoader: SafeIconLoader) :
+ RecyclerView.ViewHolder(view) {
private val recyclerView: RecyclerView
private val controlAdapter: ControlAdapter
@@ -51,7 +54,7 @@ class StructureAdapter(
init {
recyclerView = itemView.requireViewById<RecyclerView>(R.id.listAll)
val elevation = itemView.context.resources.getFloat(R.dimen.control_card_elevation)
- controlAdapter = ControlAdapter(elevation, currentUserId)
+ controlAdapter = ControlAdapter(elevation, currentUserId, safeIconLoader)
setUpRecyclerView()
}
@@ -60,23 +63,29 @@ class StructureAdapter(
}
private fun setUpRecyclerView() {
- val margin = itemView.context.resources
- .getDimensionPixelSize(R.dimen.controls_card_margin)
+ val margin =
+ itemView.context.resources.getDimensionPixelSize(R.dimen.controls_card_margin)
val itemDecorator = MarginItemDecorator(margin, margin)
val spanCount = ControlAdapter.findMaxColumns(itemView.resources)
recyclerView.apply {
this.adapter = controlAdapter
- layoutManager = GridLayoutManager(recyclerView.context, spanCount).apply {
- spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
- override fun getSpanSize(position: Int): Int {
- return if (adapter?.getItemViewType(position)
- != ControlAdapter.TYPE_CONTROL) spanCount else 1
- }
+ layoutManager =
+ GridLayoutManager(recyclerView.context, spanCount).apply {
+ spanSizeLookup =
+ object : GridLayoutManager.SpanSizeLookup() {
+ override fun getSpanSize(position: Int): Int {
+ return if (
+ adapter?.getItemViewType(position) !=
+ ControlAdapter.TYPE_CONTROL
+ )
+ spanCount
+ else 1
+ }
+ }
}
- }
addItemDecoration(itemDecorator)
}
}
}
-} \ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
index fdb9971a7d63..5e09b7f03822 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
@@ -48,12 +48,13 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.VisibleForTesting
-import com.android.internal.graphics.ColorUtils
-import com.android.systemui.res.R
import com.android.app.animation.Interpolators
+import com.android.internal.graphics.ColorUtils
import com.android.systemui.controls.ControlsMetricsLogger
import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.res.R
import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.utils.SafeIconLoader
import java.util.function.Supplier
/**
@@ -70,6 +71,7 @@ class ControlViewHolder(
val controlsMetricsLogger: ControlsMetricsLogger,
val uid: Int,
val currentUserId: Int,
+ val safeIconLoader: SafeIconLoader,
) {
companion object {
@@ -78,10 +80,8 @@ class ControlViewHolder(
private const val ALPHA_DISABLED = 0
private const val STATUS_ALPHA_ENABLED = 1f
private const val STATUS_ALPHA_DIMMED = 0.45f
- private val FORCE_PANEL_DEVICES = setOf(
- DeviceTypes.TYPE_THERMOSTAT,
- DeviceTypes.TYPE_CAMERA
- )
+ private val FORCE_PANEL_DEVICES =
+ setOf(DeviceTypes.TYPE_THERMOSTAT, DeviceTypes.TYPE_CAMERA)
private val ATTR_ENABLED = intArrayOf(android.R.attr.state_enabled)
private val ATTR_DISABLED = intArrayOf(-android.R.attr.state_enabled)
const val MIN_LEVEL = 0
@@ -89,8 +89,8 @@ class ControlViewHolder(
}
private val canUseIconPredicate = CanUseIconPredicate(currentUserId)
- private val toggleBackgroundIntensity: Float = layout.context.resources
- .getFraction(R.fraction.controls_toggle_bg_intensity, 1, 1)
+ private val toggleBackgroundIntensity: Float =
+ layout.context.resources.getFraction(R.fraction.controls_toggle_bg_intensity, 1, 1)
private var stateAnimator: ValueAnimator? = null
private var statusAnimator: Animator? = null
private val baseLayer: GradientDrawable
@@ -112,8 +112,10 @@ class ControlViewHolder(
val deviceType: Int
get() = cws.control?.let { it.deviceType } ?: cws.ci.deviceType
+
val controlStatus: Int
get() = cws.control?.let { it.status } ?: Control.STATUS_UNKNOWN
+
val controlTemplate: ControlTemplate
get() = cws.control?.let { it.controlTemplate } ?: ControlTemplate.NO_TEMPLATE
@@ -129,14 +131,15 @@ class ControlViewHolder(
}
fun findBehaviorClass(
- status: Int,
- template: ControlTemplate,
- deviceType: Int
+ status: Int,
+ template: ControlTemplate,
+ deviceType: Int,
): Supplier<out Behavior> {
return when {
status != Control.STATUS_OK -> Supplier { StatusBehavior() }
template == ControlTemplate.NO_TEMPLATE -> Supplier { TouchBehavior() }
- template is ThumbnailTemplate -> Supplier { ThumbnailBehavior(currentUserId) }
+ template is ThumbnailTemplate ->
+ Supplier { ThumbnailBehavior(currentUserId, safeIconLoader) }
// Required for legacy support, or where cameras do not use the new template
deviceType == DeviceTypes.TYPE_CAMERA -> Supplier { TouchBehavior() }
@@ -172,18 +175,20 @@ class ControlViewHolder(
cws.control?.let {
layout.setClickable(true)
- layout.setOnLongClickListener(View.OnLongClickListener() {
- controlActionCoordinator.longPress(this@ControlViewHolder)
- true
- })
+ layout.setOnLongClickListener(
+ View.OnLongClickListener() {
+ controlActionCoordinator.longPress(this@ControlViewHolder)
+ true
+ }
+ )
controlActionCoordinator.runPendingAction(cws.ci.controlId)
}
val wasLoading = isLoading
isLoading = false
- behavior = bindBehavior(behavior,
- findBehaviorClass(controlStatus, controlTemplate, deviceType))
+ behavior =
+ bindBehavior(behavior, findBehaviorClass(controlStatus, controlTemplate, deviceType))
updateContentDescription()
// Only log one event per control, at the moment we have determined that the control
@@ -198,8 +203,7 @@ class ControlViewHolder(
// OK responses signal normal behavior, and the app will provide control updates
val failedAttempt = lastChallengeDialog != null
when (response) {
- ControlAction.RESPONSE_OK ->
- lastChallengeDialog = null
+ ControlAction.RESPONSE_OK -> lastChallengeDialog = null
ControlAction.RESPONSE_UNKNOWN -> {
lastChallengeDialog = null
setErrorStatus()
@@ -209,18 +213,28 @@ class ControlViewHolder(
setErrorStatus()
}
ControlAction.RESPONSE_CHALLENGE_PIN -> {
- lastChallengeDialog = ChallengeDialogs.createPinDialog(
- this, false /* useAlphanumeric */, failedAttempt, onDialogCancel)
+ lastChallengeDialog =
+ ChallengeDialogs.createPinDialog(
+ this,
+ false /* useAlphanumeric */,
+ failedAttempt,
+ onDialogCancel,
+ )
lastChallengeDialog?.show()
}
ControlAction.RESPONSE_CHALLENGE_PASSPHRASE -> {
- lastChallengeDialog = ChallengeDialogs.createPinDialog(
- this, true /* useAlphanumeric */, failedAttempt, onDialogCancel)
+ lastChallengeDialog =
+ ChallengeDialogs.createPinDialog(
+ this,
+ true /* useAlphanumeric */,
+ failedAttempt,
+ onDialogCancel,
+ )
lastChallengeDialog?.show()
}
ControlAction.RESPONSE_CHALLENGE_ACK -> {
- lastChallengeDialog = ChallengeDialogs.createConfirmationDialog(
- this, onDialogCancel)
+ lastChallengeDialog =
+ ChallengeDialogs.createConfirmationDialog(this, onDialogCancel)
lastChallengeDialog?.show()
}
}
@@ -235,9 +249,7 @@ class ControlViewHolder(
fun setErrorStatus() {
val text = context.resources.getString(R.string.controls_error_failed)
- animateStatusChange(/* animated */ true, {
- setStatusText(text, /* immediately */ true)
- })
+ animateStatusChange(/* animated */ true, { setStatusText(text, /* immediately */ true) })
}
private fun updateContentDescription() =
@@ -256,34 +268,32 @@ class ControlViewHolder(
fun bindBehavior(
existingBehavior: Behavior?,
supplier: Supplier<out Behavior>,
- offset: Int = 0
+ offset: Int = 0,
): Behavior {
val newBehavior = supplier.get()
- val behavior = if (existingBehavior == null ||
- existingBehavior::class != newBehavior::class) {
- // Behavior changes can signal a change in template from the app or
- // first time setup
- newBehavior.initialize(this)
-
- // let behaviors define their own, if necessary, and clear any existing ones
- layout.setAccessibilityDelegate(null)
- newBehavior
- } else {
- existingBehavior
- }
+ val behavior =
+ if (existingBehavior == null || existingBehavior::class != newBehavior::class) {
+ // Behavior changes can signal a change in template from the app or
+ // first time setup
+ newBehavior.initialize(this)
+
+ // let behaviors define their own, if necessary, and clear any existing ones
+ layout.setAccessibilityDelegate(null)
+ newBehavior
+ } else {
+ existingBehavior
+ }
- return behavior.also {
- it.bind(cws, offset)
- }
+ return behavior.also { it.bind(cws, offset) }
}
internal fun applyRenderInfo(enabled: Boolean, offset: Int, animated: Boolean = true) {
- val deviceTypeOrError = if (controlStatus == Control.STATUS_OK ||
- controlStatus == Control.STATUS_UNKNOWN) {
- deviceType
- } else {
- RenderInfo.ERROR_ICON
- }
+ val deviceTypeOrError =
+ if (controlStatus == Control.STATUS_OK || controlStatus == Control.STATUS_UNKNOWN) {
+ deviceType
+ } else {
+ RenderInfo.ERROR_ICON
+ }
val ri = RenderInfo.lookup(context, cws.componentName, deviceTypeOrError, offset)
val fg = context.resources.getColorStateList(ri.foreground, context.theme)
val newText = nextStatusText
@@ -317,28 +327,31 @@ class ControlViewHolder(
private fun animateBackgroundChange(
animated: Boolean,
enabled: Boolean,
- @ColorRes bgColor: Int
+ @ColorRes bgColor: Int,
) {
val bg = context.resources.getColor(R.color.control_default_background, context.theme)
- val (newClipColor, newAlpha) = if (enabled) {
- // allow color overrides for the enabled state only
- val color = cws.control?.getCustomColor()?.let {
- val state = intArrayOf(android.R.attr.state_enabled)
- it.getColorForState(state, it.getDefaultColor())
- } ?: context.resources.getColor(bgColor, context.theme)
- listOf(color, ALPHA_ENABLED)
- } else {
- listOf(
- context.resources.getColor(R.color.control_default_background, context.theme),
- ALPHA_DISABLED
- )
- }
- val newBaseColor = if (behavior is ToggleRangeBehavior) {
- ColorUtils.blendARGB(bg, newClipColor, toggleBackgroundIntensity)
- } else {
- bg
- }
+ val (newClipColor, newAlpha) =
+ if (enabled) {
+ // allow color overrides for the enabled state only
+ val color =
+ cws.control?.getCustomColor()?.let {
+ val state = intArrayOf(android.R.attr.state_enabled)
+ it.getColorForState(state, it.getDefaultColor())
+ } ?: context.resources.getColor(bgColor, context.theme)
+ listOf(color, ALPHA_ENABLED)
+ } else {
+ listOf(
+ context.resources.getColor(R.color.control_default_background, context.theme),
+ ALPHA_DISABLED,
+ )
+ }
+ val newBaseColor =
+ if (behavior is ToggleRangeBehavior) {
+ ColorUtils.blendARGB(bg, newClipColor, toggleBackgroundIntensity)
+ } else {
+ bg
+ }
clipLayer.drawable?.apply {
clipLayer.alpha = ALPHA_DISABLED
@@ -347,7 +360,11 @@ class ControlViewHolder(
startBackgroundAnimation(this, newAlpha, newClipColor, newBaseColor)
} else {
applyBackgroundChange(
- this, newAlpha, newClipColor, newBaseColor, newLayoutAlpha = 1f
+ this,
+ newAlpha,
+ newClipColor,
+ newBaseColor,
+ newLayoutAlpha = 1f,
)
}
}
@@ -357,41 +374,45 @@ class ControlViewHolder(
clipDrawable: Drawable,
newAlpha: Int,
@ColorInt newClipColor: Int,
- @ColorInt newBaseColor: Int
+ @ColorInt newBaseColor: Int,
) {
- val oldClipColor = if (clipDrawable is GradientDrawable) {
- clipDrawable.color?.defaultColor ?: newClipColor
- } else {
- newClipColor
- }
+ val oldClipColor =
+ if (clipDrawable is GradientDrawable) {
+ clipDrawable.color?.defaultColor ?: newClipColor
+ } else {
+ newClipColor
+ }
val oldBaseColor = baseLayer.color?.defaultColor ?: newBaseColor
val oldAlpha = layout.alpha
- stateAnimator = ValueAnimator.ofInt(clipLayer.alpha, newAlpha).apply {
- addUpdateListener {
- val updatedAlpha = it.animatedValue as Int
- val updatedClipColor = ColorUtils.blendARGB(oldClipColor, newClipColor,
- it.animatedFraction)
- val updatedBaseColor = ColorUtils.blendARGB(oldBaseColor, newBaseColor,
- it.animatedFraction)
- val updatedLayoutAlpha = MathUtils.lerp(oldAlpha, 1f, it.animatedFraction)
- applyBackgroundChange(
+ stateAnimator =
+ ValueAnimator.ofInt(clipLayer.alpha, newAlpha).apply {
+ addUpdateListener {
+ val updatedAlpha = it.animatedValue as Int
+ val updatedClipColor =
+ ColorUtils.blendARGB(oldClipColor, newClipColor, it.animatedFraction)
+ val updatedBaseColor =
+ ColorUtils.blendARGB(oldBaseColor, newBaseColor, it.animatedFraction)
+ val updatedLayoutAlpha = MathUtils.lerp(oldAlpha, 1f, it.animatedFraction)
+ applyBackgroundChange(
clipDrawable,
updatedAlpha,
updatedClipColor,
updatedBaseColor,
- updatedLayoutAlpha
+ updatedLayoutAlpha,
+ )
+ }
+ addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ stateAnimator = null
+ }
+ }
)
+ duration = STATE_ANIMATION_DURATION
+ interpolator = Interpolators.CONTROL_STATE
+ start()
}
- addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- stateAnimator = null
- }
- })
- duration = STATE_ANIMATION_DURATION
- interpolator = Interpolators.CONTROL_STATE
- start()
- }
}
/**
@@ -405,7 +426,7 @@ class ControlViewHolder(
newAlpha: Int,
@ColorInt newClipColor: Int,
@ColorInt newBaseColor: Int,
- newLayoutAlpha: Float
+ newLayoutAlpha: Float,
) {
clipDrawable.alpha = newAlpha
if (clipDrawable is GradientDrawable) {
@@ -425,38 +446,46 @@ class ControlViewHolder(
if (isLoading) {
statusRowUpdater.invoke()
- statusAnimator = ObjectAnimator.ofFloat(status, "alpha", STATUS_ALPHA_DIMMED).apply {
- repeatMode = ValueAnimator.REVERSE
- repeatCount = ValueAnimator.INFINITE
- duration = 500L
- interpolator = Interpolators.LINEAR
- startDelay = 900L
- start()
- }
+ statusAnimator =
+ ObjectAnimator.ofFloat(status, "alpha", STATUS_ALPHA_DIMMED).apply {
+ repeatMode = ValueAnimator.REVERSE
+ repeatCount = ValueAnimator.INFINITE
+ duration = 500L
+ interpolator = Interpolators.LINEAR
+ startDelay = 900L
+ start()
+ }
} else {
- val fadeOut = ObjectAnimator.ofFloat(status, "alpha", 0f).apply {
- duration = 200L
- interpolator = Interpolators.LINEAR
- addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- statusRowUpdater.invoke()
- }
- })
- }
- val fadeIn = ObjectAnimator.ofFloat(status, "alpha", STATUS_ALPHA_ENABLED).apply {
- duration = 200L
- interpolator = Interpolators.LINEAR
- }
- statusAnimator = AnimatorSet().apply {
- playSequentially(fadeOut, fadeIn)
- addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- status.alpha = STATUS_ALPHA_ENABLED
- statusAnimator = null
- }
- })
- start()
- }
+ val fadeOut =
+ ObjectAnimator.ofFloat(status, "alpha", 0f).apply {
+ duration = 200L
+ interpolator = Interpolators.LINEAR
+ addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ statusRowUpdater.invoke()
+ }
+ }
+ )
+ }
+ val fadeIn =
+ ObjectAnimator.ofFloat(status, "alpha", STATUS_ALPHA_ENABLED).apply {
+ duration = 200L
+ interpolator = Interpolators.LINEAR
+ }
+ statusAnimator =
+ AnimatorSet().apply {
+ playSequentially(fadeOut, fadeIn)
+ addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ status.alpha = STATUS_ALPHA_ENABLED
+ statusAnimator = null
+ }
+ }
+ )
+ start()
+ }
}
}
@@ -466,7 +495,7 @@ class ControlViewHolder(
text: CharSequence,
drawable: Drawable,
color: ColorStateList,
- control: Control?
+ control: Control?,
) {
setEnabled(enabled)
@@ -475,29 +504,30 @@ class ControlViewHolder(
status.setTextColor(color)
- control?.customIcon
- ?.takeIf(canUseIconPredicate)
- ?.let {
- icon.setImageIcon(it)
+ control?.customIcon?.takeIf(canUseIconPredicate)?.let { it ->
+ val loadedDrawable = safeIconLoader.load(it)
+ icon.setImageDrawable(loadedDrawable)
icon.imageTintList = it.tintList
- } ?: run {
- if (drawable is StateListDrawable) {
- // Only reset the drawable if it is a different resource, as it will interfere
- // with the image state and animation.
- if (icon.drawable == null || !(icon.drawable is StateListDrawable)) {
+ loadedDrawable
+ }
+ ?: run {
+ if (drawable is StateListDrawable) {
+ // Only reset the drawable if it is a different resource, as it will interfere
+ // with the image state and animation.
+ if (icon.drawable == null || !(icon.drawable is StateListDrawable)) {
+ icon.setImageDrawable(drawable)
+ }
+ val state = if (enabled) ATTR_ENABLED else ATTR_DISABLED
+ icon.setImageState(state, true)
+ } else {
icon.setImageDrawable(drawable)
}
- val state = if (enabled) ATTR_ENABLED else ATTR_DISABLED
- icon.setImageState(state, true)
- } else {
- icon.setImageDrawable(drawable)
- }
- // do not color app icons
- if (deviceType != DeviceTypes.TYPE_ROUTINE) {
- icon.imageTintList = color
+ // do not color app icons
+ if (deviceType != DeviceTypes.TYPE_ROUTINE) {
+ icon.imageTintList = color
+ }
}
- }
chevronIcon.imageTintList = icon.imageTintList
}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index 8831dc61e452..66bfa986901d 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -52,7 +52,6 @@ import android.widget.Space
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import com.android.systemui.Dumpable
-import com.android.systemui.res.R
import com.android.systemui.controls.ControlsMetricsLogger
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.CustomIconCache
@@ -74,11 +73,13 @@ import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.res.R
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.asIndenting
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.withIncreasedIndent
+import com.android.systemui.utils.SafeIconLoader
import com.android.wm.shell.taskview.TaskViewFactory
import dagger.Lazy
import java.io.PrintWriter
@@ -90,26 +91,29 @@ import javax.inject.Inject
private data class ControlKey(val componentName: ComponentName, val controlId: String)
@SysUISingleton
-class ControlsUiControllerImpl @Inject constructor (
- val controlsController: Lazy<ControlsController>,
- val context: Context,
- private val packageManager: PackageManager,
- @Main val uiExecutor: DelayableExecutor,
- @Background val bgExecutor: DelayableExecutor,
- val controlsListingController: Lazy<ControlsListingController>,
- private val controlActionCoordinator: ControlActionCoordinator,
- private val activityStarter: ActivityStarter,
- private val iconCache: CustomIconCache,
- private val controlsMetricsLogger: ControlsMetricsLogger,
- private val keyguardStateController: KeyguardStateController,
- private val userTracker: UserTracker,
- private val taskViewFactory: Optional<TaskViewFactory>,
- private val controlsSettingsRepository: ControlsSettingsRepository,
- private val authorizedPanelsRepository: AuthorizedPanelsRepository,
- private val selectedComponentRepository: SelectedComponentRepository,
- private val featureFlags: FeatureFlags,
- private val dialogsFactory: ControlsDialogsFactory,
- dumpManager: DumpManager
+class ControlsUiControllerImpl
+@Inject
+constructor(
+ val controlsController: Lazy<ControlsController>,
+ val context: Context,
+ private val packageManager: PackageManager,
+ @Main val uiExecutor: DelayableExecutor,
+ @Background val bgExecutor: DelayableExecutor,
+ val controlsListingController: Lazy<ControlsListingController>,
+ private val controlActionCoordinator: ControlActionCoordinator,
+ private val activityStarter: ActivityStarter,
+ private val iconCache: CustomIconCache,
+ private val controlsMetricsLogger: ControlsMetricsLogger,
+ private val keyguardStateController: KeyguardStateController,
+ private val userTracker: UserTracker,
+ private val taskViewFactory: Optional<TaskViewFactory>,
+ private val controlsSettingsRepository: ControlsSettingsRepository,
+ private val authorizedPanelsRepository: AuthorizedPanelsRepository,
+ private val selectedComponentRepository: SelectedComponentRepository,
+ private val featureFlags: FeatureFlags,
+ private val safeIconLoaderFactory: SafeIconLoader.Factory,
+ private val dialogsFactory: ControlsDialogsFactory,
+ dumpManager: DumpManager,
) : ControlsUiController, Dumpable {
companion object {
@@ -139,26 +143,26 @@ class ControlsUiControllerImpl @Inject constructor (
private var taskViewController: PanelTaskViewController? = null
private val collator = Collator.getInstance(context.resources.configuration.locales[0])
- private val localeComparator = compareBy<SelectionItem, CharSequence>(collator) {
- it.getTitle()
- }
+ private val localeComparator =
+ compareBy<SelectionItem, CharSequence>(collator) { it.getTitle() }
private var openAppIntent: Intent? = null
private var overflowMenuAdapter: BaseAdapter? = null
private var removeAppDialog: Dialog? = null
- private val onSeedingComplete = Consumer<Boolean> {
- accepted ->
+ private val onSeedingComplete =
+ Consumer<Boolean> { accepted ->
if (accepted) {
- selectedItem = controlsController.get().getFavorites().maxByOrNull {
- it.controls.size
- }?.let {
- SelectedItem.StructureItem(it)
- } ?: SelectedItem.EMPTY_SELECTION
+ selectedItem =
+ controlsController
+ .get()
+ .getFavorites()
+ .maxByOrNull { it.controls.size }
+ ?.let { SelectedItem.StructureItem(it) } ?: SelectedItem.EMPTY_SELECTION
updatePreferences(selectedItem)
}
reload(parent)
- }
+ }
private lateinit var activityContext: Context
private lateinit var listingCallback: ControlsListingController.ControlsListingCallback
@@ -176,10 +180,11 @@ class ControlsUiControllerImpl @Inject constructor (
return object : ControlsListingController.ControlsListingCallback {
override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
val authorizedPanels = authorizedPanelsRepository.getAuthorizedPanels()
- val lastItems = serviceInfos.map {
- val uid = it.serviceInfo.applicationInfo.uid
+ val lastItems =
+ serviceInfos.map {
+ val uid = it.serviceInfo.applicationInfo.uid
- SelectionItem(
+ SelectionItem(
it.loadLabel(),
"",
it.loadIcon(),
@@ -189,9 +194,9 @@ class ControlsUiControllerImpl @Inject constructor (
it.panelActivity
} else {
null
- }
- )
- }
+ },
+ )
+ }
uiExecutor.execute {
parent.removeAllViews()
if (lastItems.size > 0) {
@@ -205,8 +210,8 @@ class ControlsUiControllerImpl @Inject constructor (
override fun resolveActivity(): Class<*> {
val allStructures = controlsController.get().getFavorites()
val selected = getPreferredSelectedItem(allStructures)
- val anyPanels = controlsListingController.get().getCurrentServices()
- .any { it.panelActivity != null }
+ val anyPanels =
+ controlsListingController.get().getCurrentServices().any { it.panelActivity != null }
return if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) {
ControlsActivity::class.java
@@ -217,11 +222,7 @@ class ControlsUiControllerImpl @Inject constructor (
}
}
- override fun show(
- parent: ViewGroup,
- onDismiss: Runnable,
- activityContext: Context
- ) {
+ override fun show(parent: ViewGroup, onDismiss: Runnable, activityContext: Context) {
Log.d(ControlsUiController.TAG, "show()")
Trace.instant(Trace.TRACE_TAG_APP, "ControlsUiControllerImpl#show")
this.parent = parent
@@ -241,7 +242,7 @@ class ControlsUiControllerImpl @Inject constructor (
if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) {
listingCallback = createCallback(::showSeedingView)
} else if (
- selectedItem !is SelectedItem.PanelItem &&
+ selectedItem !is SelectedItem.PanelItem &&
!selectedItem.hasControls &&
allStructures.size <= 1
) {
@@ -250,11 +251,11 @@ class ControlsUiControllerImpl @Inject constructor (
} else {
val selected = selectedItem
if (selected is SelectedItem.StructureItem) {
- selected.structure.controls.map {
- ControlWithState(selected.structure.componentName, it, null)
- }.associateByTo(controlsById) {
- ControlKey(selected.structure.componentName, it.ci.controlId)
- }
+ selected.structure.controls
+ .map { ControlWithState(selected.structure.componentName, it, null) }
+ .associateByTo(controlsById) {
+ ControlKey(selected.structure.componentName, it.ci.controlId)
+ }
controlsController.get().subscribeToFavorites(selected.structure)
} else {
controlsController.get().bindComponentForPanel(selected.componentName)
@@ -285,18 +286,20 @@ class ControlsUiControllerImpl @Inject constructor (
val fadeAnim = ObjectAnimator.ofFloat(parent, "alpha", 1.0f, 0.0f)
fadeAnim.setInterpolator(AccelerateInterpolator(1.0f))
fadeAnim.setDuration(FADE_IN_MILLIS)
- fadeAnim.addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- controlViewsById.clear()
- controlsById.clear()
-
- show(parent, onDismiss, activityContext)
- val showAnim = ObjectAnimator.ofFloat(parent, "alpha", 0.0f, 1.0f)
- showAnim.setInterpolator(DecelerateInterpolator(1.0f))
- showAnim.setDuration(FADE_IN_MILLIS)
- showAnim.start()
+ fadeAnim.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ controlViewsById.clear()
+ controlsById.clear()
+
+ show(parent, onDismiss, activityContext)
+ val showAnim = ObjectAnimator.ofFloat(parent, "alpha", 0.0f, 1.0f)
+ showAnim.setInterpolator(DecelerateInterpolator(1.0f))
+ showAnim.setDuration(FADE_IN_MILLIS)
+ showAnim.start()
+ }
}
- })
+ )
fadeAnim.start()
}
@@ -321,39 +324,48 @@ class ControlsUiControllerImpl @Inject constructor (
}
private fun startDefaultActivity() {
- openAppIntent?.let {
- startActivity(it, animateExtra = false)
- }
+ openAppIntent?.let { startActivity(it, animateExtra = false) }
}
@VisibleForTesting
internal fun startRemovingApp(componentName: ComponentName, appName: CharSequence) {
- activityStarter.dismissKeyguardThenExecute({
- showAppRemovalDialog(componentName, appName)
- true
- }, null, true)
+ activityStarter.dismissKeyguardThenExecute(
+ {
+ showAppRemovalDialog(componentName, appName)
+ true
+ },
+ null,
+ true,
+ )
}
private fun showAppRemovalDialog(componentName: ComponentName, appName: CharSequence) {
removeAppDialog?.cancel()
- removeAppDialog = dialogsFactory.createRemoveAppDialog(context, appName) { shouldRemove ->
- if (!shouldRemove || !controlsController.get().removeFavorites(componentName)) {
- return@createRemoveAppDialog
- }
+ removeAppDialog =
+ dialogsFactory
+ .createRemoveAppDialog(context, appName) { shouldRemove ->
+ if (!shouldRemove || !controlsController.get().removeFavorites(componentName)) {
+ return@createRemoveAppDialog
+ }
- if (selectedComponentRepository.getSelectedComponent()?.componentName ==
- componentName) {
- selectedComponentRepository.removeSelectedComponent()
- }
+ if (
+ selectedComponentRepository.getSelectedComponent()?.componentName ==
+ componentName
+ ) {
+ selectedComponentRepository.removeSelectedComponent()
+ }
- val selectedItem = getPreferredSelectedItem(controlsController.get().getFavorites())
- if (selectedItem == SelectedItem.EMPTY_SELECTION) {
- // User removed the last panel. In this case we start app selection flow and don't
- // want to auto-add it again
- selectedComponentRepository.setShouldAddDefaultComponent(false)
- }
- reload(parent)
- }.apply { show() }
+ val selectedItem =
+ getPreferredSelectedItem(controlsController.get().getFavorites())
+ if (selectedItem == SelectedItem.EMPTY_SELECTION) {
+ // User removed the last panel. In this case we start app selection flow and
+ // don't
+ // want to auto-add it again
+ selectedComponentRepository.setShouldAddDefaultComponent(false)
+ }
+ reload(parent)
+ }
+ .apply { show() }
}
private fun startTargetedActivity(si: StructureInfo, klazz: Class<*>) {
@@ -366,8 +378,10 @@ class ControlsUiControllerImpl @Inject constructor (
private fun putIntentExtras(intent: Intent, si: StructureInfo) {
intent.apply {
- putExtra(ControlsFavoritingActivity.EXTRA_APP,
- controlsListingController.get().getAppLabel(si.componentName))
+ putExtra(
+ ControlsFavoritingActivity.EXTRA_APP,
+ controlsListingController.get().getAppLabel(si.componentName),
+ )
putExtra(ControlsFavoritingActivity.EXTRA_STRUCTURE, si.structure)
putExtra(Intent.EXTRA_COMPONENT_NAME, si.componentName)
}
@@ -390,7 +404,7 @@ class ControlsUiControllerImpl @Inject constructor (
} else {
activityContext.startActivity(
intent,
- ActivityOptions.makeSceneTransitionAnimation(activityContext as Activity).toBundle()
+ ActivityOptions.makeSceneTransitionAnimation(activityContext as Activity).toBundle(),
)
}
}
@@ -401,8 +415,8 @@ class ControlsUiControllerImpl @Inject constructor (
val (panels, structures) = items.partition { it.isPanel }
val panelComponents = panels.map { it.componentName }.toSet()
- val itemsByComponent = structures.associateBy { it.componentName }
- .filterNot { it.key in panelComponents }
+ val itemsByComponent =
+ structures.associateBy { it.componentName }.filterNot { it.key in panelComponents }
val panelsAndStructures = mutableListOf<SelectionItem>()
allStructures.mapNotNullTo(panelsAndStructures) {
itemsByComponent.get(it.componentName)?.copy(structure = it.structure)
@@ -413,7 +427,8 @@ class ControlsUiControllerImpl @Inject constructor (
lastSelections = panelsAndStructures
- val selectionItem = findSelectionItem(selectedItem, panelsAndStructures)
+ val selectionItem =
+ findSelectionItem(selectedItem, panelsAndStructures)
?: if (panels.isNotEmpty()) {
// If we couldn't find a good selected item, but there's at least one panel,
// show a panel.
@@ -428,8 +443,10 @@ class ControlsUiControllerImpl @Inject constructor (
if (taskViewFactory.isPresent && selectionItem.isPanel) {
createPanelView(selectionItem.panelComponentName!!)
} else if (!selectionItem.isPanel) {
- controlsMetricsLogger
- .refreshBegin(selectionItem.uid, !keyguardStateController.isUnlocked())
+ controlsMetricsLogger.refreshBegin(
+ selectionItem.uid,
+ !keyguardStateController.isUnlocked(),
+ )
createListView(selectionItem)
} else {
Log.w(ControlsUiController.TAG, "Not TaskViewFactory to display panel $selectionItem")
@@ -437,176 +454,200 @@ class ControlsUiControllerImpl @Inject constructor (
this.selectionItem = selectionItem
bgExecutor.execute {
- val intent = Intent(Intent.ACTION_MAIN)
+ val intent =
+ Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER)
.setPackage(selectionItem.componentName.packageName)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
- Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
- val intents = packageManager
- .queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0L))
- intents.firstOrNull { it.activityInfo.exported }?.let { resolved ->
- intent.setPackage(null)
- intent.setComponent(resolved.activityInfo.componentName)
- openAppIntent = intent
- parent.post {
- // This will call show on the PopupWindow in the same thread, so make sure this
- // happens in the view thread.
- overflowMenuAdapter?.notifyDataSetChanged()
+ .addFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+ )
+ val intents =
+ packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0L))
+ intents
+ .firstOrNull { it.activityInfo.exported }
+ ?.let { resolved ->
+ intent.setPackage(null)
+ intent.setComponent(resolved.activityInfo.componentName)
+ openAppIntent = intent
+ parent.post {
+ // This will call show on the PopupWindow in the same thread, so make sure
+ // this
+ // happens in the view thread.
+ overflowMenuAdapter?.notifyDataSetChanged()
+ }
}
- }
}
createDropDown(panelsAndStructures, selectionItem)
val currentApps = panelsAndStructures.map { it.componentName }.toSet()
- val allApps = controlsListingController.get()
- .getCurrentServices().map { it.componentName }.toSet()
- createMenu(
- selectionItem = selectionItem,
- extraApps = (allApps - currentApps).isNotEmpty(),
- )
+ val allApps =
+ controlsListingController.get().getCurrentServices().map { it.componentName }.toSet()
+ createMenu(selectionItem = selectionItem, extraApps = (allApps - currentApps).isNotEmpty())
}
private fun createPanelView(componentName: ComponentName) {
- val setting = controlsSettingsRepository
- .allowActionOnTrivialControlsInLockscreen.value
- val pendingIntent = PendingIntent.getActivityAsUser(
+ val setting = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value
+ val pendingIntent =
+ PendingIntent.getActivityAsUser(
context,
0,
Intent().apply {
component = componentName
putExtra(
ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS,
- setting
+ setting,
)
if (homePanelDream()) {
- putExtra(ControlsProviderService.EXTRA_CONTROLS_SURFACE,
- ControlsProviderService.CONTROLS_SURFACE_ACTIVITY_PANEL)
+ putExtra(
+ ControlsProviderService.EXTRA_CONTROLS_SURFACE,
+ ControlsProviderService.CONTROLS_SURFACE_ACTIVITY_PANEL,
+ )
}
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
ActivityOptions.makeBasic()
.setPendingIntentCreatorBackgroundActivityStartMode(
- ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle(),
- userTracker.userHandle
- )
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+ )
+ .toBundle(),
+ userTracker.userHandle,
+ )
parent.requireViewById<View>(R.id.controls_scroll_view).visibility = View.GONE
val container = parent.requireViewById<FrameLayout>(R.id.controls_panel)
container.visibility = View.VISIBLE
container.post {
taskViewFactory.get().create(activityContext, uiExecutor) { taskView ->
- taskViewController = PanelTaskViewController(
- activityContext,
- uiExecutor,
- pendingIntent,
- taskView,
- onDismiss::run
- ).also {
- container.addView(taskView)
- it.launchTaskView()
- }
+ taskViewController =
+ PanelTaskViewController(
+ activityContext,
+ uiExecutor,
+ pendingIntent,
+ taskView,
+ onDismiss::run,
+ )
+ .also {
+ container.addView(taskView)
+ it.launchTaskView()
+ }
}
}
}
private fun createMenu(selectionItem: SelectionItem, extraApps: Boolean) {
val isPanel = selectedItem is SelectedItem.PanelItem
- val selectedStructure = (selectedItem as? SelectedItem.StructureItem)?.structure
- ?: EMPTY_STRUCTURE
+ val selectedStructure =
+ (selectedItem as? SelectedItem.StructureItem)?.structure ?: EMPTY_STRUCTURE
val items = buildList {
- add(OverflowMenuAdapter.MenuItem(
+ add(
+ OverflowMenuAdapter.MenuItem(
context.getText(R.string.controls_open_app),
- OPEN_APP_ID
- ))
+ OPEN_APP_ID,
+ )
+ )
if (extraApps) {
- add(OverflowMenuAdapter.MenuItem(
+ add(
+ OverflowMenuAdapter.MenuItem(
context.getText(R.string.controls_menu_add_another_app),
- ADD_APP_ID
- ))
+ ADD_APP_ID,
+ )
+ )
}
- add(OverflowMenuAdapter.MenuItem(
+ add(
+ OverflowMenuAdapter.MenuItem(
context.getText(R.string.controls_menu_remove),
REMOVE_APP_ID,
- ))
+ )
+ )
if (!isPanel) {
- add(OverflowMenuAdapter.MenuItem(
+ add(
+ OverflowMenuAdapter.MenuItem(
context.getText(R.string.controls_menu_edit),
- EDIT_CONTROLS_ID
- ))
+ EDIT_CONTROLS_ID,
+ )
+ )
}
}
- val adapter = OverflowMenuAdapter(context, R.layout.controls_more_item, items) { position ->
+ val adapter =
+ OverflowMenuAdapter(context, R.layout.controls_more_item, items) { position ->
getItemId(position) != OPEN_APP_ID || openAppIntent != null
- }
+ }
val anchor = parent.requireViewById<ImageView>(R.id.controls_more)
- anchor.setOnClickListener(object : View.OnClickListener {
- override fun onClick(v: View) {
- popup = ControlsPopupMenu(popupThemedContext).apply {
- width = ViewGroup.LayoutParams.WRAP_CONTENT
- anchorView = anchor
- setDropDownGravity(Gravity.END)
- setAdapter(adapter)
-
- setOnItemClickListener(object : AdapterView.OnItemClickListener {
- override fun onItemClick(
- parent: AdapterView<*>,
- view: View,
- pos: Int,
- id: Long
- ) {
- when (id) {
- OPEN_APP_ID -> startDefaultActivity()
- ADD_APP_ID -> startProviderSelectorActivity()
- ADD_CONTROLS_ID -> startFavoritingActivity(selectedStructure)
- EDIT_CONTROLS_ID -> startEditingActivity(selectedStructure)
- REMOVE_APP_ID -> startRemovingApp(
- selectionItem.componentName, selectionItem.appName
- )
- }
- dismiss()
+ anchor.setOnClickListener(
+ object : View.OnClickListener {
+ override fun onClick(v: View) {
+ popup =
+ ControlsPopupMenu(popupThemedContext).apply {
+ width = ViewGroup.LayoutParams.WRAP_CONTENT
+ anchorView = anchor
+ setDropDownGravity(Gravity.END)
+ setAdapter(adapter)
+
+ setOnItemClickListener(
+ object : AdapterView.OnItemClickListener {
+ override fun onItemClick(
+ parent: AdapterView<*>,
+ view: View,
+ pos: Int,
+ id: Long,
+ ) {
+ when (id) {
+ OPEN_APP_ID -> startDefaultActivity()
+ ADD_APP_ID -> startProviderSelectorActivity()
+ ADD_CONTROLS_ID ->
+ startFavoritingActivity(selectedStructure)
+ EDIT_CONTROLS_ID ->
+ startEditingActivity(selectedStructure)
+ REMOVE_APP_ID ->
+ startRemovingApp(
+ selectionItem.componentName,
+ selectionItem.appName,
+ )
+ }
+ dismiss()
+ }
+ }
+ )
+ show()
+ listView?.post { listView?.requestAccessibilityFocus() }
}
- })
- show()
- listView?.post { listView?.requestAccessibilityFocus() }
}
}
- })
+ )
overflowMenuAdapter = adapter
}
private fun createDropDown(items: List<SelectionItem>, selected: SelectionItem) {
- items.forEach {
- RenderInfo.registerComponentIcon(it.componentName, it.icon)
- }
+ items.forEach { RenderInfo.registerComponentIcon(it.componentName, it.icon) }
- val adapter = ItemAdapter(context, R.layout.controls_spinner_item).apply {
- add(selected)
- addAll(items
- .filter { it !== selected }
- .sortedBy { it.appName.toString() }
- )
- }
+ val adapter =
+ ItemAdapter(context, R.layout.controls_spinner_item).apply {
+ add(selected)
+ addAll(items.filter { it !== selected }.sortedBy { it.appName.toString() })
+ }
- val iconSize = context.resources
- .getDimensionPixelSize(R.dimen.controls_header_app_icon_size)
+ val iconSize =
+ context.resources.getDimensionPixelSize(R.dimen.controls_header_app_icon_size)
/*
* Default spinner widget does not work with the window type required
* for this dialog. Use a textView with the ListPopupWindow to achieve
* a similar effect
*/
- val spinner = parent.requireViewById<TextView>(R.id.app_or_structure_spinner).apply {
- setText(selected.getTitle())
- // override the default color on the dropdown drawable
- (getBackground() as LayerDrawable).getDrawable(0)
- .setTint(context.resources.getColor(R.color.control_spinner_dropdown, null))
- selected.icon.setBounds(0, 0, iconSize, iconSize)
- compoundDrawablePadding = (iconSize / 2.4f).toInt()
- setCompoundDrawablesRelative(selected.icon, null, null, null)
- }
+ val spinner =
+ parent.requireViewById<TextView>(R.id.app_or_structure_spinner).apply {
+ setText(selected.getTitle())
+ // override the default color on the dropdown drawable
+ (getBackground() as LayerDrawable)
+ .getDrawable(0)
+ .setTint(context.resources.getColor(R.color.control_spinner_dropdown, null))
+ selected.icon.setBounds(0, 0, iconSize, iconSize)
+ compoundDrawablePadding = (iconSize / 2.4f).toInt()
+ setCompoundDrawablesRelative(selected.icon, null, null, null)
+ }
val anchor = parent.requireViewById<View>(R.id.app_or_structure_spinner)
if (items.size == 1) {
@@ -615,34 +656,40 @@ class ControlsUiControllerImpl @Inject constructor (
anchor.isClickable = false
return
} else {
- spinner.background = parent.context.resources
- .getDrawable(R.drawable.control_spinner_background)
- }
-
- anchor.setOnClickListener(object : View.OnClickListener {
- override fun onClick(v: View) {
- popup = ControlsPopupMenu(popupThemedContext).apply {
- anchorView = anchor
- width = ViewGroup.LayoutParams.MATCH_PARENT
- setAdapter(adapter)
-
- setOnItemClickListener(object : AdapterView.OnItemClickListener {
- override fun onItemClick(
- parent: AdapterView<*>,
- view: View,
- pos: Int,
- id: Long
- ) {
- val listItem = parent.getItemAtPosition(pos) as SelectionItem
- this@ControlsUiControllerImpl.switchAppOrStructure(listItem)
- dismiss()
+ spinner.background =
+ parent.context.resources.getDrawable(R.drawable.control_spinner_background)
+ }
+
+ anchor.setOnClickListener(
+ object : View.OnClickListener {
+ override fun onClick(v: View) {
+ popup =
+ ControlsPopupMenu(popupThemedContext).apply {
+ anchorView = anchor
+ width = ViewGroup.LayoutParams.MATCH_PARENT
+ setAdapter(adapter)
+
+ setOnItemClickListener(
+ object : AdapterView.OnItemClickListener {
+ override fun onItemClick(
+ parent: AdapterView<*>,
+ view: View,
+ pos: Int,
+ id: Long,
+ ) {
+ val listItem =
+ parent.getItemAtPosition(pos) as SelectionItem
+ this@ControlsUiControllerImpl.switchAppOrStructure(listItem)
+ dismiss()
+ }
+ }
+ )
+ show()
+ listView?.post { listView?.requestAccessibilityFocus() }
}
- })
- show()
- listView?.post { listView?.requestAccessibilityFocus() }
}
}
- })
+ )
}
private fun createControlsSpaceFrame() {
@@ -658,6 +705,12 @@ class ControlsUiControllerImpl @Inject constructor (
private fun createListView(selected: SelectionItem) {
if (selectedItem !is SelectedItem.StructureItem) return
val selectedStructure = (selectedItem as SelectedItem.StructureItem).structure
+ val safeIconLoader =
+ safeIconLoaderFactory.create(
+ selected.uid,
+ selected.componentName.packageName,
+ controlsController.get().currentUserId,
+ )
val inflater = LayoutInflater.from(activityContext)
val maxColumns = ControlAdapter.findMaxColumns(activityContext.resources)
@@ -671,8 +724,8 @@ class ControlsUiControllerImpl @Inject constructor (
if (lastRow.getChildCount() == maxColumns) {
lastRow = createRow(inflater, listView)
}
- val baseLayout = inflater.inflate(
- R.layout.controls_base_item, lastRow, false) as ViewGroup
+ val baseLayout =
+ inflater.inflate(R.layout.controls_base_item, lastRow, false) as ViewGroup
lastRow.addView(baseLayout)
// Use ConstraintLayout in the future... for now, manually adjust margins
@@ -680,16 +733,18 @@ class ControlsUiControllerImpl @Inject constructor (
val lp = baseLayout.getLayoutParams() as ViewGroup.MarginLayoutParams
lp.setMarginStart(0)
}
- val cvh = ControlViewHolder(
- baseLayout,
- controlsController.get(),
- uiExecutor,
- bgExecutor,
- controlActionCoordinator,
- controlsMetricsLogger,
- selected.uid,
- controlsController.get().currentUserId,
- )
+ val cvh =
+ ControlViewHolder(
+ baseLayout,
+ controlsController.get(),
+ uiExecutor,
+ bgExecutor,
+ controlActionCoordinator,
+ controlsMetricsLogger,
+ selected.uid,
+ controlsController.get().currentUserId,
+ safeIconLoader,
+ )
cvh.bindData(it, false /* isLocked, will be ignored on initial load */)
controlViewsById.put(key, cvh)
}
@@ -700,9 +755,7 @@ class ControlsUiControllerImpl @Inject constructor (
var spacersToAdd = if (mod == 0) 0 else maxColumns - mod
val margin = context.resources.getDimensionPixelSize(R.dimen.control_spacing)
while (spacersToAdd > 0) {
- val lp = LinearLayout.LayoutParams(0, 0, 1f).apply {
- setMarginStart(margin)
- }
+ val lp = LinearLayout.LayoutParams(0, 0, 1f).apply { setMarginStart(margin) }
lastRow.addView(Space(context), lp)
spacersToAdd--
}
@@ -715,27 +768,32 @@ class ControlsUiControllerImpl @Inject constructor (
SelectedItem.PanelItem(preferredPanel.name, component)
} else {
if (structures.isEmpty()) return SelectedItem.EMPTY_SELECTION
- SelectedItem.StructureItem(structures.firstOrNull {
- component == it.componentName && preferredPanel?.name == it.structure
- } ?: structures[0])
+ SelectedItem.StructureItem(
+ structures.firstOrNull {
+ component == it.componentName && preferredPanel?.name == it.structure
+ } ?: structures[0]
+ )
}
}
private fun updatePreferences(selectedItem: SelectedItem) {
selectedComponentRepository.setSelectedComponent(
- SelectedComponentRepository.SelectedComponent(selectedItem)
+ SelectedComponentRepository.SelectedComponent(selectedItem)
)
}
private fun maybeUpdateSelectedItem(item: SelectionItem): Boolean {
- val newSelection = if (item.isPanel) {
- SelectedItem.PanelItem(item.appName, item.componentName)
- } else {
- SelectedItem.StructureItem(allStructures.firstOrNull {
- it.structure == item.structure && it.componentName == item.componentName
- } ?: EMPTY_STRUCTURE)
- }
- return if (newSelection != selectedItem ) {
+ val newSelection =
+ if (item.isPanel) {
+ SelectedItem.PanelItem(item.appName, item.componentName)
+ } else {
+ SelectedItem.StructureItem(
+ allStructures.firstOrNull {
+ it.structure == item.structure && it.componentName == item.componentName
+ } ?: EMPTY_STRUCTURE
+ )
+ }
+ return if (newSelection != selectedItem) {
selectedItem = newSelection
updatePreferences(selectedItem)
true
@@ -758,9 +816,7 @@ class ControlsUiControllerImpl @Inject constructor (
}
popup = null
- controlViewsById.forEach {
- it.value.dismiss()
- }
+ controlViewsById.forEach { it.value.dismiss() }
controlActionCoordinator.closeDialogs()
removeAppDialog?.cancel()
}
@@ -798,18 +854,14 @@ class ControlsUiControllerImpl @Inject constructor (
val key = ControlKey(componentName, c.getControlId())
controlsById.put(key, cws)
- controlViewsById.get(key)?.let {
- uiExecutor.execute { it.bindData(cws, isLocked) }
- }
+ controlViewsById.get(key)?.let { uiExecutor.execute { it.bindData(cws, isLocked) } }
}
}
}
override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) {
val key = ControlKey(componentName, controlId)
- uiExecutor.execute {
- controlViewsById.get(key)?.actionResponse(response)
- }
+ uiExecutor.execute { controlViewsById.get(key)?.actionResponse(response) }
}
override fun onSizeChange() {
@@ -830,16 +882,19 @@ class ControlsUiControllerImpl @Inject constructor (
private fun findSelectionItem(si: SelectedItem, items: List<SelectionItem>): SelectionItem? =
items.firstOrNull { it.matches(si) }
- override fun dump(pw: PrintWriter, args: Array<out String>) = pw.asIndenting().run {
- println("ControlsUiControllerImpl:")
- withIncreasedIndent {
- println("hidden: $hidden")
- println("selectedItem: $selectedItem")
- println("lastSelections: $lastSelections")
- println("setting: ${controlsSettingsRepository
- .allowActionOnTrivialControlsInLockscreen.value}")
+ override fun dump(pw: PrintWriter, args: Array<out String>) =
+ pw.asIndenting().run {
+ println("ControlsUiControllerImpl:")
+ withIncreasedIndent {
+ println("hidden: $hidden")
+ println("selectedItem: $selectedItem")
+ println("lastSelections: $lastSelections")
+ println(
+ "setting: ${controlsSettingsRepository
+ .allowActionOnTrivialControlsInLockscreen.value}"
+ )
+ }
}
- }
}
@VisibleForTesting
@@ -849,9 +904,14 @@ internal data class SelectionItem(
val icon: Drawable,
val componentName: ComponentName,
val uid: Int,
- val panelComponentName: ComponentName?
+ val panelComponentName: ComponentName?,
) {
- fun getTitle() = if (structure.isEmpty()) { appName } else { structure }
+ fun getTitle() =
+ if (structure.isEmpty()) {
+ appName
+ } else {
+ structure
+ }
val isPanel: Boolean = panelComponentName != null
@@ -872,7 +932,7 @@ internal data class SelectionItem(
}
private class ItemAdapter(parentContext: Context, val resource: Int) :
- ArrayAdapter<SelectionItem>(parentContext, resource) {
+ ArrayAdapter<SelectionItem>(parentContext, resource) {
private val layoutInflater = LayoutInflater.from(context)!!
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ThumbnailBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ThumbnailBehavior.kt
index 41907882a443..e640be94073e 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ThumbnailBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ThumbnailBehavior.kt
@@ -20,21 +20,21 @@ import android.graphics.BlendMode
import android.graphics.BlendModeColorFilter
import android.graphics.drawable.ClipDrawable
import android.graphics.drawable.LayerDrawable
-import android.view.View
import android.service.controls.Control
import android.service.controls.templates.TemperatureControlTemplate
import android.service.controls.templates.ThumbnailTemplate
import android.util.TypedValue
-
-import com.android.systemui.res.R
+import android.view.View
import com.android.systemui.controls.ui.ControlViewHolder.Companion.MAX_LEVEL
import com.android.systemui.controls.ui.ControlViewHolder.Companion.MIN_LEVEL
+import com.android.systemui.res.R
+import com.android.systemui.utils.SafeIconLoader
/**
* Supports display of static images on the background of the tile. When marked active, the title
* and subtitle will not be visible. To be used with {@link Thumbnailtemplate} only.
*/
-class ThumbnailBehavior(currentUserId: Int) : Behavior {
+class ThumbnailBehavior(currentUserId: Int, private val safeIconLoader: SafeIconLoader) : Behavior {
lateinit var template: ThumbnailTemplate
lateinit var control: Control
lateinit var cvh: ControlViewHolder
@@ -61,17 +61,20 @@ class ThumbnailBehavior(currentUserId: Int) : Behavior {
shadowRadius = outValue.getFloat()
shadowColor = cvh.context.resources.getColor(R.color.control_thumbnail_shadow_color)
- cvh.layout.setOnClickListener(View.OnClickListener() {
- cvh.controlActionCoordinator.touch(cvh, template.getTemplateId(), control)
- })
+ cvh.layout.setOnClickListener(
+ View.OnClickListener() {
+ cvh.controlActionCoordinator.touch(cvh, template.getTemplateId(), control)
+ }
+ )
}
override fun bind(cws: ControlWithState, colorOffset: Int) {
this.control = cws.control!!
cvh.setStatusText(control.getStatusText())
- template = control.controlTemplate as? ThumbnailTemplate
+ template =
+ control.controlTemplate as? ThumbnailTemplate
?: (control.controlTemplate as TemperatureControlTemplate).template
- as ThumbnailTemplate
+ as ThumbnailTemplate
val ld = cvh.layout.getBackground() as LayerDrawable
val clipLayer = ld.findDrawableByLayerId(R.id.clip_layer) as ClipDrawable
@@ -84,18 +87,22 @@ class ThumbnailBehavior(currentUserId: Int) : Behavior {
cvh.status.setShadowLayer(shadowOffsetX, shadowOffsetY, shadowRadius, shadowColor)
cvh.bgExecutor.execute {
- val drawable = template.thumbnail
- ?.takeIf(canUseIconPredicate)
- ?.loadDrawable(cvh.context)
+ val drawable =
+ template.thumbnail.takeIf(canUseIconPredicate)?.let { safeIconLoader.load(it) }
cvh.uiExecutor.execute {
- val radius = cvh.context.getResources()
- .getDimensionPixelSize(R.dimen.control_corner_radius).toFloat()
+ val radius =
+ cvh.context
+ .getResources()
+ .getDimensionPixelSize(R.dimen.control_corner_radius)
+ .toFloat()
// TODO(b/290037843): Add a placeholder
- drawable?.let {
- clipLayer.drawable = CornerDrawable(it, radius)
- }
- clipLayer.setColorFilter(BlendModeColorFilter(cvh.context.resources
- .getColor(R.color.control_thumbnail_tint), BlendMode.LUMINOSITY))
+ drawable?.let { clipLayer.drawable = CornerDrawable(it, radius) }
+ clipLayer.setColorFilter(
+ BlendModeColorFilter(
+ cvh.context.resources.getColor(R.color.control_thumbnail_tint),
+ BlendMode.LUMINOSITY,
+ )
+ )
cvh.applyRenderInfo(enabled, colorOffset)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/utils/SafeIconLoader.kt b/packages/SystemUI/src/com/android/systemui/utils/SafeIconLoader.kt
new file mode 100644
index 000000000000..41bef7f94fa2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/utils/SafeIconLoader.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 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.utils
+
+import android.app.IUriGrantsManager
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.Icon
+import android.os.UserHandle
+import com.android.systemui.dagger.qualifiers.Application
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/**
+ * Use to load an icon (from another app) safely. It will prevent cross user icon loading if there
+ * are no permissions.
+ */
+class SafeIconLoader
+@AssistedInject
+constructor(
+ @Assisted("serviceUid") private val serviceUid: Int,
+ @Assisted private val packageName: String,
+ @Assisted("userId") private val userId: Int,
+ @Application private val applicationContext: Context,
+ private val iUriGrantsManager: IUriGrantsManager,
+) {
+
+ private val serviceContext =
+ applicationContext.createPackageContextAsUser(packageName, 0, UserHandle.of(userId))
+
+ /**
+ * Tries to load the icon. If it fails in any way (for example, cross user permissions), it will
+ * return `null`. Prefer calling this in a background thread.
+ */
+ fun load(icon: Icon): Drawable? {
+ return icon.loadDrawableCheckingUriGrant(
+ serviceContext,
+ iUriGrantsManager,
+ serviceUid,
+ packageName,
+ )
+ }
+
+ @AssistedFactory
+ interface Factory {
+
+ fun create(
+ @Assisted("serviceUid") serviceUid: Int,
+ packageName: String,
+ @Assisted("userId") userId: Int,
+ ): SafeIconLoader
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsEditingActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsEditingActivityTest.kt
index 39e1e1d8bb57..b1d0f86c364c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsEditingActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsEditingActivityTest.kt
@@ -2,6 +2,9 @@ package com.android.systemui.controls.management
import android.content.ComponentName
import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.ServiceInfo
+import android.graphics.drawable.Drawable
import android.os.Bundle
import android.testing.TestableLooper
import android.view.View
@@ -11,25 +14,31 @@ import android.window.OnBackInvokedDispatcher
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.rule.ActivityTestRule
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.activity.SingleActivityFactory
+import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.CustomIconCache
import com.android.systemui.controls.controller.ControlsControllerImpl
+import com.android.systemui.res.R
import com.android.systemui.settings.UserTracker
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.utils.SafeIconLoader
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Answers
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.eq
import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@SmallTest
@@ -41,6 +50,7 @@ class ControlsEditingActivityTest : SysuiTestCase() {
val TEST_COMPONENT = ComponentName("TestPackageName", "TestClassName")
val TEST_STRUCTURE: CharSequence = "TestStructure"
val TEST_APP: CharSequence = "TestApp"
+ val TEST_UID = 12345
}
private val uiExecutor = FakeExecutor(FakeSystemClock())
@@ -51,6 +61,10 @@ class ControlsEditingActivityTest : SysuiTestCase() {
@Mock lateinit var customIconCache: CustomIconCache
+ @Mock lateinit var controlsListingController: ControlsListingController
+
+ @Mock(answer = Answers.RETURNS_MOCKS) lateinit var safeIconLoaderFactory: SafeIconLoader.Factory
+
private var latch: CountDownLatch = CountDownLatch(1)
@Mock private lateinit var mockDispatcher: OnBackInvokedDispatcher
@@ -66,8 +80,10 @@ class ControlsEditingActivityTest : SysuiTestCase() {
controller,
userTracker,
customIconCache,
+ controlsListingController,
+ safeIconLoaderFactory,
mockDispatcher,
- latch
+ latch,
)
},
/* initialTouchMode= */ false,
@@ -77,6 +93,9 @@ class ControlsEditingActivityTest : SysuiTestCase() {
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+
+ val serviceInfo = ControlsServiceInfo(TEST_COMPONENT, "", TEST_UID)
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listOf(serviceInfo))
}
@Test
@@ -86,7 +105,7 @@ class ControlsEditingActivityTest : SysuiTestCase() {
verify(mockDispatcher)
.registerOnBackInvokedCallback(
eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT),
- captureCallback.capture()
+ captureCallback.capture(),
)
activityRule.finishActivity()
latch.await() // ensure activity is finished
@@ -158,19 +177,40 @@ class ControlsEditingActivityTest : SysuiTestCase() {
}
)
+ private fun ControlsServiceInfo(
+ componentName: ComponentName,
+ label: CharSequence,
+ uid: Int,
+ ): ControlsServiceInfo {
+ val serviceInfo =
+ ServiceInfo().apply {
+ applicationInfo = ApplicationInfo().apply { this.uid = uid }
+ packageName = componentName.packageName
+ name = componentName.className
+ }
+ return Mockito.spy(ControlsServiceInfo(mContext, serviceInfo)).apply {
+ Mockito.doReturn(label).`when`(this).loadLabel()
+ Mockito.doReturn(mock(Drawable::class.java)).`when`(this).loadIcon()
+ }
+ }
+
class TestableControlsEditingActivity(
executor: FakeExecutor,
controller: ControlsControllerImpl,
userTracker: UserTracker,
customIconCache: CustomIconCache,
+ controlsListingController: ControlsListingController,
+ safeIconLoaderFactory: SafeIconLoader.Factory,
private val mockDispatcher: OnBackInvokedDispatcher,
- private val latch: CountDownLatch
+ private val latch: CountDownLatch,
) :
ControlsEditingActivity(
executor,
controller,
userTracker,
customIconCache,
+ controlsListingController,
+ safeIconLoaderFactory,
) {
var startActivityData: StartActivityData? = null
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt
index 7fb74b3439bc..5eb93721e735 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt
@@ -2,6 +2,9 @@ package com.android.systemui.controls.management
import android.content.ComponentName
import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.ServiceInfo
+import android.graphics.drawable.Drawable
import android.os.Bundle
import android.service.controls.Control
import android.testing.TestableLooper
@@ -13,19 +16,21 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.FlakyTest
import androidx.test.filters.SmallTest
import androidx.test.rule.ActivityTestRule
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.activity.SingleActivityFactory
import com.android.systemui.controls.ControlStatus
+import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.controller.ControlsControllerImpl
import com.android.systemui.controls.controller.createLoadDataObject
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.res.R
import com.android.systemui.settings.UserTracker
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.utils.SafeIconLoader
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors
import java.util.concurrent.CountDownLatch
@@ -39,6 +44,7 @@ import org.mockito.Answers
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito
import org.mockito.Mockito.eq
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
@@ -57,6 +63,7 @@ class ControlsFavoritingActivityTest : SysuiTestCase() {
whenever(structure).thenReturn(TEST_STRUCTURE)
}
val TEST_APP: CharSequence = "TestApp"
+ val TEST_UID = 12345
private fun View.waitForPost() {
val latch = CountDownLatch(1)
@@ -72,6 +79,10 @@ class ControlsFavoritingActivityTest : SysuiTestCase() {
@Mock lateinit var userTracker: UserTracker
+ @Mock lateinit var controlsListingController: ControlsListingController
+
+ @Mock(answer = Answers.RETURNS_MOCKS) lateinit var safeIconLoaderFactory: SafeIconLoader.Factory
+
private var latch: CountDownLatch = CountDownLatch(1)
@Mock private lateinit var mockDispatcher: OnBackInvokedDispatcher
@@ -88,8 +99,10 @@ class ControlsFavoritingActivityTest : SysuiTestCase() {
executor,
controller,
userTracker,
+ controlsListingController,
+ safeIconLoaderFactory,
mockDispatcher,
- latch
+ latch,
)
},
/* initialTouchMode= */ false,
@@ -99,6 +112,10 @@ class ControlsFavoritingActivityTest : SysuiTestCase() {
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+
+ val serviceInfo = ControlsServiceInfo(TEST_COMPONENT, "", TEST_UID)
+ Mockito.`when`(controlsListingController.getCurrentServices())
+ .thenReturn(listOf(serviceInfo))
}
// b/259549854 to root-cause and fix
@@ -110,7 +127,7 @@ class ControlsFavoritingActivityTest : SysuiTestCase() {
verify(mockDispatcher)
.registerOnBackInvokedCallback(
eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT),
- captureCallback.capture()
+ captureCallback.capture(),
)
activityRule.finishActivity()
latch.await() // ensure activity is finished
@@ -178,17 +195,38 @@ class ControlsFavoritingActivityTest : SysuiTestCase() {
}
)
+ private fun ControlsServiceInfo(
+ componentName: ComponentName,
+ label: CharSequence,
+ uid: Int,
+ ): ControlsServiceInfo {
+ val serviceInfo =
+ ServiceInfo().apply {
+ applicationInfo = ApplicationInfo().apply { this.uid = uid }
+ packageName = componentName.packageName
+ name = componentName.className
+ }
+ return Mockito.spy(ControlsServiceInfo(mContext, serviceInfo)).apply {
+ Mockito.doReturn(label).`when`(this).loadLabel()
+ Mockito.doReturn(mock(Drawable::class.java)).`when`(this).loadIcon()
+ }
+ }
+
class TestableControlsFavoritingActivity(
executor: Executor,
controller: ControlsControllerImpl,
userTracker: UserTracker,
+ controlsListingController: ControlsListingController,
+ safeIconLoaderFactory: SafeIconLoader.Factory,
private val mockDispatcher: OnBackInvokedDispatcher,
- private val latch: CountDownLatch
+ private val latch: CountDownLatch,
) :
ControlsFavoritingActivity(
executor,
controller,
userTracker,
+ safeIconLoaderFactory,
+ controlsListingController,
) {
var triedToFinish = false
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlViewHolderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlViewHolderTest.kt
index 4b30fa5dd161..c1c90bd41fea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlViewHolderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlViewHolderTest.kt
@@ -21,6 +21,7 @@ import android.content.ComponentName
import android.content.res.ColorStateList
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.Icon
+import android.graphics.drawable.ShapeDrawable
import android.service.controls.Control
import android.service.controls.DeviceTypes
import android.service.controls.templates.ControlTemplate
@@ -30,18 +31,21 @@ import android.view.View
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.mockito.any
import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.utils.SafeIconLoader
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
+import org.mockito.Mockito.`when`
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -52,14 +56,20 @@ class ControlViewHolderTest : SysuiTestCase() {
private lateinit var cvh: ControlViewHolder
private lateinit var baseLayout: ViewGroup
+ private lateinit var safeIconLoader: SafeIconLoader
@Before
fun setUp() {
TestableLooper.get(this).runWithLooper {
- baseLayout = LayoutInflater.from(mContext).inflate(
- R.layout.controls_base_item, null, false) as ViewGroup
+ baseLayout =
+ LayoutInflater.from(mContext).inflate(R.layout.controls_base_item, null, false)
+ as ViewGroup
- cvh = ControlViewHolder(
+ safeIconLoader = mock(SafeIconLoader::class.java)
+ `when`(safeIconLoader.load(any())).thenReturn(PLAIN_DRAWABLE)
+
+ cvh =
+ ControlViewHolder(
baseLayout,
mock(ControlsController::class.java),
FakeExecutor(clock),
@@ -68,15 +78,20 @@ class ControlViewHolderTest : SysuiTestCase() {
mock(ControlsMetricsLogger::class.java),
uid = 100,
0,
- )
+ safeIconLoader,
+ )
- val cws = ControlWithState(
+ val cws =
+ ControlWithState(
ComponentName.createRelative("pkg", "cls"),
ControlInfo(
- CONTROL_ID, CONTROL_TITLE, "subtitle", DeviceTypes.TYPE_AIR_FRESHENER
+ CONTROL_ID,
+ CONTROL_TITLE,
+ "subtitle",
+ DeviceTypes.TYPE_AIR_FRESHENER,
),
- Control.StatelessBuilder(CONTROL_ID, mock(PendingIntent::class.java)).build()
- )
+ Control.StatelessBuilder(CONTROL_ID, mock(PendingIntent::class.java)).build(),
+ )
cvh.bindData(cws, isLocked = false)
}
@@ -84,10 +99,11 @@ class ControlViewHolderTest : SysuiTestCase() {
@Test
fun updateStatusRow_customIconWithTint_iconTintRemains() {
- val control = Control.StatelessBuilder(DEFAULT_CONTROL)
+ val control =
+ Control.StatelessBuilder(DEFAULT_CONTROL)
.setCustomIcon(
- Icon.createWithResource(mContext.resources, R.drawable.ic_emergency_star)
- .setTint(TINT_COLOR)
+ Icon.createWithResource(mContext.resources, R.drawable.ic_emergency_star)
+ .setTint(TINT_COLOR)
)
.build()
@@ -99,10 +115,11 @@ class ControlViewHolderTest : SysuiTestCase() {
@Test
fun updateStatusRow_customIconWithTintList_iconTintListRemains() {
val customIconTintList = ColorStateList.valueOf(TINT_COLOR)
- val control = Control.StatelessBuilder(CONTROL_ID, mock(PendingIntent::class.java))
+ val control =
+ Control.StatelessBuilder(CONTROL_ID, mock(PendingIntent::class.java))
.setCustomIcon(
- Icon.createWithResource(mContext.resources, R.drawable.ic_emergency_star)
- .setTintList(customIconTintList)
+ Icon.createWithResource(mContext.resources, R.drawable.ic_emergency_star)
+ .setTintList(customIconTintList)
)
.build()
@@ -113,22 +130,54 @@ class ControlViewHolderTest : SysuiTestCase() {
@Test
fun chevronIcon() {
- val control = Control.StatefulBuilder(CONTROL_ID, mock(PendingIntent::class.java))
- .setStatus(Control.STATUS_OK)
- .setControlTemplate(ControlTemplate.NO_TEMPLATE)
- .build()
- val cws = ControlWithState(
- ComponentName.createRelative("pkg", "cls"),
- ControlInfo(
- CONTROL_ID, CONTROL_TITLE, "subtitle", DeviceTypes.TYPE_AIR_FRESHENER
- ),
- control
- )
+ val control =
+ Control.StatefulBuilder(CONTROL_ID, mock(PendingIntent::class.java))
+ .setStatus(Control.STATUS_OK)
+ .setControlTemplate(ControlTemplate.NO_TEMPLATE)
+ .build()
+ val cws =
+ ControlWithState(
+ ComponentName.createRelative("pkg", "cls"),
+ ControlInfo(CONTROL_ID, CONTROL_TITLE, "subtitle", DeviceTypes.TYPE_AIR_FRESHENER),
+ control,
+ )
cvh.bindData(cws, false)
val chevronIcon = baseLayout.requireViewById<View>(R.id.chevron_icon)
assertThat(chevronIcon.visibility).isEqualTo(View.VISIBLE)
}
+
+ @Test
+ fun drawableLoadedSafely_showsPlainDrawableLoaded() {
+ val control =
+ Control.StatelessBuilder(DEFAULT_CONTROL)
+ .setCustomIcon(
+ Icon.createWithResource(mContext.resources, R.drawable.ic_emergency_star)
+ .setTint(TINT_COLOR)
+ )
+ .build()
+
+ cvh.updateStatusRow(enabled = true, CONTROL_TITLE, DRAWABLE, COLOR, control)
+
+ assertThat(cvh.icon.drawable).isSameInstanceAs(PLAIN_DRAWABLE)
+ }
+
+ @Test
+ fun drawableNotLoadedSafely_showsDefaultDrawable() {
+ `when`(safeIconLoader.load(any())).thenReturn(null)
+
+ val control =
+ Control.StatelessBuilder(DEFAULT_CONTROL)
+ .setCustomIcon(
+ Icon.createWithResource(mContext.resources, R.drawable.ic_emergency_star)
+ .setTint(TINT_COLOR)
+ )
+ .build()
+
+ cvh.updateStatusRow(enabled = true, CONTROL_TITLE, DRAWABLE, COLOR, control)
+
+ assertThat(cvh.icon.drawable).isSameInstanceAs(DRAWABLE)
+ }
}
private const val CONTROL_ID = "CONTROL_ID"
@@ -136,6 +185,7 @@ private const val CONTROL_TITLE = "CONTROL_TITLE"
private const val TINT_COLOR = 0x00ff00 // Should be different from [COLOR]
private val DRAWABLE = GradientDrawable()
+private val PLAIN_DRAWABLE = ShapeDrawable()
private val COLOR = ColorStateList.valueOf(0xffff00)
-private val DEFAULT_CONTROL = Control.StatelessBuilder(
- CONTROL_ID, mock(PendingIntent::class.java)).build()
+private val DEFAULT_CONTROL =
+ Control.StatelessBuilder(CONTROL_ID, mock(PendingIntent::class.java)).build()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
index 20890a7780c8..3a0bda4b3259 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
@@ -63,6 +63,7 @@ import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.utils.SafeIconLoader
import com.android.wm.shell.taskview.TaskView
import com.android.wm.shell.taskview.TaskViewFactory
import com.google.common.truth.Truth.assertThat
@@ -71,6 +72,7 @@ import java.util.function.Consumer
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Answers
import org.mockito.Mock
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.doAnswer
@@ -102,6 +104,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
@Mock lateinit var featureFlags: FeatureFlags
@Mock lateinit var packageManager: PackageManager
@Mock lateinit var systemUIDialogFactory: SystemUIDialog.Factory
+ @Mock(answer = Answers.RETURNS_MOCKS) lateinit var safeIconLoaderFactory: SafeIconLoader.Factory
private val preferredPanelRepository = kosmos.selectedComponentRepository
private lateinit var fakeDialogController: FakeSystemUIDialogController
@@ -130,7 +133,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
Context.LAYOUT_INFLATER_SERVICE,
mContext.baseContext
.getSystemService(LayoutInflater::class.java)!!
- .cloneInContext(mContext)
+ .cloneInContext(mContext),
)
parent = FrameLayout(mContext)
@@ -154,6 +157,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
authorizedPanelsRepository,
preferredPanelRepository,
featureFlags,
+ safeIconLoaderFactory,
ControlsDialogsFactory(systemUIDialogFactory),
dumpManager,
)
@@ -303,7 +307,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
assertThat(
intent.getBooleanExtra(
ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS,
- false
+ false,
)
)
.isTrue()
@@ -341,7 +345,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
assertThat(
intent.getBooleanExtra(
ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS,
- false
+ false,
)
)
.isTrue()
@@ -374,7 +378,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
assertThat(
pendingIntent.intent.getBooleanExtra(
ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS,
- false
+ false,
)
)
.isTrue()
@@ -393,7 +397,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
assertThat(
newPendingIntent.intent.getBooleanExtra(
ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS,
- false
+ false,
)
)
.isFalse()
@@ -416,9 +420,9 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
StructureInfo(
checkNotNull(ComponentName.unflattenFromString("pkg/.cls1")),
"a",
- ArrayList()
+ ArrayList(),
)
- ),
+ )
)
preferredPanelRepository.setSelectedComponent(
SelectedComponentRepository.SelectedComponent(selectedItems[0])
@@ -598,7 +602,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
private fun ControlsServiceInfo(
componentName: ComponentName,
label: CharSequence,
- panelComponentName: ComponentName? = null
+ panelComponentName: ComponentName? = null,
): ControlsServiceInfo {
val serviceInfo =
ServiceInfo().apply {
@@ -621,7 +625,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
view: View?,
name: String,
context: Context,
- attrs: AttributeSet
+ attrs: AttributeSet,
): View? {
return onCreateView(name, context, attrs)
}
@@ -629,7 +633,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
override fun onCreateView(
name: String,
context: Context,
- attrs: AttributeSet
+ attrs: AttributeSet,
): View? {
if (FrameLayout::class.java.simpleName.equals(name)) {
val mock: FrameLayout = mock {