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