add support for rendering lottie animations through a LottieDrawable

This is an initial push that only supports basic playback

Test: frameworks/base/tests/VectorDrawableTest and run LottieDrawable activity
Change-Id: Ic34366b0cd0984a512d8684d476227830903f778
Bug: 257304231
diff --git a/graphics/java/android/graphics/drawable/LottieDrawable.java b/graphics/java/android/graphics/drawable/LottieDrawable.java
new file mode 100644
index 0000000..c1f1b50
--- /dev/null
+++ b/graphics/java/android/graphics/drawable/LottieDrawable.java
@@ -0,0 +1,151 @@
+/*
+ * 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.graphics.drawable;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.PixelFormat;
+
+import dalvik.annotation.optimization.FastNative;
+
+import libcore.util.NativeAllocationRegistry;
+
+import java.io.IOException;
+
+/**
+ * {@link Drawable} for drawing Lottie files.
+ *
+ * <p>The framework handles decoding subsequent frames in another thread and
+ * updating when necessary. The drawable will only animate while it is being
+ * displayed.</p>
+ *
+ * @hide
+ */
+@SuppressLint("NotCloseable")
+public class LottieDrawable extends Drawable implements Animatable {
+    private long mNativePtr;
+
+    /**
+     * Create an animation from the provided JSON string
+     * @hide
+     */
+    private LottieDrawable(@NonNull String animationJson) throws IOException {
+        mNativePtr = nCreate(animationJson);
+        if (mNativePtr == 0) {
+            throw new IOException("could not make LottieDrawable from json");
+        }
+
+        final long nativeSize = nNativeByteSize(mNativePtr);
+        NativeAllocationRegistry registry = new NativeAllocationRegistry(
+                LottieDrawable.class.getClassLoader(), nGetNativeFinalizer(), nativeSize);
+        registry.registerNativeAllocation(this, mNativePtr);
+    }
+
+    /**
+     * Factory for LottieDrawable, throws IOException if native Skottie Builder fails to parse
+     */
+    public static LottieDrawable makeLottieDrawable(@NonNull String animationJson)
+            throws IOException {
+        return new LottieDrawable(animationJson);
+    }
+
+
+
+    /**
+     * Draw the current frame to the Canvas.
+     * @hide
+     */
+    @Override
+    public void draw(@NonNull Canvas canvas) {
+        if (mNativePtr == 0) {
+            throw new IllegalStateException("called draw on empty LottieDrawable");
+        }
+
+        nDraw(mNativePtr, canvas.getNativeCanvasWrapper());
+    }
+
+    /**
+     * Start the animation. Needs to be called before draw calls.
+     * @hide
+     */
+    @Override
+    public void start() {
+        if (mNativePtr == 0) {
+            throw new IllegalStateException("called start on empty LottieDrawable");
+        }
+
+        if (nStart(mNativePtr)) {
+            invalidateSelf();
+        }
+    }
+
+    /**
+     * Stops the animation playback. Does not release anything.
+     * @hide
+     */
+    @Override
+    public void stop() {
+        if (mNativePtr == 0) {
+            throw new IllegalStateException("called stop on empty LottieDrawable");
+        }
+        nStop(mNativePtr);
+    }
+
+    /**
+     *  Return whether the animation is currently running.
+     */
+    @Override
+    public boolean isRunning() {
+        if (mNativePtr == 0) {
+            throw new IllegalStateException("called isRunning on empty LottieDrawable");
+        }
+        return nIsRunning(mNativePtr);
+    }
+
+    @Override
+    public int getOpacity() {
+        // We assume translucency to avoid checking each pixel.
+        return PixelFormat.TRANSLUCENT;
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        //TODO
+    }
+
+    @Override
+    public void setColorFilter(@Nullable ColorFilter colorFilter) {
+        //TODO
+    }
+
+    private static native long nCreate(String json);
+    private static native void nDraw(long nativeInstance, long nativeCanvas);
+    @FastNative
+    private static native long nGetNativeFinalizer();
+    @FastNative
+    private static native long nNativeByteSize(long nativeInstance);
+    @FastNative
+    private static native boolean nIsRunning(long nativeInstance);
+    @FastNative
+    private static native boolean nStart(long nativeInstance);
+    @FastNative
+    private static native boolean nStop(long nativeInstance);
+
+}
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index 3e3d77b..9c4ea0e 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -78,6 +78,7 @@
                 "external/skia/src/utils",
                 "external/skia/src/gpu",
                 "external/skia/src/shaders",
