diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index 55300b3fec364..91d5b208c2370 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -1789,4 +1789,11 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall t.apply(); } } + + /** + * @hide + */ + public void syncNextFrame(Transaction t) { + mBlastBufferQueue.setSyncTransaction(t); + } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index feb17f54acd67..8bb7e677362c9 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -195,6 +195,7 @@ import android.view.contentcapture.MainContentCaptureSession; import android.view.inputmethod.InputMethodManager; import android.widget.Scroller; import android.window.ClientWindowFrames; +import android.window.SurfaceSyncer; import android.window.WindowOnBackInvokedDispatcher; import com.android.internal.R; @@ -10873,4 +10874,71 @@ public final class ViewRootImpl implements ViewParent, IWindowSession getWindowSession() { return mWindowSession; } + + private void registerCallbacksForSync( + final SurfaceSyncer.SyncBufferCallback syncBufferCallback) { + if (!isHardwareEnabled()) { + // TODO: correctly handle when hardware disabled + syncBufferCallback.onBufferReady(null); + return; + } + + mAttachInfo.mThreadedRenderer.registerRtFrameCallback(new FrameDrawingCallback() { + @Override + public void onFrameDraw(long frame) { + } + + @Override + public HardwareRenderer.FrameCommitCallback onFrameDraw(int syncResult, long frame) { + if (DEBUG_BLAST) { + Log.d(mTag, + "Received frameDrawingCallback syncResult=" + syncResult + " frameNum=" + + frame + "."); + } + + final Transaction t = new Transaction(); + + // If the syncResults are SYNC_LOST_SURFACE_REWARD_IF_FOUND or + // SYNC_CONTEXT_IS_STOPPED it means nothing will draw. There's no need to set up + // any blast sync or commit callback, and the code should directly call + // pendingDrawFinished. + if ((syncResult + & (SYNC_LOST_SURFACE_REWARD_IF_FOUND | SYNC_CONTEXT_IS_STOPPED)) != 0) { + t.merge(mBlastBufferQueue.gatherPendingTransactions(frame)); + syncBufferCallback.onBufferReady(t); + return null; + } + + mBlastBufferQueue.setSyncTransaction(t); + if (DEBUG_BLAST) { + Log.d(mTag, "Setting up sync and frameCommitCallback"); + } + + return didProduceBuffer -> { + if (DEBUG_BLAST) { + Log.d(mTag, "Received frameCommittedCallback" + + " lastAttemptedDrawFrameNum=" + frame + + " didProduceBuffer=" + didProduceBuffer); + } + + // If frame wasn't drawn, clear out the next transaction so it doesn't affect + // the next draw attempt. The next transaction and transaction complete callback + // were only set for the current draw attempt. + if (!didProduceBuffer) { + mBlastBufferQueue.setSyncTransaction(null); + // Gather the transactions that were sent to mergeWithNextTransaction + // since the frame didn't draw on this vsync. It's possible the frame will + // draw later, but it's better to not be sync than to block on a frame that + // may never come. + t.merge(mBlastBufferQueue.gatherPendingTransactions(frame)); + } + + syncBufferCallback.onBufferReady(t); + }; + } + }); + } + + public final SurfaceSyncer.SyncTarget mSyncTarget = + syncBufferCallback -> registerCallbacksForSync(syncBufferCallback); } diff --git a/core/java/android/window/SurfaceSyncer.java b/core/java/android/window/SurfaceSyncer.java new file mode 100644 index 0000000000000..a5930757cae63 --- /dev/null +++ b/core/java/android/window/SurfaceSyncer.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2022 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 android.window; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UiThread; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.util.SparseArray; +import android.view.SurfaceControl.Transaction; +import android.view.SurfaceView; +import android.view.View; + +import com.android.internal.annotations.GuardedBy; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Used to organize syncs for surfaces. + * + * The SurfaceSyncer allows callers to add desired syncs into a set and wait for them to all + * complete before getting a callback. The purpose of the Syncer is to be an accounting mechanism + * so each sync implementation doesn't need to handle it themselves. The Syncer class is used the + * following way. + * + * 1. {@link #setupSync(Runnable)} is called + * 2. {@link #addToSync(int, SyncTarget)} is called for every SyncTarget object that wants to be + * included in the sync. If the addSync is called for a View or SurfaceView it needs to be called + * on the UI thread. When addToSync is called, it's guaranteed that any UI updates that were + * requested before addToSync but after the last frame drew, will be included in the sync. + * 3. {@link #markSyncReady(int)} should be called when all the {@link SyncTarget}s have been added + * to the SyncSet. Now the SyncSet is closed and no more SyncTargets can be added to it. + * 4. The SyncSet will gather the data for each SyncTarget using the steps described below. When + * all the SyncTargets have finished, the syncRequestComplete will be invoked and the transaction + * will either be applied or sent to the caller. In most cases, only the SurfaceSyncer should be + * handling the Transaction object directly. However, there are some cases where the framework + * needs to send the Transaction elsewhere, like in ViewRootImpl, so that option is provided. + * + * The following is what happens within the {@link SyncSet} + * 1. Each SyncableTarget will get a {@link SyncTarget#onReadyToSync} callback that contains + * a {@link SyncBufferCallback}. + * 2. Each {@link SyncTarget} needs to invoke {@link SyncBufferCallback#onBufferReady(Transaction)}. + * This makes sure the SyncSet knows when the SyncTarget is complete, allowing the SyncSet to get + * the Transaction that contains the buffer. + * 3. When the final SyncBufferCallback finishes for the SyncSet, the syncRequestComplete Consumer + * will be invoked with the transaction that contains all information requested in the sync. This + * could include buffers and geometry changes. The buffer update will include the UI changes that + * were requested for the View. + * + * @hide + */ +public class SurfaceSyncer { + private static final String TAG = "SurfaceSyncer"; + private static final boolean DEBUG = false; + + private static Supplier sTransactionFactory = Transaction::new; + + private final Object mSyncSetLock = new Object(); + @GuardedBy("mSyncSetLock") + private final SparseArray mSyncSets = new SparseArray<>(); + @GuardedBy("mSyncSetLock") + private int mIdCounter = 0; + + /** + * @hide + */ + public static void setTransactionFactory(Supplier transactionFactory) { + sTransactionFactory = transactionFactory; + } + + /** + * Starts a sync and will automatically apply the final, merged transaction. + * + * @param onComplete The runnable that is invoked when the sync has completed. This will run on + * the same thread that the sync was started on. + * @return The syncId for the newly created sync. + * @see #setupSync(Consumer) + */ + public int setupSync(@Nullable Runnable onComplete) { + Handler handler = new Handler(Looper.myLooper()); + return setupSync(transaction -> { + transaction.apply(); + handler.post(onComplete); + }); + } + + /** + * Starts a sync. + * + * @param syncRequestComplete The complete callback that contains the syncId and transaction + * with all the sync data merged. + * @return The syncId for the newly created sync. + * @hide + * @see #setupSync(Runnable) + */ + public int setupSync(@NonNull Consumer syncRequestComplete) { + synchronized (mSyncSetLock) { + mIdCounter++; + if (DEBUG) { + Log.d(TAG, "setupSync " + mIdCounter); + } + SyncSet syncSet = new SyncSet(mIdCounter, syncRequestComplete); + mSyncSets.put(mIdCounter, syncSet); + return mIdCounter; + } + } + + /** + * Mark the sync set as ready to complete. No more data can be added to the specified syncId. + * Once the sync set is marked as ready, it will be able to complete once all Syncables in the + * set have completed their sync + * + * @param syncId The syncId to mark as ready. + */ + public void markSyncReady(int syncId) { + SyncSet syncSet; + synchronized (mSyncSetLock) { + syncSet = mSyncSets.get(syncId); + mSyncSets.remove(syncId); + } + if (syncSet == null) { + Log.e(TAG, "Failed to find syncSet for syncId=" + syncId); + return; + } + syncSet.markSyncReady(); + } + + /** + * 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. + * + * @param syncId The syncId to add an entry to. + * @param surfaceView The SurfaceView to add to the sync. + * @param frameCallbackConsumer The callback that's invoked to allow the caller to notify the + * Syncer when the SurfaceView has started drawing and finished. + * + * @return true if the SurfaceView was successfully added to the SyncSet, false otherwise. + */ + @UiThread + public boolean addToSync(int syncId, SurfaceView surfaceView, + Consumer frameCallbackConsumer) { + return addToSync(syncId, new SurfaceViewSyncTarget(surfaceView, frameCallbackConsumer)); + } + + /** + * Add a View's rootView to a sync set. + * + * @param syncId The syncId to add an entry to. + * @param view The view where the root will be add to the sync set + * + * @return true if the View was successfully added to the SyncSet, false otherwise. + */ + @UiThread + public boolean addToSync(int syncId, @NonNull View view) { + return addToSync(syncId, view.getViewRootImpl().mSyncTarget); + } + + /** + * Add a {@link SyncTarget} to a sync set. The sync set will wait for all + * SyncableSurfaces to complete before notifying. + * + * @param syncId The syncId to add an entry to. + * @param syncTarget A SyncableSurface that implements how to handle syncing + * buffers. + * + * @return true if the SyncTarget was successfully added to the SyncSet, false otherwise. + */ + public boolean addToSync(int syncId, @NonNull SyncTarget syncTarget) { + SyncSet syncSet = getAndValidateSyncSet(syncId); + if (syncSet == null) { + return false; + } + if (DEBUG) { + Log.d(TAG, "addToSync id=" + syncId); + } + syncSet.addSyncableSurface(syncTarget); + return true; + } + + /** + * Add a transaction to a specific sync so it can be merged along with the frames from the + * Syncables in the set. This is so the caller can add arbitrary transaction info that will be + * applied at the same time as the buffers + * @param syncId The syncId where the transaction will be merged to. + * @param t The transaction to merge in the sync set. + */ + public void addTransactionToSync(int syncId, Transaction t) { + SyncSet syncSet = getAndValidateSyncSet(syncId); + if (syncSet != null) { + syncSet.addTransactionToSync(t); + } + } + + private SyncSet getAndValidateSyncSet(int syncId) { + SyncSet syncSet; + synchronized (mSyncSetLock) { + syncSet = mSyncSets.get(syncId); + } + if (syncSet == null) { + Log.e(TAG, "Failed to find sync for id=" + syncId); + return null; + } + return syncSet; + } + + /** + * A SyncTarget that can be added to a sync set. + */ + public interface SyncTarget { + /** + * Called when the Syncable is ready to begin handing a sync request. When invoked, the + * implementor is required to call {@link SyncBufferCallback#onBufferReady(Transaction)} + * and {@link SyncBufferCallback#onBufferReady(Transaction)} in order for this Syncable + * to be marked as complete. + * + * @param syncBufferCallback A SyncBufferCallback that the caller must invoke onBufferReady + */ + void onReadyToSync(SyncBufferCallback syncBufferCallback); + } + + /** + * Interface so the SurfaceSyncer can know when it's safe to start and when everything has been + * completed. The caller should invoke the calls when the rendering has started and finished a + * frame. + */ + public interface SyncBufferCallback { + /** + * Invoked when the transaction contains the buffer and is ready to sync. + * + * @param t The transaction that contains the buffer to be synced. This can be null if + * there's nothing to sync + */ + void onBufferReady(@Nullable Transaction t); + } + + /** + * Class that collects the {@link SyncTarget}s and notifies when all the surfaces have + * a frame ready. + */ + private static class SyncSet { + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private final Set mPendingSyncs = new HashSet<>(); + @GuardedBy("mLock") + private final Transaction mTransaction = sTransactionFactory.get(); + @GuardedBy("mLock") + private boolean mSyncReady; + + private final int mSyncId; + private final Consumer mSyncRequestCompleteCallback; + + private SyncSet(int syncId, Consumer syncRequestComplete) { + mSyncId = syncId; + mSyncRequestCompleteCallback = syncRequestComplete; + } + + void addSyncableSurface(SyncTarget syncTarget) { + SyncBufferCallback syncBufferCallback = new SyncBufferCallback() { + @Override + public void onBufferReady(Transaction t) { + synchronized (mLock) { + if (t != null) { + mTransaction.merge(t); + } + mPendingSyncs.remove(hashCode()); + checkIfSyncIsComplete(); + } + } + }; + + synchronized (mLock) { + mPendingSyncs.add(syncBufferCallback.hashCode()); + } + syncTarget.onReadyToSync(syncBufferCallback); + } + + void markSyncReady() { + synchronized (mLock) { + mSyncReady = true; + checkIfSyncIsComplete(); + } + } + + @GuardedBy("mLock") + private void checkIfSyncIsComplete() { + if (!mSyncReady || !mPendingSyncs.isEmpty()) { + if (DEBUG) { + Log.d(TAG, "Syncable is not complete. mSyncReady=" + mSyncReady + + " mPendingSyncs=" + mPendingSyncs.size()); + } + return; + } + + if (DEBUG) { + Log.d(TAG, "Successfully finished sync id=" + mSyncId); + } + mSyncRequestCompleteCallback.accept(mTransaction); + } + + /** + * Add a Transaction to this sync set. This allows the caller to provide other info that + * should be synced with the buffers. + */ + void addTransactionToSync(Transaction t) { + synchronized (mLock) { + mTransaction.merge(t); + } + } + } + + /** + * Wrapper class to help synchronize SurfaceViews + */ + private static class SurfaceViewSyncTarget implements SyncTarget { + private final SurfaceView mSurfaceView; + private final Consumer mFrameCallbackConsumer; + + SurfaceViewSyncTarget(SurfaceView surfaceView, + Consumer frameCallbackConsumer) { + mSurfaceView = surfaceView; + mFrameCallbackConsumer = frameCallbackConsumer; + } + + @Override + public void onReadyToSync(SyncBufferCallback syncBufferCallback) { + Transaction mTransaction = sTransactionFactory.get(); + mFrameCallbackConsumer.accept(new SurfaceViewFrameCallback() { + @Override + public void onFrameStarted() { + mSurfaceView.syncNextFrame(mTransaction); + } + + @Override + public void onFrameComplete() { + syncBufferCallback.onBufferReady(mTransaction); + } + }); + } + } + + /** + * A frame callback that is used to synchronize SurfaceViews. The owner of the SurfaceView must + * implement onFrameStarted and onFrameComplete when trying to sync the SurfaceView. This is to + * ensure the sync knows when the frame is ready to add to the sync. + */ + public interface SurfaceViewFrameCallback { + /** + * Called when the SurfaceView is going to render a frame + */ + void onFrameStarted(); + + /** + * Called when the SurfaceView has finished rendering a frame. + */ + void onFrameComplete(); + } +} diff --git a/services/tests/wmtests/Android.bp b/services/tests/wmtests/Android.bp index 90dac47e353c1..57bbe40c5b065 100644 --- a/services/tests/wmtests/Android.bp +++ b/services/tests/wmtests/Android.bp @@ -57,6 +57,7 @@ android_test { "ub-uiautomator", "hamcrest-library", "platform-compat-test-rules", + "CtsSurfaceValidatorLib", ], libs: [ diff --git a/services/tests/wmtests/AndroidManifest.xml b/services/tests/wmtests/AndroidManifest.xml index c2298d0f3d206..2918365b94c87 100644 --- a/services/tests/wmtests/AndroidManifest.xml +++ b/services/tests/wmtests/AndroidManifest.xml @@ -43,6 +43,8 @@ + + + + + + mActivityRule = + new ActivityTestRule<>(CapturedActivity.class); + + public CapturedActivity mCapturedActivity; + + @Before + public void setup() { + mCapturedActivity = mActivityRule.getActivity(); + mCapturedActivity.setLogicalDisplaySize(getLogicalDisplaySize()); + + final KeyguardManager km = mCapturedActivity.getSystemService(KeyguardManager.class); + if (km != null && km.isKeyguardLocked() || !Objects.requireNonNull( + mCapturedActivity.getSystemService(PowerManager.class)).isInteractive()) { + pressWakeupButton(); + pressUnlockButton(); + } + } + + @Test + public void testSurfaceViewSyncDuringResize() throws Throwable { + SurfaceSyncer.setTransactionFactory(SurfaceControl.Transaction::new); + mCapturedActivity.verifyTest(new SurfaceSyncerValidatorTestCase(), mName); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncerTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncerTest.java new file mode 100644 index 0000000000000..cc28ea6f55d2f --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncerTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import android.platform.test.annotations.Presubmit; +import android.view.SurfaceControl; +import android.window.SurfaceSyncer; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@SmallTest +@Presubmit +public class SurfaceSyncerTest { + private SurfaceSyncer mSurfaceSyncer; + + @Before + public void setup() { + mSurfaceSyncer = new SurfaceSyncer(); + SurfaceSyncer.setTransactionFactory(StubTransaction::new); + } + + @Test + 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); + mSurfaceSyncer.markSyncReady(startSyncId); + + syncable.onBufferReady(); + + finishedLatch.await(5, TimeUnit.SECONDS); + assertEquals(0, finishedLatch.getCount()); + } + + @Test + 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(); + + mSurfaceSyncer.addToSync(startSyncId, syncable1); + mSurfaceSyncer.addToSync(startSyncId, syncable2); + mSurfaceSyncer.addToSync(startSyncId, syncable3); + mSurfaceSyncer.markSyncReady(startSyncId); + + syncable1.onBufferReady(); + assertNotEquals(0, finishedLatch.getCount()); + + syncable3.onBufferReady(); + assertNotEquals(0, finishedLatch.getCount()); + + syncable2.onBufferReady(); + + finishedLatch.await(5, TimeUnit.SECONDS); + assertEquals(0, finishedLatch.getCount()); + } + + @Test + public void testInvalidSyncId() { + assertFalse(mSurfaceSyncer.addToSync(0, new Syncable())); + } + + @Test + public void testAddSyncWhenSyncComplete() throws InterruptedException { + final CountDownLatch finishedLatch = new CountDownLatch(1); + int startSyncId = mSurfaceSyncer.setupSync(transaction -> finishedLatch.countDown()); + + Syncable syncable1 = new Syncable(); + Syncable syncable2 = new Syncable(); + + assertTrue(mSurfaceSyncer.addToSync(startSyncId, syncable1)); + 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)); + } + + @Test + public void testMultipleSyncSets() 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()); + + Syncable syncable1 = new Syncable(); + Syncable syncable2 = new Syncable(); + + assertTrue(mSurfaceSyncer.addToSync(startSyncId1, syncable1)); + assertTrue(mSurfaceSyncer.addToSync(startSyncId2, syncable2)); + mSurfaceSyncer.markSyncReady(startSyncId1); + mSurfaceSyncer.markSyncReady(startSyncId2); + + syncable1.onBufferReady(); + + finishedLatch1.await(5, TimeUnit.SECONDS); + assertEquals(0, finishedLatch1.getCount()); + assertNotEquals(0, finishedLatch2.getCount()); + + syncable2.onBufferReady(); + + finishedLatch2.await(5, TimeUnit.SECONDS); + assertEquals(0, finishedLatch2.getCount()); + } + + private static class Syncable implements SurfaceSyncer.SyncTarget { + private SurfaceSyncer.SyncBufferCallback mSyncBufferCallback; + + @Override + public void onReadyToSync(SurfaceSyncer.SyncBufferCallback syncBufferCallback) { + mSyncBufferCallback = syncBufferCallback; + } + + void onBufferReady() { + SurfaceControl.Transaction t = new StubTransaction(); + mSyncBufferCallback.onBufferReady(t); + } + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncerValidatorTestCase.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncerValidatorTestCase.java new file mode 100644 index 0000000000000..77a8615e20d68 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncerValidatorTestCase.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.ViewGroup; +import android.view.cts.surfacevalidator.ISurfaceValidatorTestCase; +import android.view.cts.surfacevalidator.PixelChecker; +import android.widget.FrameLayout; +import android.window.SurfaceSyncer; + +import androidx.annotation.NonNull; + +/** + * A validator class that will create a SurfaceView and then update its size over and over. The code + * will request to sync the SurfaceView content with the main window and validate that there was + * never an empty area (black color). The test uses {@link SurfaceSyncer} class to gather the + * content it wants to synchronize. + */ +public class SurfaceSyncerValidatorTestCase implements ISurfaceValidatorTestCase { + private static final String TAG = "SurfaceSyncerValidatorTestCase"; + + private final Runnable mRunnable = new Runnable() { + @Override + public void run() { + updateSurfaceViewSize(); + mHandler.postDelayed(this, 100); + } + }; + + private Handler mHandler; + private SurfaceView mSurfaceView; + private boolean mLastExpanded = true; + private final SurfaceSyncer mSurfaceSyncer = new SurfaceSyncer(); + + private RenderingThread mRenderingThread; + private FrameLayout mParent; + + private int mLastSyncId = -1; + + final SurfaceHolder.Callback mCallback = new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(@NonNull SurfaceHolder holder) { + final Canvas canvas = holder.lockCanvas(); + canvas.drawARGB(255, 100, 100, 100); + holder.unlockCanvasAndPost(canvas); + Log.d(TAG, "surfaceCreated"); + mRenderingThread = new RenderingThread(holder); + mRenderingThread.start(); + } + + @Override + public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, + int height) { + if (mLastSyncId >= 0) { + mSurfaceSyncer.addToSync(mLastSyncId, mSurfaceView, frameCallback -> + mRenderingThread.setFrameCallback(frameCallback)); + mSurfaceSyncer.markSyncReady(mLastSyncId); + mLastSyncId = -1; + } + } + + @Override + public void surfaceDestroyed(@NonNull SurfaceHolder holder) { + mRenderingThread.stopRendering(); + } + }; + + @Override + public PixelChecker getChecker() { + return new PixelChecker(Color.BLACK) { + @Override + public boolean checkPixels(int matchingPixelCount, int width, int height) { + return matchingPixelCount == 0; + } + }; + } + + @Override + public void start(Context context, FrameLayout parent) { + mSurfaceView = new SurfaceView(context); + mSurfaceView.getHolder().addCallback(mCallback); + mParent = parent; + + FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(MATCH_PARENT, 600); + parent.addView(mSurfaceView, layoutParams); + mHandler = new Handler(Looper.getMainLooper()); + mHandler.post(mRunnable); + } + + @Override + public void end() { + mHandler.removeCallbacks(mRunnable); + } + + public void updateSurfaceViewSize() { + if (mRenderingThread == null || mLastSyncId >= 0 || !mRenderingThread.isReadyToSync()) { + return; + } + + Log.d(TAG, "updateSurfaceViewSize"); + + final int height; + if (mLastExpanded) { + height = 300; + } else { + height = 600; + } + mLastExpanded = !mLastExpanded; + + mRenderingThread.pauseRendering(); + mLastSyncId = mSurfaceSyncer.setupSync(() -> { }); + mSurfaceSyncer.addToSync(mLastSyncId, mParent); + + ViewGroup.LayoutParams svParams = mSurfaceView.getLayoutParams(); + svParams.height = height; + mSurfaceView.setLayoutParams(svParams); + } + + private static class RenderingThread extends HandlerThread { + private final SurfaceHolder mSurfaceHolder; + private SurfaceSyncer.SurfaceViewFrameCallback mFrameCallback; + private boolean mPauseRendering; + private boolean mComplete; + + int mColorValue = 0; + int mColorDelta = 10; + + @Override + public void run() { + try { + while (true) { + sleep(10); + synchronized (this) { + if (mComplete) { + break; + } + if (mPauseRendering) { + continue; + } + + if (mFrameCallback != null) { + Log.d(TAG, "onFrameStarted"); + mFrameCallback.onFrameStarted(); + } + + mColorValue += mColorDelta; + if (mColorValue > 245 || mColorValue < 10) { + mColorDelta *= -1; + } + + Canvas c = mSurfaceHolder.lockCanvas(); + if (c != null) { + c.drawRGB(255, mColorValue, 255 - mColorValue); + mSurfaceHolder.unlockCanvasAndPost(c); + } + + if (mFrameCallback != null) { + Log.d(TAG, "onFrameComplete"); + mFrameCallback.onFrameComplete(); + } + + mFrameCallback = null; + } + } + } catch (InterruptedException e) { + } + } + + RenderingThread(SurfaceHolder holder) { + super("RenderingThread"); + mSurfaceHolder = holder; + } + + public void pauseRendering() { + synchronized (this) { + mPauseRendering = true; + } + } + + private boolean isReadyToSync() { + synchronized (this) { + return mFrameCallback == null; + } + } + public void setFrameCallback(SurfaceSyncer.SurfaceViewFrameCallback frameCallback) { + synchronized (this) { + mFrameCallback = frameCallback; + mPauseRendering = false; + } + } + + public void stopRendering() { + synchronized (this) { + mComplete = true; + } + } + } +} diff --git a/tests/SurfaceViewSyncTest/Android.bp b/tests/SurfaceViewSyncTest/Android.bp new file mode 100644 index 0000000000000..1c6e380aad634 --- /dev/null +++ b/tests/SurfaceViewSyncTest/Android.bp @@ -0,0 +1,31 @@ +// +// Copyright (C) 2022 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "SurfaceViewSyncTest", + srcs: ["**/*.java"], + platform_apis: true, + certificate: "platform", +} diff --git a/tests/SurfaceViewSyncTest/AndroidManifest.xml b/tests/SurfaceViewSyncTest/AndroidManifest.xml new file mode 100644 index 0000000000000..d085f8c168d31 --- /dev/null +++ b/tests/SurfaceViewSyncTest/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + diff --git a/tests/SurfaceViewSyncTest/OWNERS b/tests/SurfaceViewSyncTest/OWNERS new file mode 100644 index 0000000000000..0862c05e0ee41 --- /dev/null +++ b/tests/SurfaceViewSyncTest/OWNERS @@ -0,0 +1 @@ +include /services/core/java/com/android/server/wm/OWNERS diff --git a/tests/SurfaceViewSyncTest/res/layout/activity_surfaceview_sync.xml b/tests/SurfaceViewSyncTest/res/layout/activity_surfaceview_sync.xml new file mode 100644 index 0000000000000..4433b21483846 --- /dev/null +++ b/tests/SurfaceViewSyncTest/res/layout/activity_surfaceview_sync.xml @@ -0,0 +1,47 @@ + + + + + + + + +