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

Commit b0db219c authored by Chris Göllner's avatar Chris Göllner
Browse files

PrivacyDotWindowController: dynamically add/remove windows as needed

Before, the all 4 windows (one for each corner) were always added on
startup. Since having these windows is costly, we optimize to show
them when needed.

This only affects the connected displays implementation of the privacy
dot.

Flag: com.android.systemui.shared.status_bar_connected_displays
Fixes: 422343339
Test: PrivacyDotWindowControllerTest
Change-Id: I38a6af5a9027f68a521b9b0b84cdb378d63fd406
parent af2a9e60
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(