diff options
20 files changed, 653 insertions, 70 deletions
diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 3c6e3e682bc3..74a444059452 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -866,6 +866,14 @@ package android.companion { } +package android.companion.virtual { + + public final class VirtualDeviceManager { + method @FlaggedApi("android.companion.virtual.flags.interactive_screen_mirror") public boolean isVirtualDeviceOwnedMirrorDisplay(int); + } + +} + package android.content { public final class AttributionSource implements android.os.Parcelable { diff --git a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl index b665036c202f..04933126f294 100644 --- a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl +++ b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl @@ -122,4 +122,10 @@ interface IVirtualDeviceManager { * {@code android.media.AudioManager.SystemSoundEffect} */ void playSoundEffect(int deviceId, int effectType); + + /** + * Returns whether the given display is an auto-mirror display owned by a virtual + * device. + */ + boolean isVirtualDeviceOwnedMirrorDisplay(int displayId); } diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index 25693660e1ab..d40a5911d8f4 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -28,6 +28,7 @@ import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SystemApi; import android.annotation.SystemService; +import android.annotation.TestApi; import android.annotation.UserIdInt; import android.app.PendingIntent; import android.companion.AssociationInfo; @@ -436,6 +437,25 @@ public final class VirtualDeviceManager { } /** + * Returns whether the given display is an auto-mirror display owned by a virtual device. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR) + @TestApi + public boolean isVirtualDeviceOwnedMirrorDisplay(int displayId) { + if (mService == null) { + Log.w(TAG, "Failed to retrieve virtual devices; no virtual device manager service."); + return false; + } + try { + return mService.isVirtualDeviceOwnedMirrorDisplay(displayId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * A representation of a virtual device. * * <p>A virtual device can have its own virtual displays, audio input/output, sensors, etc. diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig index 21427ac49dac..55ae8eec35d9 100644 --- a/core/java/android/companion/virtual/flags.aconfig +++ b/core/java/android/companion/virtual/flags.aconfig @@ -56,3 +56,10 @@ flag { description: "Enable express metrics in VDM" bug: "307297730" } + +flag { + name: "interactive_screen_mirror" + namespace: "virtual_devices" + description: "Enable interactive screen mirroring using Virtual Devices" + bug: "292212199" +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index d57f31f91df1..8b992fcae67e 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -41,6 +41,7 @@ import android.app.smartspace.SmartspaceManager; import android.app.trust.TrustManager; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothManager; +import android.companion.virtual.VirtualDeviceManager; import android.content.ClipboardManager; import android.content.ContentResolver; import android.content.Context; @@ -236,6 +237,12 @@ public class FrameworkServicesModule { @Provides @Singleton + static VirtualDeviceManager provideVirtualDeviceManager(Context context) { + return context.getSystemService(VirtualDeviceManager.class); + } + + @Provides + @Singleton static DeviceStateManager provideDeviceStateManager(Context context) { return context.getSystemService(DeviceStateManager.class); } diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt index 3e2ecee1fe3e..0c8dbe7ed916 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt @@ -56,6 +56,9 @@ interface DisplayRepository { /** Display change event indicating a change to the given displayId has occurred. */ val displayChangeEvent: Flow<Int> + /** Display addition event indicating a new display has been added. */ + val displayAdditionEvent: Flow<Display?> + /** Provides the current set of displays. */ val displays: Flow<Set<Display>> @@ -126,6 +129,11 @@ constructor( override val displayChangeEvent: Flow<Int> = allDisplayEvents.filter { it is DisplayEvent.Changed }.map { it.displayId } + override val displayAdditionEvent: Flow<Display?> = + allDisplayEvents + .filter { it is DisplayEvent.Added } + .map { displayManager.getDisplay(it.displayId) } + private val enabledDisplays = allDisplayEvents .map { getDisplays() } diff --git a/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt b/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt index 11ed96d5c7eb..cf868856c419 100644 --- a/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt @@ -16,6 +16,8 @@ package com.android.systemui.display.domain.interactor +import android.companion.virtual.VirtualDeviceManager +import android.companion.virtual.flags.Flags import android.view.Display import com.android.systemui.dagger.SysUISingleton import com.android.systemui.display.data.repository.DisplayRepository @@ -26,6 +28,7 @@ import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map /** Provides information about an external connected display. */ @@ -40,6 +43,12 @@ interface ConnectedDisplayInteractor { */ val connectedDisplayState: Flow<State> + /** + * Indicates that there is a new connected display (either an external display or a virtual + * device owned mirror display). + */ + val connectedDisplayAddition: Flow<Unit> + /** Pending display that can be enabled to be used by the system. */ val pendingDisplay: Flow<PendingDisplay?> @@ -69,6 +78,7 @@ interface ConnectedDisplayInteractor { class ConnectedDisplayInteractorImpl @Inject constructor( + private val virtualDeviceManager: VirtualDeviceManager, keyguardRepository: KeyguardRepository, displayRepository: DisplayRepository, ) : ConnectedDisplayInteractor { @@ -76,13 +86,14 @@ constructor( override val connectedDisplayState: Flow<State> = displayRepository.displays .map { displays -> - val externalDisplays = - displays.filter { display -> display.type == Display.TYPE_EXTERNAL } + val externalDisplays = displays.filter { isExternalDisplay(it) } - val secureExternalDisplays = - externalDisplays.filter { it.flags and Display.FLAG_SECURE != 0 } + val secureExternalDisplays = externalDisplays.filter { isSecureDisplay(it) } - if (externalDisplays.isEmpty()) { + val virtualDeviceMirrorDisplays = + displays.filter { isVirtualDeviceOwnedMirrorDisplay(it) } + + if (externalDisplays.isEmpty() && virtualDeviceMirrorDisplays.isEmpty()) { State.DISCONNECTED } else if (!secureExternalDisplays.isEmpty()) { State.CONNECTED_SECURE @@ -92,6 +103,13 @@ constructor( } .distinctUntilChanged() + override val connectedDisplayAddition: Flow<Unit> = + displayRepository.displayAdditionEvent + .filter { + it != null && (isExternalDisplay(it) || isVirtualDeviceOwnedMirrorDisplay(it)) + } + .map {} // map to Unit + // Provides the pending display only if the lockscreen is unlocked override val pendingDisplay: Flow<PendingDisplay?> = displayRepository.pendingDisplay.combine(keyguardRepository.isKeyguardShowing) { @@ -109,4 +127,17 @@ constructor( override suspend fun enable() = this@toInteractorPendingDisplay.enable() override suspend fun ignore() = this@toInteractorPendingDisplay.ignore() } + + private fun isExternalDisplay(display: Display): Boolean { + return display.type == Display.TYPE_EXTERNAL + } + + private fun isSecureDisplay(display: Display): Boolean { + return display.flags and Display.FLAG_SECURE != 0 + } + + private fun isVirtualDeviceOwnedMirrorDisplay(display: Display): Boolean { + return Flags.interactiveScreenMirror() && + virtualDeviceManager.isVirtualDeviceOwnedMirrorDisplay(display.displayId) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt index 8ee1ade7a185..e9d5deccac94 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt @@ -24,7 +24,6 @@ import com.android.systemui.res.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor -import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.privacy.PrivacyChipBuilder @@ -35,7 +34,6 @@ import com.android.systemui.util.time.SystemClock import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -56,8 +54,7 @@ constructor( connectedDisplayInteractor: ConnectedDisplayInteractor ) { private val onDisplayConnectedFlow = - connectedDisplayInteractor.connectedDisplayState - .filter { it != State.DISCONNECTED } + connectedDisplayInteractor.connectedDisplayAddition private var connectedDisplayCollectionJob: Job? = null private lateinit var scheduler: SystemStatusAnimationScheduler diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt index 511562f2aec0..d80dd76b23e5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt @@ -390,6 +390,17 @@ class DisplayRepositoryTest : SysuiTestCase() { assertThat(pendingDisplay!!.id).isEqualTo(1) } + @Test + fun onDisplayAdded_emitsDisplayAdditionEvent() = + testScope.runTest { + val display by lastDisplayAdditionEvent() + + sendOnDisplayAdded(1, TYPE_EXTERNAL) + + assertThat(display!!.displayId).isEqualTo(1) + assertThat(display!!.type).isEqualTo(TYPE_EXTERNAL) + } + private fun Iterable<Display>.ids(): List<Int> = map { it.displayId } // Wrapper to capture the displayListener. @@ -411,6 +422,12 @@ class DisplayRepositoryTest : SysuiTestCase() { return flowValue } + private fun TestScope.lastDisplayAdditionEvent(): FlowValue<Display?> { + val flowValue = collectLastValue(displayRepository.displayAdditionEvent) + captureAddedRemovedListener() + return flowValue + } + private fun captureAddedRemovedListener() { verify(displayManager) .registerDisplayListener( @@ -423,9 +440,17 @@ class DisplayRepositoryTest : SysuiTestCase() { ) ) } + + private fun sendOnDisplayAdded(id: Int, displayType: Int) { + val mockDisplay = display(id = id, type = displayType) + whenever(displayManager.getDisplay(eq(id))).thenReturn(mockDisplay) + displayListener.value.onDisplayAdded(id) + } + private fun sendOnDisplayAdded(id: Int) { displayListener.value.onDisplayAdded(id) } + private fun sendOnDisplayRemoved(id: Int) { displayListener.value.onDisplayRemoved(id) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt index 26ee09412687..0db3de2ce0dd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt @@ -16,11 +16,14 @@ package com.android.systemui.display.domain.interactor +import android.companion.virtual.VirtualDeviceManager +import android.companion.virtual.flags.Flags.FLAG_INTERACTIVE_SCREEN_MIRROR import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.Display import android.view.Display.TYPE_EXTERNAL import android.view.Display.TYPE_INTERNAL +import android.view.Display.TYPE_VIRTUAL import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.FlowValue @@ -31,14 +34,20 @@ import com.android.systemui.display.data.repository.display import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.anyInt @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper @@ -46,14 +55,22 @@ import org.junit.runner.RunWith @SmallTest class ConnectedDisplayInteractorTest : SysuiTestCase() { + private val virtualDeviceManager = mock<VirtualDeviceManager>() + private val fakeDisplayRepository = FakeDisplayRepository() private val fakeKeyguardRepository = FakeKeyguardRepository() private val connectedDisplayStateProvider: ConnectedDisplayInteractor = - ConnectedDisplayInteractorImpl(fakeKeyguardRepository, fakeDisplayRepository) + ConnectedDisplayInteractorImpl( + virtualDeviceManager, + fakeKeyguardRepository, + fakeDisplayRepository + ) private val testScope = TestScope(UnconfinedTestDispatcher()) @Before fun setup() { + mSetFlagsRule.disableFlags(FLAG_INTERACTIVE_SCREEN_MIRROR) + whenever(virtualDeviceManager.isVirtualDeviceOwnedMirrorDisplay(anyInt())).thenReturn(false) fakeKeyguardRepository.setKeyguardShowing(false) } @@ -137,6 +154,96 @@ class ConnectedDisplayInteractorTest : SysuiTestCase() { } @Test + fun displayState_virtualDeviceOwnedMirrorVirtualDisplay_connected() = + testScope.runTest { + mSetFlagsRule.enableFlags(FLAG_INTERACTIVE_SCREEN_MIRROR) + whenever(virtualDeviceManager.isVirtualDeviceOwnedMirrorDisplay(anyInt())) + .thenReturn(true) + val value by lastValue() + + fakeDisplayRepository.emit(setOf(display(type = TYPE_VIRTUAL))) + + assertThat(value).isEqualTo(State.CONNECTED) + } + + @Test + fun displayState_virtualDeviceUnownedMirrorVirtualDisplay_disconnected() = + testScope.runTest { + val value by lastValue() + + fakeDisplayRepository.emit(setOf(display(type = TYPE_VIRTUAL))) + + assertThat(value).isEqualTo(State.DISCONNECTED) + } + + @Test + fun virtualDeviceOwnedMirrorVirtualDisplay_emitsConnectedDisplayAddition() = + testScope.runTest { + mSetFlagsRule.enableFlags(FLAG_INTERACTIVE_SCREEN_MIRROR) + whenever(virtualDeviceManager.isVirtualDeviceOwnedMirrorDisplay(anyInt())) + .thenReturn(true) + var count = 0 + val job = + connectedDisplayStateProvider.connectedDisplayAddition + .onEach { count++ } + .launchIn(this) + + fakeDisplayRepository.emit(display(type = TYPE_VIRTUAL)) + + runCurrent() + assertThat(count).isEqualTo(1) + job.cancel() + } + + @Test + fun virtualDeviceUnownedMirrorVirtualDisplay_doesNotEmitConnectedDisplayAddition() = + testScope.runTest { + var count = 0 + val job = + connectedDisplayStateProvider.connectedDisplayAddition + .onEach { count++ } + .launchIn(this) + + fakeDisplayRepository.emit(display(type = TYPE_VIRTUAL)) + + runCurrent() + assertThat(count).isEqualTo(0) + job.cancel() + } + + @Test + fun externalDisplay_emitsConnectedDisplayAddition() = + testScope.runTest { + var count = 0 + val job = + connectedDisplayStateProvider.connectedDisplayAddition + .onEach { count++ } + .launchIn(this) + + fakeDisplayRepository.emit(display(type = TYPE_EXTERNAL)) + + runCurrent() + assertThat(count).isEqualTo(1) + job.cancel() + } + + @Test + fun internalDisplay_doesNotEmitConnectedDisplayAddition() = + testScope.runTest { + var count = 0 + val job = + connectedDisplayStateProvider.connectedDisplayAddition + .onEach { count++ } + .launchIn(this) + + fakeDisplayRepository.emit(display(type = TYPE_INTERNAL)) + + runCurrent() + assertThat(count).isEqualTo(0) + job.cancel() + } + + @Test fun pendingDisplay_propagated() = testScope.runTest { val value by lastPendingDisplay() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventCoordinatorTest.kt index 66d2465750ad..c289ff3bc19d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventCoordinatorTest.kt @@ -21,7 +21,6 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay -import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State.CONNECTED import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.privacy.PrivacyItemController import com.android.systemui.statusbar.policy.BatteryController @@ -79,8 +78,8 @@ class SystemEventCoordinatorTest : SysuiTestCase() { testScope.runTest { systemEventCoordinator.startObserving() - connectedDisplayInteractor.emit(CONNECTED) - connectedDisplayInteractor.emit(CONNECTED) + connectedDisplayInteractor.emit() + connectedDisplayInteractor.emit() verify(scheduler, times(2)).onStatusEvent(any<ConnectedDisplayEvent>()) } @@ -90,21 +89,23 @@ class SystemEventCoordinatorTest : SysuiTestCase() { testScope.runTest { systemEventCoordinator.startObserving() - connectedDisplayInteractor.emit(CONNECTED) + connectedDisplayInteractor.emit() verify(scheduler).onStatusEvent(any<ConnectedDisplayEvent>()) systemEventCoordinator.stopObserving() - connectedDisplayInteractor.emit(CONNECTED) + connectedDisplayInteractor.emit() verifyNoMoreInteractions(scheduler) } class FakeConnectedDisplayInteractor : ConnectedDisplayInteractor { - private val flow = MutableSharedFlow<ConnectedDisplayInteractor.State>() - suspend fun emit(value: ConnectedDisplayInteractor.State) = flow.emit(value) + private val flow = MutableSharedFlow<Unit>() + suspend fun emit() = flow.emit(Unit) override val connectedDisplayState: Flow<ConnectedDisplayInteractor.State> + get() = MutableSharedFlow<ConnectedDisplayInteractor.State>() + override val connectedDisplayAddition: Flow<Unit> get() = flow override val pendingDisplay: Flow<PendingDisplay?> get() = MutableSharedFlow<PendingDisplay>() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt index 71c27dec74d4..da6c28ad9af4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt @@ -317,6 +317,8 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() { suspend fun emit(value: State) = flow.emit(value) override val connectedDisplayState: Flow<State> get() = flow + override val connectedDisplayAddition: Flow<Unit> + get() = TODO("Not yet implemented") override val pendingDisplay: Flow<PendingDisplay?> get() = TODO("Not yet implemented") } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt index 5fd0b4f61b43..d8098b7ffab3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt @@ -47,6 +47,10 @@ class FakeDisplayRepository : DisplayRepository { private val flow = MutableSharedFlow<Set<Display>>(replay = 1) private val pendingDisplayFlow = MutableSharedFlow<DisplayRepository.PendingDisplay?>(replay = 1) + private val displayAdditionEventFlow = MutableSharedFlow<Display?>(replay = 1) + + /** Emits [value] as [displayAdditionEvent] flow value. */ + suspend fun emit(value: Display?) = displayAdditionEventFlow.emit(value) /** Emits [value] as [displays] flow value. */ suspend fun emit(value: Set<Display>) = flow.emit(value) @@ -60,6 +64,9 @@ class FakeDisplayRepository : DisplayRepository { override val pendingDisplay: Flow<DisplayRepository.PendingDisplay?> get() = pendingDisplayFlow + override val displayAdditionEvent: Flow<Display?> + get() = displayAdditionEventFlow + private val _displayChangeEvent = MutableSharedFlow<Int>(replay = 1) override val displayChangeEvent: Flow<Int> = _displayChangeEvent suspend fun emitDisplayChangeEvent(displayId: Int) = _displayChangeEvent.emit(displayId) diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java index 852e36dcc605..8c728f1eeffd 100644 --- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java +++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java @@ -118,6 +118,7 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController private final Object mGenericWindowPolicyControllerLock = new Object(); @Nullable private final ActivityBlockedCallback mActivityBlockedCallback; private int mDisplayId = Display.INVALID_DISPLAY; + private boolean mIsMirrorDisplay = false; @NonNull @GuardedBy("mGenericWindowPolicyControllerLock") @@ -203,8 +204,9 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController /** * Expected to be called once this object is associated with a newly created display. */ - public void setDisplayId(int displayId) { + void setDisplayId(int displayId, boolean isMirrorDisplay) { mDisplayId = displayId; + mIsMirrorDisplay = isMirrorDisplay; } /** @@ -256,9 +258,7 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController @Nullable Intent intent, @WindowConfiguration.WindowingMode int windowingMode, int launchingFromDisplayId, boolean isNewTask) { if (!canContainActivity(activityInfo, windowingMode, launchingFromDisplayId, isNewTask)) { - if (mActivityBlockedCallback != null) { - mActivityBlockedCallback.onActivityBlocked(mDisplayId, activityInfo); - } + notifyActivityBlocked(activityInfo); return false; } if (mIntentListenerCallback != null && intent != null @@ -273,6 +273,11 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController public boolean canContainActivity(@NonNull ActivityInfo activityInfo, @WindowConfiguration.WindowingMode int windowingMode, int launchingFromDisplayId, boolean isNewTask) { + // Mirror displays cannot contain activities. + if (mIsMirrorDisplay) { + Slog.d(TAG, "Mirror virtual displays cannot contain activities."); + return false; + } if (!isWindowingModeSupported(windowingMode)) { Slog.d(TAG, "Virtual device doesn't support windowing mode " + windowingMode); return false; @@ -344,9 +349,7 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController // TODO(b/201712607): Add checks for the apps that use SurfaceView#setSecure. if ((windowFlags & FLAG_SECURE) != 0 || (systemWindowFlags & SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS) != 0) { - if (mActivityBlockedCallback != null) { - mActivityBlockedCallback.onActivityBlocked(mDisplayId, activityInfo); - } + notifyActivityBlocked(activityInfo); return false; } } @@ -428,6 +431,14 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController && mDisplayCategories.contains(activityInfo.requiredDisplayCategory); } + private void notifyActivityBlocked(ActivityInfo activityInfo) { + // Don't trigger activity blocked callback for mirror displays, because we can't show + // any activity or presentation on it anyway. + if (!mIsMirrorDisplay && mActivityBlockedCallback != null) { + mActivityBlockedCallback.onActivityBlocked(mDisplayId, activityInfo); + } + } + private static boolean isAllowedByPolicy(boolean allowedByDefault, Set<ComponentName> exemptions, ComponentName component) { // Either allowed and the exemptions do not contain the component, diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 4298c079a63e..70c449fe147c 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -167,7 +167,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub private final VirtualDeviceLog mVirtualDeviceLog; private final String mOwnerPackageName; private final int mDeviceId; - private @Nullable final String mPersistentDeviceId; + @Nullable + private final String mPersistentDeviceId; // Thou shall not hold the mVirtualDeviceLock over the mInputController calls. // Holding the lock can lead to lock inversion with GlobalWindowManagerLock. // 1. After display is created the window manager calls into VDM during construction @@ -191,6 +192,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub private final IVirtualDeviceActivityListener mActivityListener; private final IVirtualDeviceSoundEffectListener mSoundEffectListener; private final DisplayManagerGlobal mDisplayManager; + private final DisplayManagerInternal mDisplayManagerInternal; @GuardedBy("mVirtualDeviceLock") private final Map<IBinder, IntentFilter> mIntentInterceptors = new ArrayMap<>(); @NonNull @@ -313,6 +315,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub mParams = params; mDevicePolicies = params.getDevicePolicies(); mDisplayManager = displayManager; + mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); if (inputController == null) { mInputController = new InputController( context.getMainThreadHandler(), @@ -971,8 +974,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub @NonNull Set<String> displayCategories) { final boolean activityLaunchAllowedByDefault = Flags.dynamicPolicy() - ? getDevicePolicy(POLICY_TYPE_ACTIVITY) == DEVICE_POLICY_DEFAULT - : mParams.getDefaultActivityPolicy() == ACTIVITY_POLICY_DEFAULT_ALLOWED; + ? getDevicePolicy(POLICY_TYPE_ACTIVITY) == DEVICE_POLICY_DEFAULT + : mParams.getDefaultActivityPolicy() == ACTIVITY_POLICY_DEFAULT_ALLOWED; final boolean crossTaskNavigationAllowedByDefault = mParams.getDefaultNavigationPolicy() == NAVIGATION_POLICY_DEFAULT_ALLOWED; final boolean showTasksInHostDeviceRecents = @@ -987,7 +990,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub activityLaunchAllowedByDefault, mActivityPolicyExemptions, crossTaskNavigationAllowedByDefault, - /*crossTaskNavigationExemptions=*/crossTaskNavigationAllowedByDefault + /* crossTaskNavigationExemptions= */crossTaskNavigationAllowedByDefault ? mParams.getBlockedCrossTaskNavigations() : mParams.getAllowedCrossTaskNavigations(), mPermissionDialogComponent, @@ -1016,12 +1019,12 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub synchronized (mVirtualDeviceLock) { gwpc = createWindowPolicyControllerLocked(virtualDisplayConfig.getDisplayCategories()); } - DisplayManagerInternal displayManager = LocalServices.getService( - DisplayManagerInternal.class); int displayId; - displayId = displayManager.createVirtualDisplay(virtualDisplayConfig, callback, + displayId = mDisplayManagerInternal.createVirtualDisplay(virtualDisplayConfig, callback, this, gwpc, packageName); - gwpc.setDisplayId(displayId); + gwpc.setDisplayId(displayId, /* isMirrorDisplay= */ Flags.interactiveScreenMirror() + && mDisplayManagerInternal.getDisplayIdToMirror(displayId) + != Display.INVALID_DISPLAY); boolean showPointer; synchronized (mVirtualDeviceLock) { diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index 06045109b3e5..3031a840f4b1 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -42,6 +42,7 @@ import android.companion.virtualnative.IVirtualDeviceManagerNative; import android.content.AttributionSource; import android.content.Context; import android.content.Intent; +import android.hardware.display.DisplayManagerInternal; import android.hardware.display.IVirtualDisplayCallback; import android.hardware.display.VirtualDisplayConfig; import android.os.Binder; @@ -66,6 +67,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DumpUtils; import com.android.modules.expresslog.Counter; +import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.companion.virtual.VirtualDeviceImpl.PendingTrampoline; import com.android.server.wm.ActivityInterceptorCallback; @@ -545,6 +547,17 @@ public class VirtualDeviceManagerService extends SystemService { } } + @Override // Binder call + public boolean isVirtualDeviceOwnedMirrorDisplay(int displayId) { + if (getDeviceIdForDisplayId(displayId) == Context.DEVICE_ID_DEFAULT) { + return false; + } + + DisplayManagerInternal displayManager = LocalServices.getService( + DisplayManagerInternal.class); + return displayManager.getDisplayIdToMirror(displayId) != Display.INVALID_DISPLAY; + } + @Nullable private AssociationInfo getAssociationInfo(String packageName, int associationId) { final UserHandle userHandle = getCallingUserHandle(); diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 087cf20a6682..e475fe63746d 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -31,6 +31,7 @@ import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_CAN_S import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP; +import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_SECURE; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; @@ -57,6 +58,7 @@ import android.app.AppOpsManager; import android.app.compat.CompatChanges; import android.companion.virtual.IVirtualDevice; import android.companion.virtual.VirtualDeviceManager; +import android.companion.virtual.flags.Flags; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.content.BroadcastReceiver; @@ -1482,7 +1484,12 @@ public final class DisplayManagerService extends SystemService { if ((flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) { flags &= ~VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP; } - if ((flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) == 0 && virtualDevice != null) { + // Put the display in the virtual device's display group only if it's not a mirror display, + // and if it doesn't need its own display group. So effectively, mirror displays go into the + // default display group. + if ((flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) == 0 + && (flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0 + && virtualDevice != null) { flags |= VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP; } @@ -1516,11 +1523,16 @@ public final class DisplayManagerService extends SystemService { if (callingUid != Process.SYSTEM_UID && (flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) { - if (!canProjectVideo(projection)) { + // Only a valid media projection or a virtual device can create a mirror virtual + // display. + if (!canProjectVideo(projection) + && !isMirroringSupportedByVirtualDevice(virtualDevice)) { throw new SecurityException("Requires CAPTURE_VIDEO_OUTPUT or " + "CAPTURE_SECURE_VIDEO_OUTPUT permission, or an appropriate " + "MediaProjection token in order to create a screen sharing virtual " - + "display."); + + "display. In order to create a virtual display that does not perform" + + "screen sharing (mirroring), please use the flag" + + "VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY."); } } if (callingUid != Process.SYSTEM_UID && (flags & VIRTUAL_DISPLAY_FLAG_SECURE) != 0) { @@ -1540,6 +1552,15 @@ public final class DisplayManagerService extends SystemService { } } + // Mirror virtual displays created by a virtual device are not allowed to show + // presentations. + if (virtualDevice != null && (flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0 + && (flags & VIRTUAL_DISPLAY_FLAG_PRESENTATION) != 0) { + Slog.d(TAG, "Mirror displays created by a virtual device cannot show " + + "presentations, hence ignoring flag VIRTUAL_DISPLAY_FLAG_PRESENTATION."); + flags &= ~VIRTUAL_DISPLAY_FLAG_PRESENTATION; + } + if (callingUid != Process.SYSTEM_UID && (flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) != 0) { // The virtualDevice instance has been validated above using isValidVirtualDevice @@ -1739,6 +1760,10 @@ public final class DisplayManagerService extends SystemService { return -1; } + private static boolean isMirroringSupportedByVirtualDevice(IVirtualDevice virtualDevice) { + return Flags.interactiveScreenMirror() && virtualDevice != null; + } + private void resizeVirtualDisplayInternal(IBinder appToken, int width, int height, int densityDpi) { synchronized (mSyncRoot) { diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index 16d72e40fbb5..163d248fa317 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -18,10 +18,13 @@ package com.android.server.display; import static android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY; import static android.Manifest.permission.ADD_TRUSTED_DISPLAY; +import static android.Manifest.permission.CAPTURE_VIDEO_OUTPUT; import static android.Manifest.permission.MANAGE_DISPLAYS; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED; +import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP; +import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION; import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY; import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK; @@ -60,6 +63,7 @@ import android.app.PropertyInvalidatedCache; import android.companion.virtual.IVirtualDevice; import android.companion.virtual.IVirtualDeviceManager; import android.companion.virtual.VirtualDeviceManager; +import android.companion.virtual.flags.Flags; import android.compat.testing.PlatformCompatChangeRule; import android.content.Context; import android.content.ContextWrapper; @@ -92,6 +96,7 @@ import android.os.Looper; import android.os.MessageQueue; import android.os.Process; import android.os.RemoteException; +import android.platform.test.flag.junit.SetFlagsRule; import android.view.ContentRecordingSession; import android.view.Display; import android.view.DisplayCutout; @@ -181,6 +186,8 @@ public class DisplayManagerServiceTest { public TestRule compatChangeRule = new PlatformCompatChangeRule(); @Rule(order = 1) public Expect expect = Expect.create(); + @Rule + public SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private Context mContext; @@ -312,7 +319,6 @@ public class DisplayManagerServiceTest { @Mock DisplayDeviceConfig mMockDisplayDeviceConfig; @Mock PackageManagerInternal mMockPackageManagerInternal; - @Captor ArgumentCaptor<ContentRecordingSession> mContentRecordingSessionCaptor; @Mock DisplayManagerFlags mMockFlags; @@ -320,6 +326,7 @@ public class DisplayManagerServiceTest { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(false); + mSetFlagsRule.disableFlags(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR); LocalServices.removeServiceForTest(InputManagerInternal.class); LocalServices.addService(InputManagerInternal.class, mMockInputManagerInternal); @@ -1140,6 +1147,236 @@ public class DisplayManagerServiceTest { 0); } + /** + * Tests that it's not allowed to create an auto-mirror virtual display without + * CAPTURE_VIDEO_OUTPUT permission or a virtual device. + */ + @Test + public void createAutoMirrorDisplay_withoutPermission_withoutVirtualDevice_throwsException() { + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + registerDefaultDisplays(displayManager); + when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); + when(mContext.checkCallingPermission(CAPTURE_VIDEO_OUTPUT)).thenReturn( + PackageManager.PERMISSION_DENIED); + + final VirtualDisplayConfig.Builder builder = + new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) + .setFlags(VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) + .setUniqueId("uniqueId --- mirror display"); + assertThrows(SecurityException.class, () -> { + localService.createVirtualDisplay( + builder.build(), + mMockAppToken /* callback */, + null /* virtualDeviceToken */, + mock(DisplayWindowPolicyController.class), + PACKAGE_NAME); + }); + } + + /** + * Tests that it's not allowed to create an auto-mirror virtual display when display mirroring + * is not supported in a virtual device. + */ + @Test + public void createAutoMirrorDisplay_virtualDeviceDoesntSupportMirroring_throwsException() + throws Exception { + mSetFlagsRule.disableFlags(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR); + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + registerDefaultDisplays(displayManager); + when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); + when(mContext.checkCallingPermission(CAPTURE_VIDEO_OUTPUT)).thenReturn( + PackageManager.PERMISSION_DENIED); + IVirtualDevice virtualDevice = mock(IVirtualDevice.class); + when(virtualDevice.getDeviceId()).thenReturn(1); + when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); + + final VirtualDisplayConfig.Builder builder = + new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) + .setFlags(VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) + .setUniqueId("uniqueId --- mirror display"); + assertThrows(SecurityException.class, () -> { + localService.createVirtualDisplay( + builder.build(), + mMockAppToken /* callback */, + virtualDevice /* virtualDeviceToken */, + mock(DisplayWindowPolicyController.class), + PACKAGE_NAME); + }); + } + + /** + * Tests that the virtual display is added to the default display group when created with + * VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR using a virtual device. + */ + @Test + public void createAutoMirrorVirtualDisplay_addsDisplayToDefaultDisplayGroup() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR); + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + registerDefaultDisplays(displayManager); + when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); + IVirtualDevice virtualDevice = mock(IVirtualDevice.class); + when(virtualDevice.getDeviceId()).thenReturn(1); + when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); + + // Create an auto-mirror virtual display using a virtual device. + final VirtualDisplayConfig.Builder builder = + new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) + .setFlags(VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) + .setUniqueId("uniqueId --- default display group"); + int displayId = + localService.createVirtualDisplay( + builder.build(), + mMockAppToken /* callback */, + virtualDevice /* virtualDeviceToken */, + mock(DisplayWindowPolicyController.class), + PACKAGE_NAME); + + // The virtual display should be in the default display group. + assertEquals(Display.DEFAULT_DISPLAY_GROUP, + localService.getDisplayInfo(displayId).displayGroupId); + } + + /** + * Tests that the virtual display mirrors the default display when created with + * VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR using a virtual device. + */ + @Test + public void createAutoMirrorVirtualDisplay_mirrorsDefaultDisplay() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR); + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + registerDefaultDisplays(displayManager); + when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); + IVirtualDevice virtualDevice = mock(IVirtualDevice.class); + when(virtualDevice.getDeviceId()).thenReturn(1); + when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); + + // Create an auto-mirror virtual display using a virtual device. + final VirtualDisplayConfig.Builder builder = + new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) + .setFlags(VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) + .setUniqueId("uniqueId --- mirror display"); + int displayId = + localService.createVirtualDisplay( + builder.build(), + mMockAppToken /* callback */, + virtualDevice /* virtualDeviceToken */, + mock(DisplayWindowPolicyController.class), + PACKAGE_NAME); + + // The virtual display should mirror the default display. + assertEquals(Display.DEFAULT_DISPLAY, localService.getDisplayIdToMirror(displayId)); + } + + /** + * Tests that the virtual display does not mirror any other display when created with + * VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY using a virtual device. + */ + @Test + public void createOwnContentOnlyVirtualDisplay_doesNotMirrorAnyDisplay() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR); + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + registerDefaultDisplays(displayManager); + when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); + IVirtualDevice virtualDevice = mock(IVirtualDevice.class); + when(virtualDevice.getDeviceId()).thenReturn(1); + when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); + + // Create an auto-mirror virtual display using a virtual device. + final VirtualDisplayConfig.Builder builder = + new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) + .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY) + .setUniqueId("uniqueId --- own content only display"); + int displayId = + localService.createVirtualDisplay( + builder.build(), + mMockAppToken /* callback */, + virtualDevice /* virtualDeviceToken */, + mock(DisplayWindowPolicyController.class), + PACKAGE_NAME); + + // The virtual display should not mirror any display. + assertEquals(Display.INVALID_DISPLAY, localService.getDisplayIdToMirror(displayId)); + // The virtual display should have FLAG_OWN_CONTENT_ONLY set. + assertEquals(DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY, + (displayManager.getDisplayDeviceInfoInternal(displayId).flags + & DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY)); + } + + /** + * Tests that the virtual display should not be always unlocked when created with + * VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR using a virtual device. + */ + @Test + public void createAutoMirrorVirtualDisplay_flagAlwaysUnlockedNotSet() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR); + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + registerDefaultDisplays(displayManager); + when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); + IVirtualDevice virtualDevice = mock(IVirtualDevice.class); + when(virtualDevice.getDeviceId()).thenReturn(1); + when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); + when(mContext.checkCallingPermission(ADD_ALWAYS_UNLOCKED_DISPLAY)) + .thenReturn(PackageManager.PERMISSION_GRANTED); + + // Create an auto-mirror virtual display using a virtual device. + final VirtualDisplayConfig.Builder builder = + new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) + .setFlags(VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR + | VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED) + .setUniqueId("uniqueId --- mirror display"); + int displayId = + localService.createVirtualDisplay( + builder.build(), + mMockAppToken /* callback */, + virtualDevice /* virtualDeviceToken */, + mock(DisplayWindowPolicyController.class), + PACKAGE_NAME); + + // The virtual display should not have FLAG_ALWAYS_UNLOCKED set. + assertEquals(0, (displayManager.getDisplayDeviceInfoInternal(displayId).flags + & DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED)); + } + + /** + * Tests that the virtual display should not allow presentation when created with + * VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR using a virtual device. + */ + @Test + public void createAutoMirrorVirtualDisplay_flagPresentationNotSet() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR); + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + registerDefaultDisplays(displayManager); + when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); + IVirtualDevice virtualDevice = mock(IVirtualDevice.class); + when(virtualDevice.getDeviceId()).thenReturn(1); + when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); + + // Create an auto-mirror virtual display using a virtual device. + final VirtualDisplayConfig.Builder builder = + new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) + .setFlags(VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR + | VIRTUAL_DISPLAY_FLAG_PRESENTATION) + .setUniqueId("uniqueId --- mirror display"); + int displayId = + localService.createVirtualDisplay( + builder.build(), + mMockAppToken /* callback */, + virtualDevice /* virtualDeviceToken */, + mock(DisplayWindowPolicyController.class), + PACKAGE_NAME); + + // The virtual display should not have FLAG_PRESENTATION set. + assertEquals(0, (displayManager.getDisplayDeviceInfoInternal(displayId).flags + & DisplayDeviceInfo.FLAG_PRESENTATION)); + } + @Test public void testGetDisplayIdToMirror() throws Exception { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java index a7c8a6cee0d9..b732d38aefe7 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java @@ -150,7 +150,7 @@ public class GenericWindowPolicyControllerTest { @Test public void openNonBlockedAppOnVirtualDisplay_isNotBlocked() { GenericWindowPolicyController gwpc = createGwpc(); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( NONBLOCKED_APP_PACKAGE_NAME, @@ -163,7 +163,7 @@ public class GenericWindowPolicyControllerTest { @Test public void activityDoesNotSupportDisplayOnRemoteDevices_isBlocked() { GenericWindowPolicyController gwpc = createGwpc(); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( NONBLOCKED_APP_PACKAGE_NAME, @@ -176,7 +176,7 @@ public class GenericWindowPolicyControllerTest { @Test public void openBlockedComponentOnVirtualDisplay_isBlocked() { GenericWindowPolicyController gwpc = createGwpcWithBlockedComponent(BLOCKED_COMPONENT); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( BLOCKED_PACKAGE_NAME, @@ -189,7 +189,7 @@ public class GenericWindowPolicyControllerTest { @Test public void addActivityPolicyExemption_openBlockedOnVirtualDisplay_isBlocked() { GenericWindowPolicyController gwpc = createGwpc(); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); gwpc.setActivityLaunchDefaultAllowed(true); gwpc.addActivityPolicyExemption(BLOCKED_COMPONENT); @@ -204,7 +204,7 @@ public class GenericWindowPolicyControllerTest { @Test public void openNotAllowedComponentOnBlocklistVirtualDisplay_isBlocked() { GenericWindowPolicyController gwpc = createGwpcWithAllowedComponent(NONBLOCKED_COMPONENT); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( BLOCKED_PACKAGE_NAME, @@ -217,7 +217,7 @@ public class GenericWindowPolicyControllerTest { @Test public void addActivityPolicyExemption_openNotAllowedOnVirtualDisplay_isBlocked() { GenericWindowPolicyController gwpc = createGwpc(); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); gwpc.setActivityLaunchDefaultAllowed(false); gwpc.addActivityPolicyExemption(NONBLOCKED_COMPONENT); @@ -232,7 +232,7 @@ public class GenericWindowPolicyControllerTest { @Test public void openAllowedComponentOnBlocklistVirtualDisplay_startsActivity() { GenericWindowPolicyController gwpc = createGwpcWithAllowedComponent(NONBLOCKED_COMPONENT); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( NONBLOCKED_APP_PACKAGE_NAME, @@ -245,7 +245,7 @@ public class GenericWindowPolicyControllerTest { @Test public void addActivityPolicyExemption_openAllowedOnVirtualDisplay_startsActivity() { GenericWindowPolicyController gwpc = createGwpc(); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); gwpc.setActivityLaunchDefaultAllowed(false); gwpc.addActivityPolicyExemption(NONBLOCKED_COMPONENT); @@ -258,9 +258,22 @@ public class GenericWindowPolicyControllerTest { } @Test + public void openNonBlockedAppOnMirrorVirtualDisplay_isBlocked() { + GenericWindowPolicyController gwpc = createGwpc(); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ true); + + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertNoActivityLaunched(gwpc, DISPLAY_ID, activityInfo); + } + + @Test public void canActivityBeLaunched_mismatchingUserHandle_isBlocked() { GenericWindowPolicyController gwpc = createGwpc(); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( NONBLOCKED_APP_PACKAGE_NAME, @@ -274,7 +287,7 @@ public class GenericWindowPolicyControllerTest { @Test public void canActivityBeLaunched_blockedAppStreamingComponent_isNeverBlocked() { GenericWindowPolicyController gwpc = createGwpc(); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( BLOCKED_APP_STREAMING_COMPONENT.getPackageName(), @@ -288,7 +301,7 @@ public class GenericWindowPolicyControllerTest { public void canActivityBeLaunched_blockedAppStreamingComponentExplicitlyBlocked_isNeverBlocked() { GenericWindowPolicyController gwpc = createGwpcWithBlockedComponent( BLOCKED_APP_STREAMING_COMPONENT); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( BLOCKED_APP_STREAMING_COMPONENT.getPackageName(), @@ -302,7 +315,7 @@ public class GenericWindowPolicyControllerTest { @Test public void canActivityBeLaunched_blockedAppStreamingComponentExemptFromStreaming_isNeverBlocked() { GenericWindowPolicyController gwpc = createGwpc(); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); gwpc.setActivityLaunchDefaultAllowed(true); gwpc.addActivityPolicyExemption(BLOCKED_APP_STREAMING_COMPONENT); @@ -318,7 +331,7 @@ public class GenericWindowPolicyControllerTest { @Test public void canActivityBeLaunched_blockedAppStreamingComponentNotAllowlisted_isNeverBlocked() { GenericWindowPolicyController gwpc = createGwpcWithAllowedComponent(NONBLOCKED_COMPONENT); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( BLOCKED_APP_STREAMING_COMPONENT.getPackageName(), @@ -332,7 +345,7 @@ public class GenericWindowPolicyControllerTest { @Test public void canActivityBeLaunched_blockedAppStreamingComponentNotExemptFromBlocklist_isNeverBlocked() { GenericWindowPolicyController gwpc = createGwpc(); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); gwpc.setActivityLaunchDefaultAllowed(false); gwpc.addActivityPolicyExemption(NONBLOCKED_COMPONENT); @@ -348,7 +361,7 @@ public class GenericWindowPolicyControllerTest { @Test public void canActivityBeLaunched_customDisplayCategoryMatches_isNotBlocked() { GenericWindowPolicyController gwpc = createGwpcWithDisplayCategory(DISPLAY_CATEGORY); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( NONBLOCKED_APP_PACKAGE_NAME, @@ -362,7 +375,7 @@ public class GenericWindowPolicyControllerTest { @Test public void canActivityBeLaunched_customDisplayCategoryDoesNotMatch_isBlocked() { GenericWindowPolicyController gwpc = createGwpcWithDisplayCategory(DISPLAY_CATEGORY); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( NONBLOCKED_APP_PACKAGE_NAME, @@ -375,7 +388,7 @@ public class GenericWindowPolicyControllerTest { @Test public void canActivityBeLaunched_crossTaskLaunch_fromDefaultDisplay_isNotBlocked() { GenericWindowPolicyController gwpc = createGwpc(); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( NONBLOCKED_APP_PACKAGE_NAME, @@ -390,7 +403,7 @@ public class GenericWindowPolicyControllerTest { public void canActivityBeLaunched_crossTaskLaunchFromVirtualDisplay_notExplicitlyBlocked_isNotBlocked() { GenericWindowPolicyController gwpc = createGwpcWithCrossTaskNavigationBlockedFor( BLOCKED_COMPONENT); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( NONBLOCKED_APP_PACKAGE_NAME, @@ -406,7 +419,7 @@ public class GenericWindowPolicyControllerTest { public void canActivityBeLaunched_crossTaskLaunchFromVirtualDisplay_explicitlyBlocked_isBlocked() { GenericWindowPolicyController gwpc = createGwpcWithCrossTaskNavigationBlockedFor( BLOCKED_COMPONENT); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( BLOCKED_PACKAGE_NAME, @@ -421,7 +434,7 @@ public class GenericWindowPolicyControllerTest { public void canActivityBeLaunched_crossTaskLaunchFromVirtualDisplay_notAllowed_isBlocked() { GenericWindowPolicyController gwpc = createGwpcWithCrossTaskNavigationAllowed( NONBLOCKED_COMPONENT); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( BLOCKED_PACKAGE_NAME, @@ -436,7 +449,7 @@ public class GenericWindowPolicyControllerTest { public void canActivityBeLaunched_crossTaskLaunchFromVirtualDisplay_allowed_isNotBlocked() { GenericWindowPolicyController gwpc = createGwpcWithCrossTaskNavigationAllowed( NONBLOCKED_COMPONENT); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( NONBLOCKED_APP_PACKAGE_NAME, @@ -450,7 +463,7 @@ public class GenericWindowPolicyControllerTest { @Test public void canActivityBeLaunched_unsupportedWindowingMode_isBlocked() { GenericWindowPolicyController gwpc = createGwpc(); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( NONBLOCKED_APP_PACKAGE_NAME, @@ -464,7 +477,7 @@ public class GenericWindowPolicyControllerTest { @Test public void canActivityBeLaunched_permissionComponent_isBlocked() { GenericWindowPolicyController gwpc = createGwpcWithPermissionComponent(BLOCKED_COMPONENT); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( BLOCKED_PACKAGE_NAME, @@ -490,7 +503,7 @@ public class GenericWindowPolicyControllerTest { public void onRunningAppsChanged_empty_onDisplayEmpty() { ArraySet<Integer> uids = new ArraySet<>(); GenericWindowPolicyController gwpc = createGwpc(); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); gwpc.onRunningAppsChanged(uids); @@ -585,7 +598,7 @@ public class GenericWindowPolicyControllerTest { public void onTopActivitychanged_activityListenerCallbackObserved() { int userId = 1000; GenericWindowPolicyController gwpc = createGwpc(); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); gwpc.onTopActivityChanged(BLOCKED_COMPONENT, 0, userId); verify(mActivityListener) @@ -595,7 +608,7 @@ public class GenericWindowPolicyControllerTest { @Test public void keepActivityOnWindowFlagsChanged_noChange() { GenericWindowPolicyController gwpc = createGwpc(); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( NONBLOCKED_APP_PACKAGE_NAME, @@ -613,7 +626,7 @@ public class GenericWindowPolicyControllerTest { @Test public void keepActivityOnWindowFlagsChanged_flagSecure_isAllowedAfterTM() { GenericWindowPolicyController gwpc = createGwpc(); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( NONBLOCKED_APP_PACKAGE_NAME, @@ -631,7 +644,7 @@ public class GenericWindowPolicyControllerTest { @Test public void keepActivityOnWindowFlagsChanged_systemFlagHideNonSystemOverlayWindows_isAllowedAfterTM() { GenericWindowPolicyController gwpc = createGwpc(); - gwpc.setDisplayId(DISPLAY_ID); + gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false); ActivityInfo activityInfo = getActivityInfo( NONBLOCKED_APP_PACKAGE_NAME, @@ -887,4 +900,14 @@ public class GenericWindowPolicyControllerTest { verify(mActivityBlockedCallback).onActivityBlocked(fromDisplay, activityInfo); verify(mIntentListenerCallback, never()).shouldInterceptIntent(any(Intent.class)); } + + private void assertNoActivityLaunched(GenericWindowPolicyController gwpc, int fromDisplay, + ActivityInfo activityInfo) { + assertThat(gwpc.canActivityBeLaunched(activityInfo, null, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID, true)) + .isFalse(); + + verify(mActivityBlockedCallback, never()).onActivityBlocked(fromDisplay, activityInfo); + verify(mIntentListenerCallback, never()).shouldInterceptIntent(any(Intent.class)); + } } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index a3d415e4918f..461d63745c31 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -59,7 +59,6 @@ import android.companion.virtual.VirtualDeviceParams; import android.companion.virtual.audio.IAudioConfigChangedCallback; import android.companion.virtual.audio.IAudioRoutingCallback; import android.companion.virtual.flags.Flags; -import android.companion.virtual.sensor.IVirtualSensorCallback; import android.companion.virtual.sensor.VirtualSensor; import android.companion.virtual.sensor.VirtualSensorCallback; import android.companion.virtual.sensor.VirtualSensorConfig; @@ -250,8 +249,6 @@ public class VirtualDeviceManagerServiceTest { @Mock private SensorManagerInternal mSensorManagerInternalMock; @Mock - private IVirtualSensorCallback mVirtualSensorCallback; - @Mock private VirtualSensorCallback mSensorCallback; @Mock private IVirtualDeviceActivityListener mActivityListener; @@ -269,7 +266,6 @@ public class VirtualDeviceManagerServiceTest { IPowerManager mIPowerManagerMock; @Mock IThermalService mIThermalServiceMock; - private PowerManager mPowerManager; @Mock private IAudioRoutingCallback mRoutingCallback; @Mock @@ -361,9 +357,10 @@ public class VirtualDeviceManagerServiceTest { when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE)).thenReturn( mDevicePolicyManagerMock); - mPowerManager = new PowerManager(mContext, mIPowerManagerMock, mIThermalServiceMock, + PowerManager powerManager = new PowerManager(mContext, mIPowerManagerMock, + mIThermalServiceMock, new Handler(TestableLooper.get(this).getLooper())); - when(mContext.getSystemService(Context.POWER_SERVICE)).thenReturn(mPowerManager); + when(mContext.getSystemService(Context.POWER_SERVICE)).thenReturn(powerManager); mInputManagerMockHelper = new InputManagerMockHelper( TestableLooper.get(this), mNativeWrapperMock, mIInputManagerMock); @@ -1604,6 +1601,54 @@ public class VirtualDeviceManagerServiceTest { } @Test + public void openNonBlockedAppOnMirrorDisplay_flagEnabled_cannotBeLaunched() { + mSetFlagsRule.enableFlags(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR); + when(mDisplayManagerInternalMock.getDisplayIdToMirror(anyInt())) + .thenReturn(Display.DEFAULT_DISPLAY); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + GenericWindowPolicyController gwpc = mDeviceImpl.getDisplayWindowPolicyControllerForTest( + DISPLAY_ID_1); + doNothing().when(mContext).startActivityAsUser(any(), any(), any()); + + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertThat(gwpc.canActivityBeLaunched(activityInfo, null, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID_1, /*isNewTask=*/ false)) + .isFalse(); + // Verify that BlockedAppStreamingActivity also doesn't launch for mirror displays. + Intent blockedAppIntent = BlockedAppStreamingActivity.createIntent( + activityInfo, mAssociationInfo.getDisplayName()); + verify(mContext, never()).startActivityAsUser(argThat(intent -> + intent.filterEquals(blockedAppIntent)), any(), any()); + } + + @Test + public void openNonBlockedAppOnMirrorDisplay_flagDisabled_launchesActivity() { + when(mDisplayManagerInternalMock.getDisplayIdToMirror(anyInt())) + .thenReturn(Display.DEFAULT_DISPLAY); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + GenericWindowPolicyController gwpc = mDeviceImpl.getDisplayWindowPolicyControllerForTest( + DISPLAY_ID_1); + doNothing().when(mContext).startActivityAsUser(any(), any(), any()); + + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertThat(gwpc.canActivityBeLaunched(activityInfo, null, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID_1, /*isNewTask=*/ false)) + .isTrue(); + Intent blockedAppIntent = BlockedAppStreamingActivity.createIntent( + activityInfo, mAssociationInfo.getDisplayName()); + verify(mContext, never()).startActivityAsUser(argThat(intent -> + intent.filterEquals(blockedAppIntent)), any(), any()); + } + + @Test public void registerRunningAppsChangedListener_onRunningAppsChanged_listenersNotified() { ArraySet<Integer> uids = new ArraySet<>(Arrays.asList(UID_1, UID_2)); addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); |