Loading packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt +7 −0 Original line number Diff line number Diff line Loading @@ -20,6 +20,8 @@ import com.android.systemui.display.data.repository.DeviceStateRepository import com.android.systemui.display.data.repository.DeviceStateRepositoryImpl import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.DisplayRepositoryImpl import com.android.systemui.display.data.repository.FocusedDisplayRepository import com.android.systemui.display.data.repository.FocusedDisplayRepositoryImpl import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractorImpl import dagger.Binds Loading @@ -39,4 +41,9 @@ interface DisplayModule { fun bindsDeviceStateRepository( deviceStateRepository: DeviceStateRepositoryImpl ): DeviceStateRepository @Binds fun bindsFocusedDisplayRepository( focusedDisplayRepository: FocusedDisplayRepositoryImpl ): FocusedDisplayRepository } packages/SystemUI/src/com/android/systemui/display/data/repository/FocusedDisplayRepository.kt +8 −4 Original line number Diff line number Diff line Loading @@ -37,16 +37,21 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn /** Repository tracking display focus. */ interface FocusedDisplayRepository { /** Provides the currently focused display. */ val focusedDisplayId: StateFlow<Int> } @SysUISingleton @MainThread class FocusedDisplayRepository class FocusedDisplayRepositoryImpl @Inject constructor( @Background val backgroundScope: CoroutineScope, @Background private val backgroundExecutor: Executor, transitions: ShellTransitions, @FocusedDisplayRepoLog logBuffer: LogBuffer, ) { ) : FocusedDisplayRepository { val focusedTask: Flow<Int> = conflatedCallbackFlow<Int> { val listener = Loading @@ -67,7 +72,6 @@ constructor( ) } /** Provides the currently focused display. */ val focusedDisplayId: StateFlow<Int> override val focusedDisplayId: StateFlow<Int> get() = focusedTask.stateIn(backgroundScope, SharingStarted.Eagerly, DEFAULT_DISPLAY) } packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt +6 −5 Original line number Diff line number Diff line Loading @@ -28,6 +28,7 @@ import com.android.systemui.Flags.screenshotMultidisplayFocusChange import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.FocusedDisplayRepository import com.android.systemui.res.R import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER Loading Loading @@ -83,6 +84,7 @@ constructor( private val uiEventLogger: UiEventLogger, private val screenshotNotificationControllerFactory: ScreenshotNotificationsController.Factory, private val headlessScreenshotHandler: HeadlessScreenshotHandler, private val focusedDisplayRepository: FocusedDisplayRepository, ) : TakeScreenshotExecutor { private val displays = displayRepository.displays private var screenshotController: InteractiveScreenshotHandler? = null Loading Loading @@ -216,14 +218,13 @@ constructor( ?: error("Can't find default display") // All other invocations use the focused display else -> focusedDisplay() else -> displayRepository.getDisplay(focusedDisplayRepository.focusedDisplayId.value) ?: displayRepository.getDisplay(Display.DEFAULT_DISPLAY) ?: error("Can't find default display") } } // TODO(b/367394043): Determine the focused display here. private suspend fun focusedDisplay() = displayRepository.getDisplay(Display.DEFAULT_DISPLAY) ?: error("Can't find default display") /** Propagates the close system dialog signal to the ScreenshotController. */ override fun onCloseSystemDialogsReceived() { if (screenshotController?.isPendingSharedTransition() == false) { Loading packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt +56 −0 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import com.android.internal.util.ScreenshotRequest import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.display.data.repository.FakeFocusedDisplayRepository import com.android.systemui.display.data.repository.display import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq Loading Loading @@ -58,6 +59,7 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { private val testScope = TestScope(UnconfinedTestDispatcher()) private val eventLogger = UiEventLoggerFake() private val headlessHandler = mock<HeadlessScreenshotHandler>() private val focusedDisplayRepository = FakeFocusedDisplayRepository() private val screenshotExecutor = TakeScreenshotExecutorImpl( Loading @@ -68,6 +70,7 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { eventLogger, notificationControllerFactory, headlessHandler, focusedDisplayRepository, ) @Before Loading Loading @@ -308,6 +311,59 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { screenshotExecutor.onDestroy() } @Test @EnableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE) fun executeScreenshots_keyOther_usesFocusedDisplay() = testScope.runTest { val displayId = 1 setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = displayId)) val onSaved = { _: Uri? -> } focusedDisplayRepository.emit(displayId) screenshotExecutor.executeScreenshots( createScreenshotRequest( source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER ), onSaved, callback, ) val dataCaptor = ArgumentCaptor<ScreenshotData>() verify(controller).handleScreenshot(dataCaptor.capture(), any(), any()) assertThat(dataCaptor.value.displayId).isEqualTo(displayId) screenshotExecutor.onDestroy() } @Test @EnableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE) fun executeScreenshots_keyOtherInvalidDisplay_usesDefault() = testScope.runTest { setDisplays( display(TYPE_INTERNAL, id = Display.DEFAULT_DISPLAY), display(TYPE_EXTERNAL, id = 1), ) focusedDisplayRepository.emit(5) // invalid display val onSaved = { _: Uri? -> } screenshotExecutor.executeScreenshots( createScreenshotRequest( source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER ), onSaved, callback, ) val dataCaptor = ArgumentCaptor<ScreenshotData>() verify(controller).handleScreenshot(dataCaptor.capture(), any(), any()) assertThat(dataCaptor.value.displayId).isEqualTo(Display.DEFAULT_DISPLAY) screenshotExecutor.onDestroy() } @Test fun onDestroy_propagatedToControllers() = testScope.runTest { Loading packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeFocusedDisplayRepository.kt 0 → 100644 +42 −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.display.data.repository import android.view.Display import com.android.systemui.dagger.SysUISingleton import dagger.Binds import dagger.Module import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @SysUISingleton /** Fake [FocusedDisplayRepository] for testing. */ class FakeFocusedDisplayRepository @Inject constructor() : FocusedDisplayRepository { private val flow = MutableStateFlow<Int>(Display.DEFAULT_DISPLAY) override val focusedDisplayId: StateFlow<Int> get() = flow.asStateFlow() suspend fun emit(focusedDisplay: Int) = flow.emit(focusedDisplay) } @Module interface FakeFocusedDisplayRepositoryModule { @Binds fun bindFake(fake: FakeFocusedDisplayRepository): FocusedDisplayRepository } Loading
packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt +7 −0 Original line number Diff line number Diff line Loading @@ -20,6 +20,8 @@ import com.android.systemui.display.data.repository.DeviceStateRepository import com.android.systemui.display.data.repository.DeviceStateRepositoryImpl import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.DisplayRepositoryImpl import com.android.systemui.display.data.repository.FocusedDisplayRepository import com.android.systemui.display.data.repository.FocusedDisplayRepositoryImpl import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractorImpl import dagger.Binds Loading @@ -39,4 +41,9 @@ interface DisplayModule { fun bindsDeviceStateRepository( deviceStateRepository: DeviceStateRepositoryImpl ): DeviceStateRepository @Binds fun bindsFocusedDisplayRepository( focusedDisplayRepository: FocusedDisplayRepositoryImpl ): FocusedDisplayRepository }
packages/SystemUI/src/com/android/systemui/display/data/repository/FocusedDisplayRepository.kt +8 −4 Original line number Diff line number Diff line Loading @@ -37,16 +37,21 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn /** Repository tracking display focus. */ interface FocusedDisplayRepository { /** Provides the currently focused display. */ val focusedDisplayId: StateFlow<Int> } @SysUISingleton @MainThread class FocusedDisplayRepository class FocusedDisplayRepositoryImpl @Inject constructor( @Background val backgroundScope: CoroutineScope, @Background private val backgroundExecutor: Executor, transitions: ShellTransitions, @FocusedDisplayRepoLog logBuffer: LogBuffer, ) { ) : FocusedDisplayRepository { val focusedTask: Flow<Int> = conflatedCallbackFlow<Int> { val listener = Loading @@ -67,7 +72,6 @@ constructor( ) } /** Provides the currently focused display. */ val focusedDisplayId: StateFlow<Int> override val focusedDisplayId: StateFlow<Int> get() = focusedTask.stateIn(backgroundScope, SharingStarted.Eagerly, DEFAULT_DISPLAY) }
packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt +6 −5 Original line number Diff line number Diff line Loading @@ -28,6 +28,7 @@ import com.android.systemui.Flags.screenshotMultidisplayFocusChange import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.FocusedDisplayRepository import com.android.systemui.res.R import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER Loading Loading @@ -83,6 +84,7 @@ constructor( private val uiEventLogger: UiEventLogger, private val screenshotNotificationControllerFactory: ScreenshotNotificationsController.Factory, private val headlessScreenshotHandler: HeadlessScreenshotHandler, private val focusedDisplayRepository: FocusedDisplayRepository, ) : TakeScreenshotExecutor { private val displays = displayRepository.displays private var screenshotController: InteractiveScreenshotHandler? = null Loading Loading @@ -216,14 +218,13 @@ constructor( ?: error("Can't find default display") // All other invocations use the focused display else -> focusedDisplay() else -> displayRepository.getDisplay(focusedDisplayRepository.focusedDisplayId.value) ?: displayRepository.getDisplay(Display.DEFAULT_DISPLAY) ?: error("Can't find default display") } } // TODO(b/367394043): Determine the focused display here. private suspend fun focusedDisplay() = displayRepository.getDisplay(Display.DEFAULT_DISPLAY) ?: error("Can't find default display") /** Propagates the close system dialog signal to the ScreenshotController. */ override fun onCloseSystemDialogsReceived() { if (screenshotController?.isPendingSharedTransition() == false) { Loading
packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt +56 −0 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import com.android.internal.util.ScreenshotRequest import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.display.data.repository.FakeFocusedDisplayRepository import com.android.systemui.display.data.repository.display import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq Loading Loading @@ -58,6 +59,7 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { private val testScope = TestScope(UnconfinedTestDispatcher()) private val eventLogger = UiEventLoggerFake() private val headlessHandler = mock<HeadlessScreenshotHandler>() private val focusedDisplayRepository = FakeFocusedDisplayRepository() private val screenshotExecutor = TakeScreenshotExecutorImpl( Loading @@ -68,6 +70,7 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { eventLogger, notificationControllerFactory, headlessHandler, focusedDisplayRepository, ) @Before Loading Loading @@ -308,6 +311,59 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { screenshotExecutor.onDestroy() } @Test @EnableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE) fun executeScreenshots_keyOther_usesFocusedDisplay() = testScope.runTest { val displayId = 1 setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = displayId)) val onSaved = { _: Uri? -> } focusedDisplayRepository.emit(displayId) screenshotExecutor.executeScreenshots( createScreenshotRequest( source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER ), onSaved, callback, ) val dataCaptor = ArgumentCaptor<ScreenshotData>() verify(controller).handleScreenshot(dataCaptor.capture(), any(), any()) assertThat(dataCaptor.value.displayId).isEqualTo(displayId) screenshotExecutor.onDestroy() } @Test @EnableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE) fun executeScreenshots_keyOtherInvalidDisplay_usesDefault() = testScope.runTest { setDisplays( display(TYPE_INTERNAL, id = Display.DEFAULT_DISPLAY), display(TYPE_EXTERNAL, id = 1), ) focusedDisplayRepository.emit(5) // invalid display val onSaved = { _: Uri? -> } screenshotExecutor.executeScreenshots( createScreenshotRequest( source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER ), onSaved, callback, ) val dataCaptor = ArgumentCaptor<ScreenshotData>() verify(controller).handleScreenshot(dataCaptor.capture(), any(), any()) assertThat(dataCaptor.value.displayId).isEqualTo(Display.DEFAULT_DISPLAY) screenshotExecutor.onDestroy() } @Test fun onDestroy_propagatedToControllers() = testScope.runTest { Loading
packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeFocusedDisplayRepository.kt 0 → 100644 +42 −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.display.data.repository import android.view.Display import com.android.systemui.dagger.SysUISingleton import dagger.Binds import dagger.Module import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @SysUISingleton /** Fake [FocusedDisplayRepository] for testing. */ class FakeFocusedDisplayRepository @Inject constructor() : FocusedDisplayRepository { private val flow = MutableStateFlow<Int>(Display.DEFAULT_DISPLAY) override val focusedDisplayId: StateFlow<Int> get() = flow.asStateFlow() suspend fun emit(focusedDisplay: Int) = flow.emit(focusedDisplay) } @Module interface FakeFocusedDisplayRepositoryModule { @Binds fun bindFake(fake: FakeFocusedDisplayRepository): FocusedDisplayRepository }