Create BluetoothDetailsContentManager to support tile details view.

Extract the non-dialog related logic from BluetoothTileDialogDelegate and put it in BluetoothDetailsContentManager.

Bug: 378513956
Flag: NONE refactor
Test: BluetoothDetailsContentManagerTest, BluetoothTileDialogDelegaTetest, BluetoothTileDialogViewModelTest
No-Typo-Check: CUJ in this CL is not a typo

Change-Id: I22ea76d631e0836ca78c20a8f5b6b1ea6d8c667f
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 0a7d880..1f2890c 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -314,6 +314,7 @@
         "tests/src/**/systemui/statusbar/policy/WalletControllerImplTest.kt",
         "tests/src/**/keyguard/ClockEventControllerTest.kt",
         "tests/src/**/systemui/bluetooth/qsdialog/BluetoothStateInteractorTest.kt",
+        "tests/src/**/systemui/bluetooth/qsdialog/BluetoothDetailsContentManagerTest.kt",
         "tests/src/**/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt",
         "tests/src/**/systemui/bluetooth/qsdialog/BluetoothTileDialogRepositoryTest.kt",
         "tests/src/**/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt",
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
index 0303048..94fca21 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
@@ -53,7 +53,7 @@
     private val deviceItemActionInteractorImpl: DeviceItemActionInteractorImpl,
 ) : DeviceItemActionInteractor {
 
-    override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {
+    override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog?) {
         withContext(backgroundDispatcher) {
             if (!audioSharingInteractor.audioSharingAvailable()) {
                 return@withContext deviceItemActionInteractorImpl.onClick(deviceItem, dialog)
@@ -70,10 +70,18 @@
                     DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
                     if (audioSharingInteractor.qsDialogImprovementAvailable()) {
                         withContext(mainDispatcher) {
-                            delegateFactory
-                                .create(deviceItem.cachedBluetoothDevice)
-                                .createDialog()
-                                .let { dialogTransitionAnimator.showFromDialog(it, dialog) }
+                            val audioSharingDialog =
+                                delegateFactory
+                                    .create(deviceItem.cachedBluetoothDevice)
+                                    .createDialog()
+
+                            if (dialog != null) {
+                                audioSharingDialog.let {
+                                    dialogTransitionAnimator.showFromDialog(it, dialog)
+                                }
+                            } else {
+                                audioSharingDialog.show()
+                            }
                         }
                     } else {
                         launchSettings(deviceItem.cachedBluetoothDevice.device, dialog)
@@ -141,7 +149,7 @@
             )
     }
 
-    private fun launchSettings(device: BluetoothDevice, dialog: SystemUIDialog) {
+    private fun launchSettings(device: BluetoothDevice, dialog: SystemUIDialog?) {
         val intent =
             Intent(Settings.ACTION_BLUETOOTH_SETTINGS).apply {
                 putExtra(
@@ -155,7 +163,8 @@
         activityStarter.postStartActivityDismissingKeyguard(
             intent,
             0,
-            dialogTransitionAnimator.createActivityTransitionController(dialog),
+            if (dialog == null) null
+            else dialogTransitionAnimator.createActivityTransitionController(dialog),
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManager.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManager.kt
new file mode 100644
index 0000000..0be28f3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManager.kt
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.AccessibilityDelegate
+import android.view.View.GONE
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.view.accessibility.AccessibilityNodeInfo
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.ProgressBar
+import android.widget.Switch
+import android.widget.TextView
+import androidx.annotation.StringRes
+import androidx.recyclerview.widget.AsyncListDiffer
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.android.internal.R as InternalR
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
+import com.android.systemui.util.time.SystemClock
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.withContext
+
+data class DeviceItemClick(val deviceItem: DeviceItem, val clickedView: View, val target: Target) {
+    enum class Target {
+        ENTIRE_ROW,
+        ACTION_ICON,
+    }
+}
+
+/** View content manager for showing active, connected and saved bluetooth devices. */
+class BluetoothDetailsContentManager
+@AssistedInject
+internal constructor(
+    @Assisted private val initialUiProperties: BluetoothTileDialogViewModel.UiProperties,
+    @Assisted private val cachedContentHeight: Int,
+    @Assisted private val bluetoothTileDialogCallback: BluetoothTileDialogCallback,
+    @Assisted private val isInDialog: Boolean,
+    @Assisted private val doneButtonCallback: () -> Unit,
+    @Main private val mainDispatcher: CoroutineDispatcher,
+    private val systemClock: SystemClock,
+    private val uiEventLogger: UiEventLogger,
+    private val logger: BluetoothTileDialogLogger,
+) {
+
+    private val mutableBluetoothStateToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null)
+    internal val bluetoothStateToggle
+        get() = mutableBluetoothStateToggle.asStateFlow()
+
+    private val mutableBluetoothAutoOnToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null)
+    internal val bluetoothAutoOnToggle
+        get() = mutableBluetoothAutoOnToggle.asStateFlow()
+
+    private val mutableDeviceItemClick: MutableStateFlow<DeviceItemClick?> = MutableStateFlow(null)
+    internal val deviceItemClick
+        get() = mutableDeviceItemClick.asStateFlow()
+
+    private val mutableContentHeight: MutableStateFlow<Int?> = MutableStateFlow(null)
+    internal val contentHeight
+        get() = mutableContentHeight.asStateFlow()
+
+    private val deviceItemAdapter: Adapter = Adapter()
+
+    private var lastUiUpdateMs: Long = -1
+
+    private var lastItemRow: Int = -1
+
+    // UI Components
+    private lateinit var contentView: View
+    private lateinit var doneButton: Button
+    private lateinit var bluetoothToggle: Switch
+    private lateinit var subtitleTextView: TextView
+    private lateinit var seeAllButton: View
+    private lateinit var pairNewDeviceButton: View
+    private lateinit var deviceListView: RecyclerView
+    private lateinit var autoOnToggle: Switch
+    private lateinit var autoOnToggleLayout: View
+    private lateinit var autoOnToggleInfoTextView: TextView
+    private lateinit var audioSharingButton: Button
+    private lateinit var progressBarAnimation: ProgressBar
+    private lateinit var progressBarBackground: View
+    private lateinit var scrollViewContent: View
+
+    @AssistedFactory
+    internal interface Factory {
+        fun create(
+            initialUiProperties: BluetoothTileDialogViewModel.UiProperties,
+            cachedContentHeight: Int,
+            dialogCallback: BluetoothTileDialogCallback,
+            isInDialog: Boolean,
+            doneButtonCallback: () -> Unit,
+        ): BluetoothDetailsContentManager
+    }
+
+    fun bind(contentView: View) {
+        this.contentView = contentView
+
+        doneButton = contentView.requireViewById(R.id.done_button)
+        bluetoothToggle = contentView.requireViewById(R.id.bluetooth_toggle)
+        subtitleTextView = contentView.requireViewById(R.id.bluetooth_tile_dialog_subtitle)
+        seeAllButton = contentView.requireViewById(R.id.see_all_button)
+        pairNewDeviceButton = contentView.requireViewById(R.id.pair_new_device_button)
+        deviceListView = contentView.requireViewById(R.id.device_list)
+        autoOnToggle = contentView.requireViewById(R.id.bluetooth_auto_on_toggle)
+        autoOnToggleLayout = contentView.requireViewById(R.id.bluetooth_auto_on_toggle_layout)
+        autoOnToggleInfoTextView =
+            contentView.requireViewById(R.id.bluetooth_auto_on_toggle_info_text)
+        audioSharingButton = contentView.requireViewById(R.id.audio_sharing_button)
+        progressBarAnimation =
+            contentView.requireViewById(R.id.bluetooth_tile_dialog_progress_animation)
+        progressBarBackground =
+            contentView.requireViewById(R.id.bluetooth_tile_dialog_progress_background)
+        scrollViewContent = contentView.requireViewById(R.id.scroll_view)
+
+        setupToggle()
+        setupRecyclerView()
+        setupDoneButton()
+
+        subtitleTextView.text = contentView.context.getString(initialUiProperties.subTitleResId)
+        seeAllButton.setOnClickListener { bluetoothTileDialogCallback.onSeeAllClicked(it) }
+        pairNewDeviceButton.setOnClickListener {
+            bluetoothTileDialogCallback.onPairNewDeviceClicked(it)
+        }
+        audioSharingButton.apply {
+            setOnClickListener { bluetoothTileDialogCallback.onAudioSharingButtonClicked(it) }
+            accessibilityDelegate =
+                object : AccessibilityDelegate() {
+                    override fun onInitializeAccessibilityNodeInfo(
+                        host: View,
+                        info: AccessibilityNodeInfo,
+                    ) {
+                        super.onInitializeAccessibilityNodeInfo(host, info)
+                        info.addAction(
+                            AccessibilityAction(
+                                AccessibilityAction.ACTION_CLICK.id,
+                                contentView.context.getString(
+                                    R.string
+                                        .quick_settings_bluetooth_audio_sharing_button_accessibility
+                                ),
+                            )
+                        )
+                    }
+                }
+        }
+        scrollViewContent.apply {
+            minimumHeight =
+                resources.getDimensionPixelSize(initialUiProperties.scrollViewMinHeightResId)
+            layoutParams.height = maxOf(cachedContentHeight, minimumHeight)
+        }
+    }
+
+    fun start() {
+        lastUiUpdateMs = systemClock.elapsedRealtime()
+    }
+
+    fun releaseView() {
+        mutableContentHeight.value = scrollViewContent.measuredHeight
+    }
+
+    internal suspend fun animateProgressBar(animate: Boolean) {
+        withContext(mainDispatcher) {
+            if (animate) {
+                showProgressBar()
+            } else {
+                delay(PROGRESS_BAR_ANIMATION_DURATION_MS)
+                hideProgressBar()
+            }
+        }
+    }
+
+    internal suspend fun onDeviceItemUpdated(
+        deviceItem: List<DeviceItem>,
+        showSeeAll: Boolean,
+        showPairNewDevice: Boolean,
+    ) {
+        withContext(mainDispatcher) {
+            val start = systemClock.elapsedRealtime()
+            val itemRow = deviceItem.size + showSeeAll.toInt() + showPairNewDevice.toInt()
+            // If not the first load, add a slight delay for smoother dialog height change
+            if (itemRow != lastItemRow && lastItemRow != -1) {
+                delay(MIN_HEIGHT_CHANGE_INTERVAL_MS - (start - lastUiUpdateMs))
+            }
+            if (isActive) {
+                deviceItemAdapter.refreshDeviceItemList(deviceItem) {
+                    seeAllButton.visibility = if (showSeeAll) VISIBLE else GONE
+                    pairNewDeviceButton.visibility = if (showPairNewDevice) VISIBLE else GONE
+                    // Update the height after data is updated
+                    scrollViewContent.layoutParams.height = WRAP_CONTENT
+                    lastUiUpdateMs = systemClock.elapsedRealtime()
+                    lastItemRow = itemRow
+                    logger.logDeviceUiUpdate(lastUiUpdateMs - start)
+                }
+            }
+        }
+    }
+
+    internal fun onBluetoothStateUpdated(
+        isEnabled: Boolean,
+        uiProperties: BluetoothTileDialogViewModel.UiProperties,
+    ) {
+        bluetoothToggle.apply {
+            isChecked = isEnabled
+            setEnabled(true)
+            alpha = ENABLED_ALPHA
+        }
+        subtitleTextView.text = contentView.context.getString(uiProperties.subTitleResId)
+        autoOnToggleLayout.visibility = uiProperties.autoOnToggleVisibility
+    }
+
+    internal fun onBluetoothAutoOnUpdated(isEnabled: Boolean, @StringRes infoResId: Int) {
+        autoOnToggle.isChecked = isEnabled
+        autoOnToggleInfoTextView.text = contentView.context.getString(infoResId)
+    }
+
+    internal fun onAudioSharingButtonUpdated(visibility: Int, label: String?, isActive: Boolean) {
+        audioSharingButton.apply {
+            this.visibility = visibility
+            label?.let { text = it }
+            this.isActivated = isActive
+        }
+    }
+
+    private fun setupToggle() {
+        bluetoothToggle.setOnCheckedChangeListener { view, isChecked ->
+            mutableBluetoothStateToggle.value = isChecked
+            view.apply {
+                isEnabled = false
+                alpha = DISABLED_ALPHA
+            }
+            logger.logBluetoothState(BluetoothStateStage.USER_TOGGLED, isChecked.toString())
+            uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TOGGLE_CLICKED)
+        }
+
+        autoOnToggleLayout.visibility = initialUiProperties.autoOnToggleVisibility
+        autoOnToggle.setOnCheckedChangeListener { _, isChecked ->
+            mutableBluetoothAutoOnToggle.value = isChecked
+            uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_AUTO_ON_TOGGLE_CLICKED)
+        }
+    }
+
+    private fun setupDoneButton() {
+        if (isInDialog) {
+            doneButton.setOnClickListener { doneButtonCallback() }
+        } else {
+            doneButton.visibility = GONE
+        }
+    }
+
+    private fun setupRecyclerView() {
+        deviceListView.apply {
+            layoutManager = LinearLayoutManager(contentView.context)
+            adapter = deviceItemAdapter
+        }
+    }
+
+    private fun showProgressBar() {
+        if (progressBarAnimation.visibility != VISIBLE) {
+            progressBarAnimation.visibility = VISIBLE
+            progressBarBackground.visibility = INVISIBLE
+        }
+    }
+
+    private fun hideProgressBar() {
+        if (progressBarAnimation.visibility != INVISIBLE) {
+            progressBarAnimation.visibility = INVISIBLE
+            progressBarBackground.visibility = VISIBLE
+        }
+    }
+
+    internal inner class Adapter : RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() {
+
+        private val diffUtilCallback =
+            object : DiffUtil.ItemCallback<DeviceItem>() {
+                override fun areItemsTheSame(
+                    deviceItem1: DeviceItem,
+                    deviceItem2: DeviceItem,
+                ): Boolean {
+                    return deviceItem1.cachedBluetoothDevice == deviceItem2.cachedBluetoothDevice
+                }
+
+                override fun areContentsTheSame(
+                    deviceItem1: DeviceItem,
+                    deviceItem2: DeviceItem,
+                ): Boolean {
+                    return deviceItem1.type == deviceItem2.type &&
+                        deviceItem1.cachedBluetoothDevice == deviceItem2.cachedBluetoothDevice &&
+                        deviceItem1.deviceName == deviceItem2.deviceName &&
+                        deviceItem1.connectionSummary == deviceItem2.connectionSummary &&
+                        // Ignored the icon drawable
+                        deviceItem1.iconWithDescription?.second ==
+                            deviceItem2.iconWithDescription?.second &&
+                        deviceItem1.background == deviceItem2.background &&
+                        deviceItem1.isEnabled == deviceItem2.isEnabled &&
+                        deviceItem1.actionAccessibilityLabel == deviceItem2.actionAccessibilityLabel
+                }
+            }
+
+        private val asyncListDiffer = AsyncListDiffer(this, diffUtilCallback)
+
+        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceItemViewHolder {
+            val view =
+                LayoutInflater.from(parent.context)
+                    .inflate(R.layout.bluetooth_device_item, parent, false)
+            return DeviceItemViewHolder(view)
+        }
+
+        override fun getItemCount() = asyncListDiffer.currentList.size
+
+        override fun onBindViewHolder(holder: DeviceItemViewHolder, position: Int) {
+            val item = getItem(position)
+            holder.bind(item)
+        }
+
+        internal fun getItem(position: Int) = asyncListDiffer.currentList[position]
+
+        internal fun refreshDeviceItemList(updated: List<DeviceItem>, callback: () -> Unit) {
+            asyncListDiffer.submitList(updated, callback)
+        }
+
+        internal inner class DeviceItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+            private val container = view.requireViewById<View>(R.id.bluetooth_device_row)
+            private val nameView = view.requireViewById<TextView>(R.id.bluetooth_device_name)
+            private val summaryView = view.requireViewById<TextView>(R.id.bluetooth_device_summary)
+            private val iconView = view.requireViewById<ImageView>(R.id.bluetooth_device_icon)
+            private val actionIcon = view.requireViewById<ImageView>(R.id.gear_icon_image)
+            private val actionIconView = view.requireViewById<View>(R.id.gear_icon)
+            private val divider = view.requireViewById<View>(R.id.divider)
+
+            internal fun bind(item: DeviceItem) {
+                container.apply {
+                    isEnabled = item.isEnabled
+                    background = item.background?.let { context.getDrawable(it) }
+                    setOnClickListener {
+                        mutableDeviceItemClick.value =
+                            DeviceItemClick(item, it, DeviceItemClick.Target.ENTIRE_ROW)
+                        uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_CLICKED)
+                    }
+
+                    // updating icon colors
+                    val tintColor =
+                        context.getColor(
+                            if (item.isActive) InternalR.color.materialColorOnPrimaryContainer
+                            else InternalR.color.materialColorOnSurface
+                        )
+
+                    // update icons
+                    iconView.apply {
+                        item.iconWithDescription?.let {
+                            setImageDrawable(it.first)
+                            contentDescription = it.second
+                        }
+                    }
+
+                    actionIcon.setImageResource(item.actionIconRes)
+                    actionIcon.drawable?.setTint(tintColor)
+
+                    divider.setBackgroundColor(tintColor)
+
+                    // update text styles
+                    nameView.setTextAppearance(
+                        if (item.isActive) R.style.TextAppearance_BluetoothTileDialog_Active
+                        else R.style.TextAppearance_BluetoothTileDialog
+                    )
+                    summaryView.setTextAppearance(
+                        if (item.isActive) R.style.TextAppearance_BluetoothTileDialog_Active
+                        else R.style.TextAppearance_BluetoothTileDialog
+                    )
+
+                    accessibilityDelegate =
+                        object : AccessibilityDelegate() {
+                            override fun onInitializeAccessibilityNodeInfo(
+                                host: View,
+                                info: AccessibilityNodeInfo,
+                            ) {
+                                super.onInitializeAccessibilityNodeInfo(host, info)
+                                info.addAction(
+                                    AccessibilityAction(
+                                        AccessibilityAction.ACTION_CLICK.id,
+                                        item.actionAccessibilityLabel,
+                                    )
+                                )
+                            }
+                        }
+                }
+                nameView.text = item.deviceName
+                summaryView.text = item.connectionSummary
+
+                actionIconView.setOnClickListener {
+                    mutableDeviceItemClick.value =
+                        DeviceItemClick(item, it, DeviceItemClick.Target.ACTION_ICON)
+                }
+            }
+        }
+    }
+
+    internal companion object {
+        const val MIN_HEIGHT_CHANGE_INTERVAL_MS = 800L
+        const val ACTION_BLUETOOTH_DEVICE_DETAILS =
+            "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS"
+        const val ACTION_PREVIOUSLY_CONNECTED_DEVICE =
+            "com.android.settings.PREVIOUSLY_CONNECTED_DEVICE"
+        const val ACTION_PAIR_NEW_DEVICE = "android.settings.BLUETOOTH_PAIRING_SETTINGS"
+        const val ACTION_AUDIO_SHARING = "com.android.settings.BLUETOOTH_AUDIO_SHARING_SETTINGS"
+        const val DISABLED_ALPHA = 0.3f
+        const val ENABLED_ALPHA = 1f
+        const val PROGRESS_BAR_ANIMATION_DURATION_MS = 1500L
+
+        private fun Boolean.toInt(): Int {
+            return if (this) 1 else 0
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
index 56caddf..3e61c45 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
@@ -18,50 +18,14 @@
 
 import android.os.Bundle
 import android.view.LayoutInflater
-import android.view.View
-import android.view.View.AccessibilityDelegate
-import android.view.View.GONE
-import android.view.View.INVISIBLE
-import android.view.View.VISIBLE
-import android.view.ViewGroup
-import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
-import android.view.accessibility.AccessibilityNodeInfo
-import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
-import android.widget.Button
-import android.widget.ImageView
-import android.widget.ProgressBar
-import android.widget.Switch
-import android.widget.TextView
-import androidx.annotation.StringRes
-import androidx.recyclerview.widget.AsyncListDiffer
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import com.android.internal.R as InternalR
 import com.android.internal.logging.UiEventLogger
-import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.flags.QsDetailedView
 import com.android.systemui.res.R
 import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor
 import com.android.systemui.statusbar.phone.SystemUIDialog
-import com.android.systemui.util.time.SystemClock
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.isActive
-import kotlinx.coroutines.withContext
-
-data class DeviceItemClick(val deviceItem: DeviceItem, val clickedView: View, val target: Target) {
-    enum class Target {
-        ENTIRE_ROW,
-        ACTION_ICON,
-    }
-}
 
 /** Dialog for showing active, connected and saved bluetooth devices. */
 class BluetoothTileDialogDelegate
@@ -71,37 +35,13 @@
     @Assisted private val cachedContentHeight: Int,
     @Assisted private val bluetoothTileDialogCallback: BluetoothTileDialogCallback,
     @Assisted private val dismissListener: Runnable,
-    @Main private val mainDispatcher: CoroutineDispatcher,
-    private val systemClock: SystemClock,
     private val uiEventLogger: UiEventLogger,
-    private val logger: BluetoothTileDialogLogger,
     private val systemuiDialogFactory: SystemUIDialog.Factory,
     private val shadeDialogContextInteractor: ShadeDialogContextInteractor,
+    private val bluetoothDetailsContentManagerFactory: BluetoothDetailsContentManager.Factory,
 ) : SystemUIDialog.Delegate {
 
-    private val mutableBluetoothStateToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null)
-    internal val bluetoothStateToggle
-        get() = mutableBluetoothStateToggle.asStateFlow()
-
-    private val mutableBluetoothAutoOnToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null)
-    internal val bluetoothAutoOnToggle
-        get() = mutableBluetoothAutoOnToggle.asStateFlow()
-
-    private val mutableDeviceItemClick: MutableSharedFlow<DeviceItemClick> =
-        MutableSharedFlow(extraBufferCapacity = 1)
-    internal val deviceItemClick
-        get() = mutableDeviceItemClick.asSharedFlow()
-
-    private val mutableContentHeight: MutableSharedFlow<Int> =
-        MutableSharedFlow(extraBufferCapacity = 1)
-    internal val contentHeight
-        get() = mutableContentHeight.asSharedFlow()
-
-    private val deviceItemAdapter: Adapter = Adapter()
-
-    private var lastUiUpdateMs: Long = -1
-
-    private var lastItemRow: Int = -1
+    lateinit var contentManager: BluetoothDetailsContentManager
 
     @AssistedFactory
     internal interface Factory {
@@ -114,6 +54,9 @@
     }
 
     override fun createDialog(): SystemUIDialog {
+        // If `QsDetailedView` is enabled, it should show the details view.
+        QsDetailedView.assertInLegacyMode()
+
         return systemuiDialogFactory.create(this, shadeDialogContextInteractor.context)
     }
 
@@ -127,362 +70,24 @@
             dialog.setContentView(this)
         }
 
-        setupToggle(dialog)
-        setupRecyclerView(dialog)
-
-        getSubtitleTextView(dialog).text = context.getString(initialUiProperties.subTitleResId)
-        dialog.requireViewById<View>(R.id.done_button).setOnClickListener { dialog.dismiss() }
-        getSeeAllButton(dialog).setOnClickListener {
-            bluetoothTileDialogCallback.onSeeAllClicked(it)
-        }
-        getPairNewDeviceButton(dialog).setOnClickListener {
-            bluetoothTileDialogCallback.onPairNewDeviceClicked(it)
-        }
-        getAudioSharingButtonView(dialog).apply {
-            setOnClickListener { bluetoothTileDialogCallback.onAudioSharingButtonClicked(it) }
-            accessibilityDelegate =
-                object : AccessibilityDelegate() {
-                    override fun onInitializeAccessibilityNodeInfo(
-                        host: View,
-                        info: AccessibilityNodeInfo,
-                    ) {
-                        super.onInitializeAccessibilityNodeInfo(host, info)
-                        info.addAction(
-                            AccessibilityAction(
-                                AccessibilityAction.ACTION_CLICK.id,
-                                context.getString(
-                                    R.string
-                                        .quick_settings_bluetooth_audio_sharing_button_accessibility
-                                ),
-                            )
-                        )
-                    }
-                }
-        }
-        getScrollViewContent(dialog).apply {
-            minimumHeight =
-                resources.getDimensionPixelSize(initialUiProperties.scrollViewMinHeightResId)
-            layoutParams.height = maxOf(cachedContentHeight, minimumHeight)
-        }
+        contentManager =
+            bluetoothDetailsContentManagerFactory.create(
+                initialUiProperties,
+                cachedContentHeight,
+                bluetoothTileDialogCallback,
+                /* isInDialog= */ true,
+                /* doneButtonCallback= */ fun() {
+                    dialog.dismiss()
+                },
+            )
+        contentManager.bind(dialog.requireViewById(R.id.root))
     }
 
     override fun onStart(dialog: SystemUIDialog) {
-        lastUiUpdateMs = systemClock.elapsedRealtime()
+        contentManager.start()
     }
 
     override fun onStop(dialog: SystemUIDialog) {
-        mutableContentHeight.tryEmit(getScrollViewContent(dialog).measuredHeight)
-    }
-
-    internal suspend fun animateProgressBar(dialog: SystemUIDialog, animate: Boolean) {
-        withContext(mainDispatcher) {
-            if (animate) {
-                showProgressBar(dialog)
-            } else {
-                delay(PROGRESS_BAR_ANIMATION_DURATION_MS)
-                hideProgressBar(dialog)
-            }
-        }
-    }
-
-    internal suspend fun onDeviceItemUpdated(
-        dialog: SystemUIDialog,
-        deviceItem: List<DeviceItem>,
-        showSeeAll: Boolean,
-        showPairNewDevice: Boolean,
-    ) {
-        withContext(mainDispatcher) {
-            val start = systemClock.elapsedRealtime()
-            val itemRow = deviceItem.size + showSeeAll.toInt() + showPairNewDevice.toInt()
-            // If not the first load, add a slight delay for smoother dialog height change
-            if (itemRow != lastItemRow && lastItemRow != -1) {
-                delay(MIN_HEIGHT_CHANGE_INTERVAL_MS - (start - lastUiUpdateMs))
-            }
-            if (isActive) {
-                deviceItemAdapter.refreshDeviceItemList(deviceItem) {
-                    getSeeAllButton(dialog).visibility = if (showSeeAll) VISIBLE else GONE
-                    getPairNewDeviceButton(dialog).visibility =
-                        if (showPairNewDevice) VISIBLE else GONE
-                    // Update the height after data is updated
-                    getScrollViewContent(dialog).layoutParams.height = WRAP_CONTENT
-                    lastUiUpdateMs = systemClock.elapsedRealtime()
-                    lastItemRow = itemRow
-                    logger.logDeviceUiUpdate(lastUiUpdateMs - start)
-                }
-            }
-        }
-    }
-
-    internal fun onBluetoothStateUpdated(
-        dialog: SystemUIDialog,
-        isEnabled: Boolean,
-        uiProperties: BluetoothTileDialogViewModel.UiProperties,
-    ) {
-        getToggleView(dialog).apply {
-            isChecked = isEnabled
-            setEnabled(true)
-            alpha = ENABLED_ALPHA
-        }
-        getSubtitleTextView(dialog).text = dialog.context.getString(uiProperties.subTitleResId)
-        getAutoOnToggleView(dialog).visibility = uiProperties.autoOnToggleVisibility
-    }
-
-    internal fun onBluetoothAutoOnUpdated(
-        dialog: SystemUIDialog,
-        isEnabled: Boolean,
-        @StringRes infoResId: Int,
-    ) {
-        getAutoOnToggle(dialog).isChecked = isEnabled
-        getAutoOnToggleInfoTextView(dialog).text = dialog.context.getString(infoResId)
-    }
-
-    internal fun onAudioSharingButtonUpdated(
-        dialog: SystemUIDialog,
-        visibility: Int,
-        label: String?,
-        isActive: Boolean,
-    ) {
-        getAudioSharingButtonView(dialog).apply {
-            this.visibility = visibility
-            label?.let { text = it }
-            this.isActivated = isActive
-        }
-    }
-
-    private fun setupToggle(dialog: SystemUIDialog) {
-        val toggleView = getToggleView(dialog)
-        toggleView.setOnCheckedChangeListener { view, isChecked ->
-            mutableBluetoothStateToggle.value = isChecked
-            view.apply {
-                isEnabled = false
-                alpha = DISABLED_ALPHA
-            }
-            logger.logBluetoothState(BluetoothStateStage.USER_TOGGLED, isChecked.toString())
-            uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TOGGLE_CLICKED)
-        }
-
-        getAutoOnToggleView(dialog).visibility = initialUiProperties.autoOnToggleVisibility
-        getAutoOnToggle(dialog).setOnCheckedChangeListener { _, isChecked ->
-            mutableBluetoothAutoOnToggle.value = isChecked
-            uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_AUTO_ON_TOGGLE_CLICKED)
-        }
-    }
-
-    private fun getToggleView(dialog: SystemUIDialog): Switch {
-        return dialog.requireViewById(R.id.bluetooth_toggle)
-    }
-
-    private fun getSubtitleTextView(dialog: SystemUIDialog): TextView {
-        return dialog.requireViewById(R.id.bluetooth_tile_dialog_subtitle)
-    }
-
-    private fun getSeeAllButton(dialog: SystemUIDialog): View {
-        return dialog.requireViewById(R.id.see_all_button)
-    }
-
-    private fun getPairNewDeviceButton(dialog: SystemUIDialog): View {
-        return dialog.requireViewById(R.id.pair_new_device_button)
-    }
-
-    private fun getDeviceListView(dialog: SystemUIDialog): RecyclerView {
-        return dialog.requireViewById(R.id.device_list)
-    }
-
-    private fun getAutoOnToggle(dialog: SystemUIDialog): Switch {
-        return dialog.requireViewById(R.id.bluetooth_auto_on_toggle)
-    }
-
-    private fun getAudioSharingButtonView(dialog: SystemUIDialog): Button {
-        return dialog.requireViewById(R.id.audio_sharing_button)
-    }
-
-    private fun getAutoOnToggleView(dialog: SystemUIDialog): View {
-        return dialog.requireViewById(R.id.bluetooth_auto_on_toggle_layout)
-    }
-
-    private fun getAutoOnToggleInfoTextView(dialog: SystemUIDialog): TextView {
-        return dialog.requireViewById(R.id.bluetooth_auto_on_toggle_info_text)
-    }
-
-    private fun getProgressBarAnimation(dialog: SystemUIDialog): ProgressBar {
-        return dialog.requireViewById(R.id.bluetooth_tile_dialog_progress_animation)
-    }
-
-    private fun getProgressBarBackground(dialog: SystemUIDialog): View {
-        return dialog.requireViewById(R.id.bluetooth_tile_dialog_progress_background)
-    }
-
-    private fun getScrollViewContent(dialog: SystemUIDialog): View {
-        return dialog.requireViewById(R.id.scroll_view)
-    }
-
-    private fun setupRecyclerView(dialog: SystemUIDialog) {
-        getDeviceListView(dialog).apply {
-            layoutManager = LinearLayoutManager(dialog.context)
-            adapter = deviceItemAdapter
-        }
-    }
-
-    private fun showProgressBar(dialog: SystemUIDialog) {
-        val progressBarAnimation = getProgressBarAnimation(dialog)
-        val progressBarBackground = getProgressBarBackground(dialog)
-        if (progressBarAnimation.visibility != VISIBLE) {
-            progressBarAnimation.visibility = VISIBLE
-            progressBarBackground.visibility = INVISIBLE
-        }
-    }
-
-    private fun hideProgressBar(dialog: SystemUIDialog) {
-        val progressBarAnimation = getProgressBarAnimation(dialog)
-        val progressBarBackground = getProgressBarBackground(dialog)
-        if (progressBarAnimation.visibility != INVISIBLE) {
-            progressBarAnimation.visibility = INVISIBLE
-            progressBarBackground.visibility = VISIBLE
-        }
-    }
-
-    internal inner class Adapter : RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() {
-
-        private val diffUtilCallback =
-            object : DiffUtil.ItemCallback<DeviceItem>() {
-                override fun areItemsTheSame(
-                    deviceItem1: DeviceItem,
-                    deviceItem2: DeviceItem,
-                ): Boolean {
-                    return deviceItem1.cachedBluetoothDevice == deviceItem2.cachedBluetoothDevice
-                }
-
-                override fun areContentsTheSame(
-                    deviceItem1: DeviceItem,
-                    deviceItem2: DeviceItem,
-                ): Boolean {
-                    return deviceItem1.type == deviceItem2.type &&
-                        deviceItem1.cachedBluetoothDevice == deviceItem2.cachedBluetoothDevice &&
-                        deviceItem1.deviceName == deviceItem2.deviceName &&
-                        deviceItem1.connectionSummary == deviceItem2.connectionSummary &&
-                        // Ignored the icon drawable
-                        deviceItem1.iconWithDescription?.second ==
-                            deviceItem2.iconWithDescription?.second &&
-                        deviceItem1.background == deviceItem2.background &&
-                        deviceItem1.isEnabled == deviceItem2.isEnabled &&
-                        deviceItem1.actionAccessibilityLabel == deviceItem2.actionAccessibilityLabel
-                }
-            }
-
-        private val asyncListDiffer = AsyncListDiffer(this, diffUtilCallback)
-
-        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceItemViewHolder {
-            val view =
-                LayoutInflater.from(parent.context)
-                    .inflate(R.layout.bluetooth_device_item, parent, false)
-            return DeviceItemViewHolder(view)
-        }
-
-        override fun getItemCount() = asyncListDiffer.currentList.size
-
-        override fun onBindViewHolder(holder: DeviceItemViewHolder, position: Int) {
-            val item = getItem(position)
-            holder.bind(item)
-        }
-
-        internal fun getItem(position: Int) = asyncListDiffer.currentList[position]
-
-        internal fun refreshDeviceItemList(updated: List<DeviceItem>, callback: () -> Unit) {
-            asyncListDiffer.submitList(updated, callback)
-        }
-
-        internal inner class DeviceItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
-            private val container = view.requireViewById<View>(R.id.bluetooth_device_row)
-            private val nameView = view.requireViewById<TextView>(R.id.bluetooth_device_name)
-            private val summaryView = view.requireViewById<TextView>(R.id.bluetooth_device_summary)
-            private val iconView = view.requireViewById<ImageView>(R.id.bluetooth_device_icon)
-            private val actionIcon = view.requireViewById<ImageView>(R.id.gear_icon_image)
-            private val actionIconView = view.requireViewById<View>(R.id.gear_icon)
-            private val divider = view.requireViewById<View>(R.id.divider)
-
-            internal fun bind(item: DeviceItem) {
-                container.apply {
-                    isEnabled = item.isEnabled
-                    background = item.background?.let { context.getDrawable(it) }
-                    setOnClickListener {
-                        mutableDeviceItemClick.tryEmit(
-                            DeviceItemClick(item, it, DeviceItemClick.Target.ENTIRE_ROW)
-                        )
-                        uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_CLICKED)
-                    }
-
-                    // updating icon colors
-                    val tintColor =
-                        context.getColor(
-                            if (item.isActive) InternalR.color.materialColorOnPrimaryContainer
-                            else InternalR.color.materialColorOnSurface
-                        )
-
-                    // update icons
-                    iconView.apply {
-                        item.iconWithDescription?.let {
-                            setImageDrawable(it.first)
-                            contentDescription = it.second
-                        }
-                    }
-
-                    actionIcon.setImageResource(item.actionIconRes)
-                    actionIcon.drawable?.setTint(tintColor)
-
-                    divider.setBackgroundColor(tintColor)
-
-                    // update text styles
-                    nameView.setTextAppearance(
-                        if (item.isActive) R.style.TextAppearance_BluetoothTileDialog_Active
-                        else R.style.TextAppearance_BluetoothTileDialog
-                    )
-                    summaryView.setTextAppearance(
-                        if (item.isActive) R.style.TextAppearance_BluetoothTileDialog_Active
-                        else R.style.TextAppearance_BluetoothTileDialog
-                    )
-
-                    accessibilityDelegate =
-                        object : AccessibilityDelegate() {
-                            override fun onInitializeAccessibilityNodeInfo(
-                                host: View,
-                                info: AccessibilityNodeInfo,
-                            ) {
-                                super.onInitializeAccessibilityNodeInfo(host, info)
-                                info.addAction(
-                                    AccessibilityAction(
-                                        AccessibilityAction.ACTION_CLICK.id,
-                                        item.actionAccessibilityLabel,
-                                    )
-                                )
-                            }
-                        }
-                }
-                nameView.text = item.deviceName
-                summaryView.text = item.connectionSummary
-
-                actionIconView.setOnClickListener {
-                    mutableDeviceItemClick.tryEmit(
-                        DeviceItemClick(item, it, DeviceItemClick.Target.ACTION_ICON)
-                    )
-                }
-            }
-        }
-    }
-
-    internal companion object {
-        const val MIN_HEIGHT_CHANGE_INTERVAL_MS = 800L
-        const val ACTION_BLUETOOTH_DEVICE_DETAILS =
-            "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS"
-        const val ACTION_PREVIOUSLY_CONNECTED_DEVICE =
-            "com.android.settings.PREVIOUSLY_CONNECTED_DEVICE"
-        const val ACTION_PAIR_NEW_DEVICE = "android.settings.BLUETOOTH_PAIRING_SETTINGS"
-        const val ACTION_AUDIO_SHARING = "com.android.settings.BLUETOOTH_AUDIO_SHARING_SETTINGS"
-        const val DISABLED_ALPHA = 0.3f
-        const val ENABLED_ALPHA = 1f
-        const val PROGRESS_BAR_ANIMATION_DURATION_MS = 1500L
-
-        private fun Boolean.toInt(): Int {
-            return if (this) 1 else 0
-        }
+        contentManager.releaseView()
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
index bf04897..9492abb 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.bluetooth.qsdialog
 
+import android.content.Context
 import android.content.Intent
 import android.content.SharedPreferences
 import android.os.Bundle
@@ -34,15 +35,16 @@
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.animation.Expandable
-import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_AUDIO_SHARING
-import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PAIR_NEW_DEVICE
-import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE
+import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContentManager.Companion.ACTION_AUDIO_SHARING
+import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContentManager.Companion.ACTION_PAIR_NEW_DEVICE
+import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContentManager.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
@@ -57,7 +59,12 @@
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.withContext
 
-/** ViewModel for Bluetooth Dialog after clicking on the Bluetooth QS tile. */
+/**
+ * ViewModel for Bluetooth Dialog or Bluetooth Details View after clicking on the Bluetooth QS tile.
+ *
+ * TODO: b/378513956 Rename this class to BluetoothDetailsContentViewModel, since it's not only used
+ *   by the dialog view.
+ */
 @SysUISingleton
 internal class BluetoothTileDialogViewModel
 @Inject
@@ -78,36 +85,61 @@
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     @Main private val sharedPreferences: SharedPreferences,
     private val bluetoothDialogDelegateFactory: BluetoothTileDialogDelegate.Factory,
+    private val bluetoothDetailsContentManagerFactory: BluetoothDetailsContentManager.Factory,
 ) : BluetoothTileDialogCallback {
 
+    lateinit var contentManager: BluetoothDetailsContentManager
     private var job: Job? = null
 
     /**
-     * Shows the dialog.
+     * Shows the details content.
      *
-     * @param view The view from which the dialog is shown.
+     * @param view The view from which the dialog is shown. If view is null, it should show the
+     *   bluetooth tile details view.
+     *
+     * TODO: b/378513956 Refactor this method into 2. One is called by the dialog to show the
+     *   dialog, another is called by the details view model to bind the view.
      */
-    fun showDialog(expandable: Expandable?) {
+    fun showDetailsContent(expandable: Expandable?, view: View?) {
         cancelJob()
 
         job =
             coroutineScope.launch(context = mainDispatcher) {
                 var updateDeviceItemJob: Job?
                 var updateDialogUiJob: Job? = null
-                val dialogDelegate = createBluetoothTileDialog()
-                val dialog = dialogDelegate.createDialog()
-                val context = dialog.context
+                val dialog: SystemUIDialog?
+                val context: Context
 
-                val controller =
-                    expandable?.dialogTransitionController(
-                        DialogCuj(
-                            InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                            INTERACTION_JANK_TAG,
+                if (view == null) {
+                    // Render with dialog
+                    val dialogDelegate = createBluetoothTileDialog()
+                    dialog = dialogDelegate.createDialog()
+                    context = dialog.context
+
+                    val controller =
+                        expandable?.dialogTransitionController(
+                            DialogCuj(
+                                InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+                                INTERACTION_JANK_TAG,
+                            )
                         )
-                    )
-                controller?.let {
-                    dialogTransitionAnimator.show(dialog, it, animateBackgroundBoundsChange = true)
-                } ?: dialog.show()
+                    controller?.let {
+                        dialogTransitionAnimator.show(
+                            dialog,
+                            it,
+                            animateBackgroundBoundsChange = true,
+                        )
+                    } ?: dialog.show()
+                    // contentManager is created after dialog.show
+                    contentManager = dialogDelegate.contentManager
+                } else {
+                    // Render with tile details view
+                    dialog = null
+                    context = view.context
+                    contentManager = createContentManager()
+                    contentManager.bind(view)
+                    contentManager.start()
+                }
 
                 updateDeviceItemJob = launch {
                     deviceItemInteractor.updateDeviceItems(context, DeviceFetchTrigger.FIRST_LOAD)
@@ -121,15 +153,14 @@
                     ) { deviceItem, showSeeAll ->
                         updateDialogUiJob?.cancel()
                         updateDialogUiJob = launch {
-                            dialogDelegate.apply {
+                            contentManager.apply {
                                 onDeviceItemUpdated(
-                                    dialog,
                                     deviceItem,
                                     showSeeAll,
                                     showPairNewDevice =
                                         bluetoothStateInteractor.isBluetoothEnabled(),
                                 )
-                                animateProgressBar(dialog, false)
+                                animateProgressBar(false)
                             }
                         }
                     }
@@ -150,7 +181,7 @@
                         },
                     )
                     .onEach {
-                        dialogDelegate.animateProgressBar(dialog, true)
+                        contentManager.animateProgressBar(true)
                         updateDeviceItemJob?.cancel()
                         updateDeviceItemJob = launch {
                             deviceItemInteractor.updateDeviceItems(
@@ -171,16 +202,14 @@
                             .onEach {
                                 when (it) {
                                     is AudioSharingButtonState.Visible -> {
-                                        dialogDelegate.onAudioSharingButtonUpdated(
-                                            dialog,
+                                        contentManager.onAudioSharingButtonUpdated(
                                             VISIBLE,
                                             context.getString(it.resId),
                                             it.isActive,
                                         )
                                     }
                                     is AudioSharingButtonState.Gone -> {
-                                        dialogDelegate.onAudioSharingButtonUpdated(
-                                            dialog,
+                                        contentManager.onAudioSharingButtonUpdated(
                                             GONE,
                                             label = null,
                                             isActive = false,
@@ -197,8 +226,7 @@
                 // the device item list.
                 bluetoothStateInteractor.bluetoothStateUpdate
                     .onEach {
-                        dialogDelegate.onBluetoothStateUpdated(
-                            dialog,
+                        contentManager.onBluetoothStateUpdated(
                             it,
                             UiProperties.build(it, isAutoOnToggleFeatureAvailable()),
                         )
@@ -214,16 +242,17 @@
 
                 // bluetoothStateToggle is emitted when user toggles the bluetooth state switch,
                 // send the new value to the bluetoothStateInteractor and animate the progress bar.
-                dialogDelegate.bluetoothStateToggle
+                contentManager.bluetoothStateToggle
                     .filterNotNull()
                     .onEach {
-                        dialogDelegate.animateProgressBar(dialog, true)
+                        contentManager.animateProgressBar(true)
                         bluetoothStateInteractor.setBluetoothEnabled(it)
                     }
                     .launchIn(this)
 
                 // deviceItemClick is emitted when user clicked on a device item.
-                dialogDelegate.deviceItemClick
+                contentManager.deviceItemClick
+                    .filterNotNull()
                     .onEach {
                         when (it.target) {
                             DeviceItemClick.Target.ENTIRE_ROW -> {
@@ -245,7 +274,8 @@
                     .launchIn(this)
 
                 // contentHeight is emitted when the dialog is dismissed.
-                dialogDelegate.contentHeight
+                contentManager.contentHeight
+                    .filterNotNull()
                     .onEach {
                         withContext(backgroundDispatcher) {
                             sharedPreferences.edit().putInt(CONTENT_HEIGHT_PREF_KEY, it).apply()
@@ -258,8 +288,7 @@
                     // changed.
                     bluetoothAutoOnInteractor.isEnabled
                         .onEach {
-                            dialogDelegate.onBluetoothAutoOnUpdated(
-                                dialog,
+                            contentManager.onBluetoothAutoOnUpdated(
                                 it,
                                 if (it) R.string.turn_on_bluetooth_auto_info_enabled
                                 else R.string.turn_on_bluetooth_auto_info_disabled,
@@ -269,36 +298,48 @@
 
                     // bluetoothAutoOnToggle is emitted when user toggles the bluetooth auto on
                     // switch, send the new value to the bluetoothAutoOnInteractor.
-                    dialogDelegate.bluetoothAutoOnToggle
+                    contentManager.bluetoothAutoOnToggle
                         .filterNotNull()
                         .onEach { bluetoothAutoOnInteractor.setEnabled(it) }
                         .launchIn(this)
                 }
 
-                produce<Unit> { awaitClose { dialog.cancel() } }
+                produce<Unit> { awaitClose { dialog?.cancel() } }
             }
     }
 
     private suspend fun createBluetoothTileDialog(): BluetoothTileDialogDelegate {
-        val cachedContentHeight =
-            withContext(backgroundDispatcher) {
-                sharedPreferences.getInt(
-                    CONTENT_HEIGHT_PREF_KEY,
-                    ViewGroup.LayoutParams.WRAP_CONTENT,
-                )
-            }
-
         return bluetoothDialogDelegateFactory.create(
-            UiProperties.build(
-                bluetoothStateInteractor.isBluetoothEnabled(),
-                isAutoOnToggleFeatureAvailable(),
-            ),
-            cachedContentHeight,
+            getUiProperties(),
+            getCachedContentHeight(),
             this@BluetoothTileDialogViewModel,
             { cancelJob() },
         )
     }
 
+    private suspend fun createContentManager(): BluetoothDetailsContentManager {
+        return bluetoothDetailsContentManagerFactory.create(
+            getUiProperties(),
+            getCachedContentHeight(),
+            this@BluetoothTileDialogViewModel,
+            /* isInDialog= */ false,
+            /* doneButtonCallback= */ fun() {},
+        )
+    }
+
+    private suspend fun getUiProperties(): UiProperties {
+        return UiProperties.build(
+            bluetoothStateInteractor.isBluetoothEnabled(),
+            isAutoOnToggleFeatureAvailable(),
+        )
+    }
+
+    private suspend fun getCachedContentHeight(): Int {
+        return withContext(backgroundDispatcher) {
+            sharedPreferences.getInt(CONTENT_HEIGHT_PREF_KEY, ViewGroup.LayoutParams.WRAP_CONTENT)
+        }
+    }
+
     override fun onSeeAllClicked(view: View) {
         uiEventLogger.log(BluetoothTileDialogUiEvent.SEE_ALL_CLICKED)
         startSettingsActivity(Intent(ACTION_PREVIOUSLY_CONNECTED_DEVICE), view)
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
index cb4ec37..26996ac 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
@@ -27,7 +27,7 @@
 import kotlinx.coroutines.withContext
 
 interface DeviceItemActionInteractor {
-    suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog)
+    suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog?) {}
 
     suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit)
 }
@@ -40,7 +40,7 @@
     private val uiEventLogger: UiEventLogger,
 ) : DeviceItemActionInteractor {
 
-    override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {
+    override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog?) {
         withContext(backgroundDispatcher) {
             deviceItem.cachedBluetoothDevice.apply {
                 when (deviceItem.type) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
index 0109e70a..1cfa663 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
@@ -158,7 +158,7 @@
 
     private void handleClickEvent(@Nullable Expandable expandable) {
         if (mFeatureFlags.isEnabled(Flags.BLUETOOTH_QS_TILE_DIALOG)) {
-            mDialogViewModel.showDialog(expandable);
+            mDialogViewModel.showDetailsContent(expandable, /* view= */ null);
         } else {
             // Secondary clicks are header clicks, just toggle.
             toggleBluetooth();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManagerTest.kt
new file mode 100644
index 0000000..6ed990d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManagerTest.kt
@@ -0,0 +1,461 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import android.graphics.drawable.Drawable
+import android.testing.TestableLooper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.model.SysUiState
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.SystemUIDialogManager
+import com.android.systemui.testKosmos
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class BluetoothDetailsContentManagerTest : SysuiTestCase() {
+    companion object {
+        const val DEVICE_NAME = "device"
+        const val DEVICE_CONNECTION_SUMMARY = "active"
+        const val ENABLED = true
+        const val CONTENT_HEIGHT = WRAP_CONTENT
+    }
+
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    private val cachedBluetoothDevice = mock<CachedBluetoothDevice>()
+
+    private val bluetoothTileDialogCallback = mock<BluetoothTileDialogCallback>()
+
+    private val drawable = mock<Drawable>()
+
+    private val uiEventLogger = mock<UiEventLogger>()
+
+    private val logger = mock<BluetoothTileDialogLogger>()
+
+    private val sysuiDialogFactory = mock<SystemUIDialog.Factory>()
+    private val dialogManager = mock<SystemUIDialogManager>()
+    private val sysuiState = mock<SysUiState>()
+    private val dialogTransitionAnimator = mock<DialogTransitionAnimator>()
+
+    private val fakeSystemClock = FakeSystemClock()
+
+    private val uiProperties =
+        BluetoothTileDialogViewModel.UiProperties.build(
+            isBluetoothEnabled = ENABLED,
+            isAutoOnToggleFeatureAvailable = ENABLED,
+        )
+
+    private lateinit var icon: Pair<Drawable, String>
+    private lateinit var mBluetoothDetailsContentManager: BluetoothDetailsContentManager
+    private lateinit var deviceItem: DeviceItem
+    private lateinit var contentView: View
+
+    private val kosmos = testKosmos()
+
+    @Before
+    fun setUp() {
+        with(kosmos) {
+            contentView =
+                LayoutInflater.from(mContext).inflate(R.layout.bluetooth_tile_dialog, null)
+
+            whenever(sysuiState.setFlag(anyLong(), anyBoolean())).thenReturn(sysuiState)
+
+            mBluetoothDetailsContentManager =
+                BluetoothDetailsContentManager(
+                    uiProperties,
+                    CONTENT_HEIGHT,
+                    bluetoothTileDialogCallback,
+                    /* isInDialog= */ true,
+                    {},
+                    testDispatcher,
+                    fakeSystemClock,
+                    uiEventLogger,
+                    logger,
+                )
+
+            whenever(sysuiDialogFactory.create(any<SystemUIDialog.Delegate>(), any())).thenAnswer {
+                SystemUIDialog(
+                    mContext,
+                    0,
+                    SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK,
+                    dialogManager,
+                    sysuiState,
+                    fakeBroadcastDispatcher,
+                    dialogTransitionAnimator,
+                    it.getArgument(0),
+                )
+            }
+
+            icon = Pair(drawable, DEVICE_NAME)
+            deviceItem =
+                DeviceItem(
+                    type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
+                    cachedBluetoothDevice = cachedBluetoothDevice,
+                    deviceName = DEVICE_NAME,
+                    connectionSummary = DEVICE_CONNECTION_SUMMARY,
+                    iconWithDescription = icon,
+                    background = null,
+                )
+            whenever(cachedBluetoothDevice.isBusy).thenReturn(false)
+        }
+    }
+
+    @Test
+    fun testShowDialog_createRecyclerViewWithAdapter() {
+        mBluetoothDetailsContentManager.bind(contentView)
+        mBluetoothDetailsContentManager.start()
+
+        val recyclerView = contentView.requireViewById<RecyclerView>(R.id.device_list)
+
+        assertThat(recyclerView).isNotNull()
+        assertThat(recyclerView.visibility).isEqualTo(VISIBLE)
+        assertThat(recyclerView.adapter).isNotNull()
+        assertThat(recyclerView.layoutManager is LinearLayoutManager).isTrue()
+        mBluetoothDetailsContentManager.releaseView()
+    }
+
+    @Test
+    fun testShowDialog_displayBluetoothDevice() {
+        with(kosmos) {
+            testScope.runTest {
+                mBluetoothDetailsContentManager.bind(contentView)
+                mBluetoothDetailsContentManager.start()
+                fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
+                mBluetoothDetailsContentManager.onDeviceItemUpdated(
+                    listOf(deviceItem),
+                    showSeeAll = false,
+                    showPairNewDevice = false,
+                )
+
+                val recyclerView = contentView.requireViewById<RecyclerView>(R.id.device_list)
+                val adapter = recyclerView?.adapter as BluetoothDetailsContentManager.Adapter
+                assertThat(adapter.itemCount).isEqualTo(1)
+                assertThat(adapter.getItem(0).deviceName).isEqualTo(DEVICE_NAME)
+                assertThat(adapter.getItem(0).connectionSummary)
+                    .isEqualTo(DEVICE_CONNECTION_SUMMARY)
+                assertThat(adapter.getItem(0).iconWithDescription).isEqualTo(icon)
+                mBluetoothDetailsContentManager.releaseView()
+            }
+        }
+    }
+
+    @Test
+    fun testDeviceItemViewHolder_cachedDeviceNotBusy() {
+        with(kosmos) {
+            testScope.runTest {
+                deviceItem.isEnabled = true
+
+                val view =
+                    LayoutInflater.from(mContext)
+                        .inflate(R.layout.bluetooth_device_item, null, false)
+                val viewHolder =
+                    mBluetoothDetailsContentManager.Adapter().DeviceItemViewHolder(view)
+                viewHolder.bind(deviceItem)
+                val container = view.requireViewById<View>(R.id.bluetooth_device_row)
+
+                assertThat(container).isNotNull()
+                assertThat(container.isEnabled).isTrue()
+                assertThat(container.hasOnClickListeners()).isTrue()
+                val value by collectLastValue(mBluetoothDetailsContentManager.deviceItemClick)
+                runCurrent()
+                container.performClick()
+                runCurrent()
+                assertThat(value).isNotNull()
+                value?.let {
+                    assertThat(it.target).isEqualTo(DeviceItemClick.Target.ENTIRE_ROW)
+                    assertThat(it.clickedView).isEqualTo(container)
+                    assertThat(it.deviceItem).isEqualTo(deviceItem)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun testDeviceItemViewHolder_cachedDeviceBusy() {
+        with(kosmos) {
+            deviceItem.isEnabled = false
+
+            val view =
+                LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
+            val viewHolder =
+                BluetoothDetailsContentManager(
+                        uiProperties,
+                        CONTENT_HEIGHT,
+                        bluetoothTileDialogCallback,
+                        /* isInDialog= */ true,
+                        {},
+                        testDispatcher,
+                        fakeSystemClock,
+                        uiEventLogger,
+                        logger,
+                    )
+                    .Adapter()
+                    .DeviceItemViewHolder(view)
+            viewHolder.bind(deviceItem)
+            val container = view.requireViewById<View>(R.id.bluetooth_device_row)
+
+            assertThat(container).isNotNull()
+            assertThat(container.isEnabled).isFalse()
+            assertThat(container.hasOnClickListeners()).isTrue()
+        }
+    }
+
+    @Test
+    fun testDeviceItemViewHolder_clickActionIcon() {
+        with(kosmos) {
+            testScope.runTest {
+                deviceItem.isEnabled = true
+
+                val view =
+                    LayoutInflater.from(mContext)
+                        .inflate(R.layout.bluetooth_device_item, null, false)
+                val viewHolder =
+                    mBluetoothDetailsContentManager.Adapter().DeviceItemViewHolder(view)
+                viewHolder.bind(deviceItem)
+                val actionIconView = view.requireViewById<View>(R.id.gear_icon)
+
+                assertThat(actionIconView).isNotNull()
+                assertThat(actionIconView.hasOnClickListeners()).isTrue()
+                val value by collectLastValue(mBluetoothDetailsContentManager.deviceItemClick)
+                runCurrent()
+                actionIconView.performClick()
+                runCurrent()
+                assertThat(value).isNotNull()
+                value?.let {
+                    assertThat(it.target).isEqualTo(DeviceItemClick.Target.ACTION_ICON)
+                    assertThat(it.clickedView).isEqualTo(actionIconView)
+                    assertThat(it.deviceItem).isEqualTo(deviceItem)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun testOnDeviceUpdated_hideSeeAll_showPairNew() {
+        with(kosmos) {
+            testScope.runTest {
+                mBluetoothDetailsContentManager.bind(contentView)
+                mBluetoothDetailsContentManager.start()
+                fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
+                mBluetoothDetailsContentManager.onDeviceItemUpdated(
+                    listOf(deviceItem),
+                    showSeeAll = false,
+                    showPairNewDevice = true,
+                )
+
+                val seeAllButton = contentView.requireViewById<View>(R.id.see_all_button)
+                val pairNewButton = contentView.requireViewById<View>(R.id.pair_new_device_button)
+                val recyclerView = contentView.requireViewById<RecyclerView>(R.id.device_list)
+                val adapter = recyclerView?.adapter as BluetoothDetailsContentManager.Adapter
+                val scrollViewContent = contentView.requireViewById<View>(R.id.scroll_view)
+
+                assertThat(seeAllButton).isNotNull()
+                assertThat(seeAllButton.visibility).isEqualTo(GONE)
+                assertThat(pairNewButton).isNotNull()
+                assertThat(pairNewButton.visibility).isEqualTo(VISIBLE)
+                assertThat(adapter.itemCount).isEqualTo(1)
+                assertThat(scrollViewContent.layoutParams.height).isEqualTo(WRAP_CONTENT)
+                mBluetoothDetailsContentManager.releaseView()
+            }
+        }
+    }
+
+    @Test
+    fun testShowDialog_cachedHeightLargerThanMinHeight_displayFromCachedHeight() {
+        with(kosmos) {
+            testScope.runTest {
+                val cachedHeight = Int.MAX_VALUE
+                val contentManager =
+                    BluetoothDetailsContentManager(
+                        BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
+                        cachedHeight,
+                        bluetoothTileDialogCallback,
+                        /* isInDialog= */ true,
+                        {},
+                        testDispatcher,
+                        fakeSystemClock,
+                        uiEventLogger,
+                        logger,
+                    )
+                contentManager.bind(contentView)
+                contentManager.start()
+                assertThat(contentView.requireViewById<View>(R.id.scroll_view).layoutParams.height)
+                    .isEqualTo(cachedHeight)
+                contentManager.releaseView()
+            }
+        }
+    }
+
+    @Test
+    fun testShowDialog_cachedHeightLessThanMinHeight_displayFromUiProperties() {
+        with(kosmos) {
+            testScope.runTest {
+                val contentManager =
+                    BluetoothDetailsContentManager(
+                        BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
+                        MATCH_PARENT,
+                        bluetoothTileDialogCallback,
+                        /* isInDialog= */ true,
+                        {},
+                        testDispatcher,
+                        fakeSystemClock,
+                        uiEventLogger,
+                        logger,
+                    )
+                contentManager.bind(contentView)
+                contentManager.start()
+                assertThat(contentView.requireViewById<View>(R.id.scroll_view).layoutParams.height)
+                    .isGreaterThan(MATCH_PARENT)
+                contentManager.releaseView()
+            }
+        }
+    }
+
+    @Test
+    fun testShowDialog_bluetoothEnabled_autoOnToggleGone() {
+        with(kosmos) {
+            testScope.runTest {
+                val contentManager =
+                    BluetoothDetailsContentManager(
+                        BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
+                        MATCH_PARENT,
+                        bluetoothTileDialogCallback,
+                        /* isInDialog= */ true,
+                        {},
+                        testDispatcher,
+                        fakeSystemClock,
+                        uiEventLogger,
+                        logger,
+                    )
+                contentManager.bind(contentView)
+                contentManager.start()
+                assertThat(
+                        contentView
+                            .requireViewById<View>(R.id.bluetooth_auto_on_toggle_layout)
+                            .visibility
+                    )
+                    .isEqualTo(GONE)
+                contentManager.releaseView()
+            }
+        }
+    }
+
+    @Test
+    fun testOnAudioSharingButtonUpdated_visibleActive_activateButton() {
+        with(kosmos) {
+            testScope.runTest {
+                mBluetoothDetailsContentManager.bind(contentView)
+                mBluetoothDetailsContentManager.start()
+                fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
+                mBluetoothDetailsContentManager.onAudioSharingButtonUpdated(
+                    visibility = VISIBLE,
+                    label = null,
+                    isActive = true,
+                )
+
+                val audioSharingButton =
+                    contentView.requireViewById<View>(R.id.audio_sharing_button)
+
+                assertThat(audioSharingButton).isNotNull()
+                assertThat(audioSharingButton.visibility).isEqualTo(VISIBLE)
+                assertThat(audioSharingButton.isActivated).isTrue()
+                mBluetoothDetailsContentManager.releaseView()
+            }
+        }
+    }
+
+    @Test
+    fun testOnAudioSharingButtonUpdated_visibleNotActive_inactivateButton() {
+        with(kosmos) {
+            testScope.runTest {
+                mBluetoothDetailsContentManager.bind(contentView)
+                mBluetoothDetailsContentManager.start()
+                fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
+                mBluetoothDetailsContentManager.onAudioSharingButtonUpdated(
+                    visibility = VISIBLE,
+                    label = null,
+                    isActive = false,
+                )
+
+                val audioSharingButton =
+                    contentView.requireViewById<View>(R.id.audio_sharing_button)
+
+                assertThat(audioSharingButton).isNotNull()
+                assertThat(audioSharingButton.visibility).isEqualTo(VISIBLE)
+                assertThat(audioSharingButton.isActivated).isFalse()
+                mBluetoothDetailsContentManager.releaseView()
+            }
+        }
+    }
+
+    @Test
+    fun testOnAudioSharingButtonUpdated_gone_inactivateButton() {
+        with(kosmos) {
+            testScope.runTest {
+                mBluetoothDetailsContentManager.bind(contentView)
+                mBluetoothDetailsContentManager.start()
+                fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
+                mBluetoothDetailsContentManager.onAudioSharingButtonUpdated(
+                    visibility = GONE,
+                    label = null,
+                    isActive = false,
+                )
+
+                val audioSharingButton =
+                    contentView.requireViewById<View>(R.id.audio_sharing_button)
+
+                assertThat(audioSharingButton).isNotNull()
+                assertThat(audioSharingButton.visibility).isEqualTo(GONE)
+                assertThat(audioSharingButton.isActivated).isFalse()
+                mBluetoothDetailsContentManager.releaseView()
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt
index 4396b0a..ffc7518 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt
@@ -16,47 +16,34 @@
 
 package com.android.systemui.bluetooth.qsdialog
 
-import android.graphics.drawable.Drawable
 import android.testing.TestableLooper
-import android.view.LayoutInflater
-import android.view.View
-import android.view.View.GONE
-import android.view.View.VISIBLE
-import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
-import com.android.settingslib.bluetooth.CachedBluetoothDevice
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogTransitionAnimator
-import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.model.SysUiState
-import com.android.systemui.res.R
 import com.android.systemui.shade.data.repository.shadeDialogContextInteractor
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.test.TestCoroutineScheduler
 import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.anyLong
 import org.mockito.Mock
-import org.mockito.Mockito.`when`
+import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
 
@@ -73,33 +60,31 @@
 
     @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
 
-    @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice
+    @Mock
+    private lateinit var bluetoothDetailsContentManagerFactory:
+        BluetoothDetailsContentManager.Factory
+
+    @Mock private lateinit var bluetoothDetailsContentManager: BluetoothDetailsContentManager
 
     @Mock private lateinit var bluetoothTileDialogCallback: BluetoothTileDialogCallback
 
-    @Mock private lateinit var drawable: Drawable
-
     @Mock private lateinit var uiEventLogger: UiEventLogger
 
-    @Mock private lateinit var logger: BluetoothTileDialogLogger
+    @Mock private lateinit var sysuiDialogFactory: SystemUIDialog.Factory
+    @Mock private lateinit var dialogManager: SystemUIDialogManager
+    @Mock private lateinit var sysuiState: SysUiState
+    @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator
 
     private val uiProperties =
         BluetoothTileDialogViewModel.UiProperties.build(
             isBluetoothEnabled = ENABLED,
             isAutoOnToggleFeatureAvailable = ENABLED,
         )
-    @Mock private lateinit var sysuiDialogFactory: SystemUIDialog.Factory
-    @Mock private lateinit var dialogManager: SystemUIDialogManager
-    @Mock private lateinit var sysuiState: SysUiState
-    @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator
 
-    private val fakeSystemClock = FakeSystemClock()
-
+    private lateinit var scheduler: TestCoroutineScheduler
     private lateinit var dispatcher: CoroutineDispatcher
     private lateinit var testScope: TestScope
-    private lateinit var icon: Pair<Drawable, String>
     private lateinit var mBluetoothTileDialogDelegate: BluetoothTileDialogDelegate
-    private lateinit var deviceItem: DeviceItem
 
     private val kosmos = testKosmos()
 
@@ -116,12 +101,10 @@
                 CONTENT_HEIGHT,
                 bluetoothTileDialogCallback,
                 {},
-                dispatcher,
-                fakeSystemClock,
                 uiEventLogger,
-                logger,
                 sysuiDialogFactory,
                 kosmos.shadeDialogContextInteractor,
+                bluetoothDetailsContentManagerFactory,
             )
 
         whenever(sysuiDialogFactory.create(any(SystemUIDialog.Delegate::class.java), any()))
@@ -138,17 +121,16 @@
                 )
             }
 
-        icon = Pair(drawable, DEVICE_NAME)
-        deviceItem =
-            DeviceItem(
-                type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
-                cachedBluetoothDevice = cachedBluetoothDevice,
-                deviceName = DEVICE_NAME,
-                connectionSummary = DEVICE_CONNECTION_SUMMARY,
-                iconWithDescription = icon,
-                background = null,
+        whenever(
+                bluetoothDetailsContentManagerFactory.create(
+                    any(),
+                    anyInt(),
+                    any(),
+                    anyBoolean(),
+                    any(),
+                )
             )
-        `when`(cachedBluetoothDevice.isBusy).thenReturn(false)
+            .thenReturn(bluetoothDetailsContentManager)
     }
 
     @Test
@@ -156,287 +138,9 @@
         val dialog = mBluetoothTileDialogDelegate.createDialog()
         dialog.show()
 
-        val recyclerView = dialog.requireViewById<RecyclerView>(R.id.device_list)
-
-        assertThat(recyclerView).isNotNull()
-        assertThat(recyclerView.visibility).isEqualTo(VISIBLE)
-        assertThat(recyclerView.adapter).isNotNull()
-        assertThat(recyclerView.layoutManager is LinearLayoutManager).isTrue()
+        verify(bluetoothDetailsContentManager).bind(any())
+        verify(bluetoothDetailsContentManager).start()
         dialog.dismiss()
-    }
-
-    @Test
-    fun testShowDialog_displayBluetoothDevice() {
-        testScope.runTest {
-            val dialog = mBluetoothTileDialogDelegate.createDialog()
-            dialog.show()
-            fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
-            mBluetoothTileDialogDelegate.onDeviceItemUpdated(
-                dialog,
-                listOf(deviceItem),
-                showSeeAll = false,
-                showPairNewDevice = false,
-            )
-
-            val recyclerView = dialog.requireViewById<RecyclerView>(R.id.device_list)
-            val adapter = recyclerView?.adapter as BluetoothTileDialogDelegate.Adapter
-            assertThat(adapter.itemCount).isEqualTo(1)
-            assertThat(adapter.getItem(0).deviceName).isEqualTo(DEVICE_NAME)
-            assertThat(adapter.getItem(0).connectionSummary).isEqualTo(DEVICE_CONNECTION_SUMMARY)
-            assertThat(adapter.getItem(0).iconWithDescription).isEqualTo(icon)
-            dialog.dismiss()
-        }
-    }
-
-    @Test
-    fun testDeviceItemViewHolder_cachedDeviceNotBusy() {
-        testScope.runTest {
-            deviceItem.isEnabled = true
-
-            val view =
-                LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
-            val viewHolder = mBluetoothTileDialogDelegate.Adapter().DeviceItemViewHolder(view)
-            viewHolder.bind(deviceItem)
-            val container = view.requireViewById<View>(R.id.bluetooth_device_row)
-
-            assertThat(container).isNotNull()
-            assertThat(container.isEnabled).isTrue()
-            assertThat(container.hasOnClickListeners()).isTrue()
-            val value by collectLastValue(mBluetoothTileDialogDelegate.deviceItemClick)
-            runCurrent()
-            container.performClick()
-            runCurrent()
-            assertThat(value).isNotNull()
-            value?.let {
-                assertThat(it.target).isEqualTo(DeviceItemClick.Target.ENTIRE_ROW)
-                assertThat(it.clickedView).isEqualTo(container)
-                assertThat(it.deviceItem).isEqualTo(deviceItem)
-            }
-        }
-    }
-
-    @Test
-    fun testDeviceItemViewHolder_cachedDeviceBusy() {
-        deviceItem.isEnabled = false
-
-        val view =
-            LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
-        val viewHolder =
-            BluetoothTileDialogDelegate(
-                    uiProperties,
-                    CONTENT_HEIGHT,
-                    bluetoothTileDialogCallback,
-                    {},
-                    dispatcher,
-                    fakeSystemClock,
-                    uiEventLogger,
-                    logger,
-                    sysuiDialogFactory,
-                    kosmos.shadeDialogContextInteractor,
-                )
-                .Adapter()
-                .DeviceItemViewHolder(view)
-        viewHolder.bind(deviceItem)
-        val container = view.requireViewById<View>(R.id.bluetooth_device_row)
-
-        assertThat(container).isNotNull()
-        assertThat(container.isEnabled).isFalse()
-        assertThat(container.hasOnClickListeners()).isTrue()
-    }
-
-    @Test
-    fun testDeviceItemViewHolder_clickActionIcon() {
-        testScope.runTest {
-            deviceItem.isEnabled = true
-
-            val view =
-                LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
-            val viewHolder = mBluetoothTileDialogDelegate.Adapter().DeviceItemViewHolder(view)
-            viewHolder.bind(deviceItem)
-            val actionIconView = view.requireViewById<View>(R.id.gear_icon)
-
-            assertThat(actionIconView).isNotNull()
-            assertThat(actionIconView.hasOnClickListeners()).isTrue()
-            val value by collectLastValue(mBluetoothTileDialogDelegate.deviceItemClick)
-            runCurrent()
-            actionIconView.performClick()
-            runCurrent()
-            assertThat(value).isNotNull()
-            value?.let {
-                assertThat(it.target).isEqualTo(DeviceItemClick.Target.ACTION_ICON)
-                assertThat(it.clickedView).isEqualTo(actionIconView)
-                assertThat(it.deviceItem).isEqualTo(deviceItem)
-            }
-        }
-    }
-
-    @Test
-    fun testOnDeviceUpdated_hideSeeAll_showPairNew() {
-        testScope.runTest {
-            val dialog = mBluetoothTileDialogDelegate.createDialog()
-            dialog.show()
-            fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
-            mBluetoothTileDialogDelegate.onDeviceItemUpdated(
-                dialog,
-                listOf(deviceItem),
-                showSeeAll = false,
-                showPairNewDevice = true,
-            )
-
-            val seeAllButton = dialog.requireViewById<View>(R.id.see_all_button)
-            val pairNewButton = dialog.requireViewById<View>(R.id.pair_new_device_button)
-            val recyclerView = dialog.requireViewById<RecyclerView>(R.id.device_list)
-            val adapter = recyclerView?.adapter as BluetoothTileDialogDelegate.Adapter
-            val scrollViewContent = dialog.requireViewById<View>(R.id.scroll_view)
-
-            assertThat(seeAllButton).isNotNull()
-            assertThat(seeAllButton.visibility).isEqualTo(GONE)
-            assertThat(pairNewButton).isNotNull()
-            assertThat(pairNewButton.visibility).isEqualTo(VISIBLE)
-            assertThat(adapter.itemCount).isEqualTo(1)
-            assertThat(scrollViewContent.layoutParams.height).isEqualTo(WRAP_CONTENT)
-            dialog.dismiss()
-        }
-    }
-
-    @Test
-    fun testShowDialog_cachedHeightLargerThanMinHeight_displayFromCachedHeight() {
-        testScope.runTest {
-            val cachedHeight = Int.MAX_VALUE
-            val dialog =
-                BluetoothTileDialogDelegate(
-                        BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
-                        cachedHeight,
-                        bluetoothTileDialogCallback,
-                        {},
-                        dispatcher,
-                        fakeSystemClock,
-                        uiEventLogger,
-                        logger,
-                        sysuiDialogFactory,
-                        kosmos.shadeDialogContextInteractor,
-                    )
-                    .createDialog()
-            dialog.show()
-            assertThat(dialog.requireViewById<View>(R.id.scroll_view).layoutParams.height)
-                .isEqualTo(cachedHeight)
-            dialog.dismiss()
-        }
-    }
-
-    @Test
-    fun testShowDialog_cachedHeightLessThanMinHeight_displayFromUiProperties() {
-        testScope.runTest {
-            val dialog =
-                BluetoothTileDialogDelegate(
-                        BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
-                        MATCH_PARENT,
-                        bluetoothTileDialogCallback,
-                        {},
-                        dispatcher,
-                        fakeSystemClock,
-                        uiEventLogger,
-                        logger,
-                        sysuiDialogFactory,
-                        kosmos.shadeDialogContextInteractor,
-                    )
-                    .createDialog()
-            dialog.show()
-            assertThat(dialog.requireViewById<View>(R.id.scroll_view).layoutParams.height)
-                .isGreaterThan(MATCH_PARENT)
-            dialog.dismiss()
-        }
-    }
-
-    @Test
-    fun testShowDialog_bluetoothEnabled_autoOnToggleGone() {
-        testScope.runTest {
-            val dialog =
-                BluetoothTileDialogDelegate(
-                        BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
-                        MATCH_PARENT,
-                        bluetoothTileDialogCallback,
-                        {},
-                        dispatcher,
-                        fakeSystemClock,
-                        uiEventLogger,
-                        logger,
-                        sysuiDialogFactory,
-                        kosmos.shadeDialogContextInteractor,
-                    )
-                    .createDialog()
-            dialog.show()
-            assertThat(
-                    dialog.requireViewById<View>(R.id.bluetooth_auto_on_toggle_layout).visibility
-                )
-                .isEqualTo(GONE)
-            dialog.dismiss()
-        }
-    }
-
-    @Test
-    fun testOnAudioSharingButtonUpdated_visibleActive_activateButton() {
-        testScope.runTest {
-            val dialog = mBluetoothTileDialogDelegate.createDialog()
-            dialog.show()
-            fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
-            mBluetoothTileDialogDelegate.onAudioSharingButtonUpdated(
-                dialog,
-                visibility = VISIBLE,
-                label = null,
-                isActive = true,
-            )
-
-            val audioSharingButton = dialog.requireViewById<View>(R.id.audio_sharing_button)
-
-            assertThat(audioSharingButton).isNotNull()
-            assertThat(audioSharingButton.visibility).isEqualTo(VISIBLE)
-            assertThat(audioSharingButton.isActivated).isTrue()
-            dialog.dismiss()
-        }
-    }
-
-    @Test
-    fun testOnAudioSharingButtonUpdated_visibleNotActive_inactivateButton() {
-        testScope.runTest {
-            val dialog = mBluetoothTileDialogDelegate.createDialog()
-            dialog.show()
-            fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
-            mBluetoothTileDialogDelegate.onAudioSharingButtonUpdated(
-                dialog,
-                visibility = VISIBLE,
-                label = null,
-                isActive = false,
-            )
-
-            val audioSharingButton = dialog.requireViewById<View>(R.id.audio_sharing_button)
-
-            assertThat(audioSharingButton).isNotNull()
-            assertThat(audioSharingButton.visibility).isEqualTo(VISIBLE)
-            assertThat(audioSharingButton.isActivated).isFalse()
-            dialog.dismiss()
-        }
-    }
-
-    @Test
-    fun testOnAudioSharingButtonUpdated_gone_inactivateButton() {
-        testScope.runTest {
-            val dialog = mBluetoothTileDialogDelegate.createDialog()
-            dialog.show()
-            fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
-            mBluetoothTileDialogDelegate.onAudioSharingButtonUpdated(
-                dialog,
-                visibility = GONE,
-                label = null,
-                isActive = false,
-            )
-
-            val audioSharingButton = dialog.requireViewById<View>(R.id.audio_sharing_button)
-
-            assertThat(audioSharingButton).isNotNull()
-            assertThat(audioSharingButton.visibility).isEqualTo(GONE)
-            assertThat(audioSharingButton.isActivated).isFalse()
-            dialog.dismiss()
-        }
+        verify(bluetoothDetailsContentManager).releaseView()
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
index a56c2cb..fa34579 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
@@ -78,8 +78,6 @@
 
     private lateinit var bluetoothTileDialogViewModel: BluetoothTileDialogViewModel
 
-    @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
-
     @Mock private lateinit var bluetoothDeviceMetadataInteractor: BluetoothDeviceMetadataInteractor
 
     @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
@@ -108,9 +106,16 @@
 
     @Mock private lateinit var bluetoothTileDialogDelegate: BluetoothTileDialogDelegate
 
+    @Mock
+    private lateinit var bluetoothDetailsContentManagerFactory:
+        BluetoothDetailsContentManager.Factory
+
+    @Mock private lateinit var bluetoothDetailsContentManager: BluetoothDetailsContentManager
+
     @Mock private lateinit var sysuiDialog: SystemUIDialog
     @Mock private lateinit var expandable: Expandable
     @Mock private lateinit var controller: DialogTransitionAnimator.Controller
+    @Mock private lateinit var mockView: View
 
     private val sharedPreferences = FakeSharedPreferences()
 
@@ -131,7 +136,7 @@
                     localBluetoothManager,
                     bluetoothTileDialogLogger,
                     testScope.backgroundScope,
-                    dispatcher
+                    dispatcher,
                 ),
                 // TODO(b/316822488): Create FakeBluetoothAutoOnInteractor.
                 BluetoothAutoOnInteractor(
@@ -139,7 +144,7 @@
                         localBluetoothManager,
                         bluetoothAdapter,
                         testScope.backgroundScope,
-                        dispatcher
+                        dispatcher,
                     )
                 ),
                 kosmos.audioSharingInteractor,
@@ -153,7 +158,8 @@
                 dispatcher,
                 dispatcher,
                 sharedPreferences,
-                mBluetoothTileDialogDelegateDelegateFactory
+                mBluetoothTileDialogDelegateDelegateFactory,
+                bluetoothDetailsContentManagerFactory,
             )
         whenever(deviceItemInteractor.deviceItemUpdate).thenReturn(MutableSharedFlow())
         whenever(deviceItemInteractor.deviceItemUpdateRequest)
@@ -163,20 +169,34 @@
         whenever(mBluetoothTileDialogDelegateDelegateFactory.create(any(), anyInt(), any(), any()))
             .thenReturn(bluetoothTileDialogDelegate)
         whenever(bluetoothTileDialogDelegate.createDialog()).thenReturn(sysuiDialog)
+        whenever(bluetoothTileDialogDelegate.contentManager)
+            .thenReturn(bluetoothDetailsContentManager)
+        whenever(
+                bluetoothDetailsContentManagerFactory.create(
+                    any(),
+                    anyInt(),
+                    any(),
+                    anyBoolean(),
+                    any(),
+                )
+            )
+            .thenReturn(bluetoothDetailsContentManager)
         whenever(sysuiDialog.context).thenReturn(mContext)
-        whenever(bluetoothTileDialogDelegate.bluetoothStateToggle)
+        whenever(bluetoothDetailsContentManager.bluetoothStateToggle)
             .thenReturn(getMutableStateFlow(false))
-        whenever(bluetoothTileDialogDelegate.deviceItemClick).thenReturn(MutableSharedFlow())
-        whenever(bluetoothTileDialogDelegate.contentHeight).thenReturn(getMutableStateFlow(0))
-        whenever(bluetoothTileDialogDelegate.bluetoothAutoOnToggle)
+        whenever(bluetoothDetailsContentManager.deviceItemClick)
+            .thenReturn(getMutableStateFlow(null))
+        whenever(bluetoothDetailsContentManager.contentHeight).thenReturn(getMutableStateFlow(0))
+        whenever(bluetoothDetailsContentManager.bluetoothAutoOnToggle)
             .thenReturn(getMutableStateFlow(false))
         whenever(expandable.dialogTransitionController(any())).thenReturn(controller)
+        whenever(mockView.context).thenReturn(mContext)
     }
 
     @Test
-    fun testShowDialog_noAnimation() {
+    fun testShowDetailsContent_noAnimation() {
         testScope.runTest {
-            bluetoothTileDialogViewModel.showDialog(null)
+            bluetoothTileDialogViewModel.showDetailsContent(null, null)
             runCurrent()
 
             verify(mDialogTransitionAnimator, never()).show(any(), any(), any())
@@ -184,9 +204,9 @@
     }
 
     @Test
-    fun testShowDialog_animated() {
+    fun testShowDetailsContent_animated() {
         testScope.runTest {
-            bluetoothTileDialogViewModel.showDialog(expandable)
+            bluetoothTileDialogViewModel.showDetailsContent(expandable, null)
             runCurrent()
 
             verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
@@ -194,10 +214,21 @@
     }
 
     @Test
-    fun testShowDialog_animated_callInBackgroundThread() {
+    fun testShowDetailsContent_animated_inDetailsView() {
+        testScope.runTest {
+            bluetoothTileDialogViewModel.showDetailsContent(expandable, mockView)
+            runCurrent()
+
+            verify(bluetoothDetailsContentManager).bind(mockView)
+            verify(bluetoothDetailsContentManager).start()
+        }
+    }
+
+    @Test
+    fun testShowDetailsContent_animated_callInBackgroundThread() {
         testScope.runTest {
             backgroundExecutor.execute {
-                bluetoothTileDialogViewModel.showDialog(expandable)
+                bluetoothTileDialogViewModel.showDetailsContent(expandable, null)
                 runCurrent()
 
                 verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
@@ -206,9 +237,22 @@
     }
 
     @Test
-    fun testShowDialog_fetchDeviceItem() {
+    fun testShowDetailsContent_animated_callInBackgroundThread_inDetailsView() {
         testScope.runTest {
-            bluetoothTileDialogViewModel.showDialog(null)
+            backgroundExecutor.execute {
+                bluetoothTileDialogViewModel.showDetailsContent(expandable, mockView)
+                runCurrent()
+
+                verify(bluetoothDetailsContentManager).bind(mockView)
+                verify(bluetoothDetailsContentManager).start()
+            }
+        }
+    }
+
+    @Test
+    fun testShowDetailsContent_fetchDeviceItem() {
+        testScope.runTest {
+            bluetoothTileDialogViewModel.showDetailsContent(null, null)
             runCurrent()
 
             verify(deviceItemInteractor).deviceItemUpdate
@@ -219,7 +263,7 @@
     fun testStartSettingsActivity_activityLaunched_dialogDismissed() {
         testScope.runTest {
             whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
-            bluetoothTileDialogViewModel.showDialog(null)
+            bluetoothTileDialogViewModel.showDetailsContent(null, null)
             runCurrent()
 
             val clickedView = View(context)
@@ -236,7 +280,7 @@
             val actual =
                 BluetoothTileDialogViewModel.UiProperties.build(
                     isBluetoothEnabled = true,
-                    isAutoOnToggleFeatureAvailable = true
+                    isAutoOnToggleFeatureAvailable = true,
                 )
             assertThat(actual.autoOnToggleVisibility).isEqualTo(GONE)
         }
@@ -248,7 +292,7 @@
             val actual =
                 BluetoothTileDialogViewModel.UiProperties.build(
                     isBluetoothEnabled = false,
-                    isAutoOnToggleFeatureAvailable = true
+                    isAutoOnToggleFeatureAvailable = true,
                 )
             assertThat(actual.autoOnToggleVisibility).isEqualTo(VISIBLE)
         }
@@ -260,7 +304,7 @@
             val actual =
                 BluetoothTileDialogViewModel.UiProperties.build(
                     isBluetoothEnabled = false,
-                    isAutoOnToggleFeatureAvailable = false
+                    isAutoOnToggleFeatureAvailable = false,
                 )
             assertThat(actual.autoOnToggleVisibility).isEqualTo(GONE)
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
index 330b887..1305b0c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
@@ -238,7 +238,8 @@
 
         tile.handleClick(null)
 
-        verify(bluetoothTileDialogViewModel).showDialog(null)
+        verify(bluetoothTileDialogViewModel)
+            .showDetailsContent(/* expandable= */ null, /* view= */ null)
     }
 
     @Test