Support RenderNode on M-O devices.

Bug: 162061312

To avoid reflection, a new stub library was created to
support RenderNode on older devices. This should also improve
allocation performance as RenderNodes should be much smaller
and easier to allocate than Views.

This does not currently work on P devices because ART
does not allow access to APIs in the unsupported list on P.

TextInColumnBenchmark#first_draw is around 30% faster in
my local tests.

This CL will make benchmark performance slower on M-O devices,
but it is more representative of actual performance. It turns
out that PictureRecordingCanvas has faster access than
DisplayListCanvas and was giving artificially fast results
on devices that don't support hardware accelerated canvas.

Test: demo on API 23, 26, 29
Test: TextInColumnBenchmark#first_draw
Test: New test for fallback for M RenderNode to View

Change-Id: Ia10caea550f328368c4f0f4384267058d1dbc541
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index 5d16bbd..e5ed981 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -43,6 +43,9 @@
 
         androidMain.dependencies {
             api "androidx.activity:activity:1.2.0-alpha02"
+            // This has stub APIs for access to legacy Android APIs, so we don't want
+            // any dependency on this module.
+            compileOnly project(":compose:ui:ui-android-stubs")
             implementation(ANDROIDX_TEST_RULES)
         }
 
diff --git a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.kt b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.kt
index e16be09..6f6c560 100644
--- a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.kt
+++ b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.kt
@@ -24,6 +24,7 @@
 import android.graphics.RenderNode
 import android.os.Build
 import android.util.DisplayMetrics
+import android.view.DisplayListCanvas
 import android.view.View
 import android.view.ViewGroup
 import android.widget.ImageView
@@ -69,11 +70,20 @@
     override var didLastRecomposeHaveChanges = false
         private set
 
-    private val supportsRenderNode = Build.VERSION.SDK_INT >= 29
+    private val supportsRenderNode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
+    private val supportsMRenderNode = Build.VERSION.SDK_INT < Build.VERSION_CODES.P &&
+            Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
 
     private val screenWithSpec: Int
     private val screenHeightSpec: Int
-    private val capture = if (supportsRenderNode) RenderNodeCapture() else PictureCapture()
+    private val capture = if (supportsRenderNode) {
+        RenderNodeCapture()
+    } else if (supportsMRenderNode) {
+        MRenderNodeCapture()
+    } else {
+        PictureCapture()
+    }
+
     private var canvas: Canvas? = null
 
     private class AutoFrameClock(
@@ -358,3 +368,20 @@
         picture.endRecording()
     }
 }
