Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1bc541e8 authored by Linus Tufvesson's avatar Linus Tufvesson
Browse files

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
parent 544739c6
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -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);
    }

    /**
@@ -6764,6 +6768,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);
+171 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }

}