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 }