Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 020580d7 authored by Anton Potapov's avatar Anton Potapov Committed by Android (Google) Code Review
Browse files

Merge "Make SystemUIBottomSheetDialog use DialogDelegate" into main

parents a375d1cc 8f061389
Loading
Loading
Loading
Loading
+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.
@@ -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

/**
@@ -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
@@ -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)
            }
@@ -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
@@ -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,
            )
    }
}
+4 −6
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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()
@@ -102,7 +101,6 @@ constructor(
                        hideDialog()
                    },
                    navbarBottomInsetsProvider = { Utils.getNavbarInsets(context).bottom },
                    configurationController,
                    showConcurrentDisplayInfo = concurrentDisplaysInProgess
                )
                .apply { show() }
+92 −49
Original line number Diff line number Diff line
@@ -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 @@ 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)
    }
}
+54 −26
Original line number Diff line number Diff line
@@ -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()

@@ -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()

@@ -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())
@@ -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())
@@ -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)
@@ -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)
@@ -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 {
+55 −20

File changed.

Preview size limit exceeded, changes collapsed.

Loading