Loading packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt +16 −8 Original line number Original line Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.systemui.display.ui.view import android.content.Context import android.content.Context import android.os.Bundle import android.os.Bundle import android.view.View import android.view.View import android.view.WindowInsets import android.widget.TextView import android.widget.TextView import androidx.core.view.updatePadding import androidx.core.view.updatePadding import com.android.systemui.res.R import com.android.systemui.res.R Loading @@ -44,7 +45,10 @@ class MirroringConfirmationDialog( private lateinit var mirrorButton: TextView private lateinit var mirrorButton: TextView private lateinit var dismissButton: TextView private lateinit var dismissButton: TextView private lateinit var dualDisplayWarning: TextView private lateinit var dualDisplayWarning: TextView private lateinit var bottomSheet: View private var enabledPressed = false private var enabledPressed = false private val defaultDialogBottomInset = context.resources.getDimensionPixelSize(R.dimen.dialog_bottom_padding) override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) super.onCreate(savedInstanceState) Loading @@ -63,6 +67,8 @@ class MirroringConfirmationDialog( visibility = if (showConcurrentDisplayInfo) View.VISIBLE else View.GONE visibility = if (showConcurrentDisplayInfo) View.VISIBLE else View.GONE } } bottomSheet = requireViewById(R.id.cd_bottom_sheet) setOnDismissListener { setOnDismissListener { if (!enabledPressed) { if (!enabledPressed) { onCancelMirroring.onClick(null) onCancelMirroring.onClick(null) Loading @@ -71,15 +77,17 @@ class MirroringConfirmationDialog( setupInsets() setupInsets() } } private fun setupInsets() { private fun setupInsets(navbarInsets: Int = navbarBottomInsetsProvider()) { // This avoids overlap between dialog content and navigation bars. // 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 // 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. // 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 Original line Diff line number Diff line Loading @@ -32,9 +32,12 @@ import dagger.Module import dagger.multibindings.ClassKey import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap import dagger.multibindings.IntoMap import javax.inject.Inject import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.launch import kotlinx.coroutines.launch Loading @@ -57,6 +60,7 @@ constructor( private var dialog: Dialog? = null private var dialog: Dialog? = null /** Starts listening for pending displays. */ /** Starts listening for pending displays. */ @OptIn(FlowPreview::class) override fun start() { override fun start() { val pendingDisplayFlow = connectedDisplayInteractor.pendingDisplay val pendingDisplayFlow = connectedDisplayInteractor.pendingDisplay val concurrentDisplaysInProgessFlow = val concurrentDisplaysInProgessFlow = Loading @@ -66,6 +70,13 @@ constructor( flow { emit(false) } flow { emit(false) } } } pendingDisplayFlow 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 .combine(concurrentDisplaysInProgessFlow) { pendingDisplay, concurrentDisplaysInProgress -> -> if (pendingDisplay == null) { if (pendingDisplay == null) { Loading packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialog.kt +30 −0 Original line number Original line Diff line number Diff line Loading @@ -23,6 +23,9 @@ import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.os.Bundle import android.view.Gravity import android.view.Gravity import android.view.ViewGroup.LayoutParams.WRAP_CONTENT 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.MATCH_PARENT import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION 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.SYSTEM_FLAG_SHOW_FOR_ALL_USERS Loading Loading @@ -70,16 +73,43 @@ open class SystemUIBottomSheetDialog( override fun onStart() { override fun onStart() { super.onStart() super.onStart() configurationController?.addCallback(onConfigChanged) configurationController?.addCallback(onConfigChanged) window?.decorView?.setWindowInsetsAnimationCallback(insetsAnimationCallback) } } override fun onStop() { override fun onStop() { super.onStop() super.onStop() configurationController?.removeCallback(onConfigChanged) 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. */ /** Can be overridden by subclasses to receive config changed events. */ open fun onConfigurationChanged() {} 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 = private val onConfigChanged = object : ConfigurationListener { object : ConfigurationListener { override fun onConfigChanged(newConfig: Configuration?) { override fun onConfigChanged(newConfig: Configuration?) { Loading packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt +34 −0 Original line number Original line Diff line number Diff line Loading @@ -16,14 +16,17 @@ package com.android.systemui.display.ui.view package com.android.systemui.display.ui.view import android.graphics.Insets import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.testing.TestableLooper import android.view.View import android.view.View import android.view.WindowInsets import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase import com.android.systemui.res.R import com.android.systemui.res.R import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.After import org.junit.Before import org.junit.Before import org.junit.Test import org.junit.Test Loading @@ -41,6 +44,7 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { private val onStartMirroringCallback = mock<View.OnClickListener>() private val onStartMirroringCallback = mock<View.OnClickListener>() private val onCancelCallback = mock<View.OnClickListener>() private val onCancelCallback = mock<View.OnClickListener>() @Before @Before fun setUp() { fun setUp() { MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this) Loading Loading @@ -96,10 +100,40 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { verify(onStartMirroringCallback).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 @After fun teardown() { fun teardown() { if (::dialog.isInitialized) { if (::dialog.isInitialized) { dialog.dismiss() 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 Original line Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.systemui.display.ui.view import android.content.Context import android.content.Context import android.os.Bundle import android.os.Bundle import android.view.View import android.view.View import android.view.WindowInsets import android.widget.TextView import android.widget.TextView import androidx.core.view.updatePadding import androidx.core.view.updatePadding import com.android.systemui.res.R import com.android.systemui.res.R Loading @@ -44,7 +45,10 @@ class MirroringConfirmationDialog( private lateinit var mirrorButton: TextView private lateinit var mirrorButton: TextView private lateinit var dismissButton: TextView private lateinit var dismissButton: TextView private lateinit var dualDisplayWarning: TextView private lateinit var dualDisplayWarning: TextView private lateinit var bottomSheet: View private var enabledPressed = false private var enabledPressed = false private val defaultDialogBottomInset = context.resources.getDimensionPixelSize(R.dimen.dialog_bottom_padding) override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) super.onCreate(savedInstanceState) Loading @@ -63,6 +67,8 @@ class MirroringConfirmationDialog( visibility = if (showConcurrentDisplayInfo) View.VISIBLE else View.GONE visibility = if (showConcurrentDisplayInfo) View.VISIBLE else View.GONE } } bottomSheet = requireViewById(R.id.cd_bottom_sheet) setOnDismissListener { setOnDismissListener { if (!enabledPressed) { if (!enabledPressed) { onCancelMirroring.onClick(null) onCancelMirroring.onClick(null) Loading @@ -71,15 +77,17 @@ class MirroringConfirmationDialog( setupInsets() setupInsets() } } private fun setupInsets() { private fun setupInsets(navbarInsets: Int = navbarBottomInsetsProvider()) { // This avoids overlap between dialog content and navigation bars. // 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 // 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. // 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 Original line Diff line number Diff line Loading @@ -32,9 +32,12 @@ import dagger.Module import dagger.multibindings.ClassKey import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap import dagger.multibindings.IntoMap import javax.inject.Inject import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.launch import kotlinx.coroutines.launch Loading @@ -57,6 +60,7 @@ constructor( private var dialog: Dialog? = null private var dialog: Dialog? = null /** Starts listening for pending displays. */ /** Starts listening for pending displays. */ @OptIn(FlowPreview::class) override fun start() { override fun start() { val pendingDisplayFlow = connectedDisplayInteractor.pendingDisplay val pendingDisplayFlow = connectedDisplayInteractor.pendingDisplay val concurrentDisplaysInProgessFlow = val concurrentDisplaysInProgessFlow = Loading @@ -66,6 +70,13 @@ constructor( flow { emit(false) } flow { emit(false) } } } pendingDisplayFlow 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 .combine(concurrentDisplaysInProgessFlow) { pendingDisplay, concurrentDisplaysInProgress -> -> if (pendingDisplay == null) { if (pendingDisplay == null) { Loading
packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialog.kt +30 −0 Original line number Original line Diff line number Diff line Loading @@ -23,6 +23,9 @@ import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.os.Bundle import android.view.Gravity import android.view.Gravity import android.view.ViewGroup.LayoutParams.WRAP_CONTENT 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.MATCH_PARENT import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION 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.SYSTEM_FLAG_SHOW_FOR_ALL_USERS Loading Loading @@ -70,16 +73,43 @@ open class SystemUIBottomSheetDialog( override fun onStart() { override fun onStart() { super.onStart() super.onStart() configurationController?.addCallback(onConfigChanged) configurationController?.addCallback(onConfigChanged) window?.decorView?.setWindowInsetsAnimationCallback(insetsAnimationCallback) } } override fun onStop() { override fun onStop() { super.onStop() super.onStop() configurationController?.removeCallback(onConfigChanged) 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. */ /** Can be overridden by subclasses to receive config changed events. */ open fun onConfigurationChanged() {} 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 = private val onConfigChanged = object : ConfigurationListener { object : ConfigurationListener { override fun onConfigChanged(newConfig: Configuration?) { override fun onConfigChanged(newConfig: Configuration?) { Loading
packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt +34 −0 Original line number Original line Diff line number Diff line Loading @@ -16,14 +16,17 @@ package com.android.systemui.display.ui.view package com.android.systemui.display.ui.view import android.graphics.Insets import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.testing.TestableLooper import android.view.View import android.view.View import android.view.WindowInsets import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase import com.android.systemui.res.R import com.android.systemui.res.R import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.After import org.junit.Before import org.junit.Before import org.junit.Test import org.junit.Test Loading @@ -41,6 +44,7 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { private val onStartMirroringCallback = mock<View.OnClickListener>() private val onStartMirroringCallback = mock<View.OnClickListener>() private val onCancelCallback = mock<View.OnClickListener>() private val onCancelCallback = mock<View.OnClickListener>() @Before @Before fun setUp() { fun setUp() { MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this) Loading Loading @@ -96,10 +100,40 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { verify(onStartMirroringCallback).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 @After fun teardown() { fun teardown() { if (::dialog.isInitialized) { if (::dialog.isInitialized) { dialog.dismiss() dialog.dismiss() } } } } private companion object { const val TEST_BOTTOM_INSETS = 1000 // arbitrarily high number } } }