diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 100c44b210b4a..fc7cbbae5d51a 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 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // 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 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A createTime = _createTime; } mAtmService.mPackageConfigPersister.updateConfigIfNeeded(this, mUserId, packageName); + + mActivityRecordInputSink = new ActivityRecordInputSink(this); } /** @@ -6771,6 +6775,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } 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 0000000000000..b183281e0f520 --- /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); + } + } + +}