Introduce a CustomIconCache for editing favorites
The cache will store custom icons for the current component and will clear whenever the current component changes (as only the current component can be edited). Test: manual Test: atest com.android.systemui.controls Bug: 161440353 Change-Id: I8ac2e1ee983ad8ab714f050edaa8981a2307beca
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.graphics.drawable.Icon
|
||||
import androidx.annotation.GuardedBy
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Icon cache for custom icons sent with controls.
|
||||
*
|
||||
* It assumes that only one component can be current at the time, to minimize the number of icons
|
||||
* stored at a given time.
|
||||
*/
|
||||
@Singleton
|
||||
class CustomIconCache @Inject constructor() {
|
||||
|
||||
private var currentComponent: ComponentName? = null
|
||||
@GuardedBy("cache")
|
||||
private val cache: MutableMap<String, Icon> = LinkedHashMap()
|
||||
|
||||
/**
|
||||
* Store an icon in the cache.
|
||||
*
|
||||
* If the icons currently stored do not correspond to the component to be stored, the cache is
|
||||
* cleared first.
|
||||
*/
|
||||
fun store(component: ComponentName, controlId: String, icon: Icon?) {
|
||||
if (component != currentComponent) {
|
||||
clear()
|
||||
currentComponent = component
|
||||
}
|
||||
synchronized(cache) {
|
||||
if (icon != null) {
|
||||
cache.put(controlId, icon)
|
||||
} else {
|
||||
cache.remove(controlId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a custom icon stored in the cache.
|
||||
*
|
||||
* It will return null if the component requested is not the one whose icons are stored, or if
|
||||
* there is no icon cached for that id.
|
||||
*/
|
||||
fun retrieve(component: ComponentName, controlId: String): Icon? {
|
||||
if (component != currentComponent) return null
|
||||
return synchronized(cache) {
|
||||
cache.get(controlId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clear() {
|
||||
synchronized(cache) {
|
||||
cache.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
import com.android.systemui.controls.CustomIconCache
|
||||
import com.android.systemui.controls.controller.ControlsControllerImpl
|
||||
import com.android.systemui.controls.controller.StructureInfo
|
||||
import com.android.systemui.globalactions.GlobalActionsComponent
|
||||
@@ -42,7 +43,8 @@ import javax.inject.Inject
|
||||
class ControlsEditingActivity @Inject constructor(
|
||||
private val controller: ControlsControllerImpl,
|
||||
broadcastDispatcher: BroadcastDispatcher,
|
||||
private val globalActionsComponent: GlobalActionsComponent
|
||||
private val globalActionsComponent: GlobalActionsComponent,
|
||||
private val customIconCache: CustomIconCache
|
||||
) : LifecycleActivity() {
|
||||
|
||||
companion object {
|
||||
@@ -170,7 +172,7 @@ class ControlsEditingActivity @Inject constructor(
|
||||
|
||||
private fun setUpList() {
|
||||
val controls = controller.getFavoritesForStructure(component, structure)
|
||||
model = FavoritesModel(component, controls, favoritesModelCallback)
|
||||
model = FavoritesModel(customIconCache, component, controls, favoritesModelCallback)
|
||||
val elevation = resources.getFloat(R.dimen.control_card_elevation)
|
||||
val recyclerView = requireViewById<RecyclerView>(R.id.list)
|
||||
recyclerView.alpha = 0.0f
|
||||
|
||||
@@ -114,11 +114,27 @@ data class ControlStatusWrapper(
|
||||
val controlStatus: ControlStatus
|
||||
) : ElementWrapper(), ControlInterface by controlStatus
|
||||
|
||||
private fun nullIconGetter(_a: ComponentName, _b: String): Icon? = null
|
||||
|
||||
data class ControlInfoWrapper(
|
||||
override val component: ComponentName,
|
||||
val controlInfo: ControlInfo,
|
||||
override var favorite: Boolean
|
||||
) : ElementWrapper(), ControlInterface {
|
||||
|
||||
var customIconGetter: (ComponentName, String) -> Icon? = ::nullIconGetter
|
||||
private set
|
||||
|
||||
// Separate constructor so the getter is not used in auto-generated methods
|
||||
constructor(
|
||||
component: ComponentName,
|
||||
controlInfo: ControlInfo,
|
||||
favorite: Boolean,
|
||||
customIconGetter: (ComponentName, String) -> Icon?
|
||||
): this(component, controlInfo, favorite) {
|
||||
this.customIconGetter = customIconGetter
|
||||
}
|
||||
|
||||
override val controlId: String
|
||||
get() = controlInfo.controlId
|
||||
override val title: CharSequence
|
||||
@@ -128,8 +144,7 @@ data class ControlInfoWrapper(
|
||||
override val deviceType: Int
|
||||
get() = controlInfo.deviceType
|
||||
override val customIcon: Icon?
|
||||
// Will need to address to support for edit activity
|
||||
get() = null
|
||||
get() = customIconGetter(component, controlId)
|
||||
}
|
||||
|
||||
data class DividerWrapper(
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.util.Log
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.systemui.controls.ControlInterface
|
||||
import com.android.systemui.controls.CustomIconCache
|
||||
import com.android.systemui.controls.controller.ControlInfo
|
||||
import java.util.Collections
|
||||
|
||||
@@ -35,6 +36,7 @@ import java.util.Collections
|
||||
* @property favoritesModelCallback callback to notify on first change and empty favorites
|
||||
*/
|
||||
class FavoritesModel(
|
||||
private val customIconCache: CustomIconCache,
|
||||
private val componentName: ComponentName,
|
||||
favorites: List<ControlInfo>,
|
||||
private val favoritesModelCallback: FavoritesModelCallback
|
||||
@@ -83,7 +85,7 @@ class FavoritesModel(
|
||||
}
|
||||
|
||||
override val elements: List<ElementWrapper> = favorites.map {
|
||||
ControlInfoWrapper(componentName, it, true)
|
||||
ControlInfoWrapper(componentName, it, true, customIconCache::retrieve)
|
||||
} + DividerWrapper()
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,6 +44,7 @@ import android.widget.Space
|
||||
import android.widget.TextView
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.controls.ControlsServiceInfo
|
||||
import com.android.systemui.controls.CustomIconCache
|
||||
import com.android.systemui.controls.controller.ControlInfo
|
||||
import com.android.systemui.controls.controller.ControlsController
|
||||
import com.android.systemui.controls.controller.StructureInfo
|
||||
@@ -75,7 +76,8 @@ class ControlsUiControllerImpl @Inject constructor (
|
||||
@Main val sharedPreferences: SharedPreferences,
|
||||
val controlActionCoordinator: ControlActionCoordinator,
|
||||
private val activityStarter: ActivityStarter,
|
||||
private val shadeController: ShadeController
|
||||
private val shadeController: ShadeController,
|
||||
private val iconCache: CustomIconCache
|
||||
) : ControlsUiController {
|
||||
|
||||
companion object {
|
||||
@@ -502,6 +504,7 @@ class ControlsUiControllerImpl @Inject constructor (
|
||||
controls.forEach { c ->
|
||||
controlsById.get(ControlKey(componentName, c.getControlId()))?.let {
|
||||
Log.d(ControlsUiController.TAG, "onRefreshState() for id: " + c.getControlId())
|
||||
iconCache.store(componentName, c.controlId, c.customIcon)
|
||||
val cws = ControlWithState(componentName, it.ci, c)
|
||||
val key = ControlKey(componentName, c.getControlId())
|
||||
controlsById.put(key, cws)
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.graphics.drawable.Icon
|
||||
import android.testing.AndroidTestingRunner
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
class CustomIconCacheTest : SysuiTestCase() {
|
||||
|
||||
companion object {
|
||||
private val TEST_COMPONENT1 = ComponentName.unflattenFromString("pkg/.cls1")!!
|
||||
private val TEST_COMPONENT2 = ComponentName.unflattenFromString("pkg/.cls2")!!
|
||||
private const val CONTROL_ID_1 = "TEST_CONTROL_1"
|
||||
private const val CONTROL_ID_2 = "TEST_CONTROL_2"
|
||||
}
|
||||
|
||||
@Mock(stubOnly = true)
|
||||
private lateinit var icon1: Icon
|
||||
@Mock(stubOnly = true)
|
||||
private lateinit var icon2: Icon
|
||||
private lateinit var customIconCache: CustomIconCache
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
|
||||
customIconCache = CustomIconCache()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIconStoredCorrectly() {
|
||||
customIconCache.store(TEST_COMPONENT1, CONTROL_ID_1, icon1)
|
||||
|
||||
assertTrue(icon1 === customIconCache.retrieve(TEST_COMPONENT1, CONTROL_ID_1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIconNotStoredReturnsNull() {
|
||||
customIconCache.store(TEST_COMPONENT1, CONTROL_ID_1, icon1)
|
||||
|
||||
assertNull(customIconCache.retrieve(TEST_COMPONENT1, CONTROL_ID_2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWrongComponentReturnsNull() {
|
||||
customIconCache.store(TEST_COMPONENT1, CONTROL_ID_1, icon1)
|
||||
|
||||
assertNull(customIconCache.retrieve(TEST_COMPONENT2, CONTROL_ID_1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChangeComponentOldComponentIsRemoved() {
|
||||
customIconCache.store(TEST_COMPONENT1, CONTROL_ID_1, icon1)
|
||||
customIconCache.store(TEST_COMPONENT2, CONTROL_ID_2, icon2)
|
||||
|
||||
assertNull(customIconCache.retrieve(TEST_COMPONENT1, CONTROL_ID_1))
|
||||
assertNull(customIconCache.retrieve(TEST_COMPONENT1, CONTROL_ID_2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChangeComponentCorrectIconRetrieved() {
|
||||
customIconCache.store(TEST_COMPONENT1, CONTROL_ID_1, icon1)
|
||||
customIconCache.store(TEST_COMPONENT2, CONTROL_ID_1, icon2)
|
||||
|
||||
assertTrue(icon2 === customIconCache.retrieve(TEST_COMPONENT2, CONTROL_ID_1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStoreNull() {
|
||||
customIconCache.store(TEST_COMPONENT1, CONTROL_ID_1, icon1)
|
||||
customIconCache.store(TEST_COMPONENT1, CONTROL_ID_1, null)
|
||||
|
||||
assertNull(customIconCache.retrieve(TEST_COMPONENT1, CONTROL_ID_1))
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.controls.ControlInterface
|
||||
import com.android.systemui.controls.CustomIconCache
|
||||
import com.android.systemui.controls.controller.ControlInfo
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.eq
|
||||
@@ -57,6 +58,8 @@ class FavoritesModelTest : SysuiTestCase() {
|
||||
private lateinit var callback: FavoritesModel.FavoritesModelCallback
|
||||
@Mock
|
||||
private lateinit var adapter: RecyclerView.Adapter<*>
|
||||
@Mock
|
||||
private lateinit var customIconCache: CustomIconCache
|
||||
private lateinit var model: FavoritesModel
|
||||
private lateinit var dividerWrapper: DividerWrapper
|
||||
|
||||
@@ -64,7 +67,7 @@ class FavoritesModelTest : SysuiTestCase() {
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
|
||||
model = FavoritesModel(TEST_COMPONENT, INITIAL_FAVORITES, callback)
|
||||
model = FavoritesModel(customIconCache, TEST_COMPONENT, INITIAL_FAVORITES, callback)
|
||||
model.attachAdapter(adapter)
|
||||
dividerWrapper = model.elements.first { it is DividerWrapper } as DividerWrapper
|
||||
}
|
||||
@@ -89,7 +92,7 @@ class FavoritesModelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun testInitialElements() {
|
||||
val expected = INITIAL_FAVORITES.map {
|
||||
ControlInfoWrapper(TEST_COMPONENT, it, true)
|
||||
ControlInfoWrapper(TEST_COMPONENT, it, true, customIconCache::retrieve)
|
||||
} + DividerWrapper()
|
||||
assertEquals(expected, model.elements)
|
||||
}
|
||||
@@ -287,5 +290,13 @@ class FavoritesModelTest : SysuiTestCase() {
|
||||
verify(callback).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCacheCalledWhenGettingCustomIcon() {
|
||||
val wrapper = model.elements[0] as ControlInfoWrapper
|
||||
wrapper.customIcon
|
||||
|
||||
verify(customIconCache).retrieve(TEST_COMPONENT, wrapper.controlId)
|
||||
}
|
||||
|
||||
private fun getDividerPosition(): Int = model.elements.indexOf(dividerWrapper)
|
||||
}
|
||||
Reference in New Issue
Block a user