diff options
7 files changed, 218 insertions, 8 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/controls/CustomIconCache.kt b/packages/SystemUI/src/com/android/systemui/controls/CustomIconCache.kt new file mode 100644 index 000000000000..cca0f1653757 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/CustomIconCache.kt @@ -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() + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt index ff40a8a883ae..f68388d5db3f 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt index 4ef64a5cddbf..ad0e7a541f98 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt @@ -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( diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/FavoritesModel.kt b/packages/SystemUI/src/com/android/systemui/controls/management/FavoritesModel.kt index 524250134e9b..f9ce6362f4f8 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/FavoritesModel.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/FavoritesModel.kt @@ -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() /** diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index 1eb7e2168a6a..5f75c96be128 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -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) diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/CustomIconCacheTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/CustomIconCacheTest.kt new file mode 100644 index 000000000000..4d0f2ed47495 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/CustomIconCacheTest.kt @@ -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)) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/FavoritesModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/FavoritesModelTest.kt index ce33a8d49fac..f0003ed603ab 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/FavoritesModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/FavoritesModelTest.kt @@ -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) }
\ No newline at end of file |