Merge "Make canDisplayOnRemoteDevices applicable on all virtual displays." into main
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 19de793..a1ae9da 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -84,6 +84,13 @@
}
flag {
+ namespace: "virtual_devices"
+ name: "enforce_remote_device_opt_out_on_all_virtual_displays"
+ description: "Respect canDisplayOnRemoteDevices on all virtual displays"
+ bug: "338973239"
+}
+
+flag {
namespace: "virtual_devices"
name: "virtual_display_multi_window_mode_support"
description: "Add support for WINDOWING_MODE_MULTI_WINDOW to virtual displays by default"
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 6952a09..481e6b5 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -617,7 +617,7 @@
*/
public static final int FLAG_ENABLE_VR_MODE = 0x8000;
/**
- * Bit in {@link #flags} indicating if the activity can be displayed on a remote device.
+ * Bit in {@link #flags} indicating if the activity can be displayed on a virtual display.
* Corresponds to {@link android.R.attr#canDisplayOnRemoteDevices}
* @hide
*/
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 2e3dbda..0be33c2 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -3296,8 +3296,8 @@
usually TVs.
<p>Requires permission {@code android.permission.DISABLE_SYSTEM_SOUND_EFFECTS}. -->
<attr name="playHomeTransitionSound" format="boolean"/>
- <!-- Indicates whether the activity can be displayed on a remote device which may or
- may not be running Android. -->
+ <!-- Indicates whether the activity can be displayed on a display that may belong to a
+ remote device which may or may not be running Android. -->
<attr name="canDisplayOnRemoteDevices" format="boolean"/>
<attr name="allowUntrustedActivityEmbedding" />
<attr name="knownActivityEmbeddingCerts" />
diff --git a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
index e0d69b0..4ec318b 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
@@ -16,9 +16,12 @@
package com.android.server.wm;
+import static android.content.pm.ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.WindowConfiguration;
+import android.companion.virtualdevice.flags.Flags;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -26,6 +29,7 @@
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Slog;
+import android.view.Display;
import android.window.DisplayWindowPolicyController;
import java.io.PrintWriter;
@@ -80,6 +84,9 @@
if (hasDisplayCategory(activities.get(i))) {
return false;
}
+ if (!launchAllowedByDisplayPolicy(activities.get(i))) {
+ return false;
+ }
}
return true;
}
@@ -95,7 +102,13 @@
if (mDisplayWindowPolicyController == null) {
// Missing controller means that this display has no categories for activity launch
// restriction.
- return !hasDisplayCategory(activityInfo);
+ if (hasDisplayCategory(activityInfo)) {
+ return false;
+ }
+ if (!launchAllowedByDisplayPolicy(activityInfo)) {
+ return false;
+ }
+ return true;
}
return mDisplayWindowPolicyController.canActivityBeLaunched(activityInfo, intent,
windowingMode, launchingFromDisplayId, isNewTask);
@@ -112,6 +125,24 @@
return false;
}
+ private boolean launchAllowedByDisplayPolicy(ActivityInfo aInfo) {
+ if (!Flags.enforceRemoteDeviceOptOutOnAllVirtualDisplays()) {
+ return true;
+ }
+ int displayType = mDisplayContent.getDisplay().getType();
+ if (displayType != Display.TYPE_VIRTUAL && displayType != Display.TYPE_WIFI) {
+ return true;
+ }
+ if ((aInfo.flags & FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES) == 0) {
+ Slog.d(TAG,
+ String.format("Checking activity launch on display %d, activity requires"
+ + " android:canDisplayOnRemoteDevices=true",
+ mDisplayContent.mDisplayId));
+ return false;
+ }
+ return true;
+ }
+
/**
* @see DisplayWindowPolicyController#keepActivityOnWindowFlagsChanged(ActivityInfo, int, int)
*/
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index ff1c6c8..d0080d2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -102,6 +102,7 @@
import android.service.voice.IVoiceInteractionSession;
import android.util.Pair;
import android.util.Size;
+import android.view.Display;
import android.view.Gravity;
import android.view.RemoteAnimationAdapter;
import android.window.TaskFragmentOrganizerToken;
@@ -941,6 +942,91 @@
notNull() /* options */);
}
+
+ /**
+ * This test ensures that activity launch on a secondary display is allowed if the activity did
+ * not opt out from showing on remote devices.
+ */
+ @Test
+ public void testStartActivityOnVirtualDisplay() {
+ final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK,
+ false /* mockGetRootTask */);
+ starter.mRequest.activityInfo.flags |= ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
+
+ // Create a virtual display at bottom.
+ final TestDisplayContent secondaryDisplay =
+ new TestDisplayContent.Builder(mAtm, 1000, 1500)
+ .setType(Display.TYPE_VIRTUAL)
+ .setPosition(POSITION_BOTTOM).build();
+ final TaskDisplayArea secondaryTaskContainer = secondaryDisplay.getDefaultTaskDisplayArea();
+ final Task stack = secondaryTaskContainer.createRootTask(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+
+ // Create an activity record on the top of secondary display.
+ final ActivityRecord topActivityOnSecondaryDisplay = createSingleTaskActivityOn(stack);
+
+ // Put an activity on default display as the top focused activity.
+ new ActivityBuilder(mAtm).setCreateTask(true).build();
+
+ // Start activity with the same intent as {@code topActivityOnSecondaryDisplay}
+ // on secondary display.
+ final ActivityOptions options = ActivityOptions.makeBasic()
+ .setLaunchDisplayId(secondaryDisplay.mDisplayId);
+ final int result = starter.setReason("testStartActivityOnVirtualDisplay")
+ .setIntent(topActivityOnSecondaryDisplay.intent)
+ .setActivityOptions(options.toBundle())
+ .execute();
+
+ // Ensure result is delivering intent to top.
+ assertEquals(START_DELIVERED_TO_TOP, result);
+
+ // Ensure secondary display only creates one stack.
+ verify(secondaryTaskContainer, times(1)).createRootTask(anyInt(), anyInt(), anyBoolean());
+ }
+
+ /**
+ * This test ensures that activity launch on a secondary display is disallowed if the activity
+ * opted out from showing on remote devices.
+ */
+ @EnableFlags(android.companion.virtualdevice.flags.Flags
+ .FLAG_ENFORCE_REMOTE_DEVICE_OPT_OUT_ON_ALL_VIRTUAL_DISPLAYS)
+ @Test
+ public void testStartOptedOutActivityOnVirtualDisplay() {
+ final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK,
+ false /* mockGetRootTask */);
+ starter.mRequest.activityInfo.flags &= ~ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
+
+ // Create a virtual display at bottom.
+ final TestDisplayContent secondaryDisplay =
+ new TestDisplayContent.Builder(mAtm, 1000, 1500)
+ .setType(Display.TYPE_VIRTUAL)
+ .setPosition(POSITION_BOTTOM).build();
+ final TaskDisplayArea secondaryTaskContainer = secondaryDisplay.getDefaultTaskDisplayArea();
+ final Task stack = secondaryTaskContainer.createRootTask(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+
+ // Create an activity record on the top of secondary display.
+ final ActivityRecord topActivityOnSecondaryDisplay = createSingleTaskActivityOn(stack);
+
+ // Put an activity on default display as the top focused activity.
+ new ActivityBuilder(mAtm).setCreateTask(true).build();
+
+ // Start activity with the same intent as {@code topActivityOnSecondaryDisplay}
+ // on secondary display.
+ final ActivityOptions options = ActivityOptions.makeBasic()
+ .setLaunchDisplayId(secondaryDisplay.mDisplayId);
+ final int result = starter.setReason("testStartOptedOutActivityOnVirtualDisplay")
+ .setIntent(topActivityOnSecondaryDisplay.intent)
+ .setActivityOptions(options.toBundle())
+ .execute();
+
+ // Ensure result is canceled.
+ assertEquals(START_CANCELED, result);
+
+ // Ensure secondary display only creates one stack.
+ verify(secondaryTaskContainer, times(1)).createRootTask(anyInt(), anyInt(), anyBoolean());
+ }
+
@Test
public void testWasVisibleInRestartAttempt() {
final ActivityStarter starter = prepareStarter(