Snap for 10447354 from ac210c97211a51828d906741c22683312f1c1a41 to mainline-cellbroadcast-release

Change-Id: Iacd0c11ec800350e557890360ffc17c0c961455a
diff --git a/animationlib/Android.bp b/animationlib/Android.bp
new file mode 100644
index 0000000..d67a5de
--- /dev/null
+++ b/animationlib/Android.bp
@@ -0,0 +1,59 @@
+// Copyright (C) 2018 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+
+android_library {
+    name: "animationlib",
+    manifest: "AndroidManifest.xml",
+    sdk_version: "system_current",
+    min_sdk_version: "26",
+    static_libs: [
+        "androidx.core_core-animation",
+        "androidx.core_core-ktx",
+        "androidx.annotation_annotation",
+    ],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt"
+    ],
+    kotlincflags: ["-Xjvm-default=all"],
+    // This library is meant to access only public APIs
+    // do not flip this flag to true
+    platform_apis: false
+}
+
+android_test {
+    name: "animationlib_tests",
+    manifest: "tests/AndroidManifest.xml",
+
+    static_libs: [
+        "animationlib",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "testables",
+    ],
+    libs: [
+        "android.test.base",
+    ],
+    srcs: [
+        "**/*.java",
+        "**/*.kt"
+    ],
+    kotlincflags: ["-Xjvm-default=all"],
+    test_suites: ["general-tests"],
+}
diff --git a/searchuilib/AndroidManifest.xml b/animationlib/AndroidManifest.xml
similarity index 87%
rename from searchuilib/AndroidManifest.xml
rename to animationlib/AndroidManifest.xml
index 6c6c5f6..b05fb11 100644
--- a/searchuilib/AndroidManifest.xml
+++ b/animationlib/AndroidManifest.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-     Copyright (C) 2020 The Android Open Source Project
+     Copyright (C) 2023 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.
@@ -16,5 +16,5 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.app.search">
+    package="com.android.app.animation">
 </manifest>
diff --git a/animationlib/TEST_MAPPING b/animationlib/TEST_MAPPING
new file mode 100644
index 0000000..4fd6f09
--- /dev/null
+++ b/animationlib/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "animationlib_tests"
+    }
+  ]
+}
diff --git a/animationlib/build.gradle b/animationlib/build.gradle
new file mode 100644
index 0000000..f9c4485
--- /dev/null
+++ b/animationlib/build.gradle
@@ -0,0 +1,46 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+    namespace = "com.android.app.animation"
+    testNamespace = "com.android.app.animation.tests"
+    defaultConfig {
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    sourceSets {
+        main {
+            java.srcDirs = ['src']
+            manifest.srcFile 'AndroidManifest.xml'
+        }
+        androidTest {
+            java.srcDirs = ["tests"]
+        }
+    }
+    compileSdk 33
+
+    defaultConfig {
+        minSdk 33
+        targetSdk 33
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+    tasks.lint.enabled = false
+    tasks.withType(JavaCompile) {
+        options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
+    }
+    kotlinOptions {
+        jvmTarget = '1.8'
+        freeCompilerArgs = ["-Xjvm-default=all"]
+    }
+}
+
+dependencies {
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0"
+    implementation "androidx.core:core-animation:1.0.0-alpha02"
+    implementation "androidx.core:core-ktx:1.9.0"
+    androidTestImplementation "androidx.test.ext:junit:1.1.3"
+    androidTestImplementation "androidx.test:rules:1.4.0"
+}
diff --git a/animationlib/src/com/android/app/animation/Interpolators.java b/animationlib/src/com/android/app/animation/Interpolators.java
new file mode 100644
index 0000000..0f3776c
--- /dev/null
+++ b/animationlib/src/com/android/app/animation/Interpolators.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2023 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.app.animation;
+
+import android.graphics.Path;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.BounceInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.PathInterpolator;
+
+/**
+ * Utility class to receive interpolators from.
+ *
+ * Make sure that changes made to this class are also reflected in {@link InterpolatorsAndroidX}.
+ * Please consider using the androidx dependencies featuring better testability altogether.
+ */
+public class Interpolators {
+
+    /*
+     * ============================================================================================
+     * Emphasized interpolators.
+     * ============================================================================================
+     */
+
+    /**
+     * The default emphasized interpolator. Used for hero / emphasized movement of content.
+     */
+    public static final Interpolator EMPHASIZED = createEmphasizedInterpolator();
+
+    /**
+     * The accelerated emphasized interpolator. Used for hero / emphasized movement of content that
+     * is disappearing e.g. when moving off screen.
+     */
+    public static final Interpolator EMPHASIZED_ACCELERATE = new PathInterpolator(
+            0.3f, 0f, 0.8f, 0.15f);
+
+    /**
+     * The decelerating emphasized interpolator. Used for hero / emphasized movement of content that
+     * is appearing e.g. when coming from off screen
+     */
+    public static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator(
+            0.05f, 0.7f, 0.1f, 1f);
+
+
+    /*
+     * ============================================================================================
+     * Standard interpolators.
+     * ============================================================================================
+     */
+
+    /**
+     * The standard interpolator that should be used on every normal animation
+     */
+    public static final Interpolator STANDARD = new PathInterpolator(
+            0.2f, 0f, 0f, 1f);
+
+    /**
+     * The standard accelerating interpolator that should be used on every regular movement of
+     * content that is disappearing e.g. when moving off screen.
+     */
+    public static final Interpolator STANDARD_ACCELERATE = new PathInterpolator(
+            0.3f, 0f, 1f, 1f);
+
+    /**
+     * The standard decelerating interpolator that should be used on every regular movement of
+     * content that is appearing e.g. when coming from off screen.
+     */
+    public static final Interpolator STANDARD_DECELERATE = new PathInterpolator(
+            0f, 0f, 0f, 1f);
+
+    /*
+     * ============================================================================================
+     * Legacy
+     * ============================================================================================
+     */
+
+    /**
+     * The default legacy interpolator as defined in Material 1. Also known as FAST_OUT_SLOW_IN.
+     */
+    public static final Interpolator LEGACY = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
+
+    /**
+     * The default legacy accelerating interpolator as defined in Material 1.
+     * Also known as FAST_OUT_LINEAR_IN.
+     */
+    public static final Interpolator LEGACY_ACCELERATE = new PathInterpolator(0.4f, 0f, 1f, 1f);
+
+    /**
+     * The default legacy decelerating interpolator as defined in Material 1.
+     * Also known as LINEAR_OUT_SLOW_IN.
+     */
+    public static final Interpolator LEGACY_DECELERATE = new PathInterpolator(0f, 0f, 0.2f, 1f);
+
+    /**
+     * Linear interpolator. Often used if the interpolator is for different properties who need
+     * different interpolations.
+     */
+    public static final Interpolator LINEAR = new LinearInterpolator();
+
+    /*
+     * ============================================================================================
+     * Custom interpolators
+     * ============================================================================================
+     */
+
+    public static final Interpolator FAST_OUT_SLOW_IN = LEGACY;
+    public static final Interpolator FAST_OUT_LINEAR_IN = LEGACY_ACCELERATE;
+    public static final Interpolator LINEAR_OUT_SLOW_IN = LEGACY_DECELERATE;
+
+    /**
+     * Like {@link #FAST_OUT_SLOW_IN}, but used in case the animation is played in reverse (i.e. t
+     * goes from 1 to 0 instead of 0 to 1).
+     */
+    public static final Interpolator FAST_OUT_SLOW_IN_REVERSE =
+            new PathInterpolator(0.8f, 0f, 0.6f, 1f);
+    public static final Interpolator SLOW_OUT_LINEAR_IN = new PathInterpolator(0.8f, 0f, 1f, 1f);
+    public static final Interpolator ALPHA_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
+    public static final Interpolator ALPHA_OUT = new PathInterpolator(0f, 0f, 0.8f, 1f);
+    public static final Interpolator ACCELERATE = new AccelerateInterpolator();
+    public static final Interpolator ACCELERATE_DECELERATE = new AccelerateDecelerateInterpolator();
+    public static final Interpolator DECELERATE_QUINT = new DecelerateInterpolator(2.5f);
+    public static final Interpolator CUSTOM_40_40 = new PathInterpolator(0.4f, 0f, 0.6f, 1f);
+    public static final Interpolator ICON_OVERSHOT = new PathInterpolator(0.4f, 0f, 0.2f, 1.4f);
+    public static final Interpolator ICON_OVERSHOT_LESS = new PathInterpolator(0.4f, 0f, 0.2f,
+            1.1f);
+    public static final Interpolator PANEL_CLOSE_ACCELERATED = new PathInterpolator(0.3f, 0, 0.5f,
+            1);
+    public static final Interpolator BOUNCE = new BounceInterpolator();
+    /**
+     * For state transitions on the control panel that lives in GlobalActions.
+     */
+    public static final Interpolator CONTROL_STATE = new PathInterpolator(0.4f, 0f, 0.2f,
+            1.0f);
+
+    /**
+     * Interpolator to be used when animating a move based on a click. Pair with enough duration.
+     */
+    public static final Interpolator TOUCH_RESPONSE =
+            new PathInterpolator(0.3f, 0f, 0.1f, 1f);
+
+    /**
+     * Like {@link #TOUCH_RESPONSE}, but used in case the animation is played in reverse (i.e. t
+     * goes from 1 to 0 instead of 0 to 1).
+     */
+    public static final Interpolator TOUCH_RESPONSE_REVERSE =
+            new PathInterpolator(0.9f, 0f, 0.7f, 1f);
+
+    /*
+     * ============================================================================================
+     * Functions / Utilities
+     * ============================================================================================
+     */
+
+    /**
+     * Calculate the amount of overshoot using an exponential falloff function with desired
+     * properties, where the overshoot smoothly transitions at the 1.0f boundary into the
+     * overshoot, retaining its acceleration.
+     *
+     * @param progress a progress value going from 0 to 1
+     * @param overshootAmount the amount > 0 of overshoot desired. A value of 0.1 means the max
+     *                        value of the overall progress will be at 1.1.
+     * @param overshootStart the point in (0,1] where the result should reach 1
+     * @return the interpolated overshoot
+     */
+    public static float getOvershootInterpolation(float progress, float overshootAmount,
+            float overshootStart) {
+        if (overshootAmount == 0.0f || overshootStart == 0.0f) {
+            throw new IllegalArgumentException("Invalid values for overshoot");
+        }
+        float b = MathUtils.log((overshootAmount + 1) / (overshootAmount)) / overshootStart;
+        return MathUtils.max(0.0f,
+                (float) (1.0f - Math.exp(-b * progress)) * (overshootAmount + 1.0f));
+    }
+
+    /**
+     * Similar to {@link #getOvershootInterpolation(float, float, float)} but the overshoot
+     * starts immediately here, instead of first having a section of non-overshooting
+     *
+     * @param progress a progress value going from 0 to 1
+     */
+    public static float getOvershootInterpolation(float progress) {
+        return MathUtils.max(0.0f, (float) (1.0f - Math.exp(-4 * progress)));
+    }
+
+    // Create the default emphasized interpolator
+    private static PathInterpolator createEmphasizedInterpolator() {
+        Path path = new Path();
+        // Doing the same as fast_out_extra_slow_in
+        path.moveTo(0f, 0f);
+        path.cubicTo(0.05f, 0f, 0.133333f, 0.06f, 0.166666f, 0.4f);
+        path.cubicTo(0.208333f, 0.82f, 0.25f, 1f, 1f, 1f);
+        return new PathInterpolator(path);
+    }
+}
\ No newline at end of file
diff --git a/animationlib/src/com/android/app/animation/InterpolatorsAndroidX.java b/animationlib/src/com/android/app/animation/InterpolatorsAndroidX.java
new file mode 100644
index 0000000..7142f54
--- /dev/null
+++ b/animationlib/src/com/android/app/animation/InterpolatorsAndroidX.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2023 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.app.animation;
+
+import android.graphics.Path;
+
+import androidx.core.animation.AccelerateDecelerateInterpolator;
+import androidx.core.animation.AccelerateInterpolator;
+import androidx.core.animation.BounceInterpolator;
+import androidx.core.animation.DecelerateInterpolator;
+import androidx.core.animation.Interpolator;
+import androidx.core.animation.LinearInterpolator;
+import androidx.core.animation.PathInterpolator;
+
+/**
+ * Utility class to receive interpolators from. (androidx compatible version)
+ *
+ * This is the androidx compatible version of {@link Interpolators}. Make sure that changes made to
+ * this class are also reflected in {@link Interpolators}.
+ *
+ * Using the androidx versions of {@link androidx.core.animation.ValueAnimator} or
+ * {@link androidx.core.animation.ObjectAnimator} improves animation testability. This file provides
+ * the androidx compatible versions of the interpolators defined in {@link Interpolators}.
+ * AnimatorTestRule can be used in Tests to manipulate the animation under test (e.g. artificially
+ * advancing the time).
+ */
+public class InterpolatorsAndroidX {
+
+    /*
+     * ============================================================================================
+     * Emphasized interpolators.
+     * ============================================================================================
+     */
+
+    /**
+     * The default emphasized interpolator. Used for hero / emphasized movement of content.
+     */
+    public static final Interpolator EMPHASIZED = createEmphasizedInterpolator();
+
+    /**
+     * The accelerated emphasized interpolator. Used for hero / emphasized movement of content that
+     * is disappearing e.g. when moving off screen.
+     */
+    public static final Interpolator EMPHASIZED_ACCELERATE = new PathInterpolator(
+            0.3f, 0f, 0.8f, 0.15f);
+
+    /**
+     * The decelerating emphasized interpolator. Used for hero / emphasized movement of content that
+     * is appearing e.g. when coming from off screen
+     */
+    public static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator(
+            0.05f, 0.7f, 0.1f, 1f);
+
+
+    /*
+     * ============================================================================================
+     * Standard interpolators.
+     * ============================================================================================
+     */
+
+    /**
+     * The standard interpolator that should be used on every normal animation
+     */
+    public static final Interpolator STANDARD = new PathInterpolator(
+            0.2f, 0f, 0f, 1f);
+
+    /**
+     * The standard accelerating interpolator that should be used on every regular movement of
+     * content that is disappearing e.g. when moving off screen.
+     */
+    public static final Interpolator STANDARD_ACCELERATE = new PathInterpolator(
+            0.3f, 0f, 1f, 1f);
+
+    /**
+     * The standard decelerating interpolator that should be used on every regular movement of
+     * content that is appearing e.g. when coming from off screen.
+     */
+    public static final Interpolator STANDARD_DECELERATE = new PathInterpolator(
+            0f, 0f, 0f, 1f);
+
+    /*
+     * ============================================================================================
+     * Legacy
+     * ============================================================================================
+     */
+
+    /**
+     * The default legacy interpolator as defined in Material 1. Also known as FAST_OUT_SLOW_IN.
+     */
+    public static final Interpolator LEGACY = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
+
+    /**
+     * The default legacy accelerating interpolator as defined in Material 1.
+     * Also known as FAST_OUT_LINEAR_IN.
+     */
+    public static final Interpolator LEGACY_ACCELERATE = new PathInterpolator(0.4f, 0f, 1f, 1f);
+
+    /**
+     * The default legacy decelerating interpolator as defined in Material 1.
+     * Also known as LINEAR_OUT_SLOW_IN.
+     */
+    public static final Interpolator LEGACY_DECELERATE = new PathInterpolator(0f, 0f, 0.2f, 1f);
+
+    /**
+     * Linear interpolator. Often used if the interpolator is for different properties who need
+     * different interpolations.
+     */
+    public static final Interpolator LINEAR = new LinearInterpolator();
+
+    /*
+     * ============================================================================================
+     * Custom interpolators
+     * ============================================================================================
+     */
+
+    public static final Interpolator FAST_OUT_SLOW_IN = LEGACY;
+    public static final Interpolator FAST_OUT_LINEAR_IN = LEGACY_ACCELERATE;
+    public static final Interpolator LINEAR_OUT_SLOW_IN = LEGACY_DECELERATE;
+
+    /**
+     * Like {@link #FAST_OUT_SLOW_IN}, but used in case the animation is played in reverse (i.e. t
+     * goes from 1 to 0 instead of 0 to 1).
+     */
+    public static final Interpolator FAST_OUT_SLOW_IN_REVERSE =
+            new PathInterpolator(0.8f, 0f, 0.6f, 1f);
+    public static final Interpolator SLOW_OUT_LINEAR_IN = new PathInterpolator(0.8f, 0f, 1f, 1f);
+    public static final Interpolator ALPHA_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
+    public static final Interpolator ALPHA_OUT = new PathInterpolator(0f, 0f, 0.8f, 1f);
+    public static final Interpolator ACCELERATE = new AccelerateInterpolator();
+    public static final Interpolator ACCELERATE_DECELERATE = new AccelerateDecelerateInterpolator();
+    public static final Interpolator DECELERATE_QUINT = new DecelerateInterpolator(2.5f);
+    public static final Interpolator CUSTOM_40_40 = new PathInterpolator(0.4f, 0f, 0.6f, 1f);
+    public static final Interpolator ICON_OVERSHOT = new PathInterpolator(0.4f, 0f, 0.2f, 1.4f);
+    public static final Interpolator ICON_OVERSHOT_LESS = new PathInterpolator(0.4f, 0f, 0.2f,
+            1.1f);
+    public static final Interpolator PANEL_CLOSE_ACCELERATED = new PathInterpolator(0.3f, 0, 0.5f,
+            1);
+    public static final Interpolator BOUNCE = new BounceInterpolator();
+    /**
+     * For state transitions on the control panel that lives in GlobalActions.
+     */
+    public static final Interpolator CONTROL_STATE = new PathInterpolator(0.4f, 0f, 0.2f,
+            1.0f);
+
+    /**
+     * Interpolator to be used when animating a move based on a click. Pair with enough duration.
+     */
+    public static final Interpolator TOUCH_RESPONSE =
+            new PathInterpolator(0.3f, 0f, 0.1f, 1f);
+
+    /**
+     * Like {@link #TOUCH_RESPONSE}, but used in case the animation is played in reverse (i.e. t
+     * goes from 1 to 0 instead of 0 to 1).
+     */
+    public static final Interpolator TOUCH_RESPONSE_REVERSE =
+            new PathInterpolator(0.9f, 0f, 0.7f, 1f);
+
+    /*
+     * ============================================================================================
+     * Functions / Utilities
+     * ============================================================================================
+     */
+
+    /**
+     * Calculate the amount of overshoot using an exponential falloff function with desired
+     * properties, where the overshoot smoothly transitions at the 1.0f boundary into the
+     * overshoot, retaining its acceleration.
+     *
+     * @param progress a progress value going from 0 to 1
+     * @param overshootAmount the amount > 0 of overshoot desired. A value of 0.1 means the max
+     *                        value of the overall progress will be at 1.1.
+     * @param overshootStart the point in (0,1] where the result should reach 1
+     * @return the interpolated overshoot
+     */
+    public static float getOvershootInterpolation(float progress, float overshootAmount,
+            float overshootStart) {
+        if (overshootAmount == 0.0f || overshootStart == 0.0f) {
+            throw new IllegalArgumentException("Invalid values for overshoot");
+        }
+        float b = MathUtils.log((overshootAmount + 1) / (overshootAmount)) / overshootStart;
+        return MathUtils.max(0.0f,
+                (float) (1.0f - Math.exp(-b * progress)) * (overshootAmount + 1.0f));
+    }
+
+    /**
+     * Similar to {@link #getOvershootInterpolation(float, float, float)} but the overshoot
+     * starts immediately here, instead of first having a section of non-overshooting
+     *
+     * @param progress a progress value going from 0 to 1
+     */
+    public static float getOvershootInterpolation(float progress) {
+        return MathUtils.max(0.0f, (float) (1.0f - Math.exp(-4 * progress)));
+    }
+
+    // Create the default emphasized interpolator
+    private static PathInterpolator createEmphasizedInterpolator() {
+        Path path = new Path();
+        // Doing the same as fast_out_extra_slow_in
+        path.moveTo(0f, 0f);
+        path.cubicTo(0.05f, 0f, 0.133333f, 0.06f, 0.166666f, 0.4f);
+        path.cubicTo(0.208333f, 0.82f, 0.25f, 1f, 1f, 1f);
+        return new PathInterpolator(path);
+    }
+}
\ No newline at end of file
diff --git a/animationlib/src/com/android/app/animation/MathUtils.java b/animationlib/src/com/android/app/animation/MathUtils.java
new file mode 100644
index 0000000..d0a34c8
--- /dev/null
+++ b/animationlib/src/com/android/app/animation/MathUtils.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2023 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.app.animation;
+
+public final class MathUtils {
+    public static float log(float a) {
+        return (float) Math.log(a);
+    }
+
+    public static float max(float a, float b) {
+        return a > b ? a : b;
+    }
+}
diff --git a/animationlib/tests/AndroidManifest.xml b/animationlib/tests/AndroidManifest.xml
new file mode 100644
index 0000000..77a5990
--- /dev/null
+++ b/animationlib/tests/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.app.animation.tests">
+
+    <instrumentation
+        android:name="android.testing.TestableInstrumentation"
+        android:label="Tests for public Animation Lib"
+        android:targetPackage="com.android.app.animation.tests"/>
+
+</manifest>
diff --git a/animationlib/tests/com/android/app/animation/InterpolatorsAndroidXTest.kt b/animationlib/tests/com/android/app/animation/InterpolatorsAndroidXTest.kt
new file mode 100644
index 0000000..841e141
--- /dev/null
+++ b/animationlib/tests/com/android/app/animation/InterpolatorsAndroidXTest.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 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.app.animation
+
+import androidx.test.filters.SmallTest
+import java.lang.reflect.Modifier
+import junit.framework.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class InterpolatorsAndroidXTest {
+
+    @Test
+    fun testInterpolatorsAndInterpolatorsAndroidXPublicMethodsAreEqual() {
+        assertEquals(
+                Interpolators::class.java.getPublicMethods(),
+                InterpolatorsAndroidX::class.java.getPublicMethods()
+        )
+    }
+
+    @Test
+    fun testInterpolatorsAndInterpolatorsAndroidXPublicFieldsAreEqual() {
+        assertEquals(
+                Interpolators::class.java.getPublicFields(),
+                InterpolatorsAndroidX::class.java.getPublicFields()
+        )
+    }
+
+    private fun <T> Class<T>.getPublicMethods() =
+            declaredMethods
+                    .filter { Modifier.isPublic(it.modifiers) }
+                    .map { it.toString().replace(name, "") }
+                    .toSet()
+
+    private fun <T> Class<T>.getPublicFields() =
+            fields.filter { Modifier.isPublic(it.modifiers) }.map { it.name }.toSet()
+}
diff --git a/iconloaderlib/Android.bp b/iconloaderlib/Android.bp
index 6867e6b..083091d 100644
--- a/iconloaderlib/Android.bp
+++ b/iconloaderlib/Android.bp
@@ -45,4 +45,8 @@
         "src/**/*.java",
         "src_full_lib/**/*.java",
     ],
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.permission",
+    ],
 }
