Block touches from passing through activities
By setting an InputWindowHandle for ActivityRecord it is no longer
possible for Activities to shrink their own window size to allow touches
to pass through to activities behind. The touchable region is cropped by
the parent, meaning that it will occupy all availble space.
Feature is disabled by default and can be enabled per package using adb
shell am compat enable ENABLE_TOUCH_OPAQUE_ACTIVITIES <package>
Test: Manually enabled appcompat feature and verified with sample from
b/194480991 that touches are blocked.
Bug: 194480991
Bug: 196054901
Change-Id: I5cc782953bf9bf855f7e49041a0f784a7aae6934
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index ffc70da..4c29800 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);
}
/**
@@ -6764,6 +6768,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);
+ }
+ }
+
+}