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

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

Show privacy dot on connected displays

Introduces PrivacyDotWindowController, which adds the privacy dots
to WindowManager.

This controller is only used for connected displays for now, and the
old logic in ScreenDecorations is used for the default/main display.

Also extracts the ScreenDecorations ui thread to a Dagger module, so
it can be reused in PrivacyDotWindowController.

Test: PrivacyDotWindowControllerTest
Test: PrivacyDotWindowControllerStoreImplTest
Test: PrivacyDotViewControllerTest
Test: DisplayWindowPropertiesRepositoryImplTest
Test: Add a connected display and start a voice recording. Also change
      rotation of the display and see that the dot positions correctly.
Bug: 362720432
Flag: com.android.systemui.status_bar_connected_displays
Change-Id: I5f0b8ddb37b55a53b66f3357bc1279e4b765e744
parent 261bbcfa
Loading
Loading
Loading
Loading
+54 −9
Original line number Diff line number Diff line
@@ -17,11 +17,13 @@
package com.android.systemui.statusbar.core

import android.platform.test.annotations.EnableFlags
import android.view.Display
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.data.repository.fakePrivacyDotWindowControllerStore
import com.android.systemui.testKosmos
import com.google.common.truth.Expect
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -30,6 +32,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.never
import org.mockito.kotlin.verify

@OptIn(ExperimentalCoroutinesApi::class)
@@ -39,16 +42,12 @@ import org.mockito.kotlin.verify
class MultiDisplayStatusBarStarterTest : SysuiTestCase() {
    @get:Rule val expect: Expect = Expect.create()

    private val kosmos =
        testKosmos().also {
            it.statusBarOrchestratorFactory = it.fakeStatusBarOrchestratorFactory
            it.statusBarInitializerStore = it.fakeStatusBarInitializerStore
        }
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val fakeDisplayRepository = kosmos.displayRepository
    private val fakeOrchestratorFactory = kosmos.fakeStatusBarOrchestratorFactory
    private val fakeInitializerStore = kosmos.fakeStatusBarInitializerStore

    private val fakePrivacyDotStore = kosmos.fakePrivacyDotWindowControllerStore
    // Lazy, so that @EnableFlags is set before initializer is instantiated.
    private val underTest by lazy { kosmos.multiDisplayStatusBarStarter }

@@ -82,6 +81,31 @@ class MultiDisplayStatusBarStarterTest : SysuiTestCase() {
            verify(fakeOrchestratorFactory.createdOrchestratorForDisplay(displayId = 2)!!).start()
        }

    @Test
    fun start_startsPrivacyDotForCurrentDisplays() =
        testScope.runTest {
            fakeDisplayRepository.addDisplay(displayId = 1)
            fakeDisplayRepository.addDisplay(displayId = 2)

            underTest.start()
            runCurrent()

            verify(fakePrivacyDotStore.forDisplay(displayId = 1)).start()
            verify(fakePrivacyDotStore.forDisplay(displayId = 2)).start()
        }

    @Test
    fun start_doesNotStartPrivacyDotForDefaultDisplay() =
        testScope.runTest {
            fakeDisplayRepository.addDisplay(displayId = Display.DEFAULT_DISPLAY)

            underTest.start()
            runCurrent()

            verify(fakePrivacyDotStore.forDisplay(displayId = Display.DEFAULT_DISPLAY), never())
                .start()
        }

