SurfaceControlCompat.kt

/*
 * Copyright 2021 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 androidx.graphics.surface

import android.graphics.Rect
import android.graphics.Region
import android.hardware.HardwareBuffer
import android.os.Build
import android.view.AttachedSurfaceControl
import android.view.Surface
import android.view.SurfaceControl
import android.view.SurfaceView
import androidx.annotation.IntDef
import androidx.annotation.RequiresApi
import androidx.graphics.lowlatency.SyncFenceCompat
import java.util.concurrent.Executor

/**
 * Handle to an on-screen Surface managed by the system compositor. [SurfaceControlCompat] is a
 * combination of a buffer source, and metadata about how to display the buffers. By constructing a
 * [Surface] from this [SurfaceControl] you can submit buffers to be composited. Using
 * [SurfaceControlCompat.Transaction] you can manipulate various properties of how the buffer will
 * be displayed on-screen. [SurfaceControlCompat]s are arranged into a scene-graph like hierarchy,
 * and as such any [SurfaceControlCompat] may have a parent. Geometric properties like transform,
 * crop, and Z-ordering will be inherited from the parent, as if the child were content in the
 * parents buffer stream.
 *
 * This class differs slightly than [SurfaceControl] in that it backports some functionality
 * to Android R and above by delegating to the related APIs available in the NDK. For newer Android
 * versions, this leverages the equivalent [SurfaceControl] API available in the SDK
 */
