Support alpha for SurfaceView

Basically, this removes an excuse for apps to use TextureView for video
playback or one-off HDR images prior to HDR UI being ready everywhere.

The idea is that for Z-above, applying alpha is easy, because the
surface just needs to be have alpha modulated. Z-below is a little bit
more creative - the alpha is applied to the hole punch and drawn using
DST_OUT blending semantics. This allows for views underneath the
SurfaceView to blend with surface, while being occluded by views on top
of the SurfaceView.

There may need to be some complex view hierarchies that would be useful
to test, but simple view layouts seem to work.

Note that this is guarded with a target SDK check, to defend against
applications that are propagated alpha to child views while expecting
opaque SurfaceViews.

Bug: 241474646
Test: HWAccelerationTest doesn't look broken
Change-Id: Ibc14b18f1ce6f25250318db50275c6b8c972bade
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index b6c92e3..586c193 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -107,6 +107,14 @@
  * and scaling a SurfaceView on screen will not cause rendering artifacts. Such
  * artifacts may occur on previous versions of the platform when its window is
  * positioned asynchronously.</p>
+ *
+ * <p class="note"><strong>Note:</strong> Starting in platform version
+ * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, SurfaceView will support arbitrary
+ * alpha blending. Prior platform versions ignored alpha values on the SurfaceView if they were
+ * between 0 and 1. If the SurfaceView is configured with Z-above, then the alpha is applied
+ * directly to the Surface. If the SurfaceView is configured with Z-below, then the alpha is
+ * applied to the hole punch directly. Note that when using Z-below, overlapping SurfaceViews
+ * may not blend properly as a consequence of not applying alpha to the surface content directly.
  */
 public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCallback {
     private static final String TAG = "SurfaceView";
@@ -146,6 +154,7 @@
     Paint mRoundedViewportPaint;
 
     int mSubLayer = APPLICATION_MEDIA_SUBLAYER;
+    int mRequestedSubLayer = APPLICATION_MEDIA_SUBLAYER;
 
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     boolean mIsCreating = false;
@@ -177,8 +186,7 @@
     @UnsupportedAppUsage
     int mRequestedFormat = PixelFormat.RGB_565;
 
-    boolean mUseAlpha = false;
-    float mSurfaceAlpha = 1f;
+    float mAlpha = 1f;
     boolean mClipSurfaceToBounds;
     int mBackgroundColor = Color.BLACK;
 
@@ -335,58 +343,25 @@
      * @hide
      */
     public void setUseAlpha() {
-        if (!mUseAlpha) {
-            mUseAlpha = true;
-            updateSurfaceAlpha();
-        }
+        // TODO(b/241474646): Remove me
+        return;
     }
 
     @Override
     public void setAlpha(float alpha) {
-        // Sets the opacity of the view to a value, where 0 means the view is completely transparent
-        // and 1 means the view is completely opaque.
-        //
-        // Note: Alpha value of this view is ignored by default. To enable alpha blending, you need
-        // to call setUseAlpha() as well.
-        // This view doesn't support translucent opacity if the view is located z-below, since the
-        // logic to punch a hole in the view hierarchy cannot handle such case. See also
-        // #clearSurfaceViewPort(Canvas)
         if (DEBUG) {
             Log.d(TAG, System.identityHashCode(this)
-                    + " setAlpha: mUseAlpha = " + mUseAlpha + " alpha=" + alpha);
+                    + " setAlpha: alpha=" + alpha);
         }
         super.setAlpha(alpha);
-        updateSurfaceAlpha();
     }
 
