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:
Fabian Kozynski
2020-08-10 16:45:03 +00:00
committed by Automerger Merge Worker
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)
}