@RequiresApi(Build.VERSION_CODES.Q)
class SurfaceControlCompat internal constructor(
    internal val scImpl: SurfaceControlImpl
) {

    /**
     * Constants for [Transaction.setBufferTransform].
     *
     * Various transformations that can be applied to a buffer.
     */
    companion object {
        @Suppress("AcronymName")
        @IntDef(
            value = [BUFFER_TRANSFORM_IDENTITY, BUFFER_TRANSFORM_MIRROR_HORIZONTAL,
                BUFFER_TRANSFORM_MIRROR_VERTICAL, BUFFER_TRANSFORM_ROTATE_180,
                BUFFER_TRANSFORM_ROTATE_90, BUFFER_TRANSFORM_ROTATE_270]
        )
        internal annotation class BufferTransform

        /**
         * The identity transformation. Maps a coordinate (x, y) onto itself.
         */
        const val BUFFER_TRANSFORM_IDENTITY = 0

        /**
         * Mirrors the buffer horizontally. Maps a point (x, y) to (-x, y)
         */
        const val BUFFER_TRANSFORM_MIRROR_HORIZONTAL = 1

        /**
         * Mirrors the buffer vertically. Maps a point (x, y) to (x, -y)
         */
        const val BUFFER_TRANSFORM_MIRROR_VERTICAL = 2

        /**
         * Rotates the buffer 180 degrees clockwise. Maps a point (x, y) to (-x, -y)
         */
        const val BUFFER_TRANSFORM_ROTATE_180 = 3

        /**
         * Rotates the buffer 90 degrees clockwise. Maps a point (x, y) to (-y, x)
         */
        const val BUFFER_TRANSFORM_ROTATE_90 = 4

        /**
         * Rotates the buffer 270 degrees clockwise. Maps a point (x, y) to (y, -x)
         */
        const val BUFFER_TRANSFORM_ROTATE_270 = 7
    }

    /**
     * Check whether this instance points to a valid layer with the system-compositor.
     * For example this may be false if the layer was released ([release]).
     */
    fun isValid(): Boolean = scImpl.isValid()

    /**
     * Release the local reference to the server-side surface. The [Surface] may continue to exist
     * on-screen as long as its parent continues to exist. To explicitly remove a [Surface] from the
     * screen use [Transaction.reparent] with a null-parent. After release, [isValid] will return
     * false and other methods will throw an exception. Always call [release] when you are done with
     * a [SurfaceControlCompat] instance.
     */
    fun release() {
        scImpl.release()
    }

    /**
     * Builder class for [SurfaceControlCompat] objects. By default the [Surface] will be hidden,
     * and have "unset" bounds, meaning it can be as large as the bounds of its parent if a buffer
     * or child so requires. It is necessary to set at least a name via [Builder.setName]
     */
    class Builder {

        private val mBuilderImpl = createImpl()

        /**
         * Set a parent [Surface] from the provided [SurfaceView] for our new
         * [SurfaceControlCompat]. Child surfaces are constrained to the onscreen region of their
         * parent. Furthermore they stack relatively in Z order, and inherit the transformation of
         * the parent.
         * @param surfaceView Target [SurfaceView] used to provide the [Surface] this
         * [SurfaceControlCompat] is associated with.
         */
        @Suppress("MissingGetterMatchingBuilder")
        fun setParent(surfaceView: SurfaceView): Builder {
            mBuilderImpl.setParent(surfaceView)
            return this
        }

        /**
         * Set a debugging-name for the [SurfaceControlCompat].
         * @param name Debugging name configured on the [SurfaceControlCompat] instance.
         */
        @Suppress("MissingGetterMatchingBuilder")
        fun setName(name: String): Builder {
            mBuilderImpl.setName(name)
            return this
        }

        /**
         * Construct a new [SurfaceControlCompat] with the set parameters.
         * The builder remains valid after the [SurfaceControlCompat] instance is created.
         */
        fun build(): SurfaceControlCompat = SurfaceControlCompat(mBuilderImpl.build())

        internal companion object {
            @RequiresApi(Build.VERSION_CODES.Q)
            fun createImpl(): SurfaceControlImpl.Builder =
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    SurfaceControlVerificationHelper.createBuilderV33()
                } else {
                    SurfaceControlVerificationHelper.createBuilderV29()
                }
        }
    }

    /**
     * Interface to handle request to
     * [SurfaceControlV29.Transaction.addTransactionCompletedListener]
     */
    internal interface TransactionCompletedListener {
        /**
         * Invoked when a frame including the updates in a transaction was presented.
         *
         * Buffers which are replaced or removed from the scene in the transaction invoking
         * this callback may be reused after this point.
         */
        fun onTransactionCompleted()
    }

    /**
     * Interface to handle request to
     * [SurfaceControlCompat.Transaction.addTransactionCommittedListener]
     */
    interface TransactionCommittedListener {
        /**
         * Invoked when the transaction has been committed in SurfaceFlinger
         */
        fun onTransactionCommitted()
    }

    /**
     * An atomic set of changes to a set of [SurfaceControlCompat].
     */
    class Transaction : AutoCloseable {
        private val mImpl = createImpl()

        /**
         * Indicates whether the surface must be considered opaque, even if its pixel format is
         * set to translucent. This can be useful if an application needs full RGBA 8888 support for
         * instance but will still draw every pixel opaque.
         * This flag only determines whether opacity will be sampled from the alpha channel.
         * Plane-alpha from calls to setAlpha() can still result in blended composition regardless
         * of the opaque setting. Combined effects are (assuming a buffer format with an alpha
         * channel):
         *
         * OPAQUE + alpha(1.0) == opaque composition
         * OPAQUE + alpha(0.x) == blended composition
         * OPAQUE + alpha(0.0) == no composition
         * !OPAQUE + alpha(1.0) == blended composition
         * !OPAQUE + alpha(0.x) == blended composition
         * !OPAQUE + alpha(0.0) == no composition
         * If the underlying buffer lacks an alpha channel, it is as if setOpaque(true) were set
         * automatically.
         *
         * @param surfaceControl Target [SurfaceControlCompat] to change the opaque flag for
         * @param isOpaque Flag indicating if the [SurfaceControlCompat] should be fully opaque or
         * transparent
         */
        fun setOpaque(surfaceControl: SurfaceControlCompat, isOpaque: Boolean): Transaction {
            mImpl.setOpaque(surfaceControl.scImpl, isOpaque)
            return this
        }

        /**
         * Sets the visibility of a given Layer and it's sub-tree.
         * @param surfaceControl Target [SurfaceControlCompat] to change the visibility
         * @param visible `true` to indicate the [SurfaceControlCompat] should be visible, `false`
         * otherwise
         */
        fun setVisibility(surfaceControl: SurfaceControlCompat, visible: Boolean): Transaction {
            mImpl.setVisibility(surfaceControl.scImpl, visible)
            return this
        }

        /**
         * Re-parents a given [SurfaceControlCompat] to a new parent. Children inherit transform
         * (position, scaling) crop, visibility, and Z-ordering from their parents, as if the
         * children were pixels within the parent [Surface].
         * @param surfaceControl Target [SurfaceControlCompat] instance to reparent
         * @param newParent Parent [SurfaceControlCompat] that the target [SurfaceControlCompat]
         * instance is added to. This can be null indicating that the target [SurfaceControlCompat]
         * should be removed from the scene.
         */
        fun reparent(
            surfaceControl: SurfaceControlCompat,
            newParent: SurfaceControlCompat?
        ): Transaction {
            mImpl.reparent(surfaceControl.scImpl, newParent?.scImpl)
            return this
        }

        /**
         * Re-parents a given [SurfaceControlCompat] to be a child of the [AttachedSurfaceControl].
         * Children inherit transform (position, scaling) crop, visibility, and Z-ordering from
         * their parents, as if the children were pixels within the parent [Surface].
         * @param surfaceControl Target [SurfaceControlCompat] instance to reparent
         * @param attachedSurfaceControl [AttachedSurfaceControl] instance that acts as the new
         * parent of the provided [SurfaceControlCompat] instance.
         */
        @RequiresApi(Build.VERSION_CODES.TIRAMISU)
        fun reparent(
            surfaceControl: SurfaceControlCompat,
            attachedSurfaceControl: AttachedSurfaceControl
        ): Transaction {
            mImpl.reparent(surfaceControl.scImpl, attachedSurfaceControl)
            return this
        }

        /**
         * Updates the [HardwareBuffer] displayed for the [SurfaceControlCompat]. Note that the
         * buffer must be allocated with [HardwareBuffer.USAGE_COMPOSER_OVERLAY] as well as
         * [HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE] as the surface control might be composited using
         * either an overlay or using the GPU. A presentation fence may be passed to improve
         * performance by allowing the buffer to complete rendering while it is waiting for the
         * transaction to be applied. For example, if the buffer is being produced by rendering with
         * OpenGL ES then a fence created with the eglDupNativeFenceFDANDROID EGL extension API
         * can be used to allow the GPU rendering to be concurrent with the transaction.
         * The compositor will wait for the fence to be signaled before the buffer is displayed.
         * If multiple buffers are set as part of the same transaction, the presentation fences of
         * all of them must signal before any buffer is displayed. That is, the entire transaction
         * is delayed until all presentation fences have signaled, ensuring the transaction remains
         * consistent.
         *
         * @param surfaceControl Target [SurfaceControlCompat] to configure the provided buffer.
         * @param buffer [HardwareBuffer] instance to be rendered by the [SurfaceControlCompat]
         * instance.
         * @param fence Optional [SyncFenceCompat] that serves as the presentation fence. If set,
         * the [SurfaceControlCompat.Transaction] will not apply until the fence signals.
         * @param releaseCallback Optional callback invoked when the buffer is ready for re-use
         * after being presented to the display.
         */
        @JvmOverloads
        fun setBuffer(
            surfaceControl: SurfaceControlCompat,
            buffer: HardwareBuffer,
            fence: SyncFenceCompat? = null,
            releaseCallback: (() -> Unit)? = null
        ): Transaction {
            mImpl.setBuffer(surfaceControl.scImpl, buffer, fence?.mImpl, releaseCallback)
            return this
        }

        /**
         * Set the Z-order for a given [SurfaceControlCompat], relative to it's siblings.
         * If two siblings share the same Z order the ordering is undefined.
         * [Surface]s with a negative Z will be placed below the parent [Surface].
         */
        fun setLayer(
            surfaceControl: SurfaceControlCompat,
            z: Int
        ): Transaction {
            mImpl.setLayer(surfaceControl.scImpl, z)
            return this
        }

        /**
         * Request to add a [SurfaceControlCompat.TransactionCommittedListener]. The callback is
         * invoked when transaction is applied and the updates are ready to be presented. Once
         * applied, any callbacks added before the commit will be cleared from the Transaction.
         * This callback does not mean buffers have been released! It simply means that any new
         * transactions applied will not overwrite the transaction for which we are receiving a
         * callback and instead will be included in the next frame.
         * If you are trying to avoid dropping frames (overwriting transactions), and unable to
         * use timestamps (Which provide a more efficient solution), then this method provides a
         * method to pace your transaction application.
         * @param executor [Executor] to provide the thread the callback is invoked on.
         * @param listener [TransactionCommittedListener] instance that is invoked when the
         * transaction has been committed.
         */
        @Suppress("PairedRegistration")
        fun addTransactionCommittedListener(
            executor: Executor,
            listener: TransactionCommittedListener
        ): Transaction {
            mImpl.addTransactionCommittedListener(executor, listener)
            return this
        }

        /**
         * Updates the region for the content on this surface updated in this transaction. The
         * damage region is the area of the buffer that has changed since the previously
         * sent buffer. This can be used to reduce the amount of recomposition that needs to
         * happen when only a small region of the buffer is being updated, such as for a small
         * blinking cursor or a loading indicator.
         * @param surfaceControl Target [SurfaceControlCompat] to set damage region of.
         * @param region The region to be set. If null, the entire buffer is assumed dirty. This is
         * equivalent to not setting a damage region at all.
         */
        fun setDamageRegion(
            surfaceControl: SurfaceControlCompat,
            region: Region?
        ): Transaction {
            mImpl.setDamageRegion(surfaceControl.scImpl, region)
            return this
        }

        /**
         * Set the alpha for a given surface. If the alpha is non-zero the SurfaceControl will
         * be blended with the Surfaces under it according to the specified ratio.
         * @param surfaceControl Target [SurfaceControlCompat] to set the alpha of.
         * @param alpha The alpha to set. Value is between 0.0 and 1.0 inclusive.
         */
        fun setAlpha(
            surfaceControl: SurfaceControlCompat,
            alpha: Float
        ): Transaction {
            mImpl.setAlpha(surfaceControl.scImpl, alpha)
            return this
        }

        /**
         * Bounds the surface and its children to the bounds specified. Size of the surface
         * will be ignored and only the crop and buffer size will be used to determine the
         * bounds of the surface. If no crop is specified and the surface has no buffer,
         * the surface bounds is only constrained by the size of its parent bounds.
         *
         * @param surfaceControl The [SurfaceControlCompat] to apply the crop to. This value
         * cannot be null.
         *
         * @param crop Bounds of the crop to apply. This value can be null.
         *
         * @throws IllegalArgumentException if crop is not a valid rectangle.
         */
        fun setCrop(
            surfaceControl: SurfaceControlCompat,
            crop: Rect?
        ): Transaction {
            mImpl.setCrop(surfaceControl.scImpl, crop)
            return this
        }

        /**
         * Sets the SurfaceControl to the specified position relative to the parent SurfaceControl
         *
         * @param surfaceControl The [SurfaceControlCompat] to change position. This value cannot
         * be null
         *
         * @param x the X position
         *
         * @param y the Y position
         */
        fun setPosition(
            surfaceControl: SurfaceControlCompat,
            x: Float,
            y: Float
        ): Transaction {
            mImpl.setPosition(surfaceControl.scImpl, x, y)
            return this
        }

        /**
         * Sets the SurfaceControl to the specified scale with (0, 0) as the
         * center point of the scale.
         *
         * @param surfaceControl The [SurfaceControlCompat] to change scale. This value cannot
         * be null.
         *
         * @param scaleX the X scale
         *
         * @param scaleY the Y scale
         */
        fun setScale(
            surfaceControl: SurfaceControlCompat,
            scaleX: Float,
            scaleY: Float
        ): Transaction {
            mImpl.setScale(surfaceControl.scImpl, scaleX, scaleY)
            return this
        }

        /**
         * Sets the buffer transform that should be applied to the current buffer
         *
         * @param surfaceControl the [SurfaceControlCompat] to update. This value cannot be null.
         *
         * @param transformation The transform to apply to the buffer. Value is
         * [SurfaceControlCompat.BUFFER_TRANSFORM_IDENTITY],
         * [SurfaceControlCompat.BUFFER_TRANSFORM_MIRROR_HORIZONTAL],
         * [SurfaceControlCompat.BUFFER_TRANSFORM_MIRROR_VERTICAL],
         * [SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_90],
         * [SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_180],
         * [SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_270],
         * [SurfaceControlCompat.BUFFER_TRANSFORM_MIRROR_HORIZONTAL] |
         * [SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_90], or
         * [SurfaceControlCompat.BUFFER_TRANSFORM_MIRROR_VERTICAL] |
         * [SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_90]
         */
        fun setBufferTransform(
            surfaceControl: SurfaceControlCompat,
            @BufferTransform transformation: Int
        ): Transaction {
            mImpl.setBufferTransform(surfaceControl.scImpl, transformation)
            return this
        }

        /**
         * Commit the transaction, clearing it's state, and making it usable as a new transaction.
         * This will not release any resources and [SurfaceControlCompat.Transaction.close] must be
         * called to release the transaction.
         */
        fun commit() {
            mImpl.commit()
        }

        /**
         * Release the native transaction object, without committing it.
         */
        override fun close() {
            mImpl.close()
        }

        /**
         * Consume the passed in transaction, and request the View hierarchy to apply it atomically
         * with the next draw. This transaction will be merged with the buffer transaction from the
         * ViewRoot and they will show up on-screen atomically synced. This will not cause a draw to
         * be scheduled, and if there are no other changes to the View hierarchy you may need to
         * call View.invalidate()
         * @param attachedSurfaceControl [AttachedSurfaceControl] associated with the ViewRoot that
         * will apply the provided transaction on the next draw pass
         */
        @RequiresApi(Build.VERSION_CODES.TIRAMISU)
        fun commitTransactionOnDraw(attachedSurfaceControl: AttachedSurfaceControl) {
            mImpl.commitTransactionOnDraw(attachedSurfaceControl)
        }

        internal companion object {
            @RequiresApi(Build.VERSION_CODES.Q)
            fun createImpl(): SurfaceControlImpl.Transaction =
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    SurfaceControlVerificationHelper.createTransactionV33()
                } else {
                    SurfaceControlVerificationHelper.createTransactionV29()
                }
        }
    }
}

/**
 * Helper class to avoid class verification failures
 */
internal class SurfaceControlVerificationHelper private constructor() {

    companion object {
        @RequiresApi(Build.VERSION_CODES.TIRAMISU)
        @androidx.annotation.DoNotInline
        fun createBuilderV33(): SurfaceControlImpl.Builder = SurfaceControlV33.Builder()

        @RequiresApi(Build.VERSION_CODES.Q)
        @androidx.annotation.DoNotInline
        fun createBuilderV29(): SurfaceControlImpl.Builder = SurfaceControlV29.Builder()

        @RequiresApi(Build.VERSION_CODES.TIRAMISU)
        @androidx.annotation.DoNotInline
        fun createTransactionV33(): SurfaceControlImpl.Transaction = SurfaceControlV33.Transaction()

        @RequiresApi(Build.VERSION_CODES.Q)
        @androidx.annotation.DoNotInline
        fun createTransactionV29(): SurfaceControlImpl.Transaction = SurfaceControlV29.Transaction()
    }
}