Add SurfaceSyncer class

Add Syncer class that 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. SurfaceSyncer#setupSync is called
2. addToSync 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. SurfaceSyncer#markSyncReady should be called when all the SyncTargets
   have been added to the SyncSet. Now the SyncSet is closed and no more
   SyncTargets can be added to it.
4. When all SyncTargets are complete, the final merged Transaction will
   either be applied or sent back to the caller.

The following is what happens within the SyncSet
1. Each SyncableTarget will get an onReadyToSync callback that contains
   a SyncBufferCallback.
2. Each SyncableTarget needs to invoke SyncBufferCallback#onBufferReady.
   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 FrameCallback 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.

Test: SurfaceSyncerTest
Test: SurfaceSyncerContinuousTest
Bug: 200284684
Change-Id: Iab87bff8a0483581e57803724eae88f0a3d96c8e
diff --git a/tests/SurfaceViewSyncTest/Android.bp b/tests/SurfaceViewSyncTest/Android.bp
new file mode 100644
index 0000000..1c6e380
--- /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 0000000..d085f8c
--- /dev/null
+++ b/tests/SurfaceViewSyncTest/AndroidManifest.xml
@@ -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>
diff --git a/tests/SurfaceViewSyncTest/OWNERS b/tests/SurfaceViewSyncTest/OWNERS
new file mode 100644
index 0000000..0862c05
--- /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 0000000..4433b21
--- /dev/null
+++ b/tests/SurfaceViewSyncTest/res/layout/activity_surfaceview_sync.xml
@@ -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>
\ No newline at end of file
diff --git a/tests/SurfaceViewSyncTest/src/com/android/test/SurfaceViewSyncActivity.java b/tests/SurfaceViewSyncTest/src/com/android/test/SurfaceViewSyncActivity.java
new file mode 100644
index 0000000..06accec
--- /dev/null
+++ b/tests/SurfaceViewSyncTest/src/com/android/test/SurfaceViewSyncActivity.java
@@ -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));
+            }
+        }
+    }
+}