Loading packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt→packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogDelegate.kt +162 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * 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. Loading @@ -15,15 +15,22 @@ */ 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 com.android.systemui.statusbar.policy.ConfigurationController import javax.inject.Inject import kotlin.math.max /** Loading @@ -32,15 +39,15 @@ import kotlin.math.max * [onCancelMirroring] is called **only** if mirroring didn't start, or when the dismiss button is * pressed. */ class MirroringConfirmationDialog( 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, configurationController: ConfigurationController? = null, private val showConcurrentDisplayInfo: Boolean = false, theme: Int = R.style.Theme_SystemUI_Dialog, ) : SystemUIBottomSheetDialog(context, configurationController, theme) { ) : DialogDelegate<Dialog> { private lateinit var mirrorButton: TextView private lateinit var dismissButton: TextView Loading @@ -50,26 +57,27 @@ class MirroringConfirmationDialog( 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) override fun onCreate(dialog: Dialog, savedInstanceState: Bundle?) { dialog.setContentView(R.layout.connected_display_dialog) mirrorButton = requireViewById<TextView>(R.id.enable_display).apply { dialog.requireViewById<TextView>(R.id.enable_display).apply { setOnClickListener(onStartMirroringClickListener) enabledPressed = true } dismissButton = requireViewById<TextView>(R.id.cancel).apply { setOnClickListener(onCancelMirroring) } dialog.requireViewById<TextView>(R.id.cancel).apply { setOnClickListener(onCancelMirroring) } dualDisplayWarning = requireViewById<TextView>(R.id.dual_display_warning).apply { dialog.requireViewById<TextView>(R.id.dual_display_warning).apply { visibility = if (showConcurrentDisplayInfo) View.VISIBLE else View.GONE } bottomSheet = requireViewById(R.id.cd_bottom_sheet) bottomSheet = dialog.requireViewById(R.id.cd_bottom_sheet) setOnDismissListener { dialog.setOnDismissListener { if (!enabledPressed) { onCancelMirroring.onClick(null) } Loading @@ -77,6 +85,14 @@ class MirroringConfirmationDialog( 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 Loading @@ -84,15 +100,63 @@ class MirroringConfirmationDialog( bottomSheet.updatePadding(bottom = max(navbarInsets, defaultDialogBottomInset)) } override fun onInsetsChanged(changedTypes: Int, insets: WindowInsets) { 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) } } } override fun onConfigurationChanged() { super.onConfigurationChanged() setupInsets() 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, ) } } packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt +4 −6 Original line number Diff line number Diff line Loading @@ -25,8 +25,7 @@ import com.android.systemui.dagger.qualifiers.Application 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 Loading Loading @@ -54,7 +53,7 @@ constructor( 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 Loading Loading @@ -91,8 +90,8 @@ constructor( private fun showDialog(pendingDisplay: PendingDisplay, concurrentDisplaysInProgess: Boolean) { hideDialog() dialog = MirroringConfirmationDialog( context, bottomSheetFactory .createDialog( onStartMirroringClickListener = { scope.launch(bgDispatcher) { pendingDisplay.enable() } hideDialog() Loading @@ -102,7 +101,6 @@ constructor( hideDialog() }, navbarBottomInsetsProvider = { Utils.getNavbarInsets(context).bottom }, configurationController, showConcurrentDisplayInfo = concurrentDisplaysInProgess ) .apply { show() } Loading packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialog.kt +92 −49 Original line number Diff line number Diff line Loading @@ -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() { Loading @@ -62,60 +81,84 @@ open class SystemUIBottomSheetDialog( } } 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) } } packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt→packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogDelegateTest.kt +54 −26 Original line number Diff line number Diff line Loading @@ -16,51 +16,74 @@ 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.After 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 import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) class MirroringConfirmationDialogTest : SysuiTestCase() { class MirroringConfirmationDialogDelegateTest : SysuiTestCase() { private lateinit var dialog: MirroringConfirmationDialog 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() { MockitoAnnotations.initMocks(this) dialog = MirroringConfirmationDialog( context, onStartMirroringCallback, onCancelCallback, underTest = MirroringConfirmationDialogDelegate( context = context, showConcurrentDisplayInfo = false, onStartMirroringClickListener = onStartMirroringCallback, onCancelMirroring = onCancelCallback, navbarBottomInsetsProvider = { 0 }, ) } @Test fun startMirroringButton_clicked_callsCorrectCallback() { dialog.show() underTest.onCreate(dialog, null) dialog.requireViewById<View>(R.id.enable_display).callOnClick() Loading @@ -70,7 +93,7 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { @Test fun cancelButton_clicked_callsCorrectCallback() { dialog.show() underTest.onCreate(dialog, null) dialog.requireViewById<View>(R.id.cancel).callOnClick() Loading @@ -80,10 +103,10 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { @Test fun onCancel_afterEnablingMirroring_cancelCallbackNotCalled() { dialog.show() underTest.onCreate(dialog, null) dialog.requireViewById<View>(R.id.enable_display).callOnClick() dialog.cancel() underTest.onStop(dialog) verify(onCancelCallback, never()).onClick(any()) verify(onStartMirroringCallback).onClick(any()) Loading @@ -91,10 +114,10 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { @Test fun onDismiss_afterEnablingMirroring_cancelCallbackNotCalled() { dialog.show() underTest.onCreate(dialog, null) dialog.requireViewById<View>(R.id.enable_display).callOnClick() dialog.dismiss() underTest.onStop(dialog) verify(onCancelCallback, never()).onClick(any()) verify(onStartMirroringCallback).onClick(any()) Loading @@ -102,10 +125,12 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { @Test fun onInsetsChanged_navBarInsets_updatesBottomPadding() { dialog.show() underTest.onCreate(dialog, null) underTest.onStart(dialog) val insets = buildInsets(WindowInsets.Type.navigationBars(), TEST_BOTTOM_INSETS) dialog.onInsetsChanged(WindowInsets.Type.navigationBars(), insets) triggerInsetsChanged(WindowInsets.Type.navigationBars(), insets) assertThat(dialog.requireViewById<View>(R.id.cd_bottom_sheet).paddingBottom) .isEqualTo(TEST_BOTTOM_INSETS) Loading @@ -113,10 +138,11 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { @Test fun onInsetsChanged_otherType_doesNotUpdateBottomPadding() { dialog.show() underTest.onCreate(dialog, null) underTest.onStart(dialog) val insets = buildInsets(WindowInsets.Type.ime(), TEST_BOTTOM_INSETS) dialog.onInsetsChanged(WindowInsets.Type.ime(), insets) triggerInsetsChanged(WindowInsets.Type.ime(), insets) assertThat(dialog.requireViewById<View>(R.id.cd_bottom_sheet).paddingBottom) .isNotEqualTo(TEST_BOTTOM_INSETS) Loading @@ -126,11 +152,13 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { return WindowInsets.Builder().setInsets(type, Insets.of(0, 0, 0, bottom)).build() } @After fun teardown() { if (::dialog.isInitialized) { dialog.dismiss() } 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 { Loading packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt +55 −20 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt→packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogDelegate.kt +162 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * 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. Loading @@ -15,15 +15,22 @@ */ 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 com.android.systemui.statusbar.policy.ConfigurationController import javax.inject.Inject import kotlin.math.max /** Loading @@ -32,15 +39,15 @@ import kotlin.math.max * [onCancelMirroring] is called **only** if mirroring didn't start, or when the dismiss button is * pressed. */ class MirroringConfirmationDialog( 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, configurationController: ConfigurationController? = null, private val showConcurrentDisplayInfo: Boolean = false, theme: Int = R.style.Theme_SystemUI_Dialog, ) : SystemUIBottomSheetDialog(context, configurationController, theme) { ) : DialogDelegate<Dialog> { private lateinit var mirrorButton: TextView private lateinit var dismissButton: TextView Loading @@ -50,26 +57,27 @@ class MirroringConfirmationDialog( 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) override fun onCreate(dialog: Dialog, savedInstanceState: Bundle?) { dialog.setContentView(R.layout.connected_display_dialog) mirrorButton = requireViewById<TextView>(R.id.enable_display).apply { dialog.requireViewById<TextView>(R.id.enable_display).apply { setOnClickListener(onStartMirroringClickListener) enabledPressed = true } dismissButton = requireViewById<TextView>(R.id.cancel).apply { setOnClickListener(onCancelMirroring) } dialog.requireViewById<TextView>(R.id.cancel).apply { setOnClickListener(onCancelMirroring) } dualDisplayWarning = requireViewById<TextView>(R.id.dual_display_warning).apply { dialog.requireViewById<TextView>(R.id.dual_display_warning).apply { visibility = if (showConcurrentDisplayInfo) View.VISIBLE else View.GONE } bottomSheet = requireViewById(R.id.cd_bottom_sheet) bottomSheet = dialog.requireViewById(R.id.cd_bottom_sheet) setOnDismissListener { dialog.setOnDismissListener { if (!enabledPressed) { onCancelMirroring.onClick(null) } Loading @@ -77,6 +85,14 @@ class MirroringConfirmationDialog( 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 Loading @@ -84,15 +100,63 @@ class MirroringConfirmationDialog( bottomSheet.updatePadding(bottom = max(navbarInsets, defaultDialogBottomInset)) } override fun onInsetsChanged(changedTypes: Int, insets: WindowInsets) { 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) } } } override fun onConfigurationChanged() { super.onConfigurationChanged() setupInsets() 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, ) } }
packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt +4 −6 Original line number Diff line number Diff line Loading @@ -25,8 +25,7 @@ import com.android.systemui.dagger.qualifiers.Application 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 Loading Loading @@ -54,7 +53,7 @@ constructor( 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 Loading Loading @@ -91,8 +90,8 @@ constructor( private fun showDialog(pendingDisplay: PendingDisplay, concurrentDisplaysInProgess: Boolean) { hideDialog() dialog = MirroringConfirmationDialog( context, bottomSheetFactory .createDialog( onStartMirroringClickListener = { scope.launch(bgDispatcher) { pendingDisplay.enable() } hideDialog() Loading @@ -102,7 +101,6 @@ constructor( hideDialog() }, navbarBottomInsetsProvider = { Utils.getNavbarInsets(context).bottom }, configurationController, showConcurrentDisplayInfo = concurrentDisplaysInProgess ) .apply { show() } Loading
packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialog.kt +92 −49 Original line number Diff line number Diff line Loading @@ -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() { Loading @@ -62,60 +81,84 @@ open class SystemUIBottomSheetDialog( } } 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) } }
packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt→packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogDelegateTest.kt +54 −26 Original line number Diff line number Diff line Loading @@ -16,51 +16,74 @@ 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.After 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 import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) class MirroringConfirmationDialogTest : SysuiTestCase() { class MirroringConfirmationDialogDelegateTest : SysuiTestCase() { private lateinit var dialog: MirroringConfirmationDialog 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() { MockitoAnnotations.initMocks(this) dialog = MirroringConfirmationDialog( context, onStartMirroringCallback, onCancelCallback, underTest = MirroringConfirmationDialogDelegate( context = context, showConcurrentDisplayInfo = false, onStartMirroringClickListener = onStartMirroringCallback, onCancelMirroring = onCancelCallback, navbarBottomInsetsProvider = { 0 }, ) } @Test fun startMirroringButton_clicked_callsCorrectCallback() { dialog.show() underTest.onCreate(dialog, null) dialog.requireViewById<View>(R.id.enable_display).callOnClick() Loading @@ -70,7 +93,7 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { @Test fun cancelButton_clicked_callsCorrectCallback() { dialog.show() underTest.onCreate(dialog, null) dialog.requireViewById<View>(R.id.cancel).callOnClick() Loading @@ -80,10 +103,10 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { @Test fun onCancel_afterEnablingMirroring_cancelCallbackNotCalled() { dialog.show() underTest.onCreate(dialog, null) dialog.requireViewById<View>(R.id.enable_display).callOnClick() dialog.cancel() underTest.onStop(dialog) verify(onCancelCallback, never()).onClick(any()) verify(onStartMirroringCallback).onClick(any()) Loading @@ -91,10 +114,10 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { @Test fun onDismiss_afterEnablingMirroring_cancelCallbackNotCalled() { dialog.show() underTest.onCreate(dialog, null) dialog.requireViewById<View>(R.id.enable_display).callOnClick() dialog.dismiss() underTest.onStop(dialog) verify(onCancelCallback, never()).onClick(any()) verify(onStartMirroringCallback).onClick(any()) Loading @@ -102,10 +125,12 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { @Test fun onInsetsChanged_navBarInsets_updatesBottomPadding() { dialog.show() underTest.onCreate(dialog, null) underTest.onStart(dialog) val insets = buildInsets(WindowInsets.Type.navigationBars(), TEST_BOTTOM_INSETS) dialog.onInsetsChanged(WindowInsets.Type.navigationBars(), insets) triggerInsetsChanged(WindowInsets.Type.navigationBars(), insets) assertThat(dialog.requireViewById<View>(R.id.cd_bottom_sheet).paddingBottom) .isEqualTo(TEST_BOTTOM_INSETS) Loading @@ -113,10 +138,11 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { @Test fun onInsetsChanged_otherType_doesNotUpdateBottomPadding() { dialog.show() underTest.onCreate(dialog, null) underTest.onStart(dialog) val insets = buildInsets(WindowInsets.Type.ime(), TEST_BOTTOM_INSETS) dialog.onInsetsChanged(WindowInsets.Type.ime(), insets) triggerInsetsChanged(WindowInsets.Type.ime(), insets) assertThat(dialog.requireViewById<View>(R.id.cd_bottom_sheet).paddingBottom) .isNotEqualTo(TEST_BOTTOM_INSETS) Loading @@ -126,11 +152,13 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { return WindowInsets.Builder().setInsets(type, Insets.of(0, 0, 0, bottom)).build() } @After fun teardown() { if (::dialog.isInitialized) { dialog.dismiss() } 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 { Loading
packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt +55 −20 File changed.Preview size limit exceeded, changes collapsed. Show changes