diff options
| author | 2020-08-04 11:30:55 -0400 | |
|---|---|---|
| committer | 2020-08-04 12:24:09 -0400 | |
| commit | 8f659543faae7d66efbc2e6ce00cbfb97f49259c (patch) | |
| tree | f99d706d75b09517dc37d79a29b62cef70a91867 | |
| parent | 07514b6192ea99819f5a7ea0ff648c6c4927c152 (diff) | |
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
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 |