+                "external/skia/modules/skottie",
             ],
         },
         host: {
@@ -374,6 +375,7 @@
         "external/skia/src/effects",
         "external/skia/src/image",
         "external/skia/src/images",
+        "external/skia/modules/skottie",
     ],
 
     shared_libs: [
@@ -400,6 +402,7 @@
                 "jni/BitmapRegionDecoder.cpp",
                 "jni/GIFMovie.cpp",
                 "jni/GraphicsStatsService.cpp",
+                "jni/LottieDrawable.cpp",
                 "jni/Movie.cpp",
                 "jni/MovieImpl.cpp",
                 "jni/pdf/PdfDocument.cpp",
@@ -507,6 +510,7 @@
         "hwui/BlurDrawLooper.cpp",
         "hwui/Canvas.cpp",
         "hwui/ImageDecoder.cpp",
+        "hwui/LottieDrawable.cpp",
         "hwui/MinikinSkia.cpp",
         "hwui/MinikinUtils.cpp",
         "hwui/PaintImpl.cpp",
diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp
index d83d78f..a1c4b49 100644
--- a/libs/hwui/SkiaCanvas.cpp
+++ b/libs/hwui/SkiaCanvas.cpp
@@ -736,6 +736,10 @@
     return imgDrawable->drawStaging(mCanvas);
 }
 
+void SkiaCanvas::drawLottie(LottieDrawable* lottieDrawable) {
+    lottieDrawable->drawStaging(mCanvas);
+}
+
 void SkiaCanvas::drawVectorDrawable(VectorDrawableRoot* vectorDrawable) {
     vectorDrawable->drawStaging(this);
 }
diff --git a/libs/hwui/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h
index 31e3b4c..fd8b6cd 100644
--- a/libs/hwui/SkiaCanvas.h
+++ b/libs/hwui/SkiaCanvas.h
@@ -145,6 +145,7 @@
                                float dstTop, float dstRight, float dstBottom,
                                const Paint* paint) override;
     virtual double drawAnimatedImage(AnimatedImageDrawable* imgDrawable) override;
+    virtual void drawLottie(LottieDrawable* lottieDrawable) override;
 
     virtual void drawVectorDrawable(VectorDrawableRoot* vectorDrawable) override;
 
diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp
index e6cfa7b..5a96174 100644
--- a/libs/hwui/apex/jni_runtime.cpp
+++ b/libs/hwui/apex/jni_runtime.cpp
@@ -37,6 +37,7 @@
 extern int register_android_graphics_Graphics(JNIEnv* env);
 extern int register_android_graphics_ImageDecoder(JNIEnv*);
 extern int register_android_graphics_drawable_AnimatedImageDrawable(JNIEnv*);
+extern int register_android_graphics_drawable_LottieDrawable(JNIEnv*);
 extern int register_android_graphics_Interpolator(JNIEnv* env);
 extern int register_android_graphics_MaskFilter(JNIEnv* env);
 extern int register_android_graphics_Movie(JNIEnv* env);
@@ -116,6 +117,7 @@
             REG_JNI(register_android_graphics_HardwareRendererObserver),
             REG_JNI(register_android_graphics_ImageDecoder),
             REG_JNI(register_android_graphics_drawable_AnimatedImageDrawable),
+            REG_JNI(register_android_graphics_drawable_LottieDrawable),
             REG_JNI(register_android_graphics_Interpolator),
             REG_JNI(register_android_graphics_MaskFilter),
             REG_JNI(register_android_graphics_Matrix),
