Merge "Block touches from passing through activities"
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 100c44b..fc7cbba 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -830,6 +830,8 @@
     // SystemUi sets the pinned mode on activity after transition is done.
     boolean mWaitForEnteringPinnedMode;
 
+    private final ActivityRecordInputSink mActivityRecordInputSink;
+
     private final Runnable mPauseTimeoutRunnable = new Runnable() {
         @Override
         public void run() {
@@ -1785,6 +1787,8 @@
             createTime = _createTime;
         }
         mAtmService.mPackageConfigPersister.updateConfigIfNeeded(this, mUserId, packageName);
+
+        mActivityRecordInputSink = new ActivityRecordInputSink(this);
     }
 
     /**
@@ -6771,6 +6775,10 @@
             } else if (!show && mLastSurfaceShowing) {
                 getSyncTransaction().hide(mSurfaceControl);
             }
+            if (show) {
+                mActivityRecordInputSink.applyChangesToSurfaceIfChanged(
+                        getSyncTransaction(), mSurfaceControl);
+            }
         }
         if (mThumbnail != null) {
             mThumbnail.setShowing(getPendingTransaction(), show);
diff --git a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
new file mode 100644
index 0000000..b183281
--- /dev/null
+++ b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2021 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 com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
+import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
+import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
+
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.Disabled;
+import android.os.IBinder;
+import android.os.InputConstants;
+import android.os.Looper;
+import android.util.Slog;
+import android.view.InputChannel;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
+import android.view.InputWindowHandle;
+import android.view.MotionEvent;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+/**
+ * Creates a InputWindowHandle that catches all touches that would otherwise pass through an
+ * Activity.
+ */
+class ActivityRecordInputSink {
+
+    /**
+     * Feature flag for making Activities consume all touches within their task bounds.
+     */
+    @ChangeId
+    @Disabled
+    static final long ENABLE_TOUCH_OPAQUE_ACTIVITIES = 194480991L;
+
+    private static final String TAG = "ActivityRecordInputSink";
+    private static final int NUMBER_OF_TOUCHES_TO_DISABLE = 3;
+    private static final long TOAST_COOL_DOWN_MILLIS = 3000L;
+
+    private final ActivityRecord mActivityRecord;
+    private final boolean mIsCompatEnabled;
+
+    // Hold on to InputEventReceiver to prevent it from getting GCd.
+    private InputEventReceiver mInputEventReceiver;
+    private InputWindowHandleWrapper mInputWindowHandleWrapper;
+    private final String mName = Integer.toHexString(System.identityHashCode(this))
+            + " ActivityRecordInputSink";
+    private int mRapidTouchCount = 0;
+    private IBinder mToken;
+    private boolean mDisabled = false;
+
+    ActivityRecordInputSink(ActivityRecord activityRecord) {
+        mActivityRecord = activityRecord;
+        mIsCompatEnabled = CompatChanges.isChangeEnabled(ENABLE_TOUCH_OPAQUE_ACTIVITIES,
+                mActivityRecord.getUid());
+    }
+
+    public void applyChangesToSurfaceIfChanged(
+            SurfaceControl.Transaction transaction, SurfaceControl surfaceControl) {
+        InputWindowHandleWrapper inputWindowHandleWrapper = getInputWindowHandleWrapper();
+        if (inputWindowHandleWrapper.isChanged()) {
+            inputWindowHandleWrapper.applyChangesToSurface(transaction, surfaceControl);
+        }
+    }
+
+    private InputWindowHandleWrapper getInputWindowHandleWrapper() {
+        if (mInputWindowHandleWrapper == null) {
+            mInputWindowHandleWrapper = new InputWindowHandleWrapper(createInputWindowHandle());
+            InputChannel inputChannel =
+                    mActivityRecord.mWmService.mInputManager.createInputChannel(mName);
+            mToken = inputChannel.getToken();
+            mInputEventReceiver = createInputEventReceiver(inputChannel);
+        }
+        if (mDisabled || !mIsCompatEnabled || mActivityRecord.isAnimating(TRANSITION | PARENTS,
+                ANIMATION_TYPE_APP_TRANSITION)) {
+            // TODO(b/208662670): Investigate if we can have feature active during animations.
+            mInputWindowHandleWrapper.setToken(null);
+        } else if (mActivityRecord.mStartingData != null) {
+            // TODO(b/208659130): Remove this special case
+            // Don't block touches during splash screen. This is done to not show toasts for
+            // touches passing through splash screens. b/171772640
+            mInputWindowHandleWrapper.setToken(null);
+        } else {
+            mInputWindowHandleWrapper.setToken(mToken);
+        }
+        return mInputWindowHandleWrapper;
+    }
+
+    private InputWindowHandle createInputWindowHandle() {
+        InputWindowHandle inputWindowHandle = new InputWindowHandle(
+                mActivityRecord.getInputApplicationHandle(false),
+                mActivityRecord.getDisplayId());
+        inputWindowHandle.replaceTouchableRegionWithCrop(
+                mActivityRecord.getParentSurfaceControl());
+        inputWindowHandle.name = mName;
+        inputWindowHandle.ownerUid = mActivityRecord.getUid();
+        inputWindowHandle.ownerPid = mActivityRecord.getPid();
+        inputWindowHandle.layoutParamsFlags =
+                WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                        | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
+        inputWindowHandle.dispatchingTimeoutMillis =
+                InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS;
+        return inputWindowHandle;
+    }
+
+    private InputEventReceiver createInputEventReceiver(InputChannel inputChannel) {
+        return new SinkInputEventReceiver(inputChannel,
+                mActivityRecord.mAtmService.mUiHandler.getLooper());
+    }
+
+    private void showAsToastAndLog(String message) {
+        Toast.makeText(mActivityRecord.mAtmService.mUiContext, message,
+                Toast.LENGTH_LONG).show();
+        Slog.wtf(TAG, message + " " + mActivityRecord.mActivityComponent);
+    }
+
+    private class SinkInputEventReceiver extends InputEventReceiver {
+        private long mLastToast = 0;
+
+        SinkInputEventReceiver(InputChannel inputChannel, Looper looper) {
+            super(inputChannel, looper);
+        }
+
+        public void onInputEvent(InputEvent event) {
+            if (!(event instanceof MotionEvent)) {
+                Slog.wtf(TAG,
+                        "Received InputEvent that was not a MotionEvent");
+                finishInputEvent(event, true);
+                return;
+            }
+            MotionEvent motionEvent = (MotionEvent) event;
+            if (motionEvent.getAction() != MotionEvent.ACTION_DOWN) {
+                finishInputEvent(event, true);
+                return;
+            }
+
+            if (event.getEventTime() - mLastToast > TOAST_COOL_DOWN_MILLIS) {
+                String message = "go/activity-touch-opaque - "
+                        + mActivityRecord.mActivityComponent.getPackageName()
+                        + " blocked the touch!";
+                showAsToastAndLog(message);
+                mLastToast = event.getEventTime();
+                mRapidTouchCount = 1;
+            } else if (++mRapidTouchCount >= NUMBER_OF_TOUCHES_TO_DISABLE && !mDisabled) {
+                // Disable touch blocking until Activity Record is recreated.
+                String message = "Disabled go/activity-touch-opaque - "
+                        + mActivityRecord.mActivityComponent.getPackageName();
+                showAsToastAndLog(message);
+                mDisabled = true;
+            }
+            finishInputEvent(event, true);
+        }
+    }
+
+}