Merge "Allow system windows to disable ActivityRecordInputSink" into main
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index bb335fa..b5f7f23 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -20,6 +20,7 @@
 import static android.Manifest.permission.DETECT_SCREEN_CAPTURE;
 import static android.Manifest.permission.INTERACT_ACROSS_USERS;
 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.inMultiWindowMode;
 import static android.os.Process.myUid;
@@ -9439,6 +9440,15 @@
         ActivityClient.getInstance().enableTaskLocaleOverride(mToken);
     }
 
+    /**
+     * Request ActivityRecordInputSink to enable or disable blocking input events.
+     * @hide
+     */
+    @RequiresPermission(INTERNAL_SYSTEM_WINDOW)
+    public void setActivityRecordInputSinkEnabled(boolean enabled) {
+        ActivityClient.getInstance().setActivityRecordInputSinkEnabled(mToken, enabled);
+    }
+
     class HostCallbacks extends FragmentHostCallback<Activity> {
         public HostCallbacks() {
             super(Activity.this /*activity*/);
diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java
index b35e87b..b8bd030 100644
--- a/core/java/android/app/ActivityClient.java
+++ b/core/java/android/app/ActivityClient.java
@@ -16,6 +16,8 @@
 
 package android.app;
 
+import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
+
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.content.ComponentName;
@@ -614,6 +616,15 @@
         }
     }
 
+    @RequiresPermission(INTERNAL_SYSTEM_WINDOW)
+    void setActivityRecordInputSinkEnabled(IBinder activityToken, boolean enabled) {
+        try {
+            getActivityClientController().setActivityRecordInputSinkEnabled(activityToken, enabled);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
     /**
      * Shows or hides a Camera app compat toggle for stretched issues with the requested state.
      *
diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl
index a3c5e1c..7370fc3 100644
--- a/core/java/android/app/IActivityClientController.aidl
+++ b/core/java/android/app/IActivityClientController.aidl
@@ -191,4 +191,14 @@
      */
     boolean isRequestedToLaunchInTaskFragment(in IBinder activityToken,
             in IBinder taskFragmentToken);
+
+    /**
+     * Enable or disable ActivityRecordInputSink to block input events.
+     *
+     * @param token The token for the activity that requests to toggle.
+     * @param enabled Whether the input evens are blocked by ActivityRecordInputSink.
+     */
+    @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+            + ".permission.INTERNAL_SYSTEM_WINDOW)")
+    oneway void setActivityRecordInputSinkEnabled(in IBinder activityToken, boolean enabled);
 }
diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig
index 11bd22f..0da03fb 100644
--- a/core/java/android/window/flags/window_surfaces.aconfig
+++ b/core/java/android/window/flags/window_surfaces.aconfig
@@ -39,4 +39,12 @@
     description: "Remove uses of ScreenCapture#captureDisplay"
     is_fixed_read_only: true
     bug: "293445881"
-}
\ No newline at end of file
+}
+
+flag {
+    namespace: "window_surfaces"
+    name: "allow_disable_activity_record_input_sink"
+    description: "Whether to allow system activity to disable ActivityRecordInputSink"
+    is_fixed_read_only: true
+    bug: "262477923"
+}
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index faccca8..315e7d8 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -55,6 +55,7 @@
 import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_NONE;
 import static com.android.server.wm.ActivityTaskManagerService.TAG_SWITCH;
 import static com.android.server.wm.ActivityTaskManagerService.enforceNotIsolatedCaller;
+import static com.android.window.flags.Flags.allowDisableActivityRecordInputSink;
 
 import android.Manifest;
 import android.annotation.ColorInt;
@@ -1688,4 +1689,20 @@
             return r.mRequestedLaunchingTaskFragmentToken == taskFragmentToken;
         }
     }