diff --git a/libs/hwui/hwui/Canvas.h b/libs/hwui/hwui/Canvas.h
index 2a20191..07e2fe2 100644
--- a/libs/hwui/hwui/Canvas.h
+++ b/libs/hwui/hwui/Canvas.h
@@ -60,6 +60,7 @@
 typedef std::function<void(uint16_t* text, float* positions)> ReadGlyphFunc;
 
 class AnimatedImageDrawable;
+class LottieDrawable;
 class Bitmap;
 class Paint;
 struct Typeface;
@@ -242,6 +243,7 @@
                                const Paint* paint) = 0;
 
     virtual double drawAnimatedImage(AnimatedImageDrawable* imgDrawable) = 0;
+    virtual void drawLottie(LottieDrawable* lottieDrawable) = 0;
     virtual void drawPicture(const SkPicture& picture) = 0;
 
     /**
diff --git a/libs/hwui/hwui/LottieDrawable.cpp b/libs/hwui/hwui/LottieDrawable.cpp
new file mode 100644
index 0000000..92dc51e
--- /dev/null
+++ b/libs/hwui/hwui/LottieDrawable.cpp
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+
+#include "LottieDrawable.h"
+
+#include <SkTime.h>
+#include <log/log.h>
+#include <pipeline/skia/SkiaUtils.h>
+
+namespace android {
+
+sk_sp<LottieDrawable> LottieDrawable::Make(sk_sp<skottie::Animation> animation, size_t bytesUsed) {
+    if (animation) {
+        return sk_sp<LottieDrawable>(new LottieDrawable(std::move(animation), bytesUsed));
+    }
+    return nullptr;
+}
+LottieDrawable::LottieDrawable(sk_sp<skottie::Animation> animation, size_t bytesUsed)
+        : mAnimation(std::move(animation)), mBytesUsed(bytesUsed) {}
+
+bool LottieDrawable::start() {
+    if (mRunning) {
+        return false;
+    }
+
+    mRunning = true;
+    return true;
+}
+
+bool LottieDrawable::stop() {
+    bool wasRunning = mRunning;
+    mRunning = false;
+    return wasRunning;
+}
+
+bool LottieDrawable::isRunning() {
+    return mRunning;
+}
+
+// TODO: Check to see if drawable is actually dirty
+bool LottieDrawable::isDirty() {
+    return true;
+}
+
+void LottieDrawable::onDraw(SkCanvas* canvas) {
+    if (mRunning) {
+        const nsecs_t currentTime = systemTime(SYSTEM_TIME_MONOTONIC);
+
+        nsecs_t t = 0;
+        if (mStartTime == 0) {
+            mStartTime = currentTime;
+        } else {
+            t = currentTime - mStartTime;
+        }
+        double seekTime = std::fmod((double)t * 1e-9, mAnimation->duration());
+        mAnimation->seekFrameTime(seekTime);
+        mAnimation->render(canvas);
+    }
+}
+
+void LottieDrawable::drawStaging(SkCanvas* canvas) {
+    onDraw(canvas);
+}
+
+SkRect LottieDrawable::onGetBounds() {
+    // We do not actually know the bounds, so give a conservative answer.
+    return SkRectMakeLargest();
+}
+
+}  // namespace android
diff --git a/libs/hwui/hwui/LottieDrawable.h b/libs/hwui/hwui/LottieDrawable.h
new file mode 100644
index 0000000..9cc34bf
--- /dev/null
+++ b/libs/hwui/hwui/LottieDrawable.h
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <SkDrawable.h>
+#include <Skottie.h>
+#include <utils/Timers.h>
+
+class SkCanvas;
+
+namespace android {
+
+/**
+ * Native component of android.graphics.drawable.LottieDrawable.java.
+ * This class can be drawn into Canvas.h and maintains the state needed to drive
+ * the animation from the RenderThread.
+ */
+class LottieDrawable : public SkDrawable {
+public:
+    static sk_sp<LottieDrawable> Make(sk_sp<skottie::Animation> animation, size_t bytes);
+
+    // Draw to software canvas
+    void drawStaging(SkCanvas* canvas);
+
+    // Returns true if the animation was started; false otherwise (e.g. it was
+    // already running)
+    bool start();
+    // Returns true if the animation was stopped; false otherwise (e.g. it was
+    // already stopped)
+    bool stop();
+    bool isRunning();
+
+    // TODO: Is dirty should take in a time til next frame to determine if it is dirty
+    bool isDirty();
+
+    SkRect onGetBounds() override;
+
+    size_t byteSize() const { return sizeof(*this) + mBytesUsed; }
+
+protected:
+    void onDraw(SkCanvas* canvas) override;
+
+private:
+    LottieDrawable(sk_sp<skottie::Animation> animation, size_t bytes_used);
+
+    sk_sp<skottie::Animation> mAnimation;
+    bool mRunning = false;
+    // The start time for the drawable itself.
+    nsecs_t mStartTime = 0;
+    const size_t mBytesUsed = 0;
+};
+
+}  // namespace android
diff --git a/libs/hwui/jni/LottieDrawable.cpp b/libs/hwui/jni/LottieDrawable.cpp
new file mode 100644
index 0000000..fb6eede
--- /dev/null
+++ b/libs/hwui/jni/LottieDrawable.cpp
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ */
+
+#include <SkRect.h>
+#include <Skottie.h>
+#include <hwui/Canvas.h>
+#include <hwui/LottieDrawable.h>
+
+#include "GraphicsJNI.h"
+#include "Utils.h"
+
+using namespace android;
+
+static jclass gLottieDrawableClass;
+
+static jlong LottieDrawable_nCreate(JNIEnv* env, jobject, jstring jjson) {
+    const ScopedUtfChars cstr(env, jjson);
+    // TODO(b/259267150) provide more accurate byteSize
+    size_t bytes = strlen(cstr.c_str());
+    auto animation = skottie::Animation::Builder().make(cstr.c_str(), bytes);
+    sk_sp<LottieDrawable> drawable(LottieDrawable::Make(std::move(animation), bytes));
+    if (!drawable) {
+        return 0;
+    }
+    return reinterpret_cast<jlong>(drawable.release());
+}
+
+static void LottieDrawable_destruct(LottieDrawable* drawable) {
+    SkSafeUnref(drawable);
+}
+
+static jlong LottieDrawable_nGetNativeFinalizer(JNIEnv* /*env*/, jobject /*clazz*/) {
+    return static_cast<jlong>(reinterpret_cast<uintptr_t>(&LottieDrawable_destruct));
+}
+
+static void LottieDrawable_nDraw(JNIEnv* env, jobject /*clazz*/, jlong nativePtr, jlong canvasPtr) {
+    auto* drawable = reinterpret_cast<LottieDrawable*>(nativePtr);
+    auto* canvas = reinterpret_cast<Canvas*>(canvasPtr);
+    canvas->drawLottie(drawable);
+}
+
+static jboolean LottieDrawable_nIsRunning(JNIEnv* env, jobject /*clazz*/, jlong nativePtr) {
+    auto* drawable = reinterpret_cast<LottieDrawable*>(nativePtr);
+    return drawable->isRunning();
+}
+
+static jboolean LottieDrawable_nStart(JNIEnv* env, jobject /*clazz*/, jlong nativePtr) {
+    auto* drawable = reinterpret_cast<LottieDrawable*>(nativePtr);
+    return drawable->start();
+}
+
+static jboolean LottieDrawable_nStop(JNIEnv* env, jobject /*clazz*/, jlong nativePtr) {
+    auto* drawable = reinterpret_cast<LottieDrawable*>(nativePtr);
+    return drawable->stop();
+}
+
+static jlong LottieDrawable_nNativeByteSize(JNIEnv* env, jobject /*clazz*/, jlong nativePtr) {
+    auto* drawable = reinterpret_cast<LottieDrawable*>(nativePtr);
+    return drawable->byteSize();
+}
+
+static const JNINativeMethod gLottieDrawableMethods[] = {
+        {"nCreate", "(Ljava/lang/String;)J", (void*)LottieDrawable_nCreate},
+        {"nNativeByteSize", "(J)J", (void*)LottieDrawable_nNativeByteSize},
+        {"nGetNativeFinalizer", "()J", (void*)LottieDrawable_nGetNativeFinalizer},
+        {"nDraw", "(JJ)V", (void*)LottieDrawable_nDraw},
+        {"nIsRunning", "(J)Z", (void*)LottieDrawable_nIsRunning},
+        {"nStart", "(J)Z", (void*)LottieDrawable_nStart},
+        {"nStop", "(J)Z", (void*)LottieDrawable_nStop},
+};
+
+int register_android_graphics_drawable_LottieDrawable(JNIEnv* env) {
+    gLottieDrawableClass = reinterpret_cast<jclass>(
+            env->NewGlobalRef(FindClassOrDie(env, "android/graphics/drawable/LottieDrawable")));
+
+    return android::RegisterMethodsOrDie(env, "android/graphics/drawable/LottieDrawable",
+                                         gLottieDrawableMethods, NELEM(gLottieDrawableMethods));
+}
diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp
index fcfc4f8..f0dc5eb 100644
--- a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp
+++ b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp
@@ -146,6 +146,16 @@
         }
     }
 