-    private float getFixedAlpha() {
-        // Compute alpha value to be set on the underlying surface.
-        final float alpha = getAlpha();
-        return mUseAlpha && (mSubLayer > 0 || alpha == 0f) ? alpha : 1f;
-    }
-
-    private void updateSurfaceAlpha() {
-        if (!mUseAlpha || !mHaveFrame || mSurfaceControl == null) {
-            return;
+    @Override
+    protected boolean onSetAlpha(int alpha) {
+        if (Math.round(mAlpha * 255) != alpha) {
+            updateSurface();
         }
-        final float viewAlpha = getAlpha();
-        if (mSubLayer < 0 && 0f < viewAlpha && viewAlpha < 1f) {
-            Log.w(TAG, System.identityHashCode(this)
-                    + " updateSurfaceAlpha:"
-                    + " translucent color is not supported for a surface placed z-below.");
-        }
-        final ViewRootImpl viewRoot = getViewRootImpl();
-        if (viewRoot == null) {
-            return;
-        }
-        final float alpha = getFixedAlpha();
-        if (alpha != mSurfaceAlpha) {
-            final Transaction transaction = new Transaction();
-            transaction.setAlpha(mSurfaceControl, alpha);
-            viewRoot.applyTransactionOnDraw(transaction);
-            damageInParent();
-            mSurfaceAlpha = alpha;
-        }
+        return true;
     }
 
     private void performDrawFinished() {
@@ -534,7 +509,15 @@
         invalidate();
     }
 
+    @Override
+    public boolean hasOverlappingRendering() {
+        // SurfaceViews only alpha composite by modulating the Surface alpha for Z-above, or
+        // applying alpha on the hole punch for Z-below - no deferral to a layer is necessary.
+        return false;
+    }
+
     private void clearSurfaceViewPort(Canvas canvas) {
+        final float alpha = getAlpha();
         if (mCornerRadius > 0f) {
             canvas.getClipBounds(mTmpRect);
             if (mClipSurfaceToBounds && mClipBounds != null) {
@@ -546,10 +529,11 @@
                     mTmpRect.right,
                     mTmpRect.bottom,
                     mCornerRadius,
-                    mCornerRadius
+                    mCornerRadius,
+                    alpha
             );
         } else {
-            canvas.punchHole(0f, 0f, getWidth(), getHeight(), 0f, 0f);
+            canvas.punchHole(0f, 0f, getWidth(), getHeight(), 0f, 0f, alpha);
         }
     }
 
@@ -652,10 +636,10 @@
         } else {
             subLayer = APPLICATION_MEDIA_SUBLAYER;
         }
-        if (mSubLayer == subLayer) {
+        if (mRequestedSubLayer == subLayer) {
             return false;
         }
-        mSubLayer = subLayer;
+        mRequestedSubLayer = subLayer;
 
         if (!allowDynamicChange) {
             return false;
@@ -667,9 +651,8 @@
         if (viewRoot == null) {
             return true;
         }
-        final Transaction transaction = new SurfaceControl.Transaction();
-        updateRelativeZ(transaction);
-        viewRoot.applyTransactionOnDraw(transaction);
+
+        updateSurface();
         invalidate();
         return true;
     }
@@ -722,8 +705,7 @@
     }
 
     private void releaseSurfaces(boolean releaseSurfacePackage) {
-        mSurfaceAlpha = 1f;
-
+        mAlpha = 1f;
         synchronized (mSurfaceControlLock) {
             mSurface.destroy();
             if (mBlastBufferQueue != null) {
@@ -770,7 +752,7 @@
     }
 
     private boolean performSurfaceTransaction(ViewRootImpl viewRoot, Translator translator,
-            boolean creating, boolean sizeChanged, boolean hintChanged,
+            boolean creating, boolean sizeChanged, boolean hintChanged, boolean relativeZChanged,
             Transaction surfaceUpdateTransaction) {
         boolean realSizeChanged = false;
 
@@ -800,14 +782,20 @@
                 surfaceUpdateTransaction.hide(mSurfaceControl);
             }
 
-
-
             updateBackgroundVisibility(surfaceUpdateTransaction);
             updateBackgroundColor(surfaceUpdateTransaction);
-            if (mUseAlpha) {
-                float alpha = getFixedAlpha();
+            if (isAboveParent()) {
+                float alpha = getAlpha();
                 surfaceUpdateTransaction.setAlpha(mSurfaceControl, alpha);
-                mSurfaceAlpha = alpha;
+            }
+
+            if (relativeZChanged) {
+                if (!isAboveParent()) {
+                    // If we're moving from z-above to z-below, then restore the surface alpha back to 1
+                    // and let the holepunch drive visibility and blending.
+                    surfaceUpdateTransaction.setAlpha(mSurfaceControl, 1.f);
+                }
+                updateRelativeZ(surfaceUpdateTransaction);
             }
 
             surfaceUpdateTransaction.setCornerRadius(mSurfaceControl, mCornerRadius);
@@ -873,6 +861,7 @@
         } finally {
             mSurfaceLock.unlock();
         }
+
         return realSizeChanged;
     }
 
@@ -906,10 +895,10 @@
         int myHeight = mRequestedHeight;
         if (myHeight <= 0) myHeight = getHeight();
 
-        final float alpha = getFixedAlpha();
+        final float alpha = getAlpha();
         final boolean formatChanged = mFormat != mRequestedFormat;
         final boolean visibleChanged = mVisible != mRequestedVisible;
-        final boolean alphaChanged = mSurfaceAlpha != alpha;
+        final boolean alphaChanged = mAlpha != alpha;
         final boolean creating = (mSurfaceControl == null || formatChanged || visibleChanged)
                 && mRequestedVisible;
         final boolean sizeChanged = mSurfaceWidth != myWidth || mSurfaceHeight != myHeight;
@@ -921,17 +910,17 @@
             || getHeight() != mScreenRect.height();
         final boolean hintChanged = (viewRoot.getBufferTransformHint() != mTransformHint)
                 && mRequestedVisible;
+        final boolean relativeZChanged = mSubLayer != mRequestedSubLayer;
 
-        if (creating || formatChanged || sizeChanged || visibleChanged ||
-                (mUseAlpha && alphaChanged) || windowVisibleChanged ||
-                positionChanged || layoutSizeChanged || hintChanged) {
+        if (creating || formatChanged || sizeChanged || visibleChanged
+                || alphaChanged || windowVisibleChanged || positionChanged
+                || layoutSizeChanged || hintChanged || relativeZChanged) {
 
             if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
                     + "Changes: creating=" + creating
                     + " format=" + formatChanged + " size=" + sizeChanged
                     + " visible=" + visibleChanged + " alpha=" + alphaChanged
                     + " hint=" + hintChanged
-                    + " mUseAlpha=" + mUseAlpha
                     + " visible=" + visibleChanged
                     + " left=" + (mWindowSpaceLeft != mLocation[0])
                     + " top=" + (mWindowSpaceTop != mLocation[1]));
@@ -943,8 +932,10 @@
                 mSurfaceWidth = myWidth;
                 mSurfaceHeight = myHeight;
                 mFormat = mRequestedFormat;
+                mAlpha = alpha;
                 mLastWindowVisibility = mWindowVisibility;
                 mTransformHint = viewRoot.getBufferTransformHint();
+                mSubLayer = mRequestedSubLayer;
 
                 mScreenRect.left = mWindowSpaceLeft;
                 mScreenRect.top = mWindowSpaceTop;
@@ -968,7 +959,7 @@
                 }
 
                 final boolean redrawNeeded = sizeChanged || creating || hintChanged
-                        || (mVisible && !mDrawFinished);
+                        || (mVisible && !mDrawFinished) || alphaChanged || relativeZChanged;
                 boolean shouldSyncBuffer =
                         redrawNeeded && viewRoot.wasRelayoutRequested() && viewRoot.isInLocalSync();
                 SyncBufferTransactionCallback syncBufferTransactionCallback = null;
@@ -979,8 +970,9 @@
                             syncBufferTransactionCallback::onTransactionReady);
                 }
 
