Merge "Make SystemUIBottomSheetDialog use DialogDelegate" into main
diff --git a/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt b/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt
deleted file mode 100644
index 989b0de..0000000
--- a/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2023 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.display.ui.view
-
-import android.content.Context
-import android.os.Bundle
-import android.view.View
-import android.view.WindowInsets
-import android.widget.TextView
-import androidx.core.view.updatePadding
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.phone.SystemUIBottomSheetDialog
-import com.android.systemui.statusbar.policy.ConfigurationController
-import kotlin.math.max
-
-/**
- * Dialog used to decide what to do with a connected display.
- *
- * [onCancelMirroring] is called **only** if mirroring didn't start, or when the dismiss button is
- * pressed.
- */
-class MirroringConfirmationDialog(
-    context: Context,
-    private val onStartMirroringClickListener: View.OnClickListener,
-    private val onCancelMirroring: View.OnClickListener,
-    private val navbarBottomInsetsProvider: () -> Int,
-    configurationController: ConfigurationController? = null,
-    private val showConcurrentDisplayInfo: Boolean = false,
-    theme: Int = R.style.Theme_SystemUI_Dialog,
-) : SystemUIBottomSheetDialog(context, configurationController, theme) {
-
-    private lateinit var mirrorButton: TextView
-    private lateinit var dismissButton: TextView
-    private lateinit var dualDisplayWarning: TextView
-    private lateinit var bottomSheet: View
-    private var enabledPressed = false
-    private val defaultDialogBottomInset =
-        context.resources.getDimensionPixelSize(R.dimen.dialog_bottom_padding)
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        setContentView(R.layout.connected_display_dialog)
-
-        mirrorButton =
-            requireViewById<TextView>(R.id.enable_display).apply {
-                setOnClickListener(onStartMirroringClickListener)
-                enabledPressed = true
-            }
-        dismissButton =
-            requireViewById<TextView>(R.id.cancel).apply { setOnClickListener(onCancelMirroring) }
-
-        dualDisplayWarning =
-            requireViewById<TextView>(R.id.dual_display_warning).apply {
-                visibility = if (showConcurrentDisplayInfo) View.VISIBLE else View.GONE
-            }
-
-        bottomSheet = requireViewById(R.id.cd_bottom_sheet)
-
-        setOnDismissListener {
-            if (!enabledPressed) {
-                onCancelMirroring.onClick(null)
-            }
-        }
-        setupInsets()
-    }
-
-    private fun setupInsets(navbarInsets: Int = navbarBottomInsetsProvider()) {
-        // This avoids overlap between dialog content and navigation bars.
-        // we only care about the bottom inset as in all other configuration where navigations
-        // are in other display sides there is no overlap with the dialog.
-        bottomSheet.updatePadding(bottom = max(navbarInsets, defaultDialogBottomInset))
-    }
-
-    override fun onInsetsChanged(changedTypes: Int, insets: WindowInsets) {
-        val navbarType = WindowInsets.Type.navigationBars()
-        if (changedTypes and navbarType != 0) {
-            setupInsets(insets.getInsets(navbarType).bottom)
-        }
-    }
-
-    override fun onConfigurationChanged() {
-        super.onConfigurationChanged()
-        setupInsets()
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogDelegate.kt
new file mode 100644
index 0000000..19b2673
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogDelegate.kt
@@ -0,0 +1,162 @@
+/*
+ * 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.display.ui.view
+
+import android.app.Dialog
+import android.content.Context
+import android.content.res.Configuration
+import android.os.Bundle
+import android.view.View
+import android.view.WindowInsets
+import android.view.WindowInsetsAnimation
+import android.widget.TextView
+import androidx.annotation.StyleRes
+import androidx.annotation.VisibleForTesting
+import androidx.core.view.updatePadding
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.DialogDelegate
+import com.android.systemui.statusbar.phone.SystemUIBottomSheetDialog
+import javax.inject.Inject
+import kotlin.math.max
+
+/**
+ * Dialog used to decide what to do with a connected display.
+ *
+ * [onCancelMirroring] is called **only** if mirroring didn't start, or when the dismiss button is
+ * pressed.
+ */
+class MirroringConfirmationDialogDelegate
+@VisibleForTesting
+constructor(
+    context: Context,
+    private val showConcurrentDisplayInfo: Boolean = false,
+    private val onStartMirroringClickListener: View.OnClickListener,
+    private val onCancelMirroring: View.OnClickListener,
+    private val navbarBottomInsetsProvider: () -> Int,
+) : DialogDelegate<Dialog> {
+
+    private lateinit var mirrorButton: TextView
+    private lateinit var dismissButton: TextView
+    private lateinit var dualDisplayWarning: TextView
+    private lateinit var bottomSheet: View
+    private var enabledPressed = false
+    private val defaultDialogBottomInset =
+        context.resources.getDimensionPixelSize(R.dimen.dialog_bottom_padding)
+
+    override fun onCreate(dialog: Dialog, savedInstanceState: Bundle?) {
+        dialog.setContentView(R.layout.connected_display_dialog)
+
+        mirrorButton =
+            dialog.requireViewById<TextView>(R.id.enable_display).apply {
+                setOnClickListener(onStartMirroringClickListener)
+                enabledPressed = true
+            }
+        dismissButton =
+            dialog.requireViewById<TextView>(R.id.cancel).apply {
+                setOnClickListener(onCancelMirroring)
+            }
+
+        dualDisplayWarning =
+            dialog.requireViewById<TextView>(R.id.dual_display_warning).apply {
+                visibility = if (showConcurrentDisplayInfo) View.VISIBLE else View.GONE
+            }
+
+        bottomSheet = dialog.requireViewById(R.id.cd_bottom_sheet)
+
+        dialog.setOnDismissListener {
+            if (!enabledPressed) {
+                onCancelMirroring.onClick(null)
+            }
+        }
+        setupInsets()
+    }
+
+    override fun onStart(dialog: Dialog) {
+        dialog.window?.decorView?.setWindowInsetsAnimationCallback(insetsAnimationCallback)
+    }
+
+    override fun onStop(dialog: Dialog) {
+        dialog.window?.decorView?.setWindowInsetsAnimationCallback(null)
+    }
+
+    private fun setupInsets(navbarInsets: Int = navbarBottomInsetsProvider()) {
+        // This avoids overlap between dialog content and navigation bars.
+        // we only care about the bottom inset as in all other configuration where navigations
+        // are in other display sides there is no overlap with the dialog.
+        bottomSheet.updatePadding(bottom = max(navbarInsets, defaultDialogBottomInset))
+    }
+
+    override fun onConfigurationChanged(dialog: Dialog, configuration: Configuration) {
+        setupInsets()
+    }
+
+    private val insetsAnimationCallback =
+        object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
+
+            private var lastInsets: WindowInsets? = null
+
+            override fun onEnd(animation: WindowInsetsAnimation) {
+                lastInsets?.let { onInsetsChanged(animation.typeMask, it) }
+            }
+
+            override fun onProgress(
+                insets: WindowInsets,
+                animations: MutableList<WindowInsetsAnimation>,
+            ): WindowInsets {
+                lastInsets = insets
+                onInsetsChanged(changedTypes = allAnimationMasks(animations), insets)
+                return insets
+            }
+
+            private fun allAnimationMasks(animations: List<WindowInsetsAnimation>): Int =
+                animations.fold(0) { acc: Int, it -> acc or it.typeMask }
+
+            private fun onInsetsChanged(changedTypes: Int, insets: WindowInsets) {
+                val navbarType = WindowInsets.Type.navigationBars()
+                if (changedTypes and navbarType != 0) {
+                    setupInsets(insets.getInsets(navbarType).bottom)
+                }
+            }
+        }
+
+    class Factory
+    @Inject
+    constructor(
+        @Application private val context: Context,
+        private val dialogFactory: SystemUIBottomSheetDialog.Factory,
+    ) {
+
+        fun createDialog(
+            showConcurrentDisplayInfo: Boolean = false,
+            onStartMirroringClickListener: View.OnClickListener,
+            onCancelMirroring: View.OnClickListener,
+            navbarBottomInsetsProvider: () -> Int,
+            @StyleRes theme: Int = R.style.Theme_SystemUI_Dialog,
+        ): Dialog =
+            dialogFactory.create(
+                delegate =
+                    MirroringConfirmationDialogDelegate(
+                        context = context,
+                        showConcurrentDisplayInfo = showConcurrentDisplayInfo,
+                        onStartMirroringClickListener = onStartMirroringClickListener,
+                        onCancelMirroring = onCancelMirroring,
+                        navbarBottomInsetsProvider = navbarBottomInsetsProvider,
+                    ),
+                theme = theme,
+            )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt
index fbf0538..81ea2e7 100644
--- a/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt
@@ -25,8 +25,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
 import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay
-import com.android.systemui.display.ui.view.MirroringConfirmationDialog
-import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.display.ui.view.MirroringConfirmationDialogDelegate
 import dagger.Binds
 import dagger.Module
 import dagger.multibindings.ClassKey
@@ -54,7 +53,7 @@
     private val connectedDisplayInteractor: ConnectedDisplayInteractor,
     @Application private val scope: CoroutineScope,
     @Background private val bgDispatcher: CoroutineDispatcher,
-    private val configurationController: ConfigurationController,
+    private val bottomSheetFactory: MirroringConfirmationDialogDelegate.Factory,
 ) : CoreStartable {
 
     private var dialog: Dialog? = null
@@ -91,8 +90,8 @@
     private fun showDialog(pendingDisplay: PendingDisplay, concurrentDisplaysInProgess: Boolean) {
         hideDialog()
         dialog =
-            MirroringConfirmationDialog(
-                    context,
+            bottomSheetFactory
+                .createDialog(
                     onStartMirroringClickListener = {
                         scope.launch(bgDispatcher) { pendingDisplay.enable() }
                         hideDialog()
@@ -102,7 +101,6 @@
                         hideDialog()
                     },
                     navbarBottomInsetsProvider = { Utils.getNavbarInsets(context).bottom },
-                    configurationController,
                     showConcurrentDisplayInfo = concurrentDisplaysInProgess
                 )
                 .apply { show() }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialog.kt
index 541ac48..31c2134 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialog.kt
@@ -15,36 +15,55 @@
  */
 package com.android.systemui.statusbar.phone
 
+import android.annotation.StyleRes
 import android.app.Dialog
 import android.content.Context
-import android.content.res.Configuration
 import android.graphics.Color
 import android.graphics.drawable.ColorDrawable
 import android.os.Bundle
 import android.view.Gravity
 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
-import android.view.WindowInsets
-import android.view.WindowInsets.Type.InsetsType
-import android.view.WindowInsetsAnimation
 import android.view.WindowManager.LayoutParams.MATCH_PARENT
 import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
 import android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
 import android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL
+import androidx.activity.ComponentDialog
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
+import com.android.systemui.statusbar.policy.onConfigChanged
+import dagger.Lazy
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
 
 /** A dialog shown as a bottom sheet. */
-open class SystemUIBottomSheetDialog(
+class SystemUIBottomSheetDialog
+@VisibleForTesting
+constructor(
     context: Context,
-    private val configurationController: ConfigurationController? = null,
-    theme: Int = R.style.Theme_SystemUI_Dialog
-) : Dialog(context, theme) {
+    private val coroutineScope: CoroutineScope,
+    private val configurationController: ConfigurationController,
+    private val delegate: DialogDelegate<in Dialog>,
+    private val windowLayout: WindowLayout,
+    theme: Int,
+) : ComponentDialog(context, theme) {
+
+    private var job: Job? = null
+
     override fun onCreate(savedInstanceState: Bundle?) {
+        delegate.beforeCreate(this, savedInstanceState)
         super.onCreate(savedInstanceState)
         setupWindow()
-        setupEdgeToEdge()
         setCanceledOnTouchOutside(true)
+        delegate.onCreate(this, savedInstanceState)
     }
 
     private fun setupWindow() {
@@ -62,60 +81,84 @@
         }
     }
 
-    private fun setupEdgeToEdge() {
-        val edgeToEdgeHorizontally =
-            context.resources.getBoolean(R.bool.config_edgeToEdgeBottomSheetDialog)
-        val width = if (edgeToEdgeHorizontally) MATCH_PARENT else WRAP_CONTENT
-        val height = WRAP_CONTENT
-        window?.setLayout(width, height)
-    }
-
     override fun onStart() {
         super.onStart()
-        configurationController?.addCallback(onConfigChanged)
-        window?.decorView?.setWindowInsetsAnimationCallback(insetsAnimationCallback)
+        job?.cancel()
+        job =
+            coroutineScope.launch {
+                windowLayout
+                    .calculate()
+                    .onEach { window?.apply { setLayout(it.width, it.height) } }
+                    .launchIn(this)
+                configurationController.onConfigChanged
+                    .onEach { delegate.onConfigurationChanged(this@SystemUIBottomSheetDialog, it) }
+                    .launchIn(this)
+            }
+        delegate.onStart(this)
     }
 
     override fun onStop() {
+        job?.cancel()
+        delegate.onStop(this)
         super.onStop()
-        configurationController?.removeCallback(onConfigChanged)
-        window?.decorView?.setWindowInsetsAnimationCallback(null)
     }
 
-    /** Called after any insets change. */
-    open fun onInsetsChanged(@InsetsType changedTypes: Int, insets: WindowInsets) {}
+    override fun onWindowFocusChanged(hasFocus: Boolean) {
+        super.onWindowFocusChanged(hasFocus)
+        delegate.onWindowFocusChanged(this, hasFocus)
+    }
 
-    /** Can be overridden by subclasses to receive config changed events. */
-    open fun onConfigurationChanged() {}
+    class Factory
+    @Inject
+    constructor(
+        @Application private val context: Context,
+        @Application private val coroutineScope: CoroutineScope,
+        private val defaultWindowLayout: Lazy<WindowLayout.LimitedEdgeToEdge>,
+        private val configurationController: ConfigurationController,
+    ) {
 
-    private val insetsAnimationCallback =
-        object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
+        fun create(
+            delegate: DialogDelegate<in Dialog>,
+            windowLayout: WindowLayout = defaultWindowLayout.get(),
+            @StyleRes theme: Int = R.style.Theme_SystemUI_Dialog,
+        ): SystemUIBottomSheetDialog =
+            SystemUIBottomSheetDialog(
+                context = context,
+                configurationController = configurationController,
+                coroutineScope = coroutineScope,
+                delegate = delegate,
+                windowLayout = windowLayout,
+                theme = theme,
+            )
+    }
 
-            private var lastInsets: WindowInsets? = null
+    /** [SystemUIBottomSheetDialog] uses this to determine the [android.view.Window] layout. */
+    interface WindowLayout {
 
-            override fun onEnd(animation: WindowInsetsAnimation) {
-                lastInsets?.let { onInsetsChanged(animation.typeMask, it) }
-            }
+        /** Returns a [Layout] to apply to [android.view.Window.setLayout]. */
+        fun calculate(): Flow<Layout>
 
-            override fun onProgress(
-                insets: WindowInsets,
-                animations: MutableList<WindowInsetsAnimation>,
-            ): WindowInsets {
-                lastInsets = insets
-                onInsetsChanged(changedTypes = allAnimationMasks(animations), insets)
-                return insets
-            }
+        /** Edge to edge with which doesn't fill the whole space on the large screen. */
+        class LimitedEdgeToEdge
+        @Inject
+        constructor(
+            @Application private val context: Context,
+            private val configurationController: ConfigurationController,
+        ) : WindowLayout {
 
-            private fun allAnimationMasks(animations: List<WindowInsetsAnimation>): Int =
-                animations.fold(0) { acc: Int, it -> acc or it.typeMask }
-        }
+            override fun calculate(): Flow<Layout> {
+                return configurationController.onConfigChanged
+                    .onStart { emit(context.resources.configuration) }
+                    .map {
+                        val edgeToEdgeHorizontally =
+                            context.resources.getBoolean(R.bool.config_edgeToEdgeBottomSheetDialog)
+                        val width = if (edgeToEdgeHorizontally) MATCH_PARENT else WRAP_CONTENT
 
-    private val onConfigChanged =
-        object : ConfigurationListener {
-            override fun onConfigChanged(newConfig: Configuration?) {
-                super.onConfigChanged(newConfig)
-                setupEdgeToEdge()
-                onConfigurationChanged()
+                        Layout(width, WRAP_CONTENT)
+                    }
             }
         }
+
+        data class Layout(val width: Int, val height: Int)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogDelegateTest.kt
new file mode 100644
index 0000000..d118cc7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogDelegateTest.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2023 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.display.ui.view
+
+import android.app.Dialog
+import android.graphics.Insets
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.Window
+import android.view.WindowInsets
+import android.view.WindowInsetsAnimation
+import androidx.test.filters.SmallTest
+import com.android.app.animation.Interpolators
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.res.R
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class MirroringConfirmationDialogDelegateTest : SysuiTestCase() {
+
+    private lateinit var underTest: MirroringConfirmationDialogDelegate
+
+    private val onStartMirroringCallback = mock<View.OnClickListener>()
+    private val onCancelCallback = mock<View.OnClickListener>()
+    private val windowDecorView: View = mock {}
+    private val windowInsetsAnimationCallbackCaptor =
+        ArgumentCaptor.forClass(WindowInsetsAnimation.Callback::class.java)
+    private val dialog: Dialog =
+        mock<Dialog> {
+            var view: View? = null
+            whenever(setContentView(any<Int>())).then {
+                view =
+                    LayoutInflater.from(this@MirroringConfirmationDialogDelegateTest.context)
+                        .inflate(it.arguments[0] as Int, null, false)
+                Unit
+            }
+            whenever(requireViewById<View>(any<Int>())).then {
+                view?.requireViewById(it.arguments[0] as Int)
+            }
+            val window: Window = mock { whenever(decorView).thenReturn(windowDecorView) }
+            whenever(this.window).thenReturn(window)
+        }
+
+    @Before
+    fun setUp() {
+        underTest =
+            MirroringConfirmationDialogDelegate(
+                context = context,
+                showConcurrentDisplayInfo = false,
+                onStartMirroringClickListener = onStartMirroringCallback,
+                onCancelMirroring = onCancelCallback,
+                navbarBottomInsetsProvider = { 0 },
+            )
+    }
+
+    @Test
+    fun startMirroringButton_clicked_callsCorrectCallback() {
+        underTest.onCreate(dialog, null)
+
+        dialog.requireViewById<View>(R.id.enable_display).callOnClick()
+
+        verify(onStartMirroringCallback).onClick(any())
+        verify(onCancelCallback, never()).onClick(any())
+    }
+
+    @Test
+    fun cancelButton_clicked_callsCorrectCallback() {
+        underTest.onCreate(dialog, null)
+
+        dialog.requireViewById<View>(R.id.cancel).callOnClick()
+
+        verify(onCancelCallback).onClick(any())
+        verify(onStartMirroringCallback, never()).onClick(any())
+    }
+
+    @Test
+    fun onCancel_afterEnablingMirroring_cancelCallbackNotCalled() {
+        underTest.onCreate(dialog, null)
+        dialog.requireViewById<View>(R.id.enable_display).callOnClick()
+
+        underTest.onStop(dialog)
+
+        verify(onCancelCallback, never()).onClick(any())
+        verify(onStartMirroringCallback).onClick(any())
+    }
+
+    @Test
+    fun onDismiss_afterEnablingMirroring_cancelCallbackNotCalled() {
+        underTest.onCreate(dialog, null)
+        dialog.requireViewById<View>(R.id.enable_display).callOnClick()
+
+        underTest.onStop(dialog)
+
+        verify(onCancelCallback, never()).onClick(any())
+        verify(onStartMirroringCallback).onClick(any())
+    }
+
+    @Test
+    fun onInsetsChanged_navBarInsets_updatesBottomPadding() {
+        underTest.onCreate(dialog, null)
+        underTest.onStart(dialog)
+
+        val insets = buildInsets(WindowInsets.Type.navigationBars(), TEST_BOTTOM_INSETS)
+
+        triggerInsetsChanged(WindowInsets.Type.navigationBars(), insets)
+
+        assertThat(dialog.requireViewById<View>(R.id.cd_bottom_sheet).paddingBottom)
+            .isEqualTo(TEST_BOTTOM_INSETS)
+    }
+
+    @Test
+    fun onInsetsChanged_otherType_doesNotUpdateBottomPadding() {
+        underTest.onCreate(dialog, null)
+        underTest.onStart(dialog)
+
+        val insets = buildInsets(WindowInsets.Type.ime(), TEST_BOTTOM_INSETS)
+        triggerInsetsChanged(WindowInsets.Type.ime(), insets)
+
+        assertThat(dialog.requireViewById<View>(R.id.cd_bottom_sheet).paddingBottom)
+            .isNotEqualTo(TEST_BOTTOM_INSETS)
+    }
+
+    private fun buildInsets(@WindowInsets.Type.InsetsType type: Int, bottom: Int): WindowInsets {
+        return WindowInsets.Builder().setInsets(type, Insets.of(0, 0, 0, bottom)).build()
+    }
+
+    private fun triggerInsetsChanged(type: Int, insets: WindowInsets) {
+        verify(windowDecorView)
+            .setWindowInsetsAnimationCallback(capture(windowInsetsAnimationCallbackCaptor))
+        windowInsetsAnimationCallbackCaptor.value.onProgress(
+            insets,
+            listOf(WindowInsetsAnimation(type, Interpolators.INSTANT, 0))
+        )
+    }
+
+    private companion object {
+        const val TEST_BOTTOM_INSETS = 1000 // arbitrarily high number
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt
deleted file mode 100644
index 30519b0..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (C) 2023 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.display.ui.view
-
-import android.graphics.Insets
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.view.View
-import android.view.WindowInsets
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.res.R
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
-import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-class MirroringConfirmationDialogTest : SysuiTestCase() {
-
-    private lateinit var dialog: MirroringConfirmationDialog
-
-    private val onStartMirroringCallback = mock<View.OnClickListener>()
-    private val onCancelCallback = mock<View.OnClickListener>()
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
-        dialog =
-            MirroringConfirmationDialog(
-                context,
-                onStartMirroringCallback,
-                onCancelCallback,
-                navbarBottomInsetsProvider = { 0 },
-            )
-    }
-
-    @Test
-    fun startMirroringButton_clicked_callsCorrectCallback() {
-        dialog.show()
-
-        dialog.requireViewById<View>(R.id.enable_display).callOnClick()
-
-        verify(onStartMirroringCallback).onClick(any())
-        verify(onCancelCallback, never()).onClick(any())
-    }
-
-    @Test
-    fun cancelButton_clicked_callsCorrectCallback() {
-        dialog.show()
-
-        dialog.requireViewById<View>(R.id.cancel).callOnClick()
-
-        verify(onCancelCallback).onClick(any())
-        verify(onStartMirroringCallback, never()).onClick(any())
-    }
-
-    @Test
-    fun onCancel_afterEnablingMirroring_cancelCallbackNotCalled() {
-        dialog.show()
-        dialog.requireViewById<View>(R.id.enable_display).callOnClick()
-
-        dialog.cancel()
-
-        verify(onCancelCallback, never()).onClick(any())
-        verify(onStartMirroringCallback).onClick(any())
-    }
-
-    @Test
-    fun onDismiss_afterEnablingMirroring_cancelCallbackNotCalled() {
-        dialog.show()
-        dialog.requireViewById<View>(R.id.enable_display).callOnClick()
-
-        dialog.dismiss()
-
-        verify(onCancelCallback, never()).onClick(any())
-        verify(onStartMirroringCallback).onClick(any())
-    }
-
-    @Test
-    fun onInsetsChanged_navBarInsets_updatesBottomPadding() {
-        dialog.show()
-
-        val insets = buildInsets(WindowInsets.Type.navigationBars(), TEST_BOTTOM_INSETS)
-        dialog.onInsetsChanged(WindowInsets.Type.navigationBars(), insets)
-
-        assertThat(dialog.requireViewById<View>(R.id.cd_bottom_sheet).paddingBottom)
-            .isEqualTo(TEST_BOTTOM_INSETS)
-    }
-
-    @Test
-    fun onInsetsChanged_otherType_doesNotUpdateBottomPadding() {
-        dialog.show()
-
-        val insets = buildInsets(WindowInsets.Type.ime(), TEST_BOTTOM_INSETS)
-        dialog.onInsetsChanged(WindowInsets.Type.ime(), insets)
-
-        assertThat(dialog.requireViewById<View>(R.id.cd_bottom_sheet).paddingBottom)
-            .isNotEqualTo(TEST_BOTTOM_INSETS)
-    }
-
-    private fun buildInsets(@WindowInsets.Type.InsetsType type: Int, bottom: Int): WindowInsets {
-        return WindowInsets.Builder().setInsets(type, Insets.of(0, 0, 0, bottom)).build()
-    }
-
-    @After
-    fun teardown() {
-        if (::dialog.isInitialized) {
-            dialog.dismiss()
-        }
-    }
-
-    private companion object {
-        const val TEST_BOTTOM_INSETS = 1000 // arbitrarily high number
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt
index 1e628bd..dedd0af 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt
@@ -13,68 +13,103 @@
  */
 package com.android.systemui.statusbar.phone
 
+import android.app.Dialog
 import android.content.res.Configuration
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
+import android.view.WindowManager
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.mock
-import com.google.common.truth.Truth.assertThat
 import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.runner.RunWith
 import org.mockito.Mockito.verify
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper(setAsMainLooper = true)
 class SystemUIBottomSheetDialogTest : SysuiTestCase() {
 
+    private val kosmos = testKosmos()
     private val configurationController = mock<ConfigurationController>()
     private val config = mock<Configuration>()
+    private val delegate = mock<DialogDelegate<Dialog>>()
 
     private lateinit var dialog: SystemUIBottomSheetDialog
 
     @Before
     fun setup() {
-        dialog = SystemUIBottomSheetDialog(mContext, configurationController)
+        dialog =
+            with(kosmos) {
+                SystemUIBottomSheetDialog(
+                    context,
+                    testScope.backgroundScope,
+                    configurationController,
+                    delegate,
+                    TestLayout(),
+                    0,
+                )
+            }
     }
 
     @Test
     fun onStart_registersConfigCallback() {
-        dialog.show()
+        kosmos.testScope.runTest {
+            dialog.show()
+            runCurrent()
 
-        verify(configurationController).addCallback(any())
+            verify(configurationController).addCallback(any())
+        }
     }
 
     @Test
     fun onStop_unregisterConfigCallback() {
-        dialog.show()
-        dialog.dismiss()
+        kosmos.testScope.runTest {
+            dialog.show()
+            runCurrent()
+            dialog.dismiss()
+            runCurrent()
 
-        verify(configurationController).removeCallback(any())
+            verify(configurationController).removeCallback(any())
+        }
     }
 
     @Test
-    fun onConfigurationChanged_calledInSubclass() {
-        var onConfigChangedCalled = false
-        val subclass =
-            object : SystemUIBottomSheetDialog(mContext, configurationController) {
-                override fun onConfigurationChanged() {
-                    onConfigChangedCalled = true
-                }
-            }
+    fun onConfigurationChanged_calledInDelegate() {
+        kosmos.testScope.runTest {
+            dialog.show()
+            runCurrent()
+            val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
+            verify(configurationController).addCallback(capture(captor))
 
-        subclass.show()
+            captor.value.onConfigChanged(config)
+            runCurrent()
 
-        val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
-        verify(configurationController).addCallback(capture(captor))
-        captor.value.onConfigChanged(config)
+            verify(delegate).onConfigurationChanged(any(), any())
+        }
+    }
 
-        assertThat(onConfigChangedCalled).isTrue()
+    private class TestLayout : SystemUIBottomSheetDialog.WindowLayout {
+        override fun calculate(): Flow<SystemUIBottomSheetDialog.WindowLayout.Layout> {
+            return flowOf(
+                SystemUIBottomSheetDialog.WindowLayout.Layout(
+                    WindowManager.LayoutParams.MATCH_PARENT,
+                    WindowManager.LayoutParams.MATCH_PARENT,
+                )
+            )
+        }
     }
 }