1 /*
2  * Copyright (C) 2024 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.bluetooth.qsdialog
18 
19 import android.graphics.drawable.Drawable
20 import android.testing.TestableLooper
21 import android.view.LayoutInflater
22 import android.view.View
23 import android.view.View.GONE
24 import android.view.View.VISIBLE
25 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
26 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
27 import androidx.recyclerview.widget.LinearLayoutManager
28 import androidx.recyclerview.widget.RecyclerView
29 import androidx.test.ext.junit.runners.AndroidJUnit4
30 import androidx.test.filters.SmallTest
31 import com.android.internal.logging.UiEventLogger
32 import com.android.settingslib.bluetooth.CachedBluetoothDevice
33 import com.android.systemui.SysuiTestCase
34 import com.android.systemui.animation.DialogTransitionAnimator
35 import com.android.systemui.model.SysUiState
36 import com.android.systemui.res.R
37 import com.android.systemui.statusbar.phone.SystemUIDialog
38 import com.android.systemui.statusbar.phone.SystemUIDialogManager
39 import com.android.systemui.util.mockito.any
40 import com.android.systemui.util.mockito.whenever
41 import com.android.systemui.util.time.FakeSystemClock
42 import com.google.common.truth.Truth.assertThat
43 import kotlinx.coroutines.CoroutineDispatcher
44 import kotlinx.coroutines.test.TestCoroutineScheduler
45 import kotlinx.coroutines.test.TestScope
46 import kotlinx.coroutines.test.UnconfinedTestDispatcher
47 import kotlinx.coroutines.test.runTest
48 import org.junit.Before
49 import org.junit.Rule
50 import org.junit.Test
51 import org.junit.runner.RunWith
52 import org.mockito.ArgumentMatchers.anyBoolean
53 import org.mockito.ArgumentMatchers.anyLong
54 import org.mockito.Mock
55 import org.mockito.Mockito.`when`
56 import org.mockito.junit.MockitoJUnit
57 import org.mockito.junit.MockitoRule
58 
59 @SmallTest
60 @RunWith(AndroidJUnit4::class)
61 @TestableLooper.RunWithLooper(setAsMainLooper = true)
62 class BluetoothTileDialogDelegateTest : SysuiTestCase() {
63     companion object {
64         const val DEVICE_NAME = "device"
65         const val DEVICE_CONNECTION_SUMMARY = "active"
66         const val ENABLED = true
67         const val CONTENT_HEIGHT = WRAP_CONTENT
68     }
69 
70     @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
71 
72     @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice
73 
74     @Mock private lateinit var bluetoothTileDialogCallback: BluetoothTileDialogCallback
75 
76     @Mock private lateinit var drawable: Drawable
77 
78     @Mock private lateinit var uiEventLogger: UiEventLogger
79 
80     @Mock private lateinit var logger: BluetoothTileDialogLogger
81 
82     private val uiProperties =
83         BluetoothTileDialogViewModel.UiProperties.build(
84             isBluetoothEnabled = ENABLED,
85             isAutoOnToggleFeatureAvailable = ENABLED
86         )
87     @Mock private lateinit var sysuiDialogFactory: SystemUIDialog.Factory
88     @Mock private lateinit var dialogManager: SystemUIDialogManager
89     @Mock private lateinit var sysuiState: SysUiState
90     @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator
91 
92     private val fakeSystemClock = FakeSystemClock()
93 
94     private lateinit var scheduler: TestCoroutineScheduler
95     private lateinit var dispatcher: CoroutineDispatcher
96     private lateinit var testScope: TestScope
97     private lateinit var icon: Pair<Drawable, String>
98     private lateinit var mBluetoothTileDialogDelegate: BluetoothTileDialogDelegate
99     private lateinit var deviceItem: DeviceItem
100 
101     @Before
setUpnull102     fun setUp() {
103         scheduler = TestCoroutineScheduler()
104         dispatcher = UnconfinedTestDispatcher(scheduler)
105         testScope = TestScope(dispatcher)
106 
107         whenever(sysuiState.setFlag(anyLong(), anyBoolean())).thenReturn(sysuiState)
108 
109         mBluetoothTileDialogDelegate =
110             BluetoothTileDialogDelegate(
111                 uiProperties,
112                 CONTENT_HEIGHT,
113                 bluetoothTileDialogCallback,
114                 {},
115                 dispatcher,
116                 fakeSystemClock,
117                 uiEventLogger,
118                 logger,
119                 sysuiDialogFactory
120             )
121 
122         whenever(sysuiDialogFactory.create(any(SystemUIDialog.Delegate::class.java))).thenAnswer {
123             SystemUIDialog(
124                 mContext,
125                 0,
126                 SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK,
127                 dialogManager,
128                 sysuiState,
129                 fakeBroadcastDispatcher,
130                 dialogTransitionAnimator,
131                 it.getArgument(0)
132             )
133         }
134 
135         icon = Pair(drawable, DEVICE_NAME)
136         deviceItem =
137             DeviceItem(
138                 type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
139                 cachedBluetoothDevice = cachedBluetoothDevice,
140                 deviceName = DEVICE_NAME,
141                 connectionSummary = DEVICE_CONNECTION_SUMMARY,
142                 iconWithDescription = icon,
143                 background = null
144             )
145         `when`(cachedBluetoothDevice.isBusy).thenReturn(false)
146     }
147 
148     @Test
testShowDialog_createRecyclerViewWithAdapternull149     fun testShowDialog_createRecyclerViewWithAdapter() {
150         val dialog = mBluetoothTileDialogDelegate.createDialog()
151         dialog.show()
152 
153         val recyclerView = dialog.requireViewById<RecyclerView>(R.id.device_list)
154 
155         assertThat(recyclerView).isNotNull()
156         assertThat(recyclerView.visibility).isEqualTo(VISIBLE)
157         assertThat(recyclerView.adapter).isNotNull()
158         assertThat(recyclerView.layoutManager is LinearLayoutManager).isTrue()
159         dialog.dismiss()
160     }
161 
162     @Test
testShowDialog_displayBluetoothDevicenull163     fun testShowDialog_displayBluetoothDevice() {
164         testScope.runTest {
165             val dialog = mBluetoothTileDialogDelegate.createDialog()
166             dialog.show()
167             fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
168             mBluetoothTileDialogDelegate.onDeviceItemUpdated(
169                 dialog,
170                 listOf(deviceItem),
171                 showSeeAll = false,
172                 showPairNewDevice = false
173             )
174 
175             val recyclerView = dialog.requireViewById<RecyclerView>(R.id.device_list)
176             val adapter = recyclerView?.adapter as BluetoothTileDialogDelegate.Adapter
177             assertThat(adapter.itemCount).isEqualTo(1)
178             assertThat(adapter.getItem(0).deviceName).isEqualTo(DEVICE_NAME)
179             assertThat(adapter.getItem(0).connectionSummary).isEqualTo(DEVICE_CONNECTION_SUMMARY)
180             assertThat(adapter.getItem(0).iconWithDescription).isEqualTo(icon)
181             dialog.dismiss()
182         }
183     }
184 
185     @Test
testDeviceItemViewHolder_cachedDeviceNotBusynull186     fun testDeviceItemViewHolder_cachedDeviceNotBusy() {
187         deviceItem.isEnabled = true
188 
189         val view =
190             LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
191         val viewHolder =
192             mBluetoothTileDialogDelegate
193                 .Adapter(bluetoothTileDialogCallback)
194                 .DeviceItemViewHolder(view)
195         viewHolder.bind(deviceItem, bluetoothTileDialogCallback)
196         val container = view.requireViewById<View>(R.id.bluetooth_device_row)
197 
198         assertThat(container).isNotNull()
199         assertThat(container.isEnabled).isTrue()
200         assertThat(container.hasOnClickListeners()).isTrue()
201     }
202 
203     @Test
testDeviceItemViewHolder_cachedDeviceBusynull204     fun testDeviceItemViewHolder_cachedDeviceBusy() {
205         deviceItem.isEnabled = false
206 
207         val view =
208             LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
209         val viewHolder =
210             BluetoothTileDialogDelegate(
211                     uiProperties,
212                     CONTENT_HEIGHT,
213                     bluetoothTileDialogCallback,
214                     {},
215                     dispatcher,
216                     fakeSystemClock,
217                     uiEventLogger,
218                     logger,
219                     sysuiDialogFactory,
220                 )
221                 .Adapter(bluetoothTileDialogCallback)
222                 .DeviceItemViewHolder(view)
223         viewHolder.bind(deviceItem, bluetoothTileDialogCallback)
224         val container = view.requireViewById<View>(R.id.bluetooth_device_row)
225 
226         assertThat(container).isNotNull()
227         assertThat(container.isEnabled).isFalse()
228         assertThat(container.hasOnClickListeners()).isTrue()
229     }
230 
231     @Test
testOnDeviceUpdated_hideSeeAll_showPairNewnull232     fun testOnDeviceUpdated_hideSeeAll_showPairNew() {
233         testScope.runTest {
234             val dialog = mBluetoothTileDialogDelegate.createDialog()
235             dialog.show()
236             fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
237             mBluetoothTileDialogDelegate.onDeviceItemUpdated(
238                 dialog,
239                 listOf(deviceItem),
240                 showSeeAll = false,
241                 showPairNewDevice = true
242             )
243 
244             val seeAllButton = dialog.requireViewById<View>(R.id.see_all_button)
245             val pairNewButton = dialog.requireViewById<View>(R.id.pair_new_device_button)
246             val recyclerView = dialog.requireViewById<RecyclerView>(R.id.device_list)
247             val adapter = recyclerView?.adapter as BluetoothTileDialogDelegate.Adapter
248             val scrollViewContent = dialog.requireViewById<View>(R.id.scroll_view)
249 
250             assertThat(seeAllButton).isNotNull()
251             assertThat(seeAllButton.visibility).isEqualTo(GONE)
252             assertThat(pairNewButton).isNotNull()
253             assertThat(pairNewButton.visibility).isEqualTo(VISIBLE)
254             assertThat(adapter.itemCount).isEqualTo(1)
255             assertThat(scrollViewContent.layoutParams.height).isEqualTo(WRAP_CONTENT)
256             dialog.dismiss()
257         }
258     }
259 
260     @Test
testShowDialog_cachedHeightLargerThanMinHeight_displayFromCachedHeightnull261     fun testShowDialog_cachedHeightLargerThanMinHeight_displayFromCachedHeight() {
262         testScope.runTest {
263             val cachedHeight = Int.MAX_VALUE
264             val dialog =
265                 BluetoothTileDialogDelegate(
266                         BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
267                         cachedHeight,
268                         bluetoothTileDialogCallback,
269                         {},
270                         dispatcher,
271                         fakeSystemClock,
272                         uiEventLogger,
273                         logger,
274                         sysuiDialogFactory,
275                     )
276                     .createDialog()
277             dialog.show()
278             assertThat(dialog.requireViewById<View>(R.id.scroll_view).layoutParams.height)
279                 .isEqualTo(cachedHeight)
280             dialog.dismiss()
281         }
282     }
283 
284     @Test
testShowDialog_cachedHeightLessThanMinHeight_displayFromUiPropertiesnull285     fun testShowDialog_cachedHeightLessThanMinHeight_displayFromUiProperties() {
286         testScope.runTest {
287             val dialog =
288                 BluetoothTileDialogDelegate(
289                         BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
290                         MATCH_PARENT,
291                         bluetoothTileDialogCallback,
292                         {},
293                         dispatcher,
294                         fakeSystemClock,
295                         uiEventLogger,
296                         logger,
297                         sysuiDialogFactory,
298                     )
299                     .createDialog()
300             dialog.show()
301             assertThat(dialog.requireViewById<View>(R.id.scroll_view).layoutParams.height)
302                 .isGreaterThan(MATCH_PARENT)
303             dialog.dismiss()
304         }
305     }
306 
307     @Test
testShowDialog_bluetoothEnabled_autoOnToggleGonenull308     fun testShowDialog_bluetoothEnabled_autoOnToggleGone() {
309         testScope.runTest {
310             val dialog =
311                 BluetoothTileDialogDelegate(
312                         BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
313                         MATCH_PARENT,
314                         bluetoothTileDialogCallback,
315                         {},
316                         dispatcher,
317                         fakeSystemClock,
318                         uiEventLogger,
319                         logger,
320                         sysuiDialogFactory,
321                     )
322                     .createDialog()
323             dialog.show()
324             assertThat(
325                     dialog.requireViewById<View>(R.id.bluetooth_auto_on_toggle_layout).visibility
326                 )
327                 .isEqualTo(GONE)
328             dialog.dismiss()
329         }
330     }
331 }
332