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

Commit 994b2fa5 authored by Chris Göllner's avatar Chris Göllner Committed by Android (Google) Code Review
Browse files

Merge "PrivacyDotWindowController: dynamically add/remove windows as needed" into main

parents e95a4353 b0db219c
Loading
Loading
Loading
Loading
+97 −10
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.view.Surface
import android.view.View
import android.view.WindowManager
import android.view.fakeWindowManager
import android.widget.FrameLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -71,12 +72,13 @@ class PrivacyDotWindowControllerTest : SysuiTestCase() {
    }

    @Test
    fun start_afterUiThreadExecutes_addsWindowsOnUiThread() {
    fun start_afterUiThreadExecutes_doesNotAddWindowsInitially() {
        underTest.start()

        executor.runAllReady()

        assertThat(windowManager.addedViews).hasSize(4)
        // Windows are now added dynamically, so immediately after initialization,
        // no windows should be present until a dot is "shown".
        assertThat(windowManager.addedViews).isEmpty()
    }

    @Test
@@ -93,6 +95,8 @@ class PrivacyDotWindowControllerTest : SysuiTestCase() {
        underTest.start()
        executor.runAllReady()

        // The ID should be on the dotView, not necessarily the container anymore.
        // Assuming R.id.privacy_dot_top_left_container is the ID of the inner dotView.
        assertThat(viewController.topLeft?.id).isEqualTo(R.id.privacy_dot_top_left_container)
    }

@@ -121,13 +125,63 @@ class PrivacyDotWindowControllerTest : SysuiTestCase() {
            .isEqualTo(R.id.privacy_dot_bottom_right_container)
    }

    @Test
    fun onPrivacyDotShown_addsWindow() {
        underTest.start()
        executor.runAllReady()

        // Simulate the PrivacyDotViewController showing the top-left dot
        viewController.showingListener?.onPrivacyDotShown(viewController.topLeft!!)
        executor.runAllReady() // Ensure the addView call on UI thread is processed

        // Verify exactly one window was added
        assertThat(windowManager.addedViews).hasSize(1)

        // Get the FrameLayout that was added to the WindowManager
        val addedWindowRootView = windowManager.addedViews.keys.first()

        // Assert it's a FrameLayout (the expected container)
        expect.that(addedWindowRootView).isInstanceOf(FrameLayout::class.java)

        // Assert that this FrameLayout actually contains the specific dotView
        // (fakeViewController.topLeft)
        // The PrivacyDotWindowController's inflate method adds the dotView as a child of the
        // FrameLayout.
        expect
            .that((addedWindowRootView as FrameLayout).getChildAt(0))
            .isEqualTo(viewController.topLeft)
    }

    @Test
    fun onPrivacyDotHidden_removesWindow() {
        underTest.start()
        executor.runAllReady()

        // Show the top-left dot first
        viewController.showingListener?.onPrivacyDotShown(viewController.topLeft)
        executor.runAllReady()
        assertThat(windowManager.addedViews).hasSize(1)

        // Now hide it
        viewController.showingListener?.onPrivacyDotHidden(viewController.topLeft)
        executor.runAllReady()

        assertThat(windowManager.addedViews).isEmpty()
    }

    @Test
    fun start_viewsAddedInRespectiveCorners() {
        context.display = mock { on { rotation } doReturn Surface.ROTATION_0 }

        underTest.start()
        executor.runAllReady()

        // Now, trigger the 'shown' event for each dot
        viewController.showingListener?.onPrivacyDotShown(viewController.topLeft)
        viewController.showingListener?.onPrivacyDotShown(viewController.topRight)
        viewController.showingListener?.onPrivacyDotShown(viewController.bottomLeft)
        viewController.showingListener?.onPrivacyDotShown(viewController.bottomRight)
        executor.runAllReady() // Process all addView calls

        expect.that(gravityForView(viewController.topLeft!!)).isEqualTo(TOP or LEFT)
        expect.that(gravityForView(viewController.topRight!!)).isEqualTo(TOP or RIGHT)
        expect.that(gravityForView(viewController.bottomLeft!!)).isEqualTo(BOTTOM or LEFT)
@@ -137,10 +191,16 @@ class PrivacyDotWindowControllerTest : SysuiTestCase() {
    @Test
    fun start_rotation90_viewsPositionIsShifted90degrees() {
        context.display = mock { on { rotation } doReturn Surface.ROTATION_90 }

        underTest.start()
        executor.runAllReady()

        // Now, trigger the 'shown' event for each dot
        viewController.showingListener?.onPrivacyDotShown(viewController.topLeft)
        viewController.showingListener?.onPrivacyDotShown(viewController.topRight)
        viewController.showingListener?.onPrivacyDotShown(viewController.bottomLeft)
        viewController.showingListener?.onPrivacyDotShown(viewController.bottomRight)
        executor.runAllReady() // Process all addView calls

        expect.that(gravityForView(viewController.topLeft!!)).isEqualTo(BOTTOM or LEFT)
        expect.that(gravityForView(viewController.topRight!!)).isEqualTo(TOP or LEFT)
        expect.that(gravityForView(viewController.bottomLeft!!)).isEqualTo(BOTTOM or RIGHT)
@@ -150,10 +210,16 @@ class PrivacyDotWindowControllerTest : SysuiTestCase() {
    @Test
    fun start_rotation180_viewsPositionIsShifted180degrees() {
        context.display = mock { on { rotation } doReturn Surface.ROTATION_180 }

        underTest.start()
        executor.runAllReady()

        // Now, trigger the 'shown' event for each dot
        viewController.showingListener?.onPrivacyDotShown(viewController.topLeft)
        viewController.showingListener?.onPrivacyDotShown(viewController.topRight)
        viewController.showingListener?.onPrivacyDotShown(viewController.bottomLeft)
        viewController.showingListener?.onPrivacyDotShown(viewController.bottomRight)
        executor.runAllReady() // Process all addView calls

        expect.that(gravityForView(viewController.topLeft!!)).isEqualTo(BOTTOM or RIGHT)
        expect.that(gravityForView(viewController.topRight!!)).isEqualTo(BOTTOM or LEFT)
        expect.that(gravityForView(viewController.bottomLeft!!)).isEqualTo(TOP or RIGHT)
@@ -163,10 +229,16 @@ class PrivacyDotWindowControllerTest : SysuiTestCase() {
    @Test
    fun start_rotation270_viewsPositionIsShifted270degrees() {
        context.display = mock { on { rotation } doReturn Surface.ROTATION_270 }

        underTest.start()
        executor.runAllReady()

        // Now, trigger the 'shown' event for each dot
        viewController.showingListener?.onPrivacyDotShown(viewController.topLeft)
        viewController.showingListener?.onPrivacyDotShown(viewController.topRight)
        viewController.showingListener?.onPrivacyDotShown(viewController.bottomLeft)
        viewController.showingListener?.onPrivacyDotShown(viewController.bottomRight)
        executor.runAllReady() // Process all addView calls

        expect.that(gravityForView(viewController.topLeft!!)).isEqualTo(TOP or RIGHT)
        expect.that(gravityForView(viewController.topRight!!)).isEqualTo(BOTTOM or RIGHT)
        expect.that(gravityForView(viewController.bottomLeft!!)).isEqualTo(TOP or LEFT)
@@ -174,19 +246,34 @@ class PrivacyDotWindowControllerTest : SysuiTestCase() {
    }

    @Test
    fun onStop_removeAllWindows() {
    fun onStop_removesAllCurrentlyAddedWindows() {
        underTest.start()
        executor.runAllReady()

        // Show all dots so their windows are added
        viewController.showingListener?.onPrivacyDotShown(viewController.topLeft)
        viewController.showingListener?.onPrivacyDotShown(viewController.topRight)
        viewController.showingListener?.onPrivacyDotShown(viewController.bottomLeft)
        viewController.showingListener?.onPrivacyDotShown(viewController.bottomRight)
        executor.runAllReady()
        assertThat(windowManager.addedViews).hasSize(4)

        // Now call stop
        underTest.stop()
        executor.runAllReady()

        assertThat(windowManager.addedViews).isEmpty()
    }

    private fun paramsForView(view: View): WindowManager.LayoutParams {
    // Helper functions: Note that paramsForView needs to find the *root* view (FrameLayout)
    // that was added to the window manager, not the inner dotView.
    private fun paramsForView(dotView: View): WindowManager.LayoutParams {
        // We're looking for the FrameLayout that contains the dotView.
        // The dotView has an ID, and the FrameLayout is its parent.
        return windowManager.addedViews.entries
            .first { it.key == view || it.key.findViewById<View>(view.id) != null }
            .first { (rootView, _) ->
                rootView is FrameLayout && rootView.findViewById<View>(dotView.id) == dotView
            }
            .value
    }

+14 −0
Original line number Diff line number Diff line
@@ -16,6 +16,10 @@

package com.android.systemui.statusbar.events

import android.view.DisplayCutout.BOUNDS_POSITION_BOTTOM
import android.view.DisplayCutout.BOUNDS_POSITION_LEFT
import android.view.DisplayCutout.BOUNDS_POSITION_RIGHT
import android.view.DisplayCutout.BOUNDS_POSITION_TOP
import android.view.Gravity
import android.view.Surface

@@ -25,30 +29,40 @@ enum class PrivacyDotCorner(
    val gravity: Int,
    val innerGravity: Int,
    val title: String,
    val alignedBound1: Int,
    val alignedBound2: Int,
) {
    TopLeft(
        index = 0,
        gravity = Gravity.TOP or Gravity.LEFT,
        innerGravity = Gravity.CENTER_VERTICAL or Gravity.RIGHT,
        title = "TopLeft",
        alignedBound1 = BOUNDS_POSITION_TOP,
        alignedBound2 = BOUNDS_POSITION_LEFT,
    ),
    TopRight(
        index = 1,
        gravity = Gravity.TOP or Gravity.RIGHT,
        innerGravity = Gravity.CENTER_VERTICAL or Gravity.LEFT,
        title = "TopRight",
        alignedBound1 = BOUNDS_POSITION_TOP,
        alignedBound2 = BOUNDS_POSITION_RIGHT,
    ),
    BottomRight(
        index = 2,
        gravity = Gravity.BOTTOM or Gravity.RIGHT,
        innerGravity = Gravity.CENTER_VERTICAL or Gravity.RIGHT,
        title = "BottomRight",
        alignedBound1 = BOUNDS_POSITION_BOTTOM,
        alignedBound2 = BOUNDS_POSITION_RIGHT,
    ),
    BottomLeft(
        index = 3,
        gravity = Gravity.BOTTOM or Gravity.LEFT,
        innerGravity = Gravity.CENTER_VERTICAL or Gravity.LEFT,
        title = "BottomLeft",
        alignedBound1 = BOUNDS_POSITION_BOTTOM,
        alignedBound2 = BOUNDS_POSITION_LEFT,
    ),
}

+57 −26
Original line number Diff line number Diff line
@@ -18,10 +18,6 @@ package com.android.systemui.statusbar.events

import android.util.Log
import android.view.Display
import android.view.DisplayCutout.BOUNDS_POSITION_BOTTOM
import android.view.DisplayCutout.BOUNDS_POSITION_LEFT
import android.view.DisplayCutout.BOUNDS_POSITION_RIGHT
import android.view.DisplayCutout.BOUNDS_POSITION_TOP
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
@@ -59,50 +55,79 @@ constructor(
    @ScreenDecorationsThread private val uiExecutor: Executor,
    private val dotFactory: PrivacyDotDecorProviderFactory,
) {
    private val dotViews: MutableSet<View> = mutableSetOf()
    private val dotWindowViewsByCorner = mutableMapOf<PrivacyDotCorner, View>()
    private var displayRotationOnStartup = 0

    fun start() {
        uiExecutor.execute { startOnUiThread() }
    }

    private fun startOnUiThread() {
        displayRotationOnStartup = inflater.context.display.rotation

        val providers = dotFactory.providers

        val topLeft = providers.inflate(BOUNDS_POSITION_TOP, BOUNDS_POSITION_LEFT)
        val topRight = providers.inflate(BOUNDS_POSITION_TOP, BOUNDS_POSITION_RIGHT)
        val bottomLeft = providers.inflate(BOUNDS_POSITION_BOTTOM, BOUNDS_POSITION_LEFT)
        val bottomRight = providers.inflate(BOUNDS_POSITION_BOTTOM, BOUNDS_POSITION_RIGHT)
        val topLeftContainer = providers.inflate(TopLeft)
        val topRightContainer = providers.inflate(TopRight)
        val bottomLeftContainer = providers.inflate(BottomLeft)
        val bottomRightContainer = providers.inflate(BottomRight)

        listOfNotNull(
                topLeft.addToWindow(TopLeft),
                topRight.addToWindow(TopRight),
                bottomLeft.addToWindow(BottomLeft),
                bottomRight.addToWindow(BottomRight),
        val dotViewContainersByView =
            mapOf(
                topLeftContainer.dotView to topLeftContainer,
                topRightContainer.dotView to topRightContainer,
                bottomLeftContainer.dotView to bottomLeftContainer,
                bottomRightContainer.dotView to bottomRightContainer,
            )
            .forEach { dotViews.add(it) }

        privacyDotViewController.initialize(topLeft, topRight, bottomLeft, bottomRight)
        privacyDotViewController.showingListener =
            object : PrivacyDotViewController.ShowingListener {

                override fun onPrivacyDotShown(v: View?) {
                    val dotViewContainer = dotViewContainersByView[v]
                    if (v == null || dotViewContainer == null) {
                        return
                    }
                    v.addToWindow(dotViewContainer.corner)
                    dotWindowViewsByCorner[dotViewContainer.corner] = dotViewContainer.windowView
                }

    private fun List<DecorProvider>.inflate(alignedBound1: Int, alignedBound2: Int): View {
                override fun onPrivacyDotHidden(v: View?) {
                    val dotViewContainer = dotViewContainersByView[v]
                    val windowView = dotWindowViewsByCorner.remove(dotViewContainer?.corner)
                    if (windowView != null) {
                        windowManager.removeView(windowView)
                    }
                }
            }
        privacyDotViewController.initialize(
            topLeftContainer.dotView,
            topRightContainer.dotView,
            bottomLeftContainer.dotView,
            bottomRightContainer.dotView,
        )
    }

    private fun List<DecorProvider>.inflate(corner: PrivacyDotCorner): DotViewContainer {
        val provider =
            first { it.alignedBounds.containsExactly(alignedBound1, alignedBound2) }
            first { it.alignedBounds.containsExactly(corner.alignedBound1, corner.alignedBound2) }
                as PrivacyDotCornerDecorProviderImpl
        return inflater.inflate(/* resource= */ provider.layoutId, /* root= */ null)
        val dotView = inflater.inflate(/* resource= */ provider.layoutId, /* root= */ null)
        // PrivacyDotViewController expects the dot view to have a FrameLayout parent.
        val windowView = FrameLayout(dotView.context)
        windowView.addView(dotView)
        return DotViewContainer(windowView, dotView, corner)
    }

    private fun View.addToWindow(corner: PrivacyDotCorner): View? {
    private fun View.addToWindow(corner: PrivacyDotCorner) {
        val excludeFromScreenshots = displayId == Display.DEFAULT_DISPLAY
        val params =
            ScreenDecorations.getWindowLayoutBaseParams(excludeFromScreenshots).apply {
                width = WRAP_CONTENT
                height = WRAP_CONTENT
                gravity = corner.rotatedCorner(context.display.rotation).gravity
                gravity = corner.rotatedCorner(displayRotationOnStartup).gravity
                title = "PrivacyDot${corner.title}$displayId"
            }
        // PrivacyDotViewController expects the dot view to have a FrameLayout parent.
        val rootView = FrameLayout(context)
        rootView.addView(this)
        try {
            // Wrapping this in a try/catch to avoid crashes when a display is instantly removed
            // after being added, and initialization hasn't finished yet.
@@ -114,13 +139,19 @@ constructor(
                e,
            )
        }
        return rootView
        return
    }

    fun stop() {
        dotViews.forEach { windowManager.removeView(it) }
        dotWindowViewsByCorner.forEach { windowManager.removeView(it.value) }
    }

    private data class DotViewContainer(
        val windowView: View,
        val dotView: View,
        val corner: PrivacyDotCorner,
    )

    @AssistedFactory
    fun interface Factory {
        fun create(