-                final boolean realSizeChanged = performSurfaceTransaction(viewRoot,
-                        translator, creating, sizeChanged, hintChanged, surfaceUpdateTransaction);
+                final boolean realSizeChanged = performSurfaceTransaction(viewRoot, translator,
+                        creating, sizeChanged, hintChanged, relativeZChanged,
+                        surfaceUpdateTransaction);
 
                 try {
                     SurfaceHolder.Callback[] callbacks = null;
diff --git a/graphics/java/android/graphics/BaseCanvas.java b/graphics/java/android/graphics/BaseCanvas.java
index a8ab6d9..54d6428 100644
--- a/graphics/java/android/graphics/BaseCanvas.java
+++ b/graphics/java/android/graphics/BaseCanvas.java
@@ -670,8 +670,9 @@
     /**
      * @hide
      */
-    public void punchHole(float left, float top, float right, float bottom, float rx, float ry) {
-        nPunchHole(mNativeCanvasWrapper, left, top, right, bottom, rx, ry);
+    public void punchHole(float left, float top, float right, float bottom, float rx, float ry,
+            float alpha) {
+        nPunchHole(mNativeCanvasWrapper, left, top, right, bottom, rx, ry, alpha);
     }
 
     /**
@@ -823,5 +824,5 @@
             float hOffset, float vOffset, int flags, long nativePaint);
 
     private static native void nPunchHole(long renderer, float left, float top, float right,
-            float bottom, float rx, float ry);
+            float bottom, float rx, float ry, float alpha);
 }
diff --git a/graphics/java/android/graphics/BaseRecordingCanvas.java b/graphics/java/android/graphics/BaseRecordingCanvas.java
index d06f665..1ba79b8 100644
--- a/graphics/java/android/graphics/BaseRecordingCanvas.java
+++ b/graphics/java/android/graphics/BaseRecordingCanvas.java
@@ -610,8 +610,9 @@
      * @hide
      */
     @Override
-    public void punchHole(float left, float top, float right, float bottom, float rx, float ry) {
-        nPunchHole(mNativeCanvasWrapper, left, top, right, bottom, rx, ry);
+    public void punchHole(float left, float top, float right, float bottom, float rx, float ry,
+            float alpha) {
+        nPunchHole(mNativeCanvasWrapper, left, top, right, bottom, rx, ry, alpha);
     }
 
     @FastNative
@@ -742,5 +743,5 @@
 
     @FastNative
     private static native void nPunchHole(long renderer, float left, float top, float right,
-            float bottom, float rx, float ry);
+            float bottom, float rx, float ry, float alpha);
 }
diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp
index 023d6bf..20c6d68 100644
--- a/libs/hwui/SkiaCanvas.cpp
+++ b/libs/hwui/SkiaCanvas.cpp
@@ -18,6 +18,7 @@
 
 #include "CanvasProperty.h"
 #include "NinePatchUtils.h"
+#include "SkBlendMode.h"
 #include "VectorDrawable.h"
 #include "hwui/Bitmap.h"
 #include "hwui/MinikinUtils.h"
@@ -251,10 +252,11 @@
     return (rec && rec->saveCount == currentSaveCount) ? rec : nullptr;
 }
 