+
+private class MRenderNodeCapture : DrawCapture {
+    private var renderNode = android.view.RenderNode.create("Test", null)
+
+    private var canvas: DisplayListCanvas? = null
+
+    override fun beginRecording(width: Int, height: Int): Canvas {
+        renderNode.setLeftTopRightBottom(0, 0, width, height)
+        canvas = renderNode.start(width, height)
+        return canvas!!
+    }
+
+    override fun endRecording() {
+        renderNode.end(canvas!!)
+        canvas = null
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-android-stubs/build.gradle b/compose/ui/ui-android-stubs/build.gradle
new file mode 100644
index 0000000..41fcc7f
--- /dev/null
+++ b/compose/ui/ui-android-stubs/build.gradle
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2020 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.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.Publish
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+}
+
+android {
+    compileOptions {
+        targetCompatibility = JavaVersion.VERSION_1_7
+        sourceCompatibility = JavaVersion.VERSION_1_7
+    }
+}
+
+dependencies {
+    api("androidx.annotation:annotation:1.1.0")
+}
+
+androidx {
+    name = "Compose Android Stubs"
+    publish = Publish.NONE
+    mavenVersion = LibraryVersions.COMPOSE
+    mavenGroup = LibraryGroups.Compose.UI
+    inceptionYear = "2020"
+    description = "Stubs for classes in older Android APIs"
+}
diff --git a/compose/ui/ui-android-stubs/src/main/AndroidManifest.xml b/compose/ui/ui-android-stubs/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0abab77
--- /dev/null
+++ b/compose/ui/ui-android-stubs/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest package="androidx.compose.ui.androidstubs"/>
diff --git a/compose/ui/ui-android-stubs/src/main/java/android/view/DisplayListCanvas.java b/compose/ui/ui-android-stubs/src/main/java/android/view/DisplayListCanvas.java
new file mode 100644
index 0000000..da37905
--- /dev/null
+++ b/compose/ui/ui-android-stubs/src/main/java/android/view/DisplayListCanvas.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2020 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.view;
+
+import android.graphics.Canvas;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Stubs for DisplayListCanvas on M-P devices.
+ */
+public final class DisplayListCanvas extends Canvas {
+    /** stub */
+    public void drawRenderNode(@NonNull RenderNode renderNode) {
+    }
+}
diff --git a/compose/ui/ui-android-stubs/src/main/java/android/view/RenderNode.java b/compose/ui/ui-android-stubs/src/main/java/android/view/RenderNode.java
new file mode 100644
index 0000000..f42f5c8
--- /dev/null
+++ b/compose/ui/ui-android-stubs/src/main/java/android/view/RenderNode.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright 2020 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.view;
+
+import android.annotation.SuppressLint;
+import android.graphics.Matrix;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.Rect;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Stubs for RenderNode on M-P devices.
+ */
+public class RenderNode {
+    @SuppressWarnings("UnusedVariable")
+    private RenderNode(String name, View owningView) {
+    }
+
+    /** stub */
+    public void destroy() {
+    }
+
+    /** stub */
+    public static @NonNull RenderNode create(@Nullable String name, @Nullable View owningView) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public @NonNull DisplayListCanvas start(int width, int height) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public void end(@NonNull DisplayListCanvas canvas) {
+    }
+
+    /** stub */
+    public void discardDisplayList() {
+    }
+
+    /** stub */
+    public boolean isValid() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean hasIdentityMatrix() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public void getMatrix(@NonNull Matrix outMatrix) {
+    }
+
+    /** stub */
+    public void getInverseMatrix(@NonNull Matrix outMatrix) {
+    }
+
+    /** stub */
+    public boolean setLayerType(int layerType) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setLayerPaint(@Nullable Paint paint) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setClipBounds(@Nullable Rect rect) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setClipToBounds(boolean clipToBounds) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setProjectBackwards(boolean shouldProject) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setProjectionReceiver(boolean shouldReceive) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setOutline(@Nullable Outline outline) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean hasShadow() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setClipToOutline(boolean clipToOutline) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean getClipToOutline() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setRevealClip(boolean shouldClip,
+            float x, float y, float radius) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setStaticMatrix(@NonNull Matrix matrix) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setAnimationMatrix(@NonNull Matrix matrix) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setAlpha(float alpha) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public float getAlpha() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setHasOverlappingRendering(boolean hasOverlappingRendering) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    @SuppressLint("KotlinPropertyAccess")
+    public boolean hasOverlappingRendering() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setElevation(float lift) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public float getElevation() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setTranslationX(float translationX) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public float getTranslationX() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setTranslationY(float translationY) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public float getTranslationY() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setTranslationZ(float translationZ) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public float getTranslationZ() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setRotation(float rotation) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public float getRotation() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setRotationX(float rotationX) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public float getRotationX() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setRotationY(float rotationY) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public float getRotationY() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setScaleX(float scaleX) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public float getScaleX() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setScaleY(float scaleY) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public float getScaleY() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setPivotX(float pivotX) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public float getPivotX() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setPivotY(float pivotY) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public float getPivotY() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean isPivotExplicitlySet() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setCameraDistance(float distance) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public float getCameraDistance() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setLeft(int left) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setTop(int top) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setRight(int right) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setBottom(int bottom) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean setLeftTopRightBottom(int left, int top, int right, int bottom) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean offsetLeftAndRight(int offset) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public boolean offsetTopAndBottom(int offset) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** stub */
+    public void output() {
+    }
+
+    /** stub */
+    public boolean isAttached() {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index 49a4fc7..232a3c7 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -55,6 +55,9 @@
 
         androidMain.dependencies {
             implementation(KOTLIN_STDLIB)
+            // This has stub APIs for access to legacy Android APIs, so we don't want
+            // any dependency on this module.
+            compileOnly project(":compose:ui:ui-android-stubs")
             api "androidx.annotation:annotation:1.1.0"
             api "androidx.activity:activity:1.2.0-alpha02"
             implementation "androidx.autofill:autofill:1.0.0"
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
index 2cdf816..99b0fae 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
@@ -63,6 +63,7 @@
 import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
 import androidx.compose.ui.platform.DensityAmbient
 import androidx.compose.ui.platform.LayoutDirectionAmbient
+import androidx.compose.ui.platform.RenderNodeApi23
 import androidx.compose.ui.platform.setContent
 import androidx.compose.ui.test.TestActivity
 import androidx.compose.ui.unit.Constraints
@@ -130,6 +131,24 @@
         validateSquareColors(outerColor = yellow, innerColor = red, size = 10)
     }
 
+    // Tests that the fail-over for M RenderNode support works. Note that this would work with M
+    // and above except that our snapshots only work with O and above.
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O, maxSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun simpleDrawTestLegacyFallback() {
+        try {
+            RenderNodeApi23.testFailCreateRenderNode = true
+            val yellow = Color(0xFFFFFF00)
+            val red = Color(0xFF800000)
+            val model = SquareModel(outerColor = yellow, innerColor = red, size = 10)
+            composeSquares(model)
+
+            validateSquareColors(outerColor = yellow, innerColor = red, size = 10)
+        } finally {
+            RenderNodeApi23.testFailCreateRenderNode = false
+        }
+    }
+
     // Tests that simple drawing works with draw with nested children
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     @Test
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
index 4f622b2..9701987 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
@@ -405,6 +405,10 @@
 
     private val tmpPositionArray = intArrayOf(0, 0)
 
+    // Used to track whether or not there was an exception while creating an MRenderNode
+    // so that we don't have to continue using try/catch after fails once.
+    private var isRenderNodeCompatible = true
+
     private fun dispatchOnPositioned() {
         var positionChanged = false
         getLocationOnScreen(tmpPositionArray)
@@ -437,20 +441,41 @@
         drawBlock: (Canvas) -> Unit,
         invalidateParentLayer: () -> Unit
     ): OwnedLayer {
-        val layer = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || isInEditMode) {
-            ViewLayer(
-                this, viewLayersContainer, drawLayerModifier, drawBlock,
-                invalidateParentLayer
-            )
-        } else {
-            RenderNodeLayer(this, drawLayerModifier, drawBlock, invalidateParentLayer)
-        }
-
+        val layer = instantiateLayer(drawLayerModifier, drawBlock, invalidateParentLayer)
         updateLayerProperties(layer)
-
         return layer
     }
 
+    private fun instantiateLayer(
+        drawLayerModifier: DrawLayerModifier,
+        drawBlock: (Canvas) -> Unit,
+        invalidateParentLayer: () -> Unit
+    ): OwnedLayer {
+        // RenderNode is supported on Q+ for certain, but may also be supported on M-O.
+        // We can't be confident that RenderNode is supported, so we try and fail over to
+        // the ViewLayer implementation. We'll try even on on P devices, but it will fail
+        // until ART allows things on the unsupported list on P.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && isRenderNodeCompatible) {
+            try {
+                return RenderNodeLayer(
+                    this,
+                    drawLayerModifier,
+                    drawBlock,
+                    invalidateParentLayer
+                )
+            } catch (_: Throwable) {
+                isRenderNodeCompatible = false
+            }
+        }
+        return ViewLayer(
+            this,
+            viewLayersContainer,
+            drawLayerModifier,
+            drawBlock,
+            invalidateParentLayer
+        )
+    }
+
     override fun onSemanticsChange() {
         accessibilityDelegate.onSemanticsChange()
     }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/DeviceRenderNode.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/DeviceRenderNode.kt
new file mode 100644
index 0000000..c978473
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/DeviceRenderNode.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020 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.compose.ui.platform
+
+import android.graphics.Outline
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.CanvasHolder
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.node.OwnedLayer
+
+/**
+ * RenderNode on Q+ and RenderNode on M-P devices have different APIs. This interface
+ * unifies the access so that [RenderNodeLayer] can be used for both.
+ */
+internal interface DeviceRenderNode {
+    val uniqueId: Long
+    val left: Int
+    val top: Int
+    val right: Int
+    val bottom: Int
+    val width: Int
+    val height: Int
+    var scaleX: Float
+    var scaleY: Float
+    var translationX: Float
+    var translationY: Float
+    var elevation: Float
+    var rotationZ: Float
+    var rotationX: Float
+    var rotationY: Float
+    var pivotX: Float
+    var pivotY: Float
+    var clipToOutline: Boolean
+    var clipToBounds: Boolean
+    var alpha: Float
+    val hasDisplayList: Boolean
+
+    fun setOutline(outline: Outline?)
+    fun setPosition(left: Int, top: Int, right: Int, bottom: Int): Boolean
+    fun offsetLeftAndRight(offset: Int)
+    fun offsetTopAndBottom(offset: Int)
+    fun record(
+        canvasHolder: CanvasHolder,
+        clipPath: Path?,
+        observerScope: OwnedLayer,
+        drawBlock: (Canvas) -> Unit
+    )
+    fun getMatrix(matrix: android.graphics.Matrix)
+    fun drawInto(canvas: android.graphics.Canvas)
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeApi23.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeApi23.kt
new file mode 100644
index 0000000..2df18ed
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeApi23.kt
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2020 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.compose.ui.platform
+
+import android.graphics.Outline
+import android.view.RenderNode
+import android.view.DisplayListCanvas
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.CanvasHolder
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.node.OwnedLayer
+
+/**
+ * RenderNode on M-O devices, where RenderNode isn't officially supported. This class uses
+ * a hidden android.view.RenderNode API that we have stubs for in the ui-android-stubs module.
+ * This implementation has higher performance than the View implementation by both avoiding
+ * reflection and using the lower overhead RenderNode instead of Views.
+ */
+@RequiresApi(Build.VERSION_CODES.M)
+internal class RenderNodeApi23(val ownerView: AndroidComposeView) : DeviceRenderNode {
+    private val renderNode = RenderNode.create("Compose", ownerView)
+
+    init {
+        if (needToValidateAccess) {
+            // This is only to force loading the DisplayListCanvas class and causing the
+            // MRenderNode to fail with a NoClassDefFoundError during construction instead of
+            // later.
+            @Suppress("UNUSED_VARIABLE")
+            val displayListCanvas: DisplayListCanvas? = null
+
+            // Ensure that we can access properties of the RenderNode. We want to force an
+            // exception here if there is a problem accessing any of these so that we can
+            // fall back to the View implementation.
+            renderNode.scaleX = renderNode.scaleX
+            renderNode.scaleY = renderNode.scaleY
+            renderNode.translationX = renderNode.translationX
+            renderNode.translationY = renderNode.translationY
+            renderNode.elevation = renderNode.elevation
+            renderNode.rotation = renderNode.rotation
+            renderNode.rotationX = renderNode.rotationX
+            renderNode.rotationY = renderNode.rotationY
+            renderNode.pivotX = renderNode.pivotX
+            renderNode.pivotY = renderNode.pivotY
+            renderNode.clipToOutline = renderNode.clipToOutline
+            renderNode.setClipToBounds(false)
+            renderNode.alpha = renderNode.alpha
+            renderNode.isValid // only read
+            renderNode.setLeftTopRightBottom(0, 0, 0, 0)
+            renderNode.offsetLeftAndRight(0)
+            renderNode.offsetTopAndBottom(0)
+            needToValidateAccess = false // only need to do this once
+        }
+        if (testFailCreateRenderNode) {
+            throw NoClassDefFoundError()
+        }
+    }
+
+    override val uniqueId: Long get() = 0
+
+    override var left: Int = 0
+    override var top: Int = 0
+    override var right: Int = 0
+    override var bottom: Int = 0
+    override val width: Int get() = right - left
+    override val height: Int get() = bottom - top
+
+    override var scaleX: Float
+        get() = renderNode.scaleX
+        set(value) {
+            renderNode.scaleX = value
+        }
+
+    override var scaleY: Float
+        get() = renderNode.scaleY
+        set(value) {
+            renderNode.scaleY = value
+        }
+
+    override var translationX: Float
+        get() = renderNode.translationX
+        set(value) {
+            renderNode.translationX = value
+        }
+
+    override var translationY: Float
+        get() = renderNode.translationY
+        set(value) {
+            renderNode.translationY = value
+        }
+
+    override var elevation: Float
+        get() = renderNode.elevation
+        set(value) {
+            renderNode.elevation = value
+        }
+
+    override var rotationZ: Float
+        get() = renderNode.rotation
+        set(value) {
+            renderNode.rotation = value
+        }
+
+    override var rotationX: Float
+        get() = renderNode.rotationX
+        set(value) {
+            renderNode.rotationX = value
+        }
+
+    override var rotationY: Float
+        get() = renderNode.rotationY
+        set(value) {
+            renderNode.rotationY = value
+        }
+
+    override var pivotX: Float
+        get() = renderNode.pivotX
+        set(value) {
+            renderNode.pivotX = value
+        }
+
+    override var pivotY: Float
+        get() = renderNode.pivotY
+        set(value) {
+            renderNode.pivotY = value
+        }
+
+    override var clipToOutline: Boolean
+        get() = renderNode.clipToOutline
+        set(value) {
+            renderNode.clipToOutline = value
+        }
+
+    override var clipToBounds: Boolean = false
+        set(value) {
+            field = value
+            renderNode.setClipToBounds(value)
+        }
+
+    override var alpha: Float
+        get() = renderNode.alpha
+        set(value) {
+            renderNode.alpha = value
+        }
+
+    override val hasDisplayList: Boolean
+        get() = renderNode.isValid
+
+    override fun setOutline(outline: Outline?) {
+        renderNode.setOutline(outline)
+    }
+
+    override fun setPosition(left: Int, top: Int, right: Int, bottom: Int): Boolean {
+        this.left = left
+        this.top = top
+        this.right = right
+        this.bottom = bottom
+        return renderNode.setLeftTopRightBottom(left, top, right, bottom)
+    }
+
+    override fun offsetLeftAndRight(offset: Int) {
+        left += offset
+        right += offset
+        renderNode.offsetLeftAndRight(offset)
+    }
+
+    override fun offsetTopAndBottom(offset: Int) {
+        top += offset
+        bottom += offset
+        renderNode.offsetTopAndBottom(offset)
+    }
+
+    override fun record(
+        canvasHolder: CanvasHolder,
+        clipPath: Path?,
+        observerScope: OwnedLayer,
+        drawBlock: (Canvas) -> Unit
+    ) {
+        val canvas = renderNode.start(width, height)
+        canvasHolder.drawInto(canvas) {
+            if (clipPath != null) {
+                save()
+                clipPath(clipPath)
+            }
+            ownerView.observeLayerModelReads(observerScope) {
+                drawBlock(this)
+            }
+            if (clipPath != null) {
+                restore()
+            }
+        }
+        renderNode.end(canvas)
+    }
+
+    override fun getMatrix(matrix: android.graphics.Matrix) {
+        return renderNode.getMatrix(matrix)
+    }
+
+    override fun drawInto(canvas: android.graphics.Canvas) {
+        (canvas as DisplayListCanvas).drawRenderNode(renderNode)
+    }
+
+    companion object {
+        // Used by tests to force failing creating a RenderNode to simulate a device that
+        // doesn't support RenderNodes before Q.
+        internal var testFailCreateRenderNode = false
+
+        // We need to validate that RenderNodes can be accessed before using the RenderNode
+        // stub implementation, but we only need to validate it once. This flag indicates that
+        // validation is still needed.
+        private var needToValidateAccess = true
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeApi29.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeApi29.kt
new file mode 100644
index 0000000..446b0d3
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeApi29.kt
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2020 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.compose.ui.platform
+
+import android.graphics.Outline
+import android.graphics.RenderNode
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.CanvasHolder
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.node.OwnedLayer
+
+/**
+ * RenderNode on Q+ devices, where it is officially supported.
+ */
+@RequiresApi(Build.VERSION_CODES.Q)
+internal class RenderNodeApi29(val ownerView: AndroidComposeView) : DeviceRenderNode {
+    private val renderNode = RenderNode("Compose")
+
+    override val uniqueId: Long get() = renderNode.uniqueId
+
+    override val left: Int get() = renderNode.left
+    override val top: Int get() = renderNode.top
+    override val right: Int get() = renderNode.right
+    override val bottom: Int get() = renderNode.bottom
+    override val width: Int get() = renderNode.width
+    override val height: Int get() = renderNode.height
+
+    override var scaleX: Float
+        get() = renderNode.scaleX
+        set(value) {
+            renderNode.scaleX = value
+        }
+
+    override var scaleY: Float
+        get() = renderNode.scaleY
+        set(value) {
+            renderNode.scaleY = value
+        }
+
+    override var translationX: Float
+        get() = renderNode.translationX
+        set(value) {
+            renderNode.translationX = value
+        }
+
+    override var translationY: Float
+        get() = renderNode.translationY
+        set(value) {
+            renderNode.translationY = value
+        }
+
+    override var elevation: Float
+        get() = renderNode.elevation
+        set(value) {
+            renderNode.elevation = value
+        }
+
+    override var rotationZ: Float
+        get() = renderNode.rotationZ
+        set(value) {
+            renderNode.rotationZ = value
+        }
+
+    override var rotationX: Float
+        get() = renderNode.rotationX
+        set(value) {
+            renderNode.rotationX = value
+        }
+
+    override var rotationY: Float
+        get() = renderNode.rotationY
+        set(value) {
+            renderNode.rotationY = value
+        }
+    override var pivotX: Float
+        get() = renderNode.pivotX
+        set(value) {
+            renderNode.pivotX = value
+        }
+
+    override var pivotY: Float
+        get() = renderNode.pivotY
+        set(value) {
+            renderNode.pivotY = value
+        }
+
+    override var clipToOutline: Boolean
+        get() = renderNode.clipToOutline
+        set(value) {
+            renderNode.clipToOutline = value
+        }
+
+    override var clipToBounds: Boolean
+        get() = renderNode.clipToBounds
+        set(value) {
+            renderNode.clipToBounds = value
+        }
+
+    override var alpha: Float
+        get() = renderNode.alpha
+        set(value) {
+            renderNode.alpha = value
+        }
+
+    override val hasDisplayList: Boolean
+        get() = renderNode.hasDisplayList()
+
+    override fun setOutline(outline: Outline?) {
+        renderNode.setOutline(outline)
+    }
+
+    override fun setPosition(left: Int, top: Int, right: Int, bottom: Int): Boolean {
+        return renderNode.setPosition(left, top, right, bottom)
+    }
+
+    override fun offsetLeftAndRight(offset: Int) {
+        renderNode.offsetLeftAndRight(offset)
+    }
+
+    override fun offsetTopAndBottom(offset: Int) {
+        renderNode.offsetTopAndBottom(offset)
+    }
+
+    override fun record(
+        canvasHolder: CanvasHolder,
+        clipPath: Path?,
+        observerScope: OwnedLayer,
+        drawBlock: (Canvas) -> Unit
+    ) {
+        canvasHolder.drawInto(renderNode.beginRecording()) {
+            if (clipPath != null) {
+                save()
+                clipPath(clipPath)
+            }
+            ownerView.observeLayerModelReads(observerScope) {
+                drawBlock(this)
+            }
+            if (clipPath != null) {
+                restore()
+            }
+        }
+        renderNode.endRecording()
+    }
+
+    override fun getMatrix(matrix: android.graphics.Matrix) {
+        return renderNode.getMatrix(matrix)
+    }
+
+    override fun drawInto(canvas: android.graphics.Canvas) {
+        canvas.drawRenderNode(renderNode)
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.kt
index b711f730a..ade45d0 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.kt
@@ -16,8 +16,8 @@
 
 package androidx.compose.ui.platform
 
-import android.annotation.TargetApi
-import android.graphics.RenderNode
+import android.os.Build
+import androidx.annotation.RequiresApi
 import androidx.compose.ui.DrawLayerModifier
 import androidx.compose.ui.TransformOrigin
 import androidx.compose.ui.geometry.Size
@@ -34,7 +34,7 @@
 /**
  * RenderNode implementation of OwnedLayer.
  */
-@TargetApi(29)
+@RequiresApi(Build.VERSION_CODES.M)
 internal class RenderNodeLayer(
     val ownerView: AndroidComposeView,
     drawLayerModifier: DrawLayerModifier,
@@ -59,8 +59,10 @@
      */
     private var transformOrigin: TransformOrigin = TransformOrigin.Center
 
-    private val renderNode = RenderNode(null).apply {
-        setHasOverlappingRendering(true)
+    private val renderNode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+        RenderNodeApi29(ownerView)
+    } else {
+        RenderNodeApi23(ownerView)
     }
 
     override val layerId: Long
@@ -116,11 +118,11 @@
         renderNode.pivotX = transformOrigin.pivotFractionX * width
         renderNode.pivotY = transformOrigin.pivotFractionY * height
         if (renderNode.setPosition(
-            renderNode.left,
-            renderNode.top,
-            renderNode.left + width,
-            renderNode.top + height
-        )) {
+                renderNode.left,
+                renderNode.top,
+                renderNode.left + width,
+                renderNode.top + height
+            )) {
             outlineResolver.update(Size(width.toFloat(), height.toFloat()))
             renderNode.setOutline(outlineResolver.outline)
             invalidate()
@@ -153,7 +155,12 @@
      * to re-record its drawing.
      */
     private fun triggerRepaint() {
-        ownerView.parent?.onDescendantInvalidated(ownerView, ownerView)
+        // onDescendantInvalidated is only supported on O+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            ownerView.parent?.onDescendantInvalidated(ownerView, ownerView)
+        } else {
+            ownerView.invalidate()
+        }
     }
 
     override fun drawLayer(canvas: Canvas) {
@@ -164,7 +171,7 @@
             if (drawnWithZ) {
                 canvas.enableZ()
             }
-            androidCanvas.drawRenderNode(renderNode)
+            renderNode.drawInto(androidCanvas)
             if (drawnWithZ) {
                 canvas.disableZ()
             }
@@ -175,22 +182,11 @@
     }
 
     override fun updateDisplayList() {
-        if (isDirty || !renderNode.hasDisplayList()) {
-            canvasHolder.drawInto(renderNode.beginRecording()) {
-                val clipPath = outlineResolver.clipPath
-                val manuallyClip = renderNode.clipToOutline && clipPath != null
-                if (manuallyClip) {
-                    save()
-                    clipPath(clipPath!!)
-                }
-                ownerView.observeLayerModelReads(this@RenderNodeLayer) {
-                    drawBlock(this)
-                }
-                if (manuallyClip) {
-                    restore()
-                }
-            }
-            renderNode.endRecording()
+        if (isDirty || !renderNode.hasDisplayList) {
+            val clipPath = if (renderNode.clipToOutline) outlineResolver.clipPath else null
+
+            renderNode.record(canvasHolder, clipPath, this, drawBlock)
+
             isDirty = false
         }
     }
diff --git a/ui/settings.gradle b/ui/settings.gradle
index 4601d26..d733301 100644
--- a/ui/settings.gradle
+++ b/ui/settings.gradle
@@ -99,6 +99,7 @@
 includeProject(":compose:test-utils", "../compose/test-utils")
 includeProject(":compose:ui", "../compose/ui")
 includeProject(":compose:ui:ui", "../compose/ui/ui")
+includeProject(":compose:ui:ui-android-stubs", "../compose/ui/ui-android-stubs")
 includeProject(":compose:ui:ui-geometry", "../compose/ui/ui-geometry")
 includeProject(":compose:ui:ui-graphics", "../compose/ui/ui-graphics")
 includeProject(":compose:ui:ui-graphics:samples", "../compose/ui/ui-graphics/samples")