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:
Fabian Kozynski
2020-08-04 11:30:55 -04:00
parent 07514b6192
commit 8f659543fa
7 changed files with 218 additions and 8 deletions

View File

@@ -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()
}
}
}

View File

@@ -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

View File

@@ -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(

View File

@@ -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()
/**

View File

@@ -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)

View File

@@ -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))
}
}

View File

@@ -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)
}