diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index 9b5b8824b0e6..a5ec4e900641 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -209,7 +209,11 @@ private boolean isOpaque(WindowManager.LayoutParams attrs) { /** @hide */ protected SurfaceControl getSurfaceControl(View rootView) { - final State s = mStateForWindow.get(rootView.getViewRootImpl().mWindow.asBinder()); + final ViewRootImpl root = rootView.getViewRootImpl(); + if (root == null) { + return null; + } + final State s = mStateForWindow.get(root.mWindow.asBinder()); if (s == null) { return null; } diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index 9a732455113c..8be37e9e492d 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -264,6 +264,29 @@ public WindowContainerTransaction reorder(@NonNull WindowContainerToken child, b return this; } + /** + * Merges another WCT into this one. + * @param transfer When true, this will transfer everything from other potentially leaving + * other in an unusable state. When false, other is left alone, but + * SurfaceFlinger Transactions will not be merged. + * @hide + */ + public void merge(WindowContainerTransaction other, boolean transfer) { + for (int i = 0, n = other.mChanges.size(); i < n; ++i) { + final IBinder key = other.mChanges.keyAt(i); + Change existing = mChanges.get(key); + if (existing == null) { + existing = new Change(); + mChanges.put(key, existing); + } + existing.merge(other.mChanges.valueAt(i), transfer); + } + for (int i = 0, n = other.mHierarchyOps.size(); i < n; ++i) { + mHierarchyOps.add(transfer ? other.mHierarchyOps.get(i) + : new HierarchyOp(other.mHierarchyOps.get(i))); + } + } + /** @hide */ public Map getChanges() { return mChanges; @@ -359,6 +382,41 @@ protected Change(Parcel in) { mActivityWindowingMode = in.readInt(); } + /** + * @param transfer When true, this will transfer other into this leaving other in an + * undefined state. Use this if you don't intend to use other. When false, + * SurfaceFlinger Transactions will not merge. + */ + public void merge(Change other, boolean transfer) { + mConfiguration.setTo(other.mConfiguration, other.mConfigSetMask, other.mWindowSetMask); + mConfigSetMask |= other.mConfigSetMask; + mWindowSetMask |= other.mWindowSetMask; + if ((other.mChangeMask & CHANGE_FOCUSABLE) != 0) { + mFocusable = other.mFocusable; + } + if (transfer && (other.mChangeMask & CHANGE_BOUNDS_TRANSACTION) != 0) { + mBoundsChangeTransaction = other.mBoundsChangeTransaction; + other.mBoundsChangeTransaction = null; + } + if ((other.mChangeMask & CHANGE_PIP_CALLBACK) != 0) { + mPinnedBounds = transfer ? other.mPinnedBounds : new Rect(other.mPinnedBounds); + } + if ((other.mChangeMask & CHANGE_HIDDEN) != 0) { + mHidden = other.mHidden; + } + mChangeMask |= other.mChangeMask; + if (other.mActivityWindowingMode >= 0) { + mActivityWindowingMode = other.mActivityWindowingMode; + } + if (other.mWindowingMode >= 0) { + mWindowingMode = other.mWindowingMode; + } + if (other.mBoundsChangeSurfaceBounds != null) { + mBoundsChangeSurfaceBounds = transfer ? other.mBoundsChangeSurfaceBounds + : new Rect(other.mBoundsChangeSurfaceBounds); + } + } + public int getWindowingMode() { return mWindowingMode; } @@ -522,6 +580,12 @@ public HierarchyOp(@NonNull IBinder container, boolean toTop) { mToTop = toTop; } + public HierarchyOp(@NonNull HierarchyOp copy) { + mContainer = copy.mContainer; + mReparent = copy.mReparent; + mToTop = copy.mToTop; + } + protected HierarchyOp(Parcel in) { mContainer = in.readStrongBinder(); mReparent = in.readStrongBinder(); diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java b/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java index 95aceed1126b..21810c0e7cf5 100644 --- a/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java +++ b/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java @@ -99,16 +99,19 @@ public class Divider extends SystemUI implements DividerView.DividerCallbacks, private Handler mHandler; private KeyguardStateController mKeyguardStateController; + private WindowManagerProxy mWindowManagerProxy; + private final ArrayList>> mDockedStackExistsListeners = new ArrayList<>(); private SplitScreenTaskOrganizer mSplits = new SplitScreenTaskOrganizer(this); private DisplayChangeController.OnDisplayChangingListener mRotationController = - (display, fromRotation, toRotation, t) -> { - if (!mSplits.isSplitScreenSupported()) { + (display, fromRotation, toRotation, wct) -> { + if (!mSplits.isSplitScreenSupported() || mWindowManagerProxy == null) { return; } + WindowContainerTransaction t = new WindowContainerTransaction(); DisplayLayout displayLayout = new DisplayLayout(mDisplayController.getDisplayLayout(display)); SplitDisplayLayout sdl = new SplitDisplayLayout(mContext, displayLayout, mSplits); @@ -127,6 +130,17 @@ public class Divider extends SystemUI implements DividerView.DividerCallbacks, if (isSplitActive()) { WindowManagerProxy.applyHomeTasksMinimized(sdl, mSplits.mSecondary.token, t); } + if (mWindowManagerProxy.queueSyncTransactionIfWaiting(t)) { + // Because sync transactions are serialized, its possible for an "older" + // bounds-change to get applied after a screen rotation. In that case, we + // want to actually defer on that rather than apply immediately. Of course, + // this means that the bounds may not change until after the rotation so + // the user might see some artifacts. This should be rare. + Slog.w(TAG, "Screen rotated while other operations were pending, this may" + + " result in some graphical artifacts."); + } else { + wct.merge(t, true /* transfer */); + } }; private final DividerImeController mImePositionProcessor; @@ -159,6 +173,7 @@ public Divider(Context context, Optional> recentsOptionalLazy, mRecentsOptionalLazy = recentsOptionalLazy; mForcedResizableController = new ForcedResizableInfoActivityController(context, this); mTransactionPool = transactionPool; + mWindowManagerProxy = new WindowManagerProxy(mTransactionPool, mHandler); mImePositionProcessor = new DividerImeController(mSplits, mTransactionPool, mHandler); } @@ -278,9 +293,9 @@ private void addDivider(Configuration configuration) { LayoutInflater.from(dctx).inflate(R.layout.docked_stack_divider, null); DisplayLayout displayLayout = mDisplayController.getDisplayLayout(mContext.getDisplayId()); mView.injectDependencies(mWindowManager, mDividerState, this, mSplits, mSplitLayout, - mImePositionProcessor); + mImePositionProcessor, mWindowManagerProxy); mView.setVisibility(mVisible ? View.VISIBLE : View.INVISIBLE); - mView.setMinimizedDockStack(mMinimized, mHomeStackResizable); + mView.setMinimizedDockStack(mMinimized, mHomeStackResizable, null /* transaction */); final int size = dctx.getResources().getDimensionPixelSize( com.android.internal.R.dimen.docked_stack_divider_thickness); final boolean landscape = configuration.orientation == ORIENTATION_LANDSCAPE; @@ -303,7 +318,7 @@ private void update(Configuration configuration) { addDivider(configuration); if (mMinimized) { - mView.setMinimizedDockStack(true, mHomeStackResizable); + mView.setMinimizedDockStack(true, mHomeStackResizable, null /* transaction */); updateTouchable(); } mView.setHidden(isDividerHidden); @@ -327,11 +342,13 @@ private void updateVisibility(final boolean visible) { if (visible) { mView.enterSplitMode(mHomeStackResizable); // Update state because animations won't finish. - mView.setMinimizedDockStack(mMinimized, mHomeStackResizable); + mWindowManagerProxy.runInSync( + t -> mView.setMinimizedDockStack(mMinimized, mHomeStackResizable, t)); + } else { mView.exitSplitMode(); - // un-minimize so that next entry triggers minimize anim. - mView.setMinimizedDockStack(false /* minimized */, mHomeStackResizable); + mWindowManagerProxy.runInSync( + t -> mView.setMinimizedDockStack(false, mHomeStackResizable, t)); } // Notify existence listeners synchronized (mDockedStackExistsListeners) { @@ -344,12 +361,6 @@ private void updateVisibility(final boolean visible) { } } - void onSplitDismissed() { - updateVisibility(false /* visible */); - mMinimized = false; - removeDivider(); - } - /** Switch to minimized state if appropriate */ public void setMinimized(final boolean minimized) { if (DEBUG) Slog.d(TAG, "posting ext setMinimized " + minimized + " vis:" + mVisible); @@ -405,7 +416,7 @@ private void setHomeMinimized(final boolean minimized, boolean homeStackResizabl } } updateTouchable(); - WindowOrganizer.applyTransaction(wct); + mWindowManagerProxy.applySyncTransaction(wct); } void setAdjustedForIme(boolean adjustedForIme) { @@ -501,7 +512,14 @@ void startEnterSplit() { update(mDisplayController.getDisplayContext( mContext.getDisplayId()).getResources().getConfiguration()); // Set resizable directly here because applyEnterSplit already resizes home stack. - mHomeStackResizable = WindowManagerProxy.applyEnterSplit(mSplits, mSplitLayout); + mHomeStackResizable = mWindowManagerProxy.applyEnterSplit(mSplits, mSplitLayout); + } + + void startDismissSplit() { + mWindowManagerProxy.applyDismissSplit(mSplits, mSplitLayout, true /* dismissOrMaximize */); + updateVisibility(false /* visible */); + mMinimized = false; + removeDivider(); } void ensureMinimizedSplit() { @@ -530,6 +548,10 @@ SplitDisplayLayout getSplitLayout() { return mSplitLayout; } + WindowManagerProxy getWmProxy() { + return mWindowManagerProxy; + } + /** @return the container token for the secondary split root task. */ public WindowContainerToken getSecondaryRoot() { if (mSplits == null || mSplits.mSecondary == null) { diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerImeController.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerImeController.java index 1e0c07b1cd4c..aea87f2d1971 100644 --- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerImeController.java +++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerImeController.java @@ -29,7 +29,6 @@ import android.window.TaskOrganizer; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; -import android.window.WindowOrganizer; import androidx.annotation.Nullable; @@ -213,7 +212,7 @@ private void updateImeAdjustState() { SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); } - WindowOrganizer.applyTransaction(wct); + mSplits.mDivider.getWmProxy().applySyncTransaction(wct); // Update all the adjusted-for-ime states if (!mPaused) { diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java index e349b5af5b2d..2271f6de194f 100644 --- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java +++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java @@ -46,6 +46,7 @@ import android.view.View; import android.view.View.OnTouchListener; import android.view.ViewConfiguration; +import android.view.ViewRootImpl; import android.view.ViewTreeObserver.InternalInsetsInfo; import android.view.ViewTreeObserver.OnComputeInternalInsetsListener; import android.view.WindowManager; @@ -138,7 +139,7 @@ public interface DividerCallbacks { private final Rect mOtherInsetRect = new Rect(); private final Rect mLastResizeRect = new Rect(); private final Rect mTmpRect = new Rect(); - private final WindowManagerProxy mWindowManagerProxy = WindowManagerProxy.getInstance(); + private WindowManagerProxy mWindowManagerProxy; private DividerWindowManager mWindowManager; private VelocityTracker mVelocityTracker; private FlingAnimationUtils mFlingAnimationUtils; @@ -360,13 +361,14 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto public void injectDependencies(DividerWindowManager windowManager, DividerState dividerState, DividerCallbacks callback, SplitScreenTaskOrganizer tiles, SplitDisplayLayout sdl, - DividerImeController imeController) { + DividerImeController imeController, WindowManagerProxy wmProxy) { mWindowManager = windowManager; mState = dividerState; mCallback = callback; mTiles = tiles; mSplitLayout = sdl; mImeController = imeController; + mWindowManagerProxy = wmProxy; if (mState.mRatioPositionBeforeMinimized == 0) { // Set the middle target as the initial state @@ -376,10 +378,6 @@ public void injectDependencies(DividerWindowManager windowManager, DividerState } } - public WindowManagerProxy getWindowManagerProxy() { - return mWindowManagerProxy; - } - public Rect getNonMinimizedSplitScreenSecondaryBounds() { mOtherTaskRect.set(mSplitLayout.mSecondary); return mOtherTaskRect; @@ -519,7 +517,8 @@ public boolean onTouch(View v, MotionEvent event) { if (mMoving && mDockSide != WindowManager.DOCKED_INVALID) { SnapTarget snapTarget = getSnapAlgorithm().calculateSnapTarget( mStartPosition, 0 /* velocity */, false /* hardDismiss */); - resizeStackSurfaces(calculatePosition(x, y), mStartPosition, snapTarget); + resizeStackSurfaces(calculatePosition(x, y), mStartPosition, snapTarget, + null /* transaction */); } break; case MotionEvent.ACTION_UP: @@ -608,7 +607,7 @@ private ValueAnimator getFlingAnimator(int position, final SnapTarget snapTarget taskPositionSameAtEnd && animation.getAnimatedFraction() == 1f ? TASK_POSITION_SAME : snapTarget.taskPosition, - snapTarget)); + snapTarget, null /* transaction */)); Consumer endAction = cancelled -> { if (DEBUG) Slog.d(TAG, "End Fling " + cancelled + " min:" + mIsInMinimizeInteraction); final boolean wasMinimizeInteraction = mIsInMinimizeInteraction; @@ -716,7 +715,7 @@ private boolean commitSnapFlags(SnapTarget target) { dismissOrMaximize = mDockSide == WindowManager.DOCKED_RIGHT || mDockSide == WindowManager.DOCKED_BOTTOM; } - mWindowManagerProxy.dismissOrMaximizeDocked(mTiles, dismissOrMaximize); + mWindowManagerProxy.dismissOrMaximizeDocked(mTiles, mSplitLayout, dismissOrMaximize); Transaction t = mTiles.getTransaction(); setResizeDimLayer(t, true /* primary */, 0f); setResizeDimLayer(t, false /* primary */, 0f); @@ -806,7 +805,8 @@ private void initializeSurfaceState() { mWindowManager.setTouchRegion(touchRegion); } - public void setMinimizedDockStack(boolean minimized, boolean isHomeStackResizable) { + void setMinimizedDockStack(boolean minimized, boolean isHomeStackResizable, + Transaction t) { mHomeStackResizable = isHomeStackResizable; updateDockSide(); if (!minimized) { @@ -840,9 +840,10 @@ public void setMinimizedDockStack(boolean minimized, boolean isHomeStackResizabl // Relayout to recalculate the divider shadow when minimizing requestLayout(); mIsInMinimizeInteraction = true; - resizeStackSurfaces(mSplitLayout.getMinimizedSnapAlgorithm().getMiddleTarget()); + resizeStackSurfaces( + mSplitLayout.getMinimizedSnapAlgorithm().getMiddleTarget(), t); } else { - resizeStackSurfaces(mSnapTargetBeforeMinimized); + resizeStackSurfaces(mSnapTargetBeforeMinimized, t); mIsInMinimizeInteraction = false; } } @@ -873,10 +874,11 @@ void enterSplitMode(boolean isHomeStackResizable) { * assigned to it. */ private SurfaceControl getWindowSurfaceControl() { - if (getViewRootImpl() == null) { + final ViewRootImpl root = getViewRootImpl(); + if (root == null) { return null; } - SurfaceControl out = getViewRootImpl().getSurfaceControl(); + SurfaceControl out = root.getSurfaceControl(); if (out != null && out.isValid()) { return out; } @@ -885,15 +887,13 @@ private SurfaceControl getWindowSurfaceControl() { void exitSplitMode() { // Reset tile bounds - post(() -> { - final SurfaceControl sc = getWindowSurfaceControl(); - if (sc == null) { - return; - } - Transaction t = mTiles.getTransaction(); - t.hide(sc).apply(); - mTiles.releaseTransaction(t); - }); + final SurfaceControl sc = getWindowSurfaceControl(); + if (sc == null) { + return; + } + Transaction t = mTiles.getTransaction(); + t.hide(sc).apply(); + mTiles.releaseTransaction(t); int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position; WindowManagerProxy.applyResizeSplits(midPos, mSplitLayout); } @@ -1049,8 +1049,8 @@ public void calculateBoundsForPosition(int position, int dockSide, Rect outRect) mDividerSize); } - private void resizeStackSurfaces(SnapTarget taskSnapTarget) { - resizeStackSurfaces(taskSnapTarget.position, taskSnapTarget.position, taskSnapTarget); + private void resizeStackSurfaces(SnapTarget taskSnapTarget, Transaction t) { + resizeStackSurfaces(taskSnapTarget.position, taskSnapTarget.position, taskSnapTarget, t); } void resizeSplitSurfaces(Transaction t, Rect dockedRect, Rect otherRect) { @@ -1105,7 +1105,8 @@ void setResizeDimLayer(Transaction t, boolean primary, float alpha) { } } - void resizeStackSurfaces(int position, int taskPosition, SnapTarget taskSnapTarget) { + void resizeStackSurfaces(int position, int taskPosition, SnapTarget taskSnapTarget, + Transaction transaction) { if (mRemoved) { // This divider view has been removed so shouldn't have any additional influence. return; @@ -1123,7 +1124,8 @@ void resizeStackSurfaces(int position, int taskPosition, SnapTarget taskSnapTarg mBackground.invalidate(); } - Transaction t = mTiles.getTransaction(); + final boolean ownTransaction = transaction == null; + final Transaction t = ownTransaction ? mTiles.getTransaction() : transaction; mLastResizeRect.set(mDockedRect); if (mHomeStackResizable && mIsInMinimizeInteraction) { calculateBoundsForPosition(mSnapTargetBeforeMinimized.position, mDockSide, @@ -1138,8 +1140,10 @@ void resizeStackSurfaces(int position, int taskPosition, SnapTarget taskSnapTarg } resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect); - t.apply(); - mTiles.releaseTransaction(t); + if (ownTransaction) { + t.apply(); + mTiles.releaseTransaction(t); + } return; } @@ -1201,8 +1205,10 @@ void resizeStackSurfaces(int position, int taskPosition, SnapTarget taskSnapTarg SnapTarget closestDismissTarget = getSnapAlgorithm().getClosestDismissTarget(position); float dimFraction = getDimFraction(position, closestDismissTarget); setResizeDimLayer(t, isDismissTargetPrimary(closestDismissTarget), dimFraction); - t.apply(); - mTiles.releaseTransaction(t); + if (ownTransaction) { + t.apply(); + mTiles.releaseTransaction(t); + } } private void applyExitAnimationParallax(Rect taskRect, int position) { @@ -1383,7 +1389,8 @@ void onDockedTopTask() { resizeStackSurfaces(calculatePositionForInsetBounds(), mSplitLayout.getSnapAlgorithm().getMiddleTarget().position, - mSplitLayout.getSnapAlgorithm().getMiddleTarget()); + mSplitLayout.getSnapAlgorithm().getMiddleTarget(), + null /* transaction */); } void onRecentsDrawn() { diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java index c496d2280222..db324822994b 100644 --- a/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java +++ b/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java @@ -200,8 +200,7 @@ private void handleTaskInfoChanged(RunningTaskInfo info) { Log.d(TAG, " was in split, so this means leave it " + mPrimary.topActivityType + " " + mSecondary.topActivityType); } - WindowManagerProxy.applyDismissSplit(this, true /* dismissOrMaximize */); - mDivider.onSplitDismissed(); + mDivider.startDismissSplit(); } else if (!primaryIsEmpty && primaryWasEmpty && secondaryWasEmpty) { // Wasn't in split-mode (both were empty), but now that the primary split is // populated, we should fully enter split by moving everything else into secondary. diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/SyncTransactionQueue.java b/packages/SystemUI/src/com/android/systemui/stackdivider/SyncTransactionQueue.java new file mode 100644 index 000000000000..1ff404677ea6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/stackdivider/SyncTransactionQueue.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2020 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.systemui.stackdivider; + +import android.os.Handler; +import android.util.Slog; +import android.view.SurfaceControl; +import android.window.WindowContainerTransaction; +import android.window.WindowContainerTransactionCallback; +import android.window.WindowOrganizer; + +import androidx.annotation.NonNull; + +import com.android.systemui.TransactionPool; + +import java.util.ArrayList; + +/** + * Helper for serializing sync-transactions and corresponding callbacks. + */ +class SyncTransactionQueue { + private static final boolean DEBUG = Divider.DEBUG; + private static final String TAG = "SyncTransactionQueue"; + + // Just a little longer than the sync-engine timeout of 5s + private static final int REPLY_TIMEOUT = 5300; + + private final TransactionPool mTransactionPool; + private final Handler mHandler; + + // Sync Transactions currently don't support nesting or interleaving properly, so + // queue up transactions to run them serially. + private final ArrayList mQueue = new ArrayList<>(); + + private SyncCallback mInFlight = null; + private final ArrayList mRunnables = new ArrayList<>(); + + private final Runnable mOnReplyTimeout = () -> { + synchronized (mQueue) { + if (mInFlight != null && mQueue.contains(mInFlight)) { + Slog.w(TAG, "Sync Transaction timed-out: " + mInFlight.mWCT); + mInFlight.onTransactionReady(mInFlight.mId, new SurfaceControl.Transaction()); + } + } + }; + + SyncTransactionQueue(TransactionPool pool, Handler handler) { + mTransactionPool = pool; + mHandler = handler; + } + + /** + * Queues a sync transaction to be sent serially to WM. + */ + void queue(WindowContainerTransaction wct) { + SyncCallback cb = new SyncCallback(wct); + synchronized (mQueue) { + if (DEBUG) Slog.d(TAG, "Queueing up " + wct); + mQueue.add(cb); + if (mQueue.size() == 1) { + cb.send(); + } + } + } + + /** + * Queues a sync transaction only if there are already sync transaction(s) queued or in flight. + * Otherwise just returns without queueing. + * @return {@code true} if queued, {@code false} if not. + */ + boolean queueIfWaiting(WindowContainerTransaction wct) { + synchronized (mQueue) { + if (mQueue.isEmpty()) { + if (DEBUG) Slog.d(TAG, "Nothing in queue, so skip queueing up " + wct); + return false; + } + if (DEBUG) Slog.d(TAG, "Queue is non-empty, so queueing up " + wct); + SyncCallback cb = new SyncCallback(wct); + mQueue.add(cb); + if (mQueue.size() == 1) { + cb.send(); + } + } + return true; + } + + /** + * Runs a runnable in sync with sync transactions (ie. when the current in-flight transaction + * returns. If there are no transactions in-flight, runnable executes immediately. + */ + void runInSync(TransactionRunnable runnable) { + synchronized (mQueue) { + if (DEBUG) Slog.d(TAG, "Run in sync. mInFlight=" + mInFlight); + if (mInFlight != null) { + mRunnables.add(runnable); + return; + } + } + SurfaceControl.Transaction t = mTransactionPool.acquire(); + runnable.runWithTransaction(t); + t.apply(); + mTransactionPool.release(t); + } + + // Synchronized on mQueue + private void onTransactionReceived(@NonNull SurfaceControl.Transaction t) { + if (DEBUG) Slog.d(TAG, " Running " + mRunnables.size() + " sync runnables"); + for (int i = 0, n = mRunnables.size(); i < n; ++i) { + mRunnables.get(i).runWithTransaction(t); + } + mRunnables.clear(); + t.apply(); + t.close(); + } + + interface TransactionRunnable { + void runWithTransaction(SurfaceControl.Transaction t); + } + + private class SyncCallback extends WindowContainerTransactionCallback { + int mId = -1; + final WindowContainerTransaction mWCT; + + SyncCallback(WindowContainerTransaction wct) { + mWCT = wct; + } + + // Must be sychronized on mQueue + void send() { + if (mInFlight != null) { + throw new IllegalStateException("Sync Transactions must be serialized. In Flight: " + + mInFlight.mId + " - " + mInFlight.mWCT); + } + mInFlight = this; + if (DEBUG) Slog.d(TAG, "Sending sync transaction: " + mWCT); + mId = new WindowOrganizer().applySyncTransaction(mWCT, this); + if (DEBUG) Slog.d(TAG, " Sent sync transaction. Got id=" + mId); + mHandler.postDelayed(mOnReplyTimeout, REPLY_TIMEOUT); + } + + @Override + public void onTransactionReady(int id, + @androidx.annotation.NonNull SurfaceControl.Transaction t) { + mHandler.post(() -> { + synchronized (mQueue) { + if (mId != id) { + Slog.e(TAG, "Got an unexpected onTransactionReady. Expected " + + mId + " but got " + id); + return; + } + mInFlight = null; + mHandler.removeCallbacks(mOnReplyTimeout); + if (DEBUG) Slog.d(TAG, "onTransactionReady id=" + mId); + mQueue.remove(this); + onTransactionReceived(t); + if (!mQueue.isEmpty()) { + mQueue.get(0).send(); + } + } + }); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java index 3027bd225216..c8361c63e960 100644 --- a/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java +++ b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java @@ -25,9 +25,11 @@ import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.graphics.Rect; +import android.os.Handler; import android.os.RemoteException; import android.util.Log; import android.view.Display; +import android.view.SurfaceControl; import android.view.WindowManagerGlobal; import android.window.TaskOrganizer; import android.window.WindowContainerToken; @@ -35,6 +37,7 @@ import android.window.WindowOrganizer; import com.android.internal.annotations.GuardedBy; +import com.android.systemui.TransactionPool; import java.util.ArrayList; import java.util.List; @@ -49,8 +52,6 @@ public class WindowManagerProxy { private static final String TAG = "WindowManagerProxy"; private static final int[] HOME_AND_RECENTS = {ACTIVITY_TYPE_HOME, ACTIVITY_TYPE_RECENTS}; - private static final WindowManagerProxy sInstance = new WindowManagerProxy(); - @GuardedBy("mDockedRect") private final Rect mDockedRect = new Rect(); @@ -61,6 +62,8 @@ public class WindowManagerProxy { private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + private final SyncTransactionQueue mSyncTransactionQueue; + private final Runnable mSetTouchableRegionRunnable = new Runnable() { @Override public void run() { @@ -76,16 +79,13 @@ public void run() { } }; - private WindowManagerProxy() { + WindowManagerProxy(TransactionPool transactionPool, Handler handler) { + mSyncTransactionQueue = new SyncTransactionQueue(transactionPool, handler); } - public static WindowManagerProxy getInstance() { - return sInstance; - } - - void dismissOrMaximizeDocked( - final SplitScreenTaskOrganizer tiles, final boolean dismissOrMaximize) { - mExecutor.execute(() -> applyDismissSplit(tiles, dismissOrMaximize)); + void dismissOrMaximizeDocked(final SplitScreenTaskOrganizer tiles, SplitDisplayLayout layout, + final boolean dismissOrMaximize) { + mExecutor.execute(() -> applyDismissSplit(tiles, layout, dismissOrMaximize)); } public void setResizing(final boolean resizing) { @@ -163,7 +163,7 @@ static boolean applyHomeTasksMinimized(SplitDisplayLayout layout, WindowContaine * * @return whether the home stack is resizable */ - static boolean applyEnterSplit(SplitScreenTaskOrganizer tiles, SplitDisplayLayout layout) { + boolean applyEnterSplit(SplitScreenTaskOrganizer tiles, SplitDisplayLayout layout) { // Set launchtile first so that any stack created after // getAllStackInfos and before reparent (even if unlikely) are placed // correctly. @@ -174,6 +174,8 @@ static boolean applyEnterSplit(SplitScreenTaskOrganizer tiles, SplitDisplayLayou if (rootTasks.isEmpty()) { return false; } + ActivityManager.RunningTaskInfo topHomeTask = null; + boolean homeIsTop = false; for (int i = rootTasks.size() - 1; i >= 0; --i) { final ActivityManager.RunningTaskInfo rootTask = rootTasks.get(i); // Only move resizeable task to split secondary. WM will just ignore this anyways... @@ -183,16 +185,25 @@ static boolean applyEnterSplit(SplitScreenTaskOrganizer tiles, SplitDisplayLayou != WINDOWING_MODE_FULLSCREEN) { continue; } + // Since this iterates from bottom to top, update topHomeTask for every fullscreen task + // so it will be left with the status of the top one. + topHomeTask = isHomeOrRecentTask(rootTask) ? rootTask : null; wct.reparent(rootTask.token, tiles.mSecondary.token, true /* onTop */); } // Move the secondary split-forward. wct.reorder(tiles.mSecondary.token, true /* onTop */); boolean isHomeResizable = applyHomeTasksMinimized(layout, null /* parent */, wct); - WindowOrganizer.applyTransaction(wct); + if (isHomeResizable && topHomeTask != null) { + // Translate/update-crop of secondary out-of-band with sync transaction -- Until BALST + // is enabled, this temporarily syncs the home surface position with offset until + // sync transaction finishes. + wct.setBoundsChangeTransaction(topHomeTask.token, tiles.mHomeBounds); + } + applySyncTransaction(wct); return isHomeResizable; } - private static boolean isHomeOrRecentTask(ActivityManager.RunningTaskInfo ti) { + static boolean isHomeOrRecentTask(ActivityManager.RunningTaskInfo ti) { final int atype = ti.configuration.windowConfiguration.getActivityType(); return atype == ACTIVITY_TYPE_HOME || atype == ACTIVITY_TYPE_RECENTS; } @@ -203,7 +214,8 @@ private static boolean isHomeOrRecentTask(ActivityManager.RunningTaskInfo ti) { * split (thus resulting in the top of the secondary split becoming * fullscreen. {@code false} resolves the other way. */ - static void applyDismissSplit(SplitScreenTaskOrganizer tiles, boolean dismissOrMaximize) { + void applyDismissSplit(SplitScreenTaskOrganizer tiles, SplitDisplayLayout layout, + boolean dismissOrMaximize) { // Set launch root first so that any task created after getChildContainers and // before reparent (pretty unlikely) are put into fullscreen. TaskOrganizer.setLaunchRoot(Display.DEFAULT_DISPLAY, null); @@ -229,6 +241,7 @@ static void applyDismissSplit(SplitScreenTaskOrganizer tiles, boolean dismissOrM wct.reparent(primaryChildren.get(i).token, null /* parent */, true /* onTop */); } + boolean homeOnTop = false; // Don't need to worry about home tasks because they are already in the "proper" // order within the secondary split. for (int i = secondaryChildren.size() - 1; i >= 0; --i) { @@ -236,8 +249,31 @@ static void applyDismissSplit(SplitScreenTaskOrganizer tiles, boolean dismissOrM wct.reparent(ti.token, null /* parent */, true /* onTop */); if (isHomeOrRecentTask(ti)) { wct.setBounds(ti.token, null); + if (i == 0) { + homeOnTop = true; + } } } + if (homeOnTop) { + // Translate/update-crop of secondary out-of-band with sync transaction -- instead + // play this in sync with new home-app frame because until BALST is enabled this + // shows up on screen before the syncTransaction returns. + // We only have access to the secondary root surface, though, so in order to + // position things properly, we have to take into account the existing negative + // offset/crop of the minimized-home task. + final boolean landscape = layout.mDisplayLayout.isLandscape(); + final int posX = landscape ? layout.mSecondary.left - tiles.mHomeBounds.left + : layout.mSecondary.left; + final int posY = landscape ? layout.mSecondary.top + : layout.mSecondary.top - tiles.mHomeBounds.top; + final SurfaceControl.Transaction sft = new SurfaceControl.Transaction(); + sft.setPosition(tiles.mSecondarySurface, posX, posY); + final Rect crop = new Rect(0, 0, layout.mDisplayLayout.width(), + layout.mDisplayLayout.height()); + crop.offset(-posX, -posY); + sft.setWindowCrop(tiles.mSecondarySurface, crop); + wct.setBoundsChangeTransaction(tiles.mSecondary.token, sft); + } } else { // Maximize, so move non-home secondary split first for (int i = secondaryChildren.size() - 1; i >= 0; --i) { @@ -267,6 +303,29 @@ static void applyDismissSplit(SplitScreenTaskOrganizer tiles, boolean dismissOrM } // Reset focusable to true wct.setFocusable(tiles.mPrimary.token, true /* focusable */); - WindowOrganizer.applyTransaction(wct); + applySyncTransaction(wct); + } + + /** + * Utility to apply a sync transaction serially with other sync transactions. + * + * @see SyncTransactionQueue#queue + */ + void applySyncTransaction(WindowContainerTransaction wct) { + mSyncTransactionQueue.queue(wct); + } + + /** + * @see SyncTransactionQueue#queueIfWaiting + */ + boolean queueSyncTransactionIfWaiting(WindowContainerTransaction wct) { + return mSyncTransactionQueue.queueIfWaiting(wct); + } + + /** + * @see SyncTransactionQueue#runInSync + */ + void runInSync(SyncTransactionQueue.TransactionRunnable runnable) { + mSyncTransactionQueue.runInSync(runnable); } } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 46d6009b9ab8..045089082fd8 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -21,6 +21,7 @@ import static android.app.AppOpsManager.MODE_DEFAULT; import static android.app.AppOpsManager.OP_NONE; import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.isSplitScreenWindowingMode; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.os.PowerManager.DRAW_WAKE_LOCK; @@ -3757,6 +3758,12 @@ private boolean computeDragResizing() { if (!inSplitScreenWindowingMode() && !inFreeformWindowingMode()) { return false; } + // TODO(157912944): formalize drag-resizing so that exceptions aren't hardcoded like this + if (task.getActivityType() == ACTIVITY_TYPE_HOME) { + // The current sys-ui implementations never live-resize home, so to prevent WSA from + // creating/destroying surfaces (which messes up sync-transactions), skip HOME tasks. + return false; + } if (mAttrs.width != MATCH_PARENT || mAttrs.height != MATCH_PARENT) { // Floating windows never enter drag resize mode. return false;