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.settings.bluetooth.ui.view
18 
19 import android.app.settings.SettingsEnums;
20 import android.bluetooth.BluetoothAdapter
21 import android.content.Context
22 import android.content.Intent
23 import android.graphics.Bitmap
24 import androidx.fragment.app.FragmentActivity
25 import androidx.preference.Preference
26 import androidx.preference.PreferenceManager
27 import androidx.preference.PreferenceScreen
28 import androidx.test.core.app.ApplicationProvider
29 import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel
30 import com.android.settings.bluetooth.ui.model.FragmentTypeModel
31 import com.android.settings.dashboard.DashboardFragment
32 import com.android.settings.testutils.FakeFeatureFactory
33 import com.android.settingslib.bluetooth.CachedBluetoothDevice
34 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
35 import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
36 import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
37 import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
38 import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
39 import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
40 import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
41 import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel
42 import com.android.settingslib.core.AbstractPreferenceController
43 import com.google.common.truth.Truth.assertThat
44 import kotlinx.coroutines.ExperimentalCoroutinesApi
45 import kotlinx.coroutines.delay
46 import kotlinx.coroutines.flow.flowOf
47 import kotlinx.coroutines.flow.launchIn
48 import kotlinx.coroutines.flow.onEach
49 import kotlinx.coroutines.test.TestScope
50 import kotlinx.coroutines.test.runCurrent
51 import kotlinx.coroutines.test.runTest
52 import org.junit.Before
53 import org.junit.Rule
54 import org.junit.Test
55 import org.junit.runner.RunWith
56 import org.mockito.ArgumentMatchers.eq
57 import org.mockito.Mock
58 import org.mockito.Mockito.any
59 import org.mockito.Mockito.verify
60 import org.mockito.Mockito.`when`
61 import org.mockito.junit.MockitoJUnit
62 import org.mockito.junit.MockitoRule
63 import org.robolectric.Robolectric
64 import org.robolectric.RobolectricTestRunner
65 import org.robolectric.shadows.ShadowLooper
66 import org.robolectric.shadows.ShadowLooper.shadowMainLooper
67 
68 
69 @ExperimentalCoroutinesApi
70 @RunWith(RobolectricTestRunner::class)
71 class DeviceDetailsFragmentFormatterTest {
72     @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
73 
74     @Mock private lateinit var cachedDevice: CachedBluetoothDevice
75     @Mock private lateinit var bluetoothAdapter: BluetoothAdapter
76     @Mock private lateinit var repository: DeviceSettingRepository
77     @Mock private lateinit var profileController: AbstractPreferenceController
78     @Mock private lateinit var headerController: AbstractPreferenceController
79     @Mock private lateinit var buttonController: AbstractPreferenceController
80 
81     private lateinit var context: Context
82     private lateinit var fragment: TestFragment
83     private lateinit var underTest: DeviceDetailsFragmentFormatter
84     private lateinit var featureFactory: FakeFeatureFactory
85     private lateinit var fragmentActivity: FragmentActivity
86     private val testScope = TestScope()
87 
88     @Before
setUpnull89     fun setUp() {
90         context = ApplicationProvider.getApplicationContext()
91         featureFactory = FakeFeatureFactory.setupForTest()
92         `when`(
93                 featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
94                     eq(context), eq(bluetoothAdapter), any()))
95             .thenReturn(repository)
96         fragmentActivity = Robolectric.setupActivity(FragmentActivity::class.java)
97         assertThat(fragmentActivity.applicationContext).isNotNull()
98         fragment = TestFragment(context)
99         fragmentActivity.supportFragmentManager.beginTransaction().add(fragment, null).commit()
100         shadowMainLooper().idle()
101 
102         fragment.preferenceScreen.run {
103             addPreference(Preference(context).apply { key = "bluetooth_device_header" })
104             addPreference(Preference(context).apply { key = "action_buttons" })
105             addPreference(Preference(context).apply { key = "bluetooth_profiles" })
106         }
107         `when`(profileController.preferenceKey).thenReturn("bluetooth_profiles")
108         `when`(headerController.preferenceKey).thenReturn("bluetooth_device_header")
109         `when`(buttonController.preferenceKey).thenReturn("action_buttons")
110 
111         underTest =
112             DeviceDetailsFragmentFormatterImpl(
113                 context,
114                 fragment,
115                 listOf(profileController, headerController, buttonController),
116                 bluetoothAdapter,
117                 cachedDevice,
118                 testScope.testScheduler)
119     }
120 
121     @Test
getMenuItem_returnItemnull122     fun getMenuItem_returnItem() {
123         testScope.runTest {
124             `when`(repository.getDeviceSettingsConfig(cachedDevice))
125                 .thenReturn(
126                     DeviceSettingConfigModel(
127                         listOf(), listOf(), DeviceSettingConfigItemModel.AppProvidedItem(12345, false)))
128             val intent = Intent().apply {
129                 setAction(Intent.ACTION_VIEW)
130                 setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
131             }
132             `when`(repository.getDeviceSetting(cachedDevice, 12345))
133                 .thenReturn(
134                     flowOf(
135                         DeviceSettingModel.HelpPreference(
136                             cachedDevice = cachedDevice,
137                             id = 12345,
138                             intent = intent,
139                         )))
140 
141             var helpPreference: DeviceSettingPreferenceModel.HelpPreference? = null
142             underTest.getMenuItem(FragmentTypeModel.DeviceDetailsMoreSettingsFragment).onEach {
143                 helpPreference = it
144             }.launchIn(testScope.backgroundScope)
145             delay(100)
146             runCurrent()
147             ShadowLooper.idleMainLooper()
148 
149             assertThat(helpPreference?.intent).isSameInstanceAs(intent)
150         }
151     }
152 
153     @Test
updateLayout_configIsNull_notChangenull154     fun updateLayout_configIsNull_notChange() {
155         testScope.runTest {
156             `when`(repository.getDeviceSettingsConfig(cachedDevice)).thenReturn(null)
157 
158             underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment)
159 
160             assertThat(getDisplayedPreferences().mapNotNull { it.key })
161                 .containsExactly("bluetooth_device_header", "action_buttons", "bluetooth_profiles")
162         }
163     }
164 
165     @Test
updateLayout_itemsNotInConfig_hidenull166     fun updateLayout_itemsNotInConfig_hide() {
167         testScope.runTest {
168             `when`(repository.getDeviceSettingsConfig(cachedDevice))
169                 .thenReturn(
170                     DeviceSettingConfigModel(
171                         listOf(
172                             DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
173                                 DeviceSettingId.DEVICE_SETTING_ID_HEADER,
174                                 highlighted = false, preferenceKey = "bluetooth_device_header"),
175                             DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
176                                 DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES,
177                                 highlighted = false, preferenceKey = "bluetooth_profiles"),
178                         ),
179                         listOf(),
180                         null))
181 
182             underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment)
183             runCurrent()
184 
185             assertThat(getDisplayedPreferences().mapNotNull { it.key })
186                 .containsExactly("bluetooth_device_header", "bluetooth_profiles")
187             verify(featureFactory.metricsFeatureProvider)
188                 .action(
189                     SettingsEnums.PAGE_UNKNOWN,
190                     SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN,
191                     0,
192                     "bluetooth_device_header", 1)
193             verify(featureFactory.metricsFeatureProvider)
194                 .action(
195                     SettingsEnums.PAGE_UNKNOWN,
196                     SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN,
197                     0,
198                     "bluetooth_profiles", 1)
199         }
200     }
201 
202     @Test
updateLayout_newItems_displayNewItemsnull203     fun updateLayout_newItems_displayNewItems() {
204         testScope.runTest {
205             `when`(repository.getDeviceSettingsConfig(cachedDevice))
206                 .thenReturn(
207                     DeviceSettingConfigModel(
208                         listOf(
209                             DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
210                                 DeviceSettingId.DEVICE_SETTING_ID_HEADER,
211                                 highlighted = false,
212                                 preferenceKey = "bluetooth_device_header"),
213                             DeviceSettingConfigItemModel.AppProvidedItem(
214                                 DeviceSettingId.DEVICE_SETTING_ID_ANC, highlighted = false),
215                             DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
216                                 DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES,
217                                 highlighted = false,
218                                 preferenceKey = "bluetooth_profiles"),
219                         ),
220                         listOf(),
221                         null))
222             `when`(repository.getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_ANC))
223                 .thenReturn(
224                     flowOf(
225                         DeviceSettingModel.MultiTogglePreference(
226                             cachedDevice,
227                             DeviceSettingId.DEVICE_SETTING_ID_ANC,
228                             "title",
229                             toggles =
230                                 listOf(
231                                     ToggleModel(
232                                         "",
233                                         DeviceSettingIcon.BitmapIcon(
234                                             Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)))),
235                             isActive = true,
236                             state = DeviceSettingStateModel.MultiTogglePreferenceState(0),
237                             isAllowedChangingState = true,
238                             updateState = {})))
239 
240             underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment)
241             runCurrent()
242 
243             assertThat(getDisplayedPreferences().mapNotNull { it.key })
244                 .containsExactly(
245                     "bluetooth_device_header",
246                     "DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}",
247                     "bluetooth_profiles")
248             verify(featureFactory.metricsFeatureProvider)
249                 .action(
250                     SettingsEnums.PAGE_UNKNOWN,
251                     SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN,
252                     0,
253                     "DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}", 1
254                 )
255         }
256     }
257 
getDisplayedPreferencesnull258     private fun getDisplayedPreferences(): List<Preference> {
259         val prefs = mutableListOf<Preference>()
260         for (i in 0..<fragment.preferenceScreen.preferenceCount) {
261             prefs.add(fragment.preferenceScreen.getPreference(i))
262         }
263         return prefs
264     }
265 
266     class TestFragment(context: Context) : DashboardFragment() {
267         private val mPreferenceManager: PreferenceManager = PreferenceManager(context)
268 
269         init {
270             mPreferenceManager.setPreferences(mPreferenceManager.createPreferenceScreen(context))
271         }
272 
getPreferenceScreenResIdnull273         public override fun getPreferenceScreenResId(): Int = 0
274 
275         override fun getLogTag(): String = "TestLogTag"
276 
277         override fun getPreferenceScreen(): PreferenceScreen {
278             return mPreferenceManager.preferenceScreen
279         }
280 
getMetricsCategorynull281         override fun getMetricsCategory(): Int = 0
282 
283         override fun getPreferenceManager(): PreferenceManager {
284             return mPreferenceManager
285         }
286     }
287 
288     private companion object {}
289 }
290