diff --git a/iconloaderlib/build.gradle b/iconloaderlib/build.gradle
index 10ec889..344ac20 100644
--- a/iconloaderlib/build.gradle
+++ b/iconloaderlib/build.gradle
@@ -1,14 +1,9 @@
-apply plugin: 'com.android.library'
+plugins {
+    id 'com.android.library'
+}
 
 android {
-    compileSdkVersion COMPILE_SDK
-    buildToolsVersion BUILD_TOOLS_VERSION
-
-    defaultConfig {
-        minSdkVersion 26
-        targetSdkVersion 28
-    }
-
+    namespace = "com.android.launcher3.icons"
     sourceSets {
         main {
             java.srcDirs = ['src', 'src_full_lib']
@@ -16,21 +11,15 @@
             res.srcDirs = ['res']
         }
     }
-
-    lintOptions {
+    lint {
         abortOnError false
     }
 
     tasks.withType(JavaCompile) {
         options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
     }
-
-    compileOptions {
-        sourceCompatibility JavaVersion.VERSION_1_8
-        targetCompatibility JavaVersion.VERSION_1_8
-    }
 }
 
 dependencies {
-    implementation "androidx.core:core:${ANDROID_X_VERSION}"
+    implementation "androidx.core:core"
 }
diff --git a/iconloaderlib/res/drawable/ic_clone_app_badge.xml b/iconloaderlib/res/drawable/ic_clone_app_badge.xml
new file mode 100644
index 0000000..9f0876d
--- /dev/null
+++ b/iconloaderlib/res/drawable/ic_clone_app_badge.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="@dimen/profile_badge_size"
+    android:height="@dimen/profile_badge_size"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="#11000000"
+        android:pathData="M.5,12.25
+        A11.5,11.5 0 1,1 23.5,12.25
+        A11.5,11.5 0 1,1 .5,12.25" />
+
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M1,12
+        A11,11 0 1,1 23,12
+        A11,11 0 1,1 1,12" />
+
+    <group android:scaleX=".6" android:scaleY=".6" android:pivotX="12" android:pivotY="12">
+      <path
+        android:pathData="M22,9.5C22,13.642 18.642,17 14.5,17C10.358,17 7,13.642 7,9.5C7,5.358 10.358,2 14.5,2C18.642,2 22,5.358 22,9.5Z"
+        android:fillColor="#ff3C4043"/>
+    <path
+        android:pathData="M9.5,20.333C12.722,20.333 15.333,17.722 15.333,14.5C15.333,11.278 12.722,8.667 9.5,8.667C6.278,8.667 3.667,11.278 3.667,14.5C3.667,17.722 6.278,20.333 9.5,20.333ZM9.5,22C13.642,22 17,18.642 17,14.5C17,10.358 13.642,7 9.5,7C5.358,7 2,10.358 2,14.5C2,18.642 5.358,22 9.5,22Z"
+        android:fillColor="#ff3C4043"
+        android:fillType="evenOdd"/>
+    </group>
+</vector>
diff --git a/iconloaderlib/res/drawable/ic_clone_app_badge_themed.xml b/iconloaderlib/res/drawable/ic_clone_app_badge_themed.xml
new file mode 100644
index 0000000..3a59e3d
--- /dev/null
+++ b/iconloaderlib/res/drawable/ic_clone_app_badge_themed.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="@dimen/profile_badge_size"
+    android:height="@dimen/profile_badge_size"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="#11000000"
+        android:pathData="M.5,12.25
+        A11.5,11.5 0 1,1 23.5,12.25
+        A11.5,11.5 0 1,1 .5,12.25" />
+
+    <path
+        android:fillColor="@color/themed_icon_background_color"
+        android:pathData="M1,12
+        A11,11 0 1,1 23,12
+        A11,11 0 1,1 1,12" />
+
+    <group android:scaleX=".6" android:scaleY=".6" android:pivotX="12" android:pivotY="12">
+      <path
+        android:pathData="M22,9.5C22,13.642 18.642,17 14.5,17C10.358,17 7,13.642 7,9.5C7,5.358 10.358,2 14.5,2C18.642,2 22,5.358 22,9.5Z"
+        android:fillColor="@color/themed_badge_icon_color"/>
+    <path
+        android:pathData="M9.5,20.333C12.722,20.333 15.333,17.722 15.333,14.5C15.333,11.278 12.722,8.667 9.5,8.667C6.278,8.667 3.667,11.278 3.667,14.5C3.667,17.722 6.278,20.333 9.5,20.333ZM9.5,22C13.642,22 17,18.642 17,14.5C17,10.358 13.642,7 9.5,7C5.358,7 2,10.358 2,14.5C2,18.642 5.358,22 9.5,22Z"
+        android:fillColor="@color/themed_badge_icon_color"
+        android:fillType="evenOdd"/>
+    </group>
+</vector>
diff --git a/iconloaderlib/res/drawable/ic_instant_app_badge.xml b/iconloaderlib/res/drawable/ic_instant_app_badge.xml
index b74317e..e6b5701 100644
--- a/iconloaderlib/res/drawable/ic_instant_app_badge.xml
+++ b/iconloaderlib/res/drawable/ic_instant_app_badge.xml
@@ -20,20 +20,11 @@
     android:viewportHeight="18">
 
     <path
-        android:fillColor="@android:color/black"
-        android:strokeWidth="1"
-        android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" />
-    <path
-        android:fillColor="@android:color/white"
-        android:strokeWidth="1"
-        android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" />
-    <path
         android:fillColor="@android:color/white"
         android:strokeWidth="1"
         android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" />
     <path
         android:fillColor="@android:color/black"
-        android:fillAlpha="0.87"
         android:strokeWidth="1"
         android:pathData="M 6 10.4123279 L 8.63934949 10.4123279 L 8.63934949 15.6 L 12.5577168 7.84517705 L 9.94547194 7.84517705 L 9.94547194 2 Z" />
 </vector>
diff --git a/iconloaderlib/res/drawable/ic_instant_app_badge_themed.xml b/iconloaderlib/res/drawable/ic_instant_app_badge_themed.xml
new file mode 100644
index 0000000..6e19339
--- /dev/null
+++ b/iconloaderlib/res/drawable/ic_instant_app_badge_themed.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="@dimen/profile_badge_size"
+    android:height="@dimen/profile_badge_size"
+    android:viewportWidth="18"
+    android:viewportHeight="18">
+
+    <path
+        android:fillColor="@color/themed_badge_icon_background_color"
+        android:strokeWidth="1"
+        android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" />
+    <path
+        android:fillColor="@color/themed_badge_icon_color"
+        android:strokeWidth="1"
+        android:pathData="M 6 10.4123279 L 8.63934949 10.4123279 L 8.63934949 15.6 L 12.5577168 7.84517705 L 9.94547194 7.84517705 L 9.94547194 2 Z" />
+</vector>
diff --git a/iconloaderlib/res/drawable/ic_work_app_badge_themed.xml b/iconloaderlib/res/drawable/ic_work_app_badge_themed.xml
new file mode 100644
index 0000000..6866d2f
--- /dev/null
+++ b/iconloaderlib/res/drawable/ic_work_app_badge_themed.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="@dimen/profile_badge_size"
+    android:height="@dimen/profile_badge_size"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="#11000000"
+        android:pathData="M.5,12.25
+        A11.5,11.5 0 1,1 23.5,12.25
+        A11.5,11.5 0 1,1 .5,12.25" />
+
+    <path
+        android:fillColor="@color/themed_badge_icon_background_color"
+        android:pathData="M1,12
+        A11,11 0 1,1 23,12
+        A11,11 0 1,1 1,12" />
+
+    <group android:scaleX=".6" android:scaleY=".6" android:pivotX="12" android:pivotY="12">
+        <path
+            android:fillColor="@color/themed_badge_icon_color"
+            android:pathData="M20,6h-4L16,4c0,-1.11 -0.89,-2 -2,-2h-4c-1.11,0 -2,0.89 -2,2v2L4,6c-1.11,0 -1.99,0.89 -1.99,2L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM14,6h-4L10,4h4v2z" />
+    </group>
+</vector>
diff --git a/iconloaderlib/res/values-night-v31/colors.xml b/iconloaderlib/res/values-night-v31/colors.xml
new file mode 100644
index 0000000..e5ebda6
--- /dev/null
+++ b/iconloaderlib/res/values-night-v31/colors.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2023, 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.
+*/
+-->
+<resources>
+    <color name="themed_icon_color">@android:color/system_accent1_200</color>
+    <color name="themed_icon_background_color">@android:color/system_accent2_800</color>
+    <color name="themed_badge_icon_color">@android:color/system_accent2_800</color>
+    <color name="themed_badge_icon_background_color">@android:color/system_accent1_200</color>
+</resources>
diff --git a/iconloaderlib/res/values-night/colors.xml b/iconloaderlib/res/values-night/colors.xml
new file mode 100644
index 0000000..9de7074
--- /dev/null
+++ b/iconloaderlib/res/values-night/colors.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2023, 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.
+*/
+-->
+<resources>
+    <color name="themed_icon_color">#A8C7FA</color>
+    <color name="themed_icon_background_color">#003355</color>
+    <color name="themed_badge_icon_color">#003355</color>
+    <color name="themed_badge_icon_background_color">#A8C7FA</color>
+</resources>
diff --git a/iconloaderlib/res/values-v31/colors.xml b/iconloaderlib/res/values-v31/colors.xml
new file mode 100644
index 0000000..1405ad0
--- /dev/null
+++ b/iconloaderlib/res/values-v31/colors.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2023, 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.
+*/
+-->
+<resources>
+    <color name="themed_icon_color">@android:color/system_accent1_700</color>
+    <color name="themed_icon_background_color">@android:color/system_accent1_100</color>
+    <color name="themed_badge_icon_color">@android:color/system_accent1_700</color>
+    <color name="themed_badge_icon_background_color">@android:color/system_accent1_100</color>
+</resources>
diff --git a/iconloaderlib/res/values/colors.xml b/iconloaderlib/res/values/colors.xml
index 70582c2..8eeafb4 100644
--- a/iconloaderlib/res/values/colors.xml
+++ b/iconloaderlib/res/values/colors.xml
@@ -17,6 +17,10 @@
 */
 -->
 <resources>
+    <color name="themed_icon_color">#0842A0</color>
+    <color name="themed_icon_background_color">#D3E3FD</color>
+    <color name="themed_badge_icon_color">#0842A0</color>
+    <color name="themed_badge_icon_background_color">#D3E3FD</color>
     <color name="legacy_icon_background">#FFFFFF</color>
 
     <!-- Yellow 600, used for highlighting "important" conversations in settings & notifications -->
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
index c0be55d..704df6f 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
@@ -1,13 +1,17 @@
 package com.android.launcher3.icons;
 
+import static android.graphics.Paint.ANTI_ALIAS_FLAG;
 import static android.graphics.Paint.DITHER_FLAG;
 import static android.graphics.Paint.FILTER_BITMAP_FLAG;
 import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction;
 
+import static com.android.launcher3.icons.BitmapInfo.FLAG_CLONE;
 import static com.android.launcher3.icons.BitmapInfo.FLAG_INSTANT;
 import static com.android.launcher3.icons.BitmapInfo.FLAG_WORK;
 import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.Intent;
@@ -30,12 +34,17 @@
 import android.os.UserHandle;
 import android.util.SparseBooleanArray;
 
+import androidx.annotation.ColorInt;
+import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.icons.BitmapInfo.Extender;
 import com.android.launcher3.util.FlagOp;
 
+import java.lang.annotation.Retention;
+import java.util.Objects;
+
 /**
  * This class will be moved to androidx library. There shouldn't be any dependency outside
  * this package.
@@ -44,23 +53,47 @@
 
     private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE;
 
+    public static final int MODE_DEFAULT = 0;
+    public static final int MODE_ALPHA = 1;
+    public static final int MODE_WITH_SHADOW = 2;
+    public static final int MODE_HARDWARE = 3;
+    public static final int MODE_HARDWARE_WITH_SHADOW = 4;
+
+    @Retention(SOURCE)
+    @IntDef({MODE_DEFAULT, MODE_ALPHA, MODE_WITH_SHADOW, MODE_HARDWARE_WITH_SHADOW, MODE_HARDWARE})
+    @interface BitmapGenerationMode {}
+
     private static final float ICON_BADGE_SCALE = 0.444f;
 
+    @NonNull
     private final Rect mOldBounds = new Rect();
+
+    @NonNull
     private final SparseBooleanArray mIsUserBadged = new SparseBooleanArray();
+
+    @NonNull
     protected final Context mContext;
+
+    @NonNull
     private final Canvas mCanvas;
+
+    @NonNull
     private final PackageManager mPm;
+
+    @NonNull
     private final ColorExtractor mColorExtractor;
-    private boolean mDisableColorExtractor;
 
     protected final int mFillResIconDpi;
     protected final int mIconBitmapSize;
 
     protected boolean mMonoIconEnabled;
 
+    @Nullable
     private IconNormalizer mNormalizer;
+
+    @Nullable
     private ShadowGenerator mShadowGenerator;
+
     private final boolean mShapeDetection;
 
     // Shadow bitmap used as background for theme icons
@@ -69,8 +102,6 @@
     private Drawable mWrapperIcon;
     private int mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
 
-    private final Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
-    private static final float PLACEHOLDER_TEXT_SIZE = 20f;
     private static int PLACEHOLDER_BACKGROUND_COLOR = Color.rgb(245, 245, 245);
 
     protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize,
@@ -85,10 +116,6 @@
 
         mCanvas = new Canvas();
         mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG));
-        mTextPaint.setTextAlign(Paint.Align.CENTER);
-        mTextPaint.setColor(PLACEHOLDER_BACKGROUND_COLOR);
-        mTextPaint.setTextSize(context.getResources().getDisplayMetrics().density *
-                PLACEHOLDER_TEXT_SIZE);
         clear();
     }
 
@@ -98,9 +125,9 @@
 
     protected void clear() {
         mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
-        mDisableColorExtractor = false;
     }
 
+    @NonNull
     public ShadowGenerator getShadowGenerator() {
         if (mShadowGenerator == null) {
             mShadowGenerator = new ShadowGenerator(mIconBitmapSize);
@@ -108,6 +135,7 @@
         return mShadowGenerator;
     }
 
+    @NonNull
     public IconNormalizer getNormalizer() {
         if (mNormalizer == null) {
             mNormalizer = new IconNormalizer(mContext, mIconBitmapSize, mShapeDetection);
@@ -138,16 +166,11 @@
      * @return
      */
     public BitmapInfo createIconBitmap(String placeholder, int color) {
-        Bitmap placeholderBitmap = Bitmap.createBitmap(mIconBitmapSize, mIconBitmapSize,
-                Bitmap.Config.ARGB_8888);
-        mTextPaint.setColor(color);
-        Canvas canvas = new Canvas(placeholderBitmap);
-        canvas.drawText(placeholder, mIconBitmapSize / 2, mIconBitmapSize * 5 / 8, mTextPaint);
         AdaptiveIconDrawable drawable = new AdaptiveIconDrawable(
                 new ColorDrawable(PLACEHOLDER_BACKGROUND_COLOR),
-                new BitmapDrawable(mContext.getResources(), placeholderBitmap));
+                new CenterTextDrawable(placeholder, color));
         Bitmap icon = createIconBitmap(drawable, IconNormalizer.ICON_VISIBLE_AREA_FACTOR);
-        return BitmapInfo.of(icon, extractColor(icon));
+        return BitmapInfo.of(icon, color);
     }
 
     public BitmapInfo createIconBitmap(Bitmap icon) {
@@ -155,12 +178,13 @@
             icon = createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f);
         }
 
-        return BitmapInfo.of(icon, extractColor(icon));
+        return BitmapInfo.of(icon, mColorExtractor.findDominantColorByHue(icon));
     }
 
     /**
      * Creates an icon from the bitmap cropped to the current device icon shape
      */
+    @NonNull
     public BitmapInfo createShapedIconBitmap(Bitmap icon, IconOptions options) {
         Drawable d = new FixedSizeBitmapDrawable(icon);
         float inset = getExtraInsetFraction();
@@ -170,6 +194,7 @@
         return createBadgedIconBitmap(d, options);
     }
 
+    @NonNull
     public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon) {
         return createBadgedIconBitmap(icon, null);
     }
@@ -182,38 +207,47 @@
      * @return a bitmap suitable for disaplaying as an icon at various system UIs.
      */
     @TargetApi(Build.VERSION_CODES.TIRAMISU)
+    @NonNull
     public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon,
             @Nullable IconOptions options) {
         boolean shrinkNonAdaptiveIcons = options == null || options.mShrinkNonAdaptiveIcons;
         float[] scale = new float[1];
         icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, null, scale);
-        Bitmap bitmap = createIconBitmap(icon, scale[0]);
-        if (icon instanceof AdaptiveIconDrawable) {
-            mCanvas.setBitmap(bitmap);
-            getShadowGenerator().recreateIcon(Bitmap.createBitmap(bitmap), mCanvas);
-            mCanvas.setBitmap(null);
-        }
+        Bitmap bitmap = createIconBitmap(icon, scale[0],
+                options == null ? MODE_WITH_SHADOW : options.mGenerationMode);
 
-        int color = extractColor(bitmap);
+        int color = (options != null && options.mExtractedColor != null)
+                ? options.mExtractedColor : mColorExtractor.findDominantColorByHue(bitmap);
         BitmapInfo info = BitmapInfo.of(bitmap, color);
 
         if (icon instanceof BitmapInfo.Extender) {
             info = ((BitmapInfo.Extender) icon).getExtendedInfo(bitmap, color, this, scale[0]);
-        } else if (mMonoIconEnabled && IconProvider.ATLEAST_T
-                && icon instanceof AdaptiveIconDrawable) {
-            Drawable mono = ((AdaptiveIconDrawable) icon).getMonochrome();
+        } else if (IconProvider.ATLEAST_T && mMonoIconEnabled) {
+            Drawable mono = getMonochromeDrawable(icon);
             if (mono != null) {
-                // Convert mono drawable to bitmap
-                Drawable paddedMono = new ClippedMonoDrawable(mono);
-                info.setMonoIcon(
-                        createIconBitmap(paddedMono, scale[0], mIconBitmapSize, Config.ALPHA_8),
-                        this);
+                info.setMonoIcon(createIconBitmap(mono, scale[0], MODE_ALPHA), this);
             }
         }
         info = info.withFlags(getBitmapFlagOp(options));
         return info;
     }
 
+    /**
+     * Returns a monochromatic version of the given drawable or null, if it is not supported
+     * @param base the original icon
+     */
+    @TargetApi(Build.VERSION_CODES.TIRAMISU)
+    protected Drawable getMonochromeDrawable(Drawable base) {
+        if (base instanceof AdaptiveIconDrawable) {
+            Drawable mono = ((AdaptiveIconDrawable) base).getMonochrome();
+            if (mono != null) {
+                return new ClippedMonoDrawable(mono);
+            }
+        }
+        return null;
+    }
+
+    @NonNull
     public FlagOp getBitmapFlagOp(@Nullable IconOptions options) {
         FlagOp op = FlagOp.NO_OP;
         if (options != null) {
@@ -233,55 +267,45 @@
                     isBadged = (d != mPm.getUserBadgedIcon(d, options.mUserHandle));
                     mIsUserBadged.put(key, isBadged);
                 }
-                op = op.setFlag(FLAG_WORK, isBadged);
+                // Set the clone profile badge flag in case it is present.
+                op = op.setFlag(FLAG_CLONE, isBadged && options.mIsCloneProfile);
+                // Set the Work profile badge for all other cases.
+                op = op.setFlag(FLAG_WORK, isBadged && !options.mIsCloneProfile);
             }
         }
         return op;
     }
 
-    /** package private */
-    Bitmap getWhiteShadowLayer() {
+    @NonNull
+    public Bitmap getWhiteShadowLayer() {
         if (mWhiteShadowLayer == null) {
-            mWhiteShadowLayer = createScaledBitmapWithShadow(
-                    new AdaptiveIconDrawable(new ColorDrawable(Color.WHITE), null));
+            mWhiteShadowLayer = createScaledBitmap(
+                    new AdaptiveIconDrawable(new ColorDrawable(Color.WHITE), null),
+                    MODE_HARDWARE_WITH_SHADOW);
         }
         return mWhiteShadowLayer;
     }
 
-    /** package private */
-    public Bitmap createScaledBitmapWithShadow(Drawable d) {
-        float scale = getNormalizer().getScale(d, null, null, null);
-        Bitmap bitmap = createIconBitmap(d, scale);
-        mCanvas.setBitmap(bitmap);
-        getShadowGenerator().recreateIcon(Bitmap.createBitmap(bitmap), mCanvas);
-        mCanvas.setBitmap(null);
-        return bitmap;
-    }
-
-    public Bitmap createScaledBitmapWithoutShadow(Drawable icon) {
+    @NonNull
+    public Bitmap createScaledBitmap(@NonNull Drawable icon, @BitmapGenerationMode int mode) {
         RectF iconBounds = new RectF();
         float[] scale = new float[1];
         icon = normalizeAndWrapToAdaptiveIcon(icon, true, iconBounds, scale);
         return createIconBitmap(icon,
-                Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds)));
+                Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds)), mode);
     }
 
     /**
      * Sets the background color used for wrapped adaptive icon
      */
