Add loading screen for Device details fragment to avoid ANR

BUG: 343317785
Test: local tested
Flag: com.android.settings.flags.enable_bluetooth_device_details_polish
Change-Id: Iad57fc2fe4cb0a3f90e8d01310b9c7ad20d02233
This commit is contained in:
Haijie Hong
2024-11-28 23:25:00 +08:00
parent 9e3f075c3a
commit 67ac0faf3d
9 changed files with 186 additions and 197 deletions

View File

@@ -27,7 +27,6 @@ import android.sysprop.BluetoothProperties;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
@@ -109,28 +108,34 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
PreferenceFragmentCompat fragment,
LocalBluetoothManager manager,
CachedBluetoothDevice device,
Lifecycle lifecycle,
@Nullable List<String> invisibleProfiles,
boolean hasExtraSpace) {
Lifecycle lifecycle) {
super(context, fragment, device, lifecycle);
mManager = manager;
mProfileManager = mManager.getProfileManager();
mCachedDevice = device;
mCachedDeviceGroup = Utils.findAllCachedBluetoothDevicesByGroupId(mManager, mCachedDevice);
}
/** Sets the profiles to be hidden. */
public void setInvisibleProfiles(List<String> invisibleProfiles) {
if (invisibleProfiles != null) {
mInvisibleProfiles = Set.copyOf(invisibleProfiles);
}
mHasExtraSpace = hasExtraSpace;
}
/** Sets whether it should show an extra padding on top of the preference. */
public void setHasExtraSpace(boolean hasExtraSpace) {
if (hasExtraSpace) {
mProfilesContainer.setLayoutResource(R.layout.preference_bluetooth_profile_category);
} else {
mProfilesContainer.setLayoutResource(R.layout.preference_category_bluetooth_no_padding);
}
}
@Override
protected void init(PreferenceScreen screen) {
mProfilesContainer = (PreferenceCategory) screen.findPreference(getPreferenceKey());
if (mHasExtraSpace) {
mProfilesContainer.setLayoutResource(R.layout.preference_bluetooth_profile_category);
} else {
mProfilesContainer.setLayoutResource(R.layout.preference_category_bluetooth_no_padding);
}
// Call refresh here even though it will get called later in onResume, to avoid the
// list of switches appearing to "pop" into the page.
refresh();

View File

@@ -61,7 +61,6 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import java.util.ArrayList;
import java.util.List;
@@ -289,9 +288,12 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
getController(
SlicePreferenceController.class,
controller -> {
if (getPreferenceScreen().findPreference(controller.getPreferenceKey())
!= null) {
controller.setSliceUri(finalControlUri);
controller.onStart();
controller.displayPreference(getPreferenceScreen());
}
});
// Temporarily fix the issue that the page will be automatically scrolled to a wrong
@@ -352,9 +354,23 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
}
@Override
public void onCreatePreferences(@NonNull Bundle savedInstanceState, @NonNull String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (Flags.enableBluetoothDeviceDetailsPolish()) {
if (mFormatter == null) {
List<AbstractPreferenceController> controllers = getPreferenceControllers().stream()
.flatMap(List::stream)
.toList();
mFormatter =
FeatureFactory.getFeatureFactory()
.getBluetoothFeatureProvider()
.getDeviceDetailsFragmentFormatter(
requireContext(),
this,
mBluetoothAdapter,
mCachedDevice,
controllers);
}
mFormatter.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment.INSTANCE);
}
}
@@ -409,38 +425,8 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
return super.onOptionsItemSelected(menuItem);
}
@Override
protected void addPreferenceController(AbstractPreferenceController controller) {
if (Flags.enableBluetoothDeviceDetailsPolish()) {
List<String> keys =
mFormatter.getVisiblePreferenceKeys(
FragmentTypeModel.DeviceDetailsMainFragment.INSTANCE);
Lifecycle lifecycle = getSettingsLifecycle();
if (keys == null || keys.contains(controller.getPreferenceKey())) {
super.addPreferenceController(controller);
} else if (controller instanceof LifecycleObserver) {
lifecycle.removeObserver((LifecycleObserver) controller);
}
} else {
super.addPreferenceController(controller);
}
}
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
List<String> invisibleProfiles = List.of();
if (Flags.enableBluetoothDeviceDetailsPolish()) {
if (mFormatter == null) {
mFormatter =
FeatureFactory.getFeatureFactory()
.getBluetoothFeatureProvider()
.getDeviceDetailsFragmentFormatter(
requireContext(), this, mBluetoothAdapter, mCachedDevice);
}
invisibleProfiles =
mFormatter.getInvisibleBluetoothProfiles(
FragmentTypeModel.DeviceDetailsMainFragment.INSTANCE);
}
ArrayList<AbstractPreferenceController> controllers = new ArrayList<>();
if (mCachedDevice != null) {
@@ -459,7 +445,7 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
controllers.add(new BluetoothDetailsSpatialAudioController(context, this, mCachedDevice,
lifecycle));
controllers.add(new BluetoothDetailsProfilesController(context, this, mManager,
mCachedDevice, lifecycle, invisibleProfiles, invisibleProfiles == null));
mCachedDevice, lifecycle));
controllers.add(new BluetoothDetailsMacAddressController(context, this, mCachedDevice,
lifecycle));
controllers.add(new StylusDevicesController(context, mInputDevice, mCachedDevice,

View File

@@ -26,10 +26,11 @@ import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.preference.Preference;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository;
import com.android.settingslib.core.AbstractPreferenceController;
import kotlinx.coroutines.CoroutineScope;
@@ -100,7 +101,8 @@ public interface BluetoothFeatureProvider {
@NonNull
DeviceDetailsFragmentFormatter getDeviceDetailsFragmentFormatter(
@NonNull Context context,
@NonNull SettingsPreferenceFragment fragment,
@NonNull DashboardFragment fragment,
@NonNull BluetoothAdapter bluetoothAdapter,
@NonNull CachedBluetoothDevice cachedDevice);
@NonNull CachedBluetoothDevice cachedDevice,
@NonNull List<AbstractPreferenceController> controllers);
}

View File

@@ -23,13 +23,14 @@ import android.media.AudioManager
import android.media.Spatializer
import android.net.Uri
import androidx.preference.Preference
import com.android.settings.SettingsPreferenceFragment
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatterImpl
import com.android.settings.dashboard.DashboardFragment
import com.android.settingslib.bluetooth.BluetoothUtils
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepositoryImpl
import com.android.settingslib.core.AbstractPreferenceController
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import kotlinx.coroutines.CoroutineScope
@@ -78,13 +79,15 @@ open class BluetoothFeatureProviderImpl : BluetoothFeatureProvider {
override fun getDeviceDetailsFragmentFormatter(
context: Context,
fragment: SettingsPreferenceFragment,
fragment: DashboardFragment,
bluetoothAdapter: BluetoothAdapter,
cachedDevice: CachedBluetoothDevice
cachedDevice: CachedBluetoothDevice,
controllers: List<AbstractPreferenceController>,
): DeviceDetailsFragmentFormatter {
return DeviceDetailsFragmentFormatterImpl(
context,
fragment,
controllers,
bluetoothAdapter,
cachedDevice,
Dispatchers.IO

View File

@@ -45,7 +45,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import com.android.settings.R
import com.android.settings.SettingsPreferenceFragment
import com.android.settings.bluetooth.BlockingPrefWithSliceController
import com.android.settings.bluetooth.BluetoothDetailsProfilesController
import com.android.settings.bluetooth.ui.composable.Icon
import com.android.settings.bluetooth.ui.composable.MultiTogglePreference
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
@@ -54,12 +55,18 @@ import com.android.settings.bluetooth.ui.model.FragmentTypeModel
import com.android.settings.bluetooth.ui.view.DeviceDetailsMoreSettingsFragment.Companion.KEY_DEVICE_ADDRESS
import com.android.settings.bluetooth.ui.viewmodel.BluetoothDeviceDetailsViewModel
import com.android.settings.core.SubSettingLauncher
import com.android.settings.dashboard.DashboardFragment
import com.android.settings.overlay.FeatureFactory
import com.android.settings.spa.preference.ComposePreference
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingActionModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
import com.android.settingslib.core.AbstractPreferenceController
import com.android.settingslib.core.lifecycle.LifecycleObserver
import com.android.settingslib.core.lifecycle.events.OnPause
import com.android.settingslib.core.lifecycle.events.OnStop
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.widget.preference.Preference as SpaPreference
import com.android.settingslib.spa.widget.preference.PreferenceModel
@@ -81,16 +88,10 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.launch
/** Handles device details fragment layout according to config. */
interface DeviceDetailsFragmentFormatter {
/** Gets keys of visible preferences in built-in preference in xml. */
fun getVisiblePreferenceKeys(fragmentType: FragmentTypeModel): List<String>?
/** Updates device details fragment layout. */
fun getInvisibleBluetoothProfiles(fragmentType: FragmentTypeModel): List<String>?
/** Updates device details fragment layout. */
fun updateLayout(fragmentType: FragmentTypeModel)
@@ -104,7 +105,8 @@ interface DeviceDetailsFragmentFormatter {
@OptIn(ExperimentalCoroutinesApi::class)
class DeviceDetailsFragmentFormatterImpl(
private val context: Context,
private val fragment: SettingsPreferenceFragment,
private val fragment: DashboardFragment,
controllers: List<AbstractPreferenceController>,
private val bluetoothAdapter: BluetoothAdapter,
private val cachedDevice: CachedBluetoothDevice,
private val backgroundCoroutineContext: CoroutineContext,
@@ -112,6 +114,9 @@ class DeviceDetailsFragmentFormatterImpl(
private val metricsFeatureProvider = FeatureFactory.featureFactory.metricsFeatureProvider
private val prefVisibility = mutableMapOf<String, MutableStateFlow<Boolean>>()
private val prefVisibilityJobs = mutableListOf<Job>()
private var isLoading = false
private var prefKeyToController: Map<String, AbstractPreferenceController> =
controllers.associateBy { it.preferenceKey }
private val viewModel: BluetoothDeviceDetailsViewModel =
ViewModelProvider(
@@ -125,27 +130,16 @@ class DeviceDetailsFragmentFormatterImpl(
)
.get(BluetoothDeviceDetailsViewModel::class.java)
override fun getVisiblePreferenceKeys(fragmentType: FragmentTypeModel): List<String>? =
runBlocking {
viewModel
.getItems(fragmentType)
?.filterIsInstance<DeviceSettingConfigItemModel.BuiltinItem>()
?.map { it.preferenceKey }
}
override fun getInvisibleBluetoothProfiles(fragmentType: FragmentTypeModel): List<String>? =
runBlocking {
viewModel
.getItems(fragmentType)
?.filterIsInstance<DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem>()
?.firstOrNull()
?.invisibleProfiles
}
/** Updates bluetooth device details fragment layout. */
override fun updateLayout(fragmentType: FragmentTypeModel) = runBlocking {
val items = viewModel.getItems(fragmentType) ?: return@runBlocking
val layout = viewModel.getLayout(fragmentType) ?: return@runBlocking
override fun updateLayout(fragmentType: FragmentTypeModel) {
fragment.setLoading(true, false)
isLoading = true
fragment.lifecycleScope.launch { updateLayoutInternal(fragmentType) }
}
private suspend fun updateLayoutInternal(fragmentType: FragmentTypeModel) {
val items = viewModel.getItems(fragmentType) ?: return
val layout = viewModel.getLayout(fragmentType) ?: return
val prefKeyToSettingId =
items
@@ -156,21 +150,21 @@ class DeviceDetailsFragmentFormatterImpl(
for (i in 0 until fragment.preferenceScreen.preferenceCount) {
val pref = fragment.preferenceScreen.getPreference(i)
prefKeyToSettingId[pref.key]?.let { id -> settingIdToXmlPreferences[id] = pref }
if (pref.key !in prefKeyToSettingId) {
getController(pref.key)?.let { disableController(it) }
}
}
fragment.preferenceScreen.removeAll()
for (job in prefVisibilityJobs) {
job.cancel()
}
prefVisibilityJobs.clear()
for (row in items.indices) {
val settingId = items[row].settingId
val settingItem = items[row]
val settingId = settingItem.settingId
if (settingIdToXmlPreferences.containsKey(settingId)) {
fragment.preferenceScreen.addPreference(
settingIdToXmlPreferences[settingId]!!
.apply { order = row }
.also { logItemShown(it.key, it.isVisible) }
)
val pref = settingIdToXmlPreferences[settingId]!!.apply { order = row }
fragment.preferenceScreen.addPreference(pref)
} else {
val prefKey = getPreferenceKey(settingId)
prefVisibilityJobs.add(
@@ -195,6 +189,29 @@ class DeviceDetailsFragmentFormatterImpl(
isSelectable = false
setContent { Spacer(modifier = Modifier.height(1.dp)) }
})
for (row in items.indices) {
val settingItem = items[row]
val settingId = settingItem.settingId
if (settingIdToXmlPreferences.containsKey(settingId)) {
val pref = fragment.preferenceScreen.getPreference(row)
if (settingId == DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES) {
(getController(pref.key) as? BluetoothDetailsProfilesController)?.run {
if (settingItem is DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem) {
setInvisibleProfiles(settingItem.invisibleProfiles)
setHasExtraSpace(false)
}
}
}
getController(pref.key)?.displayPreference(fragment.preferenceScreen)
logItemShown(pref.key, pref.isVisible)
}
}
if (isLoading) {
fragment.setLoading(false, false)
isLoading = false
}
}
override fun getMenuItem(
@@ -454,6 +471,29 @@ class DeviceDetailsFragmentFormatterImpl(
}
}
private fun getController(key: String): AbstractPreferenceController? {
return prefKeyToController[key]
}
private fun disableController(controller: AbstractPreferenceController) {
if (controller is LifecycleObserver) {
fragment.settingsLifecycle.removeObserver(controller as LifecycleObserver)
}
if (controller is BlockingPrefWithSliceController) {
// Make UiBlockListener finished, otherwise UI will flicker.
controller.onChanged(null)
}
if (controller is OnPause) {
(controller as OnPause).onPause()
}
if (controller is OnStop) {
(controller as OnStop).onStop()
}
}
private fun getPreferenceKey(settingId: Int) = "DEVICE_SETTING_${settingId}"
private companion object {

View File

@@ -25,6 +25,7 @@ import android.graphics.PorterDuff
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.lifecycle.lifecycleScope
import com.android.settings.R
import com.android.settings.bluetooth.BluetoothDetailsAudioDeviceTypeController
@@ -33,12 +34,12 @@ import com.android.settings.bluetooth.Utils
import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel
import com.android.settings.bluetooth.ui.model.FragmentTypeModel
import com.android.settings.dashboard.DashboardFragment
import com.android.settings.flags.Flags
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
import com.android.settingslib.core.AbstractPreferenceController
import com.android.settingslib.core.lifecycle.LifecycleObserver
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
@@ -88,17 +89,29 @@ class DeviceDetailsMoreSettingsFragment : DashboardFragment() {
return R.xml.bluetooth_device_more_settings_fragment
}
override fun addPreferenceController(controller: AbstractPreferenceController) {
val keys: List<String>? =
formatter.getVisiblePreferenceKeys(FragmentTypeModel.DeviceDetailsMoreSettingsFragment)
val lifecycle = settingsLifecycle
if (keys == null || keys.contains(controller.preferenceKey)) {
super.addPreferenceController(controller)
} else if (controller is LifecycleObserver) {
lifecycle.removeObserver((controller as LifecycleObserver))
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (!this::formatter.isInitialized) {
val controllers = preferenceControllers.stream()
.flatMap { obj: List<AbstractPreferenceController?> -> obj.stream() }
.toList()
val bluetoothManager = requireContext().getSystemService(BluetoothManager::class.java)
formatter =
featureFactory
.bluetoothFeatureProvider
.getDeviceDetailsFragmentFormatter(
requireContext(), this, bluetoothManager.adapter, cachedDevice, controllers
)
}
formatter.updateLayout(FragmentTypeModel.DeviceDetailsMoreSettingsFragment)
helpItem =
formatter
.getMenuItem(FragmentTypeModel.DeviceDetailsMoreSettingsFragment)
.stateIn(lifecycleScope, SharingStarted.WhileSubscribed(), initialValue = null)
}
private fun getCachedDevice(): CachedBluetoothDevice? {
val bluetoothAddress = arguments?.getString(KEY_DEVICE_ADDRESS) ?: return null
localBluetoothManager = Utils.getLocalBtManager(context) ?: return null
@@ -107,32 +120,13 @@ class DeviceDetailsMoreSettingsFragment : DashboardFragment() {
return Utils.getLocalBtManager(context).cachedDeviceManager.findDevice(remoteDevice)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
super.onCreatePreferences(savedInstanceState, rootKey)
formatter.updateLayout(FragmentTypeModel.DeviceDetailsMoreSettingsFragment)
}
override fun createPreferenceControllers(context: Context): List<AbstractPreferenceController> {
val bluetoothManager = context.getSystemService(BluetoothManager::class.java)
cachedDevice =
getCachedDevice()
?: run {
finish()
return emptyList()
}
if (!this::formatter.isInitialized) {
formatter =
featureFactory.bluetoothFeatureProvider.getDeviceDetailsFragmentFormatter(
requireContext(),
this,
bluetoothManager.adapter,
cachedDevice,
)
}
helpItem =
formatter
.getMenuItem(FragmentTypeModel.DeviceDetailsMoreSettingsFragment)
.stateIn(lifecycleScope, SharingStarted.WhileSubscribed(), initialValue = null)
return listOf(
BluetoothDetailsProfilesController(
context,
@@ -140,10 +134,6 @@ class DeviceDetailsMoreSettingsFragment : DashboardFragment() {
localBluetoothManager,
cachedDevice,
settingsLifecycle,
formatter.getInvisibleBluetoothProfiles(
FragmentTypeModel.DeviceDetailsMoreSettingsFragment
),
false,
),
BluetoothDetailsAudioDeviceTypeController(
context,

View File

@@ -120,7 +120,12 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
.thenAnswer(invocation -> ImmutableList.of(mConnectableProfiles));
setupDevice(mDeviceConfig);
initController(List.of());
mController = new BluetoothDetailsProfilesController(mContext, mFragment, mLocalManager,
mCachedDevice, mLifecycle);
mProfiles.setKey(mController.getPreferenceKey());
mController.mProfilesContainer = mProfiles;
mScreen.removeAll();
mScreen.addPreference(mProfiles);
BluetoothProperties.le_audio_allow_list(Lists.newArrayList(LE_DEVICE_MODEL));
}
@@ -550,7 +555,8 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
@Test
public void prefKeyInBlockingList_hideToggle() {
initController(List.of("A2DP"));
mController.setInvisibleProfiles(List.of("A2DP"));
mController.setHasExtraSpace(true);
setupDevice(makeDefaultDeviceConfig());
addA2dpProfileToDevice(true, true, true);
@@ -565,7 +571,6 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
@Test
public void prefKeyNotInBlockingList_showToggle() {
initController(List.of());
setupDevice(makeDefaultDeviceConfig());
addA2dpProfileToDevice(true, true, true);
@@ -653,13 +658,4 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
assertThat(switches.getFirst().getTitle()).isEqualTo(
mContext.getString(mLeAudioProfile.getNameResource(mDevice)));
}
private void initController(List<String> invisibleProfiles) {
mController = new BluetoothDetailsProfilesController(mContext, mFragment, mLocalManager,
mCachedDevice, mLifecycle, invisibleProfiles, true);
mProfiles.setKey(mController.getPreferenceKey());
mController.mProfilesContainer = mProfiles;
mScreen.removeAll();
mScreen.addPreference(mProfiles);
}
}

View File

@@ -52,7 +52,6 @@ import androidx.fragment.app.FragmentTransaction;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.bluetooth.ui.model.FragmentTypeModel;
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
import com.android.settings.flags.Flags;
import com.android.settings.testutils.FakeFeatureFactory;
@@ -122,10 +121,7 @@ public class BluetoothDeviceDetailsFragmentTest {
removeInputDeviceWithMatchingBluetoothAddress();
FakeFeatureFactory fakeFeatureFactory = FakeFeatureFactory.setupForTest();
when(fakeFeatureFactory.mBluetoothFeatureProvider.getDeviceDetailsFragmentFormatter(any(),
any(), any(), eq(mCachedDevice))).thenReturn(mFormatter);
when(mFormatter.getVisiblePreferenceKeys(
FragmentTypeModel.DeviceDetailsMainFragment.INSTANCE))
.thenReturn(null);
any(), any(), eq(mCachedDevice), any())).thenReturn(mFormatter);
mFragment = setupFragment();
mFragment.onAttach(mContext);

View File

@@ -39,6 +39,7 @@ import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSetti
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.android.settingslib.core.AbstractPreferenceController
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
@@ -73,6 +74,9 @@ class DeviceDetailsFragmentFormatterTest {
@Mock private lateinit var cachedDevice: CachedBluetoothDevice
@Mock private lateinit var bluetoothAdapter: BluetoothAdapter
@Mock private lateinit var repository: DeviceSettingRepository
@Mock private lateinit var profileController: AbstractPreferenceController
@Mock private lateinit var headerController: AbstractPreferenceController
@Mock private lateinit var buttonController: AbstractPreferenceController
private lateinit var context: Context
private lateinit var fragment: TestFragment
@@ -98,55 +102,22 @@ class DeviceDetailsFragmentFormatterTest {
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" })
addPreference(Preference(context).apply { key = "bluetooth_profiles" })
}
`when`(profileController.preferenceKey).thenReturn("bluetooth_profiles")
`when`(headerController.preferenceKey).thenReturn("bluetooth_device_header")
`when`(buttonController.preferenceKey).thenReturn("action_buttons")
underTest =
DeviceDetailsFragmentFormatterImpl(
context,
fragment,
listOf(profileController, headerController, buttonController),
bluetoothAdapter,
cachedDevice,
testScope.testScheduler)
}
@Test
fun getVisiblePreferenceKeysForMainPage_hasConfig_returnList() {
testScope.runTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn(
DeviceSettingConfigModel(
listOf(
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
highlighted = false,
preferenceKey = "bluetooth_device_header"
),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, highlighted = false, preferenceKey = "action_buttons"),
),
listOf(),
null))
val keys =
underTest.getVisiblePreferenceKeys(FragmentTypeModel.DeviceDetailsMainFragment)
assertThat(keys).containsExactly("bluetooth_device_header", "action_buttons")
}
}
@Test
fun getVisiblePreferenceKeysForMainPage_noConfig_returnNull() {
testScope.runTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice)).thenReturn(null)
val keys =
underTest.getVisiblePreferenceKeys(FragmentTypeModel.DeviceDetailsMainFragment)
assertThat(keys).isNull()
}
}
@Test
fun getMenuItem_returnItem() {
testScope.runTest {
@@ -187,7 +158,7 @@ class DeviceDetailsFragmentFormatterTest {
underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment)
assertThat(getDisplayedPreferences().mapNotNull { it.key })
.containsExactly("bluetooth_device_header", "action_buttons", "keyboard_settings")
.containsExactly("bluetooth_device_header", "action_buttons", "bluetooth_profiles")
}
}
@@ -202,8 +173,8 @@ class DeviceDetailsFragmentFormatterTest {
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
highlighted = false, preferenceKey = "bluetooth_device_header"),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
highlighted = false, preferenceKey = "keyboard_settings"),
DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES,
highlighted = false, preferenceKey = "bluetooth_profiles"),
),
listOf(),
null))
@@ -212,7 +183,7 @@ class DeviceDetailsFragmentFormatterTest {
runCurrent()
assertThat(getDisplayedPreferences().mapNotNull { it.key })
.containsExactly("bluetooth_device_header", "keyboard_settings")
.containsExactly("bluetooth_device_header", "bluetooth_profiles")
verify(featureFactory.metricsFeatureProvider)
.action(
SettingsEnums.PAGE_UNKNOWN,
@@ -224,7 +195,7 @@ class DeviceDetailsFragmentFormatterTest {
SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN,
0,
"keyboard_settings", 1)
"bluetooth_profiles", 1)
}
}
@@ -242,9 +213,9 @@ class DeviceDetailsFragmentFormatterTest {
DeviceSettingConfigItemModel.AppProvidedItem(
DeviceSettingId.DEVICE_SETTING_ID_ANC, highlighted = false),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES,
highlighted = false,
preferenceKey = "keyboard_settings"),
preferenceKey = "bluetooth_profiles"),
),
listOf(),
null))
@@ -273,7 +244,7 @@ class DeviceDetailsFragmentFormatterTest {
.containsExactly(
"bluetooth_device_header",
"DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}",
"keyboard_settings")
"bluetooth_profiles")
verify(featureFactory.metricsFeatureProvider)
.action(
SettingsEnums.PAGE_UNKNOWN,