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

Commit 67ac0faf authored by Haijie Hong's avatar Haijie Hong
Browse files

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
parent 9e3f075c
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