+    for (auto& lottie : mLotties) {
+        // If any animated image in the display list needs updated, then damage the node.
+        if (lottie->isDirty()) {
+            isDirty = true;
+        }
+        if (lottie->isRunning()) {
+            info.out.hasAnimations = true;
+        }
+    }
+
     for (auto& [vectorDrawable, cachedMatrix] : mVectorDrawables) {
         // If any vector drawable in the display list needs update, damage the node.
         if (vectorDrawable->isDirty()) {
diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.h b/libs/hwui/pipeline/skia/SkiaDisplayList.h
index 2a67734..39217fc 100644
--- a/libs/hwui/pipeline/skia/SkiaDisplayList.h
+++ b/libs/hwui/pipeline/skia/SkiaDisplayList.h
@@ -22,6 +22,7 @@
 #include "RenderNodeDrawable.h"
 #include "TreeInfo.h"
 #include "hwui/AnimatedImageDrawable.h"
+#include "hwui/LottieDrawable.h"
 #include "utils/LinearAllocator.h"
 #include "utils/Pair.h"
 
@@ -186,6 +187,8 @@
         return mHasHolePunches;
     }
 
+    // TODO(b/257304231): create common base class for Lotties and AnimatedImages
+    std::vector<LottieDrawable*> mLotties;
     std::vector<AnimatedImageDrawable*> mAnimatedImages;
     DisplayListData mDisplayList;
 
diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
index 1f87865..db449d6 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
@@ -188,6 +188,11 @@
 #endif
 }
 