-void SkiaCanvas::punchHole(const SkRRect& rect) {
+void SkiaCanvas::punchHole(const SkRRect& rect, float alpha) {
     SkPaint paint = SkPaint();
-    paint.setColor(0);
-    paint.setBlendMode(SkBlendMode::kClear);
+    paint.setColor(SkColors::kBlack);
+    paint.setAlphaf(alpha);
+    paint.setBlendMode(SkBlendMode::kDstOut);
     mCanvas->drawRRect(rect, paint);
 }
 
diff --git a/libs/hwui/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h
index c6313f6..51007c5 100644
--- a/libs/hwui/SkiaCanvas.h
+++ b/libs/hwui/SkiaCanvas.h
@@ -65,7 +65,7 @@
         LOG_ALWAYS_FATAL("SkiaCanvas does not support enableZ");
     }
 
-    virtual void punchHole(const SkRRect& rect) override;
+    virtual void punchHole(const SkRRect& rect, float alpha) override;
 
     virtual void setBitmap(const SkBitmap& bitmap) override;
 
diff --git a/libs/hwui/hwui/Canvas.h b/libs/hwui/hwui/Canvas.h
index 7378351..82d23b5 100644
--- a/libs/hwui/hwui/Canvas.h
+++ b/libs/hwui/hwui/Canvas.h
@@ -152,7 +152,7 @@
         LOG_ALWAYS_FATAL("Not supported");
     }
 
-    virtual void punchHole(const SkRRect& rect) = 0;
+    virtual void punchHole(const SkRRect& rect, float alpha) = 0;
 
     // ----------------------------------------------------------------------------
     // Canvas state operations
