Merge "Add SurfaceSyncer class" into tm-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
747b5bfad7
@@ -1789,4 +1789,11 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall
|
||||
t.apply();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
public void syncNextFrame(Transaction t) {
|
||||
mBlastBufferQueue.setSyncTransaction(t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
377
core/java/android/window/SurfaceSyncer.java
Normal file
377
core/java/android/window/SurfaceSyncer.java
Normal file
@@ -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<Transaction> sTransactionFactory = Transaction::new;
|
||||
|
||||
private final Object mSyncSetLock = new Object();
|
||||
@GuardedBy("mSyncSetLock")
|
||||
private final SparseArray<SyncSet> mSyncSets = new SparseArray<>();
|
||||
@GuardedBy("mSyncSetLock")
|
||||
private int mIdCounter = 0;
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
public static void setTransactionFactory(Supplier<Transaction> 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<Transaction> 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<SurfaceViewFrameCallback> 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<Integer> mPendingSyncs = new HashSet<>();
|
||||
@GuardedBy("mLock")
|
||||
private final Transaction mTransaction = sTransactionFactory.get();
|
||||
@GuardedBy("mLock")
|
||||
private boolean mSyncReady;
|
||||
|
||||
private final int mSyncId;
|
||||
private final Consumer<Transaction> mSyncRequestCompleteCallback;
|
||||
|
||||
private SyncSet(int syncId, Consumer<Transaction> 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<SurfaceViewFrameCallback> mFrameCallbackConsumer;
|
||||
|
||||
SurfaceViewSyncTarget(SurfaceView surfaceView,
|
||||
Consumer<SurfaceViewFrameCallback> 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();
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ android_test {
|
||||
"ub-uiautomator",
|
||||
"hamcrest-library",
|
||||
"platform-compat-test-rules",
|
||||
"CtsSurfaceValidatorLib",
|
||||
],
|
||||
|
||||
libs: [
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
<uses-permission android:name="android.permission.LOG_COMPAT_CHANGE" />
|
||||
<uses-permission android:name="android.permission.CAPTURE_BLACKOUT_CONTENT"/>
|
||||
<uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- TODO: Remove largeHeap hack when memory leak is fixed (b/123984854) -->
|
||||
<application android:debuggable="true"
|
||||
@@ -78,6 +80,12 @@
|
||||
android:turnScreenOn="true"
|
||||
android:showWhenLocked="true" />
|
||||
<activity android:name="com.android.server.wm.ScreenshotTests$ScreenshotActivity" />
|
||||
<activity android:name="android.view.cts.surfacevalidator.CapturedActivity"/>
|
||||
|
||||
<service android:name="android.view.cts.surfacevalidator.LocalMediaProjectionService"
|
||||
android:foregroundServiceType="mediaProjection"
|
||||
android:enabled="true">
|
||||
</service>
|
||||
</application>
|
||||
|
||||
<instrumentation
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.server.wm.UiDeviceUtils.pressUnlockButton;
|
||||
import static android.server.wm.UiDeviceUtils.pressWakeupButton;
|
||||
import static android.server.wm.WindowManagerState.getLogicalDisplaySize;
|
||||
|
||||
import android.app.KeyguardManager;
|
||||
import android.os.PowerManager;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.cts.surfacevalidator.CapturedActivity;
|
||||
import android.window.SurfaceSyncer;
|
||||
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TestName;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class SurfaceSyncerContinuousTest {
|
||||
@Rule
|
||||
public TestName mName = new TestName();
|
||||
|
||||
@Rule
|
||||
public ActivityTestRule<CapturedActivity> 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
tests/SurfaceViewSyncTest/Android.bp
Normal file
31
tests/SurfaceViewSyncTest/Android.bp
Normal file
@@ -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",
|
||||
}
|
||||
27
tests/SurfaceViewSyncTest/AndroidManifest.xml
Normal file
27
tests/SurfaceViewSyncTest/AndroidManifest.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.android.test">
|
||||
<application>
|
||||
<activity android:name="SurfaceViewSyncActivity"
|
||||
android:label="SurfaceView Sync Test"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
1
tests/SurfaceViewSyncTest/OWNERS
Normal file
1
tests/SurfaceViewSyncTest/OWNERS
Normal file
@@ -0,0 +1 @@
|
||||
include /services/core/java/com/android/server/wm/OWNERS
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@android:color/darker_gray"
|
||||
tools:context="com.example.mysurfaceview.MainActivity">
|
||||
|
||||
<SurfaceView
|
||||
android:id="@+id/surface_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="600dp" />
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<Button
|
||||
android:text="COLLAPSE SV"
|
||||
android:id="@+id/expand_sv"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
<Switch
|
||||
android:id="@+id/enable_sync_switch"
|
||||
android:text="Enable Sync"
|
||||
android:checked="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* 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.test;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.app.Activity;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.WindowMetrics;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Switch;
|
||||
import android.window.SurfaceSyncer;
|
||||
|
||||
/**
|
||||
* Test app that allows the user to resize the SurfaceView and have the new buffer sync with the
|
||||
* main window. This tests that {@link SurfaceSyncer} is working correctly.
|
||||
*/
|
||||
public class SurfaceViewSyncActivity extends Activity implements SurfaceHolder.Callback {
|
||||
private static final String TAG = "SurfaceViewSyncActivity";
|
||||
|
||||
private SurfaceView mSurfaceView;
|
||||
private boolean mLastExpanded = true;
|
||||
|
||||
private RenderingThread mRenderingThread;
|
||||
|
||||
private final SurfaceSyncer mSurfaceSyncer = new SurfaceSyncer();
|
||||
|
||||
private Button mExpandButton;
|
||||
private Switch mEnableSyncSwitch;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_surfaceview_sync);
|
||||
mSurfaceView = findViewById(R.id.surface_view);
|
||||
mSurfaceView.getHolder().addCallback(this);
|
||||
|
||||
WindowManager windowManager = getWindowManager();
|
||||
WindowMetrics metrics = windowManager.getCurrentWindowMetrics();
|
||||
Rect bounds = metrics.getBounds();
|
||||
|
||||
LinearLayout container = findViewById(R.id.container);
|
||||
mExpandButton = findViewById(R.id.expand_sv);
|
||||
mEnableSyncSwitch = findViewById(R.id.enable_sync_switch);
|
||||
mExpandButton.setOnClickListener(view -> updateSurfaceViewSize(bounds, container));
|
||||
|
||||
mRenderingThread = new RenderingThread(mSurfaceView.getHolder());
|
||||
}
|
||||
|
||||
private void updateSurfaceViewSize(Rect bounds, View container) {
|
||||
final float height;
|
||||
if (mLastExpanded) {
|
||||
height = bounds.height() / 2f;
|
||||
mExpandButton.setText("EXPAND SV");
|
||||
} else {
|
||||
height = bounds.height() / 1.5f;
|
||||
mExpandButton.setText("COLLAPSE SV");
|
||||
}
|
||||
mLastExpanded = !mLastExpanded;
|
||||
|
||||
if (mEnableSyncSwitch.isChecked()) {
|
||||
int syncId = mSurfaceSyncer.setupSync(() -> { });
|
||||
mSurfaceSyncer.addToSync(syncId, mSurfaceView, frameCallback ->
|
||||
mRenderingThread.setFrameCallback(frameCallback));
|
||||
mSurfaceSyncer.addToSync(syncId, container);
|
||||
mSurfaceSyncer.markSyncReady(syncId);
|
||||
} else {
|
||||
mRenderingThread.renderSlow();
|
||||
}
|
||||
|
||||
ViewGroup.LayoutParams svParams = mSurfaceView.getLayoutParams();
|
||||
svParams.height = (int) height;
|
||||
mSurfaceView.setLayoutParams(svParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceCreated(@NonNull SurfaceHolder holder) {
|
||||
final Canvas canvas = holder.lockCanvas();
|
||||
canvas.drawARGB(255, 100, 100, 100);
|
||||
holder.unlockCanvasAndPost(canvas);
|
||||
mRenderingThread.startRendering();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
|
||||
mRenderingThread.stopRendering();
|
||||
}
|
||||
|
||||
private static class RenderingThread extends HandlerThread {
|
||||
private final SurfaceHolder mSurfaceHolder;
|
||||
private Handler mHandler;
|
||||
private SurfaceSyncer.SurfaceViewFrameCallback mFrameCallback;
|
||||
private boolean mRenderSlow;
|
||||
|
||||
int mColorValue = 0;
|
||||
int mColorDelta = 10;
|
||||
|
||||
RenderingThread(SurfaceHolder holder) {
|
||||
super("RenderingThread");
|
||||
mSurfaceHolder = holder;
|
||||
}
|
||||
|
||||
public void setFrameCallback(SurfaceSyncer.SurfaceViewFrameCallback frameCallback) {
|
||||
if (mHandler != null) {
|
||||
mHandler.post(() -> {
|
||||
mFrameCallback = frameCallback;
|
||||
mRenderSlow = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void renderSlow() {
|
||||
if (mHandler != null) {
|
||||
mHandler.post(() -> mRenderSlow = true);
|
||||
}
|
||||
}
|
||||
|
||||
private final Runnable mRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mFrameCallback != null) {
|
||||
mFrameCallback.onFrameStarted();
|
||||
}
|
||||
|
||||
if (mRenderSlow) {
|
||||
try {
|
||||
// Long delay from start to finish to mimic slow draw
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
mRenderSlow = false;
|
||||
}
|
||||
|
||||
mColorValue += mColorDelta;
|
||||
if (mColorValue > 245 || mColorValue < 10) {
|
||||
mColorDelta *= -1;
|
||||
}
|
||||
|
||||
Canvas c = mSurfaceHolder.lockCanvas();
|
||||
c.drawRGB(255, mColorValue, 255 - mColorValue);
|
||||
mSurfaceHolder.unlockCanvasAndPost(c);
|
||||
|
||||
if (mFrameCallback != null) {
|
||||
mFrameCallback.onFrameComplete();
|
||||
}
|
||||
mFrameCallback = null;
|
||||
|
||||
mHandler.postDelayed(this, 50);
|
||||
}
|
||||
};
|
||||
|
||||
public void startRendering() {
|
||||
start();
|
||||
mHandler = new Handler(getLooper());
|
||||
mHandler.post(mRunnable);
|
||||
}
|
||||
|
||||
public void stopRendering() {
|
||||
if (mHandler != null) {
|
||||
mHandler.post(() -> mHandler.removeCallbacks(mRunnable));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user