Wire up SurfaceView stretch effect

Bug: 179047472
Test: StretchySurfaceViewActivity

Change-Id: I7f3d582cc66fb732a557e9332edc6d186db2335c
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 66b9617..3e62fde 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -141,6 +141,9 @@
             int layerStack);
     private static native void nativeSetBlurRegions(long transactionObj, long nativeObj,
             float[][] regions, int length);
+    private static native void nativeSetStretchEffect(long transactionObj, long nativeObj,
+            float left, float top, float right, float bottom, float vecX, float vecY,
+            float maxStretchAmount);
 
     private static native boolean nativeClearContentFrameStats(long nativeObject);
     private static native boolean nativeGetContentFrameStats(long nativeObject, WindowContentFrameStats outStats);
@@ -2951,6 +2954,17 @@
         /**
          * @hide
          */
+        public Transaction setStretchEffect(SurfaceControl sc, float left, float top, float right,
+                float bottom, float vecX, float vecY, float maxStretchAmount) {
+            checkPreconditions(sc);
+            nativeSetStretchEffect(mNativeObject, sc.mNativeObject, left, top, right, bottom,
+                    vecX, vecY, maxStretchAmount);
+            return this;
+        }
+
+        /**
+         * @hide
+         */
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.O)
         public Transaction setLayerStack(SurfaceControl sc, int layerStack) {
             checkPreconditions(sc);
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index 6eba83f..ec7e4c1 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -1444,6 +1444,14 @@
         }
 
         @Override