diff --git a/libs/hwui/jni/android_graphics_Canvas.cpp b/libs/hwui/jni/android_graphics_Canvas.cpp
index fb7d5f7..0513447 100644
--- a/libs/hwui/jni/android_graphics_Canvas.cpp
+++ b/libs/hwui/jni/android_graphics_Canvas.cpp
@@ -713,9 +713,10 @@
 }
 
 static void punchHole(JNIEnv* env, jobject, jlong canvasPtr, jfloat left, jfloat top, jfloat right,
-        jfloat bottom, jfloat rx, jfloat ry) {
+        jfloat bottom, jfloat rx, jfloat ry, jfloat alpha) {
     auto canvas = reinterpret_cast<Canvas*>(canvasPtr);
-    canvas->punchHole(SkRRect::MakeRectXY(SkRect::MakeLTRB(left, top, right, bottom), rx, ry));
+    canvas->punchHole(SkRRect::MakeRectXY(SkRect::MakeLTRB(left, top, right, bottom), rx, ry),
+                      alpha);
 }
 
 }; // namespace CanvasJNI
@@ -790,7 +791,7 @@
     {"nDrawTextRun","(JLjava/lang/String;IIIIFFZJ)V", (void*) CanvasJNI::drawTextRunString},
     {"nDrawTextOnPath","(J[CIIJFFIJ)V", (void*) CanvasJNI::drawTextOnPathChars},
     {"nDrawTextOnPath","(JLjava/lang/String;JFFIJ)V", (void*) CanvasJNI::drawTextOnPathString},
-    {"nPunchHole", "(JFFFFFF)V", (void*) CanvasJNI::punchHole}
+    {"nPunchHole", "(JFFFFFFF)V", (void*) CanvasJNI::punchHole}
 };
 
 int register_android_graphics_Canvas(JNIEnv* env) {
diff --git a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
index 3bf2b2e..f2282e66 100644
--- a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
+++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
@@ -201,6 +201,7 @@
         paint.setAlpha((uint8_t)paint.getAlpha() * mAlpha);
         return true;
     }
