Loading packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt +16 −8 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ 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 Loading @@ -44,7 +45,10 @@ class MirroringConfirmationDialog( 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) Loading @@ -63,6 +67,8 @@ class MirroringConfirmationDialog( visibility = if (showConcurrentDisplayInfo) View.VISIBLE else View.GONE } bottomSheet = requireViewById(R.id.cd_bottom_sheet) setOnDismissListener { if (!enabledPressed) { onCancelMirroring.onClick(null) Loading @@ -71,15 +77,17 @@ class MirroringConfirmationDialog( setupInsets() } private fun setupInsets() { private fun setupInsets(navbarInsets: Int = navbarBottomInsetsProvider()) { // This avoids overlap between dialog content and navigation bars. requireViewById<View>(R.id.cd_bottom_sheet).apply { val navbarInsets = navbarBottomInsetsProvider() val defaultDialogBottomInset = context.resources.getDimensionPixelSize(R.dimen.dialog_bottom_padding) // 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. updatePadding(bottom = max(navbarInsets, defaultDialogBottomInset)) 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) } } Loading packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt +11 −0 Original line number Diff line number Diff line Loading @@ -32,9 +32,12 @@ import dagger.Module import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.launch Loading @@ -57,6 +60,7 @@ constructor( private var dialog: Dialog? = null /** Starts listening for pending displays. */ @OptIn(FlowPreview::class) override fun start() { val pendingDisplayFlow = connectedDisplayInteractor.pendingDisplay val concurrentDisplaysInProgessFlow = Loading @@ -66,6 +70,13 @@ constructor( flow { emit(false) } } pendingDisplayFlow // Let's debounce for 2 reasons: // - prevent fast dialog flashes in case pending displays are available for just a few // millis // - Prevent jumps related to inset changes: when in 3 buttons navigation, device // unlock triggers a change in insets that might result in a jump of the dialog (if a // display was connected while on the lockscreen). .debounce(200.milliseconds) .combine(concurrentDisplaysInProgessFlow) { pendingDisplay, concurrentDisplaysInProgress -> if (pendingDisplay == null) { Loading packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialog.kt +30 −0 Original line number Diff line number Diff line Loading @@ -23,6 +23,9 @@ 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 Loading Loading @@ -70,16 +73,43 @@ open class SystemUIBottomSheetDialog( override fun onStart() { super.onStart() configurationController?.addCallback(onConfigChanged) window?.decorView?.setWindowInsetsAnimationCallback(insetsAnimationCallback) } override fun onStop() { super.onStop() configurationController?.removeCallback(onConfigChanged) window?.decorView?.setWindowInsetsAnimationCallback(null) } /** Called after any insets change. */ open fun onInsetsChanged(@InsetsType changedTypes: Int, insets: WindowInsets) {} /** Can be overridden by subclasses to receive config changed events. */ open fun onConfigurationChanged() {} 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 val onConfigChanged = object : ConfigurationListener { override fun onConfigChanged(newConfig: Configuration?) { Loading packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt +34 −0 Original line number Diff line number Diff line Loading @@ -16,14 +16,17 @@ 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 Loading @@ -41,6 +44,7 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { private val onStartMirroringCallback = mock<View.OnClickListener>() private val onCancelCallback = mock<View.OnClickListener>() @Before fun setUp() { MockitoAnnotations.initMocks(this) Loading Loading @@ -96,10 +100,40 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { 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 } } Loading
packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt +16 −8 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ 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 Loading @@ -44,7 +45,10 @@ class MirroringConfirmationDialog( 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) Loading @@ -63,6 +67,8 @@ class MirroringConfirmationDialog( visibility = if (showConcurrentDisplayInfo) View.VISIBLE else View.GONE } bottomSheet = requireViewById(R.id.cd_bottom_sheet) setOnDismissListener { if (!enabledPressed) { onCancelMirroring.onClick(null) Loading @@ -71,15 +77,17 @@ class MirroringConfirmationDialog( setupInsets() } private fun setupInsets() { private fun setupInsets(navbarInsets: Int = navbarBottomInsetsProvider()) { // This avoids overlap between dialog content and navigation bars. requireViewById<View>(R.id.cd_bottom_sheet).apply { val navbarInsets = navbarBottomInsetsProvider() val defaultDialogBottomInset = context.resources.getDimensionPixelSize(R.dimen.dialog_bottom_padding) // 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. updatePadding(bottom = max(navbarInsets, defaultDialogBottomInset)) 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) } } Loading
packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt +11 −0 Original line number Diff line number Diff line Loading @@ -32,9 +32,12 @@ import dagger.Module import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.launch Loading @@ -57,6 +60,7 @@ constructor( private var dialog: Dialog? = null /** Starts listening for pending displays. */ @OptIn(FlowPreview::class) override fun start() { val pendingDisplayFlow = connectedDisplayInteractor.pendingDisplay val concurrentDisplaysInProgessFlow = Loading @@ -66,6 +70,13 @@ constructor( flow { emit(false) } } pendingDisplayFlow // Let's debounce for 2 reasons: // - prevent fast dialog flashes in case pending displays are available for just a few // millis // - Prevent jumps related to inset changes: when in 3 buttons navigation, device // unlock triggers a change in insets that might result in a jump of the dialog (if a // display was connected while on the lockscreen). .debounce(200.milliseconds) .combine(concurrentDisplaysInProgessFlow) { pendingDisplay, concurrentDisplaysInProgress -> if (pendingDisplay == null) { Loading
packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialog.kt +30 −0 Original line number Diff line number Diff line Loading @@ -23,6 +23,9 @@ 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 Loading Loading @@ -70,16 +73,43 @@ open class SystemUIBottomSheetDialog( override fun onStart() { super.onStart() configurationController?.addCallback(onConfigChanged) window?.decorView?.setWindowInsetsAnimationCallback(insetsAnimationCallback) } override fun onStop() { super.onStop() configurationController?.removeCallback(onConfigChanged) window?.decorView?.setWindowInsetsAnimationCallback(null) } /** Called after any insets change. */ open fun onInsetsChanged(@InsetsType changedTypes: Int, insets: WindowInsets) {} /** Can be overridden by subclasses to receive config changed events. */ open fun onConfigurationChanged() {} 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 val onConfigChanged = object : ConfigurationListener { override fun onConfigChanged(newConfig: Configuration?) { Loading
packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt +34 −0 Original line number Diff line number Diff line Loading @@ -16,14 +16,17 @@ 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 Loading @@ -41,6 +44,7 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { private val onStartMirroringCallback = mock<View.OnClickListener>() private val onCancelCallback = mock<View.OnClickListener>() @Before fun setUp() { MockitoAnnotations.initMocks(this) Loading Loading @@ -96,10 +100,40 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { 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 } }