+
+    @Override
+    public void setActivityRecordInputSinkEnabled(IBinder activityToken, boolean enabled) {
+        if (!allowDisableActivityRecordInputSink()) {
+            return;
+        }
+
+        mService.mAmInternal.enforceCallingPermission(
+                Manifest.permission.INTERNAL_SYSTEM_WINDOW, "setActivityRecordInputSinkEnabled");
+        synchronized (mGlobalLock) {
+            final ActivityRecord r = ActivityRecord.forTokenLocked(activityToken);
+            if (r != null) {
+                r.mActivityRecordInputSinkEnabled = enabled;
+            }
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 081759d..d90d4ff 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -970,6 +970,8 @@
     boolean mWaitForEnteringPinnedMode;
 
     final ActivityRecordInputSink mActivityRecordInputSink;
+    // System activities with INTERNAL_SYSTEM_WINDOW can disable ActivityRecordInputSink.
+    boolean mActivityRecordInputSinkEnabled = true;
 
     // Activities with this uid are allowed to not create an input sink while being in the same
     // task and directly above this ActivityRecord. This field is updated whenever a new activity
diff --git a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
index be7d9b6..c61d863 100644
--- a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
+++ b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
@@ -86,7 +86,8 @@
         final boolean allowPassthrough = activityBelowInTask != null && (
                 activityBelowInTask.mAllowedTouchUid == mActivityRecord.getUid()
                         || activityBelowInTask.isUid(mActivityRecord.getUid()));
-        if (allowPassthrough || !mIsCompatEnabled || mActivityRecord.isInTransition()) {
+        if (allowPassthrough || !mIsCompatEnabled || mActivityRecord.isInTransition()
+                || !mActivityRecord.mActivityRecordInputSinkEnabled) {
             mInputWindowHandleWrapper.setInputConfigMasked(InputConfig.NOT_TOUCHABLE,
                     InputConfig.NOT_TOUCHABLE);
         } else {
diff --git a/services/tests/wmtests/Android.bp b/services/tests/wmtests/Android.bp
index 1b8d746..e83f03d 100644
--- a/services/tests/wmtests/Android.bp
+++ b/services/tests/wmtests/Android.bp
@@ -99,4 +99,7 @@
         enabled: false,
     },
 
+    data: [
+        ":OverlayTestApp",
+    ],
 }
diff --git a/services/tests/wmtests/AndroidManifest.xml b/services/tests/wmtests/AndroidManifest.xml
index 762e23c..f2a1fe8 100644
--- a/services/tests/wmtests/AndroidManifest.xml
+++ b/services/tests/wmtests/AndroidManifest.xml
@@ -119,6 +119,9 @@
             </intent-filter>
         </activity>
 
+        <activity android:name="com.android.server.wm.ActivityRecordInputSinkTests$TestActivity"
+                  android:exported="true">
+        </activity>
     </application>
 
     <instrumentation
diff --git a/services/tests/wmtests/AndroidTest.xml b/services/tests/wmtests/AndroidTest.xml
index 2717ef90..f8ebead 100644
--- a/services/tests/wmtests/AndroidTest.xml
+++ b/services/tests/wmtests/AndroidTest.xml
@@ -21,6 +21,7 @@
         <option name="cleanup-apks" value="true" />
         <option name="install-arg" value="-t" />
         <option name="test-file-name" value="WmTests.apk" />
+        <option name="test-file-name" value="OverlayTestApp.apk" />
     </target_preparer>
 
     <option name="test-tag" value="WmTests" />
diff --git a/services/tests/wmtests/OverlayApp/Android.bp b/services/tests/wmtests/OverlayApp/Android.bp
new file mode 100644
index 0000000..77d5b22
--- /dev/null
+++ b/services/tests/wmtests/OverlayApp/Android.bp
@@ -0,0 +1,19 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test_helper_app {
+    name: "OverlayTestApp",
+
+    srcs: ["**/*.java"],
+
+    resource_dirs: ["res"],
+
+    certificate: "platform",
+    platform_apis: true,
+}
diff --git a/services/tests/wmtests/OverlayApp/AndroidManifest.xml b/services/tests/wmtests/OverlayApp/AndroidManifest.xml
new file mode 100644
index 0000000..5b4ef57
--- /dev/null
+++ b/services/tests/wmtests/OverlayApp/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.server.wm.overlay_app">
+    <uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" />
+
+    <application>
+        <activity android:name=".OverlayApp"
+                  android:exported="true"
+                  android:theme="@style/TranslucentFloatingTheme">
+        </activity>
+    </application>
+</manifest>
diff --git a/services/tests/wmtests/OverlayApp/res/values/styles.xml b/services/tests/wmtests/OverlayApp/res/values/styles.xml
new file mode 100644
index 0000000..fff10a3
--- /dev/null
+++ b/services/tests/wmtests/OverlayApp/res/values/styles.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<resources>
+    <style name="TranslucentFloatingTheme" >
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowIsFloating">true</item>
+        <item name="android:windowNoTitle">true</item>
+
+        <!-- Disables starting window. -->
+        <item name="android:windowDisablePreview">true</item>
+    </style>
+</resources>
diff --git a/services/tests/wmtests/OverlayApp/src/com/android/server/wm/overlay_app/OverlayApp.java b/services/tests/wmtests/OverlayApp/src/com/android/server/wm/overlay_app/OverlayApp.java
new file mode 100644
index 0000000..89161c5
--- /dev/null
+++ b/services/tests/wmtests/OverlayApp/src/com/android/server/wm/overlay_app/OverlayApp.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 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.server.wm.overlay_app;
+
+import android.app.Activity;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.view.Gravity;
+import android.view.WindowManager;
+import android.widget.LinearLayout;
+
+/**
+ * Test app that is translucent not touchable modal.
+ * If launched with "disableInputSink" extra boolean value, this activity disables
+ * ActivityRecordInputSinkEnabled as long as the permission is granted.
+ */
+public class OverlayApp extends Activity {
+    private static final String KEY_DISABLE_INPUT_SINK = "disableInputSink";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        LinearLayout tv = new LinearLayout(this);
+        tv.setBackgroundColor(Color.GREEN);
+        tv.setPadding(50, 50, 50, 50);
+        tv.setGravity(Gravity.CENTER);
+        setContentView(tv);
+
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
+        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+
+        if (getIntent().getBooleanExtra(KEY_DISABLE_INPUT_SINK, false)) {
+            setActivityRecordInputSinkEnabled(false);
+        }
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordInputSinkTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordInputSinkTests.java
new file mode 100644
index 0000000..3b280d9
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordInputSinkTests.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2023 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.server.wm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.UiAutomation;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.MotionEvent;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.window.WindowInfosListenerForTest;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.window.flags.Flags;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+/**
+ * Internal variant of {@link android.server.wm.window.ActivityRecordInputSinkTests}.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class ActivityRecordInputSinkTests {
+    private static final String OVERLAY_APP_PKG = "com.android.server.wm.overlay_app";
+    private static final String OVERLAY_ACTIVITY = OVERLAY_APP_PKG + "/.OverlayApp";
+    private static final String KEY_DISABLE_INPUT_SINK = "disableInputSink";
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Rule
+    public final ActivityScenarioRule<TestActivity> mActivityRule =
+            new ActivityScenarioRule<>(TestActivity.class);
+
+    private UiAutomation mUiAutomation;
+
+    @Before
+    public void setUp() {
+        mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+    }
+
+    @After
+    public void tearDown() {
+        ActivityManager am =
+                InstrumentationRegistry.getInstrumentation().getContext().getSystemService(
+                        ActivityManager.class);
+        mUiAutomation.adoptShellPermissionIdentity();
+        try {
+            am.forceStopPackage(OVERLAY_APP_PKG);
+        } finally {
+            mUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    @Test
+    public void testSimpleButtonPress() {
+        injectTapOnButton();
+
+        mActivityRule.getScenario().onActivity(a -> {
+            assertEquals(1, a.mNumClicked);
+        });
+    }
+
+    @Test
+    public void testSimpleButtonPress_withOverlay() throws InterruptedException {
+        startOverlayApp(false);
+        waitForOverlayApp();
+
+        injectTapOnButton();
+
+        mActivityRule.getScenario().onActivity(a -> {
+            assertEquals(0, a.mNumClicked);
+        });
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ALLOW_DISABLE_ACTIVITY_RECORD_INPUT_SINK)
+    public void testSimpleButtonPress_withOverlayDisableInputSink() throws InterruptedException {
+        startOverlayApp(true);
+        waitForOverlayApp();
+
+        injectTapOnButton();
+
+        mActivityRule.getScenario().onActivity(a -> {
+            assertEquals(1, a.mNumClicked);
+        });
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_ALLOW_DISABLE_ACTIVITY_RECORD_INPUT_SINK)
+    public void testSimpleButtonPress_withOverlayDisableInputSink_flagDisabled()
+            throws InterruptedException {
+        startOverlayApp(true);
+        waitForOverlayApp();
+
+        injectTapOnButton();
+
+        mActivityRule.getScenario().onActivity(a -> {
+            assertEquals(0, a.mNumClicked);
+        });
+    }
+
+    private void startOverlayApp(boolean disableInputSink) {
+        String launchCommand = "am start -n " + OVERLAY_ACTIVITY;
+        if (disableInputSink) {
+            launchCommand += " --ez " + KEY_DISABLE_INPUT_SINK + " true";
+        }
+
+        mUiAutomation.adoptShellPermissionIdentity();
+        try {
+            mUiAutomation.executeShellCommand(launchCommand);
+        } finally {
+            mUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    private void waitForOverlayApp() throws InterruptedException {
+        final var listenerHost = new WindowInfosListenerForTest();
+        final var latch = new CountDownLatch(1);
+        final Consumer<List<WindowInfosListenerForTest.WindowInfo>> listener = windowInfos -> {
+            final boolean inputSinkReady = windowInfos.stream().anyMatch(info ->
+                    info.isVisible
+                            && info.name.contains("ActivityRecordInputSink " + OVERLAY_ACTIVITY));
+            if (inputSinkReady) {
+                latch.countDown();
+            }
+        };
+
+        listenerHost.addWindowInfosListener(listener);
+        try {
+            assertTrue(latch.await(5, TimeUnit.SECONDS));
+        } finally {
+            listenerHost.removeWindowInfosListener(listener);
+        }
+    }
+
+    private void injectTapOnButton() {
+        Rect buttonBounds = new Rect();
+        mActivityRule.getScenario().onActivity(a -> {
+            a.mButton.getBoundsOnScreen(buttonBounds);
+        });
+        final int x = buttonBounds.centerX();
+        final int y = buttonBounds.centerY();
+
+        MotionEvent down = MotionEvent.obtain(SystemClock.uptimeMillis(),
+                SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, x, y, 0);
+        mUiAutomation.injectInputEvent(down, true);
+
+        SystemClock.sleep(10);
+
+        MotionEvent up = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),
+                MotionEvent.ACTION_UP, x, y, 0);
+        mUiAutomation.injectInputEvent(up, true);
+
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+    }
+
+    public static class TestActivity extends Activity {
+        int mNumClicked = 0;
+        Button mButton;
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            mButton = new Button(this);
+            mButton.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                    ViewGroup.LayoutParams.MATCH_PARENT));
+            setContentView(mButton);
+            mButton.setOnClickListener(v -> mNumClicked++);
+        }
+    }
+}