+void SkiaRecordingCanvas::drawLottie(LottieDrawable* lottie) {
+    drawDrawable(lottie);
+    mDisplayList->mLotties.push_back(lottie);
+}
+
 void SkiaRecordingCanvas::drawVectorDrawable(VectorDrawableRoot* tree) {
     mRecorder.drawVectorDrawable(tree);
     SkMatrix mat;
diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
index 7844e2c..c823d8d 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
@@ -78,6 +78,7 @@
                             uirenderer::CanvasPropertyPaint* paint) override;
     virtual void drawRipple(const RippleDrawableParams& params) override;
 
+    virtual void drawLottie(LottieDrawable* lottieDrawable) override;
     virtual void drawVectorDrawable(VectorDrawableRoot* vectorDrawable) override;
 
     virtual void enableZ(bool enableZ) override;
diff --git a/tests/VectorDrawableTest/Android.bp b/tests/VectorDrawableTest/Android.bp
index 9da7c5f..099d874 100644
--- a/tests/VectorDrawableTest/Android.bp
+++ b/tests/VectorDrawableTest/Android.bp
@@ -26,5 +26,7 @@
 android_test {
     name: "VectorDrawableTest",
     srcs: ["**/*.java"],
+    // certificate set as platform to allow testing of @hidden APIs
+    certificate: "platform",
     platform_apis: true,
 }
