Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7c6a8a4f authored by Haijie Hong's avatar Haijie Hong Committed by Android (Google) Code Review
Browse files

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

parents 115f92e5 67ac0faf
Loading
Loading
Loading
Loading
+14 −9
Original line number Diff line number Diff line
@@ -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;
    }

    @Override
    protected void init(PreferenceScreen screen) {
        mProfilesContainer = (PreferenceCategory)screen.findPreference(getPreferenceKey());
        if (mHasExtraSpace) {
    /** 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());
        mProfilesContainer.setLayoutResource(R.layout.preference_bluetooth_profile_category);
        // 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();
+23 −37
Original line number Diff line number Diff line
@@ -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,
+5 −3
Original line number Diff line number Diff line
@@ -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);
}
+6 −3
Original line number Diff line number Diff line
@@ -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
+90 −50
Original line number Diff line number Diff line
@@ -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) {
        fragment.setLoading(true, false)
        isLoading = true
        fragment.lifecycleScope.launch { updateLayoutInternal(fragmentType) }
    }

    /** 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
    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 {
Loading