    @Test
    fun displayAdded_orchestratorForNewDisplayIsStarted() =
        testScope.runTest {
@@ -108,6 +132,18 @@ class MultiDisplayStatusBarStarterTest : SysuiTestCase() {
                .isTrue()
        }

    @Test
    fun displayAdded_privacyDotForNewDisplayIsStarted() =
        testScope.runTest {
            underTest.start()
            runCurrent()

            fakeDisplayRepository.addDisplay(displayId = 3)
            runCurrent()

            verify(fakePrivacyDotStore.forDisplay(displayId = 3)).start()
        }

    @Test
    fun displayAddedDuringStart_initializerForNewDisplayIsStarted() =
        testScope.runTest {
@@ -129,8 +165,17 @@ class MultiDisplayStatusBarStarterTest : SysuiTestCase() {
            fakeDisplayRepository.addDisplay(displayId = 3)
            runCurrent()

            expect
                .that(fakeInitializerStore.forDisplay(displayId = 3).startedByCoreStartable)
                .isTrue()
            verify(fakeOrchestratorFactory.createdOrchestratorForDisplay(displayId = 3)!!).start()
        }

    @Test
    fun displayAddedDuringStart_privacyDotForNewDisplayIsStarted() =
        testScope.runTest {
            underTest.start()

            fakeDisplayRepository.addDisplay(displayId = 3)
            runCurrent()

            verify(fakePrivacyDotStore.forDisplay(displayId = 3)).start()
        }
}
+54 −0
Original line number Diff line number Diff line
/*
 * 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.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.statusbar.data.repository

import android.platform.test.annotations.EnableFlags
import android.view.Display
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.testKosmos
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
class PrivacyDotWindowControllerStoreImplTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val underTest by lazy { kosmos.privacyDotWindowControllerStoreImpl }

    @Before
    fun installDisplays() = runBlocking {
        kosmos.displayRepository.addDisplay(displayId = Display.DEFAULT_DISPLAY)
        kosmos.displayRepository.addDisplay(displayId = Display.DEFAULT_DISPLAY + 1)
    }

    @Test(expected = IllegalArgumentException::class)
    fun forDisplay_defaultDisplay_throws() {
        underTest.forDisplay(displayId = Display.DEFAULT_DISPLAY)
    }

    @Test
    fun forDisplay_nonDefaultDisplay_doesNotThrow() {
        underTest.forDisplay(displayId = Display.DEFAULT_DISPLAY + 1)
    }
}
+185 −0
Original line number Diff line number Diff line
/*
 * 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.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.statusbar.events

import android.view.Gravity.BOTTOM
import android.view.Gravity.LEFT
import android.view.Gravity.RIGHT
import android.view.Gravity.TOP
import android.view.Surface
import android.view.View
import android.view.WindowManager
import android.view.fakeWindowManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.google.common.truth.Expect
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock

@SmallTest
@RunWith(AndroidJUnit4::class)
class PrivacyDotWindowControllerTest : SysuiTestCase() {

    @get:Rule val expect: Expect = Expect.create()

    private val kosmos = testKosmos()
    private val underTest = kosmos.privacyDotWindowController
    private val viewController = kosmos.privacyDotViewController
    private val windowManager = kosmos.fakeWindowManager
    private val executor = kosmos.fakeExecutor

    @After
    fun cleanUpCustomDisplay() {
        context.display = null
    }

    @Test
    fun start_beforeUiThreadExecutes_doesNotAddWindows() {
        underTest.start()

        assertThat(windowManager.addedViews).isEmpty()
    }

    @Test
    fun start_beforeUiThreadExecutes_doesNotInitializeViewController() {
        underTest.start()

        assertThat(viewController.isInitialized).isFalse()
    }

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

        executor.runAllReady()

        assertThat(windowManager.addedViews).hasSize(4)
    }

    @Test
    fun start_afterUiThreadExecutes_initializesViewController() {
        underTest.start()

        executor.runAllReady()

        assertThat(viewController.isInitialized).isTrue()
    }

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

        assertThat(viewController.topLeft?.id).isEqualTo(R.id.privacy_dot_top_left_container)
    }

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

        assertThat(viewController.topRight?.id).isEqualTo(R.id.privacy_dot_top_right_container)
    }

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

        assertThat(viewController.bottomLeft?.id).isEqualTo(R.id.privacy_dot_bottom_left_container)
    }

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

        assertThat(viewController.bottomRight?.id)
            .isEqualTo(R.id.privacy_dot_bottom_right_container)
    }

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

        underTest.start()
        executor.runAllReady()

        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)
        expect.that(gravityForView(viewController.bottomRight!!)).isEqualTo(BOTTOM or RIGHT)
    }

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

        underTest.start()
        executor.runAllReady()

        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)
        expect.that(gravityForView(viewController.bottomRight!!)).isEqualTo(TOP or RIGHT)
    }

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

        underTest.start()
        executor.runAllReady()

        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)
        expect.that(gravityForView(viewController.bottomRight!!)).isEqualTo(TOP or LEFT)
    }

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

        underTest.start()
        executor.runAllReady()

        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)
        expect.that(gravityForView(viewController.bottomRight!!)).isEqualTo(BOTTOM or LEFT)
    }

    private fun paramsForView(view: View): WindowManager.LayoutParams {
        return windowManager.addedViews.entries
            .first { it.key == view || it.key.findViewById<View>(view.id) != null }
            .value
    }

    private fun gravityForView(view: View): Int {
        return paramsForView(view).gravity
    }
}
+13 −2
Original line number Diff line number Diff line
@@ -905,7 +905,18 @@ public class ScreenDecorations implements
        return lp;
    }

    private WindowManager.LayoutParams getWindowLayoutBaseParams() {
    public static WindowManager.LayoutParams getWindowLayoutBaseParams() {
        return getWindowLayoutBaseParams(/* excludeFromScreenshots= */ true);
    }

    /**
     * Creates the base {@link WindowManager.LayoutParams} that are used for all decoration windows.
     *
     * @param excludeFromScreenshots whether to set the {@link
     *     WindowManager.LayoutParams#PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY} flag.
     */
    public static WindowManager.LayoutParams getWindowLayoutBaseParams(
            boolean excludeFromScreenshots) {
        final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
@@ -921,7 +932,7 @@ public class ScreenDecorations implements
        // FLAG_SLIPPERY can only be set by trusted overlays
        lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;

        if (!DEBUG_SCREENSHOT_ROUNDED_CORNERS) {
        if (!DEBUG_SCREENSHOT_ROUNDED_CORNERS && excludeFromScreenshots) {
            lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
        }

+1 −1
Original line number Diff line number Diff line
@@ -81,7 +81,7 @@ class PrivacyDotCornerDecorProviderImpl(
    override val viewId: Int,
    @DisplayCutout.BoundsPosition override val alignedBound1: Int,
    @DisplayCutout.BoundsPosition override val alignedBound2: Int,
    private val layoutId: Int,
    val layoutId: Int,
) : CornerDecorProvider() {

    override fun onReloadResAndMeasure(
Loading