Rearrange bluetooth device details fragment according to config

BUG: 343317785
Test: atest DeviceDetailsFragmentFormatterTest
Flag: com.android.settings.flags.enable_bluetooth_device_details_polish
Change-Id: I440f85b2c402920e851915ff56fa7b3f5356e807
This commit is contained in:
Haijie Hong
2024-08-09 14:58:10 +08:00
parent 2a01b356ae
commit 41f7c222b6
13 changed files with 949 additions and 21 deletions

View File

@@ -50,6 +50,7 @@ import androidx.fragment.app.FragmentTransaction;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
@@ -101,6 +102,8 @@ public class BluetoothDeviceDetailsFragmentTest {
private InputManager mInputManager;
@Mock
private CompanionDeviceManager mCompanionDeviceManager;
@Mock
private DeviceDetailsFragmentFormatter mFormatter;
@Before
public void setUp() {
@@ -111,7 +114,10 @@ public class BluetoothDeviceDetailsFragmentTest {
.getSystemService(CompanionDeviceManager.class);
when(mCompanionDeviceManager.getAllAssociations()).thenReturn(ImmutableList.of());
removeInputDeviceWithMatchingBluetoothAddress();
FakeFeatureFactory.setupForTest();
FakeFeatureFactory fakeFeatureFactory = FakeFeatureFactory.setupForTest();
when(fakeFeatureFactory.mBluetoothFeatureProvider.getDeviceDetailsFragmentFormatter(any(),
any(), any(), eq(mCachedDevice))).thenReturn(mFormatter);
when(mFormatter.getVisiblePreferenceKeysForMainPage()).thenReturn(null);
mFragment = setupFragment();
mFragment.onAttach(mContext);

View File

@@ -0,0 +1,236 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.bluetooth.ui.view
import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.graphics.Bitmap
import androidx.fragment.app.FragmentActivity
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.PreferenceScreen
import androidx.test.core.app.ApplicationProvider
import com.android.settings.dashboard.DashboardFragment
import com.android.settings.testutils.FakeFeatureFactory
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.shadows.ShadowLooper.shadowMainLooper
@RunWith(RobolectricTestRunner::class)
class DeviceDetailsFragmentFormatterTest {
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
@Mock private lateinit var cachedDevice: CachedBluetoothDevice
@Mock private lateinit var bluetoothAdapter: BluetoothAdapter
@Mock private lateinit var repository: DeviceSettingRepository
private lateinit var fragment: TestFragment
private lateinit var underTest: DeviceDetailsFragmentFormatter
private lateinit var featureFactory: FakeFeatureFactory
private val testScope = TestScope()
@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>()
featureFactory = FakeFeatureFactory.setupForTest()
`when`(
featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
eq(context), eq(bluetoothAdapter), any()))
.thenReturn(repository)
val fragmentActivity = Robolectric.setupActivity(FragmentActivity::class.java)
assertThat(fragmentActivity.applicationContext).isNotNull()
fragment = TestFragment(context)
fragmentActivity.supportFragmentManager.beginTransaction().add(fragment, null).commit()
shadowMainLooper().idle()
fragment.preferenceScreen.run {
addPreference(Preference(context).apply { key = "bluetooth_device_header" })
addPreference(Preference(context).apply { key = "action_buttons" })
addPreference(Preference(context).apply { key = "keyboard_settings" })
}
underTest =
DeviceDetailsFragmentFormatterImpl(context, fragment, bluetoothAdapter, cachedDevice)
}
@Test
fun getVisiblePreferenceKeysForMainPage_hasConfig_returnList() {
testScope.runTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn(
DeviceSettingConfigModel(
listOf(
DeviceSettingConfigItemModel.BuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
"bluetooth_device_header"),
DeviceSettingConfigItemModel.BuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, "action_buttons"),
),
listOf(),
"footer"))
val keys = underTest.getVisiblePreferenceKeysForMainPage()
assertThat(keys).containsExactly("bluetooth_device_header", "action_buttons")
}
}
@Test
fun getVisiblePreferenceKeysForMainPage_noConfig_returnNull() {
testScope.runTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice)).thenReturn(null)
val keys = underTest.getVisiblePreferenceKeysForMainPage()
assertThat(keys).isNull()
}
}
@Test
fun updateLayout_configIsNull_notChange() {
testScope.runTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice)).thenReturn(null)
underTest.updateLayout()
assertThat(getDisplayedPreferences().map { it.key })
.containsExactly("bluetooth_device_header", "action_buttons", "keyboard_settings")
}
}
@Test
fun updateLayout_itemsNotInConfig_hide() {
testScope.runTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn(
DeviceSettingConfigModel(
listOf(
DeviceSettingConfigItemModel.BuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
"bluetooth_device_header"),
DeviceSettingConfigItemModel.BuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
"keyboard_settings"),
),
listOf(),
"footer"))
underTest.updateLayout()
assertThat(getDisplayedPreferences().map { it.key })
.containsExactly("bluetooth_device_header", "keyboard_settings")
}
}
@Test
fun updateLayout_newItems_displayNewItems() {
testScope.runTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn(
DeviceSettingConfigModel(
listOf(
DeviceSettingConfigItemModel.BuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
"bluetooth_device_header"),
DeviceSettingConfigItemModel.AppProvidedItem(
DeviceSettingId.DEVICE_SETTING_ID_ANC),
DeviceSettingConfigItemModel.BuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
"keyboard_settings"),
),
listOf(),
"footer"))
`when`(repository.getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_ANC))
.thenReturn(
flowOf(
DeviceSettingModel.MultiTogglePreference(
cachedDevice,
DeviceSettingId.DEVICE_SETTING_ID_ANC,
"title",
toggles =
listOf(
ToggleModel(
"", Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))),
isActive = true,
state = DeviceSettingStateModel.MultiTogglePreferenceState(0),
isAllowedChangingState = true,
updateState = {})))
underTest.updateLayout()
assertThat(getDisplayedPreferences().map { it.key })
.containsExactly(
"bluetooth_device_header",
"DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}",
"keyboard_settings")
}
}
private fun getDisplayedPreferences(): List<Preference> {
val prefs = mutableListOf<Preference>()
for (i in 0..<fragment.preferenceScreen.preferenceCount) {
prefs.add(fragment.preferenceScreen.getPreference(i))
}
return prefs
}
class TestFragment(context: Context) : DashboardFragment() {
private val mPreferenceManager: PreferenceManager = PreferenceManager(context)
init {
mPreferenceManager.setPreferences(mPreferenceManager.createPreferenceScreen(context))
}
public override fun getPreferenceScreenResId(): Int = 0
override fun getLogTag(): String = "TestLogTag"
override fun getPreferenceScreen(): PreferenceScreen {
return mPreferenceManager.preferenceScreen
}
override fun getMetricsCategory(): Int = 0
override fun getPreferenceManager(): PreferenceManager {
return mPreferenceManager
}
}
private companion object {}
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.bluetooth.ui.viewmodel
import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.graphics.Bitmap
import androidx.test.core.app.ApplicationProvider
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
import com.android.settings.testutils.FakeFeatureFactory
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.robolectric.RobolectricTestRunner
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
class BluetoothDeviceDetailsViewModelTest {
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
@Mock private lateinit var cachedDevice: CachedBluetoothDevice
@Mock private lateinit var bluetoothAdapter: BluetoothAdapter
@Mock private lateinit var repository: DeviceSettingRepository
private lateinit var underTest: BluetoothDeviceDetailsViewModel
private lateinit var featureFactory: FakeFeatureFactory
private val testScope = TestScope()
@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>()
featureFactory = FakeFeatureFactory.setupForTest()
`when`(
featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
eq(context), eq(bluetoothAdapter), any()))
.thenReturn(repository)
underTest = BluetoothDeviceDetailsViewModel(repository, cachedDevice)
}
@Test
fun getItems_returnConfigMainItems() {
testScope.runTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn(
DeviceSettingConfigModel(
listOf(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2), listOf(), "footer"))
val keys = underTest.getItems()
assertThat(keys).containsExactly(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2)
}
}
@Test
fun getLayout_builtinDeviceSettings() {
testScope.runTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn(
DeviceSettingConfigModel(
listOf(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2), listOf(), "footer"))
val layout = underTest.getLayout()!!
assertThat(getLatestLayout(layout))
.isEqualTo(
listOf(
listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER),
listOf(DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS)))
}
}
@Test
fun getLayout_remoteDeviceSettings() {
val remoteSettingId1 = 10001
val remoteSettingId2 = 10002
val remoteSettingId3 = 10003
testScope.runTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn(
DeviceSettingConfigModel(
listOf(
BUILTIN_SETTING_ITEM_1,
buildRemoteSettingItem(remoteSettingId1),
buildRemoteSettingItem(remoteSettingId2),
buildRemoteSettingItem(remoteSettingId3),
),
listOf(),
"footer"))
`when`(repository.getDeviceSetting(cachedDevice, remoteSettingId1))
.thenReturn(flowOf(buildMultiTogglePreference(remoteSettingId1)))
`when`(repository.getDeviceSetting(cachedDevice, remoteSettingId2))
.thenReturn(flowOf(buildMultiTogglePreference(remoteSettingId2)))
`when`(repository.getDeviceSetting(cachedDevice, remoteSettingId3))
.thenReturn(flowOf(buildActionSwitchPreference(remoteSettingId3)))
val layout = underTest.getLayout()!!
assertThat(getLatestLayout(layout))
.isEqualTo(
listOf(
listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER),
listOf(remoteSettingId1, remoteSettingId2),
listOf(remoteSettingId3),
))
}
}
private fun getLatestLayout(layout: DeviceSettingLayout): List<List<Int>> {
var latestLayout = MutableList(layout.rows.size) { emptyList<Int>() }
for (i in layout.rows.indices) {
layout.rows[i]
.settingIds
.onEach { latestLayout[i] = it }
.launchIn(testScope.backgroundScope)
}
testScope.runCurrent()
return latestLayout.filter { !it.isEmpty() }.toList()
}
private fun buildMultiTogglePreference(settingId: Int) =
DeviceSettingModel.MultiTogglePreference(
cachedDevice,
settingId,
"title",
toggles = listOf(ToggleModel("", Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))),
isActive = true,
state = DeviceSettingStateModel.MultiTogglePreferenceState(0),
isAllowedChangingState = true,
updateState = {})
private fun buildActionSwitchPreference(settingId: Int) =
DeviceSettingModel.ActionSwitchPreference(cachedDevice, settingId, "title")
private fun buildRemoteSettingItem(settingId: Int) =
DeviceSettingConfigItemModel.AppProvidedItem(settingId)
private companion object {
val BUILTIN_SETTING_ITEM_1 =
DeviceSettingConfigItemModel.BuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER, "bluetooth_device_header")
val BUILDIN_SETTING_ITEM_2 =
DeviceSettingConfigItemModel.BuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, "action_buttons")
}
}