Introduce a CustomIconCache for editing favorites am: 95e63154f8
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/12345483 Change-Id: I49ee3391eee93919dce7785c7270a6dd6a382879
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 androidx.recyclerview.widget.RecyclerView
|
||||||
import com.android.systemui.R
|
import com.android.systemui.R
|
||||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
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.ControlsControllerImpl
|
||||||
import com.android.systemui.controls.controller.StructureInfo
|
import com.android.systemui.controls.controller.StructureInfo
|
||||||
import com.android.systemui.globalactions.GlobalActionsComponent
|
import com.android.systemui.globalactions.GlobalActionsComponent
|
||||||
@@ -42,7 +43,8 @@ import javax.inject.Inject
|
|||||||
class ControlsEditingActivity @Inject constructor(
|
class ControlsEditingActivity @Inject constructor(
|
||||||
private val controller: ControlsControllerImpl,
|
private val controller: ControlsControllerImpl,
|
||||||
broadcastDispatcher: BroadcastDispatcher,
|
broadcastDispatcher: BroadcastDispatcher,
|
||||||
private val globalActionsComponent: GlobalActionsComponent
|
private val globalActionsComponent: GlobalActionsComponent,
|
||||||
|
private val customIconCache: CustomIconCache
|
||||||
) : LifecycleActivity() {
|
) : LifecycleActivity() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -170,7 +172,7 @@ class ControlsEditingActivity @Inject constructor(
|
|||||||
|
|
||||||
private fun setUpList() {
|
private fun setUpList() {
|
||||||
val controls = controller.getFavoritesForStructure(component, structure)
|
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 elevation = resources.getFloat(R.dimen.control_card_elevation)
|
||||||
val recyclerView = requireViewById<RecyclerView>(R.id.list)
|
val recyclerView = requireViewById<RecyclerView>(R.id.list)
|
||||||
recyclerView.alpha = 0.0f
|
recyclerView.alpha = 0.0f
|
||||||
|
|||||||
@@ -114,11 +114,27 @@ data class ControlStatusWrapper(
|
|||||||
val controlStatus: ControlStatus
|
val controlStatus: ControlStatus
|
||||||
) : ElementWrapper(), ControlInterface by controlStatus
|
) : ElementWrapper(), ControlInterface by controlStatus
|
||||||
|
|
||||||
|
private fun nullIconGetter(_a: ComponentName, _b: String): Icon? = null
|
||||||
|
|
||||||
data class ControlInfoWrapper(
|
data class ControlInfoWrapper(
|
||||||
override val component: ComponentName,
|
override val component: ComponentName,
|
||||||
val controlInfo: ControlInfo,
|
val controlInfo: ControlInfo,
|
||||||
override var favorite: Boolean
|
override var favorite: Boolean
|
||||||
) : ElementWrapper(), ControlInterface {
|
) : 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
|
override val controlId: String
|
||||||
get() = controlInfo.controlId
|
get() = controlInfo.controlId
|
||||||
override val title: CharSequence
|
override val title: CharSequence
|
||||||
@@ -128,8 +144,7 @@ data class ControlInfoWrapper(
|
|||||||
override val deviceType: Int
|
override val deviceType: Int
|
||||||
get() = controlInfo.deviceType
|
get() = controlInfo.deviceType
|
||||||
override val customIcon: Icon?
|
override val customIcon: Icon?
|
||||||
// Will need to address to support for edit activity
|
get() = customIconGetter(component, controlId)
|
||||||
get() = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DividerWrapper(
|
data class DividerWrapper(
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import android.util.Log
|
|||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.android.systemui.controls.ControlInterface
|
import com.android.systemui.controls.ControlInterface
|
||||||
|
import com.android.systemui.controls.CustomIconCache
|
||||||
import com.android.systemui.controls.controller.ControlInfo
|
import com.android.systemui.controls.controller.ControlInfo
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ import java.util.Collections
|
|||||||
* @property favoritesModelCallback callback to notify on first change and empty favorites
|
* @property favoritesModelCallback callback to notify on first change and empty favorites
|
||||||
*/
|
*/
|
||||||
class FavoritesModel(
|
class FavoritesModel(
|
||||||
|
private val customIconCache: CustomIconCache,
|
||||||
private val componentName: ComponentName,
|
private val componentName: ComponentName,
|
||||||
favorites: List<ControlInfo>,
|
favorites: List<ControlInfo>,
|
||||||
private val favoritesModelCallback: FavoritesModelCallback
|
private val favoritesModelCallback: FavoritesModelCallback
|
||||||
@@ -83,7 +85,7 @@ class FavoritesModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override val elements: List<ElementWrapper> = favorites.map {
|
override val elements: List<ElementWrapper> = favorites.map {
|
||||||
ControlInfoWrapper(componentName, it, true)
|
ControlInfoWrapper(componentName, it, true, customIconCache::retrieve)
|
||||||
} + DividerWrapper()
|
} + DividerWrapper()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import android.widget.Space
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.android.systemui.R
|
import com.android.systemui.R
|
||||||
import com.android.systemui.controls.ControlsServiceInfo
|
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.ControlInfo
|
||||||
import com.android.systemui.controls.controller.ControlsController
|
import com.android.systemui.controls.controller.ControlsController
|
||||||
import com.android.systemui.controls.controller.StructureInfo
|
import com.android.systemui.controls.controller.StructureInfo
|
||||||
@@ -75,7 +76,8 @@ class ControlsUiControllerImpl @Inject constructor (
|
|||||||
@Main val sharedPreferences: SharedPreferences,
|
@Main val sharedPreferences: SharedPreferences,
|
||||||
val controlActionCoordinator: ControlActionCoordinator,
|
val controlActionCoordinator: ControlActionCoordinator,
|
||||||
private val activityStarter: ActivityStarter,
|
private val activityStarter: ActivityStarter,
|
||||||
private val shadeController: ShadeController
|
private val shadeController: ShadeController,
|
||||||
|
private val iconCache: CustomIconCache
|
||||||
) : ControlsUiController {
|
) : ControlsUiController {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -502,6 +504,7 @@ class ControlsUiControllerImpl @Inject constructor (
|
|||||||
controls.forEach { c ->
|
controls.forEach { c ->
|
||||||
controlsById.get(ControlKey(componentName, c.getControlId()))?.let {
|
controlsById.get(ControlKey(componentName, c.getControlId()))?.let {
|
||||||
Log.d(ControlsUiController.TAG, "onRefreshState() for id: " + c.getControlId())
|
Log.d(ControlsUiController.TAG, "onRefreshState() for id: " + c.getControlId())
|
||||||
|
iconCache.store(componentName, c.controlId, c.customIcon)
|
||||||
val cws = ControlWithState(componentName, it.ci, c)
|
val cws = ControlWithState(componentName, it.ci, c)
|
||||||
val key = ControlKey(componentName, c.getControlId())
|
val key = ControlKey(componentName, c.getControlId())
|
||||||
controlsById.put(key, cws)
|
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 androidx.test.filters.SmallTest
|
||||||
import com.android.systemui.SysuiTestCase
|
import com.android.systemui.SysuiTestCase
|
||||||
import com.android.systemui.controls.ControlInterface
|
import com.android.systemui.controls.ControlInterface
|
||||||
|
import com.android.systemui.controls.CustomIconCache
|
||||||
import com.android.systemui.controls.controller.ControlInfo
|
import com.android.systemui.controls.controller.ControlInfo
|
||||||
import com.android.systemui.util.mockito.any
|
import com.android.systemui.util.mockito.any
|
||||||
import com.android.systemui.util.mockito.eq
|
import com.android.systemui.util.mockito.eq
|
||||||
@@ -57,6 +58,8 @@ class FavoritesModelTest : SysuiTestCase() {
|
|||||||
private lateinit var callback: FavoritesModel.FavoritesModelCallback
|
private lateinit var callback: FavoritesModel.FavoritesModelCallback
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var adapter: RecyclerView.Adapter<*>
|
private lateinit var adapter: RecyclerView.Adapter<*>
|
||||||
|
@Mock
|
||||||
|
private lateinit var customIconCache: CustomIconCache
|
||||||
private lateinit var model: FavoritesModel
|
private lateinit var model: FavoritesModel
|
||||||
private lateinit var dividerWrapper: DividerWrapper
|
private lateinit var dividerWrapper: DividerWrapper
|
||||||
|
|
||||||
@@ -64,7 +67,7 @@ class FavoritesModelTest : SysuiTestCase() {
|
|||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockitoAnnotations.initMocks(this)
|
MockitoAnnotations.initMocks(this)
|
||||||
|
|
||||||
model = FavoritesModel(TEST_COMPONENT, INITIAL_FAVORITES, callback)
|
model = FavoritesModel(customIconCache, TEST_COMPONENT, INITIAL_FAVORITES, callback)
|
||||||
model.attachAdapter(adapter)
|
model.attachAdapter(adapter)
|
||||||
dividerWrapper = model.elements.first { it is DividerWrapper } as DividerWrapper
|
dividerWrapper = model.elements.first { it is DividerWrapper } as DividerWrapper
|
||||||
}
|
}
|
||||||
@@ -89,7 +92,7 @@ class FavoritesModelTest : SysuiTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun testInitialElements() {
|
fun testInitialElements() {
|
||||||
val expected = INITIAL_FAVORITES.map {
|
val expected = INITIAL_FAVORITES.map {
|
||||||
ControlInfoWrapper(TEST_COMPONENT, it, true)
|
ControlInfoWrapper(TEST_COMPONENT, it, true, customIconCache::retrieve)
|
||||||
} + DividerWrapper()
|
} + DividerWrapper()
|
||||||
assertEquals(expected, model.elements)
|
assertEquals(expected, model.elements)
|
||||||
}
|
}
|
||||||
@@ -287,5 +290,13 @@ class FavoritesModelTest : SysuiTestCase() {
|
|||||||
verify(callback).onFirstChange()
|
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)
|
private fun getDividerPosition(): Int = model.elements.indexOf(dividerWrapper)
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user