diff options
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 { |