diff --git a/tests/VectorDrawableTest/AndroidManifest.xml b/tests/VectorDrawableTest/AndroidManifest.xml
index 5334dac..163e438 100644
--- a/tests/VectorDrawableTest/AndroidManifest.xml
+++ b/tests/VectorDrawableTest/AndroidManifest.xml
@@ -158,6 +158,15 @@
                 <category android:name="com.android.test.dynamic.TEST"/>
             </intent-filter>
         </activity>
+        <activity android:name="LottieDrawableTest"
+             android:label="Lottie test bed"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="com.android.test.dynamic.TEST" />
+            </intent-filter>
+        </activity>
     </application>
 
 </manifest>
diff --git a/tests/VectorDrawableTest/res/raw/lottie.json b/tests/VectorDrawableTest/res/raw/lottie.json
new file mode 100644
index 0000000..fea571c
--- /dev/null
+++ b/tests/VectorDrawableTest/res/raw/lottie.json
@@ -0,0 +1,123 @@
+{
+    "v":"4.6.9",
+    "fr":60,
+    "ip":0,
+    "op":200,
+    "w":800,
+    "h":600,
+    "nm":"Loader 1 JSON",
+    "ddd":0,
+
+
+    "layers":[
+       {
+          "ddd":0,
+          "ind":1,
+          "ty":4,
+          "nm":"Custom Path 1",
+          "ao": 0,
+          "ip": 0,
+          "op": 300,
+          "st": 0,
+          "sr": 1,
+          "bm": 0,
+          "ks": {
+             "o": { "a":0, "k":100 },
+             "r": { "a":1, "k": [
+               { "s": [ 0 ], "e": [ 360], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 0 },
+           { "t": 200 }
+         ] },
+             "p": { "a":0, "k":[ 300, 300, 0 ] },
+             "a": { "a":0, "k":[ 100, 100, 0 ] },
+             "s": { "a":1, "k":[
+               { "s": [ 100, 100 ], "e": [ 200, 200 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 0 },
+               { "s": [ 200, 200 ], "e": [ 100, 100 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 100 },
+           { "t": 200 }
+             ] }
+          },
+
+          "shapes":[
+            {
+              "ty":"gr",
+              "it":[
+                {
+                  "ty" : "sh",
+                  "nm" : "Path 1",
+                  "ks" : {
+                    "a" : 1,
+                    "k" : [
+                      {
+                        "s": [ {
+                          "i": [ [   0,  50 ], [ -50,   0 ], [   0, -50 ], [  50,   0 ] ],
+                          "o": [ [   0, -50 ], [  50,   0 ], [   0,  50 ], [ -50,   0 ] ],
+                          "v": [ [   0, 100 ], [ 100,   0 ], [ 200, 100 ], [ 100, 200 ] ],
+                          "c": true
+                        } ],
+                        "e": [ {
+                          "i": [ [  50,  50 ], [ -50,   0 ], [ -50, -50 ], [  50,  50 ] ],
+                          "o": [ [  50, -50 ], [  50,   0 ], [ -50,  50 ], [ -50,  50 ] ],
+                          "v": [ [   0, 100 ], [ 100,   0 ], [ 200, 100 ], [ 100, 200 ] ],
+                          "c": true
+                        } ],
+                        "i": { "x":0.5, "y":0.5 },
+                        "o": { "x":0.5, "y":0.5 },
+                        "t": 0
+                      },
+                      {
+                        "s": [ {
+                          "i": [ [  50,  50 ], [ -50,   0 ], [ -50, -50 ], [  50,  50 ] ],
+                          "o": [ [  50, -50 ], [  50,   0 ], [ -50,  50 ], [ -50,  50 ] ],
+                          "v": [ [   0, 100 ], [ 100,   0 ], [ 200, 100 ], [ 100, 200 ] ],
+                          "c": true
+                        } ],
+                        "e": [ {
+                          "i": [ [   0,  50 ], [ -50,   0 ], [   0, -50 ], [  50,   0 ] ],
+                          "o": [ [   0, -50 ], [  50,   0 ], [   0,  50 ], [ -50,   0 ] ],
+                          "v": [ [   0, 100 ], [ 100,   0 ], [ 200, 100 ], [ 100, 200 ] ],
+                          "c": true
+                        } ],
+                        "i": { "x":0.5, "y":0.5 },
+                        "o": { "x":0.5, "y":0.5 },
+                        "t": 100
+                      },
+                      {
+                        "t": 200
+                      }
+                    ]
+                  }
+                },
+
+                {
+                  "ty": "st",
+                  "nm": "Stroke 1",
+                  "lc": 1,
+                  "lj": 1,
+                  "ml": 4,
+                  "w" : { "a": 1, "k": [
+                    { "s": [ 30 ], "e": [ 50 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 0 },
+                    { "s": [ 50 ], "e": [ 30 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 100 },
+            { "t": 200 }
+                  ] },
+                  "o" : { "a": 0, "k": 100 },
+                  "c" : { "a": 1, "k": [
+                    { "s": [ 0, 1, 0 ], "e": [ 1, 0, 0 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 0   },
+                    { "s": [ 1, 0, 0 ], "e": [ 0, 1, 0 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 100 },
+                    { "t": 200 }
+                  ] }
+                },
+
+                {
+                  "ty":"tr",
+                  "p" : { "a":0, "k":[   0,   0 ] },
+                  "a" : { "a":0, "k":[   0,   0 ] },
+                  "s" : { "a":0, "k":[ 100, 100 ] },
+                  "r" : { "a":0, "k":  0 },
+                  "o" : { "a":0, "k":100 },
+                  "nm": "Transform"
+                }
+              ]
+            }
+          ]
+       }
+    ]
+ }
diff --git a/tests/VectorDrawableTest/src/com/android/test/dynamic/LottieDrawableTest.java b/tests/VectorDrawableTest/src/com/android/test/dynamic/LottieDrawableTest.java
new file mode 100644
index 0000000..05eae7b
--- /dev/null
+++ b/tests/VectorDrawableTest/src/com/android/test/dynamic/LottieDrawableTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.dynamic;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.LottieDrawable;
+import android.os.Bundle;
+import android.view.View;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Scanner;
+
+@SuppressWarnings({"UnusedDeclaration"})
+public class LottieDrawableTest extends Activity {
+    private static final String TAG = "LottieDrawableTest";
+    static final int BACKGROUND = 0xFFF44336;
+
+    class LottieDrawableView extends View {
+        private Rect mLottieBounds;
+
+        private LottieDrawable mLottie;
+
+        LottieDrawableView(Context context, InputStream is) {
+            super(context);
+            Scanner s = new Scanner(is).useDelimiter("\\A");
+            String json = s.hasNext() ? s.next() : "";
+            try {
+                mLottie = LottieDrawable.makeLottieDrawable(json);
+            } catch (IOException e) {
+                throw new RuntimeException(TAG + ": error parsing test Lottie");
+            }
+            mLottie.start();
+        }
+
+        @Override
+        protected void onDraw(Canvas canvas) {
+            canvas.drawColor(BACKGROUND);
+
+            mLottie.setBounds(mLottieBounds);
+            mLottie.draw(canvas);
+        }
+
+        public void setLottieSize(Rect bounds) {
+            mLottieBounds = bounds;
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        InputStream is = getResources().openRawResource(R.raw.lottie);
+
+        LottieDrawableView view = new LottieDrawableView(this, is);
+        view.setLottieSize(new Rect(0, 0, 900, 900));
+        setContentView(view);
+    }
+}