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(