Merge "Don't clear mUsingBLASTSyncTransaction until sending callback" into rvc-dev am: 259309f1d3
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/11854634 Change-Id: Ieb7cbb1fd3087ab8b24926027d39d843df6b3ca2
This commit is contained in:
@@ -16,17 +16,19 @@
|
|||||||
|
|
||||||
package com.android.server.wm;
|
package com.android.server.wm;
|
||||||
|
|
||||||
import android.view.SurfaceControl;
|
import android.util.ArrayMap;
|
||||||
|
import android.util.ArraySet;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class for collecting and merging transactions from various sources asynchronously.
|
* Utility class for collecting WindowContainers that will merge transactions.
|
||||||
* For example to use to synchronously resize all the children of a window container
|
* For example to use to synchronously resize all the children of a window container
|
||||||
* 1. Open a new sync set, and pass the listener that will be invoked
|
* 1. Open a new sync set, and pass the listener that will be invoked
|
||||||
* int id startSyncSet(TransactionReadyListener)
|
* int id startSyncSet(TransactionReadyListener)
|
||||||
* the returned ID will be eventually passed to the TransactionReadyListener in combination
|
* the returned ID will be eventually passed to the TransactionReadyListener in combination
|
||||||
* with the prepared transaction. You also use it to refer to the operation in future steps.
|
* with a set of WindowContainers that are ready, meaning onTransactionReady was called for
|
||||||
|
* those WindowContainers. You also use it to refer to the operation in future steps.
|
||||||
* 2. Ask each child to participate:
|
* 2. Ask each child to participate:
|
||||||
* addToSyncSet(int id, WindowContainer wc)
|
* addToSyncSet(int id, WindowContainer wc)
|
||||||
* if the child thinks it will be affected by a configuration change (a.k.a. has a visible
|
* if the child thinks it will be affected by a configuration change (a.k.a. has a visible
|
||||||
@@ -38,35 +40,37 @@ import java.util.HashMap;
|
|||||||
* setReady(int id)
|
* setReady(int id)
|
||||||
* 5. If there were no sub windows anywhere in the hierarchy to wait on, then
|
* 5. If there were no sub windows anywhere in the hierarchy to wait on, then
|
||||||
* transactionReady is immediately invoked, otherwise all the windows are poked
|
* transactionReady is immediately invoked, otherwise all the windows are poked
|
||||||
* to redraw and to deliver a buffer to WMS#finishDrawing.
|
* to redraw and to deliver a buffer to {@link WindowState#finishDrawing}.
|
||||||
* Once all this drawing is complete the combined transaction of all the buffers
|
* Once all this drawing is complete the WindowContainer that's ready will be added to the
|
||||||
* and pending transaction hierarchy changes will be delivered to the TransactionReadyListener
|
* set of ready WindowContainers. When the final onTransactionReady is called, it will merge
|
||||||
|
* the transactions of the all the WindowContainers and will be delivered to the
|
||||||
|
* TransactionReadyListener
|
||||||
*/
|
*/
|
||||||
class BLASTSyncEngine {
|
class BLASTSyncEngine {
|
||||||
private static final String TAG = "BLASTSyncEngine";
|
private static final String TAG = "BLASTSyncEngine";
|
||||||
|
|
||||||
interface TransactionReadyListener {
|
interface TransactionReadyListener {
|
||||||
void onTransactionReady(int mSyncId, SurfaceControl.Transaction mergedTransaction);
|
void onTransactionReady(int mSyncId, Set<WindowContainer> windowContainersReady);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Holds state associated with a single synchronous set of operations.
|
// Holds state associated with a single synchronous set of operations.
|
||||||
class SyncState implements TransactionReadyListener {
|
class SyncState implements TransactionReadyListener {
|
||||||
int mSyncId;
|
int mSyncId;
|
||||||
SurfaceControl.Transaction mMergedTransaction;
|
|
||||||
int mRemainingTransactions;
|
int mRemainingTransactions;
|
||||||
TransactionReadyListener mListener;
|
TransactionReadyListener mListener;
|
||||||
boolean mReady = false;
|
boolean mReady = false;
|
||||||
|
Set<WindowContainer> mWindowContainersReady = new ArraySet<>();
|
||||||
|
|
||||||
private void tryFinish() {
|
private void tryFinish() {
|
||||||
if (mRemainingTransactions == 0 && mReady) {
|
if (mRemainingTransactions == 0 && mReady) {
|
||||||
mListener.onTransactionReady(mSyncId, mMergedTransaction);
|
mListener.onTransactionReady(mSyncId, mWindowContainersReady);
|
||||||
mPendingSyncs.remove(mSyncId);
|
mPendingSyncs.remove(mSyncId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onTransactionReady(int mSyncId, SurfaceControl.Transaction mergedTransaction) {
|
public void onTransactionReady(int mSyncId, Set<WindowContainer> windowContainersReady) {
|
||||||
mRemainingTransactions--;
|
mRemainingTransactions--;
|
||||||
mMergedTransaction.merge(mergedTransaction);
|
mWindowContainersReady.addAll(windowContainersReady);
|
||||||
tryFinish();
|
tryFinish();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,14 +90,13 @@ class BLASTSyncEngine {
|
|||||||
SyncState(TransactionReadyListener l, int id) {
|
SyncState(TransactionReadyListener l, int id) {
|
||||||
mListener = l;
|
mListener = l;
|
||||||
mSyncId = id;
|
mSyncId = id;
|
||||||
mMergedTransaction = new SurfaceControl.Transaction();
|
|
||||||
mRemainingTransactions = 0;
|
mRemainingTransactions = 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
int mNextSyncId = 0;
|
private int mNextSyncId = 0;
|
||||||
|
|
||||||
final HashMap<Integer, SyncState> mPendingSyncs = new HashMap();
|
private final ArrayMap<Integer, SyncState> mPendingSyncs = new ArrayMap<>();
|
||||||
|
|
||||||
BLASTSyncEngine() {
|
BLASTSyncEngine() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ import java.lang.ref.WeakReference;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
@@ -2685,11 +2686,13 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTransactionReady(int mSyncId, SurfaceControl.Transaction mergedTransaction) {
|
public void onTransactionReady(int mSyncId, Set<WindowContainer> windowContainersReady) {
|
||||||
mergedTransaction.merge(mBLASTSyncTransaction);
|
if (mWaitingListener == null) {
|
||||||
mUsingBLASTSyncTransaction = false;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
mWaitingListener.onTransactionReady(mWaitingSyncId, mergedTransaction);
|
windowContainersReady.add(this);
|
||||||
|
mWaitingListener.onTransactionReady(mWaitingSyncId, windowContainersReady);
|
||||||
|
|
||||||
mWaitingListener = null;
|
mWaitingListener = null;
|
||||||
mWaitingSyncId = -1;
|
mWaitingSyncId = -1;
|
||||||
@@ -2740,4 +2743,9 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
|
|||||||
boolean useBLASTSync() {
|
boolean useBLASTSync() {
|
||||||
return mUsingBLASTSyncTransaction;
|
return mUsingBLASTSyncTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void mergeBlastSyncTransaction(Transaction t) {
|
||||||
|
t.merge(mBLASTSyncTransaction);
|
||||||
|
mUsingBLASTSyncTransaction = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import android.window.IWindowOrganizerController;
|
|||||||
import android.window.WindowContainerToken;
|
import android.window.WindowContainerToken;
|
||||||
import android.window.WindowContainerTransaction;
|
import android.window.WindowContainerTransaction;
|
||||||
|
|
||||||
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
import com.android.internal.util.function.pooled.PooledConsumer;
|
import com.android.internal.util.function.pooled.PooledConsumer;
|
||||||
import com.android.internal.util.function.pooled.PooledLambda;
|
import com.android.internal.util.function.pooled.PooledLambda;
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ import java.util.HashMap;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Server side implementation for the interface for organizing windows
|
* Server side implementation for the interface for organizing windows
|
||||||
@@ -142,7 +144,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
|
|||||||
// operations so we don't end up splitting effects between the WM
|
// operations so we don't end up splitting effects between the WM
|
||||||
// pending transaction and the BLASTSync transaction.
|
// pending transaction and the BLASTSync transaction.
|
||||||
if (syncId >= 0) {
|
if (syncId >= 0) {
|
||||||
mBLASTSyncEngine.addToSyncSet(syncId, wc);
|
addToSyncSet(syncId, wc);
|
||||||
}
|
}
|
||||||
|
|
||||||
int containerEffect = applyWindowContainerChange(wc, entry.getValue());
|
int containerEffect = applyWindowContainerChange(wc, entry.getValue());
|
||||||
@@ -164,7 +166,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (syncId >= 0) {
|
if (syncId >= 0) {
|
||||||
mBLASTSyncEngine.addToSyncSet(syncId, wc);
|
addToSyncSet(syncId, wc);
|
||||||
}
|
}
|
||||||
effects |= sanitizeAndApplyHierarchyOp(wc, hop);
|
effects |= sanitizeAndApplyHierarchyOp(wc, hop);
|
||||||
}
|
}
|
||||||
@@ -396,21 +398,33 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
|
|||||||
return mDisplayAreaOrganizerController;
|
return mDisplayAreaOrganizerController;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
int startSyncWithOrganizer(IWindowContainerTransactionCallback callback) {
|
int startSyncWithOrganizer(IWindowContainerTransactionCallback callback) {
|
||||||
int id = mBLASTSyncEngine.startSyncSet(this);
|
int id = mBLASTSyncEngine.startSyncSet(this);
|
||||||
mTransactionCallbacksByPendingSyncId.put(id, callback);
|
mTransactionCallbacksByPendingSyncId.put(id, callback);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
void setSyncReady(int id) {
|
void setSyncReady(int id) {
|
||||||
mBLASTSyncEngine.setReady(id);
|
mBLASTSyncEngine.setReady(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
void addToSyncSet(int syncId, WindowContainer wc) {
|
||||||
|
mBLASTSyncEngine.addToSyncSet(syncId, wc);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTransactionReady(int mSyncId, SurfaceControl.Transaction mergedTransaction) {
|
public void onTransactionReady(int mSyncId, Set<WindowContainer> windowContainersReady) {
|
||||||
final IWindowContainerTransactionCallback callback =
|
final IWindowContainerTransactionCallback callback =
|
||||||
mTransactionCallbacksByPendingSyncId.get(mSyncId);
|
mTransactionCallbacksByPendingSyncId.get(mSyncId);
|
||||||
|
|
||||||
|
SurfaceControl.Transaction mergedTransaction = new SurfaceControl.Transaction();
|
||||||
|
for (WindowContainer container : windowContainersReady) {
|
||||||
|
container.mergeBlastSyncTransaction(mergedTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
callback.onTransactionReady(mSyncId, mergedTransaction);
|
callback.onTransactionReady(mSyncId, mergedTransaction);
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
|
|||||||
@@ -242,8 +242,10 @@ import com.android.server.wm.utils.WmDisplayCutout;
|
|||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
/** A window in the window manager. */
|
/** A window in the window manager. */
|
||||||
@@ -655,6 +657,15 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
|
|||||||
|
|
||||||
private static final StringBuilder sTmpSB = new StringBuilder();
|
private static final StringBuilder sTmpSB = new StringBuilder();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the next surfacePlacement call should notify that the blast sync is ready.
|
||||||
|
* This is set to true when {@link #finishDrawing(Transaction)} is called so
|
||||||
|
* {@link #onTransactionReady(int, Set)} is called after the next surfacePlacement. This allows
|
||||||
|
* Transactions to get flushed into the syncTransaction before notifying {@link BLASTSyncEngine}
|
||||||
|
* that this WindowState is ready.
|
||||||
|
*/
|
||||||
|
private boolean mNotifyBlastOnSurfacePlacement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compares two window sub-layers and returns -1 if the first is lesser than the second in terms
|
* Compares two window sub-layers and returns -1 if the first is lesser than the second in terms
|
||||||
* of z-order and 1 otherwise.
|
* of z-order and 1 otherwise.
|
||||||
@@ -5346,6 +5357,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
|
|||||||
updateFrameRateSelectionPriorityIfNeeded();
|
updateFrameRateSelectionPriorityIfNeeded();
|
||||||
|
|
||||||
mWinAnimator.prepareSurfaceLocked(true);
|
mWinAnimator.prepareSurfaceLocked(true);
|
||||||
|
notifyBlastSyncTransaction();
|
||||||
super.prepareSurfaces();
|
super.prepareSurfaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5837,6 +5849,17 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
|
|||||||
mBLASTSyncTransaction.merge(postDrawTransaction);
|
mBLASTSyncTransaction.merge(postDrawTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mNotifyBlastOnSurfacePlacement = true;
|
||||||
|
return mWinAnimator.finishDrawingLocked(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
void notifyBlastSyncTransaction() {
|
||||||
|
if (!mNotifyBlastOnSurfacePlacement || mWaitingListener == null) {
|
||||||
|
mNotifyBlastOnSurfacePlacement = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// If localSyncId is >0 then we are syncing with children and will
|
// If localSyncId is >0 then we are syncing with children and will
|
||||||
// invoke transaction ready from our own #transactionReady callback
|
// invoke transaction ready from our own #transactionReady callback
|
||||||
// we just need to signal our side of the sync (setReady). But if we
|
// we just need to signal our side of the sync (setReady). But if we
|
||||||
@@ -5844,15 +5867,14 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
|
|||||||
// be invoked and we need to invoke it ourself.
|
// be invoked and we need to invoke it ourself.
|
||||||
if (mLocalSyncId >= 0) {
|
if (mLocalSyncId >= 0) {
|
||||||
mBLASTSyncEngine.setReady(mLocalSyncId);
|
mBLASTSyncEngine.setReady(mLocalSyncId);
|
||||||
return mWinAnimator.finishDrawingLocked(null);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mWaitingListener.onTransactionReady(mWaitingSyncId, mBLASTSyncTransaction);
|
mWaitingListener.onTransactionReady(mWaitingSyncId, Collections.singleton(this));
|
||||||
mUsingBLASTSyncTransaction = false;
|
|
||||||
|
|
||||||
mWaitingSyncId = 0;
|
mWaitingSyncId = 0;
|
||||||
mWaitingListener = null;
|
mWaitingListener = null;
|
||||||
return mWinAnimator.finishDrawingLocked(null);
|
mNotifyBlastOnSurfacePlacement = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean requestResizeForBlastSync() {
|
private boolean requestResizeForBlastSync() {
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import android.util.Rational;
|
|||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
import android.view.SurfaceControl;
|
import android.view.SurfaceControl;
|
||||||
import android.window.ITaskOrganizer;
|
import android.window.ITaskOrganizer;
|
||||||
|
import android.window.IWindowContainerTransactionCallback;
|
||||||
import android.window.WindowContainerTransaction;
|
import android.window.WindowContainerTransaction;
|
||||||
|
|
||||||
import androidx.test.filters.SmallTest;
|
import androidx.test.filters.SmallTest;
|
||||||
@@ -728,7 +729,7 @@ public class WindowOrganizerTests extends WindowTestsBase {
|
|||||||
// We should be rejected from the second sync since we are already
|
// We should be rejected from the second sync since we are already
|
||||||
// in one.
|
// in one.
|
||||||
assertEquals(false, bse.addToSyncSet(id2, task));
|
assertEquals(false, bse.addToSyncSet(id2, task));
|
||||||
w.finishDrawing(null);
|
finishAndNotifyDrawing(w);
|
||||||
assertEquals(true, bse.addToSyncSet(id2, task));
|
assertEquals(true, bse.addToSyncSet(id2, task));
|
||||||
bse.setReady(id2);
|
bse.setReady(id2);
|
||||||
}
|
}
|
||||||
@@ -752,7 +753,7 @@ public class WindowOrganizerTests extends WindowTestsBase {
|
|||||||
// Since we have a window we have to wait for it to draw to finish sync.
|
// Since we have a window we have to wait for it to draw to finish sync.
|
||||||
verify(transactionListener, never())
|
verify(transactionListener, never())
|
||||||
.onTransactionReady(anyInt(), any());
|
.onTransactionReady(anyInt(), any());
|
||||||
w.finishDrawing(null);
|
finishAndNotifyDrawing(w);
|
||||||
verify(transactionListener)
|
verify(transactionListener)
|
||||||
.onTransactionReady(anyInt(), any());
|
.onTransactionReady(anyInt(), any());
|
||||||
}
|
}
|
||||||
@@ -820,14 +821,14 @@ public class WindowOrganizerTests extends WindowTestsBase {
|
|||||||
int id = bse.startSyncSet(transactionListener);
|
int id = bse.startSyncSet(transactionListener);
|
||||||
assertEquals(true, bse.addToSyncSet(id, task));
|
assertEquals(true, bse.addToSyncSet(id, task));
|
||||||
bse.setReady(id);
|
bse.setReady(id);
|
||||||
w.finishDrawing(null);
|
finishAndNotifyDrawing(w);
|
||||||
|
|
||||||
// Since we have a child window we still shouldn't be done.
|
// Since we have a child window we still shouldn't be done.
|
||||||
verify(transactionListener, never())
|
verify(transactionListener, never())
|
||||||
.onTransactionReady(anyInt(), any());
|
.onTransactionReady(anyInt(), any());
|
||||||
reset(transactionListener);
|
reset(transactionListener);
|
||||||
|
|
||||||
child.finishDrawing(null);
|
finishAndNotifyDrawing(child);
|
||||||
// Ah finally! Done
|
// Ah finally! Done
|
||||||
verify(transactionListener)
|
verify(transactionListener)
|
||||||
.onTransactionReady(anyInt(), any());
|
.onTransactionReady(anyInt(), any());
|
||||||
@@ -979,4 +980,42 @@ public class WindowOrganizerTests extends WindowTestsBase {
|
|||||||
new IRequestFinishCallback.Default());
|
new IRequestFinishCallback.Default());
|
||||||
verify(organizer, times(1)).onBackPressedOnTaskRoot(any());
|
verify(organizer, times(1)).onBackPressedOnTaskRoot(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBLASTCallbackWithMultipleWindows() throws Exception {
|
||||||
|
final ActivityStack stackController = createStack();
|
||||||
|
final Task task = createTask(stackController);
|
||||||
|
final ITaskOrganizer organizer = registerMockOrganizer();
|
||||||
|
final WindowState w1 = createAppWindow(task, TYPE_APPLICATION, "Enlightened Window 1");
|
||||||
|
final WindowState w2 = createAppWindow(task, TYPE_APPLICATION, "Enlightened Window 2");
|
||||||
|
makeWindowVisible(w1);
|
||||||
|
makeWindowVisible(w2);
|
||||||
|
|
||||||
|
IWindowContainerTransactionCallback mockCallback =
|
||||||
|
mock(IWindowContainerTransactionCallback.class);
|
||||||
|
int id = mWm.mAtmService.mWindowOrganizerController.startSyncWithOrganizer(mockCallback);
|
||||||
|
|
||||||
|
mWm.mAtmService.mWindowOrganizerController.addToSyncSet(id, task);
|
||||||
|
mWm.mAtmService.mWindowOrganizerController.setSyncReady(id);
|
||||||
|
|
||||||
|
// Since we have a window we have to wait for it to draw to finish sync.
|
||||||
|
verify(mockCallback, never()).onTransactionReady(anyInt(), any());
|
||||||
|
assertTrue(w1.useBLASTSync());
|
||||||
|
assertTrue(w2.useBLASTSync());
|
||||||
|
finishAndNotifyDrawing(w1);
|
||||||
|
|
||||||
|
// Even though one Window finished drawing, both windows should still be using blast sync
|
||||||
|
assertTrue(w1.useBLASTSync());
|
||||||
|
assertTrue(w2.useBLASTSync());
|
||||||
|
|
||||||
|
finishAndNotifyDrawing(w2);
|
||||||
|
verify(mockCallback).onTransactionReady(anyInt(), any());
|
||||||
|
assertFalse(w1.useBLASTSync());
|
||||||
|
assertFalse(w2.useBLASTSync());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishAndNotifyDrawing(WindowState ws) {
|
||||||
|
ws.finishDrawing(null);
|
||||||
|
ws.notifyBlastSyncTransaction();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user