Merge "Add userId check before loading icon in Device Controls" into sc-v2-dev

This commit is contained in:
Anton Potapov
2023-08-01 15:51:23 +00:00
committed by Android (Google) Code Review
11 changed files with 157 additions and 42 deletions

View File

@@ -36,6 +36,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.systemui.R
import com.android.systemui.controls.ControlInterface
import com.android.systemui.controls.ui.CanUseIconPredicate
import com.android.systemui.controls.ui.RenderInfo
private typealias ModelFavoriteChanger = (String, Boolean) -> Unit
@@ -49,7 +50,8 @@ private typealias ModelFavoriteChanger = (String, Boolean) -> Unit
* @property elevation elevation of each control view
*/
class ControlAdapter(
private val elevation: Float
private val elevation: Float,
private val currentUserId: Int
) : RecyclerView.Adapter<Holder>() {
companion object {
@@ -84,6 +86,7 @@ class ControlAdapter(
background = parent.context.getDrawable(
R.drawable.control_background_ripple)
},
currentUserId,
model?.moveHelper // Indicates that position information is needed
) { id, favorite ->
model?.changeFavoriteStatus(id, favorite)
@@ -189,6 +192,7 @@ private class ZoneHolder(view: View) : Holder(view) {
*/
internal class ControlHolder(
view: View,
currentUserId: Int,
val moveHelper: ControlsModel.MoveHelper?,
val favoriteCallback: ModelFavoriteChanger
) : Holder(view) {
@@ -205,6 +209,7 @@ internal class ControlHolder(
visibility = View.VISIBLE
}
private val canUseIconPredicate = CanUseIconPredicate(currentUserId)
private val accessibilityDelegate = ControlHolderAccessibilityDelegate(
this::stateDescription,
this::getLayoutPosition,
@@ -264,7 +269,9 @@ internal class ControlHolder(
val fg = context.getResources().getColorStateList(ri.foreground, context.getTheme())
icon.imageTintList = null
ci.customIcon?.let {
ci.customIcon
?.takeIf(canUseIconPredicate)
?.let {
icon.setImageIcon(it)
} ?: run {
icon.setImageDrawable(ri.icon)

View File

@@ -180,7 +180,7 @@ class ControlsEditingActivity @Inject constructor(
val elevation = resources.getFloat(R.dimen.control_card_elevation)
val recyclerView = requireViewById<RecyclerView>(R.id.list)
recyclerView.alpha = 0.0f
val adapter = ControlAdapter(elevation).apply {
val adapter = ControlAdapter(elevation, currentUserTracker.currentUserId).apply {
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
var hasAnimated = false
override fun onChanged() {

View File

@@ -164,7 +164,8 @@ class ControlsFavoritingActivity @Inject constructor(
}
executor.execute {
structurePager.adapter = StructureAdapter(listOfStructures)
structurePager.adapter = StructureAdapter(listOfStructures,
currentUserTracker.currentUserId)
structurePager.setCurrentItem(structureIndex)
if (error) {
statusText.text = resources.getString(R.string.controls_favorite_load_error,
@@ -210,7 +211,7 @@ class ControlsFavoritingActivity @Inject constructor(
structurePager.alpha = 0.0f
pageIndicator.alpha = 0.0f
structurePager.apply {
adapter = StructureAdapter(emptyList())
adapter = StructureAdapter(emptyList(), currentUserTracker.currentUserId)
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)

View File

@@ -24,13 +24,15 @@ import androidx.recyclerview.widget.RecyclerView
import com.android.systemui.R
class StructureAdapter(
private val models: List<StructureContainer>
private val models: List<StructureContainer>,
private val currentUserId: Int
) : RecyclerView.Adapter<StructureAdapter.StructureHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, p1: Int): StructureHolder {
val layoutInflater = LayoutInflater.from(parent.context)
return StructureHolder(
layoutInflater.inflate(R.layout.controls_structure_page, parent, false)
layoutInflater.inflate(R.layout.controls_structure_page, parent, false),
currentUserId
)
}
@@ -40,7 +42,8 @@ class StructureAdapter(
holder.bind(models[index].model)
}
class StructureHolder(view: View) : RecyclerView.ViewHolder(view) {
class StructureHolder(view: View, currentUserId: Int) :
RecyclerView.ViewHolder(view) {
private val recyclerView: RecyclerView
private val controlAdapter: ControlAdapter
@@ -48,7 +51,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)
controlAdapter = ControlAdapter(elevation, currentUserId)
setUpRecyclerView()
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2023 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.controls.ui
import android.content.ContentProvider
import android.graphics.drawable.Icon
class CanUseIconPredicate(private val currentUserId: Int) : (Icon) -> Boolean {
override fun invoke(icon: Icon): Boolean =
if (icon.type == Icon.TYPE_URI || icon.type == Icon.TYPE_URI_ADAPTIVE_BITMAP) {
ContentProvider.getUserIdFromUri(icon.uri, currentUserId) == currentUserId
} else {
true
}
}

View File

@@ -54,7 +54,6 @@ import com.android.systemui.animation.Interpolators
import com.android.systemui.controls.ControlsMetricsLogger
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.util.concurrency.DelayableExecutor
import kotlin.reflect.KClass
/**
* Wraps the widgets that make up the UI representation of a {@link Control}. Updates to the view
@@ -68,7 +67,8 @@ class ControlViewHolder(
val bgExecutor: DelayableExecutor,
val controlActionCoordinator: ControlActionCoordinator,
val controlsMetricsLogger: ControlsMetricsLogger,
val uid: Int
val uid: Int,
val currentUserId: Int
) {
companion object {
@@ -85,27 +85,6 @@ class ControlViewHolder(
private val ATTR_DISABLED = intArrayOf(-android.R.attr.state_enabled)
const val MIN_LEVEL = 0
const val MAX_LEVEL = 10000
fun findBehaviorClass(
status: Int,
template: ControlTemplate,
deviceType: Int
): KClass<out Behavior> {
return when {
status != Control.STATUS_OK -> StatusBehavior::class
template == ControlTemplate.NO_TEMPLATE -> TouchBehavior::class
template is ThumbnailTemplate -> ThumbnailBehavior::class
// Required for legacy support, or where cameras do not use the new template
deviceType == DeviceTypes.TYPE_CAMERA -> TouchBehavior::class
template is ToggleTemplate -> ToggleBehavior::class
template is StatelessTemplate -> TouchBehavior::class
template is ToggleRangeTemplate -> ToggleRangeBehavior::class
template is RangeTemplate -> ToggleRangeBehavior::class
template is TemperatureControlTemplate -> TemperatureControlBehavior::class
else -> DefaultBehavior::class
}
}
}
private val toggleBackgroundIntensity: Float = layout.context.resources
@@ -146,6 +125,26 @@ class ControlViewHolder(
status.setSelected(true)
}
fun findBehavior(
status: Int,
template: ControlTemplate,
deviceType: Int
): () -> Behavior {
return when {
status != Control.STATUS_OK -> { { StatusBehavior() } }
template == ControlTemplate.NO_TEMPLATE -> { { TouchBehavior() } }
template is ThumbnailTemplate -> { { ThumbnailBehavior(currentUserId) } }
// Required for legacy support, or where cameras do not use the new template
deviceType == DeviceTypes.TYPE_CAMERA -> { { TouchBehavior() } }
template is ToggleTemplate -> { { ToggleBehavior() } }
template is StatelessTemplate -> { { TouchBehavior() } }
template is ToggleRangeTemplate -> { { ToggleRangeBehavior() } }
template is RangeTemplate -> { { ToggleRangeBehavior() } }
template is TemperatureControlTemplate -> { { TemperatureControlBehavior() } }
else -> { { DefaultBehavior() } }
}
}
fun bindData(cws: ControlWithState, isLocked: Boolean) {
// If an interaction is in progress, the update may visually interfere with the action the
// action the user wants to make. Don't apply the update, and instead assume a new update
@@ -179,7 +178,7 @@ class ControlViewHolder(
val wasLoading = isLoading
isLoading = false
behavior = bindBehavior(behavior,
findBehaviorClass(controlStatus, controlTemplate, deviceType))
findBehavior(controlStatus, controlTemplate, deviceType))
updateContentDescription()
// Only log one event per control, at the moment we have determined that the control
@@ -251,13 +250,14 @@ class ControlViewHolder(
fun bindBehavior(
existingBehavior: Behavior?,
clazz: KClass<out Behavior>,
createBehaviour: () -> Behavior,
offset: Int = 0
): Behavior {
val behavior = if (existingBehavior == null || existingBehavior!!::class != clazz) {
val newBehavior = createBehaviour()
val behavior = if (existingBehavior == null ||
existingBehavior::class != newBehavior::class) {
// Behavior changes can signal a change in template from the app or
// first time setup
val newBehavior = clazz.java.newInstance()
newBehavior.initialize(this)
// let behaviors define their own, if necessary, and clear any existing ones

View File

@@ -412,7 +412,8 @@ class ControlsUiControllerImpl @Inject constructor (
bgExecutor,
controlActionCoordinator,
controlsMetricsLogger,
selected.uid
selected.uid,
controlsController.get().currentUserId
)
cvh.bindData(it, false /* isLocked, will be ignored on initial load */)
controlViewsById.put(key, cvh)

View File

@@ -63,7 +63,7 @@ class TemperatureControlBehavior : Behavior {
// interactions (touch, range)
subBehavior = cvh.bindBehavior(
subBehavior,
ControlViewHolder.findBehaviorClass(
cvh.findBehavior(
control.status,
subTemplate,
control.deviceType

View File

@@ -33,7 +33,7 @@ import com.android.systemui.controls.ui.ControlViewHolder.Companion.MIN_LEVEL
* 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 : Behavior {
class ThumbnailBehavior(currentUserId: Int) : Behavior {
lateinit var template: ThumbnailTemplate
lateinit var control: Control
lateinit var cvh: ControlViewHolder
@@ -42,6 +42,7 @@ class ThumbnailBehavior : Behavior {
private var shadowRadius: Float = 0f
private var shadowColor: Int = 0
private val canUseIconPredicate = CanUseIconPredicate(currentUserId)
private val enabled: Boolean
get() = template.isActive()
@@ -80,11 +81,15 @@ class ThumbnailBehavior : Behavior {
cvh.status.setShadowLayer(shadowOffsetX, shadowOffsetY, shadowRadius, shadowColor)
cvh.bgExecutor.execute {
val drawable = template.getThumbnail().loadDrawable(cvh.context)
val drawable = template.thumbnail
.takeIf(canUseIconPredicate)
?.loadDrawable(cvh.context)
cvh.uiExecutor.execute {
val radius = cvh.context.getResources()
.getDimensionPixelSize(R.dimen.control_corner_radius).toFloat()
clipLayer.setDrawable(CornerDrawable(drawable, radius))
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)

View File

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2023 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.controls.ui
import android.graphics.Bitmap
import android.graphics.drawable.Icon
import android.net.Uri
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidTestingRunner::class)
class CanUseIconPredicateTest : SysuiTestCase() {
private companion object {
const val USER_ID_1 = 1
const val USER_ID_2 = 2
}
val underTest: CanUseIconPredicate = CanUseIconPredicate(USER_ID_1)
@Test
fun testReturnsFalseForDifferentUser() {
val user2Icon = Icon.createWithContentUri("content://$USER_ID_2@test")
assertThat(underTest.invoke(user2Icon)).isFalse()
}
@Test
fun testReturnsTrueForCorrectUser() {
val user1Icon = Icon.createWithContentUri("content://$USER_ID_1@test")
assertThat(underTest.invoke(user1Icon)).isTrue()
}
@Test
fun testReturnsTrueForUriWithoutUser() {
val uriIcon = Icon.createWithContentUri(Uri.parse("content://test"))
assertThat(underTest.invoke(uriIcon)).isTrue()
}
@Test
fun testReturnsTrueForNonUriIcon() {
val bitmapIcon = Icon.createWithBitmap(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
assertThat(underTest.invoke(bitmapIcon)).isTrue()
}
}

View File

@@ -63,7 +63,8 @@ class ControlViewHolderTest : SysuiTestCase() {
FakeExecutor(clock),
mock(ControlActionCoordinator::class.java),
mock(ControlsMetricsLogger::class.java),
uid = 100
uid = 100,
currentUserId = 0
)
val cws = ControlWithState(