Loading libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OffscreenTouchZone.java +25 −67 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERL import static com.android.wm.shell.common.split.SplitLayout.RESTING_TOUCH_LAYER; import android.app.TaskInfo; import android.content.Context; import android.content.res.Configuration; import android.graphics.PixelFormat; Loading Loading @@ -57,9 +58,10 @@ public class OffscreenTouchZone { private final boolean mIsTopLeft; /** The function that will be run when this zone is tapped. */ private final Runnable mOnClickRunnable; private SurfaceControlViewHost mViewHost; private SurfaceControl mLeash; private TouchInterceptLayer mInterceptLayer; private GestureDetector mGestureDetector; private final GestureDetector.SimpleOnGestureListener mTapDetector = new GestureDetector.SimpleOnGestureListener() { @Override Loading @@ -68,6 +70,7 @@ public class OffscreenTouchZone { return true; } }; private final View.OnDragListener mDragListener = new View.OnDragListener() { @Override public boolean onDrag(View view, DragEvent dragEvent) { Loading @@ -77,6 +80,14 @@ public class OffscreenTouchZone { return false; } }; private final View.OnTouchListener mTouchListener = new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { return mGestureDetector.onTouchEvent(motionEvent); } }; /** * @param isTopLeft Whether the desired touch zone will be on the top/left or the bottom/right * screen edge. Loading @@ -88,75 +99,22 @@ public class OffscreenTouchZone { } /** Sets up a touch zone. */ public void inflate(Context context, Configuration config, SyncTransactionQueue syncQueue, SurfaceControl stageRoot) { View touchableView = new View(context); mGestureDetector = new GestureDetector(context, mTapDetector); touchableView.setOnTouchListener(new OffscreenTouchListener()); // Set WM flags, tokens, and sizing on the touchable view. It will be the same size as its // parent, the stage root. // TODO (b/349828130): It's a bit wasteful to have the touch zone cover the whole app // surface, even extending offscreen (keeps buffer active in memory), so can trim it down // to the visible onscreen area in a future patch. WindowManager.LayoutParams lp = new WindowManager.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.TYPE_INPUT_CONSUMER, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); lp.token = new Binder(); lp.setTitle(TAG); lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; touchableView.setLayoutParams(lp); touchableView.setOnDragListener(mDragListener); // Create a new leash under our stage leash. final SurfaceControl.Builder builder = new SurfaceControl.Builder() .setContainerLayer() .setName(TAG + (mIsTopLeft ? "TopLeft" : "BottomRight")) .setCallsite("OffscreenTouchZone::init"); builder.setParent(stageRoot); SurfaceControl leash = builder.build(); mLeash = leash; // Create a ViewHost that will hold our view. WindowlessWindowManager wwm = new WindowlessWindowManager(config, leash, null); mViewHost = new SurfaceControlViewHost(context, context.getDisplay(), wwm, "SplitTouchZones"); mViewHost.setView(touchableView, lp); // Create a transaction so that we can activate and reposition our surface. SurfaceControl.Transaction t = new SurfaceControl.Transaction(); // Set layer to maximum. We want this surface to be above the app layer, or else touches // will be blocked. t.setLayer(leash, RESTING_TOUCH_LAYER); // Leash starts off hidden, show it. t.show(leash); syncQueue.runInSync(transaction -> { transaction.merge(t); t.close(); }); public void inflate(Context context, SurfaceControl rootLeash, TaskInfo rootTaskInfo) { mInterceptLayer = new TouchInterceptLayer(TAG + (mIsTopLeft ? "TopLeft" : "BottomRight")); mInterceptLayer.inflate(context, rootLeash, rootTaskInfo); View rootView = mInterceptLayer.getRootView(); mGestureDetector = new GestureDetector(rootView.getContext(), mTapDetector); rootView.setOnTouchListener(mTouchListener); rootView.setOnDragListener(mDragListener); } /** Releases the touch zone when it's no longer needed. */ void release(SurfaceControl.Transaction t) { if (mViewHost != null) { mViewHost.release(); } if (mLeash != null) { t.remove(mLeash); mLeash = null; } } /** * Listens for touch events. */ private class OffscreenTouchListener implements View.OnTouchListener { @Override public boolean onTouch(View view, MotionEvent motionEvent) { return mGestureDetector.onTouchEvent(motionEvent); if (mInterceptLayer != null) { mInterceptLayer.release(); mInterceptLayer = null; } } Loading libs/WindowManager/Shell/src/com/android/wm/shell/common/split/TouchInterceptLayer.kt 0 → 100644 +115 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.wm.shell.common.split import android.app.TaskInfo import android.content.Context import android.graphics.PixelFormat import android.os.Binder import android.view.SurfaceControl import android.view.SurfaceControlViewHost import android.view.View import android.view.WindowManager import android.view.WindowlessWindowManager /** * Manages a touchable surface that is intended to intercept touches. */ class TouchInterceptLayer( val name: String = TAG ) { private var viewHost: SurfaceControlViewHost? = null private var leash: SurfaceControl? = null var rootView: View? = null /** * Creates a touch zone. */ fun inflate(context: Context, rootLeash: SurfaceControl, rootTaskInfo: TaskInfo ) { rootView = View(context.createConfigurationContext(rootTaskInfo.configuration)) // Set WM flags, tokens, and sizing on the touchable view. It will be the same size as its // parent // TODO (b/349828130): It's a bit wasteful to have the touch zone cover the whole app // surface, even extending offscreen (keeps buffer active in memory), so can trim it down // to the visible onscreen area in a future patch. val lp = WindowManager.LayoutParams( rootTaskInfo.configuration.windowConfiguration.bounds.width(), rootTaskInfo.configuration.windowConfiguration.bounds.height(), WindowManager.LayoutParams.TYPE_INPUT_CONSUMER, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT ) lp.token = Binder() lp.setTitle(name) lp.privateFlags = lp.privateFlags or (WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION or WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) rootView?.setLayoutParams(lp) // Create a new leash under our stage leash. val builder = SurfaceControl.Builder() .setContainerLayer() .setName(name) .setCallsite(name + "[TouchInterceptLayer.inflate]") builder.setParent(rootLeash) leash = builder.build() // Create a ViewHost that will hold our view. val wwm = WindowlessWindowManager(rootTaskInfo.configuration, leash, null) viewHost = SurfaceControlViewHost( context, context.display, wwm, name + "[TouchInterceptLayer.inflate]" ) viewHost!!.setView(rootView!!, lp) // Request a transparent region (and mark as will not draw to ensure the full view region) // as an optimization to SurfaceFlinger so we don't need to render the transparent surface rootView?.setWillNotDraw(true) rootView?.parent?.requestTransparentRegion(rootView) // Create a transaction so that we can activate and reposition our surface. val t = SurfaceControl.Transaction() // Set layer to maximum. We want this surface to be above the app layer, or else touches // will be blocked. t.setLayer(leash!!, SplitLayout.RESTING_TOUCH_LAYER) // Leash starts off hidden, show it. t.show(leash) t.apply() } /** * Releases the touch zone when it's no longer needed. */ fun release() { if (viewHost != null) { viewHost!!.release() } if (leash != null) { val t = SurfaceControl.Transaction() t.remove(leash!!) t.apply() leash = null } } companion object { private const val TAG: String = "TouchInterceptLayer" } } libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +12 −18 Original line number Diff line number Diff line Loading @@ -158,6 +158,7 @@ import com.android.wm.shell.common.split.SplitDecorManager; import com.android.wm.shell.common.split.SplitLayout; import com.android.wm.shell.common.split.SplitState; import com.android.wm.shell.common.split.SplitWindowManager; import com.android.wm.shell.common.split.TouchInterceptLayer; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopUserRepositories; import com.android.wm.shell.desktopmode.data.DesktopRepository; Loading Loading @@ -382,10 +383,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // after making SplitLayout display aware. RunningTaskInfo rootTaskInfo = mSplitMultiDisplayHelper.getDisplayRootTaskInfo(DEFAULT_DISPLAY); touchZone.inflate( mContext.createConfigurationContext(rootTaskInfo.configuration), rootTaskInfo.configuration, mSyncQueue, touchZone.isTopLeft() ? topLeftLeash : bottomRightLeash); touchZone.inflate(mContext, touchZone.isTopLeft() ? topLeftLeash : bottomRightLeash, rootTaskInfo); } @Override Loading Loading @@ -1515,16 +1514,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, bottomRightStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; } // Don't allow windows or divider to be focused during animation (mRootTaskInfo is the // parent of all 3 leaves). We don't want the user to be able to tap and focus a window // while it is moving across the screen, because granting focus also recalculates the // layering order, which is in delicate balance during this animation. WindowContainerTransaction noFocus = new WindowContainerTransaction(); // TODO: b/393217881 - replace DEFAULT DISPLAY with the current display id RunningTaskInfo rootTaskInfo = mSplitMultiDisplayHelper.getDisplayRootTaskInfo(DEFAULT_DISPLAY); noFocus.setFocusable(rootTaskInfo.token, false); mSyncQueue.queue(noFocus); final TouchInterceptLayer touchInterceptLayer = new TouchInterceptLayer(); touchInterceptLayer.inflate(mContext, mSplitMultiDisplayHelper.getDisplayRootTaskLeash(DEFAULT_DISPLAY), mSplitMultiDisplayHelper.getDisplayRootTaskInfo(DEFAULT_DISPLAY)); // Remove touch layers, since offscreen apps coming onscreen will not need their touch // layers anymore. populateTouchZones() is called in the end callback to inflate new touch // layers in the appropriate places. Loading @@ -1537,14 +1532,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, SplitDecorManager decorManager1 = topLeftStage.getDecorManager(); SplitDecorManager decorManager2 = bottomRightStage.getDecorManager(); WindowContainerTransaction wct = new WindowContainerTransaction(); // Restore focus-ability to the windows and divider wct.setFocusable(rootTaskInfo.token, true); touchInterceptLayer.release(); if (enableFlexibleSplit()) { mStageOrderOperator.onDoubleTappedDivider(); } WindowContainerTransaction wct = new WindowContainerTransaction(); setSideStagePosition(reverseSplitPosition(mSideStagePosition), wct); mSyncQueue.queue(wct); mSyncQueue.runInSync(st -> { Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OffscreenTouchZone.java +25 −67 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERL import static com.android.wm.shell.common.split.SplitLayout.RESTING_TOUCH_LAYER; import android.app.TaskInfo; import android.content.Context; import android.content.res.Configuration; import android.graphics.PixelFormat; Loading Loading @@ -57,9 +58,10 @@ public class OffscreenTouchZone { private final boolean mIsTopLeft; /** The function that will be run when this zone is tapped. */ private final Runnable mOnClickRunnable; private SurfaceControlViewHost mViewHost; private SurfaceControl mLeash; private TouchInterceptLayer mInterceptLayer; private GestureDetector mGestureDetector; private final GestureDetector.SimpleOnGestureListener mTapDetector = new GestureDetector.SimpleOnGestureListener() { @Override Loading @@ -68,6 +70,7 @@ public class OffscreenTouchZone { return true; } }; private final View.OnDragListener mDragListener = new View.OnDragListener() { @Override public boolean onDrag(View view, DragEvent dragEvent) { Loading @@ -77,6 +80,14 @@ public class OffscreenTouchZone { return false; } }; private final View.OnTouchListener mTouchListener = new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { return mGestureDetector.onTouchEvent(motionEvent); } }; /** * @param isTopLeft Whether the desired touch zone will be on the top/left or the bottom/right * screen edge. Loading @@ -88,75 +99,22 @@ public class OffscreenTouchZone { } /** Sets up a touch zone. */ public void inflate(Context context, Configuration config, SyncTransactionQueue syncQueue, SurfaceControl stageRoot) { View touchableView = new View(context); mGestureDetector = new GestureDetector(context, mTapDetector); touchableView.setOnTouchListener(new OffscreenTouchListener()); // Set WM flags, tokens, and sizing on the touchable view. It will be the same size as its // parent, the stage root. // TODO (b/349828130): It's a bit wasteful to have the touch zone cover the whole app // surface, even extending offscreen (keeps buffer active in memory), so can trim it down // to the visible onscreen area in a future patch. WindowManager.LayoutParams lp = new WindowManager.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.TYPE_INPUT_CONSUMER, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); lp.token = new Binder(); lp.setTitle(TAG); lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; touchableView.setLayoutParams(lp); touchableView.setOnDragListener(mDragListener); // Create a new leash under our stage leash. final SurfaceControl.Builder builder = new SurfaceControl.Builder() .setContainerLayer() .setName(TAG + (mIsTopLeft ? "TopLeft" : "BottomRight")) .setCallsite("OffscreenTouchZone::init"); builder.setParent(stageRoot); SurfaceControl leash = builder.build(); mLeash = leash; // Create a ViewHost that will hold our view. WindowlessWindowManager wwm = new WindowlessWindowManager(config, leash, null); mViewHost = new SurfaceControlViewHost(context, context.getDisplay(), wwm, "SplitTouchZones"); mViewHost.setView(touchableView, lp); // Create a transaction so that we can activate and reposition our surface. SurfaceControl.Transaction t = new SurfaceControl.Transaction(); // Set layer to maximum. We want this surface to be above the app layer, or else touches // will be blocked. t.setLayer(leash, RESTING_TOUCH_LAYER); // Leash starts off hidden, show it. t.show(leash); syncQueue.runInSync(transaction -> { transaction.merge(t); t.close(); }); public void inflate(Context context, SurfaceControl rootLeash, TaskInfo rootTaskInfo) { mInterceptLayer = new TouchInterceptLayer(TAG + (mIsTopLeft ? "TopLeft" : "BottomRight")); mInterceptLayer.inflate(context, rootLeash, rootTaskInfo); View rootView = mInterceptLayer.getRootView(); mGestureDetector = new GestureDetector(rootView.getContext(), mTapDetector); rootView.setOnTouchListener(mTouchListener); rootView.setOnDragListener(mDragListener); } /** Releases the touch zone when it's no longer needed. */ void release(SurfaceControl.Transaction t) { if (mViewHost != null) { mViewHost.release(); } if (mLeash != null) { t.remove(mLeash); mLeash = null; } } /** * Listens for touch events. */ private class OffscreenTouchListener implements View.OnTouchListener { @Override public boolean onTouch(View view, MotionEvent motionEvent) { return mGestureDetector.onTouchEvent(motionEvent); if (mInterceptLayer != null) { mInterceptLayer.release(); mInterceptLayer = null; } } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/common/split/TouchInterceptLayer.kt 0 → 100644 +115 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.wm.shell.common.split import android.app.TaskInfo import android.content.Context import android.graphics.PixelFormat import android.os.Binder import android.view.SurfaceControl import android.view.SurfaceControlViewHost import android.view.View import android.view.WindowManager import android.view.WindowlessWindowManager /** * Manages a touchable surface that is intended to intercept touches. */ class TouchInterceptLayer( val name: String = TAG ) { private var viewHost: SurfaceControlViewHost? = null private var leash: SurfaceControl? = null var rootView: View? = null /** * Creates a touch zone. */ fun inflate(context: Context, rootLeash: SurfaceControl, rootTaskInfo: TaskInfo ) { rootView = View(context.createConfigurationContext(rootTaskInfo.configuration)) // Set WM flags, tokens, and sizing on the touchable view. It will be the same size as its // parent // TODO (b/349828130): It's a bit wasteful to have the touch zone cover the whole app // surface, even extending offscreen (keeps buffer active in memory), so can trim it down // to the visible onscreen area in a future patch. val lp = WindowManager.LayoutParams( rootTaskInfo.configuration.windowConfiguration.bounds.width(), rootTaskInfo.configuration.windowConfiguration.bounds.height(), WindowManager.LayoutParams.TYPE_INPUT_CONSUMER, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT ) lp.token = Binder() lp.setTitle(name) lp.privateFlags = lp.privateFlags or (WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION or WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) rootView?.setLayoutParams(lp) // Create a new leash under our stage leash. val builder = SurfaceControl.Builder() .setContainerLayer() .setName(name) .setCallsite(name + "[TouchInterceptLayer.inflate]") builder.setParent(rootLeash) leash = builder.build() // Create a ViewHost that will hold our view. val wwm = WindowlessWindowManager(rootTaskInfo.configuration, leash, null) viewHost = SurfaceControlViewHost( context, context.display, wwm, name + "[TouchInterceptLayer.inflate]" ) viewHost!!.setView(rootView!!, lp) // Request a transparent region (and mark as will not draw to ensure the full view region) // as an optimization to SurfaceFlinger so we don't need to render the transparent surface rootView?.setWillNotDraw(true) rootView?.parent?.requestTransparentRegion(rootView) // Create a transaction so that we can activate and reposition our surface. val t = SurfaceControl.Transaction() // Set layer to maximum. We want this surface to be above the app layer, or else touches // will be blocked. t.setLayer(leash!!, SplitLayout.RESTING_TOUCH_LAYER) // Leash starts off hidden, show it. t.show(leash) t.apply() } /** * Releases the touch zone when it's no longer needed. */ fun release() { if (viewHost != null) { viewHost!!.release() } if (leash != null) { val t = SurfaceControl.Transaction() t.remove(leash!!) t.apply() leash = null } } companion object { private const val TAG: String = "TouchInterceptLayer" } }
libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +12 −18 Original line number Diff line number Diff line Loading @@ -158,6 +158,7 @@ import com.android.wm.shell.common.split.SplitDecorManager; import com.android.wm.shell.common.split.SplitLayout; import com.android.wm.shell.common.split.SplitState; import com.android.wm.shell.common.split.SplitWindowManager; import com.android.wm.shell.common.split.TouchInterceptLayer; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopUserRepositories; import com.android.wm.shell.desktopmode.data.DesktopRepository; Loading Loading @@ -382,10 +383,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // after making SplitLayout display aware. RunningTaskInfo rootTaskInfo = mSplitMultiDisplayHelper.getDisplayRootTaskInfo(DEFAULT_DISPLAY); touchZone.inflate( mContext.createConfigurationContext(rootTaskInfo.configuration), rootTaskInfo.configuration, mSyncQueue, touchZone.isTopLeft() ? topLeftLeash : bottomRightLeash); touchZone.inflate(mContext, touchZone.isTopLeft() ? topLeftLeash : bottomRightLeash, rootTaskInfo); } @Override Loading Loading @@ -1515,16 +1514,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, bottomRightStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; } // Don't allow windows or divider to be focused during animation (mRootTaskInfo is the // parent of all 3 leaves). We don't want the user to be able to tap and focus a window // while it is moving across the screen, because granting focus also recalculates the // layering order, which is in delicate balance during this animation. WindowContainerTransaction noFocus = new WindowContainerTransaction(); // TODO: b/393217881 - replace DEFAULT DISPLAY with the current display id RunningTaskInfo rootTaskInfo = mSplitMultiDisplayHelper.getDisplayRootTaskInfo(DEFAULT_DISPLAY); noFocus.setFocusable(rootTaskInfo.token, false); mSyncQueue.queue(noFocus); final TouchInterceptLayer touchInterceptLayer = new TouchInterceptLayer(); touchInterceptLayer.inflate(mContext, mSplitMultiDisplayHelper.getDisplayRootTaskLeash(DEFAULT_DISPLAY), mSplitMultiDisplayHelper.getDisplayRootTaskInfo(DEFAULT_DISPLAY)); // Remove touch layers, since offscreen apps coming onscreen will not need their touch // layers anymore. populateTouchZones() is called in the end callback to inflate new touch // layers in the appropriate places. Loading @@ -1537,14 +1532,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, SplitDecorManager decorManager1 = topLeftStage.getDecorManager(); SplitDecorManager decorManager2 = bottomRightStage.getDecorManager(); WindowContainerTransaction wct = new WindowContainerTransaction(); // Restore focus-ability to the windows and divider wct.setFocusable(rootTaskInfo.token, true); touchInterceptLayer.release(); if (enableFlexibleSplit()) { mStageOrderOperator.onDoubleTappedDivider(); } WindowContainerTransaction wct = new WindowContainerTransaction(); setSideStagePosition(reverseSplitPosition(mSideStagePosition), wct); mSyncQueue.queue(wct); mSyncQueue.runInSync(st -> { Loading