Make sure VRI waits for SV to draw before report
VRI is not currently waiting for SV to draw since SV may start its draw before VRI opened the SyncSet. If that happens, VRI will not include SV in its SyncSet and report draw finished even if SV is not complete. This fix has SV create its own SyncSet when redrawNeeded. Then if VRI creates a sync, SV will merge its own SyncSet into that one. If SV already finished drawing, then nothing will get merged, but it means SV is ready already. If SV is not finished, VRI will now also wait for SV's draw to finish before calling finishDraw. Test: Long delay in surfaceRedrawNeededAsync Test: SurfaceSyncerTest Bug: 230998394 Change-Id: I44331b8f54951e6dc633a4845bbf690abde0f95e
This commit is contained in:
@@ -41,11 +41,13 @@ import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.util.ArraySet;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.SurfaceControl.Transaction;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.view.accessibility.IAccessibilityEmbeddedConnection;
|
||||
import android.window.SurfaceSyncer;
|
||||
|
||||
import com.android.internal.view.SurfaceCallbackHelper;
|
||||
|
||||
@@ -204,6 +206,9 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall
|
||||
|
||||
private int mSurfaceFlags = SurfaceControl.HIDDEN;
|
||||
|
||||
private final SurfaceSyncer mSurfaceSyncer = new SurfaceSyncer();
|
||||
private final ArraySet<Integer> mSyncIds = new ArraySet<>();
|
||||
|
||||
/**
|
||||
* Transaction that should be used from the render thread. This transaction is only thread safe
|
||||
* with other calls directly from the render thread.
|
||||
@@ -1018,7 +1023,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall
|
||||
if (shouldSyncBuffer) {
|
||||
handleSyncBufferCallback(callbacks, syncBufferTransactionCallback);
|
||||
} else {
|
||||
redrawNeededAsync(callbacks, this::onDrawFinished);
|
||||
handleSyncNoBuffer(callbacks);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1061,7 +1066,23 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall
|
||||
syncBufferCallback.onBufferReady(t);
|
||||
onDrawFinished();
|
||||
}));
|
||||
}
|
||||
|
||||
private void handleSyncNoBuffer(SurfaceHolder.Callback[] callbacks) {
|
||||
final int syncId = mSurfaceSyncer.setupSync(this::onDrawFinished);
|
||||
|
||||
mSurfaceSyncer.addToSync(syncId, syncBufferCallback -> redrawNeededAsync(callbacks,
|
||||
() -> {
|
||||
syncBufferCallback.onBufferReady(null);
|
||||
synchronized (mSyncIds) {
|
||||
mSyncIds.remove(syncId);
|
||||
}
|
||||
}));
|
||||
|
||||
mSurfaceSyncer.markSyncReady(syncId);
|
||||
synchronized (mSyncIds) {
|
||||
mSyncIds.add(syncId);
|
||||
}
|
||||
}
|
||||
|
||||
private void redrawNeededAsync(SurfaceHolder.Callback[] callbacks,
|
||||
@@ -1070,6 +1091,21 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall
|
||||
sch.dispatchSurfaceRedrawNeededAsync(mSurfaceHolder, callbacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public void surfaceSyncStarted() {
|
||||
ViewRootImpl viewRoot = getViewRootImpl();
|
||||
if (viewRoot != null) {
|
||||
synchronized (mSyncIds) {
|
||||
for (int syncId : mSyncIds) {
|
||||
viewRoot.mergeSync(syncId, mSurfaceSyncer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class SyncBufferTransactionCallback {
|
||||
private final CountDownLatch mCountDownLatch = new CountDownLatch(1);
|
||||
private Transaction mTransaction;
|
||||
|
||||
@@ -2048,6 +2048,7 @@ public final class ViewRootImpl implements ViewParent,
|
||||
void surfaceCreated(Transaction t);
|
||||
void surfaceReplaced(Transaction t);
|
||||
void surfaceDestroyed();
|
||||
default void surfaceSyncStarted() {};
|
||||
}
|
||||
|
||||
private final ArrayList<SurfaceChangedCallback> mSurfaceChangedCallbacks = new ArrayList<>();
|
||||
@@ -2082,6 +2083,12 @@ public final class ViewRootImpl implements ViewParent,
|
||||
}
|
||||
}
|
||||
|
||||
private void notifySurfaceSyncStarted() {
|
||||
for (int i = 0; i < mSurfaceChangedCallbacks.size(); i++) {
|
||||
mSurfaceChangedCallbacks.get(i).surfaceSyncStarted();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return child layer with the same bounds as its parent {@code mSurface} and cropped to the
|
||||
* surface insets. If the layer does not exist, it is created.
|
||||
@@ -3541,6 +3548,7 @@ public final class ViewRootImpl implements ViewParent,
|
||||
Log.d(mTag, "Setup new sync id=" + mSyncId);
|
||||
}
|
||||
mSurfaceSyncer.addToSync(mSyncId, mSyncTarget);
|
||||
notifySurfaceSyncStarted();
|
||||
}
|
||||
|
||||
private void notifyContentCatpureEvents() {
|
||||
@@ -10965,4 +10973,11 @@ public final class ViewRootImpl implements ViewParent,
|
||||
scheduleTraversals();
|
||||
}
|
||||
}
|
||||
|
||||
void mergeSync(int syncId, SurfaceSyncer otherSyncer) {
|
||||
if (!isInLocalSync()) {
|
||||
return;
|
||||
}
|
||||
mSurfaceSyncer.merge(mSyncId, syncId, otherSyncer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,13 +117,18 @@ public class SurfaceSyncer {
|
||||
*/
|
||||
public int setupSync(@NonNull Consumer<Transaction> syncRequestComplete) {
|
||||
synchronized (mSyncSetLock) {
|
||||
mIdCounter++;
|
||||
final int syncId = mIdCounter++;
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "setupSync " + mIdCounter);
|
||||
Log.d(TAG, "setupSync " + syncId);
|
||||
}
|
||||
SyncSet syncSet = new SyncSet(mIdCounter, syncRequestComplete);
|
||||
mSyncSets.put(mIdCounter, syncSet);
|
||||
return mIdCounter;
|
||||
SyncSet syncSet = new SyncSet(syncId, transaction -> {
|
||||
synchronized (mSyncSetLock) {
|
||||
mSyncSets.remove(syncId);
|
||||
}
|
||||
syncRequestComplete.accept(transaction);
|
||||
});
|
||||
mSyncSets.put(syncId, syncSet);
|
||||
return syncId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +143,6 @@ public class SurfaceSyncer {
|
||||
SyncSet syncSet;
|
||||
synchronized (mSyncSetLock) {
|
||||
syncSet = mSyncSets.get(syncId);
|
||||
mSyncSets.remove(syncId);
|
||||
}
|
||||
if (syncSet == null) {
|
||||
Log.e(TAG, "Failed to find syncSet for syncId=" + syncId);
|
||||
@@ -147,6 +151,31 @@ public class SurfaceSyncer {
|
||||
syncSet.markSyncReady();
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge another SyncSet into the specified syncId.
|
||||
* @param syncId The current syncId to merge into
|
||||
* @param otherSyncId The other syncId to be merged
|
||||
* @param otherSurfaceSyncer The other SurfaceSyncer where the otherSyncId is from
|
||||
*/
|
||||
public void merge(int syncId, int otherSyncId, SurfaceSyncer otherSurfaceSyncer) {
|
||||
SyncSet syncSet;
|
||||
synchronized (mSyncSetLock) {
|
||||
syncSet = mSyncSets.get(syncId);
|
||||
}
|
||||
|
||||
SyncSet otherSyncSet = otherSurfaceSyncer.getAndValidateSyncSet(otherSyncId);
|
||||
if (otherSyncSet == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG,
|
||||
"merge id=" + otherSyncId + " from=" + otherSurfaceSyncer + " into id=" + syncId
|
||||
+ " from" + this);
|
||||
}
|
||||
syncSet.merge(otherSyncSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a SurfaceView to a sync set. This is different than {@link #addToSync(int, View)} because
|
||||
* it requires the caller to notify the start and finish drawing in order to sync.
|
||||
@@ -199,8 +228,7 @@ public class SurfaceSyncer {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addToSync id=" + syncId);
|
||||
}
|
||||
syncSet.addSyncableSurface(syncTarget);
|
||||
return true;
|
||||
return syncSet.addSyncableSurface(syncTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,14 +312,21 @@ public class SurfaceSyncer {
|
||||
private final Set<SyncTarget> mSyncTargets = new ArraySet<>();
|
||||
|
||||
private final int mSyncId;
|
||||
private final Consumer<Transaction> mSyncRequestCompleteCallback;
|
||||
@GuardedBy("mLock")
|
||||
private Consumer<Transaction> mSyncRequestCompleteCallback;
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private final Set<SyncSet> mMergedSyncSets = new ArraySet<>();
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private boolean mFinished;
|
||||
|
||||
private SyncSet(int syncId, Consumer<Transaction> syncRequestComplete) {
|
||||
mSyncId = syncId;
|
||||
mSyncRequestCompleteCallback = syncRequestComplete;
|
||||
}
|
||||
|
||||
void addSyncableSurface(SyncTarget syncTarget) {
|
||||
boolean addSyncableSurface(SyncTarget syncTarget) {
|
||||
SyncBufferCallback syncBufferCallback = new SyncBufferCallback() {
|
||||
@Override
|
||||
public void onBufferReady(Transaction t) {
|
||||
@@ -306,10 +341,16 @@ public class SurfaceSyncer {
|
||||
};
|
||||
|
||||
synchronized (mLock) {
|
||||
if (mSyncReady) {
|
||||
Log.e(TAG, "Sync " + mSyncId + " was already marked as ready. No more "
|
||||
+ "SyncTargets can be added.");
|
||||
return false;
|
||||
}
|
||||
mPendingSyncs.add(syncBufferCallback.hashCode());
|
||||
mSyncTargets.add(syncTarget);
|
||||
}
|
||||
syncTarget.onReadyToSync(syncBufferCallback);
|
||||
return true;
|
||||
}
|
||||
|
||||
void markSyncReady() {
|
||||
@@ -321,10 +362,11 @@ public class SurfaceSyncer {
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void checkIfSyncIsComplete() {
|
||||
if (!mSyncReady || !mPendingSyncs.isEmpty()) {
|
||||
if (!mSyncReady || !mPendingSyncs.isEmpty() || !mMergedSyncSets.isEmpty()) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Syncable is not complete. mSyncReady=" + mSyncReady
|
||||
+ " mPendingSyncs=" + mPendingSyncs.size());
|
||||
+ " mPendingSyncs=" + mPendingSyncs.size() + " mergedSyncs="
|
||||
+ mMergedSyncSets.size());
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -338,6 +380,7 @@ public class SurfaceSyncer {
|
||||
}
|
||||
mSyncTargets.clear();
|
||||
mSyncRequestCompleteCallback.accept(mTransaction);
|
||||
mFinished = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -349,6 +392,50 @@ public class SurfaceSyncer {
|
||||
mTransaction.merge(t);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateCallback(Consumer<Transaction> transactionConsumer) {
|
||||
synchronized (mLock) {
|
||||
if (mFinished) {
|
||||
Log.e(TAG, "Attempting to merge SyncSet " + mSyncId + " when sync is"
|
||||
+ " already complete");
|
||||
transactionConsumer.accept(new Transaction());
|
||||
}
|
||||
|
||||
final Consumer<Transaction> oldCallback = mSyncRequestCompleteCallback;
|
||||
mSyncRequestCompleteCallback = transaction -> {
|
||||
oldCallback.accept(new Transaction());
|
||||
transactionConsumer.accept(transaction);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a SyncSet into this SyncSet. Since SyncSets could still have pending SyncTargets,
|
||||
* we need to make sure those can still complete before the mergeTo syncSet is considered
|
||||
* complete.
|
||||
*
|
||||
* We keep track of all the merged SyncSets until they are marked as done, and then they
|
||||
* are removed from the set. This SyncSet is not considered done until all the merged
|
||||
* SyncSets are done.
|
||||
*
|
||||
* When the merged SyncSet is complete, it will invoke the original syncRequestComplete
|
||||
* callback but send an empty transaction to ensure the changes are applied early. This
|
||||
* is needed in case the original sync is relying on the callback to continue processing.
|
||||
*
|
||||
* @param otherSyncSet The other SyncSet to merge into this one.
|
||||
*/
|
||||
public void merge(SyncSet otherSyncSet) {
|
||||
synchronized (mLock) {
|
||||
mMergedSyncSets.add(otherSyncSet);
|
||||
}
|
||||
otherSyncSet.updateCallback(transaction -> {
|
||||
synchronized (mLock) {
|
||||
mMergedSyncSets.remove(otherSyncSet);
|
||||
mTransaction.merge(transaction);
|
||||
checkIfSyncIsComplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,11 +48,11 @@ public class SurfaceSyncerTest {
|
||||
public void testSyncOne() throws InterruptedException {
|
||||
final CountDownLatch finishedLatch = new CountDownLatch(1);
|
||||
int startSyncId = mSurfaceSyncer.setupSync(transaction -> finishedLatch.countDown());
|
||||
Syncable syncable = new Syncable();
|
||||
mSurfaceSyncer.addToSync(startSyncId, syncable);
|
||||
SyncTarget syncTarget = new SyncTarget();
|
||||
mSurfaceSyncer.addToSync(startSyncId, syncTarget);
|
||||
mSurfaceSyncer.markSyncReady(startSyncId);
|
||||
|
||||
syncable.onBufferReady();
|
||||
syncTarget.onBufferReady();
|
||||
|
||||
finishedLatch.await(5, TimeUnit.SECONDS);
|
||||
assertEquals(0, finishedLatch.getCount());
|
||||
@@ -62,22 +62,22 @@ public class SurfaceSyncerTest {
|
||||
public void testSyncMultiple() throws InterruptedException {
|
||||
final CountDownLatch finishedLatch = new CountDownLatch(1);
|
||||
int startSyncId = mSurfaceSyncer.setupSync(transaction -> finishedLatch.countDown());
|
||||
Syncable syncable1 = new Syncable();
|
||||
Syncable syncable2 = new Syncable();
|
||||
Syncable syncable3 = new Syncable();
|
||||
SyncTarget syncTarget1 = new SyncTarget();
|
||||
SyncTarget syncTarget2 = new SyncTarget();
|
||||
SyncTarget syncTarget3 = new SyncTarget();
|
||||
|
||||
mSurfaceSyncer.addToSync(startSyncId, syncable1);
|
||||
mSurfaceSyncer.addToSync(startSyncId, syncable2);
|
||||
mSurfaceSyncer.addToSync(startSyncId, syncable3);
|
||||
mSurfaceSyncer.addToSync(startSyncId, syncTarget1);
|
||||
mSurfaceSyncer.addToSync(startSyncId, syncTarget2);
|
||||
mSurfaceSyncer.addToSync(startSyncId, syncTarget3);
|
||||
mSurfaceSyncer.markSyncReady(startSyncId);
|
||||
|
||||
syncable1.onBufferReady();
|
||||
syncTarget1.onBufferReady();
|
||||
assertNotEquals(0, finishedLatch.getCount());
|
||||
|
||||
syncable3.onBufferReady();
|
||||
syncTarget3.onBufferReady();
|
||||
assertNotEquals(0, finishedLatch.getCount());
|
||||
|
||||
syncable2.onBufferReady();
|
||||
syncTarget2.onBufferReady();
|
||||
|
||||
finishedLatch.await(5, TimeUnit.SECONDS);
|
||||
assertEquals(0, finishedLatch.getCount());
|
||||
@@ -85,7 +85,7 @@ public class SurfaceSyncerTest {
|
||||
|
||||
@Test
|
||||
public void testInvalidSyncId() {
|
||||
assertFalse(mSurfaceSyncer.addToSync(0, new Syncable()));
|
||||
assertFalse(mSurfaceSyncer.addToSync(0, new SyncTarget()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -93,14 +93,14 @@ public class SurfaceSyncerTest {
|
||||
final CountDownLatch finishedLatch = new CountDownLatch(1);
|
||||
int startSyncId = mSurfaceSyncer.setupSync(transaction -> finishedLatch.countDown());
|
||||
|
||||
Syncable syncable1 = new Syncable();
|
||||
Syncable syncable2 = new Syncable();
|
||||
SyncTarget syncTarget1 = new SyncTarget();
|
||||
SyncTarget syncTarget2 = new SyncTarget();
|
||||
|
||||
assertTrue(mSurfaceSyncer.addToSync(startSyncId, syncable1));
|
||||
assertTrue(mSurfaceSyncer.addToSync(startSyncId, syncTarget1));
|
||||
mSurfaceSyncer.markSyncReady(startSyncId);
|
||||
// Adding to a sync that has been completed is also invalid since the sync id has been
|
||||
// cleared.
|
||||
assertFalse(mSurfaceSyncer.addToSync(startSyncId, syncable2));
|
||||
assertFalse(mSurfaceSyncer.addToSync(startSyncId, syncTarget2));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -110,27 +110,89 @@ public class SurfaceSyncerTest {
|
||||
int startSyncId1 = mSurfaceSyncer.setupSync(transaction -> finishedLatch1.countDown());
|
||||
int startSyncId2 = mSurfaceSyncer.setupSync(transaction -> finishedLatch2.countDown());
|
||||
|
||||
Syncable syncable1 = new Syncable();
|
||||
Syncable syncable2 = new Syncable();
|
||||
SyncTarget syncTarget1 = new SyncTarget();
|
||||
SyncTarget syncTarget2 = new SyncTarget();
|
||||
|
||||
assertTrue(mSurfaceSyncer.addToSync(startSyncId1, syncable1));
|
||||
assertTrue(mSurfaceSyncer.addToSync(startSyncId2, syncable2));
|
||||
assertTrue(mSurfaceSyncer.addToSync(startSyncId1, syncTarget1));
|
||||
assertTrue(mSurfaceSyncer.addToSync(startSyncId2, syncTarget2));
|
||||
mSurfaceSyncer.markSyncReady(startSyncId1);
|
||||
mSurfaceSyncer.markSyncReady(startSyncId2);
|
||||
|
||||
syncable1.onBufferReady();
|
||||
syncTarget1.onBufferReady();
|
||||
|
||||
finishedLatch1.await(5, TimeUnit.SECONDS);
|
||||
assertEquals(0, finishedLatch1.getCount());
|
||||
assertNotEquals(0, finishedLatch2.getCount());
|
||||
|
||||
syncable2.onBufferReady();
|
||||
syncTarget2.onBufferReady();
|
||||
|
||||
finishedLatch2.await(5, TimeUnit.SECONDS);
|
||||
assertEquals(0, finishedLatch2.getCount());
|
||||
}
|
||||
|
||||
private static class Syncable implements SurfaceSyncer.SyncTarget {
|
||||
@Test
|
||||
public void testMergeSync() throws InterruptedException {
|
||||
final CountDownLatch finishedLatch1 = new CountDownLatch(1);
|
||||
final CountDownLatch finishedLatch2 = new CountDownLatch(1);
|
||||
int startSyncId1 = mSurfaceSyncer.setupSync(transaction -> finishedLatch1.countDown());
|
||||
int startSyncId2 = mSurfaceSyncer.setupSync(transaction -> finishedLatch2.countDown());
|
||||
|
||||
SyncTarget syncTarget1 = new SyncTarget();
|
||||
SyncTarget syncTarget2 = new SyncTarget();
|
||||
|
||||
assertTrue(mSurfaceSyncer.addToSync(startSyncId1, syncTarget1));
|
||||
assertTrue(mSurfaceSyncer.addToSync(startSyncId2, syncTarget2));
|
||||
mSurfaceSyncer.markSyncReady(startSyncId1);
|
||||
mSurfaceSyncer.merge(startSyncId2, startSyncId1, mSurfaceSyncer);
|
||||
mSurfaceSyncer.markSyncReady(startSyncId2);
|
||||
|
||||
// Finish syncTarget2 first to test that the syncSet is not complete until the merged sync
|
||||
// is also done.
|
||||
syncTarget2.onBufferReady();
|
||||
finishedLatch2.await(1, TimeUnit.SECONDS);
|
||||
// Sync did not complete yet
|
||||
assertNotEquals(0, finishedLatch2.getCount());
|
||||
|
||||
syncTarget1.onBufferReady();
|
||||
|
||||
// The first sync will still get a callback when it's sync requirements are done.
|
||||
finishedLatch1.await(5, TimeUnit.SECONDS);
|
||||
assertEquals(0, finishedLatch1.getCount());
|
||||
|
||||
finishedLatch2.await(5, TimeUnit.SECONDS);
|
||||
assertEquals(0, finishedLatch2.getCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMergeSyncAlreadyComplete() throws InterruptedException {
|
||||
final CountDownLatch finishedLatch1 = new CountDownLatch(1);
|
||||
final CountDownLatch finishedLatch2 = new CountDownLatch(1);
|
||||
int startSyncId1 = mSurfaceSyncer.setupSync(transaction -> finishedLatch1.countDown());
|
||||
int startSyncId2 = mSurfaceSyncer.setupSync(transaction -> finishedLatch2.countDown());
|
||||
|
||||
SyncTarget syncTarget1 = new SyncTarget();
|
||||
SyncTarget syncTarget2 = new SyncTarget();
|
||||
|
||||
assertTrue(mSurfaceSyncer.addToSync(startSyncId1, syncTarget1));
|
||||
assertTrue(mSurfaceSyncer.addToSync(startSyncId2, syncTarget2));
|
||||
mSurfaceSyncer.markSyncReady(startSyncId1);
|
||||
syncTarget1.onBufferReady();
|
||||
|
||||
// The first sync will still get a callback when it's sync requirements are done.
|
||||
finishedLatch1.await(5, TimeUnit.SECONDS);
|
||||
assertEquals(0, finishedLatch1.getCount());
|
||||
|
||||
mSurfaceSyncer.merge(startSyncId2, startSyncId1, mSurfaceSyncer);
|
||||
mSurfaceSyncer.markSyncReady(startSyncId2);
|
||||
syncTarget2.onBufferReady();
|
||||
|
||||
// Verify that the second sync will receive complete since the merged sync was already
|
||||
// completed before the merge.
|
||||
finishedLatch2.await(5, TimeUnit.SECONDS);
|
||||
assertEquals(0, finishedLatch2.getCount());
|
||||
}
|
||||
|
||||
private static class SyncTarget implements SurfaceSyncer.SyncTarget {
|
||||
private SurfaceSyncer.SyncBufferCallback mSyncBufferCallback;
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user