+        public void applyStretch(long frameNumber, float left, float top, float right,
+                float bottom, float vecX, float vecY, float maxStretch) {
+            mRtTransaction.setStretchEffect(mSurfaceControl, left, top, right, bottom, vecX, vecY,
+                    maxStretch);
+            applyRtTransaction(frameNumber);
+        }
+
+        @Override
         public void positionLost(long frameNumber) {
             if (DEBUG) {
                 Log.d(TAG, String.format("%d windowPositionLost, frameNr = %d",
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 7a3366a..ffd1380 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -580,6 +580,15 @@
     transaction->setBlurRegions(ctrl, blurRegionVector);
 }
 
+static void nativeSetStretchEffect(JNIEnv* env, jclass clazz, jlong transactionObj,
+                                   jlong nativeObject, jfloat left, jfloat top, jfloat right,
+                                   jfloat bottom, jfloat vecX, jfloat vecY,
+                                   jfloat maxStretchAmount) {
+    auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
+    SurfaceControl* const ctrl = reinterpret_cast<SurfaceControl*>(nativeObject);
+    transaction->setStretchEffect(ctrl, left, top, right, bottom, vecX, vecY, maxStretchAmount);
+}
+
 static void nativeSetSize(JNIEnv* env, jclass clazz, jlong transactionObj,
         jlong nativeObject, jint w, jint h) {
     auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
@@ -1754,6 +1763,8 @@
             (void*)nativeSetLayerStack },
     {"nativeSetBlurRegions", "(JJ[[FI)V",
             (void*)nativeSetBlurRegions },
+    {"nativeSetStretchEffect", "(JJFFFFFFF)V",
+            (void*) nativeSetStretchEffect },
     {"nativeSetShadowRadius", "(JJF)V",
             (void*)nativeSetShadowRadius },
     {"nativeSetFrameRate", "(JJFIZ)V",
diff --git a/graphics/java/android/graphics/RenderNode.java b/graphics/java/android/graphics/RenderNode.java
index 4a92cf1..f6f770b 100644
--- a/graphics/java/android/graphics/RenderNode.java
+++ b/graphics/java/android/graphics/RenderNode.java
@@ -272,6 +272,16 @@
         void positionChanged(long frameNumber, int left, int top, int right, int bottom);
 
         /**
+         * Call to apply a stretch effect to any child SurfaceControl layers
+         *
+         * TODO: Fold this into positionChanged & have HWUI do the ASurfaceControl calls?
+         *
+         * @hide
+         */
+        default void applyStretch(long frameNumber, float left, float top, float right,
+                float bottom, float vecX, float vecY, float maxStretch) { }
+
+        /**
          * Called by native on RenderThread to notify that the view is no longer in the
          * draw tree. UI thread is blocked at this point.
          *
@@ -312,6 +322,14 @@
                 pul.positionLost(frameNumber);
             }
         }
+
+        @Override
+        public void applyStretch(long frameNumber, float left, float top, float right, float bottom,
+                float vecX, float vecY, float maxStretch) {
+            for (PositionUpdateListener pul : mListeners) {
+                pul.applyStretch(frameNumber, left, top, right, bottom, vecX, vecY, maxStretch);
+            }
+        }
     }
 
     /**
@@ -707,7 +725,7 @@
         if (1.0 < vecY || vecY < -1.0) {
             throw new IllegalArgumentException("vecY must be in the range [-1, 1], was " + vecY);
         }
-        if (top <= bottom || right <= left) {
+        if (top >= bottom || left >= right) {
             throw new IllegalArgumentException(
                     "Stretch region must not be empty, got "
                             + new RectF(left, top, right, bottom).toString());
diff --git a/libs/hwui/DamageAccumulator.cpp b/libs/hwui/DamageAccumulator.cpp
index b39f4f2..0bf9480 100644
--- a/libs/hwui/DamageAccumulator.cpp
+++ b/libs/hwui/DamageAccumulator.cpp
@@ -249,5 +249,20 @@
     mHead->pendingDirty.setEmpty();
 }
 
+const StretchEffect* DamageAccumulator::findNearestStretchEffect() const {
+    DirtyStack* frame = mHead;
+    while (frame->prev != frame) {
+        frame = frame->prev;
+        if (frame->type == TransformRenderNode) {
+            const auto& effect =
+                    frame->renderNode->properties().layerProperties().getStretchEffect();
+            if (!effect.isEmpty()) {
+                return &effect;
+            }
+        }
+    }
+    return nullptr;
+}
+
 } /* namespace uirenderer */
 } /* namespace android */
diff --git a/libs/hwui/DamageAccumulator.h b/libs/hwui/DamageAccumulator.h
index 2faa9d0..89ee0e3 100644
--- a/libs/hwui/DamageAccumulator.h
+++ b/libs/hwui/DamageAccumulator.h
@@ -35,6 +35,7 @@
 struct DirtyStack;
 class RenderNode;
 class Matrix4;
+class StretchEffect;
 
 class DamageAccumulator {
     PREVENT_COPY_AND_ASSIGN(DamageAccumulator);
@@ -62,6 +63,8 @@
 
     void finish(SkRect* totalDirty);
 
+    const StretchEffect* findNearestStretchEffect() const;
+
 private:
     void pushCommon();
     void applyMatrix4Transform(DirtyStack* frame);
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index 44f54ee..f5b2675 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -226,6 +226,9 @@
     if (!mProperties.getAllowForceDark()) {
         info.disableForceDark++;
     }
+    if (!mProperties.layerProperties().getStretchEffect().isEmpty()) {
+        info.stretchEffectCount++;
+    }
 
     uint32_t animatorDirtyMask = 0;
     if (CC_LIKELY(info.runAnimations)) {
@@ -267,6 +270,9 @@
     if (!mProperties.getAllowForceDark()) {
         info.disableForceDark--;
     }
+    if (!mProperties.layerProperties().getStretchEffect().isEmpty()) {
+        info.stretchEffectCount--;
+    }
     info.damageAccumulator->popTransform();
 }
 
diff --git a/libs/hwui/RenderProperties.cpp b/libs/hwui/RenderProperties.cpp
index 8fba9cf..0589f13 100644
--- a/libs/hwui/RenderProperties.cpp
+++ b/libs/hwui/RenderProperties.cpp
@@ -70,6 +70,7 @@
     setXferMode(other.xferMode());
     setColorFilter(other.getColorFilter());
     setImageFilter(other.getImageFilter());
+    mStretchEffect = other.mStretchEffect;
     return *this;
 }
 
diff --git a/libs/hwui/TreeInfo.h b/libs/hwui/TreeInfo.h
index f2481f8..cc9094c 100644
--- a/libs/hwui/TreeInfo.h
+++ b/libs/hwui/TreeInfo.h
@@ -98,6 +98,8 @@
 
     const SkISize screenSize;
 
+    int stretchEffectCount = 0;
+
     struct Out {
         bool hasFunctors = false;
         // This is only updated if evaluateAnimations is true
diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp
index 8023968..5f60437 100644
--- a/libs/hwui/jni/android_graphics_RenderNode.cpp
+++ b/libs/hwui/jni/android_graphics_RenderNode.cpp
@@ -25,6 +25,7 @@
 #include <renderthread/CanvasContext.h>
 #endif
 #include <TreeInfo.h>
+#include <effects/StretchEffect.h>
 #include <hwui/Paint.h>
 #include <utils/TraceUtils.h>
 
@@ -549,6 +550,7 @@
 // ----------------------------------------------------------------------------
 
 jmethodID gPositionListener_PositionChangedMethod;
+jmethodID gPositionListener_ApplyStretchMethod;
 jmethodID gPositionListener_PositionLostMethod;
 
 static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject,
@@ -571,6 +573,11 @@
             Matrix4 transform;
             info.damageAccumulator->computeCurrentTransform(&transform);
             const RenderProperties& props = node.properties();
+
+            if (info.stretchEffectCount) {
+                handleStretchEffect(info, transform);
+            }
+
             uirenderer::Rect bounds(props.getWidth(), props.getHeight());
             transform.mapRect(bounds);
 
@@ -613,7 +620,7 @@
             JNIEnv* env = jnienv();
             jobject localref = env->NewLocalRef(mWeakRef);
             if (CC_UNLIKELY(!localref)) {
-                jnienv()->DeleteWeakGlobalRef(mWeakRef);
+                env->DeleteWeakGlobalRef(mWeakRef);
                 mWeakRef = nullptr;
                 return;
             }
@@ -634,6 +641,32 @@
             return env;
         }
 
+        void handleStretchEffect(const TreeInfo& info, const Matrix4& transform) {
+            // Search up to find the nearest stretcheffect parent
+            const StretchEffect* effect = info.damageAccumulator->findNearestStretchEffect();
+            if (!effect) {
+                return;
+            }
+
+            uirenderer::Rect area = effect->stretchArea;
+            transform.mapRect(area);
+            JNIEnv* env = jnienv();
+
+            jobject localref = env->NewLocalRef(mWeakRef);
+            if (CC_UNLIKELY(!localref)) {
+                env->DeleteWeakGlobalRef(mWeakRef);
+                mWeakRef = nullptr;
+                return;
+            }
+#ifdef __ANDROID__  // Layoutlib does not support CanvasContext
+            env->CallVoidMethod(localref, gPositionListener_ApplyStretchMethod,
+                                info.canvasContext.getFrameNumber(), area.left, area.top,
+                                area.right, area.bottom, effect->stretchDirection.fX,
+                                effect->stretchDirection.fY, effect->maxStretchAmount);
+#endif
+            env->DeleteLocalRef(localref);
+        }
+
         void doUpdatePositionAsync(jlong frameNumber, jint left, jint top,
                 jint right, jint bottom) {
             ATRACE_NAME("Update SurfaceView position");
@@ -775,6 +808,8 @@
     jclass clazz = FindClassOrDie(env, "android/graphics/RenderNode$PositionUpdateListener");
     gPositionListener_PositionChangedMethod = GetMethodIDOrDie(env, clazz,
             "positionChanged", "(JIIII)V");
+    gPositionListener_ApplyStretchMethod =
+            GetMethodIDOrDie(env, clazz, "applyStretch", "(JFFFFFFF)V");
     gPositionListener_PositionLostMethod = GetMethodIDOrDie(env, clazz,
             "positionLost", "(J)V");
     return RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods));
diff --git a/tests/HwAccelerationTest/AndroidManifest.xml b/tests/HwAccelerationTest/AndroidManifest.xml
index c6c67fe..62ccb1a0 100644
--- a/tests/HwAccelerationTest/AndroidManifest.xml
+++ b/tests/HwAccelerationTest/AndroidManifest.xml
@@ -400,6 +400,15 @@
             </intent-filter>
         </activity>
 
+        <activity android:name="StretchySurfaceViewActivity"
+                  android:label="SurfaceView/Stretchy Movement"
+                  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="GetBitmapSurfaceViewActivity"
              android:label="SurfaceView/GetBitmap with Camera source"
              android:exported="true">
diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/PositionListenerActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/PositionListenerActivity.java
index 818d899..65d7363 100644
--- a/tests/HwAccelerationTest/src/com/android/test/hwui/PositionListenerActivity.java
+++ b/tests/HwAccelerationTest/src/com/android/test/hwui/PositionListenerActivity.java
@@ -19,8 +19,13 @@
 import android.app.Activity;
 import android.content.Context;
 import android.graphics.Canvas;
+import android.graphics.PointF;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.RectF;
 import android.graphics.RenderNode;
 import android.os.Bundle;
+import android.view.MotionEvent;
 import android.widget.LinearLayout;
 import android.widget.ProgressBar;
 import android.widget.ScrollView;
@@ -38,7 +43,46 @@
         ProgressBar spinner = new ProgressBar(this, null, android.R.attr.progressBarStyleLarge);
         layout.addView(spinner);
 
-        ScrollView scrollingThing = new ScrollView(this);
+        ScrollView scrollingThing = new ScrollView(this) {
+            int setting = 0;
+            PointF opts[] = new PointF[] {
+                    new PointF(0, 0),
+                    new PointF(0, -1f),
+                    new PointF(1f, 0),
+                    new PointF(0, 1f),
+                    new PointF(-1f, 0),
+                    new PointF(-1f, 1f),
+            };
+            {
+                setWillNotDraw(false);
+            }
+
+            @Override
+            public boolean onTouchEvent(MotionEvent ev) {
+                if (ev.getActionMasked() == MotionEvent.ACTION_UP) {
+                    setting = (setting + 1) % opts.length;
+                    invalidate();
+                }
+                return super.onTouchEvent(ev);
+            }
+
+            @Override
+            protected void onDraw(Canvas canvas) {
+                super.onDraw(canvas);
+                RenderNode node = ((RecordingCanvas) canvas).mNode;
+                PointF dir = opts[setting];
+                float maxStretchAmount = 100f;
+                // Although we could do this in a single call, the real one won't be - so mimic that
+                if (dir.x != 0f) {
+                    node.stretch(0f, 0f, (float) getWidth(), (float) getHeight(),
+                            dir.x, 0f, maxStretchAmount);
+                }
+                if (dir.y != 0f) {
+                    node.stretch(0f, 0f, (float) getWidth(), (float) getHeight(),
+                            0f, dir.y, maxStretchAmount);
+                }
+            }
+        };
         scrollingThing.addView(new MyPositionReporter(this));
         layout.addView(scrollingThing);
 
@@ -49,6 +93,11 @@
         RenderNode mNode;
         int mCurrentCount = 0;
         int mTranslateY = 0;
+        Rect mPosition = new Rect();
+        RectF mStretchArea = new RectF();
+        float mStretchX = 0.0f;
+        float mStretchY = 0.0f;
+        float mStretchMax = 0.0f;
 
         MyPositionReporter(Context c) {
             super(c);
@@ -78,18 +127,36 @@
             canvas.drawRenderNode(mNode);
         }
 
+        void updateText() {
+            setText(String.format("%d: Position %s, stretch area %s, vec %f,%f, amount %f",
+                    mCurrentCount, mPosition.toShortString(), mStretchArea.toShortString(),
+                    mStretchX, mStretchY, mStretchMax));
+        }
+
         @Override
         public void positionChanged(long frameNumber, int left, int top, int right, int bottom) {
-            post(() -> {
+            getHandler().postAtFrontOfQueue(() -> {
                 mCurrentCount++;
-                setText(String.format("%d: Position [%d, %d, %d, %d]", mCurrentCount,
-                        left, top, right, bottom));
+                mPosition.set(left, top, right, bottom);
+                updateText();
+            });
+        }
+
+        @Override
+        public void applyStretch(long frameNumber, float left, float top, float right, float bottom,
+                float vecX, float vecY, float maxStretch) {
+            getHandler().postAtFrontOfQueue(() -> {
+                mStretchArea.set(left, top, right, bottom);
+                mStretchX = vecX;
+                mStretchY = vecY;
+                mStretchMax = maxStretch;
+                updateText();
             });
         }
 
         @Override
         public void positionLost(long frameNumber) {
-            post(() -> {
+            getHandler().postAtFrontOfQueue(() -> {
                 mCurrentCount++;
                 setText(mCurrentCount + " No position");
             });
diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/StretchySurfaceViewActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/StretchySurfaceViewActivity.java
new file mode 100644
index 0000000..d604244
--- /dev/null
+++ b/tests/HwAccelerationTest/src/com/android/test/hwui/StretchySurfaceViewActivity.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 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 com.android.test.hwui;
+
+import android.animation.ObjectAnimator;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RecordingCanvas;
+import android.graphics.RenderNode;
+import android.os.Bundle;
+import android.view.Gravity;
+import android.view.SurfaceHolder;
+import android.view.SurfaceHolder.Callback;
+import android.view.SurfaceView;
+import android.view.animation.LinearInterpolator;
+import android.widget.FrameLayout;
+
+public class StretchySurfaceViewActivity extends Activity implements Callback {
+    SurfaceView mSurfaceView;
+    ObjectAnimator mAnimator;
+
+    class MySurfaceView extends SurfaceView {
+        boolean mSlow;
+        boolean mScaled;
+        int mToggle = 0;
+
+        public MySurfaceView(Context context) {
+            super(context);
+            setOnClickListener(v -> {
+                mToggle = (mToggle + 1) % 4;
+                mSlow = (mToggle & 0x2) != 0;
+                mScaled = (mToggle & 0x1) != 0;
+
+                mSurfaceView.setScaleX(mScaled ? 1.6f : 1f);
+                mSurfaceView.setScaleY(mScaled ? 0.8f : 1f);
+
+                setTitle("Slow=" + mSlow + ", scaled=" + mScaled);
+                invalidate();
+            });
+            setWillNotDraw(false);
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            super.draw(canvas);
+            if (mSlow) {
+                try {
+                    Thread.sleep(16);
+                } catch (InterruptedException e) {}
+            }
+        }
+
+        public void setMyTranslationY(float ty) {
+            setTranslationY(ty);
+            if (mSlow) {
+                invalidate();
+            }
+        }
+
+        public float getMyTranslationY() {
+            return getTranslationY();
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        FrameLayout content = new FrameLayout(this) {
+            {
+                setWillNotDraw(false);
+            }
+
+            @Override
+            protected void onDraw(Canvas canvas) {
+                Paint paint = new Paint();
+                paint.setAntiAlias(true);
+                paint.setColor(Color.RED);
+                paint.setStyle(Paint.Style.STROKE);
+                paint.setStrokeWidth(10f);
+                canvas.drawLine(0f, 0f, getWidth(), getHeight(), paint);
+                super.onDraw(canvas);
+
+                RenderNode node = ((RecordingCanvas) canvas).mNode;
+                node.stretch(0f, 0f, getWidth(), getHeight() / 2f, 0f, 1f, 400f);
+            }
+        };
+
+        mSurfaceView = new MySurfaceView(this);
+        mSurfaceView.getHolder().addCallback(this);
+
+        final float density = getResources().getDisplayMetrics().density;
+        int size = (int) (200 * density);
+
+        content.addView(mSurfaceView, new FrameLayout.LayoutParams(
+                size, size, Gravity.CENTER_HORIZONTAL | Gravity.TOP));
+        mAnimator = ObjectAnimator.ofFloat(mSurfaceView, "myTranslationY",
+                0, size);
+        mAnimator.setRepeatMode(ObjectAnimator.REVERSE);
+        mAnimator.setRepeatCount(ObjectAnimator.INFINITE);
+        mAnimator.setDuration(1000);
+        mAnimator.setInterpolator(new LinearInterpolator());
+        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.WHITE);
+
+        Paint paint = new Paint();
+        paint.setAntiAlias(true);
+        paint.setColor(Color.GREEN);
+        paint.setStyle(Paint.Style.STROKE);
+        paint.setStrokeWidth(10f);
+        canvas.drawLine(0, 0, width, height, paint);
+
+        holder.unlockCanvasAndPost(canvas);
+    }
+
+    @Override
+    public void surfaceDestroyed(SurfaceHolder holder) {
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        mAnimator.start();
+    }
+
+    @Override
+    protected void onPause() {
+        mAnimator.pause();
+        super.onPause();
+    }
+}