-    public void setWrapperBackgroundColor(int color) {
+    public void setWrapperBackgroundColor(final int color) {
         mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color;
     }
 
-    /**
-     * Disables the dominant color extraction for all icons loaded.
-     */
-    public void disableColorExtraction() {
-        mDisableColorExtractor = true;
-    }
-
-    private Drawable normalizeAndWrapToAdaptiveIcon(@NonNull Drawable icon,
-            boolean shrinkNonAdaptiveIcons, RectF outIconBounds, float[] outScale) {
+    @Nullable
+    protected Drawable normalizeAndWrapToAdaptiveIcon(@Nullable Drawable icon,
+            final boolean shrinkNonAdaptiveIcons, @Nullable final RectF outIconBounds,
+            @NonNull final float[] outScale) {
         if (icon == null) {
             return null;
         }
@@ -312,46 +336,69 @@
         return icon;
     }
 
-    private Bitmap createIconBitmap(Drawable icon, float scale) {
-        return createIconBitmap(icon, scale, mIconBitmapSize);
+    @NonNull
+    protected Bitmap createIconBitmap(@Nullable final Drawable icon, final float scale) {
+        return createIconBitmap(icon, scale, MODE_DEFAULT);
     }
 
-    /**
-     * @param icon drawable that should be flattened to a bitmap
-     * @param scale the scale to apply before drawing {@param icon} on the canvas
-     */
-    public Bitmap createIconBitmap(@NonNull Drawable icon, float scale, int size) {
-        return createIconBitmap(icon, scale, size, Bitmap.Config.ARGB_8888);
-    }
-
-    private Bitmap createIconBitmap(@NonNull Drawable icon, float scale, int size,
-            Bitmap.Config config) {
-        Bitmap bitmap = Bitmap.createBitmap(size, size, config);
+    @NonNull
+    protected Bitmap createIconBitmap(@Nullable final Drawable icon, final float scale,
+            @BitmapGenerationMode int bitmapGenerationMode) {
+        final int size = mIconBitmapSize;
+        final Bitmap bitmap;
+        switch (bitmapGenerationMode) {
+            case MODE_ALPHA:
+                bitmap = Bitmap.createBitmap(size, size, Config.ALPHA_8);
+                break;
+            case MODE_HARDWARE:
+            case MODE_HARDWARE_WITH_SHADOW: {
+                return BitmapRenderer.createHardwareBitmap(size, size, canvas ->
+                        drawIconBitmap(canvas, icon, scale, bitmapGenerationMode, null));
+            }
+            case MODE_WITH_SHADOW:
+            default:
+                bitmap = Bitmap.createBitmap(size, size, Config.ARGB_8888);
+                break;
+        }
         if (icon == null) {
             return bitmap;
         }
         mCanvas.setBitmap(bitmap);
+        drawIconBitmap(mCanvas, icon, scale, bitmapGenerationMode, bitmap);
+        mCanvas.setBitmap(null);
+        return bitmap;
+    }
+
+    private void drawIconBitmap(@NonNull Canvas canvas, @Nullable final Drawable icon,
+            final float scale, @BitmapGenerationMode int bitmapGenerationMode,
+            @Nullable Bitmap targetBitmap) {
+        final int size = mIconBitmapSize;
         mOldBounds.set(icon.getBounds());
 
         if (icon instanceof AdaptiveIconDrawable) {
             int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size),
-                    Math.round(size * (1 - scale) / 2 ));
+                    Math.round(size * (1 - scale) / 2));
             // b/211896569: AdaptiveIconDrawable do not work properly for non top-left bounds
             icon.setBounds(0, 0, size - offset - offset, size - offset - offset);
-            int count = mCanvas.save();
-            mCanvas.translate(offset, offset);
+            int count = canvas.save();
+            canvas.translate(offset, offset);
+            if (bitmapGenerationMode == MODE_WITH_SHADOW
+                    || bitmapGenerationMode == MODE_HARDWARE_WITH_SHADOW) {
+                getShadowGenerator().addPathShadow(
+                        ((AdaptiveIconDrawable) icon).getIconMask(), canvas);
+            }
 
             if (icon instanceof BitmapInfo.Extender) {
-                ((Extender) icon).drawForPersistence(mCanvas);
+                ((Extender) icon).drawForPersistence(canvas);
             } else {
-                icon.draw(mCanvas);
+                icon.draw(canvas);
             }
-            mCanvas.restoreToCount(count);
+            canvas.restoreToCount(count);
         } else {
             if (icon instanceof BitmapDrawable) {
                 BitmapDrawable bitmapDrawable = (BitmapDrawable) icon;
                 Bitmap b = bitmapDrawable.getBitmap();
-                if (bitmap != null && b.getDensity() == Bitmap.DENSITY_NONE) {
+                if (b != null && b.getDensity() == Bitmap.DENSITY_NONE) {
                     bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics());
                 }
             }
@@ -372,15 +419,24 @@
             final int left = (size - width) / 2;
             final int top = (size - height) / 2;
             icon.setBounds(left, top, left + width, top + height);
-            mCanvas.save();
-            mCanvas.scale(scale, scale, size / 2, size / 2);
-            icon.draw(mCanvas);
-            mCanvas.restore();
 
+            canvas.save();
+            canvas.scale(scale, scale, size / 2, size / 2);
+            icon.draw(canvas);
+            canvas.restore();
+
+            if (bitmapGenerationMode == MODE_WITH_SHADOW && targetBitmap != null) {
+                // Shadow extraction only works in software mode
+                getShadowGenerator().drawShadow(targetBitmap, canvas);
+
+                // Draw the icon again on top:
+                canvas.save();
+                canvas.scale(scale, scale, size / 2, size / 2);
+                icon.draw(canvas);
+                canvas.restore();
+            }
         }
         icon.setBounds(mOldBounds);
-        mCanvas.setBitmap(null);
-        return bitmap;
     }
 
     @Override
@@ -388,36 +444,45 @@
         clear();
     }
 
+    @NonNull
     public BitmapInfo makeDefaultIcon() {
         return createBadgedIconBitmap(getFullResDefaultActivityIcon(mFillResIconDpi));
     }
 
