1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.controls.management 18 19 import android.content.ComponentName 20 import android.testing.AndroidTestingRunner 21 import androidx.recyclerview.widget.RecyclerView 22 import androidx.test.filters.SmallTest 23 import com.android.systemui.SysuiTestCase 24 import com.android.systemui.controls.ControlInterface 25 import com.android.systemui.controls.CustomIconCache 26 import com.android.systemui.controls.controller.ControlInfo 27 import com.android.systemui.util.mockito.any 28 import com.android.systemui.util.mockito.eq 29 import org.junit.After 30 import org.junit.Assert.assertEquals 31 import org.junit.Assert.assertFalse 32 import org.junit.Assert.assertTrue 33 import org.junit.Before 34 import org.junit.Test 35 import org.junit.runner.RunWith 36 import org.mockito.ArgumentMatchers.anyInt 37 import org.mockito.Mock 38 import org.mockito.Mockito.inOrder 39 import org.mockito.Mockito.never 40 import org.mockito.Mockito.times 41 import org.mockito.Mockito.verify 42 import org.mockito.Mockito.verifyNoMoreInteractions 43 import org.mockito.MockitoAnnotations 44 45 @SmallTest 46 @RunWith(AndroidTestingRunner::class) 47 class FavoritesModelTest : SysuiTestCase() { 48 49 companion object { 50 private val TEST_COMPONENT = ComponentName.unflattenFromString("test_pkg/.test_cls")!! 51 private val ID_PREFIX = "control" 52 private val INITIAL_FAVORITES = (0..5).map { 53 ControlInfo("$ID_PREFIX$it", "title$it", "subtitle$it", it) 54 } 55 } 56 57 @Mock 58 private lateinit var callback: FavoritesModel.FavoritesModelCallback 59 @Mock 60 private lateinit var adapter: RecyclerView.Adapter<*> 61 @Mock 62 private lateinit var customIconCache: CustomIconCache 63 private lateinit var model: FavoritesModel 64 private lateinit var dividerWrapper: DividerWrapper 65 66 @Before 67 fun setUp() { 68 MockitoAnnotations.initMocks(this) 69 70 model = FavoritesModel(customIconCache, TEST_COMPONENT, INITIAL_FAVORITES, callback) 71 model.attachAdapter(adapter) 72 dividerWrapper = model.elements.first { it is DividerWrapper } as DividerWrapper 73 } 74 75 @After 76 fun testListConsistency() { 77 assertEquals(INITIAL_FAVORITES.size + 1, model.elements.toSet().size) 78 val dividerIndex = getDividerPosition() 79 model.elements.forEachIndexed { index, element -> 80 if (index == dividerIndex) { 81 assertEquals(dividerWrapper, element) 82 } else { 83 element as ControlInterface 84 assertEquals(index < dividerIndex, element.favorite) 85 } 86 } 87 assertEquals(model.favorites, model.elements.take(dividerIndex).map { 88 (it as ControlInfoWrapper).controlInfo 89 }) 90 } 91 92 @Test 93 fun testInitialElements() { 94 val expected = INITIAL_FAVORITES.map { 95 ControlInfoWrapper(TEST_COMPONENT, it, true, customIconCache::retrieve) 96 } + DividerWrapper() 97 assertEquals(expected, model.elements) 98 } 99 100 @Test 101 fun testFavorites() { 102 assertEquals(INITIAL_FAVORITES, model.favorites) 103 } 104 105 @Test 106 fun testRemoveFavorite_notInFavorites() { 107 val removed = 4 108 val id = "$ID_PREFIX$removed" 109 110 model.changeFavoriteStatus(id, false) 111 112 assertTrue(model.favorites.none { it.controlId == id }) 113 114 verify(callback).onFirstChange() 115 } 116 117 @Test 118 fun testRemoveFavorite_endOfElements() { 119 val removed = 4 120 val id = "$ID_PREFIX$removed" 121 model.changeFavoriteStatus(id, false) 122 123 assertEquals(ControlInfoWrapper( 124 TEST_COMPONENT, INITIAL_FAVORITES[4], false), model.elements.last()) 125 verify(callback).onFirstChange() 126 } 127 128 @Test 129 fun testRemoveFavorite_adapterNotified() { 130 val removed = 4 131 val id = "$ID_PREFIX$removed" 132 model.changeFavoriteStatus(id, false) 133 134 val lastPos = model.elements.size - 1 135 verify(adapter).notifyItemChanged(eq(lastPos), any(Any::class.java)) 136 verify(adapter).notifyItemMoved(removed, lastPos) 137 138 verify(callback).onFirstChange() 139 } 140 141 @Test 142 fun testRemoveFavorite_dividerMovedBack() { 143 val oldDividerPosition = getDividerPosition() 144 val removed = 4 145 val id = "$ID_PREFIX$removed" 146 model.changeFavoriteStatus(id, false) 147 148 assertEquals(oldDividerPosition - 1, getDividerPosition()) 149 150 verify(callback).onFirstChange() 151 } 152 153 @Test 154 fun testRemoveFavorite_ShowDivider() { 155 val oldDividerPosition = getDividerPosition() 156 val removed = 4 157 val id = "$ID_PREFIX$removed" 158 model.changeFavoriteStatus(id, false) 159 160 assertTrue(dividerWrapper.showDivider) 161 verify(adapter).notifyItemChanged(oldDividerPosition) 162 163 verify(callback).onFirstChange() 164 } 165 166 @Test 167 fun testDoubleRemove_onlyOnce() { 168 val removed = 4 169 val id = "$ID_PREFIX$removed" 170 model.changeFavoriteStatus(id, false) 171 model.changeFavoriteStatus(id, false) 172 173 verify(adapter /* only once */).notifyItemChanged(anyInt(), any(Any::class.java)) 174 verify(adapter /* only once */).notifyItemMoved(anyInt(), anyInt()) 175 verify(adapter /* only once (divider) */).notifyItemChanged(anyInt()) 176 177 verify(callback).onFirstChange() 178 } 179 180 @Test 181 fun testRemoveTwo_InSameOrder() { 182 val removedFirst = 3 183 val removedSecond = 0 184 model.changeFavoriteStatus("$ID_PREFIX$removedFirst", false) 185 model.changeFavoriteStatus("$ID_PREFIX$removedSecond", false) 186 187 assertEquals(listOf( 188 ControlInfoWrapper(TEST_COMPONENT, INITIAL_FAVORITES[removedFirst], false), 189 ControlInfoWrapper(TEST_COMPONENT, INITIAL_FAVORITES[removedSecond], false) 190 ), model.elements.takeLast(2)) 191 192 verify(callback).onFirstChange() 193 } 194 195 @Test 196 fun testRemoveAll_showNone() { 197 INITIAL_FAVORITES.forEach { 198 model.changeFavoriteStatus(it.controlId, false) 199 } 200 assertEquals(dividerWrapper, model.elements.first()) 201 assertTrue(dividerWrapper.showNone) 202 verify(adapter, times(2)).notifyItemChanged(anyInt()) // divider 203 verify(callback).onNoneChanged(true) 204 205 verify(callback).onFirstChange() 206 } 207 208 @Test 209 fun testAddFavorite_movedToEnd() { 210 val added = 2 211 val id = "$ID_PREFIX$added" 212 model.changeFavoriteStatus(id, false) 213 model.changeFavoriteStatus(id, true) 214 215 assertEquals(id, model.favorites.last().controlId) 216 217 verify(callback).onFirstChange() 218 } 219 220 @Test 221 fun testAddFavorite_onlyOnce() { 222 val added = 2 223 val id = "$ID_PREFIX$added" 224 model.changeFavoriteStatus(id, false) 225 model.changeFavoriteStatus(id, true) 226 model.changeFavoriteStatus(id, true) 227 228 // Once for remove and once for add 229 verify(adapter, times(2)).notifyItemChanged(anyInt(), any(Any::class.java)) 230 verify(adapter, times(2)).notifyItemMoved(anyInt(), anyInt()) 231 232 verify(callback).onFirstChange() 233 } 234 235 @Test 236 fun testAddFavorite_notRemoved() { 237 val added = 2 238 val id = "$ID_PREFIX$added" 239 model.changeFavoriteStatus(id, true) 240 241 verifyNoMoreInteractions(adapter) 242 243 verify(callback, never()).onFirstChange() 244 } 245 246 @Test 247 fun testAddOnlyRemovedFavorite_dividerStopsShowing() { 248 val added = 2 249 val id = "$ID_PREFIX$added" 250 model.changeFavoriteStatus(id, false) 251 model.changeFavoriteStatus(id, true) 252 253 assertFalse(dividerWrapper.showDivider) 254 val inOrder = inOrder(adapter) 255 inOrder.verify(adapter).notifyItemChanged(model.elements.size - 1) 256 inOrder.verify(adapter).notifyItemChanged(model.elements.size - 2) 257 258 verify(callback).onFirstChange() 259 } 260 261 @Test 262 fun testAddFirstFavorite_dividerNotShowsNone() { 263 INITIAL_FAVORITES.forEach { 264 model.changeFavoriteStatus(it.controlId, false) 265 } 266 267 verify(callback).onNoneChanged(true) 268 269 model.changeFavoriteStatus("${ID_PREFIX}3", true) 270 assertEquals(1, getDividerPosition()) 271 272 verify(callback).onNoneChanged(false) 273 274 verify(callback).onFirstChange() 275 } 276 277 @Test 278 fun testMoveBetweenFavorites() { 279 val from = 2 280 val to = 4 281 282 model.onMoveItem(from, to) 283 assertEquals( 284 listOf(0, 1, 3, 4, 2, 5).map { "$ID_PREFIX$it" }, 285 model.favorites.map(ControlInfo::controlId) 286 ) 287 verify(adapter).notifyItemMoved(from, to) 288 verify(adapter, never()).notifyItemChanged(anyInt(), any(Any::class.java)) 289 290 verify(callback).onFirstChange() 291 } 292 293 @Test 294 fun testCacheCalledWhenGettingCustomIcon() { 295 val wrapper = model.elements[0] as ControlInfoWrapper 296 wrapper.customIcon 297 298 verify(customIconCache).retrieve(TEST_COMPONENT, wrapper.controlId) 299 } 300 301 private fun getDividerPosition(): Int = model.elements.indexOf(dividerWrapper) 302 }