diff options
4 files changed, 91 insertions, 8 deletions
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 index c0a873ac9a65..989b0de85e0c 100644 --- a/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt @@ -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 @@ -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) @@ -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) @@ -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)) + // 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) } } 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 190062cdcca1..fbf0538e4bae 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 @@ -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 @@ -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 = @@ -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) { 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 71e25e9556eb..541ac48a2feb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialog.kt @@ -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 @@ -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?) { 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 index b25fb6e2757a..30519b0569d7 100644 --- 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 @@ -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 @@ -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) @@ -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 + } } |