-    public static Drawable getFullResDefaultActivityIcon(int iconDpi) {
-        return Resources.getSystem().getDrawableForDensity(
-                android.R.drawable.sym_def_app_icon, iconDpi);
-    }
-
-    private int extractColor(Bitmap bitmap) {
-        return mDisableColorExtractor ? 0 : mColorExtractor.findDominantColorByHue(bitmap);
+    @NonNull
+    public static Drawable getFullResDefaultActivityIcon(final int iconDpi) {
+        return Objects.requireNonNull(Resources.getSystem().getDrawableForDensity(
+                android.R.drawable.sym_def_app_icon, iconDpi));
     }
 
     /**
      * Returns the correct badge size given an icon size
      */
-    public static int getBadgeSizeForIconSize(int iconSize) {
+    public static int getBadgeSizeForIconSize(final int iconSize) {
         return (int) (ICON_BADGE_SCALE * iconSize);
     }
 
     public static class IconOptions {
 
         boolean mShrinkNonAdaptiveIcons = true;
+
         boolean mIsInstantApp;
-        UserHandle mUserHandle;
+
+        boolean mIsCloneProfile;
+
+        @BitmapGenerationMode
+        int mGenerationMode = MODE_WITH_SHADOW;
+
+        @Nullable UserHandle mUserHandle;
+
+        @ColorInt
+        @Nullable Integer mExtractedColor;
 
         /**
          * Set to false if non-adaptive icons should not be treated
          */
-        public IconOptions setShrinkNonAdaptiveIcons(boolean shrink) {
+        @NonNull
+        public IconOptions setShrinkNonAdaptiveIcons(final boolean shrink) {
             mShrinkNonAdaptiveIcons = shrink;
             return this;
         }
@@ -425,7 +490,8 @@
         /**
          * User for this icon, in case of badging
          */
-        public IconOptions setUser(UserHandle user) {
+        @NonNull
+        public IconOptions setUser(@Nullable final UserHandle user) {
             mUserHandle = user;
             return this;
         }
@@ -433,10 +499,39 @@
         /**
          * If this icon represents an instant app
          */
-        public IconOptions setInstantApp(boolean instantApp) {
+        @NonNull
+        public IconOptions setInstantApp(final boolean instantApp) {
             mIsInstantApp = instantApp;
             return this;
         }
+
+        /**
+         * Disables auto color extraction and overrides the color to the provided value
+         */
+        @NonNull
+        public IconOptions setExtractedColor(@ColorInt int color) {
+            mExtractedColor = color;
+            return this;
+        }
+
+        /**
+         * Sets the bitmap generation mode to use for the bitmap info. Note that some generation
+         * modes do not support color extraction, so consider setting a extracted color manually
+         * in those cases.
+         */
+        public IconOptions setBitmapGenerationMode(@BitmapGenerationMode int generationMode) {
+            mGenerationMode = generationMode;
+            return this;
+        }
+
+        /**
+         * Used to determine the badge type for this icon.
+         */
+        @NonNull
+        public IconOptions setIsCloneProfile(boolean isCloneProfile) {
+            mIsCloneProfile = isCloneProfile;
+            return this;
+        }
     }
 
     /**
@@ -446,7 +541,7 @@
      */
     private static class FixedSizeBitmapDrawable extends BitmapDrawable {
 
-        public FixedSizeBitmapDrawable(Bitmap bitmap) {
+        public FixedSizeBitmapDrawable(@Nullable final Bitmap bitmap) {
             super(null, bitmap);
         }
 
@@ -473,11 +568,12 @@
         }
     }
 
-    private static class ClippedMonoDrawable extends InsetDrawable {
+    protected static class ClippedMonoDrawable extends InsetDrawable {
 
+        @NonNull
         private final AdaptiveIconDrawable mCrop;
 
-        public ClippedMonoDrawable(Drawable base) {
+        public ClippedMonoDrawable(@Nullable final Drawable base) {
             super(base, -getExtraInsetFraction());
             mCrop = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null);
         }
@@ -491,4 +587,32 @@
             canvas.restoreToCount(saveCount);
         }
     }
+
+    private static class CenterTextDrawable extends ColorDrawable {
+
+        @NonNull
+        private final Rect mTextBounds = new Rect();
+
+        @NonNull
+        private final Paint mTextPaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG);
+
+        @NonNull
+        private final String mText;
+
+        CenterTextDrawable(@NonNull final String text, final int color) {
+            mText = text;
+            mTextPaint.setColor(color);
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            Rect bounds = getBounds();
+            mTextPaint.setTextSize(bounds.height() / 3f);
+            mTextPaint.getTextBounds(mText, 0, mText.length(), mTextBounds);
+            canvas.drawText(mText,
+                    bounds.exactCenterX() - mTextBounds.exactCenterX(),
+                    bounds.exactCenterY() - mTextBounds.exactCenterY(),
+                    mTextPaint);
+        }
+    }
 }
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
index c3ca42e..d1ef6f7 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.icons;
 
 import android.content.Context;
+import android.content.res.Configuration;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.Config;
 import android.graphics.Canvas;
@@ -30,9 +31,11 @@
 
     static final int FLAG_WORK = 1 << 0;
     static final int FLAG_INSTANT = 1 << 1;
+    static final int FLAG_CLONE = 1 << 2;
     @IntDef(flag = true, value = {
             FLAG_WORK,
             FLAG_INSTANT,
+            FLAG_CLONE
     })
     @interface BitmapInfoFlags {}
 
@@ -152,9 +155,17 @@
             if (badgeInfo != null) {
                 drawable.setBadge(badgeInfo.newIcon(context, creationFlags));
             } else if ((flags & FLAG_INSTANT) != 0) {
-                drawable.setBadge(context.getDrawable(R.drawable.ic_instant_app_badge));
+                drawable.setBadge(context.getDrawable(drawable.isThemed()
+                        ? R.drawable.ic_instant_app_badge_themed
+                        : R.drawable.ic_instant_app_badge));
             } else if ((flags & FLAG_WORK) != 0) {
-                drawable.setBadge(context.getDrawable(R.drawable.ic_work_app_badge));
+                drawable.setBadge(context.getDrawable(drawable.isThemed()
+                        ? R.drawable.ic_work_app_badge_themed
+                        : R.drawable.ic_work_app_badge));
+            } else if ((flags & FLAG_CLONE) != 0) {
+                drawable.setBadge(context.getDrawable(drawable.isThemed()
+                        ? R.drawable.ic_clone_app_badge_themed
+                        : R.drawable.ic_clone_app_badge));
             }
         }
     }
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BubbleIconFactory.java b/iconloaderlib/src/com/android/launcher3/icons/BubbleIconFactory.java
new file mode 100644
index 0000000..a4ac812
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/BubbleIconFactory.java
@@ -0,0 +1,160 @@
+package com.android.launcher3.icons;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Factory for creating normalized bubble icons and app badges.
+ */
+public class BubbleIconFactory extends BaseIconFactory {
+
+    private final int mRingColor;
+    private final int mRingWidth;
+
+    private final BaseIconFactory mBadgeFactory;
+
+    /**
+     * Creates a bubble icon factory.
+     *
+     * @param context the context for the factory.
+     * @param iconSize the size of the bubble icon (i.e. the large icon for the bubble).
+     * @param badgeSize the size of the badge (i.e. smaller icon shown on top of the large icon).
+     * @param ringColor the color of the ring optionally shown around the badge.
+     * @param ringWidth the width of the ring optionally shown around the badge.
+     */
+    public BubbleIconFactory(Context context, int iconSize, int badgeSize, int ringColor,
+            int ringWidth) {
+        super(context, context.getResources().getConfiguration().densityDpi, iconSize);
+        mRingColor = ringColor;
+        mRingWidth = ringWidth;
+
+        mBadgeFactory = new BaseIconFactory(context,
+                context.getResources().getConfiguration().densityDpi,
+                badgeSize);
+    }
+
+    /**
+     * Returns the drawable that the developer has provided to display in the bubble.
+     */
+    public Drawable getBubbleDrawable(@NonNull final Context context,
+            @Nullable final ShortcutInfo shortcutInfo, @Nullable final Icon ic) {
+        if (shortcutInfo != null) {
+            LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
+            int density = context.getResources().getConfiguration().densityDpi;
+            return launcherApps.getShortcutIconDrawable(shortcutInfo, density);
+        } else {
+            if (ic != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                if (ic.getType() == Icon.TYPE_URI
+                        || ic.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) {
+                    context.grantUriPermission(context.getPackageName(),
+                            ic.getUri(),
+                            Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                }
+                return ic.loadDrawable(context);
+            }
+            return null;
+        }
+    }
+
+    /**
+     * Creates the bitmap for the provided drawable and returns the scale used for
+     * drawing the actual drawable. This is used for the larger icon shown for the bubble.
+     */
+    public Bitmap getBubbleBitmap(@NonNull Drawable icon, float[] outScale) {
+        if (outScale == null) {
+            outScale = new float[1];
+        }
+        icon = normalizeAndWrapToAdaptiveIcon(icon,
+                true /* shrinkNonAdaptiveIcons */,
+                null /* outscale */,
+                outScale);
+        return createIconBitmap(icon, outScale[0], MODE_WITH_SHADOW);
+    }
+
+    /**
+     * Returns a {@link BitmapInfo} for the app-badge that is shown on top of each bubble. This
+     * will include the workprofile indicator on the badge if appropriate.
+     */
+    public BitmapInfo getBadgeBitmap(Drawable userBadgedAppIcon, boolean isImportantConversation) {
+        if (userBadgedAppIcon instanceof AdaptiveIconDrawable) {
+            AdaptiveIconDrawable ad = (AdaptiveIconDrawable) userBadgedAppIcon;
+            userBadgedAppIcon = new CircularAdaptiveIcon(ad.getBackground(),
+                    ad.getForeground());
+        }
+        if (isImportantConversation) {
+            userBadgedAppIcon = new CircularRingDrawable(userBadgedAppIcon);
+        }
+        Bitmap userBadgedBitmap = mBadgeFactory.createIconBitmap(
+                userBadgedAppIcon, 1, MODE_WITH_SHADOW);
+        return mBadgeFactory.createIconBitmap(userBadgedBitmap);
+    }
+
+    private class CircularRingDrawable extends CircularAdaptiveIcon {
+        final Rect mInnerBounds = new Rect();
+
+        final Drawable mDr;
+
+        CircularRingDrawable(Drawable dr) {
+            super(null, null);
+            mDr = dr;
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            int save = canvas.save();
+            canvas.clipPath(getIconMask());
+            canvas.drawColor(mRingColor);
+            mInnerBounds.set(getBounds());
+            mInnerBounds.inset(mRingWidth, mRingWidth);
+            canvas.translate(mInnerBounds.left, mInnerBounds.top);
+            mDr.setBounds(0, 0, mInnerBounds.width(), mInnerBounds.height());
+            mDr.draw(canvas);
+            canvas.restoreToCount(save);
+        }
+    }
+
+    private static class CircularAdaptiveIcon extends AdaptiveIconDrawable {
+
+        final Path mPath = new Path();
+
+        CircularAdaptiveIcon(Drawable bg, Drawable fg) {
+            super(bg, fg);
+        }
+
+        @Override
+        public Path getIconMask() {
+            mPath.reset();
+            Rect bounds = getBounds();
+            mPath.addOval(bounds.left, bounds.top, bounds.right, bounds.bottom, Path.Direction.CW);
+            return mPath;
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            int save = canvas.save();
+            canvas.clipPath(getIconMask());
+
+            Drawable d;
+            if ((d = getBackground()) != null) {
+                d.draw(canvas);
+            }
+            if ((d = getForeground()) != null) {
+                d.draw(canvas);
+            }
+            canvas.restoreToCount(save);
+        }
+    }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java b/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java
index d624805..252c0c3 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java
@@ -214,7 +214,8 @@
             BaseIconFactory iconFactory, float normalizationScale) {
         AdaptiveIconDrawable background = new AdaptiveIconDrawable(
                 getBackground().getConstantState().newDrawable(), null);
-        Bitmap flattenBG = iconFactory.createScaledBitmapWithShadow(background);
+        Bitmap flattenBG = iconFactory.createScaledBitmap(background,
+                BaseIconFactory.MODE_HARDWARE_WITH_SHADOW);
 
         // Only pass theme info if mono-icon is enabled
         AnimationInfo themeInfo = iconFactory.mMonoIconEnabled ? mThemeInfo : null;
@@ -387,7 +388,8 @@
             mBgPaint.setColorFilter(cs.mBgFilter);
             mThemedFgColor = cs.mThemedFgColor;
 
-            mFullDrawable = (AdaptiveIconDrawable) mAnimInfo.baseDrawableState.newDrawable();
+            mFullDrawable =
+                    (AdaptiveIconDrawable) mAnimInfo.baseDrawableState.newDrawable().mutate();
             mFG = (LayerDrawable) mFullDrawable.getForeground();
 
             // Time needs to be applied here since drawInternal is NOT guaranteed to be called
@@ -397,6 +399,13 @@
         }
 
         @Override
+        public void setAlpha(int alpha) {
+            super.setAlpha(alpha);
+            mBgPaint.setAlpha(alpha);
+            mFG.setAlpha(alpha);
+        }
+
+        @Override
         protected void onBoundsChange(Rect bounds) {
             super.onBoundsChange(bounds);
 
@@ -434,8 +443,7 @@
         protected void updateFilter() {
             super.updateFilter();
             int alpha = mIsDisabled ? (int) (mDisabledAlpha * FULLY_OPAQUE) : FULLY_OPAQUE;
-            mBgPaint.setAlpha(alpha);
-            mFG.setAlpha(alpha);
+            setAlpha(alpha);
             mBgPaint.setColorFilter(mIsDisabled ? getDisabledColorFilter() : mBgFilter);
             mFG.setColorFilter(mIsDisabled ? getDisabledColorFilter() : null);
         }
diff --git a/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java b/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java
index 87bda82..5a5e7d0 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java
@@ -18,6 +18,9 @@
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+
 import java.util.Arrays;
 
 /**
@@ -26,16 +29,24 @@
 public class ColorExtractor {
 
     private final int NUM_SAMPLES = 20;
+
+    @NonNull
     private final float[] mTmpHsv = new float[3];
+
+    @NonNull
     private final float[] mTmpHueScoreHistogram = new float[360];
+
+    @NonNull
     private final int[] mTmpPixels = new int[NUM_SAMPLES];
+
+    @NonNull
     private final SparseArray<Float> mTmpRgbScores = new SparseArray<>();
 
     /**
      * This picks a dominant color, looking for high-saturation, high-value, repeated hues.
      * @param bitmap The bitmap to scan
      */
-    public int findDominantColorByHue(Bitmap bitmap) {
+    public int findDominantColorByHue(@NonNull final Bitmap bitmap) {
         return findDominantColorByHue(bitmap, NUM_SAMPLES);
     }
 
@@ -43,7 +54,7 @@
      * This picks a dominant color, looking for high-saturation, high-value, repeated hues.
      * @param bitmap The bitmap to scan
      */
-    public int findDominantColorByHue(Bitmap bitmap, int samples) {
+    protected int findDominantColorByHue(@NonNull final Bitmap bitmap, final int samples) {
         final int height = bitmap.getHeight();
         final int width = bitmap.getWidth();
         int sampleStride = (int) Math.sqrt((height * width) / samples);
diff --git a/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java b/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
index 17b0016..3455dba 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
@@ -16,9 +16,11 @@
 package com.android.launcher3.icons;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.Color;
+import android.graphics.Matrix;
 import android.graphics.Path;
 import android.graphics.Rect;
 import android.graphics.Region;
@@ -28,6 +30,8 @@
 import android.util.Log;
 
 import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.core.graphics.PathParser;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -35,6 +39,7 @@
 public class GraphicsUtils {
 
     private static final String TAG = "GraphicsUtils";
+    private static final float MASK_SIZE = 100f;
 
     public static Runnable sOnNewBitmapRunnable = () -> { };
 
@@ -98,7 +103,20 @@
     /**
      * Returns the default path to be used by an icon
      */
-    public static Path getShapePath(int size) {
+    public static Path getShapePath(@NonNull Context context, int size) {
+        if (IconProvider.CONFIG_ICON_MASK_RES_ID != Resources.ID_NULL) {
+            Path path = PathParser.createPathFromPathData(
+                    context.getString(IconProvider.CONFIG_ICON_MASK_RES_ID));
+            if (path != null) {
+                if (size != MASK_SIZE) {
+                    Matrix m = new Matrix();
+                    float scale = ((float) size) / MASK_SIZE;
+                    m.setScale(scale, scale);
+                    path.transform(m);
+                }
+                return path;
+            }
+        }
         AdaptiveIconDrawable drawable = new AdaptiveIconDrawable(
                 new ColorDrawable(Color.BLACK), new ColorDrawable(Color.BLACK));
         drawable.setBounds(0, 0, size, size);
diff --git a/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java b/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
index 204651c..e8ce3b1 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
@@ -61,7 +61,7 @@
 public class IconProvider {
 
     private final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
-    private static final int CONFIG_ICON_MASK_RES_ID = Resources.getSystem().getIdentifier(
+    static final int CONFIG_ICON_MASK_RES_ID = Resources.getSystem().getIdentifier(
             "config_icon_mask", "string", "android");
 
     private static final String TAG = "IconProvider";
diff --git a/iconloaderlib/src/com/android/launcher3/icons/PlaceHolderIconDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/PlaceHolderIconDrawable.java
index 5f3343e..71a80cb 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/PlaceHolderIconDrawable.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/PlaceHolderIconDrawable.java
@@ -40,7 +40,7 @@
     public PlaceHolderIconDrawable(BitmapInfo info, Context context) {
         super(info);
 
-        mProgressPath = GraphicsUtils.getShapePath(100);
+        mProgressPath = GraphicsUtils.getShapePath(context, 100);
         mPaint.setColor(ColorUtils.compositeColors(
                 GraphicsUtils.getAttrColor(context, R.attr.loadingIconColor), info.color));
     }
diff --git a/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java b/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java
index 96dee3b..99f6813 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java
@@ -24,6 +24,7 @@
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Paint;
+import android.graphics.Path;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffXfermode;
 import android.graphics.RectF;
@@ -57,30 +58,41 @@
         mDefaultBlurMaskFilter = new BlurMaskFilter(mIconSize * BLUR_FACTOR, Blur.NORMAL);
     }
 
-    public synchronized void recreateIcon(Bitmap icon, Canvas out) {
-        recreateIcon(icon, mDefaultBlurMaskFilter, AMBIENT_SHADOW_ALPHA, KEY_SHADOW_ALPHA, out);
-    }
-
-    public synchronized void recreateIcon(Bitmap icon, BlurMaskFilter blurMaskFilter,
-            int ambientAlpha, int keyAlpha, Canvas out) {
+    public synchronized void drawShadow(Bitmap icon, Canvas out) {
         if (ENABLE_SHADOWS) {
             int[] offset = new int[2];
-            mBlurPaint.setMaskFilter(blurMaskFilter);
+            mBlurPaint.setMaskFilter(mDefaultBlurMaskFilter);
             Bitmap shadow = icon.extractAlpha(mBlurPaint, offset);
 
             // Draw ambient shadow
-            mDrawPaint.setAlpha(ambientAlpha);
+            mDrawPaint.setAlpha(AMBIENT_SHADOW_ALPHA);
             out.drawBitmap(shadow, offset[0], offset[1], mDrawPaint);
 
             // Draw key shadow
-            mDrawPaint.setAlpha(keyAlpha);
+            mDrawPaint.setAlpha(KEY_SHADOW_ALPHA);
             out.drawBitmap(shadow, offset[0], offset[1] + KEY_SHADOW_DISTANCE * mIconSize,
                     mDrawPaint);
         }
+    }
 
-        // Draw the icon
-        mDrawPaint.setAlpha(255);
-        out.drawBitmap(icon, 0, 0, mDrawPaint);
+    /** package private **/
+    void addPathShadow(Path path, Canvas out) {
+        if (ENABLE_SHADOWS) {
+            mDrawPaint.setMaskFilter(mDefaultBlurMaskFilter);
+
+            // Draw ambient shadow
+            mDrawPaint.setAlpha(AMBIENT_SHADOW_ALPHA);
+            out.drawPath(path, mDrawPaint);
+
+            // Draw key shadow
+            int save = out.save();
+            mDrawPaint.setAlpha(KEY_SHADOW_ALPHA);
+            out.translate(0, KEY_SHADOW_DISTANCE * mIconSize);
+            out.drawPath(path, mDrawPaint);
+            out.restoreToCount(save);
+
+            mDrawPaint.setMaskFilter(null);
+        }
     }
 
     /**
diff --git a/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java
index 494d657..6724d6b 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java
@@ -24,6 +24,7 @@
 import android.graphics.BlendMode;
 import android.graphics.BlendModeColorFilter;
 import android.graphics.Canvas;
+import android.graphics.Color;
 import android.graphics.ColorFilter;
 import android.graphics.Paint;
 import android.graphics.Rect;
@@ -92,8 +93,11 @@
         return new ThemedConstantState(bitmapInfo, colorBg, colorFg);
     }
 
-    public void changeBackgroundColor(int colorBg){
+    public void changeBackgroundColor(int colorBg) {
+        if (mIsDisabled) return;
+
         mBgPaint.setColorFilter(new BlendModeColorFilter(colorBg, BlendMode.SRC_IN));
+        invalidateSelf();
     }
 
     static class ThemedConstantState extends FastBitmapConstantState {
@@ -125,13 +129,8 @@
     public static int[] getColors(Context context) {
         Resources res = context.getResources();
         int[] colors = new int[2];
-        if ((res.getConfiguration().uiMode & UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES) {
-            colors[0] = res.getColor(android.R.color.system_neutral1_800);
-            colors[1] = res.getColor(android.R.color.system_accent1_100);
-        } else {
-            colors[0] = res.getColor(android.R.color.system_accent1_100);
-            colors[1] = res.getColor(android.R.color.system_neutral2_700);
-        }
+        colors[0] = res.getColor(R.color.themed_icon_background_color);
+        colors[1] = res.getColor(R.color.themed_icon_color);
         return colors;
     }
 
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
index 057bdc2..bdc4410 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
@@ -45,6 +45,7 @@
 import android.os.LocaleList;
 import android.os.Looper;
 import android.os.Process;
+import android.os.SystemClock;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.text.TextUtils;
@@ -54,6 +55,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.icons.BaseIconFactory;
 import com.android.launcher3.icons.BaseIconFactory.IconOptions;
@@ -78,6 +80,8 @@
     private static final boolean DEBUG = false;
 
     private static final int INITIAL_ICON_CACHE_CAPACITY = 50;
+    // A format string which returns the original string as is.
+    private static final String IDENTITY_FORMAT_STRING = "%1$s";
 
     // Empty class name is used for storing package default entry.
     public static final String EMPTY_CLASS_NAME = ".";
@@ -86,29 +90,52 @@
 
         @NonNull
         public BitmapInfo bitmap = BitmapInfo.LOW_RES_INFO;
+        @NonNull
         public CharSequence title = "";
+        @NonNull
         public CharSequence contentDescription = "";
     }
 
+    @NonNull
     protected final Context mContext;
+
+    @NonNull
     protected final PackageManager mPackageManager;
 
+    @NonNull
     private final Map<ComponentKey, CacheEntry> mCache;
+
+    @NonNull
     protected final Handler mWorkerHandler;
 
     protected int mIconDpi;
+
+    @NonNull
     protected IconDB mIconDb;
+
+    @NonNull
     protected LocaleList mLocaleList = LocaleList.getEmptyLocaleList();
+
+    @NonNull
     protected String mSystemState = "";
 
+    @Nullable
     private BitmapInfo mDefaultIcon;
+
+    @NonNull
     private final SparseArray<FlagOp> mUserFlagOpMap = new SparseArray<>();
 
+    private final SparseArray<String> mUserFormatString = new SparseArray<>();
+
+    @Nullable
     private final String mDbFileName;
+
+    @NonNull
     private final Looper mBgLooper;
 
-    public BaseIconCache(Context context, String dbFileName, Looper bgLooper,
-            int iconDpi, int iconPixelSize, boolean inMemoryCache) {
+    public BaseIconCache(@NonNull final Context context, @Nullable final String dbFileName,
+            @NonNull final Looper bgLooper, final int iconDpi, final int iconPixelSize,
+            final boolean inMemoryCache) {
         mContext = context;
         mDbFileName = dbFileName;
         mPackageManager = context.getPackageManager();
@@ -141,23 +168,24 @@
      * Returns the persistable serial number for {@param user}. Subclass should implement proper
      * caching strategy to avoid making binder call every time.
      */
-    protected abstract long getSerialNumberForUser(UserHandle user);
+    protected abstract long getSerialNumberForUser(@NonNull final UserHandle user);
 
     /**
      * Return true if the given app is an instant app and should be badged appropriately.
      */
-    protected abstract boolean isInstantApp(ApplicationInfo info);
+    protected abstract boolean isInstantApp(@NonNull final ApplicationInfo info);
 
     /**
      * Opens and returns an icon factory. The factory is recycled by the caller.
      */
+    @NonNull
     public abstract BaseIconFactory getIconFactory();
 
-    public void updateIconParams(int iconDpi, int iconPixelSize) {
+    public void updateIconParams(final int iconDpi, final int iconPixelSize) {
         mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize));
     }
 
-    private synchronized void updateIconParamsBg(int iconDpi, int iconPixelSize) {
+    private synchronized void updateIconParamsBg(final int iconDpi, final int iconPixelSize) {
         mIconDpi = iconDpi;
         mDefaultIcon = null;
         mUserFlagOpMap.clear();
@@ -167,7 +195,8 @@
         mCache.clear();
     }
 
-    private Drawable getFullResIcon(Resources resources, int iconId) {
+    @Nullable
+    private Drawable getFullResIcon(@Nullable final Resources resources, final int iconId) {
         if (resources != null && iconId != 0) {
             try {
                 return resources.getDrawableForDensity(iconId, mIconDpi);
@@ -176,14 +205,16 @@
         return getFullResDefaultActivityIcon(mIconDpi);
     }
 
-    public Drawable getFullResIcon(String packageName, int iconId) {
+    @Nullable
+    public Drawable getFullResIcon(@NonNull final String packageName, final int iconId) {
         try {
             return getFullResIcon(mPackageManager.getResourcesForApplication(packageName), iconId);
         } catch (PackageManager.NameNotFoundException e) { }
         return getFullResDefaultActivityIcon(mIconDpi);
     }
 
-    public Drawable getFullResIcon(ActivityInfo info) {
+    @Nullable
+    public Drawable getFullResIcon(@NonNull final ActivityInfo info) {
         try {
             return getFullResIcon(mPackageManager.getResourcesForApplication(info.applicationInfo),
                     info.getIconResource());
@@ -194,14 +225,16 @@
     /**
      * Remove any records for the supplied ComponentName.
      */
-    public synchronized void remove(ComponentName componentName, UserHandle user) {
+    public synchronized void remove(@NonNull final ComponentName componentName,
+            @NonNull final UserHandle user) {
         mCache.remove(new ComponentKey(componentName, user));
     }
 
     /**
      * Remove any records for the supplied package name from memory.
      */
-    private void removeFromMemCacheLocked(String packageName, UserHandle user) {
+    private void removeFromMemCacheLocked(@Nullable final String packageName,
+            @Nullable final UserHandle user) {
         HashSet<ComponentKey> forDeletion = new HashSet<>();
         for (ComponentKey key: mCache.keySet()) {
             if (key.componentName.getPackageName().equals(packageName)
@@ -217,7 +250,8 @@
     /**
      * Removes the entries related to the given package in memory and persistent DB.
      */
-    public synchronized void removeIconsForPkg(String packageName, UserHandle user) {
+    public synchronized void removeIconsForPkg(@NonNull final String packageName,
+            @NonNull final UserHandle user) {
         removeFromMemCacheLocked(packageName, user);
         long userSerial = getSerialNumberForUser(user);
         mIconDb.delete(
@@ -225,6 +259,7 @@
                 new String[]{packageName + "/%", Long.toString(userSerial)});
     }
 
+    @NonNull
     public IconCacheUpdateHandler getUpdateHandler() {
         updateSystemState();
         return new IconCacheUpdateHandler(this);
@@ -238,12 +273,30 @@
     private void updateSystemState() {
         mLocaleList = mContext.getResources().getConfiguration().getLocales();
         mSystemState = mLocaleList.toLanguageTags() + "," + Build.VERSION.SDK_INT;
+        mUserFormatString.clear();
     }
 
-    protected String getIconSystemState(String packageName) {
+    @NonNull
+    protected String getIconSystemState(@Nullable final String packageName) {
         return mSystemState;
     }
 
+    public CharSequence getUserBadgedLabel(CharSequence label, UserHandle user) {
+        int key = user.hashCode();
+        int index = mUserFormatString.indexOfKey(key);
+        String format;
+        if (index < 0) {
+            format = mPackageManager.getUserBadgedLabel(IDENTITY_FORMAT_STRING, user).toString();
+            if (TextUtils.equals(IDENTITY_FORMAT_STRING, format)) {
+                format = null;
+            }
+            mUserFormatString.put(key, format);
+        } else {
+            format = mUserFormatString.valueAt(index);
+        }
+        return format == null ? label : String.format(format, label);
+    }
+
     /**
      * Adds an entry into the DB and the in-memory cache.
      * @param replaceExisting if true, it will recreate the bitmap even if it already exists in
@@ -251,8 +304,9 @@
      *                        old data.
      */
     @VisibleForTesting
-    public synchronized <T> void addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic,
-            PackageInfo info, long userSerial, boolean replaceExisting) {
+    public synchronized <T> void addIconToDBAndMemCache(@NonNull final T object,
+            @NonNull final CachingLogic<T> cachingLogic, @NonNull final PackageInfo info,
+            final long userSerial, final boolean replaceExisting) {
         UserHandle user = cachingLogic.getUser(object);
         ComponentName componentName = cachingLogic.getComponent(object);
 
@@ -276,11 +330,12 @@
 
         CharSequence entryTitle = cachingLogic.getLabel(object);
         if (entryTitle == null) {
-            Log.d(TAG, "No label returned from caching logic instance: " + cachingLogic);
+            Log.wtf(TAG, "No label returned from caching logic instance: " + cachingLogic);
+            entryTitle = "";
         }
         entry.title = entryTitle;
 
-        entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
+        entry.contentDescription = getUserBadgedLabel(entry.title, user);
         if (cachingLogic.addToMemCache()) mCache.put(key, entry);
 
         ContentValues values = newContentValues(entry.bitmap, entry.title.toString(),
@@ -293,8 +348,8 @@
      * Updates {@param values} to contain versioning information and adds it to the DB.
      * @param values {@link ContentValues} containing icon & title
      */
-    private void addIconToDB(ContentValues values, ComponentName key,
-            PackageInfo info, long userSerial, long lastUpdateTime) {
+    private void addIconToDB(@NonNull final ContentValues values, @NonNull final ComponentName key,
+            @NonNull final PackageInfo info, final long userSerial, final long lastUpdateTime) {
         values.put(IconDB.COLUMN_COMPONENT, key.flattenToString());
         values.put(IconDB.COLUMN_USER, userSerial);
         values.put(IconDB.COLUMN_LAST_UPDATED, lastUpdateTime);
@@ -302,7 +357,8 @@
         mIconDb.insertOrReplace(values);
     }
 
-    public synchronized BitmapInfo getDefaultIcon(UserHandle user) {
+    @NonNull
+    public synchronized BitmapInfo getDefaultIcon(@NonNull final UserHandle user) {
         if (mDefaultIcon == null) {
             try (BaseIconFactory li = getIconFactory()) {
                 mDefaultIcon = li.makeDefaultIcon();
@@ -311,7 +367,8 @@
         return mDefaultIcon.withFlags(getUserFlagOpLocked(user));
     }
 
-    protected FlagOp getUserFlagOpLocked(UserHandle user) {
+    @NonNull
+    protected FlagOp getUserFlagOpLocked(@NonNull final UserHandle user) {
         int key = user.hashCode();
         int index;
         if ((index = mUserFlagOpMap.indexOfKey(key)) >= 0) {
@@ -325,7 +382,7 @@
         }
     }
 
-    public boolean isDefaultIcon(BitmapInfo icon, UserHandle user) {
+    public boolean isDefaultIcon(@NonNull final BitmapInfo icon, @NonNull final UserHandle user) {
         return getDefaultIcon(user).icon == icon.icon;
     }
 
@@ -333,10 +390,11 @@
      * Retrieves the entry from the cache. If the entry is not present, it creates a new entry.
      * This method is not thread safe, it must be called from a synchronized method.
      */
+    @NonNull
     protected <T> CacheEntry cacheLocked(
-            @NonNull ComponentName componentName, @NonNull UserHandle user,
-            @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic,
-            boolean usePackageIcon, boolean useLowResIcon) {
+            @NonNull final ComponentName componentName, @NonNull final UserHandle user,
+            @NonNull final Supplier<T> infoProvider, @NonNull final CachingLogic<T> cachingLogic,
+            final boolean usePackageIcon, final boolean useLowResIcon) {
         return cacheLocked(
                 componentName,
                 user,
@@ -347,10 +405,12 @@
                 useLowResIcon);
     }
 
+    @NonNull
     protected <T> CacheEntry cacheLocked(
-            @NonNull ComponentName componentName, @NonNull UserHandle user,
-            @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic,
-            @Nullable Cursor cursor, boolean usePackageIcon, boolean useLowResIcon) {
+            @NonNull final ComponentName componentName, @NonNull final UserHandle user,
+            @NonNull final Supplier<T> infoProvider, @NonNull final CachingLogic<T> cachingLogic,
+            @Nullable final Cursor cursor, final boolean usePackageIcon,
+            final boolean useLowResIcon) {
         assertWorkerThread();
         ComponentKey cacheKey = new ComponentKey(componentName, user);
         CacheEntry entry = mCache.get(cacheKey);
@@ -396,30 +456,28 @@
     /**
      * Fallback method for loading an icon bitmap.
      */
-    protected <T> void loadFallbackIcon(
-            T object, CacheEntry entry, @NonNull CachingLogic<T> cachingLogic,
-            boolean usePackageIcon, boolean usePackageTitle, @NonNull ComponentName componentName,
-            @NonNull UserHandle user) {
+    protected <T> void loadFallbackIcon(@Nullable final T object, @NonNull final CacheEntry entry,
+            @NonNull final CachingLogic<T> cachingLogic, final boolean usePackageIcon,
+            final boolean usePackageTitle, @NonNull final ComponentName componentName,
+            @NonNull final UserHandle user) {
         if (object != null) {
             entry.bitmap = cachingLogic.loadIcon(mContext, object);
         } else {
             if (usePackageIcon) {
                 CacheEntry packageEntry = getEntryForPackageLocked(
                         componentName.getPackageName(), user, false);
-                if (packageEntry != null) {
-                    if (DEBUG) Log.d(TAG, "using package default icon for " +
-                            componentName.toShortString());
-                    entry.bitmap = packageEntry.bitmap;
-                    entry.contentDescription = packageEntry.contentDescription;
+                if (DEBUG) Log.d(TAG, "using package default icon for " +
+                        componentName.toShortString());
+                entry.bitmap = packageEntry.bitmap;
+                entry.contentDescription = packageEntry.contentDescription;
 
-                    if (usePackageTitle) {
-                        entry.title = packageEntry.title;
-                    }
+                if (usePackageTitle) {
+                    entry.title = packageEntry.title;
                 }
             }
             if (entry.bitmap == null) {
-                if (DEBUG) Log.d(TAG, "using default icon for " +
-                        componentName.toShortString());
+                // TODO: entry.bitmap can never be null, so this should not happen at all.
+                Log.wtf(TAG, "using default icon for " + componentName.toShortString());
                 entry.bitmap = getDefaultIcon(user);
             }
         }
@@ -429,10 +487,10 @@
      * Fallback method for loading an app title.
      */
     protected <T> void loadFallbackTitle(
-            T object, CacheEntry entry, @NonNull CachingLogic<T> cachingLogic,
-            @NonNull UserHandle user) {
+            @NonNull final T object, @NonNull final CacheEntry entry,
+            @NonNull final CachingLogic<T> cachingLogic, @NonNull final UserHandle user) {
         entry.title = cachingLogic.getLabel(object);
-        entry.contentDescription = mPackageManager.getUserBadgedLabel(
+        entry.contentDescription = getUserBadgedLabel(
                 cachingLogic.getDescription(object, entry.title), user);
     }
 
@@ -445,8 +503,9 @@
      * Adds a default package entry in the cache. This entry is not persisted and will be removed
      * when the cache is flushed.
      */
-    protected synchronized void cachePackageInstallInfo(String packageName, UserHandle user,
-            Bitmap icon, CharSequence title) {
+    protected synchronized void cachePackageInstallInfo(@NonNull final String packageName,
+            @NonNull final UserHandle user, @Nullable final Bitmap icon,
+            @Nullable final CharSequence title) {
         removeFromMemCacheLocked(packageName, user);
 
         ComponentKey cacheKey = getPackageKey(packageName, user);
@@ -469,7 +528,9 @@
         }
     }
 
-    private static ComponentKey getPackageKey(String packageName, UserHandle user) {
+    @NonNull
+    private static ComponentKey getPackageKey(@NonNull final String packageName,
+            @NonNull final UserHandle user) {
         ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME);
         return new ComponentKey(cn, user);
     }
@@ -478,8 +539,10 @@
      * Gets an entry for the package, which can be used as a fallback entry for various components.
      * This method is not thread safe, it must be called from a synchronized method.
      */
-    protected CacheEntry getEntryForPackageLocked(String packageName, UserHandle user,
-            boolean useLowResIcon) {
+    @WorkerThread
+    @NonNull
+    protected CacheEntry getEntryForPackageLocked(@NonNull final String packageName,
+            @NonNull final UserHandle user, final boolean useLowResIcon) {
         assertWorkerThread();
         ComponentKey cacheKey = getPackageKey(packageName, user);
         CacheEntry entry = mCache.get(cacheKey);
@@ -508,7 +571,7 @@
                     li.close();
 
                     entry.title = appInfo.loadLabel(mPackageManager);
-                    entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
+                    entry.contentDescription = getUserBadgedLabel(entry.title, user);
                     entry.bitmap = BitmapInfo.of(
                             useLowResIcon ? LOW_RES_ICON : iconInfo.icon, iconInfo.color);
 
@@ -533,8 +596,8 @@
         return entry;
     }
 
-    protected boolean getEntryFromDBLocked(
-            ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
+    protected boolean getEntryFromDBLocked(@NonNull final ComponentKey cacheKey,
+            @NonNull final CacheEntry entry, final boolean lowRes) {
         Cursor c = null;
         Trace.beginSection("loadIconIndividually");
         try {
@@ -559,7 +622,8 @@
     }
 
     private boolean updateTitleAndIconLocked(
-            ComponentKey cacheKey, CacheEntry entry, Cursor c, boolean lowRes) {
+            @NonNull final ComponentKey cacheKey, @NonNull final CacheEntry entry,
+            @NonNull final Cursor c, final boolean lowRes) {
         // Set the alpha to be 255, so that we never have a wrong color
         entry.bitmap = BitmapInfo.of(LOW_RES_ICON,
                 setColorAlphaBound(c.getInt(IconDB.INDEX_COLOR), 255));
@@ -568,8 +632,7 @@
             entry.title = "";
             entry.contentDescription = "";
         } else {
-            entry.contentDescription = mPackageManager.getUserBadgedLabel(
-                    entry.title, cacheKey.user);
+            entry.contentDescription = getUserBadgedLabel(entry.title, cacheKey.user);
         }
 
         if (!lowRes) {
@@ -678,8 +741,10 @@
         }
     }
 
-    private ContentValues newContentValues(BitmapInfo bitmapInfo, String label,
-            String packageName, @Nullable String keywords) {
+    @NonNull
+    private ContentValues newContentValues(@NonNull final BitmapInfo bitmapInfo,
+            @NonNull final String label, @NonNull final String packageName,
+            @Nullable final String keywords) {
         ContentValues values = new ContentValues();
         if (bitmapInfo.canPersist()) {
             values.put(IconDB.COLUMN_ICON, flattenBitmap(bitmapInfo.icon));
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
index c12e9dc..8034d6e 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
@@ -28,31 +28,36 @@
 
 public interface CachingLogic<T> {
 
-    ComponentName getComponent(T object);
+    @NonNull
+    ComponentName getComponent(@NonNull final T object);
 
-    UserHandle getUser(T object);
+    @NonNull
+    UserHandle getUser(@NonNull final T object);
 
-    CharSequence getLabel(T object);
+    @NonNull
+    CharSequence getLabel(@NonNull final T object);
 
-    default CharSequence getDescription(T object, CharSequence fallback) {
+    @NonNull
+    default CharSequence getDescription(@NonNull final T object,
+            @NonNull final CharSequence fallback) {
         return fallback;
     }
 
     @NonNull
-    BitmapInfo loadIcon(Context context, T object);
+    BitmapInfo loadIcon(@NonNull final Context context, @NonNull final T object);
 
     /**
      * Provides a option list of keywords to associate with this object
      */
     @Nullable
-    default String getKeywords(T object, LocaleList localeList) {
+    default String getKeywords(@NonNull final T object, @NonNull final LocaleList localeList) {
         return null;
     }
 
     /**
      * Returns the timestamp the entry was last updated in cache.
      */
-    default long getLastUpdatedTime(T object, PackageInfo info) {
+    default long getLastUpdatedTime(@Nullable final T object, @NonNull final PackageInfo info) {
         return info.lastUpdateTime;
     }
 
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java b/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
index 9e1ad7b..aec1cdd 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
@@ -180,7 +180,8 @@
                 long updateTime = c.getLong(indexLastUpdate);
                 int version = c.getInt(indexVersion);
                 T app = componentMap.remove(component);
-                if (version == info.versionCode && updateTime == info.lastUpdateTime
+                if (version == info.versionCode
+                        && updateTime == cachingLogic.getLastUpdatedTime(app, info)
                         && TextUtils.equals(c.getString(systemStateIndex),
                                 mIconCache.getIconSystemState(info.packageName))) {
 
diff --git a/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java b/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java
index cc4ad7b..63ba887 100644
--- a/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java
+++ b/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java
@@ -32,6 +32,8 @@
 import android.os.UserManager;
 import android.util.SparseLongArray;
 
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.icons.cache.BaseIconCache;
 
 /**
@@ -64,7 +66,7 @@
     }
 
     @Override
-    protected long getSerialNumberForUser(UserHandle user) {
+    protected long getSerialNumberForUser(@NonNull UserHandle user) {
         synchronized (mUserSerialMap) {
             int index = mUserSerialMap.indexOfKey(user.getIdentifier());
             if (index >= 0) {
@@ -83,10 +85,11 @@
     }
 
     @Override
-    protected boolean isInstantApp(ApplicationInfo info) {
+    protected boolean isInstantApp(@NonNull ApplicationInfo info) {
         return info.isInstantApp();
     }
 
+    @NonNull
     @Override
     public BaseIconFactory getIconFactory() {
         return IconFactory.obtain(mContext);
diff --git a/searchuilib/.gitignore b/motiontoollib/.gitignore
similarity index 100%
rename from searchuilib/.gitignore
rename to motiontoollib/.gitignore
diff --git a/motiontoollib/Android.bp b/motiontoollib/Android.bp
new file mode 100644
index 0000000..6762d83
--- /dev/null
+++ b/motiontoollib/Android.bp
@@ -0,0 +1,80 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "motion_tool_proto",
+    srcs: ["src/com/android/app/motiontool/proto/*.proto"],
+    proto: {
+        type: "lite",
+        local_include_dirs:[
+            "src/com/android/app/motiontool/proto"
+        ],
+        include_dirs: [
+            "frameworks/libs/systemui/viewcapturelib/src/com/android/app/viewcapture/proto"
+        ],
+    },
+    static_libs: [
+        "libprotobuf-java-lite",
+        "view_capture_proto",
+    ],
+    java_version: "1.8",
+}
+
+android_library {
+    name: "motion_tool_lib",
+    manifest: "AndroidManifest.xml",
+    platform_apis: true,
+    min_sdk_version: "26",
+
+    static_libs: [
+        "androidx.core_core",
+        "view_capture",
+        "motion_tool_proto",
+    ],
+
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt"
+    ],
+}
+
+android_test {
+    name: "motion_tool_lib_tests",
+    manifest: "tests/AndroidManifest.xml",
+    platform_apis: true,
+    min_sdk_version: "26",
+
+    static_libs: [
+        "androidx.core_core",
+        "view_capture",
+        "motion_tool_proto",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "testables"
+    ],
+    srcs: [
+        "**/*.java",
+        "**/*.kt"
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    test_suites: ["device-tests"],
+}
+
diff --git a/searchuilib/AndroidManifest.xml b/motiontoollib/AndroidManifest.xml
similarity index 87%
copy from searchuilib/AndroidManifest.xml
copy to motiontoollib/AndroidManifest.xml
index 6c6c5f6..3b8a656 100644
--- a/searchuilib/AndroidManifest.xml
+++ b/motiontoollib/AndroidManifest.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-     Copyright (C) 2020 The Android Open Source Project
+     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.
@@ -16,5 +16,5 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.app.search">
+    package="com.android.app.motiontool">
 </manifest>
diff --git a/motiontoollib/OWNERS b/motiontoollib/OWNERS
new file mode 100644
index 0000000..fd22ee7
--- /dev/null
+++ b/motiontoollib/OWNERS
@@ -0,0 +1,3 @@
+gallmann@google.com
+michschn@google.com
+cinek@google.com
\ No newline at end of file
diff --git a/motiontoollib/TEST_MAPPING b/motiontoollib/TEST_MAPPING
new file mode 100644
index 0000000..22a9e6b
--- /dev/null
+++ b/motiontoollib/TEST_MAPPING
@@ -0,0 +1,15 @@
+{
+  "presubmit": [
+    {
+      "name": "motion_tool_lib_tests",
+      "options": [
+        {
+          "exclude-annotation": "org.junit.Ignore"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    }
+  ]
+}
diff --git a/motiontoollib/build.gradle b/motiontoollib/build.gradle
new file mode 100644
index 0000000..e3750ec
--- /dev/null
+++ b/motiontoollib/build.gradle
@@ -0,0 +1,63 @@
+plugins {
+    id 'com.android.library'
+    id 'org.jetbrains.kotlin.android'
+    id 'com.google.protobuf'
+}
+
+final String PROTOS_DIR = "${ANDROID_TOP}/frameworks/libs/systemui/motiontoollib/src/com/android/app/motiontool/proto"
+
+android {
+    namespace = "com.android.app.motiontool"
+    testNamespace = "com.android.app.motiontool.tests"
+    defaultConfig {
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    sourceSets {
+        main {
+            java.srcDirs = ['src']
+            manifest.srcFile 'AndroidManifest.xml'
+            proto.srcDirs = ["${PROTOS_DIR}"]
+        }
+        androidTest {
+            java.srcDirs = ["tests"]
+            manifest.srcFile "tests/AndroidManifest.xml"
+        }
+    }
+    lint {
+        abortOnError false
+    }
+
+}
+
+dependencies {
+    implementation "androidx.core:core:1.9.0"
+    implementation "com.google.protobuf:protobuf-lite:${protobuf_lite_version}"
+    api project(":ViewCaptureLib")
+    androidTestImplementation project(':SharedTestLib')
+    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+    androidTestImplementation "androidx.test:rules:1.4.0"
+}
+
+protobuf {
+    // Configure the protoc executable
+    protoc {
+        artifact = "com.google.protobuf:protoc:${protobuf_version}${PROTO_ARCH_SUFFIX}"
+    }
+    plugins {
+        javalite {
+            // The codegen for lite comes as a separate artifact
+            artifact = "com.google.protobuf:protoc-gen-javalite:${protobuf_version}${PROTO_ARCH_SUFFIX}"
+        }
+    }
+    generateProtoTasks {
+        all().each { task ->
+            task.builtins {
+                remove java
+            }
+            task.plugins {
+                javalite { }
+            }
+        }
+    }
+}
diff --git a/motiontoollib/src/com/android/app/motiontool/DdmHandleMotionTool.kt b/motiontoollib/src/com/android/app/motiontool/DdmHandleMotionTool.kt
new file mode 100644
index 0000000..c7a6b0d
--- /dev/null
+++ b/motiontoollib/src/com/android/app/motiontool/DdmHandleMotionTool.kt
@@ -0,0 +1,165 @@
+/*
+ * 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.app.motiontool
+
+import android.ddm.DdmHandle
+import com.google.protobuf.InvalidProtocolBufferException
+import org.apache.harmony.dalvik.ddmc.Chunk
+import org.apache.harmony.dalvik.ddmc.ChunkHandler
+import org.apache.harmony.dalvik.ddmc.DdmServer
+
+/**
+ * This class handles the 'MOTO' type DDM requests (defined in [motion_tool.proto]).
+ *
+ * It executes some validity checks and forwards valid requests to the [MotionToolManager]. It
+ * requires a [MotionToolsRequest] as parameter and returns a [MotionToolsResponse]. Failures will
+ * return a [MotionToolsResponse] with the [error][MotionToolsResponse.error] field set instead of
+ * the respective return value.
+ *
+ * To activate this server, call [register]. This will register the DdmHandleMotionTool with the
+ * [DdmServer]. The DdmHandleMotionTool can be registered once per process. To unregister from the
+ * DdmServer, call [unregister].
+ */
+class DdmHandleMotionTool private constructor(
+    private val motionToolManager: MotionToolManager
+) : DdmHandle() {
+
+    companion object {
+        val CHUNK_MOTO = ChunkHandler.type("MOTO")
+        private const val SERVER_VERSION = 1
+
+        private var INSTANCE: DdmHandleMotionTool? = null
+
+        @Synchronized
+        fun getInstance(motionToolManager: MotionToolManager): DdmHandleMotionTool {
+            return INSTANCE ?: DdmHandleMotionTool(motionToolManager).also {
+                INSTANCE = it
+            }
+        }
+    }
+
+    fun register() {
+        DdmServer.registerHandler(CHUNK_MOTO, this)
+    }
+
+    fun unregister() {
+        DdmServer.unregisterHandler(CHUNK_MOTO)
+    }
+
+    override fun handleChunk(request: Chunk): Chunk {
+        val requestDataBuffer = wrapChunk(request)
+        val protoRequest =
+            try {
+                MotionToolsRequest.parseFrom(requestDataBuffer.array())
+            } catch (e: InvalidProtocolBufferException) {
+                val responseData: ByteArray = MotionToolsResponse.newBuilder()
+                        .setError(ErrorResponse.newBuilder()
+                                .setCode(ErrorResponse.Code.INVALID_REQUEST)
+                                .setMessage("Invalid request format (Protobuf parse exception)"))
+                        .build()
+                        .toByteArray()
+                return Chunk(CHUNK_MOTO, responseData, 0, responseData.size)
+            }
+
+        val response =
+            when (protoRequest.typeCase.number) {
+                MotionToolsRequest.HANDSHAKE_FIELD_NUMBER ->
+                    handleHandshakeRequest(protoRequest.handshake)
+                MotionToolsRequest.BEGIN_TRACE_FIELD_NUMBER ->
+                    handleBeginTraceRequest(protoRequest.beginTrace)
+                MotionToolsRequest.POLL_TRACE_FIELD_NUMBER ->
+                    handlePollTraceRequest(protoRequest.pollTrace)
+                MotionToolsRequest.END_TRACE_FIELD_NUMBER ->
+                    handleEndTraceRequest(protoRequest.endTrace)
+                else ->
+                    MotionToolsResponse.newBuilder().setError(ErrorResponse.newBuilder()
+                            .setCode(ErrorResponse.Code.INVALID_REQUEST)
+                            .setMessage("Unknown request type")).build()
+            }
+
+        val responseData = response.toByteArray()
+        return Chunk(CHUNK_MOTO, responseData, 0, responseData.size)
+    }
+
+    private fun handleBeginTraceRequest(beginTraceRequest: BeginTraceRequest): MotionToolsResponse =
+        MotionToolsResponse.newBuilder().apply {
+            tryCatchingMotionToolManagerExceptions {
+                setBeginTrace(BeginTraceResponse.newBuilder().setTraceId(
+                        motionToolManager.beginTrace(beginTraceRequest.window.rootWindow)))
+            }
+        }.build()
+
+    private fun handlePollTraceRequest(pollTraceRequest: PollTraceRequest): MotionToolsResponse =
+        MotionToolsResponse.newBuilder().apply {
+            tryCatchingMotionToolManagerExceptions {
+                setPollTrace(PollTraceResponse.newBuilder()
+                        .setData(motionToolManager.pollTrace(pollTraceRequest.traceId)))
+            }
+        }.build()
+
+    private fun handleEndTraceRequest(endTraceRequest: EndTraceRequest): MotionToolsResponse =
+        MotionToolsResponse.newBuilder().apply {
+            tryCatchingMotionToolManagerExceptions {
+                setEndTrace(EndTraceResponse.newBuilder()
+                        .setData(motionToolManager.endTrace(endTraceRequest.traceId)))
+            }
+        }.build()
+
+    private fun handleHandshakeRequest(handshakeRequest: HandshakeRequest): MotionToolsResponse {
+        val status = if (motionToolManager.hasWindow(handshakeRequest.window))
+            HandshakeResponse.Status.OK
+        else
+            HandshakeResponse.Status.WINDOW_NOT_FOUND
+
+        return MotionToolsResponse.newBuilder()
+                .setHandshake(HandshakeResponse.newBuilder()
+                        .setServerVersion(SERVER_VERSION)
+                        .setStatus(status))
+                .build()
+    }
+
+    /**
+     * Executes the [block] and catches all Exceptions thrown by [MotionToolManager]. In case of an
+     * exception being caught, the error response field of the [MotionToolsResponse] is being set
+     * with the according [ErrorResponse].
+     */
+    private fun MotionToolsResponse.Builder.tryCatchingMotionToolManagerExceptions(block: () -> Unit) {
+        try {
+            block()
+        } catch (e: UnknownTraceIdException) {
+            setError(createUnknownTraceIdResponse(e.traceId))
+        } catch (e: WindowNotFoundException) {
+            setError(createWindowNotFoundResponse(e.windowId))
+        }
+    }
+
+    private fun createUnknownTraceIdResponse(traceId: Int) =
+        ErrorResponse.newBuilder().apply {
+            this.code = ErrorResponse.Code.UNKNOWN_TRACE_ID
+            this.message = "No running Trace found with traceId $traceId"
+        }
+
+    private fun createWindowNotFoundResponse(windowId: String) =
+        ErrorResponse.newBuilder().apply {
+            this.code = ErrorResponse.Code.WINDOW_NOT_FOUND
+            this.message = "No window found with windowId $windowId"
+        }
+
+    override fun onConnected() {}
+
+    override fun onDisconnected() {}
+}
diff --git a/motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt b/motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt
new file mode 100644
index 0000000..a98a588
--- /dev/null
+++ b/motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.app.motiontool
+
+import android.os.Process
+import android.util.Log
+import android.view.Choreographer
+import android.view.View
+import android.view.WindowManagerGlobal
+import androidx.annotation.VisibleForTesting
+import com.android.app.viewcapture.SimpleViewCapture
+import com.android.app.viewcapture.ViewCapture
+import com.android.app.viewcapture.data.MotionWindowData
+
+/**
+ * Singleton to manage motion tracing sessions.
+ *
+ * A motion tracing session captures motion-relevant data on a frame-by-frame basis for a given
+ * window, as long as the trace is running.
+ *
+ * To start a trace, use [beginTrace]. The returned handle must be used to terminate tracing and
+ * receive the data by calling [endTrace]. While the trace is active, data is buffered, however
+ * the buffer size is limited (@see [ViewCapture.mMemorySize]. Use [pollTrace] periodically to
+ * ensure no data is dropped. Both, [pollTrace] and [endTrace] only return data captured since the
+ * last call to either [beginTrace] or [endTrace].
+ *
+ * NOTE: a running trace will incur some performance penalty. Only keep traces running while a user
+ * requested it.
+ *
+ * @see [DdmHandleMotionTool]
+ */
+class MotionToolManager private constructor(private val windowManagerGlobal: WindowManagerGlobal) {
+    private val viewCapture: ViewCapture = SimpleViewCapture("MTViewCapture")
+
+    companion object {
+        private const val TAG = "MotionToolManager"
+
+        private var INSTANCE: MotionToolManager? = null
+
+        @Synchronized
+        fun getInstance(windowManagerGlobal: WindowManagerGlobal): MotionToolManager {
+            return INSTANCE ?: MotionToolManager(windowManagerGlobal).also { INSTANCE = it }
+        }
+    }
+
+    private var traceIdCounter = 0
+    private val traces = mutableMapOf<Int, TraceMetadata>()
+
+    @Synchronized
+    fun hasWindow(windowId: WindowIdentifier): Boolean {
+        val rootView = getRootView(windowId.rootWindow)
+        return rootView != null
+    }
+
+    /** Starts [ViewCapture] and returns a traceId. */
+    @Synchronized
+    fun beginTrace(windowId: String): Int {
+        val traceId = ++traceIdCounter
+        Log.d(TAG, "Begin Trace for id: $traceId")
+        val rootView = getRootView(windowId) ?: throw WindowNotFoundException(windowId)
+        val autoCloseable = viewCapture.startCapture(rootView, windowId)
+        traces[traceId] = TraceMetadata(windowId, 0, autoCloseable::close)
+        return traceId
+    }
+
+    /**
+     * Ends [ViewCapture] and returns the captured [MotionWindowData] since the [beginTrace] call or
+     * the last [pollTrace] call.
+     */
+    @Synchronized
+    fun endTrace(traceId: Int): MotionWindowData {
+        Log.d(TAG, "End Trace for id: $traceId")
+        val traceMetadata = traces.getOrElse(traceId) { throw UnknownTraceIdException(traceId) }
+        val data = pollTrace(traceId)
+        traceMetadata.stopTrace()
+        traces.remove(traceId)
+        return data
+    }
+
+    /**
+     * Returns the [MotionWindowData] captured since the [beginTrace] call or last [pollTrace] call.
+     * This function can only be used after [beginTrace] is called and before [endTrace] is called.
+     */
+    @Synchronized
+    fun pollTrace(traceId: Int): MotionWindowData {
+        val traceMetadata = traces.getOrElse(traceId) { throw UnknownTraceIdException(traceId) }
+        val data = getDataFromViewCapture(traceMetadata)
+        traceMetadata.updateLastPolledTime(data)
+        return data
+    }
+
+    /**
+     * Stops and deletes all active [traces] and resets the [traceIdCounter].
+     */
+    @VisibleForTesting
+    @Synchronized
+    fun reset() {
+        for (traceMetadata in traces.values) {
+            traceMetadata.stopTrace()
+        }
+        traces.clear()
+        traceIdCounter = 0
+    }
+
+    private fun getDataFromViewCapture(traceMetadata: TraceMetadata): MotionWindowData {
+        val rootView =
+            getRootView(traceMetadata.windowId)
+                ?: throw WindowNotFoundException(traceMetadata.windowId)
+
+        val data: MotionWindowData = viewCapture
+            .getDumpTask(rootView).get()
+            ?.orElse(null) ?: return MotionWindowData.newBuilder().build()
+        val filteredFrameData = data.frameDataList.filter {
+            it.timestamp > traceMetadata.lastPolledTime
+        }
+        return data.toBuilder()
+            .clearFrameData()
+            .addAllFrameData(filteredFrameData)
+            .build()
+    }
+
+    private fun getRootView(windowId: String): View? {
+        return windowManagerGlobal.getRootView(windowId)
+    }
+}
+
+private data class TraceMetadata(
+    val windowId: String,
+    var lastPolledTime: Long,
+    var stopTrace: () -> Unit
+) {
+    fun updateLastPolledTime(data: MotionWindowData?) {
+        data?.frameDataList?.maxOfOrNull { it.timestamp }?.let {
+            lastPolledTime = it
+        }
+    }
+}
+
+class UnknownTraceIdException(val traceId: Int) : Exception()
+
+class WindowNotFoundException(val windowId: String) : Exception()
\ No newline at end of file
diff --git a/motiontoollib/src/com/android/app/motiontool/proto/motion_tool.proto b/motiontoollib/src/com/android/app/motiontool/proto/motion_tool.proto
new file mode 100644
index 0000000..04ee020
--- /dev/null
+++ b/motiontoollib/src/com/android/app/motiontool/proto/motion_tool.proto
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+
+package com.android.app.motiontool;
+
+import "view_capture.proto";
+
+option java_multiple_files = true;
+
+message MotionToolsRequest {
+  oneof type {
+    HandshakeRequest handshake = 1;
+    BeginTraceRequest begin_trace = 2;
+    EndTraceRequest end_trace = 3;
+    PollTraceRequest poll_trace = 4;
+  }
+}
+
+// RPC response messages.
+//
+// Returns the result from the corresponding request.
+message MotionToolsResponse {
+  oneof type {
+    // Contains error information whenever the request failed.
+    ErrorResponse error = 1;
+
+    HandshakeResponse handshake = 2;
+    BeginTraceResponse begin_trace = 3;
+    EndTraceResponse end_trace = 4;
+    PollTraceResponse poll_trace = 5;
+  }
+}
+
+message ErrorResponse {
+  enum Code {
+    UNKNOWN = 0;
+    INVALID_REQUEST = 1;
+    UNKNOWN_TRACE_ID = 2;
+    WINDOW_NOT_FOUND = 3;
+  }
+
+  optional Code code = 1;
+  // Human readable error message.
+  optional string message = 2;
+}
+
+// Identifies the window, in which context the motion tools are executed
+message WindowIdentifier {
+  // An identifier for the root view, as accepted by
+  // WindowManagerGlobal#getRootView. This is formatted as
+  // `windowName/rootViewClassName@rootViewIdentityHashCode`,
+  // for example `NotificationShade/android.view.ViewRootImpl@bab6a53`.
+  optional string root_window = 1;
+}
+
+// Verifies the motion tools are available for the specified window.
+message HandshakeRequest {
+  optional WindowIdentifier window = 1;
+  optional int32 client_version = 2;
+}
+
+message HandshakeResponse {
+  enum Status {
+    OK = 1;
+    WINDOW_NOT_FOUND = 2;
+  }
+  optional Status status = 1;
+  optional int32 server_version = 2;
+}
+
+// Enables motion tracing for the specified window
+message BeginTraceRequest {
+  optional WindowIdentifier window = 1;
+}
+
+message BeginTraceResponse {
+  optional int32 trace_id = 1;
+}
+
+// Disabled motion tracing for the specified window
+message EndTraceRequest {
+  optional int32 trace_id = 1;
+}
+
+message EndTraceResponse {
+  optional com.android.app.viewcapture.data.MotionWindowData data = 1;
+}
+
+// Polls collected motion trace data collected since the last PollTraceRequest (or the
+// BeginTraceRequest)
+message PollTraceRequest {
+  optional int32 trace_id = 1;
+}
+
+message PollTraceResponse {
+  optional com.android.app.viewcapture.data.MotionWindowData data = 1;
+}
+
diff --git a/motiontoollib/tests/AndroidManifest.xml b/motiontoollib/tests/AndroidManifest.xml
new file mode 100644
index 0000000..c16e25f
--- /dev/null
+++ b/motiontoollib/tests/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+     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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.app.motiontool.tests">
+
+    <application
+        android:debuggable="true"
+        android:theme="@android:style/Theme.NoTitleBar">
+
+        <activity
+            android:name="com.android.app.motiontool.util.TestActivity"
+            android:exported="false" />
+
+        <uses-library android:name="android.test.runner" />
+
+    </application>
+
+    <instrumentation
+        android:name="android.testing.TestableInstrumentation"
+        android:label="Tests for MotionTool Lib"
+        android:targetPackage="com.android.app.motiontool.tests"/>
+
+</manifest>
diff --git a/motiontoollib/tests/com/android/app/motiontool/DdmHandleMotionToolTest.kt b/motiontoollib/tests/com/android/app/motiontool/DdmHandleMotionToolTest.kt
new file mode 100644
index 0000000..f330980
--- /dev/null
+++ b/motiontoollib/tests/com/android/app/motiontool/DdmHandleMotionToolTest.kt
@@ -0,0 +1,195 @@
+/*
+ * 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.app.motiontool
+
+import android.content.Intent
+import android.testing.AndroidTestingRunner
+import android.view.Choreographer
+import android.view.View
+import android.view.WindowManagerGlobal
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.app.motiontool.DdmHandleMotionTool.Companion.CHUNK_MOTO
+import com.android.app.motiontool.util.TestActivity
+import junit.framework.Assert
+import junit.framework.Assert.assertEquals
+import org.apache.harmony.dalvik.ddmc.Chunk
+import org.apache.harmony.dalvik.ddmc.ChunkHandler.wrapChunk
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DdmHandleMotionToolTest {
+
+    private val windowManagerGlobal = WindowManagerGlobal.getInstance()
+    private val motionToolManager = MotionToolManager.getInstance(windowManagerGlobal)
+    private val ddmHandleMotionTool = DdmHandleMotionTool.getInstance(motionToolManager)
+    private val CLIENT_VERSION = 1
+
+    private val activityIntent =
+        Intent(InstrumentationRegistry.getInstrumentation().context, TestActivity::class.java)
+
+    @get:Rule
+    val activityScenarioRule = ActivityScenarioRule<TestActivity>(activityIntent)
+
+    @Before
+    fun setup() {
+        ddmHandleMotionTool.register()
+    }
+
+    @After
+    fun cleanup() {
+        ddmHandleMotionTool.unregister()
+    }
+
+    @Test
+    fun testHandshakeErrorWithInvalidWindowId() {
+        val handshakeResponse = performHandshakeRequest("InvalidWindowId")
+        assertEquals(HandshakeResponse.Status.WINDOW_NOT_FOUND, handshakeResponse.handshake.status)
+    }
+
+    @Test
+    fun testHandshakeOkWithValidWindowId() {
+        val handshakeResponse = performHandshakeRequest(getActivityViewRootId())
+        assertEquals(HandshakeResponse.Status.OK, handshakeResponse.handshake.status)
+    }
+
+    @Test
+    fun testBeginFailsWithInvalidWindowId() {
+        val errorResponse = performBeginTraceRequest("InvalidWindowId")
+        assertEquals(ErrorResponse.Code.WINDOW_NOT_FOUND, errorResponse.error.code)
+    }
+
+    @Test
+    fun testEndTraceFailsWithoutPrecedingBeginTrace() {
+        val errorResponse = performEndTraceRequest(0)
+        assertEquals(ErrorResponse.Code.UNKNOWN_TRACE_ID, errorResponse.error.code)
+    }
+
+    @Test
+    fun testPollTraceFailsWithoutPrecedingBeginTrace() {
+        val errorResponse = performPollTraceRequest(0)
+        assertEquals(ErrorResponse.Code.UNKNOWN_TRACE_ID, errorResponse.error.code)
+    }
+
+    @Test
+    fun testEndTraceFailsWithInvalidTraceId() {
+        val beginTraceResponse = performBeginTraceRequest(getActivityViewRootId())
+        val endTraceResponse = performEndTraceRequest(beginTraceResponse.beginTrace.traceId + 1)
+        assertEquals(ErrorResponse.Code.UNKNOWN_TRACE_ID, endTraceResponse.error.code)
+    }
+
+    @Test
+    fun testPollTraceFailsWithInvalidTraceId() {
+        val beginTraceResponse = performBeginTraceRequest(getActivityViewRootId())
+        val endTraceResponse = performPollTraceRequest(beginTraceResponse.beginTrace.traceId + 1)
+        assertEquals(ErrorResponse.Code.UNKNOWN_TRACE_ID, endTraceResponse.error.code)
+    }
+
+    @Test
+    fun testMalformedRequestFails() {
+        val requestBytes = ByteArray(9)
+        val requestChunk = Chunk(CHUNK_MOTO, requestBytes, 0, requestBytes.size)
+        val responseChunk = ddmHandleMotionTool.handleChunk(requestChunk)
+        val response = MotionToolsResponse.parseFrom(wrapChunk(responseChunk).array()).error
+        assertEquals(ErrorResponse.Code.INVALID_REQUEST, response.code)
+    }
+
+    @Test
+    fun testNoOnDrawCallReturnsEmptyTrace() {
+        activityScenarioRule.scenario.onActivity {
+            val beginTraceResponse = performBeginTraceRequest(getActivityViewRootId())
+            val endTraceResponse = performEndTraceRequest(beginTraceResponse.beginTrace.traceId)
+            Assert.assertTrue(endTraceResponse.endTrace.data.frameDataList.isEmpty())
+        }
+    }
+
+    @Test
+    fun testOneOnDrawCallReturnsOneFrameResponse() {
+        activityScenarioRule.scenario.onActivity { activity ->
+            val beginTraceResponse = performBeginTraceRequest(getActivityViewRootId())
+            val traceId = beginTraceResponse.beginTrace.traceId
+
+            Choreographer.getInstance().postFrameCallback {
+                activity.findViewById<View>(android.R.id.content).viewTreeObserver.dispatchOnDraw()
+
+                val pollTraceResponse = performPollTraceRequest(traceId)
+                assertEquals(1, pollTraceResponse.pollTrace.data.frameDataList.size)
+
+                // Verify that frameData is only included once and is not returned again
+                val endTraceResponse = performEndTraceRequest(traceId)
+                assertEquals(0, endTraceResponse.endTrace.data.frameDataList.size)
+            }
+        }
+    }
+
+    private fun performPollTraceRequest(requestTraceId: Int): MotionToolsResponse {
+        val pollTraceRequest = MotionToolsRequest.newBuilder()
+                .setPollTrace(PollTraceRequest.newBuilder()
+                        .setTraceId(requestTraceId))
+                .build()
+        return performRequest(pollTraceRequest)
+    }
+
+    private fun performEndTraceRequest(requestTraceId: Int): MotionToolsResponse {
+        val endTraceRequest = MotionToolsRequest.newBuilder()
+                .setEndTrace(EndTraceRequest.newBuilder()
+                        .setTraceId(requestTraceId))
+                .build()
+        return performRequest(endTraceRequest)
+    }
+
+    private fun performBeginTraceRequest(windowId: String): MotionToolsResponse {
+        val beginTraceRequest = MotionToolsRequest.newBuilder()
+                .setBeginTrace(BeginTraceRequest.newBuilder()
+                        .setWindow(WindowIdentifier.newBuilder()
+                                .setRootWindow(windowId)))
+                .build()
+        return performRequest(beginTraceRequest)
+    }
+
+    private fun performHandshakeRequest(windowId: String): MotionToolsResponse {
+        val handshakeRequest = MotionToolsRequest.newBuilder()
+                .setHandshake(HandshakeRequest.newBuilder()
+                        .setWindow(WindowIdentifier.newBuilder()
+                                .setRootWindow(windowId))
+                        .setClientVersion(CLIENT_VERSION))
+                .build()
+        return performRequest(handshakeRequest)
+    }
+
+    private fun performRequest(motionToolsRequest: MotionToolsRequest): MotionToolsResponse {
+        val requestBytes = motionToolsRequest.toByteArray()
+        val requestChunk = Chunk(CHUNK_MOTO, requestBytes, 0, requestBytes.size)
+        val responseChunk = ddmHandleMotionTool.handleChunk(requestChunk)
+        return MotionToolsResponse.parseFrom(wrapChunk(responseChunk).array())
+    }
+
+    private fun getActivityViewRootId(): String {
+        var activityViewRootId = ""
+        activityScenarioRule.scenario.onActivity {
+            activityViewRootId = WindowManagerGlobal.getInstance().viewRootNames.first()
+        }
+        return activityViewRootId
+    }
+
+}
diff --git a/motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt b/motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt
new file mode 100644
index 0000000..c522d0c
--- /dev/null
+++ b/motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.app.motiontool
+
+import android.content.Intent
+import android.testing.AndroidTestingRunner
+import android.view.Choreographer
+import android.view.View
+import android.view.WindowManagerGlobal
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.app.motiontool.util.TestActivity
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class MotionToolManagerTest {
+
+    private val windowManagerGlobal = WindowManagerGlobal.getInstance()
+    private val motionToolManager = MotionToolManager.getInstance(windowManagerGlobal)
+
+    private val activityIntent =
+        Intent(InstrumentationRegistry.getInstrumentation().context, TestActivity::class.java)
+
+    @get:Rule
+    val activityScenarioRule = ActivityScenarioRule<TestActivity>(activityIntent)
+
+    @Test(expected = UnknownTraceIdException::class)
+    fun testEndTraceThrowsWithoutPrecedingBeginTrace() {
+        motionToolManager.endTrace(0)
+    }
+
+    @Test(expected = UnknownTraceIdException::class)
+    fun testPollTraceThrowsWithoutPrecedingBeginTrace() {
+        motionToolManager.pollTrace(0)
+    }
+
+    @Test(expected = UnknownTraceIdException::class)
+    fun testEndTraceThrowsWithInvalidTraceId() {
+        val traceId = motionToolManager.beginTrace(getActivityViewRootId())
+        motionToolManager.endTrace(traceId + 1)
+    }
+
+    @Test(expected = UnknownTraceIdException::class)
+    fun testPollTraceThrowsWithInvalidTraceId() {
+        val traceId = motionToolManager.beginTrace(getActivityViewRootId())
+        motionToolManager.pollTrace(traceId + 1)
+    }
+
+    @Test(expected = WindowNotFoundException::class)
+    fun testBeginTraceThrowsWithInvalidWindowId() {
+        motionToolManager.beginTrace("InvalidWindowId")
+    }
+
+    @Test
+    fun testNoOnDrawCallReturnsEmptyResponse() {
+        activityScenarioRule.scenario.onActivity {
+            val traceId = motionToolManager.beginTrace(getActivityViewRootId())
+            val result = motionToolManager.endTrace(traceId)
+            assertTrue(result.frameDataList.isEmpty())
+        }
+    }
+
+    @Test
+    fun testOneOnDrawCallReturnsOneFrameResponse() {
+        activityScenarioRule.scenario.onActivity { activity ->
+            val traceId = motionToolManager.beginTrace(getActivityViewRootId())
+            Choreographer.getInstance().postFrameCallback {
+                activity.findViewById<View>(android.R.id.content).viewTreeObserver.dispatchOnDraw()
+
+                val polledData = motionToolManager.pollTrace(traceId)
+                assertEquals(1, polledData.frameDataList.size)
+
+                // Verify that frameData is only included once and is not returned again
+                val endData = motionToolManager.endTrace(traceId)
+                assertEquals(0, endData.frameDataList.size)
+            }
+        }
+    }
+
+    private fun getActivityViewRootId(): String {
+        var activityViewRootId = ""
+        activityScenarioRule.scenario.onActivity {
+            activityViewRootId = WindowManagerGlobal.getInstance().viewRootNames.first()
+        }
+        return activityViewRootId
+    }
+}
diff --git a/motiontoollib/tests/com/android/app/motiontool/util/TestActivity.kt b/motiontoollib/tests/com/android/app/motiontool/util/TestActivity.kt
new file mode 100644
index 0000000..a9d68ab
--- /dev/null
+++ b/motiontoollib/tests/com/android/app/motiontool/util/TestActivity.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.app.motiontool.util
+
+import android.app.Activity
+
+class TestActivity : Activity()
diff --git a/searchuilib/Android.bp b/searchuilib/Android.bp
deleted file mode 100644
index 2b25616..0000000
--- a/searchuilib/Android.bp
+++ /dev/null
@@ -1,28 +0,0 @@
-// 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.
-
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_library {
-    name: "search_ui",
-
-    sdk_version: "current",
-    min_sdk_version: "26",
-
-    srcs: [
-        "src/**/*.java",
-    ],
-}
diff --git a/searchuilib/build.gradle b/searchuilib/build.gradle
deleted file mode 100644
index 3bf65fe..0000000
--- a/searchuilib/build.gradle
+++ /dev/null
@@ -1,35 +0,0 @@
-apply plugin: 'com.android.library'
-
-android {
-    compileSdkVersion COMPILE_SDK
-    buildToolsVersion BUILD_TOOLS_VERSION
-
-    defaultConfig {
-        minSdkVersion 25
-        targetSdkVersion 28
-    }
-
-    sourceSets {
-        main {
-            java.srcDirs = ['src']
-            manifest.srcFile 'AndroidManifest.xml'
-        }
-    }
-
-    lintOptions {
-        abortOnError false
-    }
-
-    tasks.withType(JavaCompile) {
-        options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
-    }
-
-    compileOptions {
-        sourceCompatibility JavaVersion.VERSION_1_8
-        targetCompatibility JavaVersion.VERSION_1_8
-    }
-}
-
-dependencies {
-    implementation "androidx.core:core:${ANDROID_X_VERSION}"
-}
diff --git a/searchuilib/src/com/android/app/search/LayoutType.java b/searchuilib/src/com/android/app/search/LayoutType.java
deleted file mode 100644
index d7c28ab..0000000
--- a/searchuilib/src/com/android/app/search/LayoutType.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.app.search;
-
-/**
- * Constants to be used with {@link SearchTarget}.
- */
-public class LayoutType {
-
-    //     ------
-    //    | icon |
-    //     ------
-    //      text
-    public static final String ICON_SINGLE_VERTICAL_TEXT = "icon";
-
-    // Below three layouts (to be deprecated) and two layouts render
-    // {@link SearchTarget}s in following layout.
-    //     ------                            ------   ------
-    //    |      | title                    |(opt)|  |(opt)|
-    //    | icon | subtitle (optional)      | icon|  | icon|
-    //     ------                            ------  ------
-    @Deprecated
-    public static final String ICON_SINGLE_HORIZONTAL_TEXT = "icon_text_row";
-    @Deprecated
-    public static final String ICON_DOUBLE_HORIZONTAL_TEXT = "icon_texts_row";
-    @Deprecated
-    public static final String ICON_DOUBLE_HORIZONTAL_TEXT_BUTTON = "icon_texts_button";
-
-    // will replace ICON_DOUBLE_* ICON_SINGLE_* layouts
-    public static final String ICON_HORIZONTAL_TEXT = "icon_row";
-    public static final String HORIZONTAL_MEDIUM_TEXT = "icon_row_medium";
-    public static final String EXTRA_TALL_ICON_ROW = "extra_tall_icon_row";
-    public static final String SMALL_ICON_HORIZONTAL_TEXT = "short_icon_row";
-    public static final String SMALL_ICON_HORIZONTAL_TEXT_THUMBNAIL = "short_icon_row_thumbnail";
-
-    // This layout creates square thumbnail image (currently 3 column)
-    public static final String THUMBNAIL = "thumbnail";
-
-    // This layout contains an icon and slice
-    public static final String ICON_SLICE = "slice";
-
-    // Widget bitmap preview
-    public static final String WIDGET_PREVIEW = "widget_preview";
-
-    // Live widget search result
-    public static final String WIDGET_LIVE = "widget_live";
-
-    // Layout type used to display people tiles using shortcut info
-    public static final String PEOPLE_TILE = "people_tile";
-
-    // text based header to group various layouts in low confidence section of the results.
-    public static final String TEXT_HEADER = "header";
-
-    // horizontal bar to be inserted between fallback search results and low confidence section
-    public static final String DIVIDER = "divider";
-
-    // horizontal bar to be inserted between fallback search results and low confidence section
-    public static final String EMPTY_DIVIDER = "empty_divider";
-
-    // layout representing quick calculations
-    public static final String CALCULATOR = "calculator";
-
-    // layout for the section header
-    public static final String SECTION_HEADER = "section_header";
-}
diff --git a/searchuilib/src/com/android/app/search/ResultType.java b/searchuilib/src/com/android/app/search/ResultType.java
deleted file mode 100644
index 1e5ea12..0000000
--- a/searchuilib/src/com/android/app/search/ResultType.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.app.search;
-
-/**
- * Constants to be used with {@link android.app.search.SearchContext} and
- * {@link android.app.search.SearchTarget}.
- *
- * Note, a result type could be a of two types.
- * For example, unpublished settings result type could be in slices:
- * <code> resultType = SETTING | SLICE </code>
- */
-public class ResultType {
-
-    // published corpus by 3rd party app, supported by SystemService
-    public static final int APPLICATION = 1 << 0;
-    public static final int SHORTCUT = 1 << 1;
-    public static final int SLICE = 1 << 6;
-    public static final int WIDGETS = 1 << 7;
-
-    // Not extracted from any of the SystemService
-    public static final int PEOPLE = 1 << 2;
-    public static final int ACTION = 1 << 3;
-    public static final int SETTING = 1 << 4;
-    public static final int SCREENSHOT = 1 << 5;
-    public static final int PLAY = 1 << 8;
-    public static final int SUGGEST = 1 << 9;
-    public static final int ASSISTANT = 1 << 10;
-    public static final int CHROMETAB = 1 << 11;
-    public static final int NAVVYSITE = 1 << 12;
-    public static final int TIPS = 1 << 13;
-    public static final int PEOPLE_TILE = 1 << 14;
-    public static final int LEGACY_SHORTCUT = 1 << 15;
-    public static final int MEMORY = 1 << 16;
-    public static final int WEB_SUGGEST = 1 << 17;
-}
diff --git a/searchuilib/.gitignore b/viewcapturelib/.gitignore
similarity index 100%
copy from searchuilib/.gitignore
copy to viewcapturelib/.gitignore
diff --git a/viewcapturelib/Android.bp b/viewcapturelib/Android.bp
new file mode 100644
index 0000000..33da2dd
--- /dev/null
+++ b/viewcapturelib/Android.bp
@@ -0,0 +1,73 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "view_capture_proto",
+    srcs: ["src/com/android/app/viewcapture/proto/*.proto"],
+    proto: {
+        type: "lite",
+        local_include_dirs:[
+            "src/com/android/app/viewcapture/proto"
+        ],
+    },
+    static_libs: ["libprotobuf-java-lite"],
+    java_version: "1.8",
+}
+
+android_library {
+    name: "view_capture",
+    manifest: "AndroidManifest.xml",
+    platform_apis: true,
+    min_sdk_version: "26",
+
+    static_libs: [
+        "androidx.core_core",
+        "view_capture_proto",
+    ],
+
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt"
+    ],
+}
+
+android_test {
+    name: "view_capture_tests",
+    manifest: "tests/AndroidManifest.xml",
+    platform_apis: true,
+    min_sdk_version: "26",
+
+    static_libs: [
+        "androidx.core_core",
+        "view_capture",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "testables",
+        "mockito-target-extended-minus-junit4",
+    ],
+    srcs: [
+        "**/*.java",
+        "**/*.kt"
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+        "android.test.mock",
+    ],
+    test_suites: ["device-tests"],
+}
diff --git a/searchuilib/AndroidManifest.xml b/viewcapturelib/AndroidManifest.xml
similarity index 71%
copy from searchuilib/AndroidManifest.xml
copy to viewcapturelib/AndroidManifest.xml
index 6c6c5f6..1da8129 100644
--- a/searchuilib/AndroidManifest.xml
+++ b/viewcapturelib/AndroidManifest.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-     Copyright (C) 2020 The Android Open Source Project
+     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.
@@ -16,5 +16,8 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.app.search">
+    xmlns:tools="http://schemas.android.com/tools"
+    package="com.android.app.viewcapture">
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"
+        tools:ignore="ProtectedPermissions" />
 </manifest>
diff --git a/viewcapturelib/OWNERS b/viewcapturelib/OWNERS
new file mode 100644
index 0000000..30bdc84
--- /dev/null
+++ b/viewcapturelib/OWNERS
@@ -0,0 +1,2 @@
+sunnygoyal@google.com
+andonian@google.com
diff --git a/viewcapturelib/README.md b/viewcapturelib/README.md
new file mode 100644
index 0000000..4a6993f
--- /dev/null
+++ b/viewcapturelib/README.md
@@ -0,0 +1,11 @@
+###ViewCapture Library Readme
+
+ViewCapture.java is extremely performance sensitive. Any changes should be carried out with great caution not to hurt performance.
+
+The following measurements should serve as a performance baseline (as of 02.10.2022):
+
+
+The onDraw() function invocation time in WindowListener within ViewCapture is measured with System.nanoTime(). The following scenario was measured:
+
+1. Capturing the notification shade window root view on a freshly rebooted bluejay device (2 notifications present) -> avg. time = 204237ns (0.2ms)
+
diff --git a/viewcapturelib/TEST_MAPPING b/viewcapturelib/TEST_MAPPING
new file mode 100644
index 0000000..ecd3e96
--- /dev/null
+++ b/viewcapturelib/TEST_MAPPING
@@ -0,0 +1,15 @@
+{
+  "presubmit": [
+    {
+      "name": "view_capture_tests",
+      "options": [
+        {
+          "exclude-annotation": "org.junit.Ignore"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    }
+  ]
+}
diff --git a/viewcapturelib/build.gradle b/viewcapturelib/build.gradle
new file mode 100644
index 0000000..e5442cc
--- /dev/null
+++ b/viewcapturelib/build.gradle
@@ -0,0 +1,62 @@
+plugins {
+    id 'com.android.library'
+    id 'org.jetbrains.kotlin.android'
+    id 'com.google.protobuf'
+}
+
+final String PROTOS_DIR = "${ANDROID_TOP}/frameworks/libs/systemui/viewcapturelib/src/com/android/app/viewcapture/proto"
+
+android {
+    namespace = "com.android.app.viewcapture"
+    testNamespace = "com.android.app.viewcapture.test"
+    defaultConfig {
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    sourceSets {
+        main {
+            java.srcDirs = ['src']
+            manifest.srcFile 'AndroidManifest.xml'
+            proto.srcDirs = ["${PROTOS_DIR}"]
+        }
+        androidTest {
+            java.srcDirs = ["tests"]
+            manifest.srcFile "tests/AndroidManifest.xml"
+        }
+    }
+    lint {
+        abortOnError false
+    }
+
+}
+
+dependencies {
+    implementation "androidx.core:core:1.9.0"
+    implementation "com.google.protobuf:protobuf-lite:${protobuf_lite_version}"
+    androidTestImplementation project(':SharedTestLib')
+    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+    androidTestImplementation "androidx.test:rules:1.4.0"
+}
+
+protobuf {
+    // Configure the protoc executable
+    protoc {
+        artifact = "com.google.protobuf:protoc:${protobuf_version}${PROTO_ARCH_SUFFIX}"
+    }
+    plugins {
+        javalite {
+            // The codegen for lite comes as a separate artifact
+            artifact = "com.google.protobuf:protoc-gen-javalite:${protobuf_version}${PROTO_ARCH_SUFFIX}"
+        }
+    }
+    generateProtoTasks {
+        all().each { task ->
+            task.builtins {
+                remove java
+            }
+            task.plugins {
+                javalite { }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/viewcapturelib/src/com/android/app/viewcapture/LooperExecutor.java b/viewcapturelib/src/com/android/app/viewcapture/LooperExecutor.java
new file mode 100644
index 0000000..e3450f6
--- /dev/null
+++ b/viewcapturelib/src/com/android/app/viewcapture/LooperExecutor.java
@@ -0,0 +1,59 @@
+/*
+ * 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.app.viewcapture;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.RunnableFuture;
+
+/**
+ * Implementation of {@link Executor} which executes on a provided looper.
+ */
+public class LooperExecutor implements Executor {
+
+    private final Handler mHandler;
+
+    public LooperExecutor(Looper looper) {
+        mHandler = new Handler(looper);
+    }
+
+    @Override
+    public void execute(Runnable runnable) {
+        if (mHandler.getLooper() == Looper.myLooper()) {
+            runnable.run();
+        } else {
+            mHandler.post(runnable);
+        }
+    }
+
+    /**
+     * @throws RejectedExecutionException {@inheritDoc}
+     * @throws NullPointerException       {@inheritDoc}
+     */
+    public <T> Future<T> submit(Callable<T> task) {
+        if (task == null) throw new NullPointerException();
+        RunnableFuture<T> ftask = new FutureTask<T>(task);
+        execute(ftask);
+        return ftask;
+    }
+
+}
diff --git a/viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt b/viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt
new file mode 100644
index 0000000..8a3cf1c
--- /dev/null
+++ b/viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2023 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.app.viewcapture
+
+import android.content.Context
+import android.content.pm.LauncherApps
+import android.database.ContentObserver
+import android.os.Handler
+import android.os.Looper
+import android.os.ParcelFileDescriptor
+import android.os.Process
+import android.provider.Settings
+import android.util.Log
+import android.view.Choreographer
+import android.window.IDumpCallback
+import androidx.annotation.AnyThread
+import androidx.annotation.VisibleForTesting
+import java.util.concurrent.Executor
+
+private val TAG = SettingsAwareViewCapture::class.java.simpleName
+
+/**
+ * ViewCapture that listens to system updates and enables / disables attached ViewCapture
+ * WindowListeners accordingly. The Settings toggle is currently controlled by the Winscope
+ * developer tile in the System developer options.
+ */
+class SettingsAwareViewCapture
+@VisibleForTesting
+internal constructor(private val context: Context, choreographer: Choreographer, executor: Executor)
+    : ViewCapture(DEFAULT_MEMORY_SIZE, DEFAULT_INIT_POOL_SIZE, choreographer, executor) {
+    /** Dumps all the active view captures to the wm trace directory via LauncherAppService */
+    private val mDumpCallback: IDumpCallback.Stub = object : IDumpCallback.Stub() {
+        override fun onDump(out: ParcelFileDescriptor) {
+            try {
+                ParcelFileDescriptor.AutoCloseOutputStream(out).use { os -> dumpTo(os, context) }
+            } catch (e: Exception) {
+                Log.e(TAG, "failed to dump data to wm trace", e)
+            }
+        }
+    }
+
+    init {
+        enableOrDisableWindowListeners()
+        context.contentResolver.registerContentObserver(
+                Settings.Global.getUriFor(VIEW_CAPTURE_ENABLED),
+                false,
+                object : ContentObserver(Handler()) {
+                    override fun onChange(selfChange: Boolean) {
+                        enableOrDisableWindowListeners()
+                    }
+                })
+    }
+
+    @AnyThread
+    private fun enableOrDisableWindowListeners() {
+        mBgExecutor.execute {
+            val isEnabled = Settings.Global.getInt(context.contentResolver, VIEW_CAPTURE_ENABLED,
+                    0) != 0
+            MAIN_EXECUTOR.execute {
+                enableOrDisableWindowListeners(isEnabled)
+            }
+            val launcherApps = context.getSystemService(LauncherApps::class.java)
+            if (isEnabled) {
+                launcherApps?.registerDumpCallback(mDumpCallback)
+            } else {
+                launcherApps?.unRegisterDumpCallback(mDumpCallback)
+            }
+        }
+    }
+
+    companion object {
+        @VisibleForTesting internal const val VIEW_CAPTURE_ENABLED = "view_capture_enabled"
+
+        private var INSTANCE: ViewCapture? = null
+
+        @JvmStatic
+        fun getInstance(context: Context): ViewCapture = when {
+            INSTANCE != null -> INSTANCE!!
+            Looper.myLooper() == Looper.getMainLooper() -> SettingsAwareViewCapture(
+                    context.applicationContext, Choreographer.getInstance(),
+                    createAndStartNewLooperExecutor("SAViewCapture",
+                    Process.THREAD_PRIORITY_FOREGROUND)).also { INSTANCE = it }
+            else -> try {
+                MAIN_EXECUTOR.submit { getInstance(context) }.get()
+            } catch (e: Exception) {
+                throw e
+            }
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/viewcapturelib/src/com/android/app/viewcapture/SimpleViewCapture.kt b/viewcapturelib/src/com/android/app/viewcapture/SimpleViewCapture.kt
new file mode 100644
index 0000000..2773f6b
--- /dev/null
+++ b/viewcapturelib/src/com/android/app/viewcapture/SimpleViewCapture.kt
@@ -0,0 +1,8 @@
+package com.android.app.viewcapture
+
+import android.os.Process
+import android.view.Choreographer
+
+open class SimpleViewCapture(threadName: String) : ViewCapture(DEFAULT_MEMORY_SIZE, DEFAULT_INIT_POOL_SIZE,
+    MAIN_EXECUTOR.submit { Choreographer.getInstance() }.get(),
+    createAndStartNewLooperExecutor(threadName, Process.THREAD_PRIORITY_FOREGROUND))
\ No newline at end of file
diff --git a/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java b/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java
new file mode 100644
index 0000000..fb5abd6
--- /dev/null
+++ b/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java
@@ -0,0 +1,614 @@
+/*
+ * 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.app.viewcapture;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.media.permission.SafeCloseable;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Trace;
+import android.text.TextUtils;
+import android.util.SparseArray;
+import android.view.Choreographer;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
+
+import com.android.app.viewcapture.data.ExportedData;
+import com.android.app.viewcapture.data.FrameData;
+import com.android.app.viewcapture.data.MotionWindowData;
+import com.android.app.viewcapture.data.ViewNode;
+import com.android.app.viewcapture.data.WindowData;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * Utility class for capturing view data every frame
+ */
+public abstract class ViewCapture {
+
+    private static final String TAG = "ViewCapture";
+
+    // These flags are copies of two private flags in the View class.
+    private static final int PFLAG_INVALIDATED = 0x80000000;
+    private static final int PFLAG_DIRTY_MASK = 0x00200000;
+
+    // Number of frames to keep in memory
+    private final int mMemorySize;
+    protected static final int DEFAULT_MEMORY_SIZE = 2000;
+    // Initial size of the reference pool. This is at least be 5 * total number of views in
+    // Launcher. This allows the first free frames avoid object allocation during view capture.
+    protected static final int DEFAULT_INIT_POOL_SIZE = 300;
+
+    public static final LooperExecutor MAIN_EXECUTOR = new LooperExecutor(Looper.getMainLooper());
+
+    private final List<WindowListener> mListeners = new ArrayList<>();
+
+    protected final Executor mBgExecutor;
+    private final Choreographer mChoreographer;
+
+    // Pool used for capturing view tree on the UI thread.
+    private ViewRef mPool = new ViewRef();
+    private boolean mIsEnabled = true;
+
+    protected ViewCapture(int memorySize, int initPoolSize, Choreographer choreographer,
+            Executor bgExecutor) {
+        mMemorySize = memorySize;
+        mChoreographer = choreographer;
+        mBgExecutor = bgExecutor;
+        mBgExecutor.execute(() -> initPool(initPoolSize));
+    }
+
+    public static LooperExecutor createAndStartNewLooperExecutor(String name, int priority) {
+        HandlerThread thread = new HandlerThread(name, priority);
+        thread.start();
+        return new LooperExecutor(thread.getLooper());
+    }
+
+    @UiThread
+    private void addToPool(ViewRef start, ViewRef end) {
+        end.next = mPool;
+        mPool = start;
+    }
+
+    @WorkerThread
+    private void initPool(int initPoolSize) {
+        ViewRef start = new ViewRef();
+        ViewRef current = start;
+
+        for (int i = 0; i < initPoolSize; i++) {
+            current.next = new ViewRef();
+            current = current.next;
+        }
+
+        ViewRef finalCurrent = current;
+        MAIN_EXECUTOR.execute(() -> addToPool(start, finalCurrent));
+    }
+
+    /**
+     * Attaches the ViewCapture to the provided window and returns a handle to detach the listener
+     */
+    @NonNull
+    public SafeCloseable startCapture(Window window) {
+        String title = window.getAttributes().getTitle().toString();
+        String name = TextUtils.isEmpty(title) ? window.toString() : title;
+        return startCapture(window.getDecorView(), name);
+    }
+
+    /**
+     * Attaches the ViewCapture to the provided window and returns a handle to detach the listener.
+     * Verifies that ViewCapture is enabled before actually attaching an onDrawListener.
+     */
+    @NonNull
+    public SafeCloseable startCapture(View view, String name) {
+        WindowListener listener = new WindowListener(view, name);
+        if (mIsEnabled) MAIN_EXECUTOR.execute(listener::attachToRoot);
+        mListeners.add(listener);
+        return () -> {
+            mListeners.remove(listener);
+            listener.detachFromRoot();
+        };
+    }
+
+    /**
+     * Launcher checks for leaks in many spots during its instrumented tests. The WindowListeners
+     * appear to have leaks because they store mRoot views. In reality, attached views close their
+     * respective window listeners when they are destroyed.
+     * <p>
+     * This method deletes detaches and deletes mRoot views from windowListeners. This makes the
+     * WindowListeners unusable for anything except dumping previously captured information. They
+     * are still technically enabled to allow for dumping.
+     */
+    @VisibleForTesting
+    public void stopCapture(@NonNull View rootView) {
+        mListeners.forEach(it -> {
+            if (rootView == it.mRoot) {
+                it.mRoot.getViewTreeObserver().removeOnDrawListener(it);
+                it.mRoot = null;
+            }
+        });
+    }
+
+    @UiThread
+    protected void enableOrDisableWindowListeners(boolean isEnabled) {
+        mIsEnabled = isEnabled;
+        mListeners.forEach(WindowListener::detachFromRoot);
+        if (mIsEnabled) mListeners.forEach(WindowListener::attachToRoot);
+    }
+
+    @AnyThread
+    public void dumpTo(OutputStream os, Context context)
+            throws InterruptedException, ExecutionException, IOException {
+        if (!mIsEnabled) {
+            return;
+        }
+        ArrayList<Class> classList = new ArrayList<>();
+        ExportedData.newBuilder()
+                .setPackage(context.getPackageName())
+                .addAllWindowData(getWindowData(context, classList, l -> l.mIsActive).get())
+                .addAllClassname(toStringList(classList))
+                .build()
+                .writeTo(os);
+    }
+
+    private static List<String> toStringList(List<Class> classList) {
+        return classList.stream().map(Class::getName).toList();
+    }
+
+    public CompletableFuture<Optional<MotionWindowData>> getDumpTask(View view) {
+        ArrayList<Class> classList = new ArrayList<>();
+        return getWindowData(view.getContext().getApplicationContext(), classList,
+                l -> l.mRoot.equals(view)).thenApply(list -> list.stream().findFirst().map(w ->
+                MotionWindowData.newBuilder()
+                        .addAllFrameData(w.getFrameDataList())
+                        .addAllClassname(toStringList(classList))
+                        .build()));
+    }
+
+    @AnyThread
+    private CompletableFuture<List<WindowData>> getWindowData(Context context,
+            ArrayList<Class> outClassList, Predicate<WindowListener> filter) {
+        ViewIdProvider idProvider = new ViewIdProvider(context.getResources());
+        return CompletableFuture.supplyAsync(() ->
+                mListeners.stream().filter(filter).toList(), MAIN_EXECUTOR).thenApplyAsync(it ->
+                        it.stream().map(l -> l.dumpToProto(idProvider, outClassList)).toList(),
+                mBgExecutor);
+    }
+
+
+    /**
+     * Once this window listener is attached to a window's root view, it traverses the entire
+     * view tree on the main thread every time onDraw is called. It then saves the state of the view
+     * tree traversed in a local list of nodes, so that this list of nodes can be processed on a
+     * background thread, and prepared for being dumped into a bugreport.
+     *
+     * Since some of the work needs to be done on the main thread after every draw, this piece of
+     * code needs to be hyper optimized. That is why we are recycling ViewRef and ViewPropertyRef
+     * objects and storing the list of nodes as a flat LinkedList, rather than as a tree. This data
+     * structure allows recycling to happen in O(1) time via pointer assignment. Without this
+     * optimization, a lot of time is wasted creating ViewRef objects, or finding ViewRef objects to
+     * recycle.
+     *
+     * Another optimization is to only traverse view nodes on the main thread that have potentially
+     * changed since the last frame was drawn. This can be determined via a combination of private
+     * flags inside the View class.
+     *
+     * Another optimization is to not store or manipulate any string objects on the main thread.
+     * While this might seem trivial, using Strings in any form causes the ViewCapture to hog the
+     * main thread for up to an additional 6-7ms. It must be avoided at all costs.
+     *
+     * Another optimization is to only store the class names of the Views in the view hierarchy one
+     * time. They are then referenced via a classNameIndex value stored in each ViewPropertyRef.
+     *
+     * TODO: b/262585897: If further memory optimization is required, an effective one would be to
+     * only store the changes between frames, rather than the entire node tree for each frame.
+     * The go/web-hv UX already does this, and has reaped significant memory improves because of it.
+     *
+     * TODO: b/262585897: Another memory optimization could be to store all integer, float, and
+     * boolean information via single integer values via the Chinese remainder theorem, or a similar
+     * algorithm, which enables multiple numerical values to be stored inside 1 number. Doing this
+     * would allow each ViewProperty / ViewRef to slim down its memory footprint significantly.
+     *
+     * One important thing to remember is that bugs related to recycling will usually only appear
+     * after at least 2000 frames have been rendered. If that code is changed, the tester can
+     * use hard-coded logs to verify that recycling is happening, and test view capturing at least
+     * ~8000 frames or so to verify the recycling functionality is working properly.
+     */
+    private class WindowListener implements ViewTreeObserver.OnDrawListener {
+
+        @Nullable // Nullable in tests only
+        public View mRoot;
+        public final String name;
+
+        private final ViewRef mViewRef = new ViewRef();
+
+        private int mFrameIndexBg = -1;
+        private boolean mIsFirstFrame = true;
+        private final long[] mFrameTimesNanosBg = new long[mMemorySize];
+        private final ViewPropertyRef[] mNodesBg = new ViewPropertyRef[mMemorySize];
+
+        private boolean mIsActive = true;
+        private final Consumer<ViewRef> mCaptureCallback = this::captureViewPropertiesBg;
+
+        WindowListener(View view, String name) {
+            mRoot = view;
+            this.name = name;
+        }
+
+        /**
+         * Every time onDraw is called, it does the minimal set of work required on the main thread,
+         * i.e. capturing potentially dirty / invalidated views, and then immediately offloads the
+         * rest of the processing work (extracting the captured view properties) to a background
+         * thread via mExecutor.
+         */
+        @Override
+        public void onDraw() {
+            Trace.beginSection("view_capture");
+            captureViewTree(mRoot, mViewRef);
+            ViewRef captured = mViewRef.next;
+            if (captured != null) {
+                captured.callback = mCaptureCallback;
+                captured.choreographerTimeNanos = mChoreographer.getFrameTimeNanos();
+                mBgExecutor.execute(captured);
+            }
+            mIsFirstFrame = false;
+            Trace.endSection();
+        }
+
+        /**
+         * Captures the View property on the background thread, and transfer all the ViewRef objects
+         * back to the pool
+         */
+        @WorkerThread
+        private void captureViewPropertiesBg(ViewRef viewRefStart) {
+            long choreographerTimeNanos = viewRefStart.choreographerTimeNanos;
+            mFrameIndexBg++;
+            if (mFrameIndexBg >= mMemorySize) {
+                mFrameIndexBg = 0;
+            }
+            mFrameTimesNanosBg[mFrameIndexBg] = choreographerTimeNanos;
+
+            ViewPropertyRef recycle = mNodesBg[mFrameIndexBg];
+
+            ViewPropertyRef resultStart = null;
+            ViewPropertyRef resultEnd = null;
+
+            ViewRef viewRefEnd = viewRefStart;
+            while (viewRefEnd != null) {
+                ViewPropertyRef propertyRef = recycle;
+                if (propertyRef == null) {
+                    propertyRef = new ViewPropertyRef();
+                } else {
+                    recycle = recycle.next;
+                    propertyRef.next = null;
+                }
+
+                ViewPropertyRef copy = null;
+                if (viewRefEnd.childCount < 0) {
+                    copy = findInLastFrame(viewRefEnd.view.hashCode());
+                    viewRefEnd.childCount = (copy != null) ? copy.childCount : 0;
+                }
+                viewRefEnd.transferTo(propertyRef);
+
+                if (resultStart == null) {
+                    resultStart = propertyRef;
+                    resultEnd = resultStart;
+                } else {
+                    resultEnd.next = propertyRef;
+                    resultEnd = resultEnd.next;
+                }
+
+                if (copy != null) {
+                    int pending = copy.childCount;
+                    while (pending > 0) {
+                        copy = copy.next;
+                        pending = pending - 1 + copy.childCount;
+
+                        propertyRef = recycle;
+                        if (propertyRef == null) {
+                            propertyRef = new ViewPropertyRef();
+                        } else {
+                            recycle = recycle.next;
+                            propertyRef.next = null;
+                        }
+
+                        copy.transferTo(propertyRef);
+
+                        resultEnd.next = propertyRef;
+                        resultEnd = resultEnd.next;
+                    }
+                }
+
+                if (viewRefEnd.next == null) {
+                    // The compiler will complain about using a non-final variable from
+                    // an outer class in a lambda if we pass in viewRefEnd directly.
+                    final ViewRef finalViewRefEnd = viewRefEnd;
+                    MAIN_EXECUTOR.execute(() -> addToPool(viewRefStart, finalViewRefEnd));
+                    break;
+                }
+                viewRefEnd = viewRefEnd.next;
+            }
+            mNodesBg[mFrameIndexBg] = resultStart;
+        }
+
+        private @Nullable ViewPropertyRef findInLastFrame(int hashCode) {
+            int lastFrameIndex = (mFrameIndexBg == 0) ? mMemorySize - 1 : mFrameIndexBg - 1;
+            ViewPropertyRef viewPropertyRef = mNodesBg[lastFrameIndex];
+            while (viewPropertyRef != null && viewPropertyRef.hashCode != hashCode) {
+                viewPropertyRef = viewPropertyRef.next;
+            }
+            return viewPropertyRef;
+        }
+
+        void attachToRoot() {
+            mIsActive = true;
+            if (mRoot.isAttachedToWindow()) {
+                safelyEnableOnDrawListener();
+            } else {
+                mRoot.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+                    @Override
+                    public void onViewAttachedToWindow(View v) {
+                        if (mIsActive) {
+                            safelyEnableOnDrawListener();
+                        }
+                        mRoot.removeOnAttachStateChangeListener(this);
+                    }
+
+                    @Override
+                    public void onViewDetachedFromWindow(View v) {
+                    }
+                });
+            }
+        }
+
+        void detachFromRoot() {
+            mIsActive = false;
+            if (mRoot != null) {
+                mRoot.getViewTreeObserver().removeOnDrawListener(this);
+            }
+        }
+
+        private void safelyEnableOnDrawListener() {
+            mRoot.getViewTreeObserver().removeOnDrawListener(this);
+            mRoot.getViewTreeObserver().addOnDrawListener(this);
+        }
+
+        @WorkerThread
+        private WindowData dumpToProto(ViewIdProvider idProvider, ArrayList<Class> classList) {
+            WindowData.Builder builder = WindowData.newBuilder().setTitle(name);
+            int size = (mNodesBg[mMemorySize - 1] == null) ? mFrameIndexBg + 1 : mMemorySize;
+            for (int i = size - 1; i >= 0; i--) {
+                int index = (mMemorySize + mFrameIndexBg - i) % mMemorySize;
+                ViewNode.Builder nodeBuilder = ViewNode.newBuilder();
+                mNodesBg[index].toProto(idProvider, classList, nodeBuilder);
+                FrameData.Builder frameDataBuilder = FrameData.newBuilder()
+                        .setNode(nodeBuilder)
+                        .setTimestamp(mFrameTimesNanosBg[index]);
+                builder.addFrameData(frameDataBuilder);
+            }
+            return builder.build();
+        }
+
+        private ViewRef captureViewTree(View view, ViewRef start) {
+            ViewRef ref;
+            if (mPool != null) {
+                ref = mPool;
+                mPool = mPool.next;
+                ref.next = null;
+            } else {
+                ref = new ViewRef();
+            }
+            ref.view = view;
+            start.next = ref;
+            if (view instanceof ViewGroup) {
+                ViewGroup parent = (ViewGroup) view;
+                // If a view has not changed since the last frame, we will copy
+                // its children from the last processed frame's data.
+                if ((view.mPrivateFlags & (PFLAG_INVALIDATED | PFLAG_DIRTY_MASK)) == 0
+                        && !mIsFirstFrame) {
+                    // A negative child count is the signal to copy this view from the last frame.
+                    ref.childCount = -parent.getChildCount();
+                    return ref;
+                }
+                ViewRef result = ref;
+                int childCount = ref.childCount = parent.getChildCount();
+                for (int i = 0; i < childCount; i++) {
+                    result = captureViewTree(parent.getChildAt(i), result);
+                }
+                return result;
+            } else {
+                ref.childCount = 0;
+                return ref;
+            }
+        }
+    }
+
+    private static class ViewPropertyRef {
+        // We store reference in memory to avoid generating and storing too many strings
+        public Class clazz;
+        public int hashCode;
+        public int childCount = 0;
+
+        public int id;
+        public int left, top, right, bottom;
+        public int scrollX, scrollY;
+
+        public float translateX, translateY;
+        public float scaleX, scaleY;
+        public float alpha;
+        public float elevation;
+
+        public int visibility;
+        public boolean willNotDraw;
+        public boolean clipChildren;
+
+        public ViewPropertyRef next;
+
+        public void transferTo(ViewPropertyRef out) {
+            out.clazz = this.clazz;
+            out.hashCode = this.hashCode;
+            out.childCount = this.childCount;
+            out.id = this.id;
+            out.left = this.left;
+            out.top = this.top;
+            out.right = this.right;
+            out.bottom = this.bottom;
+            out.scrollX = this.scrollX;
+            out.scrollY = this.scrollY;
+            out.scaleX = this.scaleX;
+            out.scaleY = this.scaleY;
+            out.translateX = this.translateX;
+            out.translateY = this.translateY;
+            out.alpha = this.alpha;
+            out.visibility = this.visibility;
+            out.willNotDraw = this.willNotDraw;
+            out.clipChildren = this.clipChildren;
+            out.elevation = this.elevation;
+        }
+
+        /**
+         * Converts the data to the proto representation and returns the next property ref
+         * at the end of the iteration.
+         */
+        public ViewPropertyRef toProto(ViewIdProvider idProvider, ArrayList<Class> classList,
+                ViewNode.Builder viewNode) {
+            int classnameIndex = classList.indexOf(clazz);
+            if (classnameIndex < 0) {
+                classnameIndex = classList.size();
+                classList.add(clazz);
+            }
+
+            viewNode.setClassnameIndex(classnameIndex)
+                    .setHashcode(hashCode)
+                    .setId(idProvider.getName(id))
+                    .setLeft(left)
+                    .setTop(top)
+                    .setWidth(right - left)
+                    .setHeight(bottom - top)
+                    .setTranslationX(translateX)
+                    .setTranslationY(translateY)
+                    .setScrollX(scrollX)
+                    .setScrollY(scrollY)
+                    .setScaleX(scaleX)
+                    .setScaleY(scaleY)
+                    .setAlpha(alpha)
+                    .setVisibility(visibility)
+                    .setWillNotDraw(willNotDraw)
+                    .setElevation(elevation)
+                    .setClipChildren(clipChildren);
+
+            ViewPropertyRef result = next;
+            for (int i = 0; (i < childCount) && (result != null); i++) {
+                ViewNode.Builder childViewNode = ViewNode.newBuilder();
+                result = result.toProto(idProvider, classList, childViewNode);
+                viewNode.addChildren(childViewNode);
+            }
+            return result;
+        }
+    }
+
+
+    private static class ViewRef implements Runnable {
+        public View view;
+        public int childCount = 0;
+        public ViewRef next;
+
+        public Consumer<ViewRef> callback = null;
+        public long choreographerTimeNanos = 0;
+
+        public void transferTo(ViewPropertyRef out) {
+            out.childCount = this.childCount;
+
+            View view = this.view;
+            this.view = null;
+
+            out.clazz = view.getClass();
+            out.hashCode = view.hashCode();
+            out.id = view.getId();
+            out.left = view.getLeft();
+            out.top = view.getTop();
+            out.right = view.getRight();
+            out.bottom = view.getBottom();
+            out.scrollX = view.getScrollX();
+            out.scrollY = view.getScrollY();
+
+            out.translateX = view.getTranslationX();
+            out.translateY = view.getTranslationY();
+            out.scaleX = view.getScaleX();
+            out.scaleY = view.getScaleY();
+            out.alpha = view.getAlpha();
+            out.elevation = view.getElevation();
+
+            out.visibility = view.getVisibility();
+            out.willNotDraw = view.willNotDraw();
+        }
+
+        @Override
+        public void run() {
+            Consumer<ViewRef> oldCallback = callback;
+            callback = null;
+            if (oldCallback != null) {
+                oldCallback.accept(this);
+            }
+        }
+    }
+
+    private static final class ViewIdProvider {
+
+        private final SparseArray<String> mNames = new SparseArray<>();
+        private final Resources mRes;
+
+        ViewIdProvider(Resources res) {
+            mRes = res;
+        }
+
+        String getName(int id) {
+            String name = mNames.get(id);
+            if (name == null) {
+                if (id >= 0) {
+                    try {
+                        name = mRes.getResourceTypeName(id) + '/' + mRes.getResourceEntryName(id);
+                    } catch (Resources.NotFoundException e) {
+                        name = "id/" + "0x" + Integer.toHexString(id).toUpperCase();
+                    }
+                } else {
+                    name = "NO_ID";
+                }
+                mNames.put(id, name);
+            }
+            return name;
+        }
+    }
+}
diff --git a/viewcapturelib/src/com/android/app/viewcapture/proto/view_capture.proto b/viewcapturelib/src/com/android/app/viewcapture/proto/view_capture.proto
new file mode 100644
index 0000000..d4df2ae
--- /dev/null
+++ b/viewcapturelib/src/com/android/app/viewcapture/proto/view_capture.proto
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+
+package com.android.app.viewcapture.data;
+
+option java_multiple_files = true;
+
+message ExportedData {
+  repeated WindowData windowData = 1;
+  optional string package = 2;
+  repeated string classname = 3;
+}
+
+message WindowData {
+  repeated FrameData frameData = 1;
+  optional string title = 2;
+}
+
+message MotionWindowData {
+  repeated FrameData frameData = 1;
+  repeated string classname = 2;
+}
+
+message FrameData {
+  optional int64 timestamp = 1; // choreographer timestamp in nanoseconds
+  optional ViewNode node = 2;
+}
+
+message ViewNode {
+  optional int32 classname_index = 1;
+  optional int32 hashcode = 2;
+
+  repeated ViewNode children = 3;
+
+  optional string id = 4;
+  optional int32 left = 5;
+  optional int32 top = 6;
+  optional int32 width = 7;
+  optional int32 height = 8;
+  optional int32 scrollX = 9;
+  optional int32 scrollY = 10;
+
+  optional float translationX = 11;
+  optional float translationY = 12;
+  optional float scaleX = 13 [default = 1];
+  optional float scaleY = 14 [default = 1];
+  optional float alpha = 15 [default = 1];
+
+  optional bool willNotDraw = 16;
+  optional bool clipChildren = 17;
+  optional int32 visibility = 18;
+
+  optional float elevation = 19;
+}
diff --git a/viewcapturelib/tests/AndroidManifest.xml b/viewcapturelib/tests/AndroidManifest.xml
new file mode 100644
index 0000000..8d31c0e
--- /dev/null
+++ b/viewcapturelib/tests/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.app.viewcapture.test">
+    <application
+        android:debuggable="true"
+        android:theme="@android:style/Theme.NoTitleBar">
+
+        <activity
+            android:name="com.android.app.viewcapture.TestActivity"
+            android:exported="false" />
+
+        <uses-library android:name="android.test.runner" />
+
+    </application>
+
+    <instrumentation
+        android:name="android.testing.TestableInstrumentation"
+        android:label="Tests for MotionTool Lib"
+        android:targetPackage="com.android.app.viewcapture.test"/>
+
+</manifest>
diff --git a/viewcapturelib/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt b/viewcapturelib/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt
new file mode 100644
index 0000000..2157ee4
--- /dev/null
+++ b/viewcapturelib/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 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.app.viewcapture
+
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import android.media.permission.SafeCloseable
+import android.provider.Settings
+import android.testing.AndroidTestingRunner
+import android.view.Choreographer
+import android.view.View
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import com.android.app.viewcapture.SettingsAwareViewCapture.Companion.VIEW_CAPTURE_ENABLED
+import com.android.app.viewcapture.ViewCapture.MAIN_EXECUTOR
+import junit.framework.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class SettingsAwareViewCaptureTest {
+    private val context: Context = InstrumentationRegistry.getInstrumentation().context
+    private val activityIntent = Intent(context, TestActivity::class.java)
+
+    @get:Rule val activityScenarioRule = ActivityScenarioRule<TestActivity>(activityIntent)
+    @get:Rule val grantPermissionRule =
+        GrantPermissionRule.grant(Manifest.permission.WRITE_SECURE_SETTINGS)
+
+    @Test
+    fun do_not_capture_view_hierarchies_if_setting_is_disabled() {
+        Settings.Global.putInt(context.contentResolver, VIEW_CAPTURE_ENABLED, 0)
+
+        activityScenarioRule.scenario.onActivity { activity ->
+            val viewCapture: ViewCapture =
+                SettingsAwareViewCapture(context, Choreographer.getInstance(), MAIN_EXECUTOR)
+            val rootView: View = activity.findViewById(android.R.id.content)
+
+            val closeable: SafeCloseable = viewCapture.startCapture(rootView, "rootViewId")
+            Choreographer.getInstance().postFrameCallback {
+                rootView.viewTreeObserver.dispatchOnDraw()
+
+                assertEquals(0, viewCapture.getDumpTask(
+                        activity.findViewById(android.R.id.content)).get().get().frameDataList.size)
+                closeable.close()
+            }
+        }
+    }
+
+    @Test
+    fun capture_view_hierarchies_if_setting_is_enabled() {
+        Settings.Global.putInt(context.contentResolver, VIEW_CAPTURE_ENABLED, 1)
+
+        activityScenarioRule.scenario.onActivity { activity ->
+            val viewCapture: ViewCapture =
+                SettingsAwareViewCapture(context, Choreographer.getInstance(), MAIN_EXECUTOR)
+            val rootView: View = activity.findViewById(android.R.id.content)
+
+            val closeable: SafeCloseable = viewCapture.startCapture(rootView, "rootViewId")
+            Choreographer.getInstance().postFrameCallback {
+                rootView.viewTreeObserver.dispatchOnDraw()
+
+                assertEquals(1, viewCapture.getDumpTask(activity.findViewById(
+                        android.R.id.content)).get().get().frameDataList.size)
+
+                closeable.close()
+            }
+        }
+    }
+
+    @Test
+    fun getInstance_calledTwiceInARow_returnsSameObject() {
+        assertEquals(
+            SettingsAwareViewCapture.getInstance(context).hashCode(),
+            SettingsAwareViewCapture.getInstance(context).hashCode()
+        )
+    }
+}
diff --git a/viewcapturelib/tests/com/android/app/viewcapture/TestActivity.kt b/viewcapturelib/tests/com/android/app/viewcapture/TestActivity.kt
new file mode 100644
index 0000000..749327e
--- /dev/null
+++ b/viewcapturelib/tests/com/android/app/viewcapture/TestActivity.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 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.app.viewcapture
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.LinearLayout
+import android.widget.TextView
+
+/**
+ * Activity with the content set to a [LinearLayout] with [TextView] children.
+ */
+class TestActivity : Activity() {
+
+    companion object {
+        const val TEXT_VIEW_COUNT = 1000
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(createContentView())
+    }
+
+    private fun createContentView(): LinearLayout {
+        val root = LinearLayout(this)
+        for (i in 0 until TEXT_VIEW_COUNT) {
+            root.addView(TextView(this))
+        }
+        return root
+    }
+}
\ No newline at end of file
diff --git a/viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt b/viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt
new file mode 100644
index 0000000..b341fe9
--- /dev/null
+++ b/viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.app.viewcapture
+
+import android.content.Intent
+import android.media.permission.SafeCloseable
+import android.testing.AndroidTestingRunner
+import android.view.Choreographer
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.app.viewcapture.TestActivity.Companion.TEXT_VIEW_COUNT
+import com.android.app.viewcapture.data.MotionWindowData
+import junit.framework.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ViewCaptureTest {
+
+    private val memorySize = 100
+    private val initPoolSize = 15
+    private val viewCapture by lazy {
+        object :
+            ViewCapture(memorySize, initPoolSize, Choreographer.getInstance(), MAIN_EXECUTOR) {}
+    }
+
+    private val activityIntent =
+        Intent(InstrumentationRegistry.getInstrumentation().context, TestActivity::class.java)
+
+    @get:Rule val activityScenarioRule = ActivityScenarioRule<TestActivity>(activityIntent)
+
+    @Test
+    fun testWindowListenerDumpsOneFrameAfterInvalidate() {
+        activityScenarioRule.scenario.onActivity { activity ->
+            Choreographer.getInstance().postFrameCallback {
+                val closeable = startViewCaptureAndInvalidateNTimes(1, activity)
+                val rootView = activity.findViewById<View>(android.R.id.content)
+                val data = viewCapture.getDumpTask(rootView).get().get()
+
+                assertEquals(1, data.frameDataList.size)
+                verifyTestActivityViewHierarchy(data)
+                closeable.close()
+            }
+        }
+    }
+
+    @Test
+    fun testWindowListenerDumpsCorrectlyAfterRecyclingStarted() {
+        activityScenarioRule.scenario.onActivity { activity ->
+            Choreographer.getInstance().postFrameCallback {
+                val closeable = startViewCaptureAndInvalidateNTimes(memorySize + 5, activity)
+                val rootView = activity.findViewById<View>(android.R.id.content)
+                val data = viewCapture.getDumpTask(rootView).get().get()
+
+                // since ViewCapture MEMORY_SIZE is [viewCaptureMemorySize], only
+                // [viewCaptureMemorySize] frames are exported, although the view is invalidated
+                // [viewCaptureMemorySize + 5] times
+                assertEquals(memorySize, data.frameDataList.size)
+                verifyTestActivityViewHierarchy(data)
+                closeable.close()
+            }
+        }
+    }
+
+    private fun startViewCaptureAndInvalidateNTimes(n: Int, activity: TestActivity): SafeCloseable {
+        val rootView: View = activity.findViewById(android.R.id.content)
+        val closeable: SafeCloseable = viewCapture.startCapture(rootView, "rootViewId")
+        dispatchOnDraw(rootView, times = n)
+        return closeable
+    }
+
+    private fun dispatchOnDraw(view: View, times: Int) {
+        if (times > 0) {
+            view.viewTreeObserver.dispatchOnDraw()
+            dispatchOnDraw(view, times - 1)
+        }
+    }
+
+    private fun verifyTestActivityViewHierarchy(exportedData: MotionWindowData) {
+        for (frame in exportedData.frameDataList) {
+            val testActivityRoot =
+                frame.node // FrameLayout (android.R.id.content)
+                    .childrenList
+                    .first() // LinearLayout (set by setContentView())
+            assertEquals(TEXT_VIEW_COUNT, testActivityRoot.childrenList.size)
+            assertEquals(
+                LinearLayout::class.qualifiedName,
+                exportedData.getClassname(testActivityRoot.classnameIndex)
+            )
+            assertEquals(
+                TextView::class.qualifiedName,
+                exportedData.getClassname(testActivityRoot.childrenList.first().classnameIndex)
+            )
+        }
+    }
+}