+
     void onDrawDrawable(SkDrawable* drawable, const SkMatrix* matrix) override {
         // We unroll the drawable using "this" canvas, so that draw calls contained inside will
         // get their alpha applied. The default SkPaintFilterCanvas::onDrawDrawable does not unroll.
@@ -292,7 +293,7 @@
                 // with the same canvas transformation + clip into the target
                 // canvas then draw the layer on top
                 if (renderNode->hasHolePunches()) {
-                    TransformCanvas transformCanvas(canvas, SkBlendMode::kClear);
+                    TransformCanvas transformCanvas(canvas, SkBlendMode::kDstOut);
                     displayList->draw(&transformCanvas);
                 }
                 canvas->drawImageRect(snapshotImage, SkRect::Make(srcBounds),
diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
index 5c6117d..1f87865 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
@@ -69,20 +69,22 @@
     mDisplayList->setHasHolePunches(false);
 }
 
-void SkiaRecordingCanvas::punchHole(const SkRRect& rect) {
-    // Add the marker annotation to allow HWUI to determine where the current
-    // clip/transformation should be applied
+void SkiaRecordingCanvas::punchHole(const SkRRect& rect, float alpha) {
+    // Add the marker annotation to allow HWUI to determine the current
+    // clip/transformation and alpha should be applied
     SkVector vector = rect.getSimpleRadii();
-    float data[2];
+    float data[3];
     data[0] = vector.x();
     data[1] = vector.y();
+    data[2] = alpha;
     mRecorder.drawAnnotation(rect.rect(), HOLE_PUNCH_ANNOTATION.c_str(),
-                             SkData::MakeWithCopy(data, 2 * sizeof(float)));
+                             SkData::MakeWithCopy(data, sizeof(data)));
 
     // Clear the current rect within the layer itself
     SkPaint paint = SkPaint();
-    paint.setColor(0);
-    paint.setBlendMode(SkBlendMode::kClear);
+    paint.setColor(SkColors::kBlack);
+    paint.setAlphaf(alpha);
+    paint.setBlendMode(SkBlendMode::kDstOut);
     mRecorder.drawRRect(rect, paint);
 
     mDisplayList->setHasHolePunches(true);
diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
index 89e3a2c..7844e2c 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
@@ -50,7 +50,7 @@
         initDisplayList(renderNode, width, height);
     }
 
-    virtual void punchHole(const SkRRect& rect) override;
+    virtual void punchHole(const SkRRect& rect, float alpha) override;
 
     virtual void finishRecording(uirenderer::RenderNode* destination) override;
     std::unique_ptr<SkiaDisplayList> finishRecording();
diff --git a/libs/hwui/pipeline/skia/TransformCanvas.cpp b/libs/hwui/pipeline/skia/TransformCanvas.cpp
index 33160d0..c320df0 100644
--- a/libs/hwui/pipeline/skia/TransformCanvas.cpp
+++ b/libs/hwui/pipeline/skia/TransformCanvas.cpp
@@ -29,13 +29,15 @@
 void TransformCanvas::onDrawAnnotation(const SkRect& rect, const char* key, SkData* value) {
     if (HOLE_PUNCH_ANNOTATION == key) {
         auto* rectParams = reinterpret_cast<const float*>(value->data());
-        float radiusX = rectParams[0];
-        float radiusY = rectParams[1];
+        const float radiusX = rectParams[0];
+        const float radiusY = rectParams[1];
+        const float alpha = rectParams[2];
         SkRRect roundRect = SkRRect::MakeRectXY(rect, radiusX, radiusY);
 
         SkPaint paint;
         paint.setColor(SkColors::kBlack);
         paint.setBlendMode(mHolePunchBlendMode);
+        paint.setAlphaf(alpha);
         mWrappedCanvas->drawRRect(roundRect, paint);
     }
 }
diff --git a/libs/hwui/tests/common/scenes/StretchyListViewAnimation.cpp b/libs/hwui/tests/common/scenes/StretchyListViewAnimation.cpp
index 59230a7..7d3ca96 100644
--- a/libs/hwui/tests/common/scenes/StretchyListViewAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/StretchyListViewAnimation.cpp
@@ -138,7 +138,7 @@
         roundRectPaint.setColor(Color::White);
         if (addHolePunch) {
             // Punch a hole but then cover it up, we don't want to actually see it
-            canvas.punchHole(SkRRect::MakeRect(SkRect::MakeWH(itemWidth, itemHeight)));
+            canvas.punchHole(SkRRect::MakeRect(SkRect::MakeWH(itemWidth, itemHeight)), 1.f);
         }
         canvas.drawRoundRect(0, 0, itemWidth, itemHeight, dp(6), dp(6), roundRectPaint);
 
@@ -235,4 +235,4 @@
     StretchEffectBehavior stretchBehavior() override { return StretchEffectBehavior::UniformScale; }
     bool haveHolePunch() override { return true; }
     bool forceLayer() override { return true; }
-};
\ No newline at end of file
+};
diff --git a/tests/HwAccelerationTest/AndroidManifest.xml b/tests/HwAccelerationTest/AndroidManifest.xml
index b0ccbd1..939c7de 100644
--- a/tests/HwAccelerationTest/AndroidManifest.xml
+++ b/tests/HwAccelerationTest/AndroidManifest.xml
@@ -436,6 +436,15 @@
             </intent-filter>
         </activity>
 
+        <activity android:name="SurfaceViewAlphaActivity"
+             android:label="SurfaceView/SurfaceView with Alpha"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="com.android.test.hwui.TEST"/>
+            </intent-filter>
+        </activity>
+
         <activity android:name=".PenStylusActivity"
                   android:label="Pen/Draw"
                   android:exported="true">
diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/SurfaceViewAlphaActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/SurfaceViewAlphaActivity.java
new file mode 100644
index 0000000..01fe6ae0
--- /dev/null
+++ b/tests/HwAccelerationTest/src/com/android/test/hwui/SurfaceViewAlphaActivity.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 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.hwui;
+
+import android.app.Activity;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.view.SurfaceHolder;
+import android.view.SurfaceHolder.Callback;
+import android.view.SurfaceView;
+import android.view.View;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+public class SurfaceViewAlphaActivity extends Activity implements Callback {
+    SurfaceView mSurfaceView;
+
+    private enum ZOrder {
+        ABOVE,
+        BELOW
+    }
+
+    private float mAlpha = 127f / 255f;
+    private ZOrder mZOrder = ZOrder.BELOW;
+
+
+    private String getAlphaText() {
+        return "Alpha: " + mAlpha;
+    }
+
+    private void toggleZOrder() {
+        if (ZOrder.ABOVE.equals(mZOrder)) {
+            mZOrder = ZOrder.BELOW;
+        } else {
+            mZOrder = ZOrder.ABOVE;
+        }
+    }
+
+    // Overlaps a blue view on the left, then the SurfaceView in the center, then a blue view on the
+    // right.
+    private void overlapViews(SurfaceView view, LinearLayout parent) {
+        float density = getResources().getDisplayMetrics().density;
+        int surfaceViewSize = (int) (200 * density);
+        int blueViewSize = (int) (surfaceViewSize * 2 / 3f);
+        int totalSize = (int) (surfaceViewSize * 5 / 3f);
+
+        RelativeLayout overlapLayout = new RelativeLayout(this);
+
+        RelativeLayout.LayoutParams leftViewLayoutParams = new RelativeLayout.LayoutParams(
+                blueViewSize, surfaceViewSize);
+        leftViewLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
+
+        View leftBlueView = new View(this);
+        leftBlueView.setBackgroundColor(Color.BLUE);
+        overlapLayout.addView(leftBlueView, leftViewLayoutParams);
+
+        RelativeLayout.LayoutParams sVLayoutParams = new RelativeLayout.LayoutParams(
+                surfaceViewSize, surfaceViewSize);
+        sVLayoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
+        overlapLayout.addView(view, sVLayoutParams);
+
+        RelativeLayout.LayoutParams rightViewLayoutParams = new RelativeLayout.LayoutParams(
+                blueViewSize, surfaceViewSize);
+        rightViewLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
+
+        View rightBlueView = new View(this);
+        rightBlueView.setBackgroundColor(Color.BLUE);
+        overlapLayout.addView(rightBlueView, rightViewLayoutParams);
+
+        parent.addView(overlapLayout, new LinearLayout.LayoutParams(
+                totalSize, surfaceViewSize));
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mSurfaceView = new SurfaceView(this);
+        mSurfaceView.getHolder().addCallback(this);
+        mSurfaceView.setAlpha(mAlpha);
+
+        LinearLayout content = new LinearLayout(this);
+        content.setOrientation(LinearLayout.VERTICAL);
+
+        TextView alphaText = new TextView(this);
+        alphaText.setText(getAlphaText());
+
+        SeekBar alphaToggle = new SeekBar(this);
+        alphaToggle.setMin(0);
+        alphaToggle.setMax(255);
+        alphaToggle.setProgress(Math.round(mAlpha * 255));
+        alphaToggle.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                mAlpha = progress / 255f;
+                alphaText.setText(getAlphaText());
+                mSurfaceView.setAlpha(mAlpha);
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+
+            }
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+
+            }
+        });
+
+        content.addView(alphaText, new LinearLayout.LayoutParams(
+                LinearLayout.LayoutParams.WRAP_CONTENT,
+                LinearLayout.LayoutParams.WRAP_CONTENT));
+
+        content.addView(alphaToggle, new LinearLayout.LayoutParams(
+                LinearLayout.LayoutParams.MATCH_PARENT,
+                LinearLayout.LayoutParams.WRAP_CONTENT));
+
+        Button button = new Button(this);
+        button.setText("Z " + mZOrder.toString());
+        button.setOnClickListener(v -> {
+            toggleZOrder();
+            mSurfaceView.setZOrderOnTop(ZOrder.ABOVE.equals(mZOrder));
+            button.setText("Z " + mZOrder.toString());
+        });
+
+        content.addView(button, new LinearLayout.LayoutParams(
+                LinearLayout.LayoutParams.WRAP_CONTENT,
+                LinearLayout.LayoutParams.WRAP_CONTENT));
+
+        overlapViews(mSurfaceView, content);
+
+        setContentView(content);
+    }
+
+    @Override
+    public void surfaceCreated(SurfaceHolder holder) {
+    }
+
+    @Override
+    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+        Canvas canvas = holder.lockCanvas();
+        canvas.drawColor(Color.RED);
+        holder.unlockCanvasAndPost(canvas);
+    }
+
+    @Override
+    public void surfaceDestroyed(SurfaceHolder holder) {
+    }
+}