Merge "Add loading screen for Device details fragment to avoid ANR" into main

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

View File

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

View File

@@ -26,10 +26,11 @@ import android.net.Uri;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.preference.Preference; import androidx.preference.Preference;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter; 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.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository; import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository;
import com.android.settingslib.core.AbstractPreferenceController;
import kotlinx.coroutines.CoroutineScope; import kotlinx.coroutines.CoroutineScope;
@@ -100,7 +101,8 @@ public interface BluetoothFeatureProvider {
@NonNull @NonNull
DeviceDetailsFragmentFormatter getDeviceDetailsFragmentFormatter( DeviceDetailsFragmentFormatter getDeviceDetailsFragmentFormatter(
@NonNull Context context, @NonNull Context context,
@NonNull SettingsPreferenceFragment fragment, @NonNull DashboardFragment fragment,
@NonNull BluetoothAdapter bluetoothAdapter, @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.media.Spatializer
import android.net.Uri import android.net.Uri
import androidx.preference.Preference import androidx.preference.Preference
import com.android.settings.SettingsPreferenceFragment
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatterImpl 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.BluetoothUtils
import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepositoryImpl 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.ImmutableList
import com.google.common.collect.ImmutableSet import com.google.common.collect.ImmutableSet
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -78,13 +79,15 @@ open class BluetoothFeatureProviderImpl : BluetoothFeatureProvider {
override fun getDeviceDetailsFragmentFormatter( override fun getDeviceDetailsFragmentFormatter(
context: Context, context: Context,
fragment: SettingsPreferenceFragment, fragment: DashboardFragment,
bluetoothAdapter: BluetoothAdapter, bluetoothAdapter: BluetoothAdapter,
cachedDevice: CachedBluetoothDevice cachedDevice: CachedBluetoothDevice,
controllers: List<AbstractPreferenceController>,
): DeviceDetailsFragmentFormatter { ): DeviceDetailsFragmentFormatter {
return DeviceDetailsFragmentFormatterImpl( return DeviceDetailsFragmentFormatterImpl(
context, context,
fragment, fragment,
controllers,
bluetoothAdapter, bluetoothAdapter,
cachedDevice, cachedDevice,
Dispatchers.IO Dispatchers.IO

View File

@@ -45,7 +45,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference import androidx.preference.Preference
import com.android.settings.R 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.Icon
import com.android.settings.bluetooth.ui.composable.MultiTogglePreference import com.android.settings.bluetooth.ui.composable.MultiTogglePreference
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout 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.view.DeviceDetailsMoreSettingsFragment.Companion.KEY_DEVICE_ADDRESS
import com.android.settings.bluetooth.ui.viewmodel.BluetoothDeviceDetailsViewModel import com.android.settings.bluetooth.ui.viewmodel.BluetoothDeviceDetailsViewModel
import com.android.settings.core.SubSettingLauncher import com.android.settings.core.SubSettingLauncher
import com.android.settings.dashboard.DashboardFragment
import com.android.settings.overlay.FeatureFactory import com.android.settings.overlay.FeatureFactory
import com.android.settings.spa.preference.ComposePreference import com.android.settings.spa.preference.ComposePreference
import com.android.settingslib.bluetooth.CachedBluetoothDevice 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.DeviceSettingActionModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon 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.framework.theme.SettingsDimension
import com.android.settingslib.spa.widget.preference.Preference as SpaPreference import com.android.settingslib.spa.widget.preference.Preference as SpaPreference
import com.android.settingslib.spa.widget.preference.PreferenceModel 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.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.launch
/** Handles device details fragment layout according to config. */ /** Handles device details fragment layout according to config. */
interface DeviceDetailsFragmentFormatter { 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. */ /** Updates device details fragment layout. */
fun updateLayout(fragmentType: FragmentTypeModel) fun updateLayout(fragmentType: FragmentTypeModel)
@@ -104,7 +105,8 @@ interface DeviceDetailsFragmentFormatter {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class DeviceDetailsFragmentFormatterImpl( class DeviceDetailsFragmentFormatterImpl(
private val context: Context, private val context: Context,
private val fragment: SettingsPreferenceFragment, private val fragment: DashboardFragment,
controllers: List<AbstractPreferenceController>,
private val bluetoothAdapter: BluetoothAdapter, private val bluetoothAdapter: BluetoothAdapter,
private val cachedDevice: CachedBluetoothDevice, private val cachedDevice: CachedBluetoothDevice,
private val backgroundCoroutineContext: CoroutineContext, private val backgroundCoroutineContext: CoroutineContext,
@@ -112,40 +114,32 @@ class DeviceDetailsFragmentFormatterImpl(
private val metricsFeatureProvider = FeatureFactory.featureFactory.metricsFeatureProvider private val metricsFeatureProvider = FeatureFactory.featureFactory.metricsFeatureProvider
private val prefVisibility = mutableMapOf<String, MutableStateFlow<Boolean>>() private val prefVisibility = mutableMapOf<String, MutableStateFlow<Boolean>>()
private val prefVisibilityJobs = mutableListOf<Job>() private val prefVisibilityJobs = mutableListOf<Job>()
private var isLoading = false
private var prefKeyToController: Map<String, AbstractPreferenceController> =
controllers.associateBy { it.preferenceKey }
private val viewModel: BluetoothDeviceDetailsViewModel = private val viewModel: BluetoothDeviceDetailsViewModel =
ViewModelProvider( ViewModelProvider(
fragment, fragment,
BluetoothDeviceDetailsViewModel.Factory( BluetoothDeviceDetailsViewModel.Factory(
fragment.requireActivity().application, fragment.requireActivity().application,
bluetoothAdapter, bluetoothAdapter,
cachedDevice, cachedDevice,
backgroundCoroutineContext, backgroundCoroutineContext,
), ),
) )
.get(BluetoothDeviceDetailsViewModel::class.java) .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. */ /** Updates bluetooth device details fragment layout. */
override fun updateLayout(fragmentType: FragmentTypeModel) = runBlocking { override fun updateLayout(fragmentType: FragmentTypeModel) {
val items = viewModel.getItems(fragmentType) ?: return@runBlocking fragment.setLoading(true, false)
val layout = viewModel.getLayout(fragmentType) ?: return@runBlocking 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 = val prefKeyToSettingId =
items items
@@ -156,21 +150,21 @@ class DeviceDetailsFragmentFormatterImpl(
for (i in 0 until fragment.preferenceScreen.preferenceCount) { for (i in 0 until fragment.preferenceScreen.preferenceCount) {
val pref = fragment.preferenceScreen.getPreference(i) val pref = fragment.preferenceScreen.getPreference(i)
prefKeyToSettingId[pref.key]?.let { id -> settingIdToXmlPreferences[id] = pref } prefKeyToSettingId[pref.key]?.let { id -> settingIdToXmlPreferences[id] = pref }
if (pref.key !in prefKeyToSettingId) {
getController(pref.key)?.let { disableController(it) }
}
} }
fragment.preferenceScreen.removeAll() fragment.preferenceScreen.removeAll()
for (job in prefVisibilityJobs) { for (job in prefVisibilityJobs) {
job.cancel() job.cancel()
} }
prefVisibilityJobs.clear() prefVisibilityJobs.clear()
for (row in items.indices) { for (row in items.indices) {
val settingId = items[row].settingId val settingItem = items[row]
val settingId = settingItem.settingId
if (settingIdToXmlPreferences.containsKey(settingId)) { if (settingIdToXmlPreferences.containsKey(settingId)) {
fragment.preferenceScreen.addPreference( val pref = settingIdToXmlPreferences[settingId]!!.apply { order = row }
settingIdToXmlPreferences[settingId]!! fragment.preferenceScreen.addPreference(pref)
.apply { order = row }
.also { logItemShown(it.key, it.isVisible) }
)
} else { } else {
val prefKey = getPreferenceKey(settingId) val prefKey = getPreferenceKey(settingId)
prefVisibilityJobs.add( prefVisibilityJobs.add(
@@ -195,6 +189,29 @@ class DeviceDetailsFragmentFormatterImpl(
isSelectable = false isSelectable = false
setContent { Spacer(modifier = Modifier.height(1.dp)) } 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( override fun getMenuItem(
@@ -232,14 +249,14 @@ class DeviceDetailsFragmentFormatterImpl(
@Composable @Composable
private fun buildPreference(layout: DeviceSettingLayout, row: Int, prefKey: String) { private fun buildPreference(layout: DeviceSettingLayout, row: Int, prefKey: String) {
val contents by val contents by
remember(row) { getDevicesSettingForRow(layout, row) } remember(row) { getDevicesSettingForRow(layout, row) }
.collectAsStateWithLifecycle(initialValue = listOf()) .collectAsStateWithLifecycle(initialValue = listOf())
val highlighted by val highlighted by
remember(row) { remember(row) {
layout.rows[row].columns.map { columns -> columns.any { it.highlighted } } layout.rows[row].columns.map { columns -> columns.any { it.highlighted } }
} }
.collectAsStateWithLifecycle(initialValue = false) .collectAsStateWithLifecycle(initialValue = false)
val settings = contents val settings = contents
AnimatedVisibility(visible = settings.isNotEmpty(), enter = fadeIn(), exit = fadeOut()) { AnimatedVisibility(visible = settings.isNotEmpty(), enter = fadeIn(), exit = fadeOut()) {
@@ -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 fun getPreferenceKey(settingId: Int) = "DEVICE_SETTING_${settingId}"
private companion object { private companion object {

View File

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

View File

@@ -120,7 +120,12 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
.thenAnswer(invocation -> ImmutableList.of(mConnectableProfiles)); .thenAnswer(invocation -> ImmutableList.of(mConnectableProfiles));
setupDevice(mDeviceConfig); 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)); BluetoothProperties.le_audio_allow_list(Lists.newArrayList(LE_DEVICE_MODEL));
} }
@@ -550,7 +555,8 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
@Test @Test
public void prefKeyInBlockingList_hideToggle() { public void prefKeyInBlockingList_hideToggle() {
initController(List.of("A2DP")); mController.setInvisibleProfiles(List.of("A2DP"));
mController.setHasExtraSpace(true);
setupDevice(makeDefaultDeviceConfig()); setupDevice(makeDefaultDeviceConfig());
addA2dpProfileToDevice(true, true, true); addA2dpProfileToDevice(true, true, true);
@@ -565,7 +571,6 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
@Test @Test
public void prefKeyNotInBlockingList_showToggle() { public void prefKeyNotInBlockingList_showToggle() {
initController(List.of());
setupDevice(makeDefaultDeviceConfig()); setupDevice(makeDefaultDeviceConfig());
addA2dpProfileToDevice(true, true, true); addA2dpProfileToDevice(true, true, true);
@@ -653,13 +658,4 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
assertThat(switches.getFirst().getTitle()).isEqualTo( assertThat(switches.getFirst().getTitle()).isEqualTo(
mContext.getString(mLeAudioProfile.getNameResource(mDevice))); 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 androidx.preference.PreferenceScreen;
import com.android.settings.R; 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.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
import com.android.settings.flags.Flags; import com.android.settings.flags.Flags;
import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.FakeFeatureFactory;
@@ -122,10 +121,7 @@ public class BluetoothDeviceDetailsFragmentTest {
removeInputDeviceWithMatchingBluetoothAddress(); removeInputDeviceWithMatchingBluetoothAddress();
FakeFeatureFactory fakeFeatureFactory = FakeFeatureFactory.setupForTest(); FakeFeatureFactory fakeFeatureFactory = FakeFeatureFactory.setupForTest();
when(fakeFeatureFactory.mBluetoothFeatureProvider.getDeviceDetailsFragmentFormatter(any(), when(fakeFeatureFactory.mBluetoothFeatureProvider.getDeviceDetailsFragmentFormatter(any(),
any(), any(), eq(mCachedDevice))).thenReturn(mFormatter); any(), any(), eq(mCachedDevice), any())).thenReturn(mFormatter);
when(mFormatter.getVisiblePreferenceKeys(
FragmentTypeModel.DeviceDetailsMainFragment.INSTANCE))
.thenReturn(null);
mFragment = setupFragment(); mFragment = setupFragment();
mFragment.onAttach(mContext); 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.DeviceSettingModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel
import com.android.settingslib.core.AbstractPreferenceController
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -73,6 +74,9 @@ class DeviceDetailsFragmentFormatterTest {
@Mock private lateinit var cachedDevice: CachedBluetoothDevice @Mock private lateinit var cachedDevice: CachedBluetoothDevice
@Mock private lateinit var bluetoothAdapter: BluetoothAdapter @Mock private lateinit var bluetoothAdapter: BluetoothAdapter
@Mock private lateinit var repository: DeviceSettingRepository @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 context: Context
private lateinit var fragment: TestFragment private lateinit var fragment: TestFragment
@@ -98,55 +102,22 @@ class DeviceDetailsFragmentFormatterTest {
fragment.preferenceScreen.run { fragment.preferenceScreen.run {
addPreference(Preference(context).apply { key = "bluetooth_device_header" }) addPreference(Preference(context).apply { key = "bluetooth_device_header" })
addPreference(Preference(context).apply { key = "action_buttons" }) 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 = underTest =
DeviceDetailsFragmentFormatterImpl( DeviceDetailsFragmentFormatterImpl(
context, context,
fragment, fragment,
listOf(profileController, headerController, buttonController),
bluetoothAdapter, bluetoothAdapter,
cachedDevice, cachedDevice,
testScope.testScheduler) 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 @Test
fun getMenuItem_returnItem() { fun getMenuItem_returnItem() {
testScope.runTest { testScope.runTest {
@@ -187,7 +158,7 @@ class DeviceDetailsFragmentFormatterTest {
underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment) underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment)
assertThat(getDisplayedPreferences().mapNotNull { it.key }) 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, DeviceSettingId.DEVICE_SETTING_ID_HEADER,
highlighted = false, preferenceKey = "bluetooth_device_header"), highlighted = false, preferenceKey = "bluetooth_device_header"),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem( DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS, DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES,
highlighted = false, preferenceKey = "keyboard_settings"), highlighted = false, preferenceKey = "bluetooth_profiles"),
), ),
listOf(), listOf(),
null)) null))
@@ -212,7 +183,7 @@ class DeviceDetailsFragmentFormatterTest {
runCurrent() runCurrent()
assertThat(getDisplayedPreferences().mapNotNull { it.key }) assertThat(getDisplayedPreferences().mapNotNull { it.key })
.containsExactly("bluetooth_device_header", "keyboard_settings") .containsExactly("bluetooth_device_header", "bluetooth_profiles")
verify(featureFactory.metricsFeatureProvider) verify(featureFactory.metricsFeatureProvider)
.action( .action(
SettingsEnums.PAGE_UNKNOWN, SettingsEnums.PAGE_UNKNOWN,
@@ -224,7 +195,7 @@ class DeviceDetailsFragmentFormatterTest {
SettingsEnums.PAGE_UNKNOWN, SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN, SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN,
0, 0,
"keyboard_settings", 1) "bluetooth_profiles", 1)
} }
} }
@@ -242,9 +213,9 @@ class DeviceDetailsFragmentFormatterTest {
DeviceSettingConfigItemModel.AppProvidedItem( DeviceSettingConfigItemModel.AppProvidedItem(
DeviceSettingId.DEVICE_SETTING_ID_ANC, highlighted = false), DeviceSettingId.DEVICE_SETTING_ID_ANC, highlighted = false),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem( DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS, DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES,
highlighted = false, highlighted = false,
preferenceKey = "keyboard_settings"), preferenceKey = "bluetooth_profiles"),
), ),
listOf(), listOf(),
null)) null))
@@ -273,7 +244,7 @@ class DeviceDetailsFragmentFormatterTest {
.containsExactly( .containsExactly(
"bluetooth_device_header", "bluetooth_device_header",
"DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}", "DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}",
"keyboard_settings") "bluetooth_profiles")
verify(featureFactory.metricsFeatureProvider) verify(featureFactory.metricsFeatureProvider)
.action( .action(
SettingsEnums.PAGE_UNKNOWN, SettingsEnums.PAGE_UNKNOWN,