1 /*
2  * Copyright (C) 2022 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 package com.android.systemui.smartspace
17 
18 import android.app.smartspace.SmartspaceManager
19 import android.app.smartspace.SmartspaceSession
20 import android.app.smartspace.SmartspaceTarget
21 import android.content.Context
22 import android.graphics.drawable.Drawable
23 import android.testing.AndroidTestingRunner
24 import android.testing.TestableLooper
25 import android.view.View
26 import android.view.ViewGroup
27 import android.widget.FrameLayout
28 import androidx.test.filters.SmallTest
29 import com.android.systemui.SysuiTestCase
30 import com.android.systemui.dreams.smartspace.DreamSmartspaceController
31 import com.android.systemui.plugins.BcSmartspaceConfigPlugin
32 import com.android.systemui.plugins.BcSmartspaceDataPlugin
33 import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView
34 import com.android.systemui.plugins.FalsingManager
35 import com.android.systemui.smartspace.dagger.SmartspaceViewComponent
36 import com.android.systemui.util.concurrency.Execution
37 import com.android.systemui.util.mockito.any
38 import com.android.systemui.util.mockito.eq
39 import com.android.systemui.util.mockito.withArgCaptor
40 import com.google.common.truth.Truth.assertThat
41 import java.util.Optional
42 import java.util.concurrent.Executor
43 import org.junit.Before
44 import org.junit.Test
45 import org.junit.runner.RunWith
46 import org.mockito.Mock
47 import org.mockito.Mockito
48 import org.mockito.Mockito.verify
49 import org.mockito.Mockito.`when`
50 import org.mockito.Mockito.anyInt
51 import org.mockito.MockitoAnnotations
52 import org.mockito.Spy
53 
54 @SmallTest
55 @RunWith(AndroidTestingRunner::class)
56 @TestableLooper.RunWithLooper
57 class DreamSmartspaceControllerTest : SysuiTestCase() {
58     @Mock
59     private lateinit var smartspaceManager: SmartspaceManager
60 
61     @Mock
62     private lateinit var execution: Execution
63 
64     @Mock
65     private lateinit var uiExecutor: Executor
66 
67     @Mock
68     private lateinit var viewComponentFactory: SmartspaceViewComponent.Factory
69 
70     @Mock
71     private lateinit var viewComponent: SmartspaceViewComponent
72 
73     @Mock
74     private lateinit var weatherViewComponent: SmartspaceViewComponent
75 
76     @Spy
77     private var weatherSmartspaceView: SmartspaceView = TestView(context)
78 
79     @Mock
80     private lateinit var targetFilter: SmartspaceTargetFilter
81 
82     @Mock
83     private lateinit var plugin: BcSmartspaceDataPlugin
84 
85     @Mock
86     private lateinit var weatherPlugin: BcSmartspaceDataPlugin
87 
88     @Mock
89     private lateinit var precondition: SmartspacePrecondition
90 
91     @Spy
92     private var smartspaceView: SmartspaceView = TestView(context)
93 
94     @Mock
95     private lateinit var listener: BcSmartspaceDataPlugin.SmartspaceTargetListener
96 
97     @Mock
98     private lateinit var session: SmartspaceSession
99 
100     private lateinit var controller: DreamSmartspaceController
101 
102     // TODO(b/272811280): Remove usage of real view
103     private val fakeParent = FrameLayout(context)
104 
105     /**
106      * A class which implements SmartspaceView and extends View. This is mocked to provide the right
107      * object inheritance and interface implementation used in DreamSmartspaceController
108      */
109     private class TestView(context: Context?) : View(context), SmartspaceView {
110         override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {}
111 
112         override fun registerConfigProvider(plugin: BcSmartspaceConfigPlugin?) {}
113 
114         override fun setPrimaryTextColor(color: Int) {}
115 
116         override fun setUiSurface(uiSurface: String) {}
117 
118         override fun setDozeAmount(amount: Float) {}
119 
120         override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) {}
121 
122         override fun setFalsingManager(falsingManager: FalsingManager?) {}
123 
124         override fun setDnd(image: Drawable?, description: String?) {}
125 
126         override fun setNextAlarm(image: Drawable?, description: String?) {}
127 
128         override fun setMediaTarget(target: SmartspaceTarget?) {}
129 
130         override fun getSelectedPage(): Int { return 0; }
131 
132         override fun getCurrentCardTopPadding(): Int { return 0; }
133     }
134 
135     @Before
136     fun setup() {
137         MockitoAnnotations.initMocks(this)
138         `when`(viewComponentFactory.create(any(), eq(plugin), any(), eq(null)))
139                 .thenReturn(viewComponent)
140         `when`(viewComponent.getView()).thenReturn(smartspaceView)
141         `when`(viewComponentFactory.create(any(), eq(weatherPlugin), any(), any()))
142             .thenReturn(weatherViewComponent)
143         `when`(weatherViewComponent.getView()).thenReturn(weatherSmartspaceView)
144         `when`(smartspaceManager.createSmartspaceSession(any())).thenReturn(session)
145 
146         controller = DreamSmartspaceController(context, smartspaceManager, execution, uiExecutor,
147                 viewComponentFactory, precondition, Optional.of(targetFilter), Optional.of(plugin),
148         Optional.of(weatherPlugin))
149     }
150 
151     /**
152      * Ensures smartspace session begins on a listener only flow.
153      */
154     @Test
155     fun testConnectOnListen() {
156         `when`(precondition.conditionsMet()).thenReturn(true)
157         controller.addListener(listener)
158 
159         verify(smartspaceManager).createSmartspaceSession(any())
160 
161         var targetListener = withArgCaptor<SmartspaceSession.OnTargetsAvailableListener> {
162             verify(session).addOnTargetsAvailableListener(any(), capture())
163         }
164 
165         `when`(targetFilter.filterSmartspaceTarget(any())).thenReturn(true)
166 
167         var target = Mockito.mock(SmartspaceTarget::class.java)
168         targetListener.onTargetsAvailable(listOf(target))
169 
170         var targets = withArgCaptor<List<SmartspaceTarget>> {
171             verify(plugin).onTargetsAvailable(capture())
172         }
173 
174         assertThat(targets.contains(target)).isTrue()
175 
176         controller.removeListener(listener)
177 
178         verify(session).close()
179     }
180 
181     /**
182      * Ensures session begins when a view is attached.
183      */
184     @Test
185     fun testConnectOnViewCreate() {
186         `when`(precondition.conditionsMet()).thenReturn(true)
187         controller.buildAndConnectView(Mockito.mock(ViewGroup::class.java))
188 
189         val stateChangeListener = withArgCaptor<View.OnAttachStateChangeListener> {
190             verify(viewComponentFactory).create(any(), eq(plugin), capture(), eq(null))
191         }
192 
193         val mockView = Mockito.mock(TestView::class.java)
194         `when`(precondition.conditionsMet()).thenReturn(true)
195         stateChangeListener.onViewAttachedToWindow(mockView)
196 
197         verify(smartspaceManager).createSmartspaceSession(any())
198         verify(mockView).setDozeAmount(0f)
199 
200         stateChangeListener.onViewDetachedFromWindow(mockView)
201 
202         verify(session).close()
203     }
204 
205     /**
206      * Ensures session is created when weather smartspace view is created and attached.
207      */
208     @Test
209     fun testConnectOnWeatherViewCreate() {
210         `when`(precondition.conditionsMet()).thenReturn(true)
211 
212         val customView = Mockito.mock(TestView::class.java)
213         val weatherView = controller.buildAndConnectWeatherView(fakeParent, customView)
214         val weatherSmartspaceView = weatherView as SmartspaceView
215         fakeParent.addView(weatherView)
216 
217         // Then weather view is created with custom view and the default weatherPlugin.getView
218         // should not be called
219         verify(viewComponentFactory).create(eq(fakeParent), eq(weatherPlugin), any(),
220             eq(customView))
221         verify(weatherPlugin, Mockito.never()).getView(fakeParent)
222 
223         // And then session is created
224         controller.stateChangeListener.onViewAttachedToWindow(weatherView)
225         verify(smartspaceManager).createSmartspaceSession(any())
226         verify(weatherSmartspaceView).setPrimaryTextColor(anyInt())
227         verify(weatherSmartspaceView).setDozeAmount(0f)
228     }
229 
230     /**
231      * Ensures weather plugin registers target listener when it is added from the controller.
232      */
233     @Test
234     fun testAddListenerInController_registersListenerForWeatherPlugin() {
235         val customView = Mockito.mock(TestView::class.java)
236         `when`(precondition.conditionsMet()).thenReturn(true)
237 
238         // Given a session is created
239         val weatherView =
240             checkNotNull(controller.buildAndConnectWeatherView(fakeParent, customView))
241         controller.stateChangeListener.onViewAttachedToWindow(weatherView)
242         verify(smartspaceManager).createSmartspaceSession(any())
243 
244         // When a listener is added
245         controller.addListenerForWeatherPlugin(listener)
246 
247         // Then the listener is registered to the weather plugin only
248         verify(weatherPlugin).registerListener(listener)
249         verify(plugin, Mockito.never()).registerListener(any())
250     }
251 
252     /**
253      * Ensures session is closed and weather plugin unregisters the notifier when weather smartspace
254      * view is detached.
255      */
256     @Test
257     fun testDisconnect_emitsEmptyListAndRemovesNotifier() {
258         `when`(precondition.conditionsMet()).thenReturn(true)
259 
260         // Given a session is created
261         val customView = Mockito.mock(TestView::class.java)
262         val weatherView =
263             checkNotNull(controller.buildAndConnectWeatherView(fakeParent, customView))
264         controller.stateChangeListener.onViewAttachedToWindow(weatherView)
265         verify(smartspaceManager).createSmartspaceSession(any())
266 
267         // When view is detached
268         controller.stateChangeListener.onViewDetachedFromWindow(weatherView)
269         // Then the session is closed
270         verify(session).close()
271 
272         // And the listener receives an empty list of targets and unregisters the notifier
273         verify(weatherPlugin).onTargetsAvailable(emptyList())
274         verify(weatherPlugin).registerSmartspaceEventNotifier(null)
275     }
276 }
277