Merge "Changes some correctness compose:ui lint checks to be warnings, not errors" into androidx-main
diff --git a/annotation/annotation-experimental/proguard-rules.pro b/annotation/annotation-experimental/proguard-rules.pro
index 8392750..d1c676bf 100644
--- a/annotation/annotation-experimental/proguard-rules.pro
+++ b/annotation/annotation-experimental/proguard-rules.pro
@@ -14,7 +14,9 @@
 
 # Ignore missing Kotlin meta-annotations so that this library can be used
 # without adding a compileOnly dependency on the Kotlin standard library.
+-dontwarn kotlin.Deprecated
 -dontwarn kotlin.Metadata
+-dontwarn kotlin.ReplaceWith
 -dontwarn kotlin.annotation.AnnotationRetention
 -dontwarn kotlin.annotation.AnnotationTarget
 -dontwarn kotlin.annotation.Retention
diff --git a/benchmark/docs/app_audit.md b/benchmark/docs/app_audit.md
new file mode 100644
index 0000000..58916b8
--- /dev/null
+++ b/benchmark/docs/app_audit.md
@@ -0,0 +1,423 @@
+# Android App Audit Runbook
+
+This runbook is intended to give developers the skills to identify and fix key
+performance issues in your app independently.
+
+[TOC]
+
+## Key Performance Issues {#key-performance-issues}
+
+*   [Scroll Jank](https://developer.android.com/topic/performance/vitals/render?hl=en#fixing_jank)
+    *   "Jank" is the term used to describe the visual hiccup that occurs when
+        the system is not able to build and provide frames in time for them to
+        be drawn to the screen at the requested cadence (60hz, or higher). Jank
+        is most apparent when scrolling, when what should be a smoothly animated
+        flow has "catches".
+    *   Apps should target 90Hz refresh rates. Many newer devices such as Pixel
+        4 operate in 90Hz mode during user interactions like scrolling.
+        *   If you're curious what refresh rate your device is using at a given
+            time, you can enable an overlay via Developer Options > Show refresh
+            rate (under Debugging)
+*   Transitions that are not smooth
+    *   These concerns arise during interactions such as switching between tabs,
+        or loading a new activity. These types of transitions should have smooth
+        animations and not include delays or visual flicker.
+*   Power Inefficiency
+    *   Doing work costs battery, and doing unnecessary work reduces battery
+        life.
+    *   One common cause of unnecessary work can be related to the GC (garbage
+        collector). This can happen when an app is allocating a lot of
+        repetitive variables that it only uses for a short time. Then, the
+        garbage collector needs to run frequently to clean up those variables.
+*   [Startup Latency](https://developer.android.com/topic/performance/vitals/launch-time?hl=en#internals)
+    *   Startup latency is the amount of time it takes between clicking on the
+        app icon, notification, or other entry point and the user's data being
+        shown on the screen.
+    *   We have two startup latency goals:
+        *   For "cold" starts, the starts that require the most work from the
+            app and the system, to start consistently within 500ms.
+        *   For the P95/P99 latencies to be very close to the median latency.
+            When the app sometimes a very long time to start, user trust is
+            eroded. IPCs and unnecessary I/O during the critical path of app
+            startup can experience lock contention and introduce these
+            inconsistencies.
+
+## Identifying These Issues {#identifying-these-issues}
+
+Generally, the recommended workflow to identify and remedy performance issues is
+as follows:
+
+1.  Identify critical user journeys to inspect. These usually include:
+    1.  Common startup flows, including from launcher and notification.
+    1.  Any screens where the user scrolls through data.
+    1.  Transitions between screens.
+    1.  Long-running flows, like navigation or music playback.
+1.  Inspect what is happening during those flows using debugging tools:
+    1.  [Systrace/Perfetto](https://developer.android.com/topic/performance/tracing) -
+        Allows you to see exactly what is happening across the entire device
+        with precise timing data.
+    1.  [Memory Profiler](https://developer.android.com/studio/profile/memory-profiler) -
+        Allows you to see what memory allocations are happening on the heap.
+    1.  [Simpleperf](https://developer.android.com/ndk/guides/simpleperf) - View
+        a flamegraph of what function calls are taking up the most CPU during a
+        certain period of time. When you identify something that's taking a long
+        time in systrace, but you don't know why, simpleperf can fill in the
+        blanks.
+
+Manual debugging of individual test runs is **critical** for understanding and
+debugging these performance issues. The above steps **cannot** be replaced by
+analyzing aggregated data. However, setting up metrics collection in automated
+testing as well as in the field is also important to understand what users are
+actually seeing and identify when regressions may occur:
+
+1.  Collect metrics during those flows
+    1.  Startup Flows
+        1.  Field metrics
+            1.  [Play Console startup time](https://support.google.com/googleplay/android-developer/answer/9844486#zippy=%2Capp-start-up-time)
+        2.  Lab tests
+            1.  [Jetpack Macrobenchmark: Startup](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/benchmark/docs/macrobenchmark.md#startup)
+    2.  Jank
+        1.  Field metrics
+            1.  Play Console frame vitals
+                1.  Note that within the Play Console, it's not possible to
+                    narrow down metrics to a specific user journey, since all
+                    that is reported is overall jank throughout the app.
+            2.  Custom measurement with
+                [FrameMetricsAggregator](https://developer.android.com/reference/kotlin/androidx/core/app/FrameMetricsAggregator)
+                1.  You can utilize FrameMetricsAggregator to record Jank
+                    metrics during a particular workflow.
+        2.  Lab tests
+            1.  [Jetpack Macrobenchmark: Scrolling](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/benchmark/docs/macrobenchmark.md#scrolling-and-animation)
+            2.  Macrobenchmark collects frame timing via `dumpsys gfxinfo`
+                commands that bracket a user journey in question. This is a
+                reasonable way to understand variation in jank over a specific
+                user journey. The RenderTime metrics, which highlight how long
+                frames are taking to draw, are more important than the count of
+                janky frames for identifying regressions or improvements.
+
+## Setting Up Your App for Performance Analysis {#setting-up-your-app-for-performance-analysis}
+
+Proper setup is essential for getting accurate, repeatable, actionable
+benchmarks from an application. In general, you want to test on a system that is
+as close to production as possible, while suppressing sources of noise. Below
+are a number of APK and system specific steps you can take to prepare a test
+setup, some of which are use case specific.
+
+### Tracepoints {#tracepoints}
+
+Applications can instrument their code with the
+[androidx.tracing.Trace](https://developer.android.com/reference/kotlin/androidx/tracing/Trace)
+class. It is strongly recommended to instrument key workloads in your
+application to increase the utility of traces, both for local profiling, and for
+inspecting results from CI. In Kotlin, the `androidx.tracing:tracing-ktx` module
+[makes this very simple](https://developer.android.com/reference/kotlin/androidx/tracing/package-summary?hl=en#trace\(kotlin.String,%20kotlin.Function0\)):
+
+```kotlin
+fun loadItemData(configuration: Config) = trace("loadItemData") {
+    // perform expensive operation here ...
+}
+```
+
+While traces are being captured, tracing does incur a small overhead (roughly
+5us) per section, so don't put it around every method. Just tracing larger
+chunks of work (>0.1ms) can give significant insights into bottlenecks.
+
+### APK Considerations {#apk-considerations}
+
+**Do not measure performance on a
+[debug build](https://developer.android.com/studio/debug).**
+
+Debug variants can be helpful for troubleshooting and symbolizing stack samples,
+but they have severe non-linear impacts on performance. Devices on Q+ can use
+_[profileableFromShell](https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/android_application_profiling.md)_
+in their manifest to enable profiling in release builds.
+
+Use your production grade
+[proguard](https://developer.android.com/studio/build/shrink-code)
+configuration. Depending on the resources your application utilizes, this can
+have a substantial impact on performance. Note that some proguard configs strip
+tracepoints, consider removing those rules for the configuration you're running
+tests on
+
+#### Compilation
+
+[Compile](https://source.android.com/devices/tech/dalvik/configure#system_rom)
+your application on-device to a known state (generally speed or speed-profile).
+Background JIT activity can have a significant performance overhead, and you
+will hit it often if you are reinstalling the APK between test runs. The command
+to do this is:
+
+```shell
+adb shell cmd package compile -m speed -f com.google.packagename
+```
+
+The 'speed' compilation mode will compile the app completely; the
+'speed-profile' mode will compile the app according to a profile of the utilized
+code paths that is collected during app usage. It can be a bit tricky to collect
+profiles consistently and correctly, so if you decide to use them, you'll
+probably want to confirm they are what you expect. They're located here:
+
+```shell
+/data/misc/profiles/ref/[package-name]/primary.prof
+```
+
+Note that Macrobenchmark allows you to directly
+[specify compilation mode](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/benchmark/docs/macrobenchmark.md#compilation-mode).
+
+### System Considerations {#system-considerations}
+
+For low-level/high fidelity measurements, **calibrate your devices**. Try to run
+A/B comparisons across the same device and same OS version. There can be
+significant variations in performance, even across the same device type.
+
+On rooted devices, consider using a
+[lockClocks script](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh)
+for microbenchmarks. Among other things, these scripts: place CPUs at a fixed
+frequency, disable little cores, configure the GPU, and disable thermal
+throttling. This is not recommended for user-experience focused tests (i.e app
+launch, DoU testing, jank testing, etc.), but can be essential for cutting down
+noise in microbenchmark tests.
+
+On rooted devices, consider killing the application and dropping file caches
+(“echo 3 > /proc/sys/vm/drop\_caches”) between iterations. This will more
+accurately model cold start behavior.
+
+When possible, consider using a testing framework like
+[Macrobenchmark](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/benchmark/docs/macrobenchmark.md),
+which can reduce noise in your measurements, and prevent measurement inaccuracy.
+
+## Common Problems {#common-problems}
+
+### Slow App Startup: Unnecessary Trampoline Activity {#slow-app-startup-unnecessary-trampoline-activity}
+
+A trampoline activity can extend app startup time unnecessarily, and it's
+important to be aware if your app is doing it. As you can see in the example
+trace below, one activityStart is immediately followed by another activityStart
+without any frames being drawn by the first activity.
+
+![alt_text](app_audit_images/trace_startup.png "Trace showing trampoline activity.")
+
+This can happen both in a notification entrypoint and a regular app startup
+entrypoint, and can often be addressed by refactoring -- can you add the check
+you're firing up an activity for as a module that can be shared between
+activities?
+
+### Unnecessary Allocations Triggering Frequent GCs {#unnecessary-allocations-triggering-frequent-gcs}
+
+You may note that GCs are happening more frequently than you expect in a
+systrace.
+
+In this case, every 10 seconds during a long-running operation is an indicator
+that we might be allocating unnecessarily but consistently over time:
+
+![alt_text](app_audit_images/trace_gc.png "Trace showing space between GC events.")
+
+Or, you may notice that a specific callstack is making the vast majority of the
+allocations when using the Memory Profiler. You don't need to eliminate all
+allocations aggressively, as this can make code harder to maintain. Start
+instead by working on hotspots of allocations.
+
+### Janky Frames {#janky-frames}
+
+The graphics pipeline is relatively complicated, and there can be some nuance
+involved in determining whether a user ultimately may have seen a dropped
+frame - in some cases, the platform can "rescue" a frame using buffering.
+However, you can ignore most of that nuance to easily identify problematic
+frames from your app's perspective.
+
+When frames are being drawn with little work required from the app, the
+Choreographer#doFrame tracepoints occur on a 16.7ms cadence (assuming a 60 FPS
+device):
+
+![alt_text](app_audit_images/trace_frames1.png "Trace showing frequent fast frames.")
+
+If you zoom out and navigate through the trace, you'll sometimes see frames take
+a little longer to complete, but that's still okay because they're not taking
+more than their allotted 16.7ms time:
+
+![alt_text](app_audit_images/trace_frames2.png "Trace showing frequent fast frames with periodic bursts of work.")
+
+But when you actually see a disruption to that regular cadence, that will be a
+janky frame:
+
+![alt_text](app_audit_images/trace_frames3.png "Trace showing janky frames.")
+
+With a little practice, you'll be able to see them everywhere!
+
+![alt_text](app_audit_images/trace_frames4.png "Trace showing more janky frames.")
+
+In some cases, you'll just need to zoom into that tracepoint for more
+information about which views are being inflated or what RecyclerView is doing.
+In other cases, you may have to inspect further.
+
+For more information about identifying janky frames and debugging their causes,
+see
+
+[Slow Rendering Vitals Documentation](https://developer.android.com/topic/performance/vitals/render)
+
+### Common RecyclerView mistakes {#common-recyclerview-mistakes}
+
+*   Invalidating the entire RecyclerView's backing data unnecessarily. This can
+    lead to long frame rendering times and hence a jank. Instead, invalidate
+    only the data that has changed, to minimize the number of views that need to
+    update.
+    *   See
+        [Presenting Dynamic Data](https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView#presenting-dynamic-data)
+        for ways to avoid costly notifyDatasetChanged() calls, when content is
+        updated rather than replaced from scratch.
+*   Failing to support nested RecyclerViews properly, causing the internal
+    RecyclerView to be re-created from scratch every time.
+    *   Every nested, inner RecyclerView should have a
+        [RecycledViewPool](https://developer.android.com/reference/kotlin/androidx/recyclerview/widget/RecyclerView.RecycledViewPool)
+        set to ensure Views can be recycled between inner RecyclerViews
+*   Not prefetching enough data, or not prefetching in a timely manner. It can
+    be jarring to quickly hit the bottom of a scrolling list and need to wait
+    for more data from the server. While this isn't technically "jank" as no
+    frame deadlines are being missed, it can be a significant UX improvement
+    just to tweak the timing and quantity of prefetching so that the user
+    doesn't have to wait for data.
+
+## Tools Documentation {#tools-documentation}
+
+### Using Systrace/Perfetto {#using-systrace-perfetto}
+
+In case you like videos,
+[here's a talk](https://www.youtube.com/watch?v=qXVxuLvzKek) summarizing basic
+systrace usage.
+
+#### Debugging App Startup Using Systrace {#debugging-app-startup-using-systrace}
+
+The
+[Android developer documentation on App startup time](https://developer.android.com/topic/performance/vitals/launch-time)
+provides a good overview of the application startup process.
+
+Generally the stages of app startup are:
+
+*   Launch the process
+*   Initialize generic application objects
+*   Create and Initialize activity
+*   Inflate the layout
+*   Draw the first frame
+
+Startup types can be disambiguated by these stages:
+
+*   Cold startup -- Start at creating a new process with no
+    [saved state](https://developer.android.com/reference/android/os/Bundle).
+*   Warm startup -- Either recreates the activity while reusing the process, or
+    recreates the process with saved state. The Macrobenchmark testing library
+    supports consistent warm startup testing utilizing the first option.
+*   Hot startup -- Restarts the activity and starts at inflation.
+
+We recommend capturing systraces
+[using the on-device system tracing app available in Developer Options](https://developer.android.com/topic/performance/tracing/on-device).
+If you'd like to use command-line tools, [Perfetto](http://perfetto.dev/docs) is
+available on Android Q+, while devices on earlier versions should rely on
+[systrace](https://developer.android.com/topic/performance/vitals/launch-time).
+
+Note that “first frame” is a bit of a misnomer, applications can vary
+significantly in how they handle startup after creating the initial activity.
+Some applications will continue inflation for several frames, and others will
+even immediately launch into a secondary activity.
+
+When possible, we recommend that app developers include a
+[reportFullyDrawn](https://developer.android.com/reference/android/app/Activity#reportFullyDrawn\(\))
+(available Q+) call when startup is completed from the application’s
+perspective. RFD-defined start times can be extracted through the
+[Perfetto trace processor](https://perfetto.dev/docs/analysis/trace-processor),
+and a user-visible trace event will be emitted.
+
+Some things that you should look for include:
+
+*   [Monitor contention](app_audit_images/trace_monitor_contention.png) --
+    competition for monitor-protected resources can introduce significant delay
+    in app startup.
+*   [Synchronous binder transactions](app_audit_images/trace_sync_binder.png) --
+    Look for unnecessary transactions in your application’s critical path.
+*   [Common Sources of Jank](https://developer.android.com/topic/performance/vitals/render#common-jank)
+*   [Concurrent garbage collection](app_audit_images/trace_concurrent_gc.png) is
+    common and relatively low impact, but if you’re hitting it often consider
+    looking into it with the Android Studio memory profiler.
+*   Check for [IO](app_audit_images/trace_uninterruptable_sleep.png) performed
+    during startup, and look for long stalls.
+    *   Note that other processes performing IO at the same time can cause IO
+        contention, so ensure that other processes are not running.
+*   Significant activity on other threads can interfere with the UI thread, so
+    watch out for background work during startup. Note that devices can have
+    different CPU configurations, so the number of threads that can run in
+    parallel can vary across devices.
+
+### Using Android Studio Memory Profiler {#using-android-studio-memory-profiler}
+
+[Memory profiler documentation](https://developer.android.com/studio/profile/memory-profiler)
+
+The Android Studio memory profiler is a powerful tool to reduce memory pressure
+that could be caused by memory leaks or bad usage patterns since it provides a
+live view of object allocations
+
+To fix memory problems in your app you can use the memory profiler to track why
+and how often garbage collections happen.
+
+The overall steps taken when profiling app memory can be broken down into the
+following steps:
+
+#### 1. Detect memory problems
+
+Start recording a memory profiling session of the user journey you care about,
+then look for an
+[increasing object count](app_audit_images/studio_increasing_object_count.jpg)
+which will eventually lead to
+[garbage collections](app_audit_images/studio_gc.jpg).
+
+Once you have identified that there is a certain user journey that is adding
+memory pressure start analyzing for root causes of the memory pressure.
+
+#### 2. Diagnose memory pressure hot spots
+
+Select a range in the timeline to
+[visualize both Allocations and Shallow Size](app_audit_images/studio_alloc_and_shallow_size.jpg).
+The are multiple ways to sort this data. Here are some examples of how each view
+can help you analyze problems.
+
+##### Arrange by class
+
+Useful when you want to find classes that are generating objects that should
+otherwise be cached or reused from a memory pool.
+
+For example: Imagine you see an app creating 2000 objects of class called
+“Vertex” every second. This would increase the Allocations count by 2000 every
+second and you would see it when sorting by class. Should we be reusing such
+objects to avoid generating that garbage? If the answer is yes, then likely
+implementing a memory pool will be needed.
+
+##### Arrange by callstack
+
+Useful when there is a hot path where memory is being allocated, maybe inside a
+for loop or inside a specific function doing a lot of allocation work you will
+be able to find it here.
+
+##### Shallow size vs Retained size, which one should I use to find memory issues?
+
+Shallow size only tracks the memory of the object itself, so it will be useful
+for tracking simple classes composed mostly of primitive values only.
+
+Retained Size shows the total memory due to the object and references that are
+solely referenced by the object, so it will be useful for tracking memory
+pressure due to complex objects. To get this value, take a
+[full memory dump](app_audit_images/studio_memory_dump.jpg) and it will be
+[added as a column](app_audit_images/studio_retained_size.jpg).
+
+### 3. Measure impact of an optimization
+
+The more evident and easy to measure impact of memory optimizations are GCs.
+When an optimization reduces the memory pressure, then you should see fewer GCs.
+
+To measure this, in the profiler timeline measure the time between GCs, and you
+should see it taking longer between GCs.
+
+The ultimate impact of memory improvements like this is:
+
+*   OOM kills will likely be reduced if the app does not constantly hit memory
+    pressure
+*   Having less GCs improves jank metrics, especially in the P99. This is
+    because GCs cause CPU contention, which could lead to rendering tasks being
+    deferred while GC is happening.
diff --git a/benchmark/docs/app_audit_images/studio_alloc_and_shallow_size.jpg b/benchmark/docs/app_audit_images/studio_alloc_and_shallow_size.jpg
new file mode 100644
index 0000000..49a839d
--- /dev/null
+++ b/benchmark/docs/app_audit_images/studio_alloc_and_shallow_size.jpg
Binary files differ
diff --git a/benchmark/docs/app_audit_images/studio_gc.jpg b/benchmark/docs/app_audit_images/studio_gc.jpg
new file mode 100644
index 0000000..b6d4162
--- /dev/null
+++ b/benchmark/docs/app_audit_images/studio_gc.jpg
Binary files differ
diff --git a/benchmark/docs/app_audit_images/studio_increasing_object_count.jpg b/benchmark/docs/app_audit_images/studio_increasing_object_count.jpg
new file mode 100644
index 0000000..b8da923
--- /dev/null
+++ b/benchmark/docs/app_audit_images/studio_increasing_object_count.jpg
Binary files differ
diff --git a/benchmark/docs/app_audit_images/studio_memory_dump.jpg b/benchmark/docs/app_audit_images/studio_memory_dump.jpg
new file mode 100644
index 0000000..6c45bcd
--- /dev/null
+++ b/benchmark/docs/app_audit_images/studio_memory_dump.jpg
Binary files differ
diff --git a/benchmark/docs/app_audit_images/studio_retained_size.jpg b/benchmark/docs/app_audit_images/studio_retained_size.jpg
new file mode 100644
index 0000000..d3f4f66
--- /dev/null
+++ b/benchmark/docs/app_audit_images/studio_retained_size.jpg
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_concurrent_gc.png b/benchmark/docs/app_audit_images/trace_concurrent_gc.png
new file mode 100644
index 0000000..9e640db
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_concurrent_gc.png
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_frames1.png b/benchmark/docs/app_audit_images/trace_frames1.png
new file mode 100644
index 0000000..b4d6397
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_frames1.png
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_frames2.png b/benchmark/docs/app_audit_images/trace_frames2.png
new file mode 100644
index 0000000..9c1a54d
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_frames2.png
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_frames3.png b/benchmark/docs/app_audit_images/trace_frames3.png
new file mode 100644
index 0000000..0c66bb6
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_frames3.png
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_frames4.png b/benchmark/docs/app_audit_images/trace_frames4.png
new file mode 100644
index 0000000..3d9b145
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_frames4.png
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_gc.png b/benchmark/docs/app_audit_images/trace_gc.png
new file mode 100644
index 0000000..01b254b
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_gc.png
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_monitor_contention.png b/benchmark/docs/app_audit_images/trace_monitor_contention.png
new file mode 100644
index 0000000..af66d1e
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_monitor_contention.png
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_startup.png b/benchmark/docs/app_audit_images/trace_startup.png
new file mode 100644
index 0000000..4c75837
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_startup.png
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_sync_binder.png b/benchmark/docs/app_audit_images/trace_sync_binder.png
new file mode 100644
index 0000000..4920f17
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_sync_binder.png
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_uninterruptable_sleep.png b/benchmark/docs/app_audit_images/trace_uninterruptable_sleep.png
new file mode 100644
index 0000000..b305a9b
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_uninterruptable_sleep.png
Binary files differ
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index eb4b723..ca5eda1 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -144,7 +144,7 @@
     val WEAR_WATCHFACE_EDITOR = Version("1.0.0-alpha09")
     val WEAR_WATCHFACE_STYLE = Version("1.0.0-alpha09")
     val WEBKIT = Version("1.5.0-alpha01")
-    val WINDOW = Version("1.0.0-alpha04")
+    val WINDOW = Version("1.0.0-alpha05")
     val WINDOW_EXTENSIONS = Version("1.0.0-alpha01")
     val WINDOW_SIDECAR = Version("0.1.0-alpha01")
     val WORK = Version("2.6.0-alpha01")
diff --git a/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
index 4ce1461..6c4446d 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -159,6 +159,11 @@
                 disable("BanUncheckedReflection")
             }
 
+            // Only run certain checks where API tracking is important.
+            if (extension.type.checkApi is RunApiTasks.No) {
+                disable("IllegalExperimentalApiUsage")
+            }
+
             // If the project has not overridden the lint config, set the default one.
             if (lintConfig == null) {
                 // suppress warnings more specifically than issue-wide severity (regexes)
diff --git a/buildSrc/src/main/kotlin/androidx/build/ReportLibraryMetricsTask.kt b/buildSrc/src/main/kotlin/androidx/build/ReportLibraryMetricsTask.kt
index 9edc2ba..bbbeffc 100644
--- a/buildSrc/src/main/kotlin/androidx/build/ReportLibraryMetricsTask.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/ReportLibraryMetricsTask.kt
@@ -31,12 +31,12 @@
 import org.json.simple.JSONObject
 import java.io.File
 
-private const val AAR_FILE_EXTENSION = ".aar"
 private const val BYTECODE_SIZE = "bytecode_size"
 private const val METHOD_COUNT = "method_count"
 private const val METRICS_DIRECTORY = "librarymetrics"
 private const val JSON_FILE_EXTENSION = ".json"
 private const val JAR_FILE_EXTENSION = ".jar"
+private const val LINT_JAR = "lint$JAR_FILE_EXTENSION"
 
 @CacheableTask
 abstract class ReportLibraryMetricsTask : DefaultTask() {
@@ -77,7 +77,10 @@
 
     private fun getJarFiles(): List<File> {
         return jarFiles.files.filter { file ->
-            file.name.endsWith(JAR_FILE_EXTENSION)
+            file.name.endsWith(JAR_FILE_EXTENSION) &&
+                // AARs bundle a `lint.jar` that contains lint checks published by the library -
+                // this isn't runtime code and is not part of the actual library, so ignore it.
+                file.name != LINT_JAR
         }
     }
 
diff --git a/camera/camera-camera2-pipe-testing/build.gradle b/camera/camera-camera2-pipe-testing/build.gradle
index 4e420b00..c340844 100644
--- a/camera/camera-camera2-pipe-testing/build.gradle
+++ b/camera/camera-camera2-pipe-testing/build.gradle
@@ -50,10 +50,6 @@
         minSdkVersion 21
     }
 
-    sourceSets {
-        test.java.srcDirs += 'src/robolectric/java'
-    }
-
     // Use Robolectric 4.+
     testOptions.unitTests.includeAndroidResources = true
 }
diff --git a/camera/camera-camera2-pipe-testing/src/robolectric/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt b/camera/camera-camera2-pipe-testing/src/robolectric/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
deleted file mode 100644
index ddf221c..0000000
--- a/camera/camera-camera2-pipe-testing/src/robolectric/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.pipe.testing
-
-import org.junit.runners.model.FrameworkMethod
-import org.robolectric.RobolectricTestRunner
-import org.robolectric.internal.bytecode.InstrumentationConfiguration
-
-/**
- * A [RobolectricTestRunner] for [androidx.camera.camera2.pipe] unit tests.
- *
- * This test runner disables instrumentation for the [androidx.camera.camera2.pipe] and
- * [androidx.camera.camera2.pipe.testing] packages.
- *
- * Robolectric tries to instrument Kotlin classes, and it throws errors when it encounters
- * companion objects, constructors with default values for parameters, and data classes with
- * inline classes. We don't need shadowing of our classes because we want to use the actual
- * objects in our tests.
- */
-public class RobolectricCameraPipeTestRunner(testClass: Class<*>) :
-    RobolectricTestRunner(testClass) {
-    override fun createClassLoaderConfig(method: FrameworkMethod?): InstrumentationConfiguration {
-        val builder = InstrumentationConfiguration.Builder(super.createClassLoaderConfig(method))
-        builder.doNotInstrumentPackage("androidx.camera.camera2.pipe")
-        builder.doNotInstrumentPackage("androidx.camera.camera2.pipe.testing")
-        return builder.build()
-    }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunnerTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
similarity index 64%
rename from camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunnerTest.kt
rename to camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
index b5f7555..13a41fc 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunnerTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -21,7 +21,31 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
+import org.junit.runners.model.FrameworkMethod
+import org.robolectric.RobolectricTestRunner
 import org.robolectric.annotation.Config
+import org.robolectric.internal.bytecode.InstrumentationConfiguration
+
+/**
+ * A [RobolectricTestRunner] for [androidx.camera.camera2.pipe] unit tests.
+ *
+ * This test runner disables instrumentation for the [androidx.camera.camera2.pipe] and
+ * [androidx.camera.camera2.pipe.testing] packages.
+ *
+ * Robolectric tries to instrument Kotlin classes, and it throws errors when it encounters
+ * companion objects, constructors with default values for parameters, and data classes with
+ * inline classes. We don't need shadowing of our classes because we want to use the actual
+ * objects in our tests.
+ */
+public class RobolectricCameraPipeTestRunner(testClass: Class<*>) :
+    RobolectricTestRunner(testClass) {
+    override fun createClassLoaderConfig(method: FrameworkMethod?): InstrumentationConfiguration {
+        val builder = InstrumentationConfiguration.Builder(super.createClassLoaderConfig(method))
+        builder.doNotInstrumentPackage("androidx.camera.camera2.pipe")
+        builder.doNotInstrumentPackage("androidx.camera.camera2.pipe.testing")
+        return builder.build()
+    }
+}
 
 @Suppress("EXPERIMENTAL_FEATURE_WARNING")
 public inline class TestValue(public val value: String)
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCamerasTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCamerasTest.kt
deleted file mode 100644
index 87f16db..0000000
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCamerasTest.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.pipe.testing
-
-import android.content.Context
-import android.hardware.camera2.CameraCharacteristics
-import android.os.Build
-import android.os.Looper
-import androidx.test.core.app.ApplicationProvider
-import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.robolectric.Shadows.shadowOf
-import org.robolectric.annotation.Config
-
-@RunWith(RobolectricCameraPipeTestRunner::class)
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-class RobolectricCamerasTest {
-    private val context = ApplicationProvider.getApplicationContext() as Context
-    private val mainLooper = shadowOf(Looper.getMainLooper())
-
-    @Test
-    fun fakeCamerasCanBeOpened() {
-        val fakeCameraId = RobolectricCameras.create(
-            mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
-        )
-        val fakeCamera = RobolectricCameras.open(fakeCameraId)
-
-        assertThat(fakeCamera).isNotNull()
-        assertThat(fakeCamera.cameraId).isEqualTo(fakeCameraId)
-        assertThat(fakeCamera.cameraDevice).isNotNull()
-        assertThat(fakeCamera.characteristics).isNotNull()
-        assertThat(fakeCamera.characteristics[CameraCharacteristics.LENS_FACING]).isNotNull()
-        assertThat(fakeCamera.metadata).isNotNull()
-        assertThat(fakeCamera.metadata[CameraCharacteristics.LENS_FACING]).isNotNull()
-    }
-
-    @After
-    fun teardown() {
-        mainLooper.idle()
-        RobolectricCameras.clear()
-    }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/build.gradle b/camera/camera-camera2-pipe/build.gradle
index 014c031..50d5b87 100644
--- a/camera/camera-camera2-pipe/build.gradle
+++ b/camera/camera-camera2-pipe/build.gradle
@@ -61,11 +61,6 @@
         minSdkVersion 21
     }
 
-    // Include additional robolectric utilities in tests
-    sourceSets {
-        test.java.srcDirs += ['../camera-camera2-pipe-testing/src/robolectric/java']
-    }
-
     // Use Robolectric 4.+
     testOptions.unitTests.includeAndroidResources = true
 }
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunnerTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
similarity index 64%
copy from camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunnerTest.kt
copy to camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
index b5f7555..13a41fc 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunnerTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -21,7 +21,31 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
+import org.junit.runners.model.FrameworkMethod
+import org.robolectric.RobolectricTestRunner
 import org.robolectric.annotation.Config
+import org.robolectric.internal.bytecode.InstrumentationConfiguration
+
+/**
+ * A [RobolectricTestRunner] for [androidx.camera.camera2.pipe] unit tests.
+ *
+ * This test runner disables instrumentation for the [androidx.camera.camera2.pipe] and
+ * [androidx.camera.camera2.pipe.testing] packages.
+ *
+ * Robolectric tries to instrument Kotlin classes, and it throws errors when it encounters
+ * companion objects, constructors with default values for parameters, and data classes with
+ * inline classes. We don't need shadowing of our classes because we want to use the actual
+ * objects in our tests.
+ */
+public class RobolectricCameraPipeTestRunner(testClass: Class<*>) :
+    RobolectricTestRunner(testClass) {
+    override fun createClassLoaderConfig(method: FrameworkMethod?): InstrumentationConfiguration {
+        val builder = InstrumentationConfiguration.Builder(super.createClassLoaderConfig(method))
+        builder.doNotInstrumentPackage("androidx.camera.camera2.pipe")
+        builder.doNotInstrumentPackage("androidx.camera.camera2.pipe.testing")
+        return builder.build()
+    }
+}
 
 @Suppress("EXPERIMENTAL_FEATURE_WARNING")
 public inline class TestValue(public val value: String)
diff --git a/camera/camera-camera2-pipe-testing/src/robolectric/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt
similarity index 81%
rename from camera/camera-camera2-pipe-testing/src/robolectric/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt
rename to camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt
index 004cdf4..d21494a 100644
--- a/camera/camera-camera2-pipe-testing/src/robolectric/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt
@@ -25,14 +25,20 @@
 import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.CameraDevice
 import android.hardware.camera2.CameraManager
+import android.os.Build
 import android.os.Handler
 import android.os.Looper
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.CameraMetadata
 import androidx.camera.camera2.pipe.compat.Camera2CameraMetadata
 import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth
 import kotlinx.atomicfu.atomic
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
 import org.robolectric.Shadows.shadowOf
+import org.robolectric.annotation.Config
 import org.robolectric.shadow.api.Shadow
 import org.robolectric.shadows.ShadowApplication
 import org.robolectric.shadows.ShadowCameraCharacteristics
@@ -169,3 +175,32 @@
         }
     }
 }
+
+@RunWith(RobolectricCameraPipeTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class RobolectricCamerasTest {
+    private val context = ApplicationProvider.getApplicationContext() as Context
+    private val mainLooper = shadowOf(Looper.getMainLooper())
+
+    @Test
+    fun fakeCamerasCanBeOpened() {
+        val fakeCameraId = RobolectricCameras.create(
+            mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
+        )
+        val fakeCamera = RobolectricCameras.open(fakeCameraId)
+
+        Truth.assertThat(fakeCamera).isNotNull()
+        Truth.assertThat(fakeCamera.cameraId).isEqualTo(fakeCameraId)
+        Truth.assertThat(fakeCamera.cameraDevice).isNotNull()
+        Truth.assertThat(fakeCamera.characteristics).isNotNull()
+        Truth.assertThat(fakeCamera.characteristics[CameraCharacteristics.LENS_FACING]).isNotNull()
+        Truth.assertThat(fakeCamera.metadata).isNotNull()
+        Truth.assertThat(fakeCamera.metadata[CameraCharacteristics.LENS_FACING]).isNotNull()
+    }
+
+    @After
+    fun teardown() {
+        mainLooper.idle()
+        RobolectricCameras.clear()
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewTest.java b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewTest.java
index 41a8c1c..509895a 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewTest.java
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewTest.java
@@ -173,6 +173,45 @@
     }
 
     @Test
+    public void receiveSurfaceRequest_transformIsValid() throws InterruptedException {
+        // Arrange: set up PreviewView.
+        AtomicReference<PreviewView> previewView = new AtomicReference<>();
+        CountDownLatch countDownLatch = new CountDownLatch(1);
+        mInstrumentation.runOnMainSync(() -> {
+            previewView.set(new PreviewView(mContext));
+            setContentView(previewView.get());
+            // Feed the PreviewView with a fake SurfaceRequest
+            CameraInfo cameraInfo = createCameraInfo(CameraInfo.IMPLEMENTATION_TYPE_CAMERA2);
+            previewView.get().getSurfaceProvider().onSurfaceRequested(
+                    createSurfaceRequest(cameraInfo));
+            notifyLatchWhenLayoutReady(previewView.get(), countDownLatch);
+        });
+        updateCropRectAndWaitForIdle(DEFAULT_CROP_RECT);
+
+        // Assert: OutputTransform is not null.
+        assertThat(countDownLatch.await(1, TimeUnit.SECONDS)).isTrue();
+        mInstrumentation.runOnMainSync(
+                () -> assertThat(previewView.get().getOutputTransform()).isNotNull());
+    }
+
+    @Test
+    public void noSurfaceRequest_transformIsInvalid() throws InterruptedException {
+        // Arrange: set up PreviewView.
+        AtomicReference<PreviewView> previewView = new AtomicReference<>();
+        CountDownLatch countDownLatch = new CountDownLatch(1);
+        mInstrumentation.runOnMainSync(() -> {
+            previewView.set(new PreviewView(mContext));
+            setContentView(previewView.get());
+            notifyLatchWhenLayoutReady(previewView.get(), countDownLatch);
+        });
+
+        // Assert: OutputTransform is null.
+        assertThat(countDownLatch.await(1, TimeUnit.SECONDS)).isTrue();
+        mInstrumentation.runOnMainSync(
+                () -> assertThat(previewView.get().getOutputTransform()).isNull());
+    }
+
+    @Test
     public void previewViewPinched_pinchToZoomInvokedOnController()
             throws InterruptedException, UiObjectNotFoundException {
         // TODO(b/169058735): investigate and enable on Cuttlefish.
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
index 9291c93..8aa80f9 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
@@ -227,8 +227,7 @@
      *
      * <p> The calculation is based on making the crop rect to fill or fit the {@link PreviewView}.
      */
-    private Matrix getSurfaceToPreviewViewMatrix(Size previewViewSize,
-            int layoutDirection) {
+    Matrix getSurfaceToPreviewViewMatrix(Size previewViewSize, int layoutDirection) {
         Preconditions.checkState(isTransformationInfoReady());
         Matrix matrix = new Matrix();
 
@@ -397,6 +396,14 @@
     }
 
     /**
+     * Return the crop rect of the preview surface.
+     */
+    @Nullable
+    Rect getSurfaceCropRect() {
+        return mSurfaceCropRect;
+    }
+
+    /**
      * Creates a transformed screenshot of {@link PreviewView}.
      *
      * <p> Creates the transformed {@link Bitmap} by applying the same transformation applied to
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java b/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
index 90ed0e6..fcf66b48 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
@@ -16,11 +16,15 @@
 
 package androidx.camera.view;
 
+import static androidx.camera.view.transform.OutputTransform.getNormalizedToBuffer;
+
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Rect;
 import android.hardware.camera2.CameraCharacteristics;
 import android.os.Build;
 import android.util.AttributeSet;
@@ -40,6 +44,7 @@
 import androidx.annotation.ColorRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
 import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.experimental.UseExperimental;
@@ -60,6 +65,8 @@
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.ImageOutputConfig;
 import androidx.camera.core.impl.utils.Threads;
+import androidx.camera.view.transform.CoordinateTransform;
+import androidx.camera.view.transform.OutputTransform;
 import androidx.core.content.ContextCompat;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
@@ -827,6 +834,57 @@
         return mCameraController;
     }
 
+    /**
+     * Gets the {@link OutputTransform} associated with the {@link PreviewView}.
+     *
+     * <p> Returns a {@link OutputTransform} object that represents the transform being applied to
+     * the associated {@link Preview} use case. Returns null if the transform info is not ready.
+     * For example, when the associated {@link Preview} has not been bound or the
+     * {@link PreviewView}'s layout is not ready.
+     *
+     * <p> {@link PreviewView} needs to be in {@link ImplementationMode#COMPATIBLE} mode for the
+     * transform to work correctly. For example, the returned {@link OutputTransform} may
+     * not respect the value of {@link #getScaleX()} when {@link ImplementationMode#PERFORMANCE}
+     * mode is used.
+     *
+     * @return the transform applied on the preview by this {@link PreviewView}.
+     * @hide
+     * @see CoordinateTransform
+     */
+    // TODO(b/179827713): unhide this once all transform utils are done.
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @TransformExperimental
+    @Nullable
+    public OutputTransform getOutputTransform() {
+        Threads.checkMainThread();
+        Matrix matrix = null;
+        try {
+            matrix = mPreviewTransform.getSurfaceToPreviewViewMatrix(
+                    new Size(getWidth(), getHeight()), getLayoutDirection());
+        } catch (IllegalStateException ex) {
+            // Fall-through. It will be handled below.
+        }
+
+        Rect surfaceCropRect = mPreviewTransform.getSurfaceCropRect();
+        if (matrix == null || surfaceCropRect == null) {
+            Logger.d(TAG, "Transform info is not ready");
+            return null;
+        }
+        // Map it to the normalized space (0, 0) - (1, 1).
+        matrix.preConcat(getNormalizedToBuffer(surfaceCropRect));
+
+        // Add the custom transform applied by the app. e.g. View#setScaleX.
+        if (mImplementation instanceof TextureViewImplementation) {
+            matrix.postConcat(getMatrix());
+        } else {
+            Logger.w(TAG, "PreviewView needs to be in COMPATIBLE mode for the transform"
+                    + " to work correctly.");
+        }
+
+        return new OutputTransform(matrix, new Size(surfaceCropRect.width(),
+                surfaceCropRect.height()));
+    }
+
     @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
     private void attachToControllerIfReady(boolean shouldFailSilently) {
         Display display = getDisplay();
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/transform/OutputTransform.java b/camera/camera-view/src/main/java/androidx/camera/view/transform/OutputTransform.java
index c931b8f..4011e8e6 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/transform/OutputTransform.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/transform/OutputTransform.java
@@ -60,8 +60,10 @@
      *                     other {@link OutputTransform}, we can at least make sure that they
      *                     have the same aspect ratio. Viewports with different aspect ratios
      *                     cannot be from the same {@link UseCaseGroup}.
+     * @hide
      */
-    OutputTransform(@NonNull Matrix matrix, @NonNull Size viewPortSize) {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public OutputTransform(@NonNull Matrix matrix, @NonNull Size viewPortSize) {
         mMatrix = matrix;
         mViewPortSize = viewPortSize;
     }
@@ -76,8 +78,12 @@
         return mViewPortSize;
     }
 
+    /**
+     * @hide
+     */
     @NonNull
-    static Matrix getNormalizedToBuffer(@NonNull Rect viewPortRect) {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static Matrix getNormalizedToBuffer(@NonNull Rect viewPortRect) {
         return getNormalizedToBuffer(new RectF(viewPortRect));
     }
 
diff --git a/camera/integration-tests/coretestapp/build.gradle b/camera/integration-tests/coretestapp/build.gradle
index 65a474e..1fd48a3 100644
--- a/camera/integration-tests/coretestapp/build.gradle
+++ b/camera/integration-tests/coretestapp/build.gradle
@@ -70,7 +70,8 @@
     implementation(project(":appcompat:appcompat"))
     implementation("androidx.activity:activity:1.2.0")
     implementation("androidx.fragment:fragment:1.3.0")
-    implementation("androidx.concurrent:concurrent-futures:1.0.0")
+    // Needed because AGP enforces same version between main and androidTest classpaths
+    implementation(project(":concurrent:concurrent-futures"))
 
     // Android Support Library
     api(CONSTRAINT_LAYOUT, { transitive = true })
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java
index c23de8b..762eda3 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java
@@ -122,6 +122,9 @@
             case R.id.camera_controller:
                 mMode = Mode.CAMERA_CONTROLLER;
                 break;
+            case R.id.transform:
+                mMode = Mode.TRANSFORM;
+                break;
         }
         startFragment();
         return true;
@@ -148,6 +151,8 @@
             case CAMERA_CONTROLLER:
                 startFragment(R.string.camera_controller, new CameraControllerFragment());
                 break;
+            case TRANSFORM:
+                startFragment(R.string.transform, new TransformFragment());
         }
     }
 
@@ -165,6 +170,6 @@
     }
 
     private enum Mode {
-        CAMERA_VIEW, PREVIEW_VIEW, CAMERA_CONTROLLER
+        CAMERA_VIEW, PREVIEW_VIEW, CAMERA_CONTROLLER, TRANSFORM
     }
 }
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/OverlayView.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/OverlayView.java
new file mode 100644
index 0000000..614515df
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/OverlayView.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * A overlay view for drawing a tile with {@link Canvas}.
+ */
+public final class OverlayView extends FrameLayout {
+
+    private RectF mTile;
+    private final Paint mPaint = new Paint();
+
+    public OverlayView(@NonNull Context context) {
+        super(context);
+    }
+
+
+    public OverlayView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    void setTileRect(RectF tile) {
+        mTile = tile;
+    }
+
+    @Override
+    public void onDraw(@NonNull Canvas canvas) {
+        super.onDraw(canvas);
+        if (mTile == null) {
+            return;
+        }
+
+        // The tile paint is black stroke with a white glow so it's always visible regardless of
+        // the background.
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setColor(Color.WHITE);
+        mPaint.setStrokeWidth(10);
+        canvas.drawRect(mTile, mPaint);
+
+        mPaint.setColor(Color.BLACK);
+        mPaint.setStrokeWidth(5);
+        canvas.drawRect(mTile, mPaint);
+    }
+}
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/TransformFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/TransformFragment.java
new file mode 100644
index 0000000..fc44966
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/TransformFragment.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.view;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ToggleButton;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.experimental.UseExperimental;
+import androidx.camera.core.ImageAnalysis;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.view.LifecycleCameraController;
+import androidx.camera.view.PreviewView;
+import androidx.camera.view.TransformExperimental;
+import androidx.camera.view.transform.CoordinateTransform;
+import androidx.camera.view.transform.ImageProxyTransformFactory;
+import androidx.camera.view.transform.OutputTransform;
+import androidx.fragment.app.Fragment;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * A fragment that demos transform utilities.
+ */
+public final class TransformFragment extends Fragment {
+
+    private static final int TILE_COUNT = 4;
+
+    private LifecycleCameraController mCameraController;
+    private ExecutorService mExecutorService;
+    private ToggleButton mMirror;
+
+    // Synthetic access
+    @SuppressWarnings("WeakerAccess")
+    PreviewView mPreviewView;
+    // Synthetic access
+    @SuppressWarnings("WeakerAccess")
+    OverlayView mOverlayView;
+
+    private final ImageAnalysis.Analyzer mAnalyzer = new ImageAnalysis.Analyzer() {
+
+        private final ImageProxyTransformFactory mImageProxyTransformFactory =
+                new ImageProxyTransformFactory.Builder().build();
+
+        @Override
+        @UseExperimental(markerClass = TransformExperimental.class)
+        @SuppressWarnings("RestrictedApi")
+        public void analyze(@NonNull ImageProxy imageProxy) {
+            // Find the tile to highlight.
+            RectF brightestTile = findBrightestTile(imageProxy);
+
+            // Calculate PreviewView transform on UI thread.
+            mOverlayView.post(() -> {
+                // Calculate the transform.
+                try (ImageProxy imageToClose = imageProxy)  {
+                    OutputTransform previewViewTransform = mPreviewView.getOutputTransform();
+                    if (previewViewTransform == null) {
+                        // PreviewView transform info is not ready. No-op.
+                        return;
+                    }
+                    CoordinateTransform transform = new CoordinateTransform(
+                            mImageProxyTransformFactory.getOutputTransform(imageToClose),
+                            previewViewTransform);
+                    Matrix analysisToPreview = new Matrix();
+                    transform.getTransform(analysisToPreview);
+
+                    // Map the tile to PreviewView coordinates.
+                    analysisToPreview.mapRect(brightestTile);
+                    // Draw the tile on top of PreviewView.
+                    mOverlayView.setTileRect(brightestTile);
+                    mOverlayView.postInvalidate();
+                }
+            });
+        }
+    };
+
+    /**
+     * Finds the brightest tile in the given {@link ImageProxy}.
+     *
+     * <p> Divides the crop rect of the image into a 4x4 grid, and find the brightest tile
+     * among the 16 tiles.
+     *
+     * @return the box of the brightest tile.
+     */
+    // Synthetic access
+    @SuppressWarnings("WeakerAccess")
+    RectF findBrightestTile(ImageProxy image) {
+        // Divide the crop rect in to 4x4 tiles.
+        Rect cropRect = image.getCropRect();
+        int[][] tiles = new int[TILE_COUNT][TILE_COUNT];
+        int tileWidth = cropRect.width() / TILE_COUNT;
+        int tileHeight = cropRect.height() / TILE_COUNT;
+
+        // Loop through the y plane and get the sum of the luminance for each tile.
+        byte[] bytes = new byte[image.getPlanes()[0].getBuffer().remaining()];
+        image.getPlanes()[0].getBuffer().get(bytes);
+        for (int x = 0; x < cropRect.width(); x++) {
+            for (int y = 0; y < cropRect.height(); y++) {
+                tiles[x / tileWidth][y / tileHeight] +=
+                        bytes[(y + cropRect.top) * image.getWidth() + cropRect.left + x] & 0xFF;
+            }
+        }
+
+        // Find the brightest tile among the 16 tiles.
+        float maxLuminance = 0;
+        int brightestTileX = 0;
+        int brightestTileY = 0;
+        for (int i = 0; i < TILE_COUNT; i++) {
+            for (int j = 0; j < TILE_COUNT; j++) {
+                if (tiles[i][j] > maxLuminance) {
+                    maxLuminance = tiles[i][j];
+                    brightestTileX = i;
+                    brightestTileY = j;
+                }
+            }
+        }
+
+        // Return the rectangle of the tile.
+        return new RectF(brightestTileX * tileWidth + cropRect.left,
+                brightestTileY * tileHeight + cropRect.top,
+                (brightestTileX + 1) * tileWidth + cropRect.left,
+                (brightestTileY + 1) * tileHeight + cropRect.top);
+    }
+
+    @NonNull
+    @Override
+    public View onCreateView(
+            @NonNull LayoutInflater inflater,
+            @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        mExecutorService = Executors.newSingleThreadExecutor();
+        mCameraController = new LifecycleCameraController(requireContext());
+        mCameraController.bindToLifecycle(getViewLifecycleOwner());
+
+        View view = inflater.inflate(R.layout.transform_view, container, false);
+
+        mPreviewView = view.findViewById(R.id.preview_view);
+        // Set to compatible so the custom transform (e.g. mirroring) would work.
+        mPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
+        mPreviewView.setController(mCameraController);
+
+        mCameraController.setImageAnalysisAnalyzer(mExecutorService, mAnalyzer);
+
+        mOverlayView = view.findViewById(R.id.overlay_view);
+        mMirror = view.findViewById(R.id.mirror_preview);
+        mMirror.setOnCheckedChangeListener((buttonView, isChecked) -> updateMirrorState());
+
+        updateMirrorState();
+        return view;
+    }
+
+    private void updateMirrorState() {
+        if (mMirror.isChecked()) {
+            mPreviewView.setScaleX(-1);
+        } else {
+            mPreviewView.setScaleX(1);
+        }
+    }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        if (mExecutorService != null) {
+            mExecutorService.shutdown();
+        }
+    }
+}
diff --git a/camera/integration-tests/viewtestapp/src/main/res/layout-land/transform_view.xml b/camera/integration-tests/viewtestapp/src/main/res/layout-land/transform_view.xml
new file mode 100644
index 0000000..f7f80f0
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/src/main/res/layout-land/transform_view.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2020 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="horizontal">
+    <FrameLayout
+        android:id="@+id/container"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1">
+        <androidx.camera.view.PreviewView
+            android:id="@+id/preview_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+        <androidx.camera.integration.view.OverlayView
+            android:id="@+id/overlay_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="#00000000"/>
+    </FrameLayout>
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent">
+        <ToggleButton
+            android:id="@+id/mirror_preview"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textOff="@string/mirror_off"
+            android:textOn="@string/mirror_on"
+            android:checked="false"/>
+    </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/camera/integration-tests/viewtestapp/src/main/res/layout/transform_view.xml b/camera/integration-tests/viewtestapp/src/main/res/layout/transform_view.xml
new file mode 100644
index 0000000..7c75d7f
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/src/main/res/layout/transform_view.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2020 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+    <FrameLayout
+        android:id="@+id/container"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1">
+        <androidx.camera.view.PreviewView
+            android:id="@+id/preview_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+        <androidx.camera.integration.view.OverlayView
+            android:id="@+id/overlay_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="#00000000"/>
+    </FrameLayout>
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+        <ToggleButton
+            android:id="@+id/mirror_preview"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textOff="@string/mirror_off"
+            android:textOn="@string/mirror_on"
+            android:checked="false"/>
+    </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/camera/integration-tests/viewtestapp/src/main/res/menu/actionbar_menu.xml b/camera/integration-tests/viewtestapp/src/main/res/menu/actionbar_menu.xml
index 52d01f9..8babd1e 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/menu/actionbar_menu.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/menu/actionbar_menu.xml
@@ -28,4 +28,8 @@
         android:id="@+id/camera_controller"
         android:title="@string/camera_controller"
         app:showAsAction="never" />
+    <item
+        android:id="@+id/transform"
+        android:title="@string/transform"
+        app:showAsAction="never" />
 </menu>
\ No newline at end of file
diff --git a/camera/integration-tests/viewtestapp/src/main/res/values/strings.xml b/camera/integration-tests/viewtestapp/src/main/res/values/strings.xml
index 7c76a73..adaba7a 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/values/strings.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/values/strings.xml
@@ -43,8 +43,11 @@
     <string name="camera_view">Camera View</string>
     <string name="preview_view">Preview View</string>
     <string name="camera_controller">Camera Controller</string>
+    <string name="transform">Transform</string>
     <string name="flash_mode_auto">flash auto</string>
     <string name="flash_mode_on">flash on</string>
     <string name="flash_mode_off">flash off</string>
+    <string name="mirror_on">mirror on</string>
+    <string name="mirror_off">mirror off</string>
 
 </resources>
diff --git a/car/app/app-testing/api/current.txt b/car/app/app-testing/api/current.txt
index 126bce5..52b6e21 100644
--- a/car/app/app-testing/api/current.txt
+++ b/car/app/app-testing/api/current.txt
@@ -9,6 +9,7 @@
     method public androidx.car.app.testing.ScreenController create();
     method public androidx.car.app.testing.ScreenController destroy();
     method public androidx.car.app.Screen get();
+    method public Object? getScreenResult();
     method public java.util.List<androidx.car.app.model.Template!> getTemplatesReturned();
     method public static androidx.car.app.testing.ScreenController of(androidx.car.app.testing.TestCarContext, androidx.car.app.Screen);
     method public androidx.car.app.testing.ScreenController pause();
diff --git a/car/app/app-testing/api/public_plus_experimental_current.txt b/car/app/app-testing/api/public_plus_experimental_current.txt
index 126bce5..52b6e21 100644
--- a/car/app/app-testing/api/public_plus_experimental_current.txt
+++ b/car/app/app-testing/api/public_plus_experimental_current.txt
@@ -9,6 +9,7 @@
     method public androidx.car.app.testing.ScreenController create();
     method public androidx.car.app.testing.ScreenController destroy();
     method public androidx.car.app.Screen get();
+    method public Object? getScreenResult();
     method public java.util.List<androidx.car.app.model.Template!> getTemplatesReturned();
     method public static androidx.car.app.testing.ScreenController of(androidx.car.app.testing.TestCarContext, androidx.car.app.Screen);
     method public androidx.car.app.testing.ScreenController pause();
diff --git a/car/app/app-testing/api/restricted_current.txt b/car/app/app-testing/api/restricted_current.txt
index 126bce5..52b6e21 100644
--- a/car/app/app-testing/api/restricted_current.txt
+++ b/car/app/app-testing/api/restricted_current.txt
@@ -9,6 +9,7 @@
     method public androidx.car.app.testing.ScreenController create();
     method public androidx.car.app.testing.ScreenController destroy();
     method public androidx.car.app.Screen get();
+    method public Object? getScreenResult();
     method public java.util.List<androidx.car.app.model.Template!> getTemplatesReturned();
     method public static androidx.car.app.testing.ScreenController of(androidx.car.app.testing.TestCarContext, androidx.car.app.Screen);
     method public androidx.car.app.testing.ScreenController pause();
diff --git a/car/app/app-testing/src/main/java/androidx/car/app/testing/ScreenController.java b/car/app/app-testing/src/main/java/androidx/car/app/testing/ScreenController.java
index 60d9140..9812453 100644
--- a/car/app/app-testing/src/main/java/androidx/car/app/testing/ScreenController.java
+++ b/car/app/app-testing/src/main/java/androidx/car/app/testing/ScreenController.java
@@ -22,6 +22,7 @@
 import android.util.Pair;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.car.app.Screen;
 import androidx.car.app.model.Template;
 import androidx.lifecycle.Lifecycle.Event;
@@ -87,6 +88,22 @@
     }
 
     /**
+     * Returns the result that was set via {@link Screen#setResult(Object)}, or {@code null} if
+     * one was not set.
+     */
+    @Nullable
+    public Object getScreenResult() {
+        try {
+            Field result = Screen.class.getDeclaredField("mResult");
+            result.setAccessible(true);
+            return result.get(mScreen);
+        } catch (ReflectiveOperationException e) {
+            throw new IllegalStateException("Unable to access result from screen being "
+                    + "controlled", e);
+        }
+    }
+
+    /**
      * Creates the {@link Screen} being controlled.
      *
      * <p>This method will also push the {@link Screen} onto the {@link
diff --git a/car/app/app-testing/src/main/java/androidx/car/app/testing/TestAppManager.java b/car/app/app-testing/src/main/java/androidx/car/app/testing/TestAppManager.java
index 35c91f8..6160238 100644
--- a/car/app/app-testing/src/main/java/androidx/car/app/testing/TestAppManager.java
+++ b/car/app/app-testing/src/main/java/androidx/car/app/testing/TestAppManager.java
@@ -129,6 +129,6 @@
     }
 
     TestAppManager(TestCarContext testCarContext, HostDispatcher hostDispatcher) {
-        super(testCarContext, hostDispatcher);
+        super(testCarContext, hostDispatcher, testCarContext.getLifecycleOwner().mRegistry);
     }
 }
diff --git a/car/app/app-testing/src/main/java/androidx/car/app/testing/TestCarContext.java b/car/app/app-testing/src/main/java/androidx/car/app/testing/TestCarContext.java
index 30051d1..035ee70 100644
--- a/car/app/app-testing/src/main/java/androidx/car/app/testing/TestCarContext.java
+++ b/car/app/app-testing/src/main/java/androidx/car/app/testing/TestCarContext.java
@@ -183,7 +183,6 @@
      *
      * @throws NullPointerException if either {@code serviceClass} or {@code service} are {@code
      *                              null}
-     *
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
@@ -195,7 +194,14 @@
         mOverriddenService.put(serviceName, service);
     }
 
-    TestLifecycleOwner getLifecycleOwner() {
+    /**
+     * Returns the {@link TestLifecycleOwner} that is used for this CarContext.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @NonNull
+    public TestLifecycleOwner getLifecycleOwner() {
         return mTestLifecycleOwner;
     }
 
diff --git a/car/app/app-testing/src/main/java/androidx/car/app/testing/navigation/TestNavigationManager.java b/car/app/app-testing/src/main/java/androidx/car/app/testing/navigation/TestNavigationManager.java
index e668cdd4..6c5f94c 100644
--- a/car/app/app-testing/src/main/java/androidx/car/app/testing/navigation/TestNavigationManager.java
+++ b/car/app/app-testing/src/main/java/androidx/car/app/testing/navigation/TestNavigationManager.java
@@ -132,6 +132,6 @@
 
     public TestNavigationManager(@NonNull TestCarContext testCarContext,
             @NonNull HostDispatcher hostDispatcher) {
-        super(testCarContext, hostDispatcher);
+        super(testCarContext, hostDispatcher, testCarContext.getLifecycleOwner().mRegistry);
     }
 }
diff --git a/car/app/app-testing/src/test/java/androidx/car/app/testing/ScreenControllerTest.java b/car/app/app-testing/src/test/java/androidx/car/app/testing/ScreenControllerTest.java
index c64f636..76f4bc0 100644
--- a/car/app/app-testing/src/test/java/androidx/car/app/testing/ScreenControllerTest.java
+++ b/car/app/app-testing/src/test/java/androidx/car/app/testing/ScreenControllerTest.java
@@ -159,6 +159,16 @@
     }
 
     @Test
+    public void getScreenResult() {
+        Screen screen = mScreenController.get();
+        String result = "this is the result";
+
+        screen.setResult(result);
+
+        assertThat(mScreenController.getScreenResult()).isEqualTo(result);
+    }
+
+    @Test
     public void reset() {
         mScreenController.start();
         mScreenController.reset();
diff --git a/car/app/app/src/main/java/androidx/car/app/AppManager.java b/car/app/app/src/main/java/androidx/car/app/AppManager.java
index e0038c8..851aa8f 100644
--- a/car/app/app/src/main/java/androidx/car/app/AppManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/AppManager.java
@@ -27,9 +27,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
-import androidx.car.app.model.TemplateWrapper;
 import androidx.car.app.utils.RemoteUtils;
-import androidx.car.app.utils.ThreadUtils;
+import androidx.lifecycle.Lifecycle;
 
 /** Manages the communication between the app and the host. */
 public class AppManager {
@@ -39,6 +38,8 @@
     private final IAppManager.Stub mAppManager;
     @NonNull
     private final HostDispatcher mHostDispatcher;
+    @NonNull
+    private final Lifecycle mLifecycle;
 
     /**
      * Sets the {@link SurfaceCallback} to get changes and updates to the surface on which the
@@ -62,11 +63,12 @@
     public void setSurfaceCallback(@Nullable SurfaceCallback surfaceCallback) {
         mHostDispatcher.dispatch(
                 CarContext.APP_SERVICE,
-                (IAppHost host) -> {
-                    host.setSurfaceCallback(RemoteUtils.stubSurfaceCallback(surfaceCallback));
+                "setSurfaceListener", (IAppHost host) -> {
+                    host.setSurfaceCallback(
+                            RemoteUtils.stubSurfaceCallback(mLifecycle, surfaceCallback));
                     return null;
-                },
-                "setSurfaceListener");
+                }
+        );
     }
 
     /**
@@ -78,11 +80,11 @@
     public void invalidate() {
         mHostDispatcher.dispatch(
                 CarContext.APP_SERVICE,
-                (IAppHost host) -> {
+                "invalidate", (IAppHost host) -> {
                     host.invalidate();
                     return null;
-                },
-                "invalidate");
+                }
+        );
     }
 
     /**
@@ -97,11 +99,11 @@
         requireNonNull(text);
         mHostDispatcher.dispatch(
                 CarContext.APP_SERVICE,
-                (IAppHost host) -> {
+                "showToast", (IAppHost host) -> {
                     host.showToast(text, duration);
                     return null;
-                },
-                "showToast");
+                }
+        );
     }
 
     /** Returns the {@code IAppManager.Stub} binder. */
@@ -111,11 +113,12 @@
 
     /** Creates an instance of {@link AppManager}. */
     static AppManager create(@NonNull CarContext carContext,
-            @NonNull HostDispatcher hostDispatcher) {
+            @NonNull HostDispatcher hostDispatcher, @NonNull Lifecycle lifecycle) {
         requireNonNull(carContext);
         requireNonNull(hostDispatcher);
+        requireNonNull(lifecycle);
 
-        return new AppManager(carContext, hostDispatcher);
+        return new AppManager(carContext, hostDispatcher, lifecycle);
     }
 
     // Strictly to avoid synthetic accessor.
@@ -124,38 +127,34 @@
         return mCarContext;
     }
 
+    @NonNull
+    Lifecycle getLifecycle() {
+        return mLifecycle;
+    }
+
     /** @hide */
     @RestrictTo(LIBRARY_GROUP) // Restrict to testing library
-    protected AppManager(@NonNull CarContext carContext, @NonNull HostDispatcher hostDispatcher) {
+    protected AppManager(@NonNull CarContext carContext, @NonNull HostDispatcher hostDispatcher,
+            @NonNull Lifecycle lifecycle) {
         mCarContext = carContext;
         mHostDispatcher = hostDispatcher;
+        mLifecycle = lifecycle;
         mAppManager = new IAppManager.Stub() {
             @Override
             public void getTemplate(IOnDoneCallback callback) {
-                ThreadUtils.runOnMain(
-                        () -> {
-                            TemplateWrapper templateWrapper;
-                            try {
-                                templateWrapper = getCarContext().getCarService(
-                                        ScreenManager.class).getTopTemplate();
-                            } catch (RuntimeException e) {
-                                // Catch exceptions, notify the host of it, then rethrow it.
-                                // This allows the host to log, and show an error to the user.
-                                RemoteUtils.sendFailureResponse(callback,
-                                        "getTemplate", e);
-                                throw new RuntimeException(e);
-                            }
-
-                            RemoteUtils.sendSuccessResponse(callback, "getTemplate",
-                                    templateWrapper);
-                        });
+                RemoteUtils.dispatchCallFromHost(getLifecycle(), callback, "getTemplate",
+                        getCarContext().getCarService(
+                        ScreenManager.class)::getTopTemplate);
             }
 
             @Override
             public void onBackPressed(IOnDoneCallback callback) {
-                RemoteUtils.dispatchHostCall(
-                        carContext.getOnBackPressedDispatcher()::onBackPressed, callback,
-                        "onBackPressed");
+                RemoteUtils.dispatchCallFromHost(getLifecycle(), callback,
+                        "onBackPressed",
+                        () -> {
+                            carContext.getOnBackPressedDispatcher().onBackPressed();
+                            return null;
+                        });
             }
         };
     }
diff --git a/car/app/app/src/main/java/androidx/car/app/CarAppService.java b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
index 84d65e6..5bbb6e0 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarAppService.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
@@ -149,21 +149,17 @@
             Log.d(TAG, "onUnbind intent: " + intent);
         }
         runOnMain(() -> {
-            // Destroy the session
             if (mCurrentSession != null) {
-                CarContext carContext = mCurrentSession.getCarContext();
-
-                // Stop any active navigation
-                carContext.getCarService(NavigationManager.class).onStopNavigation();
-
-                // Destroy all screens in the stack
-                carContext.getCarService(ScreenManager.class).destroyAndClearScreenStack();
-
-                // Remove binders to the host
-                carContext.resetHosts();
-
-                ((LifecycleRegistry) mCurrentSession.getLifecycle()).handleLifecycleEvent(
-                        Event.ON_DESTROY);
+                // Destroy the session
+                // The session's lifecycle is observed by some of the manager and they will
+                // perform cleanup on destroy.  For example, the ScreenManager can destroy all
+                // Screens it holds.
+                LifecycleRegistry lifecycleRegistry = getLifecycleIfValid();
+                if (lifecycleRegistry == null) {
+                    Log.e(TAG, "Null Session when unbinding");
+                } else {
+                    lifecycleRegistry.handleLifecycleEvent(Event.ON_DESTROY);
+                }
             }
             mCurrentSession = null;
         });
@@ -323,6 +319,17 @@
         return mHandshakeInfo;
     }
 
+    @Nullable
+    LifecycleRegistry getLifecycleIfValid() {
+        Session session = getCurrentSession();
+        return session == null ? null : (LifecycleRegistry) session.getLifecycle();
+    }
+
+    @NonNull
+    LifecycleRegistry getLifecycle() {
+        return requireNonNull(getLifecycleIfValid());
+    }
+
     private final ICarApp.Stub mBinder =
             new ICarApp.Stub() {
                 // incompatible argument for parameter context of attachBaseContext.
@@ -340,10 +347,11 @@
                     if (Log.isLoggable(TAG, Log.DEBUG)) {
                         Log.d(TAG, "onAppCreate intent: " + intent);
                     }
-                    RemoteUtils.dispatchHostCall(() -> {
+
+                    RemoteUtils.dispatchCallFromHost(callback, "onAppCreate", () -> {
                         Session session = getCurrentSession();
                         if (session == null
-                                || session.getLifecycle().getCurrentState() == State.DESTROYED) {
+                                || getLifecycle().getCurrentState() == State.DESTROYED) {
                             session = onCreateSession();
                             setCurrentSession(session);
                         }
@@ -354,7 +362,7 @@
                         // Whenever the host unbinds, the screens in the stack are destroyed.  If
                         // there is another bind, before the OS has destroyed this Service, then
                         // the stack will be empty, and we need to treat it as a new instance.
-                        LifecycleRegistry registry = (LifecycleRegistry) session.getLifecycle();
+                        LifecycleRegistry registry = getLifecycle();
                         Lifecycle.State state = registry.getCurrentState();
                         int screenStackSize = session.getCarContext().getCarService(
                                 ScreenManager.class).getScreenStack().size();
@@ -374,7 +382,8 @@
                             }
                             onNewIntentInternal(session, intent);
                         }
-                    }, callback, "onAppCreate");
+                        return null;
+                    });
 
                     if (Log.isLoggable(TAG, Log.DEBUG)) {
                         Log.d(TAG, "onAppCreate completed");
@@ -383,99 +392,108 @@
 
                 @Override
                 public void onAppStart(IOnDoneCallback callback) {
-                    RemoteUtils.dispatchHostCall(
-                            () -> {
-                                ((LifecycleRegistry) throwIfInvalid(
-                                        getCurrentSession()).getLifecycle())
-                                        .handleLifecycleEvent(Event.ON_START);
-                            }, callback,
-                            "onAppStart");
+                    RemoteUtils.dispatchCallFromHost(
+                            getLifecycleIfValid(), callback,
+                            "onAppStart", () -> {
+                                getLifecycle().handleLifecycleEvent(Event.ON_START);
+                                return null;
+                            });
                 }
 
                 @Override
                 public void onAppResume(IOnDoneCallback callback) {
-                    RemoteUtils.dispatchHostCall(
-                            () -> {
-                                ((LifecycleRegistry) throwIfInvalid(
-                                        getCurrentSession()).getLifecycle())
+                    RemoteUtils.dispatchCallFromHost(
+                            getLifecycleIfValid(), callback,
+                            "onAppResume", () -> {
+                                getLifecycle()
                                         .handleLifecycleEvent(Event.ON_RESUME);
-                            }, callback,
-                            "onAppResume");
+                                return null;
+                            });
                 }
 
                 @Override
                 public void onAppPause(IOnDoneCallback callback) {
-                    RemoteUtils.dispatchHostCall(
+                    RemoteUtils.dispatchCallFromHost(
+                            getLifecycleIfValid(), callback, "onAppPause",
                             () -> {
-                                ((LifecycleRegistry) throwIfInvalid(
-                                        getCurrentSession()).getLifecycle())
-                                        .handleLifecycleEvent(Event.ON_PAUSE);
-                            }, callback, "onAppPause");
+                                getLifecycle().handleLifecycleEvent(Event.ON_PAUSE);
+                                return null;
+                            });
                 }
 
                 @Override
                 public void onAppStop(IOnDoneCallback callback) {
-                    RemoteUtils.dispatchHostCall(
+                    RemoteUtils.dispatchCallFromHost(
+                            getLifecycleIfValid(), callback, "onAppStop",
                             () -> {
-                                ((LifecycleRegistry) throwIfInvalid(
-                                        getCurrentSession()).getLifecycle())
-                                        .handleLifecycleEvent(Event.ON_STOP);
-                            }, callback, "onAppStop");
+                                getLifecycle().handleLifecycleEvent(Event.ON_STOP);
+                                return null;
+                            });
                 }
 
                 @Override
                 public void onNewIntent(Intent intent, IOnDoneCallback callback) {
-                    RemoteUtils.dispatchHostCall(
-                            () -> onNewIntentInternal(throwIfInvalid(getCurrentSession()), intent),
+                    RemoteUtils.dispatchCallFromHost(
+                            getLifecycleIfValid(),
                             callback,
-                            "onNewIntent");
+                            "onNewIntent",
+                            () -> {
+                                onNewIntentInternal(throwIfInvalid(getCurrentSession()), intent);
+                                return null;
+                            });
                 }
 
                 @Override
                 public void onConfigurationChanged(Configuration configuration,
                         IOnDoneCallback callback) {
-                    RemoteUtils.dispatchHostCall(
-                            () -> onConfigurationChangedInternal(
-                                    throwIfInvalid(getCurrentSession()), configuration),
+                    RemoteUtils.dispatchCallFromHost(
+                            getLifecycleIfValid(),
                             callback,
-                            "onConfigurationChanged");
+                            "onConfigurationChanged",
+                            () -> {
+                                onConfigurationChangedInternal(
+                                        throwIfInvalid(getCurrentSession()), configuration);
+                                return null;
+                            });
                 }
 
                 @Override
                 public void getManager(@CarServiceType @NonNull String type,
                         IOnDoneCallback callback) {
-                    Session session = throwIfInvalid(getCurrentSession());
-                    switch (type) {
-                        case CarContext.APP_SERVICE:
-                            RemoteUtils.sendSuccessResponse(
-                                    callback,
-                                    "getManager",
-                                    session.getCarContext().getCarService(
-                                            AppManager.class).getIInterface());
-                            return;
-                        case CarContext.NAVIGATION_SERVICE:
-                            RemoteUtils.sendSuccessResponse(
-                                    callback,
-                                    "getManager",
-                                    session.getCarContext().getCarService(
-                                            NavigationManager.class).getIInterface());
-                            return;
-                        default:
-                            Log.e(TAG, type + "%s is not a valid manager");
-                            RemoteUtils.sendFailureResponse(callback, "getManager",
-                                    new InvalidParameterException(
-                                            type + " is not a valid manager type"));
-                    }
+                    ThreadUtils.runOnMain(() -> {
+                        Session session = throwIfInvalid(getCurrentSession());
+                        switch (type) {
+                            case CarContext.APP_SERVICE:
+                                RemoteUtils.sendSuccessResponseToHost(
+                                        callback,
+                                        "getManager",
+                                        session.getCarContext().getCarService(
+                                                AppManager.class).getIInterface());
+                                return;
+                            case CarContext.NAVIGATION_SERVICE:
+                                RemoteUtils.sendSuccessResponseToHost(
+                                        callback,
+                                        "getManager",
+                                        session.getCarContext().getCarService(
+                                                NavigationManager.class).getIInterface());
+                                return;
+                            default:
+                                Log.e(TAG, type + "%s is not a valid manager");
+                                RemoteUtils.sendFailureResponseToHost(callback, "getManager",
+                                        new InvalidParameterException(
+                                                type + " is not a valid manager type"));
+                        }
+                    });
                 }
 
                 @Override
                 public void getAppInfo(IOnDoneCallback callback) {
                     try {
-                        RemoteUtils.sendSuccessResponse(
+                        RemoteUtils.sendSuccessResponseToHost(
                                 callback, "getAppInfo", CarAppService.this.getAppInfo());
                     } catch (IllegalArgumentException e) {
                         // getAppInfo() could fail with the specified API version is invalid.
-                        RemoteUtils.sendFailureResponse(callback, "getAppInfo", e);
+                        RemoteUtils.sendFailureResponseToHost(callback, "getAppInfo", e);
                     }
                 }
 
@@ -489,17 +507,18 @@
                         int uid = Binder.getCallingUid();
                         HostInfo hostInfo = new HostInfo(packageName, uid);
                         if (!getHostValidator().isValidHost(hostInfo)) {
-                            RemoteUtils.sendFailureResponse(callback, "onHandshakeCompleted",
+                            RemoteUtils.sendFailureResponseToHost(callback, "onHandshakeCompleted",
                                     new IllegalArgumentException("Unknown host '"
                                             + packageName + "', uid:" + uid));
                             return;
                         }
                         setHostInfo(hostInfo);
                         setHandshakeInfo(deserializedHandshakeInfo);
-                        RemoteUtils.sendSuccessResponse(callback, "onHandshakeCompleted", null);
+                        RemoteUtils.sendSuccessResponseToHost(callback, "onHandshakeCompleted",
+                                null);
                     } catch (BundlerException | IllegalArgumentException e) {
                         setHostInfo(null);
-                        RemoteUtils.sendFailureResponse(callback, "onHandshakeCompleted", e);
+                        RemoteUtils.sendFailureResponseToHost(callback, "onHandshakeCompleted", e);
                     }
                 }
 
diff --git a/car/app/app/src/main/java/androidx/car/app/CarContext.java b/car/app/app/src/main/java/androidx/car/app/CarContext.java
index f9fb37a..1589314 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarContext.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarContext.java
@@ -46,9 +46,13 @@
 import androidx.car.app.utils.ThreadUtils;
 import androidx.car.app.versioning.CarAppApiLevel;
 import androidx.car.app.versioning.CarAppApiLevels;
+import androidx.lifecycle.DefaultLifecycleObserver;
 import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
 import androidx.lifecycle.LifecycleOwner;
 
+import org.jetbrains.annotations.NotNull;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.security.InvalidParameterException;
@@ -256,11 +260,11 @@
 
         mHostDispatcher.dispatch(
                 CarContext.CAR_SERVICE,
-                (ICarHost host) -> {
+                "startCarApp", (ICarHost host) -> {
                     host.startCarApp(intent);
                     return null;
-                },
-                "startCarApp");
+                }
+        );
     }
 
     /**
@@ -294,12 +298,12 @@
 
         IStartCarApp startCarAppInterface = requireNonNull(IStartCarApp.Stub.asInterface(binder));
 
-        RemoteUtils.call(
-                () -> {
+        RemoteUtils.dispatchCallToHost(
+                "startCarApp from notification", () -> {
                     startCarAppInterface.startCarApp(appIntent);
                     return null;
-                },
-                "startCarApp from notification");
+                }
+        );
     }
 
     /**
@@ -313,11 +317,11 @@
     public void finishCarApp() {
         mHostDispatcher.dispatch(
                 CarContext.CAR_SERVICE,
-                (ICarHost host) -> {
+                "finish", (ICarHost host) -> {
                     host.finish();
                     return null;
-                },
-                "finish");
+                }
+        );
     }
 
     /**
@@ -440,14 +444,6 @@
         mHostDispatcher.setCarHost(requireNonNull(carHost));
     }
 
-    /** @hide */
-    @RestrictTo(LIBRARY_GROUP) // Restrict to testing library
-    @MainThread
-    void resetHosts() {
-        ThreadUtils.checkMainThread();
-        mHostDispatcher.resetHosts();
-    }
-
     /**
      * Retrieves the API level negotiated with the host.
      *
@@ -491,10 +487,20 @@
         super(null);
 
         mHostDispatcher = hostDispatcher;
-        mAppManager = AppManager.create(this, hostDispatcher);
-        mNavigationManager = NavigationManager.create(this, hostDispatcher);
+        mAppManager = AppManager.create(this, hostDispatcher, lifecycle);
+        mNavigationManager = NavigationManager.create(this, hostDispatcher, lifecycle);
         mScreenManager = ScreenManager.create(this, lifecycle);
         mOnBackPressedDispatcher =
                 new OnBackPressedDispatcher(() -> getCarService(ScreenManager.class).pop());
+
+        LifecycleObserver observer = new DefaultLifecycleObserver() {
+            @Override
+            public void onDestroy(@NonNull @NotNull LifecycleOwner owner) {
+                hostDispatcher.resetHosts();
+                owner.getLifecycle().removeObserver(this);
+            }
+        };
+
+        lifecycle.addObserver(observer);
     }
 }
diff --git a/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java b/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java
index ff206e3..dc21714 100644
--- a/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java
+++ b/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java
@@ -21,8 +21,9 @@
 
 import static java.util.Objects.requireNonNull;
 
-import android.annotation.SuppressLint;
 import android.os.IInterface;
+import android.os.RemoteException;
+import android.util.Log;
 
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
@@ -30,6 +31,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.car.app.CarContext.CarServiceType;
 import androidx.car.app.navigation.INavigationHost;
+import androidx.car.app.utils.LogTags;
 import androidx.car.app.utils.RemoteUtils;
 import androidx.car.app.utils.ThreadUtils;
 
@@ -53,19 +55,55 @@
      * Dispatches the {@code call} to the host for the given {@code hostType}.
      *
      * @param hostType the service to dispatch to
-     * @param call     the request to dispatch
      * @param callName the name of the call for logging purposes
+     * @param call     the request to dispatch
+     *
+     * @throws RemoteException   if the host is unresponsive
      * @throws SecurityException if the host has thrown it
      * @throws HostException     if the host throws any exception other than
      *                           {@link SecurityException}
      */
     @Nullable
     @SuppressWarnings({"unchecked", "cast.unsafe"}) // Cannot check if instanceof ServiceT
-    @SuppressLint("LambdaLast")
-    public <ServiceT, ReturnT> ReturnT dispatch(
-            @CarServiceType @NonNull String hostType, @NonNull HostCall<ServiceT, ReturnT> call,
-            @NonNull String callName) {
-        return RemoteUtils.call(() -> call.dispatch((ServiceT) getHost(hostType)), callName);
+    public <ServiceT, ReturnT> ReturnT dispatchForResult(
+            @CarServiceType @NonNull String hostType, @NonNull String callName,
+            @NonNull HostCall<ServiceT, ReturnT> call) throws RemoteException {
+        return RemoteUtils.dispatchCallToHostForResult(callName, () -> {
+            IInterface service = getHost(hostType);
+            if (service == null) {
+                Log.e(LogTags.TAG_DISPATCH,
+                        "Could not retrieve host while dispatching call " + callName);
+                return null;
+            }
+            return call.dispatch((ServiceT) service);
+        });
+    }
+
+    /**
+     * Dispatches the {@code call} to the host for the given {@code hostType}.
+     *
+     * @param hostType the service to dispatch to
+     * @param callName the name of the call for logging purposes
+     * @param call     the request to dispatch
+     *
+     * @throws SecurityException if the host has thrown it
+     * @throws HostException     if the host throws any exception other than
+     *                           {@link SecurityException}
+     */
+    @SuppressWarnings({"unchecked", "cast.unsafe"}) // Cannot check if instanceof ServiceT
+    public <ServiceT, ReturnT> void dispatch(
+            @CarServiceType @NonNull String hostType, @NonNull String callName,
+            @NonNull HostCall<ServiceT, ReturnT> call) {
+        RemoteUtils.dispatchCallToHost(callName, () -> {
+            IInterface service = getHost(hostType);
+            if (service == null) {
+                Log.e(LogTags.TAG_DISPATCH,
+                        "Could not retrieve host while dispatching call " + callName);
+                return null;
+            }
+            call.dispatch((ServiceT) service);
+            return null;
+        });
     }
 
     @MainThread
@@ -89,12 +127,16 @@
     /**
      * Retrieves the {@link IInterface} for the given {@code hostType}.
      *
+     * @throws RemoteException if the host is unresponsive
      * @hide
      */
     @RestrictTo(LIBRARY)
-    IInterface getHost(@CarServiceType String hostType) {
+    @Nullable
+    IInterface getHost(@CarServiceType String hostType) throws RemoteException {
         if (mCarHost == null) {
-            throw new HostException("Host is not bound when attempting to retrieve host service");
+            Log.e(LogTags.TAG_DISPATCH, "Host is not bound when attempting to retrieve host "
+                    + "service");
+            return null;
         }
 
         IInterface host = null;
@@ -102,21 +144,21 @@
             case CarContext.APP_SERVICE:
                 if (mAppHost == null) {
                     mAppHost =
-                            RemoteUtils.call(() ->
+                            RemoteUtils.dispatchCallToHostForResult("getHost(App)", () ->
                                     IAppHost.Stub.asInterface(requireNonNull(mCarHost).getHost(
-                                            CarContext.APP_SERVICE)), "getHost(App)");
+                                            CarContext.APP_SERVICE)));
                 }
                 host = mAppHost;
                 break;
             case CarContext.NAVIGATION_SERVICE:
                 if (mNavigationHost == null) {
                     mNavigationHost =
-                            RemoteUtils.call(
-                                    () ->
+                            RemoteUtils.dispatchCallToHostForResult(
+                                    "getHost(Navigation)", () ->
                                             INavigationHost.Stub.asInterface(
                                                     requireNonNull(mCarHost).getHost(
-                                                            CarContext.NAVIGATION_SERVICE)),
-                                    "getHost(Navigation)");
+                                                            CarContext.NAVIGATION_SERVICE))
+                            );
                 }
                 host = mNavigationHost;
                 break;
@@ -126,6 +168,6 @@
             default:
                 throw new InvalidParameterException("Invalid host type: " + hostType);
         }
-        return requireNonNull(host);
+        return host;
     }
 }
diff --git a/car/app/app/src/main/java/androidx/car/app/ScreenManager.java b/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
index 38d3707..d274a66 100644
--- a/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
@@ -406,6 +406,7 @@
         @Override
         public void onDestroy(@NonNull LifecycleOwner lifecycleOwner) {
             destroyAndClearScreenStack();
+            lifecycleOwner.getLifecycle().removeObserver(this);
         }
     }
 }
diff --git a/car/app/app/src/main/java/androidx/car/app/model/OnCheckedChangeDelegateImpl.java b/car/app/app/src/main/java/androidx/car/app/model/OnCheckedChangeDelegateImpl.java
index 9b4cf8c..202251e 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/OnCheckedChangeDelegateImpl.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnCheckedChangeDelegateImpl.java
@@ -80,9 +80,11 @@
 
         @Override
         public void onCheckedChange(boolean isChecked, IOnDoneCallback callback) {
-            RemoteUtils.dispatchHostCall(
-                    () -> mListener.onCheckedChange(isChecked), callback,
-                    "onCheckedChange");
+            RemoteUtils.dispatchCallFromHost(callback, "onCheckedChange", () -> {
+                        mListener.onCheckedChange(isChecked);
+                        return null;
+                    }
+            );
         }
     }
 }
diff --git a/car/app/app/src/main/java/androidx/car/app/model/OnClickDelegateImpl.java b/car/app/app/src/main/java/androidx/car/app/model/OnClickDelegateImpl.java
index 79612f6..a786696 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/OnClickDelegateImpl.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnClickDelegateImpl.java
@@ -93,7 +93,10 @@
 
         @Override
         public void onClick(IOnDoneCallback callback) {
-            RemoteUtils.dispatchHostCall(mOnClickListener::onClick, callback, "onClick");
+            RemoteUtils.dispatchCallFromHost(callback, "onClick", () -> {
+                mOnClickListener.onClick();
+                return null;
+            });
         }
     }
 }
diff --git a/car/app/app/src/main/java/androidx/car/app/model/OnItemVisibilityChangedDelegateImpl.java b/car/app/app/src/main/java/androidx/car/app/model/OnItemVisibilityChangedDelegateImpl.java
index b6b515e..7e7bdcdb 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/OnItemVisibilityChangedDelegateImpl.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnItemVisibilityChangedDelegateImpl.java
@@ -88,11 +88,13 @@
         @Override
         public void onItemVisibilityChanged(
                 int startIndexInclusive, int endIndexExclusive, IOnDoneCallback callback) {
-            RemoteUtils.dispatchHostCall(
-                    () -> mListener.onItemVisibilityChanged(
-                            startIndexInclusive, endIndexExclusive),
-                    callback,
-                    "onItemVisibilityChanged");
+            RemoteUtils.dispatchCallFromHost(
+                    callback, "onItemVisibilityChanged", () -> {
+                        mListener.onItemVisibilityChanged(
+                                startIndexInclusive, endIndexExclusive);
+                        return null;
+                    }
+            );
         }
     }
 }
diff --git a/car/app/app/src/main/java/androidx/car/app/model/OnSelectedDelegateImpl.java b/car/app/app/src/main/java/androidx/car/app/model/OnSelectedDelegateImpl.java
index 5297c63..f1e7414 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/OnSelectedDelegateImpl.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnSelectedDelegateImpl.java
@@ -80,8 +80,11 @@
 
         @Override
         public void onSelected(int index, IOnDoneCallback callback) {
-            RemoteUtils.dispatchHostCall(
-                    () -> mListener.onSelected(index), callback, "onSelectedListener");
+            RemoteUtils.dispatchCallFromHost(
+                    callback, "onSelectedListener", () -> {
+                        mListener.onSelected(index);
+                        return null;
+                    });
         }
     }
 }
diff --git a/car/app/app/src/main/java/androidx/car/app/model/SearchCallbackDelegateImpl.java b/car/app/app/src/main/java/androidx/car/app/model/SearchCallbackDelegateImpl.java
index 0db19ad..abfbf5d 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/SearchCallbackDelegateImpl.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/SearchCallbackDelegateImpl.java
@@ -92,15 +92,21 @@
 
         @Override
         public void onSearchTextChanged(String text, IOnDoneCallback callback) {
-            RemoteUtils.dispatchHostCall(
-                    () -> mCallback.onSearchTextChanged(text), callback,
-                    "onSearchTextChanged");
+            RemoteUtils.dispatchCallFromHost(
+                    callback, "onSearchTextChanged", () -> {
+                        mCallback.onSearchTextChanged(text);
+                        return null;
+                    }
+            );
         }
 
         @Override
         public void onSearchSubmitted(String text, IOnDoneCallback callback) {
-            RemoteUtils.dispatchHostCall(
-                    () -> mCallback.onSearchSubmitted(text), callback, "onSearchSubmitted");
+            RemoteUtils.dispatchCallFromHost(
+                    callback, "onSearchSubmitted", () -> {
+                        mCallback.onSearchSubmitted(text);
+                        return null;
+                    });
         }
     }
 }
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
index 6f22d72..c181f0d 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
@@ -41,6 +41,10 @@
 import androidx.car.app.serialization.BundlerException;
 import androidx.car.app.utils.RemoteUtils;
 import androidx.core.content.ContextCompat;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
 
 import java.util.concurrent.Executor;
 
@@ -117,11 +121,11 @@
 
         mHostDispatcher.dispatch(
                 CarContext.NAVIGATION_SERVICE,
-                (INavigationHost service) -> {
+                "updateTrip", (INavigationHost service) -> {
                     service.updateTrip(bundle);
                     return null;
-                },
-                "updateTrip");
+                }
+        );
     }
 
     /**
@@ -207,11 +211,11 @@
         mIsNavigating = true;
         mHostDispatcher.dispatch(
                 CarContext.NAVIGATION_SERVICE,
-                (INavigationHost service) -> {
+                "navigationStarted", (INavigationHost service) -> {
                     service.navigationStarted();
                     return null;
-                },
-                "navigationStarted");
+                }
+        );
     }
 
     /**
@@ -234,11 +238,11 @@
         mIsNavigating = false;
         mHostDispatcher.dispatch(
                 CarContext.NAVIGATION_SERVICE,
-                (INavigationHost service) -> {
+                "navigationEnded", (INavigationHost service) -> {
                     service.navigationEnded();
                     return null;
-                },
-                "navigationEnded");
+                }
+        );
     }
 
     /**
@@ -249,8 +253,12 @@
     @RestrictTo(LIBRARY)
     @NonNull
     public static NavigationManager create(@NonNull CarContext carContext,
-            @NonNull HostDispatcher hostDispatcher) {
-        return new NavigationManager(carContext, hostDispatcher);
+            @NonNull HostDispatcher hostDispatcher, @NonNull Lifecycle lifecycle) {
+        requireNonNull(carContext);
+        requireNonNull(hostDispatcher);
+        requireNonNull(lifecycle);
+
+        return new NavigationManager(carContext, hostDispatcher, lifecycle);
     }
 
     /**
@@ -277,9 +285,14 @@
             return;
         }
         mIsNavigating = false;
-        requireNonNull(mNavigationManagerCallbackExecutor).execute(() -> {
-            requireNonNull(mNavigationManagerCallback).onStopNavigation();
-        });
+
+        NavigationManagerCallback callback = mNavigationManagerCallback;
+        Executor executor = mNavigationManagerCallbackExecutor;
+        if (callback == null || executor == null) {
+            return;
+        }
+
+        executor.execute(callback::onStopNavigation);
     }
 
     /**
@@ -300,32 +313,45 @@
         }
 
         mIsAutoDriveEnabled = true;
+
         NavigationManagerCallback callback = mNavigationManagerCallback;
-        if (callback != null) {
-            requireNonNull(mNavigationManagerCallbackExecutor).execute(() -> {
-                callback.onAutoDriveEnabled();
-            });
-        } else {
+        Executor executor = mNavigationManagerCallbackExecutor;
+        if (callback == null || executor == null) {
             Log.w(TAG_NAVIGATION_MANAGER,
                     "NavigationManagerCallback not set, skipping onAutoDriveEnabled");
+            return;
         }
+
+        executor.execute(callback::onAutoDriveEnabled);
     }
 
     /** @hide */
     @RestrictTo(LIBRARY_GROUP) // Restrict to testing library
     @SuppressWarnings({"methodref.receiver.bound.invalid"})
     protected NavigationManager(@NonNull CarContext carContext,
-            @NonNull HostDispatcher hostDispatcher) {
-        mCarContext = carContext;
+            @NonNull HostDispatcher hostDispatcher, @NonNull Lifecycle lifecycle) {
+        mCarContext = requireNonNull(carContext);
         mHostDispatcher = requireNonNull(hostDispatcher);
         mNavigationManager =
                 new INavigationManager.Stub() {
                     @Override
                     public void onStopNavigation(IOnDoneCallback callback) {
-                        RemoteUtils.dispatchHostCall(
-                                NavigationManager.this::onStopNavigation, callback,
-                                "onStopNavigation");
+                        RemoteUtils.dispatchCallFromHost(
+                                lifecycle, callback,
+                                "onStopNavigation",
+                                () -> {
+                                    NavigationManager.this.onStopNavigation();
+                                    return null;
+                                });
                     }
                 };
+        LifecycleObserver observer = new DefaultLifecycleObserver() {
+            @Override
+            public void onDestroy(@NonNull LifecycleOwner lifecycleOwner) {
+                NavigationManager.this.onStopNavigation();
+                lifecycle.removeObserver(this);
+            }
+        };
+        lifecycle.addObserver(observer);
     }
 }
diff --git a/car/app/app/src/main/java/androidx/car/app/utils/LogTags.java b/car/app/app/src/main/java/androidx/car/app/utils/LogTags.java
index cb162f5..e831f1d 100644
--- a/car/app/app/src/main/java/androidx/car/app/utils/LogTags.java
+++ b/car/app/app/src/main/java/androidx/car/app/utils/LogTags.java
@@ -30,7 +30,10 @@
     /** Tag to use for logging in the library. */
     public static final String TAG = "CarApp";
 
-    /** Tag to use for host validation. */
+    /** Tag to use for IPC dispatching */
+    public static final String TAG_DISPATCH = TAG + ".Dispatch";
+
+    /** Tag to use for host validation */
     public static final String TAG_HOST_VALIDATION = TAG + ".Val";
 
     /** Tag to use for navigation manager. */
diff --git a/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java b/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java
index 2567d7c..9b6ce95 100644
--- a/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java
+++ b/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java
@@ -18,8 +18,8 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
 import static androidx.car.app.utils.LogTags.TAG;
+import static androidx.lifecycle.Lifecycle.State.CREATED;
 
-import android.annotation.SuppressLint;
 import android.graphics.Rect;
 import android.os.RemoteException;
 import android.util.Log;
@@ -36,6 +36,7 @@
 import androidx.car.app.SurfaceContainer;
 import androidx.car.app.serialization.Bundleable;
 import androidx.car.app.serialization.BundlerException;
+import androidx.lifecycle.Lifecycle;
 
 /**
  * Assorted utilities to deal with serialization of remote calls.
@@ -56,19 +57,28 @@
      * success/failure.
      */
     public interface HostCall {
-        void dispatch() throws BundlerException;
+        /**
+         * Dispatches the call and returns its outcome if any.
+         *
+         * @return the response from the app for the host call, or {@code null} if there is
+         * nothing to return
+         */
+        @Nullable
+        Object dispatch() throws BundlerException;
     }
 
     /**
-     * Performs the remote call and handles exceptions thrown by the host.
+     * Performs the remote call to the host and handles exceptions thrown by the host.
      *
+     * @return the value that the host returns for the IPC
+     *
+     * @throws RemoteException   if the host is unresponsive
      * @throws SecurityException as a pass through from the host
      * @throws HostException     if the remote call fails with any other exception
      */
-    @SuppressLint("LambdaLast")
     @Nullable
-    public static <ReturnT> ReturnT call(@NonNull RemoteCall<ReturnT> remoteCall,
-            @NonNull String callName) {
+    public static <ReturnT> ReturnT dispatchCallToHostForResult(@NonNull String callName,
+            @NonNull RemoteCall<ReturnT> remoteCall) throws RemoteException {
         try {
             if (Log.isLoggable(TAG, Log.DEBUG)) {
                 Log.d(TAG, "Dispatching call " + callName + " to host");
@@ -78,23 +88,44 @@
             // SecurityException is treated specially where we allow it to flow through since
             // this is specific to not having permissions to perform an API.
             throw e;
-        } catch (RemoteException | RuntimeException e) {
+        } catch (RuntimeException e) {
             throw new HostException("Remote " + callName + " call failed", e);
         }
     }
 
     /**
+     * Performs the remote call to the host and handles exceptions thrown by the host.
+     *
+     * @throws SecurityException as a pass through from the host
+     * @throws HostException     if the remote call fails with any other exception
+     */
+    public static void dispatchCallToHost(@NonNull String callName,
+            @NonNull RemoteCall<?> remoteCall) {
+        try {
+            dispatchCallToHostForResult(callName, remoteCall);
+        } catch (RemoteException e) {
+            // The host is dead, don't crash the app, just log.
+            Log.e(LogTags.TAG_DISPATCH, "Host unresponsive when dispatching call " + callName, e);
+        }
+    }
+
+    /**
      * Returns an {@link ISurfaceCallback} stub that invokes the input {@link SurfaceCallback}
      * if it is not {@code null}, or {@code null} if the input {@link SurfaceCallback} is {@code
      * null}
+     *
+     * @param lifecycle       the lifecycle of the session to be used to not dispatch calls out of
+     *                        lifecycle.
+     * @param surfaceCallback the callback to wrap in an {@link ISurfaceCallback}
      */
     @Nullable
-    public static ISurfaceCallback stubSurfaceCallback(@Nullable SurfaceCallback surfaceCallback) {
+    public static ISurfaceCallback stubSurfaceCallback(@NonNull Lifecycle lifecycle,
+            @Nullable SurfaceCallback surfaceCallback) {
         if (surfaceCallback == null) {
             return null;
         }
 
-        return new SurfaceCallbackStub(surfaceCallback);
+        return new SurfaceCallbackStub(lifecycle, surfaceCallback);
     }
 
     /**
@@ -107,22 +138,51 @@
      * <p>If the app throws an exception, will call {@link IOnDoneCallback#onFailure} with a {@link
      * FailureResponse} including information from the caught exception.
      */
-    @SuppressLint("LambdaLast")
-    public static void dispatchHostCall(
-            @NonNull HostCall hostCall, @NonNull IOnDoneCallback callback,
-            @NonNull String callName) {
+    public static void dispatchCallFromHost(
+            @NonNull IOnDoneCallback callback, @NonNull String callName,
+            @NonNull HostCall hostCall) {
+        // TODO(b/180530156): Move callers that should be lifecycle aware once we can put a
+        //  lifecycle into a Template and propagate it to the models.
         ThreadUtils.runOnMain(
                 () -> {
                     try {
-                        hostCall.dispatch();
-                    } catch (BundlerException e) {
-                        sendFailureResponse(callback, callName, e);
-                        throw new HostException("Serialization failure in " + callName, e);
+                        sendSuccessResponseToHost(callback, callName, hostCall.dispatch());
                     } catch (RuntimeException e) {
-                        sendFailureResponse(callback, callName, e);
+                        // Catch exceptions, notify the host of it, then rethrow it.
+                        // This allows the host to log, and show an error to the user.
+                        sendFailureResponseToHost(callback, callName, e);
                         throw new RuntimeException(e);
+                    } catch (BundlerException e) {
+                        sendFailureResponseToHost(callback, callName, e);
                     }
-                    sendSuccessResponse(callback, callName, null);
+                });
+    }
+
+    /**
+     * Dispatches the given {@link HostCall} to the client in the main thread, but only if the
+     * provided {@link Lifecycle} has a state of at least created, and notifies the host of outcome.
+     *
+     * <p>If the app processes the response, will call {@link IOnDoneCallback#onSuccess} with a
+     * {@code null}.
+     *
+     * <p>If the app throws an exception, will call {@link IOnDoneCallback#onFailure} with a {@link
+     * FailureResponse} including information from the caught exception.
+     *
+     * <p>If the {@code lifecycle} provided is {@code null} or not at least created, will call
+     * {@link IOnDoneCallback#onFailure} with a {@link FailureResponse}.
+     */
+    public static void dispatchCallFromHost(
+            @Nullable Lifecycle lifecycle, @NonNull IOnDoneCallback callback,
+            @NonNull String callName, @NonNull HostCall hostCall) {
+        ThreadUtils.runOnMain(
+                () -> {
+                    if (lifecycle == null || !lifecycle.getCurrentState().isAtLeast(CREATED)) {
+                        sendFailureResponseToHost(callback, callName, new IllegalStateException(
+                                "Lifecycle is not at least created when dispatching " + hostCall));
+                        return;
+                    }
+
+                    dispatchCallFromHost(callback, callName, hostCall);
                 });
     }
 
@@ -131,36 +191,35 @@
      */
     // TODO(b/178748627): the nullable annotation from the AIDL file is not being considered.
     @SuppressWarnings("NullAway")
-    public static void sendSuccessResponse(
+    public static void sendSuccessResponseToHost(
             @NonNull IOnDoneCallback callback, @NonNull String callName,
             @Nullable Object response) {
-        call(() -> {
+        dispatchCallToHost(callName + " onSuccess", () -> {
             try {
                 callback.onSuccess(response == null ? null : Bundleable.create(response));
             } catch (BundlerException e) {
-                sendFailureResponse(callback, callName, e);
-                throw new IllegalStateException("Serialization failure in " + callName, e);
+                sendFailureResponseToHost(callback, callName, e);
             }
             return null;
-        }, callName + " onSuccess");
+        });
     }
 
     /**
      * Invoke onFailure on the given {@code callback} instance with the given {@link Throwable}.
      */
-    public static void sendFailureResponse(@NonNull IOnDoneCallback callback,
+    public static void sendFailureResponseToHost(@NonNull IOnDoneCallback callback,
             @NonNull String callName,
             @NonNull Throwable e) {
-        call(() -> {
+        dispatchCallToHost(callName + " onFailure", () -> {
             try {
                 callback.onFailure(Bundleable.create(new FailureResponse(e)));
             } catch (BundlerException bundlerException) {
                 // Not possible, but catching since BundlerException is not runtime.
-                throw new IllegalStateException(
+                Log.e(LogTags.TAG_DISPATCH,
                         "Serialization failure in " + callName, bundlerException);
             }
             return null;
-        }, callName + " onFailure");
+        });
     }
 
     /**
@@ -183,43 +242,60 @@
     }
 
     private static class SurfaceCallbackStub extends ISurfaceCallback.Stub {
+        private final Lifecycle mLifecycle;
         private final SurfaceCallback mSurfaceCallback;
 
-        SurfaceCallbackStub(SurfaceCallback surfaceCallback) {
+        SurfaceCallbackStub(Lifecycle lifecycle, SurfaceCallback surfaceCallback) {
+            mLifecycle = lifecycle;
             mSurfaceCallback = surfaceCallback;
         }
 
         @Override
         public void onSurfaceAvailable(Bundleable surfaceContainer, IOnDoneCallback callback) {
-            dispatchHostCall(
-                    () -> mSurfaceCallback.onSurfaceAvailable(
-                            (SurfaceContainer) surfaceContainer.get()),
+            dispatchCallFromHost(
+                    mLifecycle,
                     callback,
-                    "onSurfaceAvailable");
+                    "onSurfaceAvailable",
+                    () -> {
+                        mSurfaceCallback.onSurfaceAvailable(
+                                (SurfaceContainer) surfaceContainer.get());
+                        return null;
+                    });
         }
 
         @Override
         public void onVisibleAreaChanged(Rect visibleArea, IOnDoneCallback callback) {
-            dispatchHostCall(
-                    () -> mSurfaceCallback.onVisibleAreaChanged(visibleArea),
+            dispatchCallFromHost(
+                    mLifecycle,
                     callback,
-                    "onVisibleAreaChanged");
+                    "onVisibleAreaChanged",
+                    () -> {
+                        mSurfaceCallback.onVisibleAreaChanged(visibleArea);
+                        return null;
+                    });
         }
 
         @Override
         public void onStableAreaChanged(Rect stableArea, IOnDoneCallback callback) {
-            dispatchHostCall(
-                    () -> mSurfaceCallback.onStableAreaChanged(stableArea), callback,
-                    "onStableAreaChanged");
+            dispatchCallFromHost(
+                    mLifecycle, callback,
+                    "onStableAreaChanged", () -> {
+                        mSurfaceCallback.onStableAreaChanged(stableArea);
+                        return null;
+                    });
         }
 
         @Override
         public void onSurfaceDestroyed(Bundleable surfaceContainer, IOnDoneCallback callback) {
-            dispatchHostCall(
-                    () -> mSurfaceCallback.onSurfaceDestroyed(
-                            (SurfaceContainer) surfaceContainer.get()),
+            dispatchCallFromHost(
+                    mLifecycle,
                     callback,
-                    "onSurfaceDestroyed");
+                    "onSurfaceDestroyed",
+                    () -> {
+                        mSurfaceCallback.onSurfaceDestroyed(
+                                (SurfaceContainer) surfaceContainer.get());
+                        return null;
+                    });
         }
     }
 
diff --git a/car/app/app/src/test/java/androidx/car/app/AppManagerTest.java b/car/app/app/src/test/java/androidx/car/app/AppManagerTest.java
index 941c03e..5b3d169 100644
--- a/car/app/app/src/test/java/androidx/car/app/AppManagerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/AppManagerTest.java
@@ -21,20 +21,28 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.graphics.Rect;
 import android.os.RemoteException;
 
+import androidx.activity.OnBackPressedCallback;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.car.app.model.Template;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
 import androidx.car.app.testing.TestCarContext;
+import androidx.lifecycle.Lifecycle;
 import androidx.test.core.app.ApplicationProvider;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.robolectric.RobolectricTestRunner;
@@ -50,6 +58,13 @@
     private IAppHost.Stub mMockAppHost;
     @Mock
     private IOnDoneCallback mMockOnDoneCallback;
+    @Mock
+    private OnBackPressedCallback mMockOnBackPressedCallback;
+    @Mock
+    private SurfaceCallback mSurfaceCallback;
+
+    @Captor
+    private ArgumentCaptor<ISurfaceCallback> mSurfaceCallbackCaptor;
 
     private TestCarContext mTestCarContext;
     private final HostDispatcher mHostDispatcher = new HostDispatcher();
@@ -86,30 +101,90 @@
 
         mHostDispatcher.setCarHost(mMockCarHost);
 
-        mAppManager = AppManager.create(mTestCarContext, mHostDispatcher);
+        mAppManager = AppManager.create(mTestCarContext, mHostDispatcher,
+                mTestCarContext.getLifecycleOwner().mRegistry);
     }
 
     @Test
-    public void getTemplate_serializationFails_throwsIllegalStateException()
-            throws RemoteException {
+    public void getTemplate_lifecycleCreated_sendsToApp() throws RemoteException {
+        mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
         mTestCarContext
                 .getCarService(ScreenManager.class)
                 .push(new Screen(mTestCarContext) {
                     @NonNull
                     @Override
                     public Template onGetTemplate() {
-                        return new Template() {
+                        return new TestTemplate();
+                    }
+                });
+
+        mAppManager.getIInterface().getTemplate(mMockOnDoneCallback);
+
+        verify(mMockOnDoneCallback).onSuccess(any());
+    }
+
+    @Test
+    public void getTemplate_lifecycleNotCreated_doesNotSendToApp() throws RemoteException {
+        mTestCarContext
+                .getCarService(ScreenManager.class)
+                .push(new Screen(mTestCarContext) {
+                    @NonNull
+                    @Override
+                    public Template onGetTemplate() {
+                        return new TestTemplate() {
                         };
                     }
                 });
 
-        assertThrows(
-                HostException.class,
-                () -> mAppManager.getIInterface().getTemplate(mMockOnDoneCallback));
+        mAppManager.getIInterface().getTemplate(mMockOnDoneCallback);
+
         verify(mMockOnDoneCallback).onFailure(any());
     }
 
     @Test
+    public void getTemplate_serializationFails_sendsFailureToHost()
+            throws RemoteException {
+        mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
+        mTestCarContext
+                .getCarService(ScreenManager.class)
+                .push(new Screen(mTestCarContext) {
+                    @NonNull
+                    @Override
+                    public Template onGetTemplate() {
+                        return new NonBundleableTemplate("foo") {
+                        };
+                    }
+                });
+
+        mAppManager.getIInterface().getTemplate(mMockOnDoneCallback);
+
+        verify(mMockOnDoneCallback).onFailure(any());
+    }
+
+    @Test
+    public void onBackPressed_lifecycleCreated_sendsToApp() throws RemoteException {
+        mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
+        when(mMockOnBackPressedCallback.isEnabled()).thenReturn(true);
+        mTestCarContext.getOnBackPressedDispatcher().addCallback(mMockOnBackPressedCallback);
+
+        mAppManager.getIInterface().onBackPressed(mMockOnDoneCallback);
+
+        verify(mMockOnDoneCallback).onSuccess(any());
+        verify(mMockOnBackPressedCallback).handleOnBackPressed();
+    }
+
+    @Test
+    public void onBackPressed_lifecycleNotCreated_doesNotSendToApp() throws RemoteException {
+        when(mMockOnBackPressedCallback.isEnabled()).thenReturn(true);
+        mTestCarContext.getOnBackPressedDispatcher().addCallback(mMockOnBackPressedCallback);
+
+        mAppManager.getIInterface().onBackPressed(mMockOnDoneCallback);
+
+        verify(mMockOnDoneCallback).onFailure(any());
+        verify(mMockOnBackPressedCallback, never()).handleOnBackPressed();
+    }
+
+    @Test
     public void invalidate_forwardsRequestToHost() throws RemoteException {
         mAppManager.invalidate();
 
@@ -117,11 +192,11 @@
     }
 
     @Test
-    public void invalidate_hostThrowsRemoteException_throwsHostException() throws
+    public void invalidate_hostThrowsRemoteException_doesNotThrow() throws
             RemoteException {
         doThrow(new RemoteException()).when(mMockAppHost).invalidate();
 
-        assertThrows(HostException.class, () -> mAppManager.invalidate());
+        mAppManager.invalidate();
     }
 
     @Test
@@ -142,10 +217,10 @@
     }
 
     @Test
-    public void showToast_hostThrowsRemoteException_throwsHostException() throws RemoteException {
+    public void showToast_hostThrowsRemoteException_doesNotThrow() throws RemoteException {
         doThrow(new RemoteException()).when(mMockAppHost).showToast(anyString(), anyInt());
 
-        assertThrows(HostException.class, () -> mAppManager.showToast("foo", 1));
+        mAppManager.showToast("foo", 1);
     }
 
     @Test
@@ -172,11 +247,11 @@
     }
 
     @Test
-    public void etSurfaceListener_hostThrowsRemoteException_throwsHostException()
+    public void etSurfaceListener_hostThrowsRemoteException_doesNotThrow()
             throws RemoteException {
         doThrow(new RemoteException()).when(mMockAppHost).setSurfaceCallback(any());
 
-        assertThrows(HostException.class, () -> mAppManager.setSurfaceCallback(null));
+        mAppManager.setSurfaceCallback(null);
     }
 
     @Test
@@ -186,4 +261,124 @@
 
         assertThrows(HostException.class, () -> mAppManager.setSurfaceCallback(null));
     }
+
+    @Test
+    public void onSurfaceAvailable_dispatches()
+            throws RemoteException, BundlerException {
+        mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
+        mAppManager.setSurfaceCallback(mSurfaceCallback);
+        verify(mMockAppHost).setSurfaceCallback(mSurfaceCallbackCaptor.capture());
+        SurfaceContainer surfaceContainer = new SurfaceContainer(null, 1, 2, 3);
+
+        mSurfaceCallbackCaptor.getValue().onSurfaceAvailable(Bundleable.create(surfaceContainer),
+                mMockOnDoneCallback);
+
+        verify(mSurfaceCallback).onSurfaceAvailable(any());
+        verify(mMockOnDoneCallback).onSuccess(any());
+    }
+
+    @Test
+    public void onSurfaceAvailable_lifecycleNotCreated_doesNotDispatch_sendsAFailure()
+            throws RemoteException, BundlerException {
+        mAppManager.setSurfaceCallback(mSurfaceCallback);
+        verify(mMockAppHost).setSurfaceCallback(mSurfaceCallbackCaptor.capture());
+        SurfaceContainer surfaceContainer = new SurfaceContainer(null, 1, 2, 3);
+
+        mSurfaceCallbackCaptor.getValue().onSurfaceAvailable(Bundleable.create(surfaceContainer),
+                mMockOnDoneCallback);
+
+        verify(mSurfaceCallback, never()).onSurfaceAvailable(surfaceContainer);
+        verify(mMockOnDoneCallback).onFailure(any());
+    }
+
+    @Test
+    public void onVisibleAreaChanged_dispatches()
+            throws RemoteException {
+        mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
+        mAppManager.setSurfaceCallback(mSurfaceCallback);
+        verify(mMockAppHost).setSurfaceCallback(mSurfaceCallbackCaptor.capture());
+        Rect rect = new Rect(0, 0, 1, 1);
+
+        mSurfaceCallbackCaptor.getValue().onVisibleAreaChanged(rect, mMockOnDoneCallback);
+
+        verify(mSurfaceCallback).onVisibleAreaChanged(rect);
+        verify(mMockOnDoneCallback).onSuccess(any());
+    }
+
+    @Test
+    public void onVisibleAreaChanged_lifecycleNotCreated_doesNotDispatch_sendsAFailure()
+            throws RemoteException {
+        mAppManager.setSurfaceCallback(mSurfaceCallback);
+        verify(mMockAppHost).setSurfaceCallback(mSurfaceCallbackCaptor.capture());
+        Rect rect = new Rect(0, 0, 1, 1);
+
+        mSurfaceCallbackCaptor.getValue().onVisibleAreaChanged(rect, mMockOnDoneCallback);
+
+        verify(mSurfaceCallback, never()).onVisibleAreaChanged(any());
+        verify(mMockOnDoneCallback).onFailure(any());
+    }
+
+    @Test
+    public void onStableAreaChanged_dispatches()
+            throws RemoteException {
+        mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
+        mAppManager.setSurfaceCallback(mSurfaceCallback);
+        verify(mMockAppHost).setSurfaceCallback(mSurfaceCallbackCaptor.capture());
+        Rect rect = new Rect(0, 0, 1, 1);
+
+        mSurfaceCallbackCaptor.getValue().onStableAreaChanged(rect, mMockOnDoneCallback);
+
+        verify(mSurfaceCallback).onStableAreaChanged(rect);
+        verify(mMockOnDoneCallback).onSuccess(any());
+    }
+
+    @Test
+    public void onStableAreaChanged_lifecycleNotCreated_doesNotDispatch_sendsAFailure()
+            throws RemoteException {
+        mAppManager.setSurfaceCallback(mSurfaceCallback);
+        verify(mMockAppHost).setSurfaceCallback(mSurfaceCallbackCaptor.capture());
+        Rect rect = new Rect(0, 0, 1, 1);
+
+        mSurfaceCallbackCaptor.getValue().onStableAreaChanged(rect, mMockOnDoneCallback);
+
+        verify(mSurfaceCallback, never()).onStableAreaChanged(any());
+        verify(mMockOnDoneCallback).onFailure(any());
+    }
+
+    @Test
+    public void onSurfaceDestroyed_dispatches()
+            throws RemoteException, BundlerException {
+        mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
+        mAppManager.setSurfaceCallback(mSurfaceCallback);
+        verify(mMockAppHost).setSurfaceCallback(mSurfaceCallbackCaptor.capture());
+        SurfaceContainer surfaceContainer = new SurfaceContainer(null, 1, 2, 3);
+
+        mSurfaceCallbackCaptor.getValue().onSurfaceDestroyed(Bundleable.create(surfaceContainer),
+                mMockOnDoneCallback);
+
+        verify(mSurfaceCallback).onSurfaceDestroyed(any());
+        verify(mMockOnDoneCallback).onSuccess(any());
+    }
+
+    @Test
+    public void onSurfaceDestroyed_lifecycleNotCreated_doesNotDispatch_sendsAFailure()
+            throws RemoteException, BundlerException {
+        mAppManager.setSurfaceCallback(mSurfaceCallback);
+        verify(mMockAppHost).setSurfaceCallback(mSurfaceCallbackCaptor.capture());
+        SurfaceContainer surfaceContainer = new SurfaceContainer(null, 1, 2, 3);
+
+        mSurfaceCallbackCaptor.getValue().onSurfaceDestroyed(Bundleable.create(surfaceContainer),
+                mMockOnDoneCallback);
+
+        verify(mSurfaceCallback, never()).onSurfaceDestroyed(surfaceContainer);
+        verify(mMockOnDoneCallback).onFailure(any());
+    }
+
+    private static class NonBundleableTemplate implements Template {
+        NonBundleableTemplate(String s) {
+        }
+    }
+
+    private static class TestTemplate implements Template {
+    }
 }
diff --git a/car/app/app/src/test/java/androidx/car/app/CarAppServiceTest.java b/car/app/app/src/test/java/androidx/car/app/CarAppServiceTest.java
index f5ea8a2..ae9ca49 100644
--- a/car/app/app/src/test/java/androidx/car/app/CarAppServiceTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/CarAppServiceTest.java
@@ -63,6 +63,8 @@
     ICarHost mMockCarHost;
     @Mock
     DefaultLifecycleObserver mLifecycleObserver;
+    @Mock
+    IOnDoneCallback mMockOnDoneCallback;
 
     private TestCarContext mCarContext;
     private final Template mTemplate =
@@ -137,8 +139,8 @@
         HandshakeInfo handshakeInfo = new HandshakeInfo(hostPackageName, hostApiLevel);
 
         mCarAppService.setCurrentSession(null);
-        carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mock(IOnDoneCallback.class));
-        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+        carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mMockOnDoneCallback);
+        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mMockOnDoneCallback);
 
         assertThat(
                 mCarAppService.getCurrentSession().getCarContext().getCarAppApiLevel()).isEqualTo(
@@ -148,7 +150,7 @@
     @Test
     public void onAppCreate_createsFirstScreen() throws RemoteException {
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
-        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mMockOnDoneCallback);
 
         assertThat(
                 mCarAppService
@@ -163,13 +165,12 @@
     @Test
     public void onAppCreate_withIntent_callsWithOnCreateScreenWithIntent() throws
             RemoteException {
-        IOnDoneCallback callback = mock(IOnDoneCallback.class);
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
         Intent intent = new Intent("Foo");
-        carApp.onAppCreate(mMockCarHost, intent, new Configuration(), callback);
+        carApp.onAppCreate(mMockCarHost, intent, new Configuration(), mMockOnDoneCallback);
 
         assertThat(mIntentSet).isEqualTo(intent);
-        verify(callback).onSuccess(any());
+        verify(mMockOnDoneCallback).onSuccess(any());
     }
 
     @Test
@@ -209,9 +210,21 @@
         carApp.onAppCreate(mMockCarHost, intent, new Configuration(), mock(IOnDoneCallback.class));
 
         Intent intent2 = new Intent("Foo2");
-        carApp.onNewIntent(intent2, mock(IOnDoneCallback.class));
+        carApp.onNewIntent(intent2, mMockOnDoneCallback);
 
         assertThat(mIntentSet).isEqualTo(intent2);
+        verify(mMockOnDoneCallback).onSuccess(any());
+    }
+
+    @Test
+    public void onNewIntent_lifecycleNotCreated_doesNotDispatch_sendsError()
+            throws RemoteException {
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+        carApp.onNewIntent(new Intent("Foo"), mMockOnDoneCallback);
+
+        assertThat(mIntentSet).isNull();
+        verify(mMockOnDoneCallback).onFailure(any());
     }
 
     @Test
@@ -224,6 +237,21 @@
     }
 
     @Test
+    public void onConfigurationChanged_lifecycleNotCreated_returnsAFailure()
+            throws RemoteException {
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+        Configuration configuration = new Configuration();
+        configuration.setToDefaults();
+        configuration.setLocale(Locale.CANADA_FRENCH);
+
+        carApp.onConfigurationChanged(configuration, mMockOnDoneCallback);
+
+        assertThat(mCarContext).isNull();
+        verify(mMockOnDoneCallback).onFailure(any());
+    }
+
+    @Test
     public void onConfigurationChanged_updatesTheConfiguration() throws RemoteException {
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
         carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
@@ -232,10 +260,11 @@
         configuration.setToDefaults();
         configuration.setLocale(Locale.CANADA_FRENCH);
 
-        carApp.onConfigurationChanged(configuration, mock(IOnDoneCallback.class));
+        carApp.onConfigurationChanged(configuration, mMockOnDoneCallback);
 
         assertThat(mCarContext.getResources().getConfiguration().getLocales().get(0))
                 .isEqualTo(Locale.CANADA_FRENCH);
+        verify(mMockOnDoneCallback).onSuccess(any());
     }
 
     @Test
@@ -243,11 +272,10 @@
         AppInfo appInfo = new AppInfo(3, 4, "foo");
         mCarAppService.setAppInfo(appInfo);
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
-        IOnDoneCallback callback = mock(IOnDoneCallback.class);
 
-        carApp.getAppInfo(callback);
+        carApp.getAppInfo(mMockOnDoneCallback);
 
-        verify(callback).onSuccess(mBundleableArgumentCaptor.capture());
+        verify(mMockOnDoneCallback).onSuccess(mBundleableArgumentCaptor.capture());
         AppInfo receivedAppInfo = (AppInfo) mBundleableArgumentCaptor.getValue().get();
         assertThat(receivedAppInfo.getMinCarAppApiLevel())
                 .isEqualTo(appInfo.getMinCarAppApiLevel());
@@ -264,7 +292,7 @@
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
         HandshakeInfo handshakeInfo = new HandshakeInfo(hostPackageName, CarAppApiLevels.LEVEL_1);
 
-        carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mock(IOnDoneCallback.class));
+        carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mMockOnDoneCallback);
 
         assertThat(mCarAppService.getHostInfo().getPackageName()).isEqualTo(hostPackageName);
     }
@@ -276,7 +304,7 @@
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
 
         HandshakeInfo handshakeInfo = new HandshakeInfo(hostPackageName, CarAppApiLevels.LEVEL_1);
-        carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mock(IOnDoneCallback.class));
+        carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mMockOnDoneCallback);
         assertThat(mCarAppService.getHandshakeInfo()).isNotNull();
         assertThat(mCarAppService.getHandshakeInfo().getHostCarAppApiLevel()).isEqualTo(
                 handshakeInfo.getHostCarAppApiLevel());
@@ -293,10 +321,9 @@
 
         HandshakeInfo handshakeInfo = new HandshakeInfo("bar",
                 appInfo.getMinCarAppApiLevel() - 1);
-        IOnDoneCallback callback = mock(IOnDoneCallback.class);
-        carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), callback);
+        carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mMockOnDoneCallback);
 
-        verify(callback).onFailure(any());
+        verify(mMockOnDoneCallback).onFailure(any());
     }
 
     @Test
@@ -308,17 +335,16 @@
 
         HandshakeInfo handshakeInfo = new HandshakeInfo("bar",
                 appInfo.getLatestCarAppApiLevel() + 1);
-        IOnDoneCallback callback = mock(IOnDoneCallback.class);
-        carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), callback);
+        carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mMockOnDoneCallback);
 
-        verify(callback).onFailure(any());
+        verify(mMockOnDoneCallback).onFailure(any());
     }
 
     @Test
     public void onUnbind_movesLifecycleStateToDestroyed() throws RemoteException {
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
-        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
-        carApp.onAppStart(mock(IOnDoneCallback.class));
+        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mMockOnDoneCallback);
+        carApp.onAppStart(mMockOnDoneCallback);
 
         mCarAppService.getCurrentSession().getLifecycle().addObserver(mLifecycleObserver);
 
@@ -331,8 +357,8 @@
     public void onUnbind_rebind_callsOnCreateScreen() throws RemoteException, BundlerException {
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
 
-        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
-        carApp.onAppStart(mock(IOnDoneCallback.class));
+        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mMockOnDoneCallback);
+        carApp.onAppStart(mMockOnDoneCallback);
 
         Session currentSession = mCarAppService.getCurrentSession();
         currentSession.getLifecycle().addObserver(mLifecycleObserver);
@@ -347,8 +373,8 @@
         String hostPackageName = "com.google.projection.gearhead";
         int hostApiLevel = CarAppApiLevels.LEVEL_1;
         HandshakeInfo handshakeInfo = new HandshakeInfo(hostPackageName, hostApiLevel);
-        carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mock(IOnDoneCallback.class));
-        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+        carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mMockOnDoneCallback);
+        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mMockOnDoneCallback);
 
         currentSession = mCarAppService.getCurrentSession();
         assertThat(currentSession.getCarContext().getCarService(
@@ -358,7 +384,7 @@
     @Test
     public void onUnbind_clearsScreenStack() throws RemoteException {
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
-        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mMockOnDoneCallback);
 
         Deque<Screen> screenStack =
                 mCarAppService.getCurrentSession().getCarContext().getCarService(
@@ -377,7 +403,7 @@
     @Test
     public void finish() throws RemoteException {
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
-        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mMockOnDoneCallback);
 
         mCarAppService.getCurrentSession().getCarContext().finishCarApp();
 
@@ -393,11 +419,189 @@
         carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
         assertThat(mIntentSet).isNull();
 
-        IOnDoneCallback callback = mock(IOnDoneCallback.class);
         Intent intent = new Intent("Foo");
-        carApp.onNewIntent(intent, callback);
+        carApp.onNewIntent(intent, mMockOnDoneCallback);
 
         assertThat(mIntentSet).isEqualTo(intent);
-        verify(callback).onSuccess(any());
+        verify(mMockOnDoneCallback).onSuccess(any());
+    }
+
+    @Test
+    public void onNewIntent_notAtLeastCreated_doesCallSessionIntent_sendsFailure() throws
+            RemoteException {
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+        Intent intent = new Intent("Foo");
+        carApp.onNewIntent(intent, mMockOnDoneCallback);
+
+        assertThat(mIntentSet).isNull();
+        verify(mMockOnDoneCallback).onFailure(any());
+    }
+
+    @Test
+    public void onNewIntent_destroyed_doesCallSessionIntent_sendsFailure() throws
+            RemoteException {
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+        // onAppCreate must be called first to create the Session before onNewIntent.
+        Intent intent1 = new Intent();
+        carApp.onAppCreate(mMockCarHost, intent1, new Configuration(), mock(IOnDoneCallback.class));
+        assertThat(mIntentSet).isEqualTo(intent1);
+
+        mCarContext.getLifecycleOwner().mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+
+        Intent intent2 = new Intent("Foo");
+        carApp.onNewIntent(intent2, mMockOnDoneCallback);
+
+        assertThat(mIntentSet).isEqualTo(intent1);
+        verify(mMockOnDoneCallback).onFailure(any());
+    }
+
+    @Test
+    public void onAppStart_movesLifecycle() throws RemoteException {
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+
+        carApp.onAppStart(mMockOnDoneCallback);
+
+        verify(mMockOnDoneCallback).onSuccess(any());
+        assertThat(mCarContext.getLifecycleOwner().mRegistry.getCurrentState()).isEqualTo(
+                Lifecycle.State.STARTED);
+    }
+
+    @Test
+    public void onAppStart_notAtLeastCreated_doesNotMoveLifecycle_sendsFailure()
+            throws RemoteException {
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+        carApp.onAppStart(mMockOnDoneCallback);
+
+        verify(mMockOnDoneCallback).onFailure(any());
+        assertThat(mCarContext).isNull();
+    }
+
+    @Test
+    public void onAppStart_destroyed_doesNotMoveLifecycle_sendsFailure()
+            throws RemoteException {
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+        mCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.DESTROYED);
+
+        carApp.onAppStart(mMockOnDoneCallback);
+
+        verify(mMockOnDoneCallback).onFailure(any());
+        assertThat(mCarContext.getLifecycleOwner().mRegistry.getCurrentState()).isEqualTo(
+                Lifecycle.State.DESTROYED);
+    }
+
+    @Test
+    public void onAppResume_movesLifecycle() throws RemoteException {
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+
+        carApp.onAppResume(mMockOnDoneCallback);
+
+        verify(mMockOnDoneCallback).onSuccess(any());
+        assertThat(mCarContext.getLifecycleOwner().mRegistry.getCurrentState()).isEqualTo(
+                Lifecycle.State.RESUMED);
+    }
+
+    @Test
+    public void onAppResume_notAtLeastCreated_doesNotMoveLifecycle_sendsFailure()
+            throws RemoteException {
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+        carApp.onAppResume(mMockOnDoneCallback);
+
+        verify(mMockOnDoneCallback).onFailure(any());
+        assertThat(mCarContext).isNull();
+    }
+
+    @Test
+    public void onAppResume_destroyed_doesNotMoveLifecycle_sendsFailure()
+            throws RemoteException {
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+        mCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.DESTROYED);
+
+        carApp.onAppResume(mMockOnDoneCallback);
+
+        verify(mMockOnDoneCallback).onFailure(any());
+        assertThat(mCarContext.getLifecycleOwner().mRegistry.getCurrentState()).isEqualTo(
+                Lifecycle.State.DESTROYED);
+    }
+
+    @Test
+    public void onAppPause_movesLifecycle() throws RemoteException {
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+
+        carApp.onAppPause(mMockOnDoneCallback);
+
+        verify(mMockOnDoneCallback).onSuccess(any());
+        assertThat(mCarContext.getLifecycleOwner().mRegistry.getCurrentState()).isEqualTo(
+                Lifecycle.State.STARTED);
+    }
+
+    @Test
+    public void onAppPause_notAtLeastCreated_doesNotMoveLifecycle_sendsFailure()
+            throws RemoteException {
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+        carApp.onAppPause(mMockOnDoneCallback);
+
+        verify(mMockOnDoneCallback).onFailure(any());
+        assertThat(mCarContext).isNull();
+    }
+
+    @Test
+    public void onAppPause_destroyed_doesNotMoveLifecycle_sendsFailure()
+            throws RemoteException {
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+        mCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.DESTROYED);
+
+        carApp.onAppPause(mMockOnDoneCallback);
+
+        verify(mMockOnDoneCallback).onFailure(any());
+        assertThat(mCarContext.getLifecycleOwner().mRegistry.getCurrentState()).isEqualTo(
+                Lifecycle.State.DESTROYED);
+    }
+
+    @Test
+    public void onAppStop_movesLifecycle() throws RemoteException {
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+
+        carApp.onAppStop(mMockOnDoneCallback);
+
+        verify(mMockOnDoneCallback).onSuccess(any());
+        assertThat(mCarContext.getLifecycleOwner().mRegistry.getCurrentState()).isEqualTo(
+                Lifecycle.State.CREATED);
+    }
+
+    @Test
+    public void onAppStop_notAtLeastCreated_doesNotMoveLifecycle_sendsFailure()
+            throws RemoteException {
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+        carApp.onAppStop(mMockOnDoneCallback);
+
+        verify(mMockOnDoneCallback).onFailure(any());
+        assertThat(mCarContext).isNull();
+    }
+
+    @Test
+    public void onAppStop_destroyed_doesNotMoveLifecycle_sendsFailure()
+            throws RemoteException {
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+        carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+        mCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.DESTROYED);
+
+        carApp.onAppStop(mMockOnDoneCallback);
+
+        verify(mMockOnDoneCallback).onFailure(any());
+        assertThat(mCarContext.getLifecycleOwner().mRegistry.getCurrentState()).isEqualTo(
+                Lifecycle.State.DESTROYED);
     }
 }
diff --git a/car/app/app/src/test/java/androidx/car/app/CarContextTest.java b/car/app/app/src/test/java/androidx/car/app/CarContextTest.java
index a0d08c2..a8eb9aa 100644
--- a/car/app/app/src/test/java/androidx/car/app/CarContextTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/CarContextTest.java
@@ -52,6 +52,7 @@
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.annotation.internal.DoNotInstrument;
 
+import java.lang.reflect.Field;
 import java.util.Locale;
 
 /** Tests for {@link CarContext}. */
@@ -402,4 +403,19 @@
 
         verify(callback).handleOnBackPressed();
     }
+
+    @Test
+    public void lifecycleDestroyed_removesHostBinders()
+            throws ReflectiveOperationException, RemoteException {
+        mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_CREATE);
+        Field field = CarContext.class.getDeclaredField("mHostDispatcher");
+        field.setAccessible(true);
+        HostDispatcher hostDispatcher = (HostDispatcher) field.get(mCarContext);
+
+        assertThat(hostDispatcher.getHost(CarContext.APP_SERVICE)).isNotNull();
+
+        mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_DESTROY);
+
+        assertThat(hostDispatcher.getHost(CarContext.APP_SERVICE)).isNull();
+    }
 }
diff --git a/car/app/app/src/test/java/androidx/car/app/HostDispatcherTest.java b/car/app/app/src/test/java/androidx/car/app/HostDispatcherTest.java
index f0abe7e..c1a93fd 100644
--- a/car/app/app/src/test/java/androidx/car/app/HostDispatcherTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/HostDispatcherTest.java
@@ -100,13 +100,58 @@
     }
 
     @Test
-    public void dispatch_callGoesToProperRemoteService() throws RemoteException {
-        mHostDispatcher.dispatch(CarContext.APP_SERVICE,
-                (IAppHost service) -> {
+    public void dispatchForResult_callGoesToProperRemoteService() throws RemoteException {
+        mHostDispatcher.dispatchForResult(CarContext.APP_SERVICE,
+                "test", (IAppHost service) -> {
                     service.invalidate();
                     return null;
-                },
-                "test");
+                }
+        );
+
+        verify(mMockAppHost).invalidate();
+    }
+
+    @Test
+    public void dispatchForResult_callThrowsSecurityException_throwsSecurityException() {
+        assertThrows(
+                SecurityException.class,
+                () -> mHostDispatcher.dispatchForResult(
+                        CarContext.APP_SERVICE,
+                        "test", (IAppHost service) -> {
+                            throw new SecurityException();
+                        }
+                ));
+    }
+
+    @Test
+    public void dispatchForResult_callThrowsRemoteException_throws() {
+        assertThrows(RemoteException.class, () -> mHostDispatcher.dispatchForResult(
+                CarContext.APP_SERVICE,
+                "test", (IAppHost service) -> {
+                    throw new RemoteException();
+                }));
+    }
+
+    @Test
+    public void dispatchForResult_callThrowsRuntimeException_throwsHostException() {
+        assertThrows(
+                HostException.class,
+                () -> mHostDispatcher.dispatchForResult(
+                        CarContext.APP_SERVICE,
+                        "test", (IAppHost service) -> {
+                            throw new IllegalStateException();
+                        }
+                ));
+    }
+
+    @Test
+    public void dispatch_callGoesToProperRemoteService() throws RemoteException {
+        mHostDispatcher.dispatch(CarContext.APP_SERVICE,
+                "test", (IAppHost service) -> {
+                    service.invalidate();
+                    return null;
+                }
+        );
 
         verify(mMockAppHost).invalidate();
     }
@@ -117,22 +162,19 @@
                 SecurityException.class,
                 () -> mHostDispatcher.dispatch(
                         CarContext.APP_SERVICE,
-                        (IAppHost service) -> {
+                        "test", (IAppHost service) -> {
                             throw new SecurityException();
-                        },
-                        "test"));
+                        }
+                ));
     }
 
     @Test
-    public void dispatch_callThrowsRemoteException_throwsHostException() {
-        assertThrows(
-                HostException.class,
-                () -> mHostDispatcher.dispatch(
-                        CarContext.APP_SERVICE,
-                        (IAppHost service) -> {
-                            throw new RemoteException();
-                        },
-                        "test"));
+    public void dispatch_callThrowsRemoteException_doesNotCrash() {
+        mHostDispatcher.dispatch(
+                CarContext.APP_SERVICE,
+                "test", (IAppHost service) -> {
+                    throw new RemoteException();
+                });
     }
 
     @Test
@@ -141,10 +183,10 @@
                 HostException.class,
                 () -> mHostDispatcher.dispatch(
                         CarContext.APP_SERVICE,
-                        (IAppHost service) -> {
+                        "test", (IAppHost service) -> {
                             throw new IllegalStateException();
-                        },
-                        "test"));
+                        }
+                ));
     }
 
     @Test
@@ -153,6 +195,7 @@
 
         mHostDispatcher.resetHosts();
 
+        mHostDispatcher.setCarHost(mMockCarHost);
         doThrow(new IllegalStateException()).when(mMockCarHost).getHost(any());
 
         assertThrows(HostException.class, () -> mHostDispatcher.getHost(CarContext.APP_SERVICE));
@@ -168,15 +211,15 @@
     }
 
     @Test
-    public void getHost_appHost_returnsProperHostService() {
+    public void getHost_appHost_returnsProperHostService() throws RemoteException {
         assertThat(mHostDispatcher.getHost(CarContext.APP_SERVICE)).isEqualTo(mAppHost);
     }
 
     @Test
-    public void getHost_appHost_hostThrowsRemoteException_throwsHostException()
+    public void getHost_appHost_hostThrowsRemoteException_throwsTheException()
             throws RemoteException {
         when(mMockCarHost.getHost(any())).thenThrow(new RemoteException());
-        assertThrows(HostException.class, () -> mHostDispatcher.getHost(CarContext.APP_SERVICE));
+        assertThrows(RemoteException.class, () -> mHostDispatcher.getHost(CarContext.APP_SERVICE));
     }
 
     @Test
@@ -187,16 +230,16 @@
     }
 
     @Test
-    public void getHost_navigationHost_returnsProperHostService() {
+    public void getHost_navigationHost_returnsProperHostService() throws RemoteException {
         assertThat(mHostDispatcher.getHost(CarContext.NAVIGATION_SERVICE)).isEqualTo(
                 mNavigationHost);
     }
 
     @Test
-    public void getHost_navigationHost_hostThrowsRemoteException_throwsHostException()
+    public void getHost_navigationHost_hostThrowsRemoteException()
             throws RemoteException {
         when(mMockCarHost.getHost(any())).thenThrow(new RemoteException());
-        assertThrows(HostException.class,
+        assertThrows(RemoteException.class,
                 () -> mHostDispatcher.getHost(CarContext.NAVIGATION_SERVICE));
     }
 
@@ -209,16 +252,16 @@
     }
 
     @Test
-    public void getHost_afterReset_throwsHostException() {
+    public void getHost_afterReset_returnsNull() throws RemoteException {
         mHostDispatcher.resetHosts();
 
-        assertThrows(HostException.class, () -> mHostDispatcher.getHost(CarContext.APP_SERVICE));
+        assertThat(mHostDispatcher.getHost(CarContext.APP_SERVICE)).isNull();
     }
 
     @Test
-    public void getHost_notBound_throwsHostException() {
+    public void getHost_notBound_returnsNull() throws RemoteException {
         mHostDispatcher = new HostDispatcher();
 
-        assertThrows(HostException.class, () -> mHostDispatcher.getHost(CarContext.APP_SERVICE));
+        assertThat(mHostDispatcher.getHost(CarContext.APP_SERVICE)).isNull();
     }
 }
diff --git a/car/app/app/src/test/java/androidx/car/app/navigation/NavigationManagerTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/NavigationManagerTest.java
index f96c7ec..8e3b9c9 100644
--- a/car/app/app/src/test/java/androidx/car/app/navigation/NavigationManagerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/NavigationManagerTest.java
@@ -38,6 +38,7 @@
 import androidx.car.app.navigation.model.Trip;
 import androidx.car.app.serialization.Bundleable;
 import androidx.car.app.testing.TestCarContext;
+import androidx.lifecycle.Lifecycle;
 import androidx.test.core.app.ApplicationProvider;
 
 import org.junit.Before;
@@ -86,12 +87,13 @@
                     .addStep(mStep, mStepTravelEstimate)
                     .setCurrentRoad(CURRENT_ROAD)
                     .build();
+    private TestCarContext mTestCarContext;
 
     @Before
     public void setUp() throws RemoteException {
         MockitoAnnotations.initMocks(this);
 
-        TestCarContext testCarContext =
+        mTestCarContext =
                 TestCarContext.createCarContext(ApplicationProvider.getApplicationContext());
 
         INavigationHost navHostStub =
@@ -115,7 +117,8 @@
 
         mHostDispatcher.setCarHost(mMockCarHost);
 
-        mNavigationManager = NavigationManager.create(testCarContext, mHostDispatcher);
+        mNavigationManager = NavigationManager.create(mTestCarContext, mHostDispatcher,
+                mTestCarContext.getLifecycleOwner().mRegistry);
     }
 
     @Test
@@ -162,14 +165,43 @@
     }
 
     @Test
+    public void lifecycleDestroyed_callsOnStopNavigation() throws RemoteException {
+        mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
+        mNavigationManager.setNavigationManagerCallback(new SynchronousExecutor(),
+                mNavigationListener);
+        mNavigationManager.navigationStarted();
+        verify(mMockNavHost).navigationStarted();
+
+        mTestCarContext.getLifecycleOwner().mRegistry.handleLifecycleEvent(
+                Lifecycle.Event.ON_DESTROY);
+
+        verify(mNavigationListener).onStopNavigation();
+    }
+
+    @Test
     public void onStopNavigation_notNavigating() throws RemoteException {
+        mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
         mNavigationManager.setNavigationManagerCallback(mNavigationListener);
+
         mNavigationManager.getIInterface().onStopNavigation(mock(IOnDoneCallback.class));
+
+        verify(mNavigationListener, never()).onStopNavigation();
+    }
+
+    @Test
+    public void onStopNavigation_lifecycleNotCreated_doesNotDispatch() throws RemoteException {
+        mNavigationManager.setNavigationManagerCallback(mNavigationListener);
+        mNavigationManager.navigationStarted();
+        verify(mMockNavHost).navigationStarted();
+
+        mNavigationManager.getIInterface().onStopNavigation(mock(IOnDoneCallback.class));
+
         verify(mNavigationListener, never()).onStopNavigation();
     }
 
     @Test
     public void onStopNavigation_navigating_restart() throws RemoteException {
+        mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
         InOrder inOrder = inOrder(mMockNavHost, mNavigationListener);
 
         mNavigationManager.setNavigationManagerCallback(new SynchronousExecutor(),
@@ -186,6 +218,24 @@
     }
 
     @Test
+    public void onStopNavigation_noListener_doesNotThrow() throws RemoteException {
+        InOrder inOrder = inOrder(mMockNavHost, mNavigationListener);
+
+        mNavigationManager.setNavigationManagerCallback(new SynchronousExecutor(),
+                mNavigationListener);
+        mNavigationManager.navigationStarted();
+        inOrder.verify(mMockNavHost).navigationStarted();
+
+        mNavigationManager.onStopNavigation();
+        mNavigationManager.clearNavigationManagerCallback();
+        inOrder.verify(mNavigationListener).onStopNavigation();
+
+        mNavigationManager.getIInterface().onStopNavigation(mock(IOnDoneCallback.class));
+
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
     public void onAutoDriveEnabled_callsListener() {
         mNavigationManager.setNavigationManagerCallback(new SynchronousExecutor(),
                 mNavigationListener);
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
index 9ca1b6ff..989ed4f 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
@@ -3588,6 +3588,24 @@
     }
 
     @Test
+    fun testRow_withArrangementSpacing() = with(density) {
+        val spacing = 5
+        val childSize = 10
+        testIntrinsics(
+            @Composable {
+                Row(horizontalArrangement = Arrangement.spacedBy(spacing.toDp())) {
+                    Box(Modifier.size(childSize.toDp()))
+                    Box(Modifier.size(childSize.toDp()))
+                    Box(Modifier.size(childSize.toDp()))
+                }
+            }
+        ) { minIntrinsicWidth, _, maxIntrinsicWidth, _ ->
+            assertEquals(childSize * 3 + 2 * spacing, minIntrinsicWidth(Constraints.Infinity))
+            assertEquals(childSize * 3 + 2 * spacing, maxIntrinsicWidth(Constraints.Infinity))
+        }
+    }
+
+    @Test
     fun testColumn_withNoWeightChildren_hasCorrectIntrinsicMeasurements() = with(density) {
         testIntrinsics(
             @Composable {
@@ -3917,6 +3935,24 @@
     }
 
     @Test
+    fun testColumn_withArrangementSpacing() = with(density) {
+        val spacing = 5
+        val childSize = 10
+        testIntrinsics(
+            @Composable {
+                Column(verticalArrangement = Arrangement.spacedBy(spacing.toDp())) {
+                    Box(Modifier.size(childSize.toDp()))
+                    Box(Modifier.size(childSize.toDp()))
+                    Box(Modifier.size(childSize.toDp()))
+                }
+            }
+        ) { _, minIntrinsicHeight, _, maxIntrinsicHeight ->
+            assertEquals(childSize * 3 + 2 * spacing, minIntrinsicHeight(Constraints.Infinity))
+            assertEquals(childSize * 3 + 2 * spacing, maxIntrinsicHeight(Constraints.Infinity))
+        }
+    }
+
+    @Test
     fun testRow_withWIHOChild_hasCorrectIntrinsicMeasurements() = with(density) {
         val dividerWidth = 10.dp
         val rowWidth = 40.dp
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
index c2603f5..bfa647b 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
@@ -268,22 +268,38 @@
         override fun IntrinsicMeasureScope.minIntrinsicWidth(
             measurables: List<IntrinsicMeasurable>,
             height: Int
-        ) = MinIntrinsicWidthMeasureBlock(orientation)(measurables, height)
+        ) = MinIntrinsicWidthMeasureBlock(orientation)(
+            measurables,
+            height,
+            arrangementSpacing.roundToPx()
+        )
 
         override fun IntrinsicMeasureScope.minIntrinsicHeight(
             measurables: List<IntrinsicMeasurable>,
             width: Int
-        ) = MinIntrinsicHeightMeasureBlock(orientation)(measurables, width)
+        ) = MinIntrinsicHeightMeasureBlock(orientation)(
+            measurables,
+            width,
+            arrangementSpacing.roundToPx()
+        )
 
         override fun IntrinsicMeasureScope.maxIntrinsicWidth(
             measurables: List<IntrinsicMeasurable>,
             height: Int
-        ) = MaxIntrinsicWidthMeasureBlock(orientation)(measurables, height)
+        ) = MaxIntrinsicWidthMeasureBlock(orientation)(
+            measurables,
+            height,
+            arrangementSpacing.roundToPx()
+        )
 
         override fun IntrinsicMeasureScope.maxIntrinsicHeight(
             measurables: List<IntrinsicMeasurable>,
             width: Int
-        ) = MaxIntrinsicHeightMeasureBlock(orientation)(measurables, width)
+        ) = MaxIntrinsicHeightMeasureBlock(orientation)(
+            measurables,
+            width,
+            arrangementSpacing.roundToPx()
+        )
     }
 }
 
@@ -559,90 +575,98 @@
     }
 
 private object IntrinsicMeasureBlocks {
-    val HorizontalMinWidth: (List<IntrinsicMeasurable>, Int) -> Int =
-        { measurables, availableHeight ->
+    val HorizontalMinWidth: (List<IntrinsicMeasurable>, Int, Int) -> Int =
+        { measurables, availableHeight, mainAxisSpacing ->
             intrinsicSize(
                 measurables,
                 { h -> minIntrinsicWidth(h) },
                 { w -> maxIntrinsicHeight(w) },
                 availableHeight,
+                mainAxisSpacing,
                 LayoutOrientation.Horizontal,
                 LayoutOrientation.Horizontal
             )
         }
-    val VerticalMinWidth: (List<IntrinsicMeasurable>, Int) -> Int =
-        { measurables, availableHeight ->
+    val VerticalMinWidth: (List<IntrinsicMeasurable>, Int, Int) -> Int =
+        { measurables, availableHeight, mainAxisSpacing ->
             intrinsicSize(
                 measurables,
                 { h -> minIntrinsicWidth(h) },
                 { w -> maxIntrinsicHeight(w) },
                 availableHeight,
+                mainAxisSpacing,
                 LayoutOrientation.Vertical,
                 LayoutOrientation.Horizontal
             )
         }
-    val HorizontalMinHeight: (List<IntrinsicMeasurable>, Int) -> Int =
-        { measurables, availableWidth ->
+    val HorizontalMinHeight: (List<IntrinsicMeasurable>, Int, Int) -> Int =
+        { measurables, availableWidth, mainAxisSpacing ->
             intrinsicSize(
                 measurables,
                 { w -> minIntrinsicHeight(w) },
                 { h -> maxIntrinsicWidth(h) },
                 availableWidth,
+                mainAxisSpacing,
                 LayoutOrientation.Horizontal,
                 LayoutOrientation.Vertical
             )
         }
-    val VerticalMinHeight: (List<IntrinsicMeasurable>, Int) -> Int =
-        { measurables, availableWidth ->
+    val VerticalMinHeight: (List<IntrinsicMeasurable>, Int, Int) -> Int =
+        { measurables, availableWidth, mainAxisSpacing ->
             intrinsicSize(
                 measurables,
                 { w -> minIntrinsicHeight(w) },
                 { h -> maxIntrinsicWidth(h) },
                 availableWidth,
+                mainAxisSpacing,
                 LayoutOrientation.Vertical,
                 LayoutOrientation.Vertical
             )
         }
-    val HorizontalMaxWidth: (List<IntrinsicMeasurable>, Int) -> Int =
-        { measurables, availableHeight ->
+    val HorizontalMaxWidth: (List<IntrinsicMeasurable>, Int, Int) -> Int =
+        { measurables, availableHeight, mainAxisSpacing ->
             intrinsicSize(
                 measurables,
                 { h -> maxIntrinsicWidth(h) },
                 { w -> maxIntrinsicHeight(w) },
                 availableHeight,
+                mainAxisSpacing,
                 LayoutOrientation.Horizontal,
                 LayoutOrientation.Horizontal
             )
         }
-    val VerticalMaxWidth: (List<IntrinsicMeasurable>, Int) -> Int =
-        { measurables, availableHeight ->
+    val VerticalMaxWidth: (List<IntrinsicMeasurable>, Int, Int) -> Int =
+        { measurables, availableHeight, mainAxisSpacing ->
             intrinsicSize(
                 measurables,
                 { h -> maxIntrinsicWidth(h) },
                 { w -> maxIntrinsicHeight(w) },
                 availableHeight,
+                mainAxisSpacing,
                 LayoutOrientation.Vertical,
                 LayoutOrientation.Horizontal
             )
         }
-    val HorizontalMaxHeight: (List<IntrinsicMeasurable>, Int) -> Int =
-        { measurables, availableWidth ->
+    val HorizontalMaxHeight: (List<IntrinsicMeasurable>, Int, Int) -> Int =
+        { measurables, availableWidth, mainAxisSpacing ->
             intrinsicSize(
                 measurables,
                 { w -> maxIntrinsicHeight(w) },
                 { h -> maxIntrinsicWidth(h) },
                 availableWidth,
+                mainAxisSpacing,
                 LayoutOrientation.Horizontal,
                 LayoutOrientation.Vertical
             )
         }
-    val VerticalMaxHeight: (List<IntrinsicMeasurable>, Int) -> Int =
-        { measurables, availableWidth ->
+    val VerticalMaxHeight: (List<IntrinsicMeasurable>, Int, Int) -> Int =
+        { measurables, availableWidth, mainAxisSpacing ->
             intrinsicSize(
                 measurables,
                 { w -> maxIntrinsicHeight(w) },
                 { h -> maxIntrinsicWidth(h) },
                 availableWidth,
+                mainAxisSpacing,
                 LayoutOrientation.Vertical,
                 LayoutOrientation.Vertical
             )
@@ -654,10 +678,11 @@
     intrinsicMainSize: IntrinsicMeasurable.(Int) -> Int,
     intrinsicCrossSize: IntrinsicMeasurable.(Int) -> Int,
     crossAxisAvailable: Int,
+    mainAxisSpacing: Int,
     layoutOrientation: LayoutOrientation,
     intrinsicOrientation: LayoutOrientation
 ) = if (layoutOrientation == intrinsicOrientation) {
-    intrinsicMainAxisSize(children, intrinsicMainSize, crossAxisAvailable)
+    intrinsicMainAxisSize(children, intrinsicMainSize, crossAxisAvailable, mainAxisSpacing)
 } else {
     intrinsicCrossAxisSize(children, intrinsicCrossSize, intrinsicMainSize, crossAxisAvailable)
 }
@@ -665,7 +690,8 @@
 private fun intrinsicMainAxisSize(
     children: List<IntrinsicMeasurable>,
     mainAxisSize: IntrinsicMeasurable.(Int) -> Int,
-    crossAxisAvailable: Int
+    crossAxisAvailable: Int,
+    mainAxisSpacing: Int
 ): Int {
     var weightUnitSpace = 0
     var fixedSpace = 0
@@ -680,7 +706,8 @@
             weightUnitSpace = max(weightUnitSpace, (size / weight).roundToInt())
         }
     }
-    return (weightUnitSpace * totalWeight).roundToInt() + fixedSpace
+    return (weightUnitSpace * totalWeight).roundToInt() + fixedSpace +
+        (children.size - 1) * mainAxisSpacing
 }
 
 private fun intrinsicCrossAxisSize(
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/animation/Animation.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/animation/Animation.kt
index 4b2b047..f674d16 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/animation/Animation.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/animation/Animation.kt
@@ -101,7 +101,6 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 import org.junit.Rule
@@ -304,7 +303,7 @@
 }
 
 @Composable
-fun TargetBasedAnimationSimple(someCustomCondition: () -> Boolean, scope: CoroutineScope) {
+fun TargetBasedAnimationSimple(someCustomCondition: () -> Boolean) {
     val anim = remember {
         TargetBasedAnimation(
             animationSpec = tween(200),
@@ -315,7 +314,7 @@
     }
     var playTime by remember { mutableStateOf(0L) }
 
-    scope.launch {
+    LaunchedEffect(anim) {
         val startTime = withFrameNanos { it }
 
         do {
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/CheatSheet.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/CheatSheet.kt
index 3feae6b..14bfe86 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/CheatSheet.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/CheatSheet.kt
@@ -371,7 +371,6 @@
 }
 
 @RequiresApi(Build.VERSION_CODES.O)
-@Composable
 private fun TestingCheatSheetOther() {
 
     // COMPOSE TEST RULE
diff --git a/compose/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt b/compose/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt
index 3806120..2cde8e4 100644
--- a/compose/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt
+++ b/compose/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt
@@ -145,8 +145,9 @@
             val expectedComposable = when (val source = parentDeclaration.sourcePsi) {
                 // The source is in Kotlin source, so check the parameter for @Composable
                 is KtCallableDeclaration -> {
-                    // Currently type annotations don't appear on the psiType, so we have to look
-                    // through the type reference (https://youtrack.jetbrains.com/issue/KT-45244)
+                    // Currently type annotations don't appear on the psiType in the version of
+                    // UAST / PSI we are using, so we have to look through the type reference.
+                    // Should be fixed when Lint upgrades the version to 1.4.30+.
                     val typeReference = source.valueParameters[parameterIndex]!!
                         .typeReference as KtTypeReference
                     typeReference.annotationEntries.any {
@@ -156,7 +157,7 @@
                 // If we cannot resolve the parent expression as a KtCallableDeclaration, then
                 // the source is Java source, or in a class file. We ignore Java source, and
                 // currently there is no way to see the @Composable annotation in the class file
-                // (https://youtrack.jetbrains.com/issue/KT-45244). Instead we can look for the
+                // (https://youtrack.jetbrains.com/issue/KT-45307). Instead we can look for the
                 // presence of a `Composer` parameter, as this is added by the Compose compiler
                 // to every Composable function / lambda.
                 else -> {
diff --git a/compose/material/material/api/public_plus_experimental_1.0.0-beta02.txt b/compose/material/material/api/public_plus_experimental_1.0.0-beta02.txt
index 72bd10a..2795abb 100644
--- a/compose/material/material/api/public_plus_experimental_1.0.0-beta02.txt
+++ b/compose/material/material/api/public_plus_experimental_1.0.0-beta02.txt
@@ -664,6 +664,7 @@
   }
 
   public final class TabKt {
+    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void LeadingIconTab-PWX9des(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> text, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional long selectedContentColor, optional long unselectedContentColor);
     method @androidx.compose.runtime.Composable public static void Tab-TC9MJzw(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional long selectedContentColor, optional long unselectedContentColor);
     method @androidx.compose.runtime.Composable public static void Tab-wUuQ7UU(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional long selectedContentColor, optional long unselectedContentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
   }
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index 72bd10a..2795abb 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -664,6 +664,7 @@
   }
 
   public final class TabKt {
+    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void LeadingIconTab-PWX9des(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> text, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional long selectedContentColor, optional long unselectedContentColor);
     method @androidx.compose.runtime.Composable public static void Tab-TC9MJzw(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional long selectedContentColor, optional long unselectedContentColor);
     method @androidx.compose.runtime.Composable public static void Tab-wUuQ7UU(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional long selectedContentColor, optional long unselectedContentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
   }
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt
index d3c01a0..aeb2681 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt
@@ -45,7 +45,13 @@
         ComposableDemo("App Bars") { AppBarDemo() },
         ComposableDemo("Backdrop") { BackdropScaffoldSample() },
         ComposableDemo("Bottom Navigation") { BottomNavigationDemo() },
-        ComposableDemo("Bottom Sheet") { BottomSheetScaffoldSample() },
+        DemoCategory(
+            "Bottom Sheets",
+            listOf(
+                ComposableDemo("Bottom Sheet") { BottomSheetScaffoldSample() },
+                ComposableDemo("Modal Bottom Sheet") { ModalBottomSheetSample() },
+            )
+        ),
         ComposableDemo("Buttons & FABs") { ButtonDemo() },
         DemoCategory(
             "Navigation drawer",
@@ -72,7 +78,6 @@
                 ActivityDemo("Dynamic Theme", DynamicThemeActivity::class)
             )
         ),
-        ComposableDemo("Modal bottom sheet") { ModalBottomSheetSample() },
         ComposableDemo("Progress Indicators") { ProgressIndicatorDemo() },
         DemoCategory(
             "Scaffold",
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/TabDemo.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/TabDemo.kt
index 1981844..43a42d1 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/TabDemo.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/TabDemo.kt
@@ -32,6 +32,7 @@
 import androidx.compose.material.samples.ScrollingFancyIndicatorContainerTabs
 import androidx.compose.material.samples.ScrollingTextTabs
 import androidx.compose.material.samples.TextAndIconTabs
+import androidx.compose.material.samples.LeadingIconTabs
 import androidx.compose.material.samples.TextTabs
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
@@ -55,6 +56,8 @@
             Spacer(Modifier.requiredHeight(24.dp))
             TextAndIconTabs()
             Spacer(Modifier.requiredHeight(24.dp))
+            LeadingIconTabs()
+            Spacer(Modifier.requiredHeight(24.dp))
             ScrollingTextTabs()
         } else {
             FancyTabs()
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/DrawerSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/DrawerSamples.kt
index 7175e72..5e3e453 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/DrawerSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/DrawerSamples.kt
@@ -18,20 +18,31 @@
 
 import androidx.annotation.Sampled
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.selection.toggleable
 import androidx.compose.material.BottomDrawer
 import androidx.compose.material.BottomDrawerValue
 import androidx.compose.material.Button
+import androidx.compose.material.Checkbox
 import androidx.compose.material.DrawerValue
 import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Icon
+import androidx.compose.material.ListItem
 import androidx.compose.material.ModalDrawer
 import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.material.rememberBottomDrawerState
 import androidx.compose.material.rememberDrawerState
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -71,28 +82,55 @@
 @Composable
 @OptIn(ExperimentalMaterialApi::class)
 fun BottomDrawerSample() {
-    val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+    val (gesturesEnabled, toggleGesturesEnabled) = remember { mutableStateOf(true) }
     val scope = rememberCoroutineScope()
-    BottomDrawer(
-        drawerState = drawerState,
-        drawerContent = {
-            Button(
-                modifier = Modifier.align(Alignment.CenterHorizontally).padding(top = 16.dp),
-                onClick = { scope.launch { drawerState.close() } },
-                content = { Text("Close Drawer") }
+    Column {
+        Row(
+            modifier = Modifier.fillMaxWidth().toggleable(
+                value = gesturesEnabled,
+                onValueChange = toggleGesturesEnabled
             )
-        },
-        content = {
-            Column(
-                modifier = Modifier.fillMaxSize().padding(16.dp),
-                horizontalAlignment = Alignment.CenterHorizontally
-            ) {
-                Text(text = if (drawerState.isClosed) "▲▲▲ Pull up ▲▲▲" else "▼▼▼ Drag down ▼▼▼")
-                Spacer(Modifier.height(20.dp))
-                Button(onClick = { scope.launch { drawerState.open() } }) {
-                    Text("Click to open")
+        ) {
+            Checkbox(gesturesEnabled, null)
+            Text(text = if (gesturesEnabled) "Gestures Enabled" else "Gestures Disabled")
+        }
+        val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+        BottomDrawer(
+            gesturesEnabled = gesturesEnabled,
+            drawerState = drawerState,
+            drawerContent = {
+                Button(
+                    modifier = Modifier.align(Alignment.CenterHorizontally).padding(top = 16.dp),
+                    onClick = { scope.launch { drawerState.close() } },
+                    content = { Text("Close Drawer") }
+                )
+                LazyColumn {
+                    items(5) {
+                        ListItem(
+                            text = { Text("Item $it") },
+                            icon = {
+                                Icon(
+                                    Icons.Default.Favorite,
+                                    contentDescription = "Localized description"
+                                )
+                            }
+                        )
+                    }
+                }
+            },
+            content = {
+                Column(
+                    modifier = Modifier.fillMaxSize().padding(16.dp),
+                    horizontalAlignment = Alignment.CenterHorizontally
+                ) {
+                    val openText = if (gesturesEnabled) "▲▲▲ Pull up ▲▲▲" else "Click the button!"
+                    Text(text = if (drawerState.isClosed) openText else "▼▼▼ Drag down ▼▼▼")
+                    Spacer(Modifier.height(20.dp))
+                    Button(onClick = { scope.launch { drawerState.open() } }) {
+                        Text("Click to open")
+                    }
                 }
             }
-        }
-    )
+        )
+    }
 }
\ No newline at end of file
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TabSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TabSamples.kt
index e0d0fdd..3d76467 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TabSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TabSamples.kt
@@ -36,6 +36,7 @@
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.ExperimentalMaterialApi
 import androidx.compose.material.Icon
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.ScrollableTabRow
@@ -43,6 +44,7 @@
 import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
 import androidx.compose.material.TabPosition
 import androidx.compose.material.TabRow
+import androidx.compose.material.LeadingIconTab
 import androidx.compose.material.Text
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
@@ -128,6 +130,34 @@
     }
 }
 
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun LeadingIconTabs() {
+    var state by remember { mutableStateOf(0) }
+    val titlesAndIcons = listOf(
+        "TAB" to Icons.Filled.Favorite,
+        "TAB & ICON" to Icons.Filled.Favorite,
+        "TAB 3 WITH LOTS OF TEXT" to Icons.Filled.Favorite
+    )
+    Column {
+        TabRow(selectedTabIndex = state) {
+            titlesAndIcons.forEachIndexed { index, (title, icon) ->
+                LeadingIconTab(
+                    text = { Text(title) },
+                    icon = { Icon(icon, contentDescription = null) },
+                    selected = state == index,
+                    onClick = { state = index }
+                )
+            }
+        }
+        Text(
+            modifier = Modifier.align(Alignment.CenterHorizontally),
+            text = "Leading icon tab ${state + 1} selected",
+            style = MaterialTheme.typography.body1
+        )
+    }
+}
+
 @Composable
 fun ScrollingTextTabs() {
     var state by remember { mutableStateOf(0) }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
index 74681ac..21bfc21 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
@@ -21,18 +21,23 @@
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.GestureScope
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.assert
 import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
 import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
 import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.bottomCenter
+import androidx.compose.ui.test.center
 import androidx.compose.ui.test.centerLeft
 import androidx.compose.ui.test.click
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -41,20 +46,22 @@
 import androidx.compose.ui.test.performClick
 import androidx.compose.ui.test.performGesture
 import androidx.compose.ui.test.performSemanticsAction
-import androidx.compose.ui.test.swipeDown
+import androidx.compose.ui.test.swipe
 import androidx.compose.ui.test.swipeLeft
 import androidx.compose.ui.test.swipeRight
-import androidx.compose.ui.test.swipeUp
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.MediumTest
+import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.runBlocking
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import kotlin.math.roundToInt
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
@@ -64,6 +71,13 @@
     @get:Rule
     val rule = createComposeRule()
 
+    private val bottomDrawerTag = "drawerContentTag"
+    private val shortBottomDrawerHeight = 256.dp
+
+    private fun advanceClock() {
+        rule.mainClock.advanceTimeBy(100_000L)
+    }
+
     @Test
     fun modalDrawer_testOffset_whenOpen() {
         rule.setMaterialContent {
@@ -117,13 +131,70 @@
     }
 
     @Test
-    fun bottomDrawer_testOffset_whenOpen() {
+    fun bottomDrawer_testOffset_shortDrawer_whenClosed() {
+        rule.setMaterialContent {
+            val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+            BottomDrawer(
+                drawerState = drawerState,
+                drawerContent = {
+                    Box(Modifier.fillMaxSize().testTag(bottomDrawerTag))
+                },
+                content = {}
+            )
+        }
+
+        val height = rule.rootHeight()
+        rule.onNodeWithTag(bottomDrawerTag)
+            .assertTopPositionInRootIsEqualTo(height)
+    }
+
+    @Test
+    fun bottomDrawer_testOffset_shortDrawer_whenExpanded() {
+        rule.setMaterialContent {
+            val drawerState = rememberBottomDrawerState(BottomDrawerValue.Expanded)
+            BottomDrawer(
+                drawerState = drawerState,
+                drawerContent = {
+                    Box(Modifier.height(shortBottomDrawerHeight).testTag(bottomDrawerTag))
+                },
+                content = {}
+            )
+        }
+
+        val height = rule.rootHeight()
+        val expectedTop = height - shortBottomDrawerHeight
+        rule.onNodeWithTag(bottomDrawerTag)
+            .assertTopPositionInRootIsEqualTo(expectedTop)
+    }
+
+    @Test
+    fun bottomDrawer_testOffset_tallDrawer_whenClosed() {
+        rule.setMaterialContent {
+            val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+            BottomDrawer(
+                drawerState = drawerState,
+                drawerContent = {
+                    Box(Modifier.fillMaxSize().testTag(bottomDrawerTag))
+                },
+                content = {}
+            )
+        }
+
+        val height = rule.rootHeight()
+        val expectedTop = height
+        rule.onNodeWithTag(bottomDrawerTag)
+            .assertTopPositionInRootIsEqualTo(expectedTop)
+    }
+
+    @Test
+    @Ignore // Disabled until b/178529942 is fixed
+    fun bottomDrawer_testOffset_tallDrawer_whenOpen() {
         rule.setMaterialContent {
             val drawerState = rememberBottomDrawerState(BottomDrawerValue.Open)
             BottomDrawer(
                 drawerState = drawerState,
                 drawerContent = {
-                    Box(Modifier.fillMaxSize().testTag("content"))
+                    Box(Modifier.fillMaxSize().testTag(bottomDrawerTag))
                 },
                 content = {}
             )
@@ -132,26 +203,44 @@
         val width = rule.rootWidth()
         val height = rule.rootHeight()
         val expectedTop = if (width > height) 0.dp else (height / 2)
-        rule.onNodeWithTag("content")
+        rule.onNodeWithTag(bottomDrawerTag)
             .assertTopPositionInRootIsEqualTo(expectedTop)
     }
 
     @Test
-    fun bottomDrawer_testOffset_whenClosed() {
+    fun bottomDrawer_testOffset_tallDrawer_whenExpanded() {
         rule.setMaterialContent {
-            val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+            val drawerState = rememberBottomDrawerState(BottomDrawerValue.Expanded)
             BottomDrawer(
                 drawerState = drawerState,
                 drawerContent = {
-                    Box(Modifier.fillMaxSize().testTag("content"))
+                    Box(Modifier.fillMaxSize().testTag(bottomDrawerTag))
                 },
                 content = {}
             )
         }
 
-        val height = rule.rootHeight()
-        rule.onNodeWithTag("content")
-            .assertTopPositionInRootIsEqualTo(height)
+        val expectedTop = 0.dp
+        rule.onNodeWithTag(bottomDrawerTag)
+            .assertTopPositionInRootIsEqualTo(expectedTop)
+    }
+
+    @Test
+    @SmallTest
+    fun bottomDrawer_hasPaneTitle() {
+        rule.setMaterialContent {
+            BottomDrawer(
+                drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed),
+                drawerContent = {
+                    Box(Modifier.fillMaxSize().testTag(bottomDrawerTag))
+                },
+                content = {}
+            )
+        }
+
+        rule.onNodeWithTag(bottomDrawerTag, useUnmergedTree = true)
+            .onParent()
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.PaneTitle))
     }
 
     @Test
@@ -263,125 +352,6 @@
 
     @Test
     @LargeTest
-    fun bottomDrawer_drawerContent_doesntPropagateClicksWhenOpen(): Unit = runBlocking {
-        var bodyClicks = 0
-        lateinit var drawerState: BottomDrawerState
-        rule.setMaterialContent {
-            drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
-            BottomDrawer(
-                drawerState = drawerState,
-                drawerContent = {
-                    Box(Modifier.fillMaxSize().testTag("Drawer"))
-                },
-                content = {
-                    Box(Modifier.fillMaxSize().clickable { bodyClicks += 1 })
-                }
-            )
-        }
-
-        // Click in the middle of the drawer
-        rule.onNodeWithTag("Drawer").performClick()
-
-        rule.runOnIdle {
-            assertThat(bodyClicks).isEqualTo(1)
-        }
-        drawerState.open()
-
-        // Click on the left-center pixel of the drawer
-        rule.onNodeWithTag("Drawer").performGesture {
-            click(centerLeft)
-        }
-
-        rule.runOnIdle {
-            assertThat(bodyClicks).isEqualTo(1)
-        }
-        drawerState.expand()
-
-        // Click on the left-center pixel of the drawer once again in a new state
-        rule.onNodeWithTag("Drawer").performGesture {
-            click(centerLeft)
-        }
-
-        rule.runOnIdle {
-            assertThat(bodyClicks).isEqualTo(1)
-        }
-    }
-
-    @Test
-    @LargeTest
-    fun bottomDrawer_openAndClose(): Unit = runBlocking {
-        lateinit var drawerState: BottomDrawerState
-        rule.setMaterialContent {
-            drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
-            BottomDrawer(
-                drawerState = drawerState,
-                drawerContent = {
-                    Box(Modifier.fillMaxSize().testTag("drawer"))
-                },
-                content = {}
-            )
-        }
-
-        val width = rule.rootWidth()
-        val height = rule.rootHeight()
-        val topWhenOpened = if (width > height) 0.dp else (height / 2)
-        val topWhenClosed = height
-
-        // Drawer should start in closed state
-        rule.onNodeWithTag("drawer").assertTopPositionInRootIsEqualTo(topWhenClosed)
-
-        // When the drawer state is set to Opened
-        drawerState.open()
-        // Then the drawer should be opened
-        rule.onNodeWithTag("drawer").assertTopPositionInRootIsEqualTo(topWhenOpened)
-
-        // When the drawer state is set to Closed
-        drawerState.close()
-        // Then the drawer should be closed
-        rule.onNodeWithTag("drawer").assertTopPositionInRootIsEqualTo(topWhenClosed)
-    }
-
-    @Test
-    fun bottomDrawer_bodyContent_clickable(): Unit = runBlocking {
-        var drawerClicks = 0
-        var bodyClicks = 0
-        lateinit var drawerState: BottomDrawerState
-        rule.setMaterialContent {
-            drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
-            // emulate click on the screen
-            BottomDrawer(
-                drawerState = drawerState,
-                drawerContent = {
-                    Box(Modifier.fillMaxSize().clickable { drawerClicks += 1 })
-                },
-                content = {
-                    Box(Modifier.testTag("Drawer").fillMaxSize().clickable { bodyClicks += 1 })
-                }
-            )
-        }
-
-        // Click in the middle of the drawer (which is the middle of the body)
-        rule.onNodeWithTag("Drawer").performGesture { click() }
-
-        rule.runOnIdle {
-            assertThat(drawerClicks).isEqualTo(0)
-            assertThat(bodyClicks).isEqualTo(1)
-        }
-
-        drawerState.open()
-        sleep(100) // TODO(147586311): remove this sleep when opening the drawer triggers a wait
-
-        // Click on the bottom-center pixel of the drawer
-        rule.onNodeWithTag("Drawer").performGesture {
-            click(bottomCenter)
-        }
-
-        assertThat(drawerClicks).isEqualTo(1)
-        assertThat(bodyClicks).isEqualTo(1)
-    }
-
-    @Test
-    @LargeTest
     fun modalDrawer_openBySwipe() {
         lateinit var drawerState: DrawerState
         rule.setMaterialContent {
@@ -453,43 +423,6 @@
 
     @Test
     @LargeTest
-    fun bottomDrawer_openBySwipe() {
-        lateinit var drawerState: BottomDrawerState
-        rule.setMaterialContent {
-            drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
-            // emulate click on the screen
-            Box(Modifier.testTag("Drawer")) {
-                BottomDrawer(
-                    drawerState = drawerState,
-                    drawerContent = {
-                        Box(Modifier.fillMaxSize().background(color = Color.Magenta))
-                    },
-                    content = {
-                        Box(Modifier.fillMaxSize().background(color = Color.Red))
-                    }
-                )
-            }
-        }
-        val isLandscape = rule.rootWidth() > rule.rootHeight()
-
-        rule.onNodeWithTag("Drawer")
-            .performGesture { swipeUp() }
-
-        rule.runOnIdle {
-            assertThat(drawerState.currentValue).isEqualTo(
-                if (isLandscape) BottomDrawerValue.Open else BottomDrawerValue.Expanded
-            )
-        }
-
-        rule.onNodeWithTag("Drawer")
-            .performGesture { swipeDown() }
-        rule.runOnIdle {
-            assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Closed)
-        }
-    }
-
-    @Test
-    @LargeTest
     fun modalDrawer_noDismissActionWhenClosed_hasDissmissActionWhenOpen(): Unit = runBlocking {
         lateinit var drawerState: DrawerState
         rule.setMaterialContent {
@@ -526,6 +459,255 @@
     }
 
     @Test
+    fun bottomDrawer_bodyContent_clickable(): Unit = runBlocking {
+        var drawerClicks = 0
+        var bodyClicks = 0
+        lateinit var drawerState: BottomDrawerState
+        rule.setMaterialContent {
+            drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+            // emulate click on the screen
+            BottomDrawer(
+                drawerState = drawerState,
+                drawerContent = {
+                    Box(Modifier.fillMaxSize().clickable { drawerClicks += 1 })
+                },
+                content = {
+                    Box(
+                        Modifier
+                            .testTag(bottomDrawerTag)
+                            .fillMaxSize()
+                            .clickable { bodyClicks += 1 }
+                    )
+                }
+            )
+        }
+
+        // Click in the middle of the drawer (which is the middle of the body)
+        rule.onNodeWithTag(bottomDrawerTag).performGesture { click() }
+
+        rule.runOnIdle {
+            assertThat(drawerClicks).isEqualTo(0)
+            assertThat(bodyClicks).isEqualTo(1)
+        }
+
+        drawerState.open()
+        sleep(100) // TODO(147586311): remove this sleep when opening the drawer triggers a wait
+
+        // Click on the bottom-center pixel of the drawer
+        rule.onNodeWithTag(bottomDrawerTag).performGesture {
+            click(bottomCenter)
+        }
+
+        assertThat(drawerClicks).isEqualTo(1)
+        assertThat(bodyClicks).isEqualTo(1)
+    }
+
+    @Test
+    @LargeTest
+    fun bottomDrawer_drawerContent_doesntPropagateClicksWhenOpen(): Unit = runBlocking {
+        var bodyClicks = 0
+        lateinit var drawerState: BottomDrawerState
+        rule.setMaterialContent {
+            drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+            BottomDrawer(
+                drawerState = drawerState,
+                drawerContent = {
+                    Box(Modifier.fillMaxSize().testTag(bottomDrawerTag))
+                },
+                content = {
+                    Box(Modifier.fillMaxSize().clickable { bodyClicks += 1 })
+                }
+            )
+        }
+
+        // Click in the middle of the drawer
+        rule.onNodeWithTag(bottomDrawerTag).performClick()
+
+        rule.runOnIdle {
+            assertThat(bodyClicks).isEqualTo(1)
+        }
+        drawerState.open()
+
+        // Click on the left-center pixel of the drawer
+        rule.onNodeWithTag(bottomDrawerTag).performGesture {
+            click(centerLeft)
+        }
+
+        rule.runOnIdle {
+            assertThat(bodyClicks).isEqualTo(1)
+        }
+        drawerState.expand()
+
+        // Click on the left-center pixel of the drawer once again in a new state
+        rule.onNodeWithTag(bottomDrawerTag).performGesture {
+            click(centerLeft)
+        }
+
+        rule.runOnIdle {
+            assertThat(bodyClicks).isEqualTo(1)
+        }
+    }
+
+    @Test
+    @LargeTest
+    fun bottomDrawer_openBySwipe_shortDrawer(): Unit = runBlocking {
+        val contentTag = "contentTestTag"
+        lateinit var drawerState: BottomDrawerState
+        rule.setMaterialContent {
+            drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+            BottomDrawer(
+                drawerState = drawerState,
+                drawerContent = {
+                    Box(
+                        Modifier.height(shortBottomDrawerHeight).testTag(bottomDrawerTag)
+                    )
+                },
+                content = { Box(Modifier.fillMaxSize().testTag(contentTag)) }
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Closed)
+        }
+
+        rule.onNodeWithTag(contentTag)
+            .performGesture { swipeUp() }
+
+        advanceClock()
+
+        rule.runOnIdle {
+            assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Expanded)
+        }
+
+        rule.onNodeWithTag(bottomDrawerTag)
+            .performGesture { swipeDown() }
+
+        advanceClock()
+
+        rule.runOnIdle {
+            assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Closed)
+        }
+    }
+
+    @Test
+    @LargeTest
+    fun bottomDrawer_expandBySwipe_tallDrawer(): Unit = runBlocking {
+        val contentTag = "contentTestTag"
+        lateinit var drawerState: BottomDrawerState
+        rule.setMaterialContent {
+            drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+            BottomDrawer(
+                drawerState = drawerState,
+                drawerContent = {
+                    Box(
+                        Modifier.fillMaxSize().testTag(bottomDrawerTag)
+                    )
+                },
+                content = { Box(Modifier.fillMaxSize().testTag(contentTag)) }
+            )
+        }
+
+        val isLandscape = rule.rootWidth() > rule.rootHeight()
+        val peekHeight = with(rule.density) { rule.rootHeight().toPx() / 2 }
+
+        rule.runOnIdle {
+            assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Closed)
+        }
+
+        rule.onNodeWithTag(contentTag)
+            .performGesture { swipeUp(endY = peekHeight) }
+
+        advanceClock()
+
+        rule.runOnIdle {
+            assertThat(drawerState.currentValue).isEqualTo(
+                if (isLandscape) BottomDrawerValue.Expanded else BottomDrawerValue.Open
+            )
+        }
+
+        rule.onNodeWithTag(bottomDrawerTag)
+            .performGesture { swipeUp() }
+
+        advanceClock()
+
+        rule.runOnIdle {
+            assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Expanded)
+        }
+
+        rule.onNodeWithTag(bottomDrawerTag)
+            .performGesture { swipeDown(endY = peekHeight) }
+
+        advanceClock()
+
+        rule.runOnIdle {
+            assertThat(drawerState.currentValue).isEqualTo(
+                if (isLandscape) BottomDrawerValue.Closed else BottomDrawerValue.Open
+            )
+        }
+
+        rule.onNodeWithTag(bottomDrawerTag)
+            .performGesture { swipeDown() }
+
+        advanceClock()
+
+        rule.runOnIdle {
+            assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Closed)
+        }
+    }
+
+    @Test
+    fun bottomDrawer_openBySwipe_onBodyContent(): Unit = runBlocking {
+        val contentTag = "contentTestTag"
+        lateinit var drawerState: BottomDrawerState
+        rule.setMaterialContent {
+            drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+            BottomDrawer(
+                drawerState = drawerState,
+                drawerContent = { Box(Modifier.height(shortBottomDrawerHeight)) },
+                content = { Box(Modifier.fillMaxSize().testTag(contentTag)) }
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Closed)
+        }
+
+        rule.onNodeWithTag(contentTag)
+            .performGesture { swipeUp() }
+
+        advanceClock()
+
+        rule.runOnIdle {
+            assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Expanded)
+        }
+    }
+
+    @Test
+    fun bottomDrawer_hasDismissAction_whenExpanded(): Unit = runBlocking {
+        lateinit var drawerState: BottomDrawerState
+        rule.setMaterialContent {
+            drawerState = rememberBottomDrawerState(BottomDrawerValue.Expanded)
+            BottomDrawer(
+                drawerState = drawerState,
+                drawerContent = {
+                    Box(Modifier.fillMaxSize().testTag(bottomDrawerTag))
+                },
+                content = {}
+            )
+        }
+
+        val height = rule.rootHeight()
+        rule.onNodeWithTag(bottomDrawerTag).onParent()
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
+            .performSemanticsAction(SemanticsActions.Dismiss)
+
+        advanceClock()
+
+        rule.onNodeWithTag(bottomDrawerTag)
+            .assertTopPositionInRootIsEqualTo(height)
+    }
+
+    @Test
     @LargeTest
     fun bottomDrawer_noDismissActionWhenClosed_hasDissmissActionWhenOpen(): Unit = runBlocking {
         lateinit var drawerState: BottomDrawerState
@@ -534,31 +716,164 @@
             BottomDrawer(
                 drawerState = drawerState,
                 drawerContent = {
-                    Box(Modifier.fillMaxSize().testTag("drawer"))
+                    Box(Modifier.fillMaxSize().testTag(bottomDrawerTag))
                 },
                 content = {}
             )
         }
 
         // Drawer should start in closed state and have no dismiss action
-        rule.onNodeWithTag("drawer", useUnmergedTree = true)
+        assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Closed)
+        rule.onNodeWithTag(bottomDrawerTag, useUnmergedTree = true)
             .onParent()
             .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Dismiss))
 
-        // When the drawer state is set to Opened
+        // When the drawer state is set to Open or Expanded
         drawerState.open()
+        assertThat(drawerState.currentValue)
+            .isAnyOf(BottomDrawerValue.Open, BottomDrawerValue.Expanded)
         // Then the drawer should be opened and have dismiss action
-        rule.onNodeWithTag("drawer", useUnmergedTree = true)
+        rule.onNodeWithTag(bottomDrawerTag, useUnmergedTree = true)
             .onParent()
             .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
 
         // When the drawer state is set to Closed using dismiss action
-        rule.onNodeWithTag("drawer", useUnmergedTree = true)
+        rule.onNodeWithTag(bottomDrawerTag, useUnmergedTree = true)
             .onParent()
             .performSemanticsAction(SemanticsActions.Dismiss)
         // Then the drawer should be closed and have no dismiss action
-        rule.onNodeWithTag("drawer", useUnmergedTree = true)
+        rule.onNodeWithTag(bottomDrawerTag, useUnmergedTree = true)
             .onParent()
             .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Dismiss))
     }
+
+    @Test
+    @LargeTest
+    fun bottomDrawer_openAndClose_shortDrawer(): Unit = runBlocking {
+        lateinit var drawerState: BottomDrawerState
+        rule.setMaterialContent {
+            drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+            BottomDrawer(
+                drawerState = drawerState,
+                drawerContent = {
+                    Box(Modifier.height(shortBottomDrawerHeight).testTag(bottomDrawerTag))
+                },
+                content = {}
+            )
+        }
+
+        val height = rule.rootHeight()
+        val topWhenOpened = height - shortBottomDrawerHeight
+        val topWhenExpanded = topWhenOpened
+        val topWhenClosed = height
+
+        // Drawer should start in closed state
+        rule.onNodeWithTag(bottomDrawerTag).assertTopPositionInRootIsEqualTo(topWhenClosed)
+
+        // When the drawer state is set to Opened
+        drawerState.open()
+        // Then the drawer should be opened
+        rule.onNodeWithTag(bottomDrawerTag).assertTopPositionInRootIsEqualTo(topWhenOpened)
+
+        // When the drawer state is set to Expanded
+        drawerState.expand()
+        // Then the drawer should be expanded
+        rule.onNodeWithTag(bottomDrawerTag).assertTopPositionInRootIsEqualTo(topWhenExpanded)
+
+        // When the drawer state is set to Closed
+        drawerState.close()
+        // Then the drawer should be closed
+        rule.onNodeWithTag(bottomDrawerTag).assertTopPositionInRootIsEqualTo(topWhenClosed)
+    }
+
+    @Test
+    @LargeTest
+    fun bottomDrawer_openAndClose_tallDrawer(): Unit = runBlocking {
+        lateinit var drawerState: BottomDrawerState
+        rule.setMaterialContent {
+            drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+            BottomDrawer(
+                drawerState = drawerState,
+                drawerContent = {
+                    Box(Modifier.fillMaxSize().testTag(bottomDrawerTag))
+                },
+                content = {}
+            )
+        }
+
+        val width = rule.rootWidth()
+        val height = rule.rootHeight()
+        val topWhenOpened = if (width > height) 0.dp else (height / 2)
+        val topWhenExpanded = 0.dp
+        val topWhenClosed = height
+
+        // Drawer should start in closed state
+        rule.onNodeWithTag(bottomDrawerTag).assertTopPositionInRootIsEqualTo(topWhenClosed)
+
+        // When the drawer state is set to Opened
+        drawerState.open()
+        // Then the drawer should be opened
+        rule.onNodeWithTag(bottomDrawerTag).assertTopPositionInRootIsEqualTo(topWhenOpened)
+
+        // When the drawer state is set to Expanded
+        drawerState.expand()
+        // Then the drawer should be expanded
+        rule.onNodeWithTag(bottomDrawerTag).assertTopPositionInRootIsEqualTo(topWhenExpanded)
+
+        // When the drawer state is set to Closed
+        drawerState.close()
+        // Then the drawer should be closed
+        rule.onNodeWithTag(bottomDrawerTag).assertTopPositionInRootIsEqualTo(topWhenClosed)
+    }
+
+    /**
+     * Performs a swipe up gesture on the associated node. The gesture starts at [startY] and
+     * ends at [endY].
+     *
+     * @param startY The start position of the gesture. By default, this is slightly above the
+     * bottom of the associated node.
+     * @param endY The end position of the gesture. By default, this is the top of the associated
+     * node.
+     */
+    private fun GestureScope.swipeUp(
+        startY: Float = (visibleSize.height * (1 - edgeFuzzFactor)).roundToInt().toFloat(),
+        endY: Float = 0.0f
+    ) {
+        require(startY >= endY) {
+            "Start position $startY needs to be equal or bigger than end position $endY"
+        }
+        val x = center.x
+        val start = Offset(x, startY)
+        val end = Offset(x, endY)
+        swipe(start, end, 200)
+    }
+
+    /**
+     * Performs a swipe down gesture on the associated node. The gesture starts at [startY] and
+     * ends at [endY].
+     *
+     * @param startY The start position of the gesture. By default, this is slightly below the
+     * top of the associated node.
+     * @param endY The end position of the gesture. By default, this is the bottom of the associated
+     * node.
+     */
+    private fun GestureScope.swipeDown(
+        startY: Float = (visibleSize.height * edgeFuzzFactor).roundToInt().toFloat(),
+        endY: Float = visibleSize.height.toFloat()
+    ) {
+        require(endY >= startY) {
+            "End position $endY needs to be equal or bigger than start position $startY"
+        }
+        val x = center.x
+        val start = Offset(x, startY)
+        val end = Offset(x, endY)
+        swipe(start, end, 200)
+    }
+
+    /**
+     * The distance of a swipe's start position from the node's edge, in terms of the node's length.
+     * We do not start the swipe exactly on the node's edge, but somewhat more inward, since swiping
+     * from the exact edge may behave in an unexpected way (e.g. may open a navigation drawer).
+     */
+    private val edgeFuzzFactor = 0.083f
 }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt
index 0ac6564..c642587 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt
@@ -21,6 +21,8 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
 import androidx.compose.foundation.layout.Box
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.testutils.assertAgainstGolden
@@ -298,6 +300,48 @@
         )
     }
 
+    @Test
+    fun leadingIconTabs_lightTheme_defaultColors() {
+        val interactionSource = MutableInteractionSource()
+
+        var scope: CoroutineScope? = null
+
+        composeTestRule.setContent {
+            scope = rememberCoroutineScope()
+            MaterialTheme(lightColors()) {
+                DefaultLeadingIconTabs(interactionSource)
+            }
+        }
+
+        assertTabsMatch(
+            scope = scope!!,
+            interactionSource = interactionSource,
+            interaction = null,
+            goldenIdentifier = "leadingIconTabs_lightTheme_defaultColors"
+        )
+    }
+
+    @Test
+    fun leadingIconTabs_darkTheme_defaultColors() {
+        val interactionSource = MutableInteractionSource()
+
+        var scope: CoroutineScope? = null
+
+        composeTestRule.setContent {
+            scope = rememberCoroutineScope()
+            MaterialTheme(darkColors()) {
+                DefaultLeadingIconTabs(interactionSource)
+            }
+        }
+
+        assertTabsMatch(
+            scope = scope!!,
+            interactionSource = interactionSource,
+            interaction = null,
+            goldenIdentifier = "leadingIconTabs_darkTheme_defaultColors"
+        )
+    }
+
     /**
      * Asserts that the tabs match the screenshot with identifier [goldenIdentifier].
      *
@@ -410,4 +454,41 @@
     }
 }
 
+/**
+ * Default colored [TabRow] with three [LeadingIconTab]s. The first [LeadingIconTab] is selected,
+ * and the rest are not.
+ *
+ * @param interactionSource the [MutableInteractionSource] for the first [LeadingIconTab], to control its
+ * visual state.
+ */
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+private fun DefaultLeadingIconTabs(
+    interactionSource: MutableInteractionSource
+) {
+    Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
+        TabRow(selectedTabIndex = 0) {
+            LeadingIconTab(
+                text = { Text("TAB") },
+                icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite") },
+                selected = true,
+                interactionSource = interactionSource,
+                onClick = {}
+            )
+            LeadingIconTab(
+                text = { Text("TAB") },
+                icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite") },
+                selected = false,
+                onClick = {}
+            )
+            LeadingIconTab(
+                text = { Text("TAB") },
+                icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite") },
+                selected = false,
+                onClick = {}
+            )
+        }
+    }
+}
+
 private const val Tag = "Tab"
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabTest.kt
index afc4989..b152c14 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabTest.kt
@@ -22,6 +22,7 @@
 import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.samples.LeadingIconTabs
 import androidx.compose.material.samples.ScrollingTextTabs
 import androidx.compose.material.samples.TextTabs
 import androidx.compose.runtime.Composable
@@ -35,8 +36,8 @@
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsProperties
 import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.assert
 import androidx.compose.ui.test.assertCountEquals
@@ -132,6 +133,55 @@
             .assertHasClickAction()
     }
 
+    @OptIn(ExperimentalMaterialApi::class)
+    @Test
+    fun leadingIconTab_defaultSemantics() {
+        rule.setMaterialContent {
+            TabRow(0) {
+                LeadingIconTab(
+                    text = { Text("Text") },
+                    icon = { Icon(icon, null) },
+                    modifier = Modifier.testTag("leadingIconTab"),
+                    selected = true,
+                    onClick = {}
+                )
+            }
+        }
+
+        rule.onNodeWithTag("leadingIconTab")
+            .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Tab))
+            .assertIsSelected()
+            .assertIsEnabled()
+            .assertHasClickAction()
+
+        rule.onNodeWithTag("leadingIconTab")
+            .onParent()
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.SelectableGroup))
+    }
+
+    @OptIn(ExperimentalMaterialApi::class)
+    @Test
+    fun leadingIconTab_disabledSemantics() {
+        rule.setMaterialContent {
+            Box {
+                LeadingIconTab(
+                    enabled = false,
+                    text = { Text("Text") },
+                    icon = { Icon(icon, null) },
+                    modifier = Modifier.testTag("leadingIconTab"),
+                    selected = true,
+                    onClick = {}
+                )
+            }
+        }
+
+        rule.onNodeWithTag("leadingIconTab")
+            .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Tab))
+            .assertIsSelected()
+            .assertIsNotEnabled()
+            .assertHasClickAction()
+    }
+
     @Test
     fun textTab_height() {
         rule
@@ -166,6 +216,23 @@
             .assertHeightIsEqualTo(ExpectedLargeTabHeight)
     }
 
+    @OptIn(ExperimentalMaterialApi::class)
+    @Test
+    fun leadingIconTab_height() {
+        rule
+            .setMaterialContentForSizeAssertions {
+                Surface {
+                    LeadingIconTab(
+                        text = { Text("Text") },
+                        icon = { Icon(icon, null) },
+                        selected = true,
+                        onClick = {}
+                    )
+                }
+            }
+            .assertHeightIsEqualTo(ExpectedSmallTabHeight)
+    }
+
     @Test
     fun fixedTabRow_indicatorPosition() {
         val indicatorHeight = 1.dp
@@ -340,6 +407,47 @@
         baselinePositionY.assertIsEqualTo(expectedPositionY, "baseline y-position")
     }
 
+    @OptIn(ExperimentalMaterialApi::class)
+    @Test
+    fun LeadingIconTab_textAndIconPosition() {
+        rule.setMaterialContent {
+            Box {
+                TabRow(
+                    modifier = Modifier.testTag("tabRow"),
+                    selectedTabIndex = 0
+                ) {
+                    LeadingIconTab(
+                        text = {
+                            Text("TAB", Modifier.testTag("text"))
+                        },
+                        icon = { Icon(Icons.Filled.Favorite, null, Modifier.testTag("icon")) },
+                        selected = true,
+                        onClick = {}
+                    )
+                }
+            }
+        }
+
+        val tabRowBounds =
+            rule.onNodeWithTag("tabRow", useUnmergedTree = true).getUnclippedBoundsInRoot()
+
+        val textBounds =
+            rule.onNodeWithTag("text", useUnmergedTree = true).getUnclippedBoundsInRoot()
+
+        val textDistanceFromIcon = 8.dp
+
+        val iconBounds =
+            rule.onNodeWithTag("icon", useUnmergedTree = true).getUnclippedBoundsInRoot()
+        textBounds.left.assertIsEqualTo(
+            iconBounds.right + textDistanceFromIcon,
+            "textBounds left-position"
+        )
+
+        val iconOffset =
+            (tabRowBounds.width - iconBounds.width - textBounds.width - textDistanceFromIcon) / 2
+        iconBounds.left.assertIsEqualTo(iconOffset, "iconBounds left-position")
+    }
+
     @Test
     fun scrollableTabRow_indicatorPosition() {
         val indicatorHeight = 1.dp
@@ -446,6 +554,52 @@
     }
 
     @Test
+    fun fixedLeadingIconTabRow_initialTabSelected() {
+        rule
+            .setMaterialContent {
+                LeadingIconTabs()
+            }
+
+        // Only the first tab should be selected
+        rule.onAllNodes(isSelectable())
+            .assertCountEquals(3)
+            .apply {
+                get(0).assertIsSelected()
+                get(1).assertIsNotSelected()
+                get(2).assertIsNotSelected()
+            }
+    }
+
+    @Test
+    fun LeadingIconTabRow_selectNewTab() {
+        rule
+            .setMaterialContent {
+                LeadingIconTabs()
+            }
+
+        // Only the first tab should be selected
+        rule.onAllNodes(isSelectable())
+            .assertCountEquals(3)
+            .apply {
+                get(0).assertIsSelected()
+                get(1).assertIsNotSelected()
+                get(2).assertIsNotSelected()
+            }
+
+        // Click the last tab
+        rule.onAllNodes(isSelectable())[2].performClick()
+
+        // Now only the last tab should be selected
+        rule.onAllNodes(isSelectable())
+            .assertCountEquals(3)
+            .apply {
+                get(0).assertIsNotSelected()
+                get(1).assertIsNotSelected()
+                get(2).assertIsSelected()
+            }
+    }
+
+    @Test
     fun scrollableTabRow_initialTabSelected() {
         rule
             .setMaterialContent {
@@ -528,4 +682,29 @@
             assertThat(clicks).isEqualTo(0)
         }
     }
+
+    @OptIn(ExperimentalMaterialApi::class)
+    @Test
+    fun leadingIconTab_disabled_noClicks() {
+        var clicks = 0
+        rule.setMaterialContent {
+            Box {
+                LeadingIconTab(
+                    enabled = false,
+                    text = { Text("Text") },
+                    icon = { Icon(icon, null) },
+                    modifier = Modifier.testTag("tab"),
+                    selected = true,
+                    onClick = { clicks++ }
+                )
+            }
+        }
+
+        rule.onNodeWithTag("tab")
+            .performClick()
+
+        rule.runOnIdle {
+            assertThat(clicks).isEqualTo(0)
+        }
+    }
 }
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
index 48389f1..77c0619 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
@@ -17,6 +17,7 @@
 package androidx.compose.material
 
 import androidx.compose.animation.core.TweenSpec
+import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.detectTapGestures
@@ -30,15 +31,19 @@
 import androidx.compose.foundation.layout.sizeIn
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.saveable.Saver
 import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.semantics.dismiss
@@ -48,9 +53,9 @@
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.lerp
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.launch
+import kotlin.math.max
 import kotlin.math.roundToInt
 
 /**
@@ -164,10 +169,10 @@
     confirmStateChange = confirmStateChange
 ) {
     /**
-     * Whether the drawer is open.
+     * Whether the drawer is open, either in opened or expanded state.
      */
     val isOpen: Boolean
-        get() = currentValue == BottomDrawerValue.Open
+        get() = currentValue != BottomDrawerValue.Closed
 
     /**
      * Whether the drawer is closed.
@@ -183,19 +188,24 @@
 
     /**
      * Open the drawer with animation and suspend until it if fully opened or animation has been
-     * cancelled. This method will throw [CancellationException] if the animation is
-     * interrupted
+     * cancelled. If the content height is less than [BottomDrawerOpenFraction], the drawer state
+     * will move to [BottomDrawerValue.Expanded] instead.
      *
-     * @return the reason the open animation ended
+     * @throws [CancellationException] if the animation is interrupted
+     *
      */
-    suspend fun open() = animateTo(BottomDrawerValue.Open)
+    suspend fun open() {
+        val targetValue =
+            if (isOpenEnabled) BottomDrawerValue.Open else BottomDrawerValue.Expanded
+        animateTo(targetValue)
+    }
 
     /**
      * Close the drawer with animation and suspend until it if fully closed or animation has been
-     * cancelled. This method will throw [CancellationException] if the animation is
-     * interrupted
+     * cancelled.
      *
-     * @return the reason the close animation ended
+     * @throws [CancellationException] if the animation is interrupted
+     *
      */
     suspend fun close() = animateTo(BottomDrawerValue.Closed)
 
@@ -203,10 +213,14 @@
      * Expand the drawer with animation and suspend until it if fully expanded or animation has
      * been cancelled.
      *
-     * @return the reason the expand animation ended
+     * @throws [CancellationException] if the animation is interrupted
+     *
      */
     suspend fun expand() = animateTo(BottomDrawerValue.Expanded)
 
+    private val isOpenEnabled: Boolean
+        get() = anchors.values.contains(BottomDrawerValue.Open)
+
     internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection
 
     companion object {
@@ -377,7 +391,7 @@
  * @sample androidx.compose.material.samples.BottomDrawerSample
  *
  * @param drawerState state of the drawer
- * @param modifier optional modifier for the drawer
+ * @param modifier optional [Modifier] for the entire component
  * @param gesturesEnabled whether or not drawer can be interacted by gestures
  * @param drawerShape shape of the drawer sheet
  * @param drawerElevation drawer sheet elevation. This controls the size of the shadow below the
@@ -390,7 +404,6 @@
  * @param scrimColor color of the scrim that obscures content when the drawer is open
  * @param content content of the rest of the UI
  *
- * @throws IllegalStateException when parent has [Float.POSITIVE_INFINITY] height
  */
 @Composable
 @ExperimentalMaterialApi
@@ -407,85 +420,77 @@
     content: @Composable () -> Unit
 ) {
     val scope = rememberCoroutineScope()
+
     BoxWithConstraints(modifier.fillMaxSize()) {
-        val modalDrawerConstraints = constraints
-        // TODO : think about Infinite max bounds case
-        if (!modalDrawerConstraints.hasBoundedHeight) {
-            throw IllegalStateException("Drawer shouldn't have infinite height")
+        val fullHeight = constraints.maxHeight.toFloat()
+        var drawerHeight by remember(fullHeight) { mutableStateOf(fullHeight) }
+        // TODO(b/178630869) Proper landscape support
+        val isLandscape = constraints.maxWidth > constraints.maxHeight
+
+        val minHeight = 0f
+        val peekHeight = fullHeight * BottomDrawerOpenFraction
+        val expandedHeight = max(minHeight, fullHeight - drawerHeight)
+        val anchors = if (drawerHeight < peekHeight || isLandscape) {
+            mapOf(
+                fullHeight to BottomDrawerValue.Closed,
+                expandedHeight to BottomDrawerValue.Expanded
+            )
+        } else {
+            mapOf(
+                fullHeight to BottomDrawerValue.Closed,
+                peekHeight to BottomDrawerValue.Open,
+                expandedHeight to BottomDrawerValue.Expanded
+            )
         }
 
-        val minValue = 0f
-        val maxValue = modalDrawerConstraints.maxHeight.toFloat()
-
-        // TODO: add proper landscape support
-        val isLandscape = modalDrawerConstraints.maxWidth > modalDrawerConstraints.maxHeight
-        val openValue = if (isLandscape) minValue else lerp(
-            minValue,
-            maxValue,
-            BottomDrawerOpenFraction
-        )
-        val anchors =
-            if (isLandscape) {
-                mapOf(
-                    maxValue to BottomDrawerValue.Closed,
-                    minValue to BottomDrawerValue.Open
-                )
-            } else {
-                mapOf(
-                    maxValue to BottomDrawerValue.Closed,
-                    openValue to BottomDrawerValue.Open,
-                    minValue to BottomDrawerValue.Expanded
-                )
-            }
         val blockClicks = if (drawerState.isClosed) {
             Modifier
         } else {
             Modifier.pointerInput(Unit) { detectTapGestures {} }
         }
-        Box(
+        val drawerConstraints = with(LocalDensity.current) {
             Modifier
-                .nestedScroll(drawerState.nestedScrollConnection)
-                .swipeable(
-                    state = drawerState,
-                    anchors = anchors,
-                    orientation = Orientation.Vertical,
-                    enabled = gesturesEnabled,
-                    resistance = null
+                .sizeIn(
+                    maxWidth = constraints.maxWidth.toDp(),
+                    maxHeight = constraints.maxHeight.toDp()
                 )
-        ) {
-            Box {
-                content()
-            }
-            Scrim(
-                open = drawerState.isOpen,
-                onClose = { scope.launch { drawerState.close() } },
-                fraction = {
-                    // as we scroll "from height to 0" , need to reverse fraction
-                    1 - calculateFraction(openValue, maxValue, drawerState.offset.value)
-                },
-                color = scrimColor
+        }
+        val swipeable = Modifier
+            .nestedScroll(drawerState.nestedScrollConnection)
+            .swipeable(
+                state = drawerState,
+                anchors = anchors,
+                orientation = Orientation.Vertical,
+                enabled = gesturesEnabled,
+                resistance = null
+            )
+
+        Box(swipeable) {
+            content()
+            BottomDrawerScrim(
+                color = scrimColor,
+                onDismiss = { scope.launch { drawerState.close() } },
+                visible = drawerState.targetValue != BottomDrawerValue.Closed
             )
             Surface(
-                modifier = with(LocalDensity.current) {
-                    Modifier.sizeIn(
-                        minWidth = modalDrawerConstraints.minWidth.toDp(),
-                        minHeight = modalDrawerConstraints.minHeight.toDp(),
-                        maxWidth = modalDrawerConstraints.maxWidth.toDp(),
-                        maxHeight = modalDrawerConstraints.maxHeight.toDp()
-                    )
-                }
+                drawerConstraints
+                    .onGloballyPositioned { position ->
+                        drawerHeight = position.size.height.toFloat()
+                    }
                     .semantics {
                         paneTitle = Strings.NavigationMenu
                         if (drawerState.isOpen) {
+                            // TODO(b/180101663) The action currently doesn't return the correct results
                             dismiss(action = { scope.launch { drawerState.close() }; true })
                         }
-                    }.offset { IntOffset(0, drawerState.offset.value.roundToInt()) },
+                    }
+                    .offset { IntOffset(x = 0, y = drawerState.offset.value.roundToInt()) },
                 shape = drawerShape,
                 color = drawerBackgroundColor,
                 contentColor = drawerContentColor,
                 elevation = drawerElevation
             ) {
-                Column(Modifier.fillMaxSize().then(blockClicks), content = drawerContent)
+                Column(blockClicks, content = drawerContent)
             }
         }
     }
@@ -515,6 +520,35 @@
     ((pos - a) / (b - a)).coerceIn(0f, 1f)
 
 @Composable
+private fun BottomDrawerScrim(
+    color: Color,
+    onDismiss: () -> Unit,
+    visible: Boolean
+) {
+    if (color != Color.Transparent) {
+        val alpha by animateFloatAsState(
+            targetValue = if (visible) 1f else 0f,
+            animationSpec = TweenSpec()
+        )
+        val dismissModifier = if (visible) {
+            Modifier.pointerInput(onDismiss) {
+                detectTapGestures { onDismiss() }
+            }
+        } else {
+            Modifier
+        }
+
+        Canvas(
+            Modifier
+                .fillMaxSize()
+                .then(dismissModifier)
+        ) {
+            drawRect(color = color, alpha = alpha)
+        }
+    }
+}
+
+@Composable
 private fun Scrim(
     open: Boolean,
     onClose: () -> Unit,
@@ -543,4 +577,4 @@
 // this is taken from the DrawerLayout's DragViewHelper as a min duration.
 private val AnimationSpec = TweenSpec<Float>(durationMillis = 256)
 
-internal const val BottomDrawerOpenFraction = 0.5f
+private const val BottomDrawerOpenFraction = 0.5f
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
index 6002125..e3ca819 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
@@ -104,6 +104,8 @@
     /**
      * Show the bottom sheet with animation and suspend until it's shown. If half expand is
      * enabled, the bottom sheet will be half expanded. Otherwise it will be fully expanded.
+     *
+     * @throws [CancellationException] if the animation is interrupted
      */
     suspend fun show() {
         val targetValue =
@@ -116,7 +118,7 @@
      * Half expand the bottom sheet if half expand is enabled with animation and suspend until it
      * animation is complete or cancelled
      *
-     * @return the reason the half expand animation ended
+     * @throws [CancellationException] if the animation is interrupted
      */
     internal suspend fun halfExpand() {
         if (!isHalfExpandedEnabled) {
@@ -127,19 +129,17 @@
 
     /**
      * Fully expand the bottom sheet with animation and suspend until it if fully expanded or
-     * animation has been cancelled. This method will throw [CancellationException] if the
-     * animation is interrupted
-     *
-     * @return the reason the expand animation ended
+     * animation has been cancelled.
+     * *
+     * @throws [CancellationException] if the animation is interrupted
      */
     internal suspend fun expand() = animateTo(ModalBottomSheetValue.Expanded)
 
     /**
      * Hide the bottom sheet with animation and suspend until it if fully hidden or animation has
-     * been cancelled. This method will throw [CancellationException] if the animation is
-     * interrupted
+     * been cancelled.
      *
-     * @return the reason the hide animation ended
+     * @throws [CancellationException] if the animation is interrupted
      */
     suspend fun hide() = animateTo(ModalBottomSheetValue.Hidden)
 
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt
index 0d107e4..daec88e 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt
@@ -26,8 +26,12 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredWidth
 import androidx.compose.foundation.selection.selectable
 import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
@@ -73,6 +77,8 @@
  * @param selectedContentColor the color for the content of this tab when selected, and the color
  * of the ripple.
  * @param unselectedContentColor the color for the content of this tab when not selected
+ *
+ * @see LeadingIconTab
  */
 @Composable
 fun Tab(
@@ -106,6 +112,74 @@
 }
 
 /**
+ * A LeadingIconTab represents a single page of content using a text label and an icon in
+ * front of the label.
+ * It represents its selected state by tinting the text label and icon with [selectedContentColor].
+ *
+ * This should typically be used inside of a [TabRow], see the corresponding documentation for
+ * example usage.
+ *
+ * @param selected whether this tab is selected or not
+ * @param onClick the callback to be invoked when this tab is selected
+ * @param text the text label displayed in this tab
+ * @param icon the icon displayed in this tab
+ * @param modifier optional [Modifier] for this tab
+ * @param enabled controls the enabled state of this tab. When `false`, this tab will not
+ * be clickable and will appear disabled to accessibility services.
+ * @param interactionSource the [MutableInteractionSource] representing the different [Interaction]s
+ * present on this tab. You can create and pass in your own remembered [MutableInteractionSource] if
+ * you want to read the [Interaction] and customize the appearance / behavior of this tab
+ * in different [Interaction]s.
+ * @param selectedContentColor the color for the content of this tab when selected, and the color
+ * of the ripple.
+ * @param unselectedContentColor the color for the content of this tab when not selected
+ *
+ * @see Tab
+ */
+@ExperimentalMaterialApi
+@Composable
+fun LeadingIconTab(
+    selected: Boolean,
+    onClick: () -> Unit,
+    text: @Composable (() -> Unit),
+    icon: @Composable (() -> Unit),
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    selectedContentColor: Color = LocalContentColor.current,
+    unselectedContentColor: Color = selectedContentColor.copy(alpha = ContentAlpha.medium)
+) {
+    // The color of the Ripple should always the be selected color, as we want to show the color
+    // before the item is considered selected, and hence before the new contentColor is
+    // provided by TabTransition.
+    val ripple = rememberRipple(bounded = false, color = selectedContentColor)
+
+    TabTransition(selectedContentColor, unselectedContentColor, selected) {
+        Row(
+            modifier = modifier
+                .height(SmallTabHeight)
+                .selectable(
+                    selected = selected,
+                    onClick = onClick,
+                    enabled = enabled,
+                    role = Role.Tab,
+                    interactionSource = interactionSource,
+                    indication = ripple
+                )
+                .padding(horizontal = HorizontalTextPadding)
+                .fillMaxWidth(),
+            horizontalArrangement = Arrangement.Center,
+            verticalAlignment = Alignment.CenterVertically
+        ) {
+            icon()
+            Spacer(Modifier.requiredWidth(TextDistanceFromLeadingIcon))
+            val style = MaterialTheme.typography.button.copy(textAlign = TextAlign.Center)
+            ProvideTextStyle(style, content = text)
+        }
+    }
+}
+
+/**
  * Generic [Tab] overload that is not opinionated about content / color. See the other overload
  * for a Tab that has specific slots for text and / or an icon, as well as providing the correct
  * colors for selected / unselected states.
@@ -372,3 +446,5 @@
 private val DoubleLineTextBaselineWithIcon = 6.dp
 // Distance from the first text baseline to the bottom of the icon in a combined tab
 private val IconDistanceFromBaseline = 20.sp
+// Distance from the end of the leading icon to the start of the text
+private val TextDistanceFromLeadingIcon = 8.dp
diff --git a/compose/runtime/runtime-lint/build.gradle b/compose/runtime/runtime-lint/build.gradle
index aa6a9b5..751acb3 100644
--- a/compose/runtime/runtime-lint/build.gradle
+++ b/compose/runtime/runtime-lint/build.gradle
@@ -24,6 +24,28 @@
     id("kotlin")
 }
 
+// New configuration that allows us to specify a dependency we will include into the resulting
+// jar, since dependencies aren't currently allowed in lint projects included via lintPublish
+// (b/182319899)
+configurations {
+    bundleWithJar
+    testImplementation.extendsFrom bundleWithJar
+    compileOnly.extendsFrom bundleWithJar
+}
+
+jar {
+    dependsOn configurations.bundleWithJar
+    from {
+        configurations.bundleWithJar
+                // The stdlib is already bundled with lint, so no need to include it manually in
+                // the lint.jar
+                .filter( { !(it.name =~ /kotlin-stdlib.*\.jar/ )})
+                .collect {
+                    it.isDirectory() ? it : zipTree(it)
+                }
+    }
+}
+
 dependencies {
     // compileOnly because we use lintChecks and it doesn't allow other types of deps
     // this ugly hack exists because of b/63873667
@@ -33,6 +55,7 @@
         compileOnly(LINT_API_MIN)
     }
     compileOnly(KOTLIN_STDLIB)
+    bundleWithJar(KOTLIN_METADATA_JVM)
 
     testImplementation(KOTLIN_STDLIB)
     testImplementation(LINT_CORE)
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetector.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetector.kt
new file mode 100644
index 0000000..2617bdf
--- /dev/null
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetector.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.runtime.lint
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiJavaFile
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
+import org.jetbrains.uast.kotlin.KotlinULambdaExpression
+import org.jetbrains.uast.kotlin.declarations.KotlinUMethod
+import java.util.EnumSet
+
+/**
+ * [Detector] that checks `async` and `launch` calls to make sure they don't happen inside the
+ * body of a composable function / lambda.
+ */
+class ComposableCoroutineCreationDetector : Detector(), SourceCodeScanner {
+    override fun getApplicableMethodNames() = listOf(AsyncShortName, LaunchShortName)
+
+    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+        val packageName = (method.containingFile as? PsiJavaFile)?.packageName
+        if (packageName != CoroutinePackageName) return
+        val name = method.name
+
+        var expression: UElement? = node
+
+        // Limit the search depth in case of an error - in most cases the depth should be
+        // fairly shallow unless there are many if / else / while statements.
+        var depth = 0
+
+        // Find the parent function / lambda this call expression is inside
+        while (depth < 10) {
+            expression = expression?.uastParent
+
+            // TODO: this won't handle inline functions, but we also don't know if they are
+            // inline when they are defined in bytecode because this information doesn't
+            // exist in PSI. If this information isn't added to PSI / UAST, we would need to
+            // manually parse the @Metadata annotation.
+            when (expression) {
+                // In the body of a lambda
+                is KotlinULambdaExpression -> {
+                    if (expression.isComposable) {
+                        context.report(
+                            CoroutineCreationDuringComposition,
+                            node,
+                            context.getNameLocation(node),
+                            "Calls to $name should happen inside a LaunchedEffect and " +
+                                "not composition"
+                        )
+                        return
+                    }
+                    val parent = expression.uastParent
+                    if (parent is KotlinUFunctionCallExpression && parent.isDeclarationInline) {
+                        // We are now in a non-composable lambda parameter inside an inline function
+                        // For example, a scoping function such as run {} or apply {} - since the
+                        // body will be inlined and this is a common case, try to see if there is
+                        // a parent composable function above us, since it is still most likely
+                        // an error to call these methods inside an inline function, inside a
+                        // Composable function.
+                        continue
+                    } else {
+                        return
+                    }
+                }
+                // In the body of a function
+                is KotlinUMethod -> {
+                    if (expression.hasAnnotation("androidx.compose.runtime.Composable")) {
+                        context.report(
+                            CoroutineCreationDuringComposition,
+                            node,
+                            context.getNameLocation(node),
+                            "Calls to $name should happen inside a LaunchedEffect and " +
+                                "not composition"
+                        )
+                    }
+                    return
+                }
+            }
+            depth++
+        }
+    }
+
+    companion object {
+        val CoroutineCreationDuringComposition = Issue.create(
+            "CoroutineCreationDuringComposition",
+            "Calls to `async` or `launch` should happen inside a LaunchedEffect and not " +
+                "composition",
+            "Creating a coroutine with `async` or `launch` during composition is often incorrect " +
+                "- this means that a coroutine will be created even if the composition fails / is" +
+                " rolled back, and it also means that multiple coroutines could end up mutating " +
+                "the same state, causing inconsistent results. Instead, use `LaunchedEffect` and " +
+                "create coroutines inside the suspending block. The block will only run after a " +
+                "successful composition, and will cancel existing coroutines when `key` changes, " +
+                "allowing correct cleanup.",
+            Category.CORRECTNESS, 3, Severity.ERROR,
+            Implementation(
+                ComposableCoroutineCreationDetector::class.java,
+                EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
+            )
+        )
+    }
+}
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt
index 4e93399..85272a1 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt
@@ -74,8 +74,9 @@
 
                 val typeReference = ktParameter.typeReference!!
 
-                // Ideally this annotation should be available on the PsiType itself
-                // https://youtrack.jetbrains.com/issue/KT-45244
+                // Currently type annotations don't appear on the psiType in the version of
+                // UAST / PSI we are using, so we have to look through the type reference.
+                // Should be fixed when Lint upgrades the version to 1.4.30+.
                 val hasComposableAnnotationOnType = typeReference.annotationEntries.any {
                     (it.toUElement() as UAnnotation).qualifiedName == ComposableFqn
                 }
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt
index a860b1a..b550b50 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:Suppress("UnstableApiUsage")
+
 package androidx.compose.runtime.lint
 
 import com.android.tools.lint.client.api.IssueRegistry
@@ -27,10 +29,11 @@
     override val api = 8
     override val minApi = CURRENT_API
     override val issues get() = listOf(
-        CompositionLocalNamingDetector.CompositionLocalNaming,
+        ComposableCoroutineCreationDetector.CoroutineCreationDuringComposition,
         ComposableLambdaParameterDetector.ComposableLambdaParameterNaming,
         ComposableLambdaParameterDetector.ComposableLambdaParameterPosition,
         ComposableNamingDetector.ComposableNaming,
+        CompositionLocalNamingDetector.CompositionLocalNaming,
         RememberDetector.RememberReturnType
     )
 }
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/Utils.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/Utils.kt
index 6d8f4d6..6d78e36 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/Utils.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/Utils.kt
@@ -16,18 +16,234 @@
 
 package androidx.compose.runtime.lint
 
+import com.intellij.lang.jvm.annotation.JvmAnnotationArrayValue
+import com.intellij.lang.jvm.annotation.JvmAnnotationAttributeValue
+import com.intellij.lang.jvm.annotation.JvmAnnotationConstantValue
+import com.intellij.psi.PsiAnnotation
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.impl.compiled.ClsMemberImpl
+import com.intellij.psi.impl.compiled.ClsMethodImpl
+import com.intellij.psi.impl.compiled.ClsParameterImpl
+import com.intellij.psi.util.ClassUtil
+import kotlinx.metadata.Flag
+import kotlinx.metadata.KmDeclarationContainer
+import kotlinx.metadata.KmFunction
+import kotlinx.metadata.jvm.KotlinClassHeader
+import kotlinx.metadata.jvm.KotlinClassMetadata
+import kotlinx.metadata.jvm.annotations
+import kotlinx.metadata.jvm.signature
+import org.jetbrains.kotlin.lexer.KtTokens.INLINE_KEYWORD
+import org.jetbrains.kotlin.psi.KtNamedFunction
+import org.jetbrains.kotlin.psi.KtParameter
+import org.jetbrains.kotlin.psi.KtTypeReference
+import org.jetbrains.uast.UAnnotation
+import org.jetbrains.uast.ULambdaExpression
 import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UParameter
+import org.jetbrains.uast.getContainingUMethod
+import org.jetbrains.uast.getParameterForArgument
+import org.jetbrains.uast.kotlin.AbstractKotlinUVariable
+import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
+import org.jetbrains.uast.resolveToUElement
+import org.jetbrains.uast.toUElement
 
 // TODO: KotlinUMethodWithFakeLightDelegate.hasAnnotation() returns null for some reason, so just
 // look at the annotations directly
 // TODO: annotations is deprecated but the replacement uAnnotations isn't available on the
 // version of lint / uast we compile against
+/**
+ * Returns whether this method is @Composable or not
+ */
 @Suppress("DEPRECATION")
-val UMethod.isComposable get() = annotations.any { it.qualifiedName == ComposableFqn }
+val UMethod.isComposable
+    get() = annotations.any { it.qualifiedName == ComposableFqn }
+
+/**
+ * Returns whether this parameter's type is @Composable or not
+ */
+val UParameter.isComposable: Boolean
+    get() = when (val source = sourcePsi) {
+        // The parameter is defined in Kotlin source
+        is KtParameter -> source.typeReference!!.isComposable
+        // The parameter is in a class file. Currently type annotations aren't added to the
+        // underlying type (https://youtrack.jetbrains.com/issue/KT-45307), so instead we use the
+        // metadata annotation.
+        is ClsParameterImpl -> {
+            // Find the containing method, so we can get metadata from the containing class
+            val containingMethod = getContainingUMethod()!!.sourcePsi as ClsMethodImpl
+            val declarationContainer = containingMethod.getKmDeclarationContainer()
+            val kmFunction = declarationContainer?.findKmFunctionForPsiMethod(containingMethod)
+
+            val kmValueParameter = kmFunction?.valueParameters?.find {
+                it.name == name
+            }
+
+            kmValueParameter?.type?.annotations?.find {
+                it.className == KmComposableFqn
+            } != null
+        }
+        // The parameter is in Java source / other, ignore
+        else -> false
+    }
+
+/**
+ * Returns whether this type reference is @Composable or not
+ */
+val KtTypeReference.isComposable: Boolean
+    // This annotation should be available on the PsiType itself in 1.4.30+, but we are
+    // currently on an older version of UAST / Kotlin embedded compiled
+    // (https://youtrack.jetbrains.com/issue/KT-45244)
+    get() = annotationEntries.any {
+        (it.toUElement() as UAnnotation).qualifiedName == ComposableFqn
+    }
+
+/**
+ * Returns whether this lambda expression is @Composable or not
+ */
+val ULambdaExpression.isComposable: Boolean
+    get() {
+        when (val lambdaParent = uastParent) {
+            // Function call with a lambda parameter
+            is KotlinUFunctionCallExpression -> {
+                val parameter = lambdaParent.getParameterForArgument(this) ?: return false
+                if (!(parameter.toUElement() as UParameter).isComposable) return false
+            }
+            // A local / non-local lambda variable
+            is AbstractKotlinUVariable -> {
+                val hasComposableAnnotationOnLambda = findAnnotation(ComposableFqn) != null
+                val hasComposableAnnotationOnType =
+                    (lambdaParent.typeReference?.sourcePsi as? KtTypeReference)
+                        ?.isComposable == true
+
+                if (!hasComposableAnnotationOnLambda && !hasComposableAnnotationOnType) return false
+            }
+            // This probably shouldn't be called, but safe return in case a new UAST type is added
+            // in the future
+            else -> return false
+        }
+        return true
+    }
+
+/**
+ * @return whether the resolved declaration for this call expression is an inline function
+ */
+val KotlinUFunctionCallExpression.isDeclarationInline: Boolean
+    get() {
+        return when (val source = resolveToUElement()?.sourcePsi) {
+            // Parsing a method defined in a class file
+            is ClsMethodImpl -> {
+                val declarationContainer = source.getKmDeclarationContainer()
+
+                val flags = declarationContainer
+                    ?.findKmFunctionForPsiMethod(source)?.flags ?: return false
+                return Flag.Function.IS_INLINE(flags)
+            }
+            // Parsing a method defined in Kotlin source
+            is KtNamedFunction -> {
+                source.hasModifier(INLINE_KEYWORD)
+            }
+            // Parsing something else (such as a property) which cannot be inline
+            else -> false
+        }
+    }
+
+// TODO: https://youtrack.jetbrains.com/issue/KT-45310
+// Currently there is no built in support for parsing kotlin metadata from kotlin class files, so
+// we need to manually inspect the annotations and work with Cls* (compiled PSI).
+/**
+ * Returns the [KmDeclarationContainer] using the kotlin.Metadata annotation present on the
+ * surrounding class. Returns null if there is no surrounding annotation (not parsing a Kotlin
+ * class file), the annotation data is for an unsupported version of Kotlin, or if the metadata
+ * represents a synthetic
+ */
+private fun ClsMemberImpl<*>.getKmDeclarationContainer(): KmDeclarationContainer? {
+    val classKotlinMetadataAnnotation = containingClass?.annotations?.find {
+        // hasQualifiedName() not available on the min version of Lint we compile against
+        it.qualifiedName == KotlinMetadataFqn
+    } ?: return null
+
+    val metadata = KotlinClassMetadata.read(classKotlinMetadataAnnotation.toHeader())
+        ?: return null
+
+    return when (metadata) {
+        is KotlinClassMetadata.Class -> metadata.toKmClass()
+        is KotlinClassMetadata.FileFacade -> metadata.toKmPackage()
+        is KotlinClassMetadata.SyntheticClass -> null
+        is KotlinClassMetadata.MultiFileClassFacade -> null
+        is KotlinClassMetadata.MultiFileClassPart -> metadata.toKmPackage()
+        is KotlinClassMetadata.Unknown -> null
+    }
+}
+
+/**
+ * Returns a [KotlinClassHeader] by parsing the attributes of this @kotlin.Metadata annotation.
+ *
+ * See: https://github.com/udalov/kotlinx-metadata-examples/blob/master/src/main/java
+ * /examples/FindKotlinGeneratedMethods.java
+ */
+private fun PsiAnnotation.toHeader(): KotlinClassHeader {
+    val attributes = attributes.associate { it.attributeName to it.attributeValue }
+
+    fun JvmAnnotationAttributeValue.parseString(): String =
+        (this as JvmAnnotationConstantValue).constantValue as String
+
+    fun JvmAnnotationAttributeValue.parseInt(): Int =
+        (this as JvmAnnotationConstantValue).constantValue as Int
+
+    fun JvmAnnotationAttributeValue.parseStringArray(): Array<String> =
+        (this as JvmAnnotationArrayValue).values.map {
+            it.parseString()
+        }.toTypedArray()
+
+    fun JvmAnnotationAttributeValue.parseIntArray(): IntArray =
+        (this as JvmAnnotationArrayValue).values.map {
+            it.parseInt()
+        }.toTypedArray().toIntArray()
+
+    val kind = attributes["k"]?.parseInt()
+    val metadataVersion = attributes["mv"]?.parseIntArray()
+    val bytecodeVersion = attributes["bv"]?.parseIntArray()
+    val data1 = attributes["d1"]?.parseStringArray()
+    val data2 = attributes["d2"]?.parseStringArray()
+    val extraString = attributes["xs"]?.parseString()
+    val packageName = attributes["pn"]?.parseString()
+    val extraInt = attributes["xi"]?.parseInt()
+
+    return KotlinClassHeader(
+        kind,
+        metadataVersion,
+        bytecodeVersion,
+        data1,
+        data2,
+        extraString,
+        packageName,
+        extraInt
+    )
+}
+
+/**
+ * @return the corresponding [KmFunction] in [this] for the given [method], matching by name and
+ * signature.
+ */
+private fun KmDeclarationContainer.findKmFunctionForPsiMethod(method: PsiMethod): KmFunction? {
+    val expectedName = method.name
+    val expectedSignature = ClassUtil.getAsmMethodSignature(method)
+
+    return functions.find {
+        it.name == expectedName && it.signature?.desc == expectedSignature
+    }
+}
 
 const val RuntimePackageName = "androidx.compose.runtime"
 
 const val ComposableFqn = "$RuntimePackageName.Composable"
-val ComposableShortName = ComposableFqn.split(".").last()
+// kotlinx.metadata represents separators as `/` instead of `.`
+val KmComposableFqn get() = ComposableFqn.replace(".", "/")
 
 const val RememberShortName = "remember"
+
+const val CoroutinePackageName = "kotlinx.coroutines"
+const val AsyncShortName = "async"
+const val LaunchShortName = "launch"
+
+private const val KotlinMetadataFqn = "kotlin.Metadata"
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetectorTest.kt
new file mode 100644
index 0000000..bd2f0bb
--- /dev/null
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetectorTest.kt
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.runtime.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/* ktlint-disable max-line-length */
+@RunWith(JUnit4::class)
+
+// TODO: add tests for methods defined in class files when we update Lint to support bytecode()
+//  test files
+
+/**
+ * Test for [ComposableCoroutineCreationDetector].
+ */
+class ComposableCoroutineCreationDetectorTest : LintDetectorTest() {
+    override fun getDetector(): Detector = ComposableCoroutineCreationDetector()
+
+    override fun getIssues(): MutableList<Issue> =
+        mutableListOf(ComposableCoroutineCreationDetector.CoroutineCreationDuringComposition)
+
+    @Test
+    fun errors() {
+        lint().files(
+            kotlin(
+                """
+                package androidx.compose.runtime.foo
+
+                import androidx.compose.runtime.Composable
+                import kotlinx.coroutines.*
+
+                @Composable
+                fun Test() {
+                    CoroutineScope.async {}
+                    CoroutineScope.launch {}
+                }
+
+                val lambda = @Composable {
+                    CoroutineScope.async {}
+                    CoroutineScope.launch {}
+                }
+
+                val lambda2: @Composable () -> Unit = {
+                    CoroutineScope.async {}
+                    CoroutineScope.launch {}
+                }
+
+                @Composable
+                fun LambdaParameter(content: @Composable () -> Unit) {}
+
+                @Composable
+                fun Test2() {
+                    LambdaParameter(content = {
+                        CoroutineScope.async {}
+                        CoroutineScope.launch {}
+                    })
+                    LambdaParameter {
+                        CoroutineScope.async {}
+                        CoroutineScope.launch {}
+                    }
+                }
+
+                fun test3() {
+                    val localLambda1 = @Composable {
+                        CoroutineScope.async {}
+                        CoroutineScope.launch {}
+                    }
+
+                    val localLambda2: @Composable () -> Unit = {
+                        CoroutineScope.async {}
+                        CoroutineScope.launch {}
+                    }
+                }
+            """
+            ),
+            composableStub,
+            coroutineBuildersStub
+        )
+            .run()
+            .expect(
+                """
+src/androidx/compose/runtime/foo/test.kt:9: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                    CoroutineScope.async {}
+                                   ~~~~~
+src/androidx/compose/runtime/foo/test.kt:10: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                    CoroutineScope.launch {}
+                                   ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:14: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                    CoroutineScope.async {}
+                                   ~~~~~
+src/androidx/compose/runtime/foo/test.kt:15: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                    CoroutineScope.launch {}
+                                   ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:19: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                    CoroutineScope.async {}
+                                   ~~~~~
+src/androidx/compose/runtime/foo/test.kt:20: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                    CoroutineScope.launch {}
+                                   ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:29: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                        CoroutineScope.async {}
+                                       ~~~~~
+src/androidx/compose/runtime/foo/test.kt:30: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                        CoroutineScope.launch {}
+                                       ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:33: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                        CoroutineScope.async {}
+                                       ~~~~~
+src/androidx/compose/runtime/foo/test.kt:34: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                        CoroutineScope.launch {}
+                                       ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:40: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                        CoroutineScope.async {}
+                                       ~~~~~
+src/androidx/compose/runtime/foo/test.kt:41: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                        CoroutineScope.launch {}
+                                       ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:45: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                        CoroutineScope.async {}
+                                       ~~~~~
+src/androidx/compose/runtime/foo/test.kt:46: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                        CoroutineScope.launch {}
+                                       ~~~~~~
+14 errors, 0 warnings
+            """
+            )
+    }
+
+    @Test
+    fun errors_inlineFunctions() {
+        lint().files(
+            kotlin(
+                """
+                package androidx.compose.runtime.foo
+
+                import androidx.compose.runtime.Composable
+                import kotlinx.coroutines.*
+
+                @Composable
+                fun Test() {
+                    run {
+                        CoroutineScope.async {}
+                        CoroutineScope.launch {}
+                    }
+                }
+
+                val lambda = @Composable {
+                    run {
+                        CoroutineScope.async {}
+                        CoroutineScope.launch {}
+                    }
+                }
+
+                val lambda2: @Composable () -> Unit = {
+                    run {
+                        CoroutineScope.async {}
+                        CoroutineScope.launch {}
+                    }
+                }
+
+                @Composable
+                fun LambdaParameter(content: @Composable () -> Unit) {}
+
+                @Composable
+                fun Test2() {
+                    LambdaParameter(content = {
+                        run {
+                            CoroutineScope.async {}
+                            CoroutineScope.launch {}
+                        }
+                    })
+                    LambdaParameter {
+                        run {
+                            CoroutineScope.async {}
+                            CoroutineScope.launch {}
+                        }
+                    }
+                }
+
+                fun test3() {
+                    val localLambda1 = @Composable {
+                        run {
+                            CoroutineScope.async {}
+                            CoroutineScope.launch {}
+                        }
+                    }
+
+                    val localLambda2: @Composable () -> Unit = {
+                        run {
+                            CoroutineScope.async {}
+                            CoroutineScope.launch {}
+                        }
+                    }
+                }
+            """
+            ),
+            composableStub,
+            coroutineBuildersStub
+        )
+            .run()
+            .expect(
+                """
+src/androidx/compose/runtime/foo/test.kt:10: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                        CoroutineScope.async {}
+                                       ~~~~~
+src/androidx/compose/runtime/foo/test.kt:11: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                        CoroutineScope.launch {}
+                                       ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:17: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                        CoroutineScope.async {}
+                                       ~~~~~
+src/androidx/compose/runtime/foo/test.kt:18: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                        CoroutineScope.launch {}
+                                       ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:24: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                        CoroutineScope.async {}
+                                       ~~~~~
+src/androidx/compose/runtime/foo/test.kt:25: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                        CoroutineScope.launch {}
+                                       ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:36: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                            CoroutineScope.async {}
+                                           ~~~~~
+src/androidx/compose/runtime/foo/test.kt:37: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                            CoroutineScope.launch {}
+                                           ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:42: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                            CoroutineScope.async {}
+                                           ~~~~~
+src/androidx/compose/runtime/foo/test.kt:43: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                            CoroutineScope.launch {}
+                                           ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:51: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                            CoroutineScope.async {}
+                                           ~~~~~
+src/androidx/compose/runtime/foo/test.kt:52: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                            CoroutineScope.launch {}
+                                           ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:58: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                            CoroutineScope.async {}
+                                           ~~~~~
+src/androidx/compose/runtime/foo/test.kt:59: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+                            CoroutineScope.launch {}
+                                           ~~~~~~
+14 errors, 0 warnings
+            """
+            )
+    }
+
+    @Test
+    fun noErrors() {
+        lint().files(
+            kotlin(
+                """
+                package androidx.compose.runtime.foo
+
+                import androidx.compose.runtime.Composable
+                import kotlinx.coroutines.*
+
+                fun test() {
+                    CoroutineScope.async {}
+                    CoroutineScope.launch {}
+                }
+
+                val lambda = {
+                    CoroutineScope.async {}
+                    CoroutineScope.launch {}
+                }
+
+                val lambda2: () -> Unit = {
+                    CoroutineScope.async {}
+                    CoroutineScope.launch {}
+                }
+
+                fun lambdaParameter(action: () -> Unit) {}
+
+                fun test2() {
+                    lambdaParameter(action = {
+                        CoroutineScope.async {}
+                        CoroutineScope.launch {}
+                    })
+                    lambdaParameter {
+                        CoroutineScope.async {}
+                        CoroutineScope.launch {}
+                    }
+                }
+
+                fun test3() {
+                    val localLambda1 = {
+                        CoroutineScope.async {}
+                        CoroutineScope.launch {}
+                    }
+
+                    val localLambda2: () -> Unit = {
+                        CoroutineScope.async {}
+                        CoroutineScope.launch {}
+                    }
+                }
+            """
+            ),
+            composableStub,
+            coroutineBuildersStub
+        )
+            .run()
+            .expectClean()
+    }
+}
+/* ktlint-enable max-line-length */
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/Stubs.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/Stubs.kt
index 9c086d2..83372e1 100644
--- a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/Stubs.kt
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/Stubs.kt
@@ -73,3 +73,19 @@
         ): V = calculation()
     """
 )
+
+val coroutineBuildersStub: LintDetectorTest.TestFile = LintDetectorTest.kotlin(
+    """
+        package kotlinx.coroutines
+
+        object CoroutineScope
+
+        fun CoroutineScope.async(
+            block: suspend CoroutineScope.() -> Unit
+        ) {}
+
+        fun CoroutineScope.launch(
+            block: suspend CoroutineScope.() -> Unit
+        ) {}
+    """
+)
diff --git a/compose/runtime/runtime-saveable/samples/src/main/java/androidx/compose/runtime/saveable/samples/SaveableStateHolderSamples.kt b/compose/runtime/runtime-saveable/samples/src/main/java/androidx/compose/runtime/saveable/samples/SaveableStateHolderSamples.kt
index 8c5feb7..d3bdb3b 100644
--- a/compose/runtime/runtime-saveable/samples/src/main/java/androidx/compose/runtime/saveable/samples/SaveableStateHolderSamples.kt
+++ b/compose/runtime/runtime-saveable/samples/src/main/java/androidx/compose/runtime/saveable/samples/SaveableStateHolderSamples.kt
@@ -46,13 +46,13 @@
         modifier: Modifier = Modifier,
         content: @Composable (T) -> Unit
     ) {
-        // create RestorableStateHolder.
-        val restorableStateHolder = rememberSaveableStateHolder()
-        // wrap the content representing the `screen` key inside `SaveableStateProvider`.
-        // you can add screen switch animations where during the animation multiple screens
-        // will displayed at the same time.
+        // create SaveableStateHolder.
+        val saveableStateHolder = rememberSaveableStateHolder()
         Box(modifier) {
-            restorableStateHolder.SaveableStateProvider(currentScreen) {
+            // Wrap the content representing the `currentScreen` inside `SaveableStateProvider`.
+            // Here you can also add a screen switch animation like Crossfade where during the
+            // animation multiple screens will be displayed at the same time.
+            saveableStateHolder.SaveableStateProvider(currentScreen) {
                 content(currentScreen)
             }
         }
diff --git a/compose/runtime/runtime/api/1.0.0-beta02.txt b/compose/runtime/runtime/api/1.0.0-beta02.txt
index a1c11df..d73a573 100644
--- a/compose/runtime/runtime/api/1.0.0-beta02.txt
+++ b/compose/runtime/runtime/api/1.0.0-beta02.txt
@@ -456,6 +456,9 @@
     method public static boolean isLiveLiteralsEnabled();
   }
 
+  public final class ThreadMapKt {
+  }
+
 }
 
 package androidx.compose.runtime.snapshots {
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index a1c11df..d73a573 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -456,6 +456,9 @@
     method public static boolean isLiveLiteralsEnabled();
   }
 
+  public final class ThreadMapKt {
+  }
+
 }
 
 package androidx.compose.runtime.snapshots {
diff --git a/compose/runtime/runtime/api/public_plus_experimental_1.0.0-beta02.txt b/compose/runtime/runtime/api/public_plus_experimental_1.0.0-beta02.txt
index f900999d..c803652 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_1.0.0-beta02.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_1.0.0-beta02.txt
@@ -544,6 +544,9 @@
     property public abstract int parameters;
   }
 
+  public final class ThreadMapKt {
+  }
+
 }
 
 package androidx.compose.runtime.snapshots {
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index f900999d..c803652 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -544,6 +544,9 @@
     property public abstract int parameters;
   }
 
+  public final class ThreadMapKt {
+  }
+
 }
 
 package androidx.compose.runtime.snapshots {
diff --git a/compose/runtime/runtime/api/restricted_1.0.0-beta02.txt b/compose/runtime/runtime/api/restricted_1.0.0-beta02.txt
index a334aa4..d80e38d 100644
--- a/compose/runtime/runtime/api/restricted_1.0.0-beta02.txt
+++ b/compose/runtime/runtime/api/restricted_1.0.0-beta02.txt
@@ -483,6 +483,9 @@
     method public static boolean isLiveLiteralsEnabled();
   }
 
+  public final class ThreadMapKt {
+  }
+
 }
 
 package androidx.compose.runtime.snapshots {
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index a334aa4..d80e38d 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -483,6 +483,9 @@
     method public static boolean isLiveLiteralsEnabled();
   }
 
+  public final class ThreadMapKt {
+  }
+
 }
 
 package androidx.compose.runtime.snapshots {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt
index 2189a52..91193ee 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt
@@ -26,6 +26,20 @@
 
 internal fun <T> ThreadLocal() = ThreadLocal<T?> { null }
 
+/**
+ * This is similar to a [ThreadLocal] but has lower overhead because it avoids a weak reference.
+ * This should only be used when the writes are delimited by a try...finally call that will clean
+ * up the reference such as [androidx.compose.runtime.snapshots.Snapshot.enter] else the reference
+ * could get pinned by the thread local causing a leak.
+ *
+ * [ThreadLocal] can be used to implement the actual for platforms that do not exhibit the same
+ * overhead for thread locals as the JVM and ART.
+ */
+internal expect class SnapshotThreadLocal<T>() {
+    fun get(): T?
+    fun set(value: T?)
+}
+
 internal expect fun identityHashCode(instance: Any?): Int
 
 @PublishedApi
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index 2f73c00..2927d96 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -20,7 +20,7 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.InternalComposeApi
-import androidx.compose.runtime.ThreadLocal
+import androidx.compose.runtime.SnapshotThreadLocal
 import androidx.compose.runtime.synchronized
 
 /**
@@ -1355,7 +1355,10 @@
  */
 private const val INVALID_SNAPSHOT = 0
 
-private val threadSnapshot = ThreadLocal<Snapshot>()
+/**
+ * Current thread snapshot
+ */
+private val threadSnapshot = SnapshotThreadLocal<Snapshot>()
 
 // A global synchronization object. This synchronization object should be taken before modifying any
 // of the fields below.
diff --git a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.jvm.kt b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.jvm.kt
index 7aa0d96..63dbcb1 100644
--- a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.jvm.kt
+++ b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.jvm.kt
@@ -16,6 +16,9 @@
 
 package androidx.compose.runtime
 
+import androidx.compose.runtime.internal.ThreadMap
+import androidx.compose.runtime.internal.emptyThreadMap
+
 internal actual typealias AtomicReference<V> = java.util.concurrent.atomic.AtomicReference<V>
 
 internal actual open class ThreadLocal<T> actual constructor(
@@ -35,6 +38,23 @@
     }
 }
 
+internal actual class SnapshotThreadLocal<T> {
+    private val map = AtomicReference<ThreadMap>(emptyThreadMap)
+    private val writeMutex = Any()
+
+    @Suppress("UNCHECKED_CAST")
+    actual fun get(): T? = map.get().get(Thread.currentThread().id) as T?
+
+    actual fun set(value: T?) {
+        val key = Thread.currentThread().id
+        synchronized(writeMutex) {
+            val current = map.get()
+            if (current.trySet(key, value)) return
+            map.set(current.newWith(key, value))
+        }
+    }
+}
+
 internal actual fun identityHashCode(instance: Any?): Int = System.identityHashCode(instance)
 
 @PublishedApi
diff --git a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/internal/ThreadMap.kt b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/internal/ThreadMap.kt
new file mode 100644
index 0000000..d4645eb
--- /dev/null
+++ b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/internal/ThreadMap.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.internal
+
+internal class ThreadMap(
+    private val size: Int,
+    private val keys: LongArray,
+    private val values: Array<Any?>
+) {
+    fun get(key: Long): Any? {
+        val index = find(key)
+        return if (index >= 0) values[index] else null
+    }
+
+    /**
+     * Set the value if it is already in the map. Otherwise a new map must be allocated to contain
+     * the new entry.
+     */
+    fun trySet(key: Long, value: Any?): Boolean {
+        val index = find(key)
+        if (index < 0) return false
+        values[index] = value
+        return true
+    }
+
+    fun newWith(key: Long, value: Any?): ThreadMap {
+        val size = size
+        val newSize = values.count { it != null } + 1
+        val newKeys = LongArray(newSize)
+        val newValues = arrayOfNulls<Any?>(newSize)
+        if (newSize > 1) {
+            var dest = 0
+            var source = 0
+            while (dest < newSize && source < size) {
+                val oldKey = keys[source]
+                val oldValue = values[source]
+                if (oldKey > key) {
+                    newKeys[dest] = key
+                    newValues[dest] = value
+                    dest++
+                    // Continue with a loop without this check
+                    break
+                }
+                if (oldValue != null) {
+                    newKeys[dest] = oldKey
+                    newValues[dest] = oldValue
+                    dest++
+                }
+                source++
+            }
+            if (source == size) {
+                // Appending a value to the end.
+                newKeys[newSize - 1] = key
+                newValues[newSize - 1] = value
+            } else {
+                while (dest < newSize) {
+                    val oldKey = keys[source]
+                    val oldValue = values[source]
+                    if (oldValue != null) {
+                        newKeys[dest] = oldKey
+                        newValues[dest] = oldValue
+                        dest++
+                    }
+                    source++
+                }
+            }
+        } else {
+            // The only element
+            newKeys[0] = key
+            newValues[0] = value
+        }
+        return ThreadMap(newSize, newKeys, newValues)
+    }
+
+    private fun find(key: Long): Int {
+        var high = size - 1
+        when (high) {
+            -1 -> return -1
+            0 -> return if (keys[0] == key) 0 else if (keys[0] > key) -2 else -1
+        }
+        var low = 0
+
+        while (low <= high) {
+            val mid = (low + high).ushr(1)
+            val midVal = keys[mid]
+            val comparison = midVal - key
+            when {
+                comparison < 0 -> low = mid + 1
+                comparison > 0 -> high = mid - 1
+                else -> return mid
+            }
+        }
+        return -(low + 1)
+    }
+}
+
+internal val emptyThreadMap = ThreadMap(0, LongArray(0), emptyArray())
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotThreadMapTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotThreadMapTests.kt
new file mode 100644
index 0000000..a50c9a8
--- /dev/null
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotThreadMapTests.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.snapshots
+
+import androidx.compose.runtime.SnapshotThreadLocal
+import androidx.compose.runtime.internal.ThreadMap
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+/**
+ * Test the internal ThreadMap
+ */
+class SnapshotThreadMapTests {
+    @Test
+    fun canCreateAMap() {
+        val map = emptyThreadMap()
+        assertNotNull(map)
+    }
+
+    @Test
+    fun setOfEmptyFails() {
+        val map = emptyThreadMap()
+        val added = map.trySet(1, 1)
+        assertFalse(added)
+    }
+
+    @Test
+    fun canAddOneToEmpty() {
+        val map = emptyThreadMap()
+        val newMap = map.newWith(1, 1)
+        assertNotEquals(map, newMap)
+        assertEquals(1, newMap.get(1))
+    }
+
+    @Test
+    fun canCreateForward() {
+        val map = testMap(0 until 100)
+        assertNotNull(map)
+        for (i in 0 until 100) {
+            assertEquals(i, map.get(i.toLong()))
+        }
+        for (i in -100 until 0) {
+            assertNull(map.get(i.toLong()))
+        }
+        for (i in 100 until 200) {
+            assertNull(map.get(i.toLong()))
+        }
+    }
+
+    @Test
+    fun canCreateBackward() {
+        val map = testMap((0 until 100).reversed())
+        assertNotNull(map)
+        for (i in 0 until 100) {
+            assertEquals(i, map.get(i.toLong()))
+        }
+        for (i in -100 until 0) {
+            assertNull(map.get(i.toLong()))
+        }
+        for (i in 100 until 200) {
+            assertNull(map.get(i.toLong()))
+        }
+    }
+
+    @Test
+    fun canCreateRandom() {
+        val list = Array<Long>(100) { it.toLong() }
+        val rand = Random(1337)
+        list.shuffle(rand)
+        var map = emptyThreadMap()
+        for (item in list) {
+            map = map.newWith(item, item)
+        }
+        for (i in 0 until 100) {
+            assertEquals(i.toLong(), map.get(i.toLong()))
+        }
+        for (i in -100 until 0) {
+            assertNull(map.get(i.toLong()))
+        }
+        for (i in 100 until 200) {
+            assertNull(map.get(i.toLong()))
+        }
+    }
+
+    @Test
+    fun canRemoveOne() {
+        val map = testMap(1..10)
+        val set = map.trySet(5, null)
+        assertTrue(set)
+        for (i in 1..10) {
+            if (i == 5) {
+                assertNull(map.get(i.toLong()))
+            } else {
+                assertEquals(i, map.get(i.toLong()))
+            }
+        }
+    }
+
+    @Test
+    fun canRemoveOneThenAddOne() {
+        val map = testMap(1..10)
+        val set = map.trySet(5, null)
+        assertTrue(set)
+        val newMap = map.newWith(11, 11)
+        assertNull(newMap.get(5))
+        assertEquals(11, newMap.get(11))
+    }
+
+    private fun emptyThreadMap() = ThreadMap(0, LongArray(0), arrayOfNulls(0))
+
+    private fun testMap(intProgression: IntProgression): ThreadMap {
+        var result = emptyThreadMap()
+        for (i in intProgression) {
+            result = result.newWith(i.toLong(), i)
+        }
+        return result
+    }
+}
+
+/**
+ * Test the thread lcoal variable
+ */
+class SnapshotThreadLocalTests {
+    @Test
+    fun canCreate() {
+        val local = SnapshotThreadLocal<Int>()
+        assertNotNull(local)
+    }
+
+    @Test
+    fun initalValueIsNull() {
+        val local = SnapshotThreadLocal<Int>()
+        assertNull(local.get())
+    }
+
+    @Test
+    fun canSetAndGetTheValue() {
+        val local = SnapshotThreadLocal<Int>()
+        local.set(100)
+        assertEquals(100, local.get())
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/UnknownResponseTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/UnknownResponseTest.kt
new file mode 100644
index 0000000..d4df199
--- /dev/null
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/UnknownResponseTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.inspection
+
+import androidx.compose.ui.inspection.rules.ComposeInspectionRule
+import androidx.compose.ui.inspection.testdata.TestActivity
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Command
+import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Response
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+class UnknownResponseTest {
+    @get:Rule
+    val rule = ComposeInspectionRule(TestActivity::class)
+
+    @Test
+    fun invalidBytesReturnedAsUnknownResponse(): Unit = runBlocking {
+        val invalidBytes = (1..99).map { it.toByte() }.toByteArray()
+        val responseBytes = rule.inspectorTester.sendCommand(invalidBytes)
+        val response = Response.parseFrom(responseBytes)
+
+        assertThat(response.specializedCase)
+            .isEqualTo(Response.SpecializedCase.UNKNOWN_COMMAND_RESPONSE)
+        assertThat(response.unknownCommandResponse.commandBytes.toByteArray())
+            .isEqualTo(invalidBytes)
+    }
+
+    @Test
+    fun unhandledCommandCaseReturnedAsUnknownResponse(): Unit = runBlocking {
+        val invalidCommand = Command.getDefaultInstance()
+        // This invalid case is handled by an else branch in ComposeLayoutInspector. In practice,
+        // this could also happen when a newer version of Studio sends a new command to an older
+        // version of an inspector.
+        assertThat(invalidCommand.specializedCase)
+            .isEqualTo(Command.SpecializedCase.SPECIALIZED_NOT_SET)
+
+        val commandBytes = invalidCommand.toByteArray()
+        val responseBytes = rule.inspectorTester.sendCommand(commandBytes)
+        val response = Response.parseFrom(responseBytes)
+
+        assertThat(response.specializedCase)
+            .isEqualTo(Response.SpecializedCase.UNKNOWN_COMMAND_RESPONSE)
+        assertThat(response.unknownCommandResponse.commandBytes.toByteArray())
+            .isEqualTo(commandBytes)
+    }
+}
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt
index 3da3c70..f8e0b80 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt
@@ -33,6 +33,8 @@
 import androidx.inspection.Inspector
 import androidx.inspection.InspectorEnvironment
 import androidx.inspection.InspectorFactory
+import com.google.protobuf.ByteString
+import com.google.protobuf.InvalidProtocolBufferException
 import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Command
 import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetAllParametersCommand
 import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetAllParametersResponse
@@ -44,6 +46,7 @@
 import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetParametersResponse
 import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.ParameterGroup
 import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Response
+import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.UnknownCommandResponse
 
 private const val LAYOUT_INSPECTION_ID = "layoutinspector.compose.inspection"
 
@@ -83,7 +86,13 @@
         }
 
     override fun onReceiveCommand(data: ByteArray, callback: CommandCallback) {
-        val command = Command.parseFrom(data)
+        val command = try {
+            Command.parseFrom(data)
+        } catch (ignored: InvalidProtocolBufferException) {
+            handleUnknownCommand(data, callback)
+            return
+        }
+
         when (command.specializedCase) {
             Command.SpecializedCase.GET_COMPOSABLES_COMMAND -> {
                 handleGetComposablesCommand(command.getComposablesCommand, callback)
@@ -97,7 +106,15 @@
             Command.SpecializedCase.GET_PARAMETER_DETAILS_COMMAND -> {
                 handleGetParameterDetailsCommand(command.getParameterDetailsCommand, callback)
             }
-            else -> error("Unexpected compose inspector command case: ${command.specializedCase}")
+            else -> handleUnknownCommand(data, callback)
+        }
+    }
+
+    private fun handleUnknownCommand(commandBytes: ByteArray, callback: CommandCallback) {
+        callback.reply {
+            unknownCommandResponse = UnknownCommandResponse.newBuilder().apply {
+                this.commandBytes = ByteString.copyFrom(commandBytes)
+            }.build()
         }
     }
 
diff --git a/compose/ui/ui-inspection/src/main/proto/compose_layout_inspection.proto b/compose/ui/ui-inspection/src/main/proto/compose_layout_inspection.proto
index a323d17..0b09814 100644
--- a/compose/ui/ui-inspection/src/main/proto/compose_layout_inspection.proto
+++ b/compose/ui/ui-inspection/src/main/proto/compose_layout_inspection.proto
@@ -174,6 +174,14 @@
 
 // ======= COMMANDS, RESPONSES, AND EVENTS =======
 
+// Response fired when incoming command bytes cannot be parsed or handled. This may occur if a newer
+// version of the client tries to interact with an older inspector.
+message UnknownCommandResponse {
+  // The initial command bytes received that couldn't be handled. By returning this back to the
+  // client, it should be able to identify what they sent that failed on the inspector side.
+  bytes command_bytes = 1;
+}
+
 // Request all composables found under a layout tree rooted under the specified view
 message GetComposablesCommand {
    int64 root_view_id = 1;
@@ -242,5 +250,7 @@
     GetParametersResponse get_parameters_response = 2;
     GetAllParametersResponse get_all_parameters_response = 3;
     GetParameterDetailsResponse get_parameter_details_response = 4;
+
+    UnknownCommandResponse unknown_command_response = 100;
   }
 }
diff --git a/compose/ui/ui-test-manifest/OWNERS b/compose/ui/ui-test-manifest/OWNERS
new file mode 100644
index 0000000..42abc4e
--- /dev/null
+++ b/compose/ui/ui-test-manifest/OWNERS
@@ -0,0 +1,2 @@
+jellefresen@google.com
+pavlis@google.com
diff --git a/compose/ui/ui-test-manifest/api/1.0.0-beta02.txt b/compose/ui/ui-test-manifest/api/1.0.0-beta02.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/compose/ui/ui-test-manifest/api/1.0.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/compose/ui/ui-test-manifest/api/current.txt b/compose/ui/ui-test-manifest/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/compose/ui/ui-test-manifest/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/compose/ui/ui-test-manifest/api/public_plus_experimental_1.0.0-beta02.txt b/compose/ui/ui-test-manifest/api/public_plus_experimental_1.0.0-beta02.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/compose/ui/ui-test-manifest/api/public_plus_experimental_1.0.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/compose/ui/ui-test-manifest/api/public_plus_experimental_current.txt b/compose/ui/ui-test-manifest/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/compose/ui/ui-test-manifest/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/compose/ui/ui-test-manifest/api/res-1.0.0-beta02.txt b/compose/ui/ui-test-manifest/api/res-1.0.0-beta02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/compose/ui/ui-test-manifest/api/res-1.0.0-beta02.txt
diff --git a/compose/ui/ui-test-manifest/api/res-current.txt b/compose/ui/ui-test-manifest/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/compose/ui/ui-test-manifest/api/res-current.txt
diff --git a/compose/ui/ui-test-manifest/api/restricted_1.0.0-beta02.txt b/compose/ui/ui-test-manifest/api/restricted_1.0.0-beta02.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/compose/ui/ui-test-manifest/api/restricted_1.0.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/compose/ui/ui-test-manifest/api/restricted_current.txt b/compose/ui/ui-test-manifest/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/compose/ui/ui-test-manifest/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/compose/ui/ui-test-manifest/build.gradle b/compose/ui/ui-test-manifest/build.gradle
new file mode 100644
index 0000000..0c92e15
--- /dev/null
+++ b/compose/ui/ui-test-manifest/build.gradle
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+import androidx.build.AndroidXUiPlugin
+import androidx.build.LibraryGroups
+import androidx.build.Publish
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("AndroidXUiPlugin")
+}
+
+dependencies {
+    api("androidx.activity:activity:1.2.0")
+}
+
+androidx {
+    name = "Compose Testing manifest dependency"
+    publish = Publish.SNAPSHOT_AND_RELEASE
+    mavenGroup = LibraryGroups.Compose.UI
+    inceptionYear = "2021"
+    description = "Compose testing library that should be added as a debugImplementation dependency to add properties to the debug manifest necessary for testing an application"
+}
diff --git a/compose/ui/ui-test-manifest/integration-tests/testapp/OWNERS b/compose/ui/ui-test-manifest/integration-tests/testapp/OWNERS
new file mode 100644
index 0000000..42abc4e
--- /dev/null
+++ b/compose/ui/ui-test-manifest/integration-tests/testapp/OWNERS
@@ -0,0 +1,2 @@
+jellefresen@google.com
+pavlis@google.com
diff --git a/compose/ui/ui-test-manifest/integration-tests/testapp/build.gradle b/compose/ui/ui-test-manifest/integration-tests/testapp/build.gradle
new file mode 100644
index 0000000..ef1ee31
--- /dev/null
+++ b/compose/ui/ui-test-manifest/integration-tests/testapp/build.gradle
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+    id("AndroidXPlugin")
+    id("AndroidXUiPlugin")
+    id("com.android.application")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    kotlinPlugin(project(":compose:compiler:compiler"))
+
+    debugImplementation(project(":compose:ui:ui-test-manifest"))
+
+    androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+    androidTestImplementation(ANDROIDX_TEST_RUNNER)
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 21
+    }
+}
diff --git a/compose/ui/ui-test-manifest/integration-tests/testapp/src/androidTest/java/androidx/compose/ui/test/manifest/integration/testapp/ComponentActivityLaunchesTest.kt b/compose/ui/ui-test-manifest/integration-tests/testapp/src/androidTest/java/androidx/compose/ui/test/manifest/integration/testapp/ComponentActivityLaunchesTest.kt
new file mode 100644
index 0000000..a88b849
--- /dev/null
+++ b/compose/ui/ui-test-manifest/integration-tests/testapp/src/androidTest/java/androidx/compose/ui/test/manifest/integration/testapp/ComponentActivityLaunchesTest.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test.manifest.integration.testapp
+
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class ComponentActivityLaunchesTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun test() {
+        rule.setContent {}
+        // Test does not crash and does not time out
+    }
+}
diff --git a/compose/ui/ui-test-manifest/integration-tests/testapp/src/main/AndroidManifest.xml b/compose/ui/ui-test-manifest/integration-tests/testapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..8f6a9da
--- /dev/null
+++ b/compose/ui/ui-test-manifest/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<!--
+  ~ Copyright (C) 2016 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="androidx.compose.ui.test.manifest.integration.testapp">
+
+    <application android:label="ui-test-manifest test app" />
+
+</manifest>
diff --git a/compose/ui/ui-test-manifest/src/main/AndroidManifest.xml b/compose/ui/ui-test-manifest/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..833d681
--- /dev/null
+++ b/compose/ui/ui-test-manifest/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.compose.ui.test.manifest">
+    <application>
+        <activity android:name="androidx.activity.ComponentActivity" />
+    </application>
+</manifest>
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
index 2897c48..ebbc0b2 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
@@ -165,7 +165,7 @@
          * have the same [zIndex] the order in which the items were placed is used.
          */
         fun Placeable.placeRelative(x: Int, y: Int, zIndex: Float = 0f) =
-            placeRelative(IntOffset(x, y), zIndex)
+            placeAutoMirrored(IntOffset(x, y), zIndex, null)
 
         /**
          * Place a [Placeable] at [x], [y] in its parent's coordinate system.
@@ -176,7 +176,8 @@
          * [zIndex] will be drawn on top of all the children with smaller [zIndex]. When children
          * have the same [zIndex] the order in which the items were placed is used.
          */
-        fun Placeable.place(x: Int, y: Int, zIndex: Float = 0f) = place(IntOffset(x, y), zIndex)
+        fun Placeable.place(x: Int, y: Int, zIndex: Float = 0f) =
+            placeApparentToRealOffset(IntOffset(x, y), zIndex, null)
 
         /**
          * Place a [Placeable] at [position] in its parent's coordinate system.
@@ -235,7 +236,7 @@
             y: Int,
             zIndex: Float = 0f,
             layerBlock: GraphicsLayerScope.() -> Unit = DefaultLayerBlock
-        ) = placeRelativeWithLayer(IntOffset(x, y), zIndex, layerBlock)
+        ) = placeAutoMirrored(IntOffset(x, y), zIndex, layerBlock)
 
         /**
          * Place a [Placeable] at [x], [y] in its parent's coordinate system with an introduced
@@ -255,7 +256,7 @@
             y: Int,
             zIndex: Float = 0f,
             layerBlock: GraphicsLayerScope.() -> Unit = DefaultLayerBlock
-        ) = placeWithLayer(IntOffset(x, y), zIndex, layerBlock)
+        ) = placeApparentToRealOffset(IntOffset(x, y), zIndex, layerBlock)
 
         /**
          * Place a [Placeable] at [position] in its parent's coordinate system with an introduced
@@ -276,10 +277,11 @@
             layerBlock: GraphicsLayerScope.() -> Unit = DefaultLayerBlock
         ) = placeApparentToRealOffset(position, zIndex, layerBlock)
 
-        private fun Placeable.placeAutoMirrored(
+        @Suppress("NOTHING_TO_INLINE")
+        internal inline fun Placeable.placeAutoMirrored(
             position: IntOffset,
             zIndex: Float,
-            layerBlock: (GraphicsLayerScope.() -> Unit)?
+            noinline layerBlock: (GraphicsLayerScope.() -> Unit)?
         ) {
             if (parentLayoutDirection == LayoutDirection.Ltr || parentWidth == 0) {
                 placeApparentToRealOffset(position, zIndex, layerBlock)
@@ -292,10 +294,11 @@
             }
         }
 
-        private fun Placeable.placeApparentToRealOffset(
+        @Suppress("NOTHING_TO_INLINE")
+        internal inline fun Placeable.placeApparentToRealOffset(
             position: IntOffset,
             zIndex: Float,
-            layerBlock: (GraphicsLayerScope.() -> Unit)?
+            noinline layerBlock: (GraphicsLayerScope.() -> Unit)?
         ) {
             placeAt(position + apparentToRealOffset, zIndex, layerBlock)
         }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
index 63529ac..9e961e0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
@@ -104,7 +104,7 @@
         }
     }
 
-    override fun performMeasure(constraints: Constraints): Placeable {
+    override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
         val placeable = wrapped.measure(constraints)
         measureResult = object : MeasureResult {
             override val width: Int = wrapped.measureResult.width
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
index 0093059..1ecd191 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
@@ -40,7 +40,7 @@
 
     override val measureScope get() = layoutNode.measureScope
 
-    override fun performMeasure(constraints: Constraints): Placeable {
+    override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
         val measureResult = with(layoutNode.measurePolicy) {
             layoutNode.measureScope.measure(layoutNode.children, constraints)
         }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
index 844d0a6..f804685 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
@@ -50,7 +50,7 @@
 internal abstract class LayoutNodeWrapper(
     internal val layoutNode: LayoutNode
 ) : Placeable(), Measurable, LayoutCoordinates, OwnerScope, (Canvas) -> Unit {
-    internal open val wrapped: LayoutNodeWrapper? = null
+    internal open val wrapped: LayoutNodeWrapper? get() = null
     internal var wrappedBy: LayoutNodeWrapper? = null
 
     /**
@@ -69,7 +69,7 @@
         private set
 
     private var _isAttached = false
-    override val isAttached: Boolean
+    final override val isAttached: Boolean
         get() {
             if (_isAttached) {
                 require(layoutNode.isAttached)
@@ -78,35 +78,46 @@
         }
 
     private var _measureResult: MeasureResult? = null
-    open var measureResult: MeasureResult
+    var measureResult: MeasureResult
         get() = _measureResult ?: error(UnmeasuredError)
         internal set(value) {
-            if (value.width != _measureResult?.width || value.height != _measureResult?.height) {
-                val layer = layer
-                if (layer != null) {
-                    layer.resize(IntSize(value.width, value.height))
-                } else {
-                    wrappedBy?.invalidateLayer()
+            val old = _measureResult
+            if (value !== old) {
+                _measureResult = value
+                if (old == null || value.width != old.width || value.height != old.height) {
+                    onMeasureResultChanged(value.width, value.height)
                 }
-                layoutNode.owner?.onLayoutChange(layoutNode)
             }
-            _measureResult = value
-            measuredSize = IntSize(measureResult.width, measureResult.height)
         }
 
+    /**
+     * Called when the width or height of [measureResult] change. The object instance pointed to
+     * by [measureResult] may or may not have changed.
+     */
+    protected open fun onMeasureResultChanged(width: Int, height: Int) {
+        val layer = layer
+        if (layer != null) {
+            layer.resize(IntSize(width, height))
+        } else {
+            wrappedBy?.invalidateLayer()
+        }
+        layoutNode.owner?.onLayoutChange(layoutNode)
+        measuredSize = IntSize(width, height)
+    }
+
     var position: IntOffset = IntOffset.Zero
         private set
 
     var zIndex: Float = 0f
         protected set
 
-    override val parentLayoutCoordinates: LayoutCoordinates?
+    final override val parentLayoutCoordinates: LayoutCoordinates?
         get() {
             check(isAttached) { ExpectAttachedLayoutCoordinates }
             return layoutNode.outerLayoutNodeWrapper.wrappedBy
         }
 
-    override val parentCoordinates: LayoutCoordinates?
+    final override val parentCoordinates: LayoutCoordinates?
         get() {
             check(isAttached) { ExpectAttachedLayoutCoordinates }
             return wrappedBy?.getWrappedByCoordinates()
@@ -143,17 +154,12 @@
         return x >= 0f && y >= 0f && x < measuredWidth && y < measuredHeight
     }
 
-    /**
-     * Measures the modified child.
-     */
-    abstract fun performMeasure(constraints: Constraints): Placeable
-
-    /**
-     * Measures the modified child.
-     */
-    final override fun measure(constraints: Constraints): Placeable {
+    protected inline fun performingMeasure(
+        constraints: Constraints,
+        block: () -> Placeable
+    ): Placeable {
         measurementConstraints = constraints
-        val result = performMeasure(constraints)
+        val result = block()
         layer?.resize(measuredSize)
         return result
     }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedDrawNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedDrawNode.kt
index 28a685a..cca6cd7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedDrawNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedDrawNode.kt
@@ -21,7 +21,6 @@
 import androidx.compose.ui.draw.DrawModifier
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Canvas
-import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.toSize
 
@@ -80,16 +79,10 @@
             invalidateCache = true
         }
 
-    override var measureResult: MeasureResult
-        get() = super.measureResult
-        set(value) {
-            if (super.measuredSize.width != value.width ||
-                super.measuredSize.height != value.height
-            ) {
-                invalidateCache = true
-            }
-            super.measureResult = value
-        }
+    override fun onMeasureResultChanged(width: Int, height: Int) {
+        super.onMeasureResultChanged(width, height)
+        invalidateCache = true
+    }
 
     // This is not thread safe
     override fun performDraw(canvas: Canvas) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt
index ec54573..614d548 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt
@@ -32,9 +32,11 @@
     modifier: LayoutModifier
 ) : DelegatingLayoutNodeWrapper<LayoutModifier>(wrapped, modifier) {
 
-    override fun performMeasure(constraints: Constraints): Placeable = with(modifier) {
-        measureResult = measureScope.measure(wrapped, constraints)
-        this@ModifiedLayoutNode
+    override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
+        with(modifier) {
+            measureResult = measureScope.measure(wrapped, constraints)
+            this@ModifiedLayoutNode
+        }
     }
 
     override fun minIntrinsicWidth(height: Int): Int =
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/RemeasureModifierWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/RemeasureModifierWrapper.kt
index f37eb1b..fdb9840 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/RemeasureModifierWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/RemeasureModifierWrapper.kt
@@ -27,8 +27,8 @@
     wrapped: LayoutNodeWrapper,
     modifier: OnRemeasuredModifier
 ) : DelegatingLayoutNodeWrapper<OnRemeasuredModifier>(wrapped, modifier) {
-    override fun performMeasure(constraints: Constraints): Placeable {
-        val placeable = super.performMeasure(constraints)
+    override fun measure(constraints: Constraints): Placeable {
+        val placeable = super.measure(constraints)
         val invokeRemeasureCallbacks = {
             modifier.onRemeasured(measuredSize)
         }
diff --git a/development/file-utils/diff-filterer.py b/development/file-utils/diff-filterer.py
index d1329bf..dd575a8 100755
--- a/development/file-utils/diff-filterer.py
+++ b/development/file-utils/diff-filterer.py
@@ -715,10 +715,17 @@
 
   def cleanupTempDirs(self):
     print("Clearing work directories")
-    if os.path.isdir(self.workPath):
-      for child in os.listdir(self.workPath):
-        if child.startswith("job-"):
-          fileIo.removePath(os.path.join(self.workPath, child))
+    numAttempts = 3
+    for attempt in range(numAttempts):
+      if os.path.isdir(self.workPath):
+        for child in os.listdir(self.workPath):
+          if child.startswith("job-"):
+            path = os.path.join(self.workPath, child)
+            try:
+              fileIo.removePath(path)
+            except IOError as e:
+              if attempt >= numAttempts - 1:
+                raise Exception("Failed to remove " + path, e)
 
   def runnerTest(self, testState, timeout = None):
     workPath = self.getWorkPath(0)
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index ea9d32c..20a697b 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -81,10 +81,10 @@
     docs("androidx.contentpager:contentpager:1.0.0")
     docs("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
     docs("androidx.core:core-role:1.1.0-alpha02")
-    docs("androidx.core:core:1.5.0-beta02")
+    docs("androidx.core:core:1.5.0-beta03")
     docs("androidx.core:core-animation:1.0.0-alpha02")
     docs("androidx.core:core-animation-testing:1.0.0-alpha02")
-    docs("androidx.core:core-ktx:1.5.0-beta02")
+    docs("androidx.core:core-ktx:1.5.0-beta03")
     docs("androidx.cursoradapter:cursoradapter:1.0.0")
     docs("androidx.customview:customview:1.1.0")
     docs("androidx.datastore:datastore:1.0.0-alpha08")
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 69bc9e6..9be8054 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -244,6 +244,9 @@
     docs(project(":wear:wear-ongoing"))
     docs(project(":wear:wear-phone-interactions"))
     docs(project(":wear:wear-remote-interactions"))
+    docs(project(":wear:wear-tiles"))
+    docs(project(":wear:wear-tiles-proto"))
+    docs(project(":wear:wear-tiles-renderer"))
     docs(project(":wear:wear-watchface"))
     docs(project(":wear:wear-watchface-complications-rendering"))
     docs(project(":wear:wear-watchface-client"))
diff --git a/emoji2/emoji2-bundled/lint-baseline.xml b/emoji2/emoji2-bundled/lint-baseline.xml
deleted file mode 100644
index 27e26a8..0000000
--- a/emoji2/emoji2-bundled/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
-
-</issues>
diff --git a/emoji2/emoji2-views-core/lint-baseline.xml b/emoji2/emoji2-views-core/lint-baseline.xml
deleted file mode 100644
index b4d30ed..0000000
--- a/emoji2/emoji2-views-core/lint-baseline.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 19, the call containing class androidx.emoji2.helpers.EmojiInputFilter.InitCallbackImpl is not annotated with @RequiresApi(x) where x is at least 19. Either annotate the containing class with at least @RequiresApi(19) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(19)."
-        errorLine1="            if (textView != null &amp;&amp; textView.isAttachedToWindow()) {"
-        errorLine2="                                             ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/helpers/EmojiInputFilter.java"
-            line="110"
-            column="46"/>
-    </issue>
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 19, the call containing class androidx.emoji2.helpers.EmojiTextWatcher.InitCallbackImpl is not annotated with @RequiresApi(x) where x is at least 19. Either annotate the containing class with at least @RequiresApi(19) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(19)."
-        errorLine1="            if (editText != null &amp;&amp; editText.isAttachedToWindow()) {"
-        errorLine2="                                             ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/helpers/EmojiTextWatcher.java"
-            line="122"
-            column="46"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="                final KeyEvent event) {"
-        errorLine2="                      ~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/helpers/EmojiKeyListener.java"
-            line="80"
-            column="23"/>
-    </issue>
-
-</issues>
diff --git a/emoji2/emoji2-views-core/src/androidTest/AndroidManifest.xml b/emoji2/emoji2-views-core/src/androidTest/AndroidManifest.xml
index ed2f1d2..6c30771 100644
--- a/emoji2/emoji2-views-core/src/androidTest/AndroidManifest.xml
+++ b/emoji2/emoji2-views-core/src/androidTest/AndroidManifest.xml
@@ -13,8 +13,7 @@
      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="androidx.emoji2.helpers">
+<manifest package="androidx.emoji2.helpers">
 
     <application>
     </application>
diff --git a/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiInputFilter.java b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiInputFilter.java
index be05e42..ccb3033 100644
--- a/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiInputFilter.java
+++ b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiInputFilter.java
@@ -96,6 +96,7 @@
         return mInitCallback;
     }
 
+    @RequiresApi(19)
     private static class InitCallbackImpl extends InitCallback {
         private final Reference<TextView> mViewRef;
 
diff --git a/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiKeyListener.java b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiKeyListener.java
index fcd8e0a..a5b2129 100644
--- a/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiKeyListener.java
+++ b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiKeyListener.java
@@ -77,7 +77,7 @@
 
     public static class EmojiCompatHandleKeyDownHelper {
         public boolean handleKeyDown(@NonNull final Editable editable, final int keyCode,
-                final KeyEvent event) {
+                @NonNull final KeyEvent event) {
             return EmojiCompat.handleOnKeyDown(editable, keyCode, event);
         }
     }
diff --git a/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiTextWatcher.java b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiTextWatcher.java
index d6676c1..43887bb 100644
--- a/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiTextWatcher.java
+++ b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiTextWatcher.java
@@ -108,6 +108,7 @@
         return mInitCallback;
     }
 
+    @RequiresApi(19)
     private static class InitCallbackImpl extends InitCallback {
         private final Reference<EditText> mViewRef;
 
diff --git a/emoji2/emoji2-views/lint-baseline.xml b/emoji2/emoji2-views/lint-baseline.xml
deleted file mode 100644
index 2f76b6b..0000000
--- a/emoji2/emoji2-views/lint-baseline.xml
+++ /dev/null
@@ -1,664 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 21, the call containing class androidx.emoji2.widget.EmojiButton is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
-        errorLine1="        super(context, attrs, defStyleAttr, defStyleRes);"
-        errorLine2="        ~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
-            line="59"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 21, the call containing class androidx.emoji2.widget.EmojiEditText is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
-        errorLine1="        super(context, attrs, defStyleAttr, defStyleRes);"
-        errorLine2="        ~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
-            line="66"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 21, the call containing class androidx.emoji2.widget.EmojiExtractEditText is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
-        errorLine1="        super(context, attrs, defStyleAttr, defStyleRes);"
-        errorLine2="        ~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
-            line="75"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 21, the call containing class androidx.emoji2.widget.EmojiExtractTextLayout is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
-        errorLine1="        super(context, attrs, defStyleAttr, defStyleRes);"
-        errorLine2="        ~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
-            line="99"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 21, the call containing class androidx.emoji2.widget.EmojiTextView is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
-        errorLine1="        super(context, attrs, defStyleAttr, defStyleRes);"
-        errorLine2="        ~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
-            line="59"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 21, the call containing class androidx.emoji2.widget.ExtractButtonCompat is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
-        errorLine1="        super(context, attrs, defStyleAttr, defStyleRes);"
-        errorLine2="        ~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
-            line="53"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EditTextAttributeHelper(@NonNull View view, AttributeSet attrs, int defStyleAttr,"
-        errorLine2="                                                       ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EditTextAttributeHelper.java"
-            line="40"
-            column="56"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiButton(Context context) {"
-        errorLine2="                       ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
-            line="42"
-            column="24"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiButton(Context context, AttributeSet attrs) {"
-        errorLine2="                       ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
-            line="47"
-            column="24"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiButton(Context context, AttributeSet attrs) {"
-        errorLine2="                                        ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
-            line="47"
-            column="41"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr) {"
-        errorLine2="                       ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
-            line="52"
-            column="24"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr) {"
-        errorLine2="                                        ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
-            line="52"
-            column="41"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
-        errorLine2="                       ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
-            line="58"
-            column="24"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
-        errorLine2="                                        ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
-            line="58"
-            column="41"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public void setFilters(InputFilter[] filters) {"
-        errorLine2="                           ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
-            line="71"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {"
-        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
-            line="93"
-            column="54"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiEditText(Context context) {"
-        errorLine2="                         ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
-            line="49"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiEditText(Context context, AttributeSet attrs) {"
-        errorLine2="                         ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
-            line="54"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiEditText(Context context, AttributeSet attrs) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
-            line="54"
-            column="43"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {"
-        errorLine2="                         ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
-            line="59"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
-            line="59"
-            column="43"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
-        errorLine2="                         ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
-            line="65"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
-            line="65"
-            column="43"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {"
-        errorLine2="           ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
-            line="89"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {"
-        errorLine2="                                                   ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
-            line="89"
-            column="52"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {"
-        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
-            line="134"
-            column="54"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiExtractEditText(Context context) {"
-        errorLine2="                                ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
-            line="57"
-            column="33"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiExtractEditText(Context context, AttributeSet attrs) {"
-        errorLine2="                                ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
-            line="62"
-            column="33"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiExtractEditText(Context context, AttributeSet attrs) {"
-        errorLine2="                                                 ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
-            line="62"
-            column="50"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr) {"
-        errorLine2="                                ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
-            line="67"
-            column="33"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr) {"
-        errorLine2="                                                 ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
-            line="67"
-            column="50"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr,"
-        errorLine2="                                ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
-            line="73"
-            column="33"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr,"
-        errorLine2="                                                 ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
-            line="73"
-            column="50"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {"
-        errorLine2="           ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
-            line="98"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {"
-        errorLine2="                                                   ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
-            line="98"
-            column="52"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {"
-        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
-            line="162"
-            column="54"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiExtractTextLayout(Context context) {"
-        errorLine2="                                  ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
-            line="79"
-            column="35"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiExtractTextLayout(Context context,"
-        errorLine2="                                  ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
-            line="84"
-            column="35"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiExtractTextLayout(Context context,"
-        errorLine2="                                  ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
-            line="90"
-            column="35"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiExtractTextLayout(Context context, AttributeSet attrs,"
-        errorLine2="                                  ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
-            line="97"
-            column="35"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiExtractTextLayout(Context context, AttributeSet attrs,"
-        errorLine2="                                                   ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
-            line="97"
-            column="52"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public void onUpdateExtractingViews(InputMethodService inputMethodService, EditorInfo ei) {"
-        errorLine2="                                        ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
-            line="163"
-            column="41"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public void onUpdateExtractingViews(InputMethodService inputMethodService, EditorInfo ei) {"
-        errorLine2="                                                                               ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
-            line="163"
-            column="80"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiTextView(Context context) {"
-        errorLine2="                         ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
-            line="42"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiTextView(Context context, AttributeSet attrs) {"
-        errorLine2="                         ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
-            line="47"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiTextView(Context context, AttributeSet attrs) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
-            line="47"
-            column="43"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) {"
-        errorLine2="                         ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
-            line="52"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
-            line="52"
-            column="43"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
-        errorLine2="                         ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
-            line="58"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
-            line="58"
-            column="43"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public void setFilters(InputFilter[] filters) {"
-        errorLine2="                           ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
-            line="71"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {"
-        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
-            line="93"
-            column="54"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public ExtractButtonCompat(Context context) {"
-        errorLine2="                               ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
-            line="38"
-            column="32"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public ExtractButtonCompat(Context context, AttributeSet attrs) {"
-        errorLine2="                               ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
-            line="42"
-            column="32"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public ExtractButtonCompat(Context context, AttributeSet attrs) {"
-        errorLine2="                                                ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
-            line="42"
-            column="49"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public ExtractButtonCompat(Context context, AttributeSet attrs, int defStyleAttr) {"
-        errorLine2="                               ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
-            line="46"
-            column="32"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public ExtractButtonCompat(Context context, AttributeSet attrs, int defStyleAttr) {"
-        errorLine2="                                                ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
-            line="46"
-            column="49"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public ExtractButtonCompat(Context context, AttributeSet attrs, int defStyleAttr,"
-        errorLine2="                               ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
-            line="51"
-            column="32"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public ExtractButtonCompat(Context context, AttributeSet attrs, int defStyleAttr,"
-        errorLine2="                                                ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
-            line="51"
-            column="49"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {"
-        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
-            line="70"
-            column="54"/>
-    </issue>
-
-</issues>
diff --git a/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EditTextAttributeHelper.java b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EditTextAttributeHelper.java
index cc397a2..1fee3e1 100644
--- a/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EditTextAttributeHelper.java
+++ b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EditTextAttributeHelper.java
@@ -24,6 +24,7 @@
 import android.view.View;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.emoji2.text.EmojiDefaults;
 
@@ -36,8 +37,8 @@
 public class EditTextAttributeHelper {
     private int mMaxEmojiCount;
 
-    public EditTextAttributeHelper(@NonNull View view, AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
+    public EditTextAttributeHelper(@NonNull View view, @Nullable AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
         if (attrs != null) {
             final Context context = view.getContext();
             TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.EmojiEditText,
diff --git a/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiButton.java b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiButton.java
index 737c951..1b20ab5 100644
--- a/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiButton.java
+++ b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiButton.java
@@ -15,6 +15,7 @@
  */
 package androidx.emoji2.widget;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.os.Build;
 import android.text.InputFilter;
@@ -22,6 +23,8 @@
 import android.view.ActionMode;
 import android.widget.Button;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.core.widget.TextViewCompat;
 import androidx.emoji2.helpers.EmojiTextViewHelper;
@@ -39,23 +42,25 @@
      */
     private boolean mInitialized;
 
-    public EmojiButton(Context context) {
+    public EmojiButton(@NonNull Context context) {
         super(context);
         init();
     }
 
-    public EmojiButton(Context context, AttributeSet attrs) {
+    public EmojiButton(@NonNull Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
         init();
     }
 
-    public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr) {
+    public EmojiButton(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         init();
     }
 
+    @SuppressLint("UnsafeNewApiCall")
     @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-    public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+    public EmojiButton(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
         init();
     }
@@ -68,7 +73,7 @@
     }
 
     @Override
-    public void setFilters(InputFilter[] filters) {
+    public void setFilters(@NonNull InputFilter[] filters) {
         super.setFilters(getEmojiTextViewHelper().getFilters(filters));
     }
 
@@ -90,7 +95,9 @@
      * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
      */
     @Override
-    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+    public void setCustomSelectionActionModeCallback(
+            @NonNull ActionMode.Callback actionModeCallback
+    ) {
         super.setCustomSelectionActionModeCallback(TextViewCompat
                 .wrapCustomSelectionActionModeCallback(this, actionModeCallback));
     }
diff --git a/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiEditText.java b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiEditText.java
index 722fd95..1dae701 100644
--- a/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiEditText.java
+++ b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiEditText.java
@@ -15,6 +15,7 @@
  */
 package androidx.emoji2.widget;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.os.Build;
 import android.text.method.KeyListener;
@@ -25,6 +26,7 @@
 import android.widget.EditText;
 
 import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.core.widget.TextViewCompat;
@@ -46,23 +48,25 @@
      */
     private boolean mInitialized;
 
-    public EmojiEditText(Context context) {
+    public EmojiEditText(@NonNull Context context) {
         super(context);
         init(null /*attrs*/, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
     }
 
-    public EmojiEditText(Context context, AttributeSet attrs) {
+    public EmojiEditText(@NonNull Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
         init(attrs, android.R.attr.editTextStyle, 0 /*defStyleRes*/);
     }
 
-    public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {
+    public EmojiEditText(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         init(attrs, defStyleAttr, 0 /*defStyleRes*/);
     }
 
+    @SuppressLint("UnsafeNewApiCall")
     @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-    public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+    public EmojiEditText(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
         init(attrs, defStyleAttr, defStyleRes);
     }
@@ -85,8 +89,9 @@
         super.setKeyListener(keyListener);
     }
 
+    @Nullable
     @Override
-    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+    public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) {
         final InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
         return getEmojiEditTextHelper().onCreateInputConnection(inputConnection, outAttrs);
     }
@@ -131,7 +136,9 @@
      * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
      */
     @Override
-    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+    public void setCustomSelectionActionModeCallback(
+            @NonNull ActionMode.Callback actionModeCallback
+    ) {
         super.setCustomSelectionActionModeCallback(TextViewCompat
                 .wrapCustomSelectionActionModeCallback(this, actionModeCallback));
     }
diff --git a/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java
index 6058386..95df225 100644
--- a/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java
+++ b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java
@@ -18,6 +18,7 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.inputmethodservice.ExtractEditText;
 import android.os.Build;
@@ -29,6 +30,7 @@
 import android.widget.TextView;
 
 import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
@@ -54,24 +56,26 @@
      */
     private boolean mInitialized;
 
-    public EmojiExtractEditText(Context context) {
+    public EmojiExtractEditText(@NonNull Context context) {
         super(context);
         init(null /*attrs*/, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
     }
 
-    public EmojiExtractEditText(Context context, AttributeSet attrs) {
+    public EmojiExtractEditText(@NonNull Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
         init(attrs, android.R.attr.editTextStyle, 0 /*defStyleRes*/);
     }
 
-    public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr) {
+    public EmojiExtractEditText(@NonNull Context context, @Nullable AttributeSet attrs,
+            int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         init(attrs, defStyleAttr, 0 /*defStyleRes*/);
     }
 
+    @SuppressLint("UnsafeNewApiCall")
     @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-    public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
+    public EmojiExtractEditText(@NonNull Context context, @Nullable AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
         init(attrs, defStyleAttr, defStyleRes);
     }
@@ -94,8 +98,9 @@
         super.setKeyListener(keyListener);
     }
 
+    @Nullable
     @Override
-    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+    public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) {
         final InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
         return getEmojiEditTextHelper().onCreateInputConnection(inputConnection, outAttrs);
     }
@@ -159,7 +164,9 @@
      * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
      */
     @Override
-    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+    public void setCustomSelectionActionModeCallback(
+            @NonNull ActionMode.Callback actionModeCallback
+    ) {
         super.setCustomSelectionActionModeCallback(TextViewCompat
                 .wrapCustomSelectionActionModeCallback(this, actionModeCallback));
     }
diff --git a/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java
index 319b1db..c930389 100644
--- a/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java
+++ b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java
@@ -16,6 +16,7 @@
 
 package androidx.emoji2.widget;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.inputmethodservice.InputMethodService;
@@ -75,25 +76,26 @@
      */
     private boolean mInitialized;
 
-    public EmojiExtractTextLayout(Context context) {
+    public EmojiExtractTextLayout(@NonNull Context context) {
         super(context);
         init(context, null /*attrs*/, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
     }
 
-    public EmojiExtractTextLayout(Context context,
+    public EmojiExtractTextLayout(@NonNull Context context,
             @Nullable AttributeSet attrs) {
         super(context, attrs);
         init(context, attrs, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
     }
 
-    public EmojiExtractTextLayout(Context context,
+    public EmojiExtractTextLayout(@NonNull Context context,
             @Nullable AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         init(context, attrs, defStyleAttr, 0 /*defStyleRes*/);
     }
 
+    @SuppressLint("UnsafeNewApiCall")
     @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-    public EmojiExtractTextLayout(Context context, AttributeSet attrs,
+    public EmojiExtractTextLayout(@NonNull Context context, @Nullable AttributeSet attrs,
             int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
         init(context, attrs, defStyleAttr, defStyleRes);
@@ -159,7 +161,8 @@
      * {@link InputMethodService#onUpdateExtractingViews(EditorInfo)
      * InputMethodService#onUpdateExtractingViews(EditorInfo)}.
      */
-    public void onUpdateExtractingViews(InputMethodService inputMethodService, EditorInfo ei) {
+    public void onUpdateExtractingViews(@NonNull InputMethodService inputMethodService,
+            @NonNull EditorInfo ei) {
         // the following code is ported as it is from InputMethodService.onUpdateExtractingViews
         if (!inputMethodService.isExtractViewShown()) {
             return;
diff --git a/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiTextView.java b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiTextView.java
index 7ec4875..709ddd3 100644
--- a/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiTextView.java
+++ b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiTextView.java
@@ -15,6 +15,7 @@
  */
 package androidx.emoji2.widget;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.os.Build;
 import android.text.InputFilter;
@@ -22,6 +23,8 @@
 import android.view.ActionMode;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.core.widget.TextViewCompat;
 import androidx.emoji2.helpers.EmojiTextViewHelper;
@@ -39,23 +42,25 @@
      */
     private boolean mInitialized;
 
-    public EmojiTextView(Context context) {
+    public EmojiTextView(@NonNull Context context) {
         super(context);
         init();
     }
 
-    public EmojiTextView(Context context, AttributeSet attrs) {
+    public EmojiTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
         init();
     }
 
-    public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+    public EmojiTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         init();
     }
 
+    @SuppressLint("UnsafeNewApiCall")
     @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-    public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+    public EmojiTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
         init();
     }
@@ -68,7 +73,7 @@
     }
 
     @Override
-    public void setFilters(InputFilter[] filters) {
+    public void setFilters(@NonNull InputFilter[] filters) {
         super.setFilters(getEmojiTextViewHelper().getFilters(filters));
     }
 
@@ -90,7 +95,9 @@
      * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
      */
     @Override
-    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+    public void setCustomSelectionActionModeCallback(
+            @NonNull ActionMode.Callback actionModeCallback
+    ) {
         super.setCustomSelectionActionModeCallback(TextViewCompat
                 .wrapCustomSelectionActionModeCallback(this, actionModeCallback));
     }
diff --git a/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java
index 26070ab..cf839e0 100644
--- a/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java
+++ b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java
@@ -18,12 +18,15 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.os.Build;
 import android.util.AttributeSet;
 import android.view.ActionMode;
 import android.widget.Button;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.core.widget.TextViewCompat;
@@ -35,21 +38,23 @@
  */
 @RestrictTo(LIBRARY_GROUP_PREFIX)
 public class ExtractButtonCompat extends Button {
-    public ExtractButtonCompat(Context context) {
+    public ExtractButtonCompat(@NonNull Context context) {
         super(context, null);
     }
 
-    public ExtractButtonCompat(Context context, AttributeSet attrs) {
+    public ExtractButtonCompat(@NonNull Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
     }
 
-    public ExtractButtonCompat(Context context, AttributeSet attrs, int defStyleAttr) {
+    public ExtractButtonCompat(@NonNull Context context, @Nullable AttributeSet attrs,
+            int defStyleAttr) {
         super(context, attrs, defStyleAttr);
     }
 
+    @SuppressLint("UnsafeNewApiCall")
     @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-    public ExtractButtonCompat(Context context, AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
+    public ExtractButtonCompat(@NonNull Context context, @Nullable AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
     }
 
@@ -59,7 +64,7 @@
      */
     @Override
     public boolean hasWindowFocus() {
-        return isEnabled() && getVisibility() == VISIBLE ? true : false;
+        return isEnabled() && getVisibility() == VISIBLE;
     }
 
     /**
@@ -67,7 +72,9 @@
      * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
      */
     @Override
-    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+    public void setCustomSelectionActionModeCallback(
+            @NonNull ActionMode.Callback actionModeCallback
+    ) {
         super.setCustomSelectionActionModeCallback(TextViewCompat
                 .wrapCustomSelectionActionModeCallback(this, actionModeCallback));
     }
diff --git a/emoji2/emoji2/lint-baseline.xml b/emoji2/emoji2/lint-baseline.xml
deleted file mode 100644
index d9ef293..0000000
--- a/emoji2/emoji2/lint-baseline.xml
+++ /dev/null
@@ -1,745 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
-
-    <issue
-        id="PrivateConstructorForUtilityClass"
-        message="Utility class with non private constructor"
-        errorLine1="    private static final class CodepointIndexFinder {"
-        errorLine2="                               ~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiProcessor.java"
-            line="653"
-            column="32"/>
-    </issue>
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 19, the call containing class androidx.emoji2.text.EmojiProcessor.CodepointIndexFinder is not annotated with @RequiresApi(x) where x is at least 19. Either annotate the containing class with at least @RequiresApi(19) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(19)."
-        errorLine1="                if (!Character.isSurrogate(c)) {"
-        errorLine2="                               ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiProcessor.java"
-            line="701"
-            column="32"/>
-    </issue>
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 19, the call containing class androidx.emoji2.text.EmojiProcessor.CodepointIndexFinder is not annotated with @RequiresApi(x) where x is at least 19. Either annotate the containing class with at least @RequiresApi(19) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(19)."
-        errorLine1="                if (!Character.isSurrogate(c)) {"
-        errorLine2="                               ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiProcessor.java"
-            line="758"
-            column="32"/>
-    </issue>
-
-    <issue
-        id="KotlinPropertyAccess"
-        message="The getter return type (`int`) and setter parameter type (`boolean`) getter and setter methods for property `hasGlyph` should have exactly the same type to allow be accessed as a property from Kotlin; see https://android.github.io/kotlin-guides/interop.html#property-prefixes"
-        errorLine1="    public int getHasGlyph() {"
-        errorLine2="               ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiMetadata.java"
-            line="184"
-            column="16"/>
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiMetadata.java"
-            line="193"
-            column="17"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public static EmojiCompat init(@NonNull final Config config) {"
-        errorLine2="                  ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="302"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public static EmojiCompat reset(@NonNull final Config config) {"
-        errorLine2="                  ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="322"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public static EmojiCompat reset(final EmojiCompat emojiCompat) {"
-        errorLine2="                  ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="337"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public static EmojiCompat reset(final EmojiCompat emojiCompat) {"
-        errorLine2="                                          ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="337"
-            column="43"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public static EmojiCompat get() {"
-        errorLine2="                  ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="352"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="            final KeyEvent event) {"
-        errorLine2="                  ~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="541"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public CharSequence process(@NonNull final CharSequence charSequence) {"
-        errorLine2="           ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="625"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public CharSequence process(@NonNull final CharSequence charSequence,"
-        errorLine2="           ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="662"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public CharSequence process(@NonNull final CharSequence charSequence,"
-        errorLine2="           ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="698"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public CharSequence process(@NonNull final CharSequence charSequence,"
-        errorLine2="           ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="739"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="        public Config registerInitCallback(@NonNull InitCallback initCallback) {"
-        errorLine2="               ~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="982"
-            column="16"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="        public Config unregisterInitCallback(@NonNull InitCallback initCallback) {"
-        errorLine2="               ~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="1000"
-            column="16"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="        public Config setReplaceAll(final boolean replaceAll) {"
-        errorLine2="               ~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="1017"
-            column="16"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="        public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle) {"
-        errorLine2="               ~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="1037"
-            column="16"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="        public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle,"
-        errorLine2="               ~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="1057"
-            column="16"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="        public Config setEmojiSpanIndicatorEnabled(boolean emojiSpanIndicatorEnabled) {"
-        errorLine2="               ~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="1081"
-            column="16"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="        public Config setEmojiSpanIndicatorColor(@ColorInt int color) {"
-        errorLine2="               ~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="1092"
-            column="16"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="        public Config setMetadataLoadStrategy(@LoadStrategy int strategy) {"
-        errorLine2="               ~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="1133"
-            column="16"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="        protected final MetadataRepoLoader getMetadataRepoLoader() {"
-        errorLine2="                        ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
-            line="1154"
-            column="25"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public Typeface getTypeface() {"
-        errorLine2="           ~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiMetadata.java"
-            line="120"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public int getSize(@NonNull final Paint paint, final CharSequence text, final int start,"
-        errorLine2="                                                         ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiSpan.java"
-            line="77"
-            column="58"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="            final int end, final Paint.FontMetricsInt fm) {"
-        errorLine2="                                 ~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/EmojiSpan.java"
-            line="78"
-            column="34"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public FontRequestEmojiCompatConfig setHandler(Handler handler) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java"
-            line="143"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public FontRequestEmojiCompatConfig setHandler(Handler handler) {"
-        errorLine2="                                                   ~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java"
-            line="143"
-            column="52"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public FontRequestEmojiCompatConfig setRetryPolicy(RetryPolicy policy) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java"
-            line="156"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public FontRequestEmojiCompatConfig setRetryPolicy(RetryPolicy policy) {"
-        errorLine2="                                                       ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java"
-            line="156"
-            column="56"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="        public FontFamilyResult fetchFonts(@NonNull Context context,"
-        errorLine2="               ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java"
-            line="335"
-            column="16"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="        public Typeface buildTypeface(@NonNull Context context,"
-        errorLine2="               ~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java"
-            line="341"
-            column="16"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public static MetadataRepo create(@NonNull final Typeface typeface,"
-        errorLine2="                  ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/MetadataRepo.java"
-            line="103"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public static MetadataRepo create(@NonNull final Typeface typeface,"
-        errorLine2="                  ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/MetadataRepo.java"
-            line="115"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public static MetadataRepo create(@NonNull final AssetManager assetManager,"
-        errorLine2="                  ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/MetadataRepo.java"
-            line="127"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="            final String assetPath) throws IOException {"
-        errorLine2="                  ~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/MetadataRepo.java"
-            line="128"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public char[] getEmojiCharArray() {"
-        errorLine2="           ~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/MetadataRepo.java"
-            line="176"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public MetadataList getMetadataList() {"
-        errorLine2="           ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/MetadataRepo.java"
-            line="184"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public static SpannableBuilder create(@NonNull Class&lt;?> clazz, @NonNull CharSequence text) {"
-        errorLine2="                  ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="95"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public CharSequence subSequence(int start, int end) {"
-        errorLine2="           ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="122"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public void setSpan(Object what, int start, int end, int flags) {"
-        errorLine2="                        ~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="132"
-            column="25"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public &lt;T> T[] getSpans(int queryStart, int queryEnd, Class&lt;T> kind) {"
-        errorLine2="               ~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="147"
-            column="16"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public &lt;T> T[] getSpans(int queryStart, int queryEnd, Class&lt;T> kind) {"
-        errorLine2="                                                          ~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="147"
-            column="59"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public void removeSpan(Object what) {"
-        errorLine2="                           ~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="165"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public int getSpanStart(Object tag) {"
-        errorLine2="                            ~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="187"
-            column="29"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public int getSpanEnd(Object tag) {"
-        errorLine2="                          ~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="201"
-            column="27"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public int getSpanFlags(Object tag) {"
-        errorLine2="                            ~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="215"
-            column="29"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public int nextSpanTransition(int start, int limit, Class type) {"
-        errorLine2="                                                        ~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="229"
-            column="57"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public SpannableStringBuilder replace(int start, int end, CharSequence tb) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="299"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public SpannableStringBuilder replace(int start, int end, CharSequence tb) {"
-        errorLine2="                                                              ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="299"
-            column="63"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart,"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="307"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart,"
-        errorLine2="                                                              ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="307"
-            column="63"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public SpannableStringBuilder insert(int where, CharSequence tb) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="316"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public SpannableStringBuilder insert(int where, CharSequence tb) {"
-        errorLine2="                                                    ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="316"
-            column="53"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="322"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) {"
-        errorLine2="                                                    ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="322"
-            column="53"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public SpannableStringBuilder delete(int start, int end) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="328"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public SpannableStringBuilder append(CharSequence text) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="334"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public SpannableStringBuilder append(CharSequence text) {"
-        errorLine2="                                         ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="334"
-            column="42"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public SpannableStringBuilder append(char text) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="340"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public SpannableStringBuilder append(CharSequence text, int start, int end) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="346"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public SpannableStringBuilder append(CharSequence text, int start, int end) {"
-        errorLine2="                                         ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="346"
-            column="42"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public SpannableStringBuilder append(CharSequence text, Object what, int flags) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="352"
-            column="12"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public SpannableStringBuilder append(CharSequence text, Object what, int flags) {"
-        errorLine2="                                         ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="352"
-            column="42"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public SpannableStringBuilder append(CharSequence text, Object what, int flags) {"
-        errorLine2="                                                            ~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/SpannableBuilder.java"
-            line="352"
-            column="61"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public TypefaceEmojiSpan(final EmojiMetadata metadata) {"
-        errorLine2="                                   ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java"
-            line="48"
-            column="36"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="    public void draw(@NonNull final Canvas canvas, final CharSequence text,"
-        errorLine2="                                                         ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java"
-            line="53"
-            column="58"/>
-    </issue>
-
-</issues>
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java
index cead30d..3039f17 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java
@@ -15,12 +15,16 @@
  */
 package androidx.emoji2.text;
 
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+
+import android.graphics.Typeface;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
+import androidx.text.emoji.flatbuffer.MetadataList;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -35,7 +39,7 @@
 
     @Before
     public void clearResourceIndex() {
-        mMetadataRepo = new MetadataRepo();
+        mMetadataRepo = MetadataRepo.create(mock(Typeface.class), new MetadataList());
     }
 
     @Test(expected = NullPointerException.class)
@@ -63,10 +67,10 @@
         mMetadataRepo.put(metadata);
         assertSame(metadata, getNode(codePoint));
 
-        assertEquals(null, getNode(new int[]{1}));
-        assertEquals(null, getNode(new int[]{1, 2}));
-        assertEquals(null, getNode(new int[]{1, 2, 3}));
-        assertEquals(null, getNode(new int[]{1, 2, 3, 5}));
+        assertNull(getNode(new int[]{1}));
+        assertNull(getNode(new int[]{1, 2}));
+        assertNull(getNode(new int[]{1, 2, 3}));
+        assertNull(getNode(new int[]{1, 2, 3, 5}));
     }
 
     @Test
@@ -88,8 +92,8 @@
         assertSame(metadata2, getNode(codePoint2));
         assertSame(metadata3, getNode(codePoint3));
 
-        assertEquals(null, getNode(new int[]{1}));
-        assertEquals(null, getNode(new int[]{1, 2, 3, 4, 5}));
+        assertNull(getNode(new int[]{1}));
+        assertNull(getNode(new int[]{1, 2, 3, 4, 5}));
     }
 
     final EmojiMetadata getNode(final int[] codepoints) {
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestConfigBuilder.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestConfigBuilder.java
index 6753840..e44b5a4 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestConfigBuilder.java
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestConfigBuilder.java
@@ -16,13 +16,16 @@
 package androidx.emoji2.text;
 
 import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
 
 import android.content.Context;
 import android.content.res.AssetManager;
+import android.graphics.Typeface;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.test.core.app.ApplicationProvider;
+import androidx.text.emoji.flatbuffer.MetadataList;
 
 import java.util.concurrent.CountDownLatch;
 
@@ -85,7 +88,8 @@
                     try {
                         mLoaderLatch.await();
                         if (mSuccess) {
-                            loaderCallback.onLoaded(new MetadataRepo());
+                            loaderCallback.onLoaded(MetadataRepo.create(mock(Typeface.class),
+                                    new MetadataList()));
                         } else {
                             loaderCallback.onFailed(null);
                         }
diff --git a/emoji2/emoji2/src/androidTest/res/layout/activity_default.xml b/emoji2/emoji2/src/androidTest/res/layout/activity_default.xml
index c566d8f..486dfbd 100644
--- a/emoji2/emoji2/src/androidTest/res/layout/activity_default.xml
+++ b/emoji2/emoji2/src/androidTest/res/layout/activity_default.xml
@@ -1,11 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-              xmlns:app="http://schemas.android.com/apk/res-auto"
-              android:id="@+id/root"
-              android:layout_width="match_parent"
-              android:layout_height="match_parent"
-              android:orientation="vertical">
+    android:id="@+id/root"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
 
     <TextView
         android:id="@+id/text"
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
index 1dc1ac4..86c3588 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
@@ -197,12 +197,12 @@
     private static final Object INSTANCE_LOCK = new Object();
 
     @GuardedBy("INSTANCE_LOCK")
-    private static volatile EmojiCompat sInstance;
+    private static volatile @Nullable EmojiCompat sInstance;
 
-    private final ReadWriteLock mInitLock;
+    private final @NonNull ReadWriteLock mInitLock;
 
     @GuardedBy("mInitLock")
-    private final Set<InitCallback> mInitCallbacks;
+    private final @NonNull Set<InitCallback> mInitCallbacks;
 
     @GuardedBy("mInitLock")
     @LoadState
@@ -211,18 +211,18 @@
     /**
      * Handler with main looper to run the callbacks on.
      */
-    private final Handler mMainHandler;
+    private final @NonNull Handler mMainHandler;
 
     /**
      * Helper class for pre 19 compatibility.
      */
-    private final CompatInternal mHelper;
+    private final @NonNull CompatInternal mHelper;
 
     /**
      * Metadata loader instance given in the Config instance.
      */
     @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final MetadataRepoLoader mMetadataLoader;
+    final @NonNull MetadataRepoLoader mMetadataLoader;
 
     /**
      * @see Config#setReplaceAll(boolean)
@@ -240,7 +240,7 @@
      * @see Config#setUseEmojiAsDefaultStyle(boolean, List)
      */
     @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final int[] mEmojiAsDefaultStyleExceptions;
+    final @Nullable int[] mEmojiAsDefaultStyleExceptions;
 
     /**
      * @see Config#setEmojiSpanIndicatorEnabled(boolean)
@@ -299,15 +299,20 @@
      * @see EmojiCompat.Config
      */
     @SuppressWarnings("GuardedBy")
+    @NonNull
     public static EmojiCompat init(@NonNull final Config config) {
-        if (sInstance == null) {
+        EmojiCompat localInstance = sInstance;
+        if (localInstance == null) {
             synchronized (INSTANCE_LOCK) {
-                if (sInstance == null) {
-                    sInstance = new EmojiCompat(config);
+                // copy ref to local for nullness checker
+                localInstance = sInstance;
+                if (localInstance == null) {
+                    localInstance = new EmojiCompat(config);
+                    sInstance = localInstance;
                 }
             }
         }
-        return sInstance;
+        return localInstance;
     }
 
     /**
@@ -319,11 +324,13 @@
     @SuppressWarnings("GuardedBy")
     @RestrictTo(LIBRARY_GROUP_PREFIX)
     @VisibleForTesting
+    @NonNull
     public static EmojiCompat reset(@NonNull final Config config) {
         synchronized (INSTANCE_LOCK) {
-            sInstance = new EmojiCompat(config);
+            EmojiCompat localInstance = new EmojiCompat(config);
+            sInstance = localInstance;
+            return localInstance;
         }
-        return sInstance;
     }
 
     /**
@@ -334,7 +341,8 @@
     @SuppressWarnings("GuardedBy")
     @RestrictTo(LIBRARY_GROUP_PREFIX)
     @VisibleForTesting
-    public static EmojiCompat reset(final EmojiCompat emojiCompat) {
+    @Nullable
+    public static EmojiCompat reset(@Nullable final EmojiCompat emojiCompat) {
         synchronized (INSTANCE_LOCK) {
             sInstance = emojiCompat;
         }
@@ -349,11 +357,13 @@
      *
      * @throws IllegalStateException if called before {@link #init(EmojiCompat.Config)}
      */
+    @NonNull
     public static EmojiCompat get() {
         synchronized (INSTANCE_LOCK) {
-            Preconditions.checkState(sInstance != null,
+            EmojiCompat localInstance = sInstance;
+            Preconditions.checkState(localInstance != null,
                     "EmojiCompat is not initialized. Please call EmojiCompat.init() first");
-            return sInstance;
+            return localInstance;
         }
     }
 
@@ -538,7 +548,7 @@
      * @return {@code true} if an {@link EmojiSpan} is deleted
      */
     public static boolean handleOnKeyDown(@NonNull final Editable editable, final int keyCode,
-            final KeyEvent event) {
+            @NonNull final KeyEvent event) {
         if (Build.VERSION.SDK_INT >= 19) {
             return EmojiProcessor.handleOnKeyDown(editable, keyCode, event);
         } else {
@@ -621,11 +631,13 @@
      * @throws IllegalStateException if not initialized yet
      * @see #process(CharSequence, int, int)
      */
+    @Nullable
     @CheckResult
-    public CharSequence process(@NonNull final CharSequence charSequence) {
+    public CharSequence process(@Nullable final CharSequence charSequence) {
         // since charSequence might be null here we have to check it. Passing through here to the
         // main function so that it can do all the checks including isInitialized. It will also
         // be the main point that decides what to return.
+
         //noinspection ConstantConditions
         @IntRange(from = 0) final int length = charSequence == null ? 0 : charSequence.length();
         return process(charSequence, 0, length);
@@ -658,8 +670,9 @@
      *                                  {@code start > charSequence.length()},
      *                                  {@code end > charSequence.length()}
      */
+    @Nullable
     @CheckResult
-    public CharSequence process(@NonNull final CharSequence charSequence,
+    public CharSequence process(@Nullable final CharSequence charSequence,
             @IntRange(from = 0) final int start, @IntRange(from = 0) final int end) {
         return process(charSequence, start, end, EMOJI_COUNT_UNLIMITED);
     }
@@ -694,8 +707,9 @@
      *                                  {@code end > charSequence.length()}
      *                                  {@code maxEmojiCount < 0}
      */
+    @Nullable
     @CheckResult
-    public CharSequence process(@NonNull final CharSequence charSequence,
+    public CharSequence process(@Nullable final CharSequence charSequence,
             @IntRange(from = 0) final int start, @IntRange(from = 0) final int end,
             @IntRange(from = 0) final int maxEmojiCount) {
         return process(charSequence, start, end, maxEmojiCount, REPLACE_STRATEGY_DEFAULT);
@@ -735,8 +749,9 @@
      *                                  {@code end > charSequence.length()}
      *                                  {@code maxEmojiCount < 0}
      */
+    @Nullable
     @CheckResult
-    public CharSequence process(@NonNull final CharSequence charSequence,
+    public CharSequence process(@Nullable final CharSequence charSequence,
             @IntRange(from = 0) final int start, @IntRange(from = 0) final int end,
             @IntRange(from = 0) final int maxEmojiCount, @ReplaceStrategy int replaceStrategy) {
         Preconditions.checkState(isInitialized(), "Not initialized yet");
@@ -944,14 +959,17 @@
      */
     public abstract static class Config {
         @SuppressWarnings("WeakerAccess") /* synthetic access */
+        @NonNull
         final MetadataRepoLoader mMetadataLoader;
         @SuppressWarnings("WeakerAccess") /* synthetic access */
         boolean mReplaceAll;
         @SuppressWarnings("WeakerAccess") /* synthetic access */
         boolean mUseEmojiAsDefaultStyle;
         @SuppressWarnings("WeakerAccess") /* synthetic access */
+        @Nullable
         int[] mEmojiAsDefaultStyleExceptions;
         @SuppressWarnings("WeakerAccess") /* synthetic access */
+        @Nullable
         Set<InitCallback> mInitCallbacks;
         @SuppressWarnings("WeakerAccess") /* synthetic access */
         boolean mEmojiSpanIndicatorEnabled;
@@ -960,6 +978,7 @@
         @SuppressWarnings("WeakerAccess") /* synthetic access */
         @LoadStrategy int mMetadataLoadStrategy = LOAD_STRATEGY_DEFAULT;
         @SuppressWarnings("WeakerAccess") /* synthetic access */
+        @NonNull
         GlyphChecker mGlyphChecker = new EmojiProcessor.DefaultGlyphChecker();
 
         /**
@@ -979,6 +998,7 @@
          *
          * @return EmojiCompat.Config instance
          */
+        @NonNull
         public Config registerInitCallback(@NonNull InitCallback initCallback) {
             Preconditions.checkNotNull(initCallback, "initCallback cannot be null");
             if (mInitCallbacks == null) {
@@ -997,6 +1017,7 @@
          *
          * @return EmojiCompat.Config instance
          */
+        @NonNull
         public Config unregisterInitCallback(@NonNull InitCallback initCallback) {
             Preconditions.checkNotNull(initCallback, "initCallback cannot be null");
             if (mInitCallbacks != null) {
@@ -1014,6 +1035,7 @@
          *
          * @return EmojiCompat.Config instance
          */
+        @NonNull
         public Config setReplaceAll(final boolean replaceAll) {
             mReplaceAll = replaceAll;
             return this;
@@ -1034,6 +1056,7 @@
          * @param useEmojiAsDefaultStyle whether to use the emoji style presentation for all emojis
          *                               that would be presented as text style by default
          */
+        @NonNull
         public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle) {
             return setUseEmojiAsDefaultStyle(useEmojiAsDefaultStyle,
                     null);
@@ -1054,6 +1077,7 @@
          *                                      {@link #setUseEmojiAsDefaultStyle(boolean)} should
          *                                      be used instead.
          */
+        @NonNull
         public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle,
                 @Nullable final List<Integer> emojiAsDefaultStyleExceptions) {
             mUseEmojiAsDefaultStyle = useEmojiAsDefaultStyle;
@@ -1078,6 +1102,7 @@
          * @param emojiSpanIndicatorEnabled when {@code true} a background is drawn for each emoji
          *                                  that is replaced
          */
+        @NonNull
         public Config setEmojiSpanIndicatorEnabled(boolean emojiSpanIndicatorEnabled) {
             mEmojiSpanIndicatorEnabled = emojiSpanIndicatorEnabled;
             return this;
@@ -1089,6 +1114,7 @@
          *
          * @see #setEmojiSpanIndicatorEnabled(boolean)
          */
+        @NonNull
         public Config setEmojiSpanIndicatorColor(@ColorInt int color) {
             mEmojiSpanIndicatorColor = color;
             return this;
@@ -1130,6 +1156,7 @@
          *                  {@link EmojiCompat#LOAD_STRATEGY_MANUAL}
          *
          */
+        @NonNull
         public Config setMetadataLoadStrategy(@LoadStrategy int strategy) {
             mMetadataLoadStrategy = strategy;
             return this;
@@ -1151,6 +1178,7 @@
         /**
          * Returns the {@link MetadataRepoLoader}.
          */
+        @NonNull
         protected final MetadataRepoLoader getMetadataRepoLoader() {
             return mMetadataLoader;
         }
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiMetadata.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiMetadata.java
index ba46f82..d6ef218 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiMetadata.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiMetadata.java
@@ -17,6 +17,7 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
 
+import android.annotation.SuppressLint;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.Typeface;
@@ -78,6 +79,7 @@
     /**
      * MetadataRepo that holds this instance.
      */
+    @NonNull
     private final MetadataRepo mMetadataRepo;
 
     /**
@@ -117,6 +119,7 @@
     /**
      * @return return typeface to be used to render this metadata
      */
+    @NonNull
     public Typeface getTypeface() {
         return mMetadataRepo.getTypeface();
     }
@@ -181,6 +184,7 @@
      * style selector 0xFE0F)
      */
     @HasGlyph
+    @SuppressLint("KotlinPropertyAccess")
     public int getHasGlyph() {
         return mHasGlyph;
     }
@@ -190,6 +194,7 @@
      *
      * @param hasGlyph {@code true} if system can render the emoji
      */
+    @SuppressLint("KotlinPropertyAccess")
     public void setHasGlyph(boolean hasGlyph) {
         mHasGlyph = hasGlyph ? HAS_GLYPH_EXISTS : HAS_GLYPH_ABSENT;
     }
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiProcessor.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiProcessor.java
index ff25b2e..98849eb 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiProcessor.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiProcessor.java
@@ -80,16 +80,19 @@
     /**
      * Factory used to create EmojiSpans.
      */
+    @NonNull
     private final EmojiCompat.SpanFactory mSpanFactory;
 
     /**
      * Emoji metadata repository.
      */
+    @NonNull
     private final MetadataRepo mMetadataRepo;
 
     /**
      * Utility class that checks if the system can render a given glyph.
      */
+    @NonNull
     private EmojiCompat.GlyphChecker mGlyphChecker;
 
     /**
@@ -100,6 +103,7 @@
     /**
      * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean, List)
      */
+    @Nullable
     private final int[] mEmojiAsDefaultStyleExceptions;
 
     EmojiProcessor(
@@ -297,7 +301,7 @@
      * @return {@code true} if an {@link EmojiSpan} is deleted
      */
     static boolean handleOnKeyDown(@NonNull final Editable editable, final int keyCode,
-            final KeyEvent event) {
+            @NonNull final KeyEvent event) {
         final boolean handled;
         switch (keyCode) {
             case KeyEvent.KEYCODE_DEL:
@@ -319,7 +323,7 @@
         return false;
     }
 
-    private static boolean delete(final Editable content, final KeyEvent event,
+    private static boolean delete(@NonNull final Editable content, @NonNull final KeyEvent event,
             final boolean forwardDelete) {
         if (hasModifiers(event)) {
             return false;
@@ -430,7 +434,7 @@
         return start == -1 || end == -1 || start != end;
     }
 
-    private static boolean hasModifiers(KeyEvent event) {
+    private static boolean hasModifiers(@NonNull KeyEvent event) {
         return !KeyEvent.metaStateHasNoModifiers(event.getMetaState());
     }
 
@@ -650,9 +654,12 @@
     /**
      * Copy of BaseInputConnection findIndexBackward and findIndexForward functions.
      */
+    @RequiresApi(19)
     private static final class CodepointIndexFinder {
         private static final int INVALID_INDEX = -1;
 
+        private CodepointIndexFinder() {}
+
         /**
          * Find start index of the character in {@code cs} that is {@code numCodePoints} behind
          * starting from {@code from}.
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java
index 70dd537..dd07ff1 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java
@@ -17,10 +17,12 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
 
+import android.annotation.SuppressLint;
 import android.graphics.Paint;
 import android.text.style.ReplacementSpan;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
@@ -43,6 +45,7 @@
      * representing same emoji to be in memory. When unparcelled, EmojiSpan tries to set it back
      * using the singleton EmojiCompat instance.
      */
+    @NonNull
     private final EmojiMetadata mMetadata;
 
     /**
@@ -74,8 +77,11 @@
     }
 
     @Override
-    public int getSize(@NonNull final Paint paint, final CharSequence text, final int start,
-            final int end, final Paint.FontMetricsInt fm) {
+    public int getSize(@NonNull final Paint paint,
+            @SuppressLint("UnknownNullness") final CharSequence text,
+            final int start,
+            final int end,
+            @Nullable final Paint.FontMetricsInt fm) {
         paint.getFontMetricsInt(mTmpFontMetrics);
         final int fontHeight = Math.abs(mTmpFontMetrics.descent - mTmpFontMetrics.ascent);
 
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java
index 4ade79f..f20049c 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java
@@ -140,7 +140,8 @@
      *               of {@code null}, the metadata loader creates own {@link HandlerThread} for
      *               initialization.
      */
-    public FontRequestEmojiCompatConfig setHandler(Handler handler) {
+    @NonNull
+    public FontRequestEmojiCompatConfig setHandler(@Nullable Handler handler) {
         ((FontRequestMetadataLoader) getMetadataRepoLoader()).setHandler(handler);
         return this;
     }
@@ -153,7 +154,8 @@
      *              file. Can be {@code null}. In case of {@code null}, the metadata loader never
      *              retries.
      */
-    public FontRequestEmojiCompatConfig setRetryPolicy(RetryPolicy policy) {
+    @NonNull
+    public FontRequestEmojiCompatConfig setRetryPolicy(@Nullable RetryPolicy policy) {
         ((FontRequestMetadataLoader) getMetadataRepoLoader()).setRetryPolicy(policy);
         return this;
     }
@@ -163,23 +165,23 @@
      * given FontRequest.
      */
     private static class FontRequestMetadataLoader implements EmojiCompat.MetadataRepoLoader {
-        private final Context mContext;
-        private final FontRequest mRequest;
-        private final FontProviderHelper mFontProviderHelper;
+        private final @NonNull Context mContext;
+        private final @NonNull FontRequest mRequest;
+        private final @NonNull FontProviderHelper mFontProviderHelper;
 
-        private final Object mLock = new Object();
+        private final @NonNull Object mLock = new Object();
         @GuardedBy("mLock")
-        private Handler mHandler;
+        private @Nullable Handler mHandler;
         @GuardedBy("mLock")
-        private HandlerThread mThread;
+        private @Nullable HandlerThread mThread;
         @GuardedBy("mLock")
         private @Nullable RetryPolicy mRetryPolicy;
 
         // Following three variables must be touched only on the thread associated with mHandler.
         @SuppressWarnings("WeakerAccess") /* synthetic access */
         EmojiCompat.MetadataRepoLoaderCallback mCallback;
-        private ContentObserver mObserver;
-        private Runnable mHandleMetadataCreationRunner;
+        private @Nullable ContentObserver mObserver;
+        private @Nullable Runnable mHandleMetadataCreationRunner;
 
         FontRequestMetadataLoader(@NonNull Context context, @NonNull FontRequest request,
                 @NonNull FontProviderHelper fontProviderHelper) {
@@ -190,13 +192,13 @@
             mFontProviderHelper = fontProviderHelper;
         }
 
-        public void setHandler(Handler handler) {
+        public void setHandler(@Nullable Handler handler) {
             synchronized (mLock) {
                 mHandler = handler;
             }
         }
 
-        public void setRetryPolicy(RetryPolicy policy) {
+        public void setRetryPolicy(@Nullable RetryPolicy policy) {
             synchronized (mLock) {
                 mRetryPolicy = policy;
             }
@@ -332,12 +334,14 @@
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
     public static class FontProviderHelper {
         /** Calls FontsContractCompat.fetchFonts. */
+        @NonNull
         public FontFamilyResult fetchFonts(@NonNull Context context,
                 @NonNull FontRequest request) throws NameNotFoundException {
             return FontsContractCompat.fetchFonts(context, null /* cancellation signal */, request);
         }
 
         /** Calls FontsContractCompat.buildTypeface. */
+        @Nullable
         public Typeface buildTypeface(@NonNull Context context,
                 @NonNull FontsContractCompat.FontInfo font) throws NameNotFoundException {
             return FontsContractCompat.buildTypeface(context, null /* cancellation signal */,
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataListReader.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataListReader.java
index f9f397f..6db7379 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataListReader.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataListReader.java
@@ -21,6 +21,7 @@
 
 import androidx.annotation.AnyThread;
 import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.text.emoji.flatbuffer.MetadataList;
@@ -236,9 +237,9 @@
      */
     private static class InputStreamOpenTypeReader implements OpenTypeReader {
 
-        private final byte[] mByteArray;
-        private final ByteBuffer mByteBuffer;
-        private final InputStream mInputStream;
+        private final @NonNull byte[] mByteArray;
+        private final @NonNull ByteBuffer mByteBuffer;
+        private final @NonNull InputStream mInputStream;
         private long mPosition = 0;
 
         /**
@@ -247,7 +248,7 @@
          *
          * @param inputStream InputStream to read from
          */
-        InputStreamOpenTypeReader(final InputStream inputStream) {
+        InputStreamOpenTypeReader(@NonNull final InputStream inputStream) {
             mInputStream = inputStream;
             mByteArray = new byte[UINT32_BYTE_COUNT];
             mByteBuffer = ByteBuffer.wrap(mByteArray);
@@ -306,14 +307,14 @@
      */
     private static class ByteBufferReader implements OpenTypeReader {
 
-        private final ByteBuffer mByteBuffer;
+        private final @NonNull ByteBuffer mByteBuffer;
 
         /**
          * Constructs the reader with the given ByteBuffer.
          *
          * @param byteBuffer ByteBuffer to read from
          */
-        ByteBufferReader(final ByteBuffer byteBuffer) {
+        ByteBufferReader(@NonNull final ByteBuffer byteBuffer) {
             mByteBuffer = byteBuffer;
             mByteBuffer.order(ByteOrder.BIG_ENDIAN);
         }
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java
index 2f5ea66..00fef53 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java
@@ -47,36 +47,23 @@
     /**
      * MetadataList that contains the emoji metadata.
      */
-    private final MetadataList mMetadataList;
+    private final @NonNull MetadataList mMetadataList;
 
     /**
      * char presentation of all EmojiMetadata's in a single array. All emojis we have are mapped to
      * Private Use Area A, in the range U+F0000..U+FFFFD. Therefore each emoji takes 2 chars.
      */
-    private final char[] mEmojiCharArray;
+    private final @NonNull char[] mEmojiCharArray;
 
     /**
      * Empty root node of the trie.
      */
-    private final Node mRootNode;
+    private final @NonNull Node mRootNode;
 
     /**
      * Typeface to be used to render emojis.
      */
-    private final Typeface mTypeface;
-
-    /**
-     * Constructor used for tests.
-     *
-     * @hide
-     */
-    @RestrictTo(LIBRARY_GROUP_PREFIX)
-    MetadataRepo() {
-        mTypeface = null;
-        mMetadataList = null;
-        mRootNode = new Node(DEFAULT_ROOT_SIZE);
-        mEmojiCharArray = new char[0];
-    }
+    private final @NonNull Typeface mTypeface;
 
     /**
      * Private constructor that is called by one of {@code create} methods.
@@ -94,12 +81,22 @@
     }
 
     /**
+     * Construct MetadataRepo from a preloaded MetadatList.
+     */
+    @NonNull
+    static MetadataRepo create(@NonNull final Typeface typeface,
+            @NonNull final MetadataList metadataList) {
+        return new MetadataRepo(typeface, metadataList);
+    }
+
+    /**
      * Construct MetadataRepo from an input stream. The library does not close the given
      * InputStream, therefore it is caller's responsibility to properly close the stream.
      *
      * @param typeface Typeface to be used to render emojis
      * @param inputStream InputStream to read emoji metadata from
      */
+    @NonNull
     public static MetadataRepo create(@NonNull final Typeface typeface,
             @NonNull final InputStream inputStream) throws IOException {
         return new MetadataRepo(typeface, MetadataListReader.read(inputStream));
@@ -112,6 +109,7 @@
      * @param typeface Typeface to be used to render emojis
      * @param byteBuffer ByteBuffer to read emoji metadata from
      */
+    @NonNull
     public static MetadataRepo create(@NonNull final Typeface typeface,
             @NonNull final ByteBuffer byteBuffer) throws IOException {
         return new MetadataRepo(typeface, MetadataListReader.read(byteBuffer));
@@ -124,8 +122,9 @@
      * @param assetPath asset manager path of the file that the Typeface and metadata will be
      *                  created from
      */
+    @NonNull
     public static MetadataRepo create(@NonNull final AssetManager assetManager,
-            final String assetPath) throws IOException {
+            @NonNull final String assetPath) throws IOException {
         final Typeface typeface = Typeface.createFromAsset(assetManager, assetPath);
         return new MetadataRepo(typeface, MetadataListReader.read(assetManager, assetPath));
     }
@@ -148,6 +147,7 @@
     /**
      * @hide
      */
+    @NonNull
     @RestrictTo(LIBRARY_GROUP_PREFIX)
     Typeface getTypeface() {
         return mTypeface;
@@ -164,6 +164,7 @@
     /**
      * @hide
      */
+    @NonNull
     @RestrictTo(LIBRARY_GROUP_PREFIX)
     Node getRootNode() {
         return mRootNode;
@@ -172,6 +173,7 @@
     /**
      * @hide
      */
+    @NonNull
     @RestrictTo(LIBRARY_GROUP_PREFIX)
     public char[] getEmojiCharArray() {
         return mEmojiCharArray;
@@ -180,6 +182,7 @@
     /**
      * @hide
      */
+    @NonNull
     @RestrictTo(LIBRARY_GROUP_PREFIX)
     public MetadataList getMetadataList() {
         return mMetadataList;
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/SpannableBuilder.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/SpannableBuilder.java
index 73a6392..01399a4 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/SpannableBuilder.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/SpannableBuilder.java
@@ -17,6 +17,7 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
 
+import android.annotation.SuppressLint;
 import android.text.Editable;
 import android.text.SpanWatcher;
 import android.text.Spannable;
@@ -51,12 +52,12 @@
     /**
      * DynamicLayout$ChangeWatcher class.
      */
-    private final Class<?> mWatcherClass;
+    private final @NonNull Class<?> mWatcherClass;
 
     /**
      * All WatcherWrappers.
      */
-    private final List<WatcherWrapper> mWatchers = new ArrayList<>();
+    private final @NonNull List<WatcherWrapper> mWatchers = new ArrayList<>();
 
     /**
      * @hide
@@ -91,6 +92,7 @@
     /**
      * @hide
      */
+    @NonNull
     @RestrictTo(LIBRARY_GROUP_PREFIX)
     public static SpannableBuilder create(@NonNull Class<?> clazz, @NonNull CharSequence text) {
         return new SpannableBuilder(clazz, text);
@@ -118,6 +120,7 @@
         return mWatcherClass == clazz;
     }
 
+    @SuppressLint("UnknownNullness")
     @Override
     public CharSequence subSequence(int start, int end) {
         return new SpannableBuilder(mWatcherClass, this, start, end);
@@ -129,7 +132,7 @@
      * this new mObject as the span.
      */
     @Override
-    public void setSpan(Object what, int start, int end, int flags) {
+    public void setSpan(@Nullable Object what, int start, int end, int flags) {
         if (isWatcher(what)) {
             final WatcherWrapper span = new WatcherWrapper(what);
             mWatchers.add(span);
@@ -142,9 +145,10 @@
      * If previously a DynamicLayout$ChangeWatcher was wrapped in a WatcherWrapper, return the
      * correct Object that the client has set.
      */
+    @SuppressLint("UnknownNullness")
     @SuppressWarnings("unchecked")
     @Override
-    public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) {
+    public <T> T[] getSpans(int queryStart, int queryEnd, @NonNull Class<T> kind) {
         if (isWatcher(kind)) {
             final WatcherWrapper[] spans = super.getSpans(queryStart, queryEnd,
                     WatcherWrapper.class);
@@ -162,7 +166,7 @@
      * instead.
      */
     @Override
-    public void removeSpan(Object what) {
+    public void removeSpan(@Nullable Object what) {
         final WatcherWrapper watcher;
         if (isWatcher(what)) {
             watcher = getWatcherFor(what);
@@ -184,7 +188,7 @@
      * Return the correct start for the DynamicLayout$ChangeWatcher span.
      */
     @Override
-    public int getSpanStart(Object tag) {
+    public int getSpanStart(@Nullable Object tag) {
         if (isWatcher(tag)) {
             final WatcherWrapper watcher = getWatcherFor(tag);
             if (watcher != null) {
@@ -198,7 +202,7 @@
      * Return the correct end for the DynamicLayout$ChangeWatcher span.
      */
     @Override
-    public int getSpanEnd(Object tag) {
+    public int getSpanEnd(@Nullable Object tag) {
         if (isWatcher(tag)) {
             final WatcherWrapper watcher = getWatcherFor(tag);
             if (watcher != null) {
@@ -212,7 +216,7 @@
      * Return the correct flags for the DynamicLayout$ChangeWatcher span.
      */
     @Override
-    public int getSpanFlags(Object tag) {
+    public int getSpanFlags(@Nullable Object tag) {
         if (isWatcher(tag)) {
             final WatcherWrapper watcher = getWatcherFor(tag);
             if (watcher != null) {
@@ -226,8 +230,8 @@
      * Return the correct transition for the DynamicLayout$ChangeWatcher span.
      */
     @Override
-    public int nextSpanTransition(int start, int limit, Class type) {
-        if (isWatcher(type)) {
+    public int nextSpanTransition(int start, int limit, @Nullable Class type) {
+        if (type == null || isWatcher(type)) {
             type = WatcherWrapper.class;
         }
         return super.nextSpanTransition(start, limit, type);
@@ -295,6 +299,7 @@
         }
     }
 
+    @SuppressLint("UnknownNullness")
     @Override
     public SpannableStringBuilder replace(int start, int end, CharSequence tb) {
         blockWatchers();
@@ -303,6 +308,7 @@
         return this;
     }
 
+    @SuppressLint("UnknownNullness")
     @Override
     public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart,
             int tbend) {
@@ -312,42 +318,51 @@
         return this;
     }
 
+    @SuppressLint("UnknownNullness")
     @Override
     public SpannableStringBuilder insert(int where, CharSequence tb) {
         super.insert(where, tb);
         return this;
     }
 
+    @SuppressLint("UnknownNullness")
     @Override
     public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) {
         super.insert(where, tb, start, end);
         return this;
     }
 
+    @SuppressLint("UnknownNullness")
     @Override
     public SpannableStringBuilder delete(int start, int end) {
         super.delete(start, end);
         return this;
     }
 
+    @NonNull
     @Override
-    public SpannableStringBuilder append(CharSequence text) {
+    public SpannableStringBuilder append(@SuppressLint("UnknownNullness") CharSequence text) {
         super.append(text);
         return this;
     }
 
+    @NonNull
     @Override
     public SpannableStringBuilder append(char text) {
         super.append(text);
         return this;
     }
 
+    @NonNull
     @Override
-    public SpannableStringBuilder append(CharSequence text, int start, int end) {
+    public SpannableStringBuilder append(@SuppressLint("UnknownNullness") CharSequence text,
+            int start,
+            int end) {
         super.append(text, start, end);
         return this;
     }
 
+    @SuppressLint("UnknownNullness")
     @Override
     public SpannableStringBuilder append(CharSequence text, Object what, int flags) {
         super.append(text, what, flags);
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java
index 2c3ab58..5eb3ff9 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java
@@ -17,12 +17,14 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
 
+import android.annotation.SuppressLint;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.text.TextPaint;
 
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 
@@ -38,19 +40,20 @@
     /**
      * Paint object used to draw a background in debug mode.
      */
-    private static Paint sDebugPaint;
+    private static @Nullable Paint sDebugPaint;
 
     /**
      * Default constructor.
      *
      * @param metadata metadata representing the emoji that this span will draw
      */
-    public TypefaceEmojiSpan(final EmojiMetadata metadata) {
+    public TypefaceEmojiSpan(final @NonNull EmojiMetadata metadata) {
         super(metadata);
     }
 
     @Override
-    public void draw(@NonNull final Canvas canvas, final CharSequence text,
+    public void draw(@NonNull final Canvas canvas,
+            @SuppressLint("UnknownNullness") final CharSequence text,
             @IntRange(from = 0) final int start, @IntRange(from = 0) final int end, final float x,
             final int top, final int y, final int bottom, @NonNull final Paint paint) {
         if (EmojiCompat.get().isEmojiSpanIndicatorEnabled()) {
@@ -59,6 +62,7 @@
         getMetadata().draw(canvas, x, y, paint);
     }
 
+    @NonNull
     private static Paint getDebugPaint() {
         if (sDebugPaint == null) {
             sDebugPaint = new TextPaint();
diff --git a/fragment/fragment/api/current.txt b/fragment/fragment/api/current.txt
index a700d16..b850c56 100644
--- a/fragment/fragment/api/current.txt
+++ b/fragment/fragment/api/current.txt
@@ -427,6 +427,8 @@
     field public static final int TRANSIT_EXIT_MASK = 8192; // 0x2000
     field public static final int TRANSIT_FRAGMENT_CLOSE = 8194; // 0x2002
     field public static final int TRANSIT_FRAGMENT_FADE = 4099; // 0x1003
+    field public static final int TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE = 8197; // 0x2005
+    field public static final int TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN = 4100; // 0x1004
     field public static final int TRANSIT_FRAGMENT_OPEN = 4097; // 0x1001
     field public static final int TRANSIT_NONE = 0; // 0x0
     field public static final int TRANSIT_UNSET = -1; // 0xffffffff
diff --git a/fragment/fragment/api/public_plus_experimental_current.txt b/fragment/fragment/api/public_plus_experimental_current.txt
index a2f6d9f..97a49c7 100644
--- a/fragment/fragment/api/public_plus_experimental_current.txt
+++ b/fragment/fragment/api/public_plus_experimental_current.txt
@@ -433,6 +433,8 @@
     field public static final int TRANSIT_EXIT_MASK = 8192; // 0x2000
     field public static final int TRANSIT_FRAGMENT_CLOSE = 8194; // 0x2002
     field public static final int TRANSIT_FRAGMENT_FADE = 4099; // 0x1003
+    field public static final int TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE = 8197; // 0x2005
+    field public static final int TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN = 4100; // 0x1004
     field public static final int TRANSIT_FRAGMENT_OPEN = 4097; // 0x1001
     field public static final int TRANSIT_NONE = 0; // 0x0
     field public static final int TRANSIT_UNSET = -1; // 0xffffffff
diff --git a/fragment/fragment/api/restricted_current.txt b/fragment/fragment/api/restricted_current.txt
index 060b006..ab90399 100644
--- a/fragment/fragment/api/restricted_current.txt
+++ b/fragment/fragment/api/restricted_current.txt
@@ -435,6 +435,8 @@
     field public static final int TRANSIT_EXIT_MASK = 8192; // 0x2000
     field public static final int TRANSIT_FRAGMENT_CLOSE = 8194; // 0x2002
     field public static final int TRANSIT_FRAGMENT_FADE = 4099; // 0x1003
+    field public static final int TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE = 8197; // 0x2005
+    field public static final int TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN = 4100; // 0x1004
     field public static final int TRANSIT_FRAGMENT_OPEN = 4097; // 0x1001
     field public static final int TRANSIT_NONE = 0; // 0x0
     field public static final int TRANSIT_UNSET = -1; // 0xffffffff
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitTest.kt
index 1ded023..0f9a36a 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitTest.kt
@@ -18,6 +18,9 @@
 
 import android.animation.Animator
 import android.view.animation.Animation
+import androidx.annotation.LayoutRes
+import androidx.fragment.app.FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE
+import androidx.fragment.app.FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN
 import androidx.fragment.app.test.FragmentTestActivity
 import androidx.fragment.test.R
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -35,7 +38,6 @@
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class FragmentTransitTest {
-
     @Suppress("DEPRECATION")
     @get:Rule
     var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
@@ -62,11 +64,57 @@
             .containsExactly(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
     }
 
-    class TransitFragment : StrictViewFragment() {
+    @Test
+    fun testFragmentAnimationWithActivityTransition() {
+        val fragmentA = FragmentA()
+        val fragmentB = FragmentB()
+        val fm = activityRule.activity.supportFragmentManager
+
+        // set TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN to navigate forward when fragmentA entering.
+        fm.beginTransaction()
+            .add(R.id.fragmentContainer, fragmentA)
+            .setTransition(TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN)
+            .commit()
+        activityRule.executePendingTransactions(fm)
+
+        assertThat(fragmentA.transitValues).containsExactly(TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN)
+        assertThat(fragmentA.isEnterTransit).isTrue()
+
+        // set TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN to navigate forward when fragmentB entering
+        // and fragmentA exiting.
+        fm.beginTransaction()
+            .replace(R.id.fragmentContainer, fragmentB)
+            .setTransition(TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN)
+            .addToBackStack(null)
+            .commit()
+        activityRule.executePendingTransactions(fm)
+
+        assertThat(fragmentA.transitValues).containsExactly(TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN)
+        assertThat(fragmentA.isEnterTransit).isFalse()
+        assertThat(fragmentB.transitValues).containsExactly(TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN)
+        assertThat(fragmentB.isEnterTransit).isTrue()
+
+        // Simulating back key with popBackStack, system will set
+        // TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE to navigate backward when fragmentB exiting
+        // and fragmentA entering.
+        fm.popBackStack()
+        activityRule.executePendingTransactions(fm)
+
+        assertThat(fragmentB.transitValues).contains(TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE)
+        assertThat(fragmentB.isEnterTransit).isFalse()
+        assertThat(fragmentA.transitValues).contains(TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE)
+        assertThat(fragmentA.isEnterTransit).isTrue()
+    }
+
+    public open class TransitFragment(
+        @LayoutRes contentLayoutId: Int = R.layout.strict_view_fragment
+    ) : StrictFragment(contentLayoutId) {
         val transitValues = mutableSetOf<Int>()
+        var isEnterTransit: Boolean = false
 
         override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
             transitValues += transit
+            isEnterTransit = enter
             return super.onCreateAnimation(transit, enter, nextAnim)
         }
 
@@ -75,4 +123,8 @@
             return super.onCreateAnimator(transit, enter, nextAnim)
         }
     }
+
+    class FragmentA : TransitFragment(R.layout.fragment_a)
+
+    class FragmentB : TransitFragment(R.layout.fragment_b)
 }
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentAnim.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentAnim.java
index 76f34ef..df536f1 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentAnim.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentAnim.java
@@ -21,6 +21,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.content.Context;
 import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.animation.Animation;
@@ -60,6 +61,7 @@
         if (fragment.mContainer != null && fragment.mContainer.getLayoutTransition() != null) {
             return null;
         }
+
         Animation animation = fragment.onCreateAnimation(transit, enter, nextAnim);
         if (animation != null) {
             return new AnimationOrAnimator(animation);
@@ -71,10 +73,9 @@
         }
 
         if (nextAnim == 0 && transit != 0) {
-            nextAnim = transitToAnimResourceId(transit, enter);
+            nextAnim = transitToAnimResourceId(context, transit, enter);
         }
 
-
         if (nextAnim != 0) {
             String dir = context.getResources().getResourceTypeName(nextAnim);
             boolean isAnim = "anim".equals(dir);
@@ -195,7 +196,8 @@
     }
 
     @AnimRes
-    private static int transitToAnimResourceId(int transit, boolean enter) {
+    private static int transitToAnimResourceId(@NonNull Context context, int transit,
+            boolean enter) {
         int animAttr = -1;
         switch (transit) {
             case FragmentTransaction.TRANSIT_FRAGMENT_OPEN:
@@ -207,10 +209,32 @@
             case FragmentTransaction.TRANSIT_FRAGMENT_FADE:
                 animAttr = enter ? R.animator.fragment_fade_enter : R.animator.fragment_fade_exit;
                 break;
+            case FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN:
+                animAttr = enter
+                        ? toActivityTransitResId(context, android.R.attr.activityOpenEnterAnimation)
+                        : toActivityTransitResId(context, android.R.attr.activityOpenExitAnimation);
+                break;
+            case FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE:
+                animAttr = enter
+                        ? toActivityTransitResId(context,
+                        android.R.attr.activityCloseEnterAnimation)
+                        : toActivityTransitResId(context,
+                                android.R.attr.activityCloseExitAnimation);
+                break;
         }
         return animAttr;
     }
 
+    @AnimRes
+    private static int toActivityTransitResId(@NonNull Context context, int attrInt) {
+        int resId;
+        TypedArray typedArray = context.obtainStyledAttributes(
+                android.R.style.Animation_Activity, new int[]{attrInt});
+        resId = typedArray.getResourceId(0, View.NO_ID);
+        typedArray.recycle();
+        return resId;
+    }
+
     /**
      * Contains either an animator or animation. One of these should be null.
      */
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index 811156c..6abf46e 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -3618,6 +3618,12 @@
             case FragmentTransaction.TRANSIT_FRAGMENT_FADE:
                 rev = FragmentTransaction.TRANSIT_FRAGMENT_FADE;
                 break;
+            case FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN:
+                rev = FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE;
+                break;
+            case FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE:
+                rev = FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN;
+                break;
         }
         return rev;
 
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java
index 0140639..b94e94c 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java
@@ -504,7 +504,8 @@
 
     /** @hide */
     @RestrictTo(LIBRARY_GROUP_PREFIX)
-    @IntDef({TRANSIT_NONE, TRANSIT_FRAGMENT_OPEN, TRANSIT_FRAGMENT_CLOSE, TRANSIT_FRAGMENT_FADE})
+    @IntDef({TRANSIT_NONE, TRANSIT_FRAGMENT_OPEN, TRANSIT_FRAGMENT_CLOSE, TRANSIT_FRAGMENT_FADE,
+            TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN, TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE})
     @Retention(RetentionPolicy.SOURCE)
     private @interface Transit {}
 
@@ -521,6 +522,22 @@
     public static final int TRANSIT_FRAGMENT_FADE = 3 | TRANSIT_ENTER_MASK;
 
     /**
+     * Fragment is being added onto the stack with Activity open transition.
+     *
+     * @see android.R.attr#activityOpenEnterAnimation
+     * @see android.R.attr#activityOpenExitAnimation
+     */
+    public static final int TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN = 4 | TRANSIT_ENTER_MASK;
+
+    /**
+     * Fragment is being removed from the stack with Activity close transition.
+     *
+     * @see android.R.attr#activityCloseEnterAnimation
+     * @see android.R.attr#activityCloseExitAnimation
+     */
+    public static final int TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE = 5 | TRANSIT_EXIT_MASK;
+
+    /**
      * Set specific animation resources to run for the fragments that are
      * entering and exiting in this transaction. These animations will not be
      * played when popping the back stack.
diff --git a/jetifier/jetifier/migration.config b/jetifier/jetifier/migration.config
index bd39e34..de39296 100644
--- a/jetifier/jetifier/migration.config
+++ b/jetifier/jetifier/migration.config
@@ -485,11 +485,7 @@
       "to": "android/support/v4/app/SpecialEffectsController{0}"
     },
     {
-      "from": "androidx/fragment/app/strictmode/FragmentStrictMode(.*)",
-      "to": "ignore"
-    },
-    {
-      "from": "androidx/fragment/app/strictmode/Violation(.*)",
+      "from": "androidx/fragment/app/strictmode/(.*)",
       "to": "ignore"
     },
     {
diff --git a/lint-checks/build.gradle b/lint-checks/build.gradle
index 470f379..5733dc1 100644
--- a/lint-checks/build.gradle
+++ b/lint-checks/build.gradle
@@ -23,6 +23,14 @@
     id("kotlin")
 }
 
+sourceSets {
+    // Pull integration test source code in for use by lint testing framework.
+    test.resources.srcDirs(
+            project(":lint-checks:integration-tests")
+                    .projectDir.absolutePath + "/src/main"
+    )
+}
+
 dependencies {
     compileOnly(LINT_API_LATEST)
     compileOnly(LINT_CHECKS_LATEST)
diff --git a/lint-checks/integration-tests/build.gradle b/lint-checks/integration-tests/build.gradle
index 825602b..50a9b49 100644
--- a/lint-checks/integration-tests/build.gradle
+++ b/lint-checks/integration-tests/build.gradle
@@ -17,22 +17,31 @@
 import androidx.build.BuildOnServerKt
 import androidx.build.dependencyTracker.AffectedModuleDetector
 import androidx.build.uptodatedness.EnableCachingKt
-import org.apache.tools.ant.filters.ReplaceTokens
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 import static androidx.build.dependencies.DependenciesKt.KOTLIN_STDLIB
 
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
-    id("AndroidXUiPlugin")
-    id("org.jetbrains.kotlin.android")
+    id("kotlin-android")
 }
 
 dependencies {
-    implementation("androidx.annotation:annotation:1.0.0")
+    implementation(projectOrArtifact(":annotation:annotation"))
     implementation(KOTLIN_STDLIB)
 }
 
+// Allow usage of Kotlin's @Experimental and @RequiresOptIn annotations.
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += [
+                "-Xuse-experimental=kotlin.Experimental",
+                "-Xopt-in=kotlin.RequiresOptIn",
+        ]
+    }
+}
+
 androidx {
     name = "Lint Checks Integration Tests"
     description = "This is a sample library for confirming that lint checks execute correctly, b/177437928"
diff --git a/lint-checks/integration-tests/expected-lint-results.xml b/lint-checks/integration-tests/expected-lint-results.xml
index 7dd7360..e8f2901 100644
--- a/lint-checks/integration-tests/expected-lint-results.xml
+++ b/lint-checks/integration-tests/expected-lint-results.xml
@@ -2,54 +2,6 @@
 <issues format="5" by="lint 4.2.0-beta04">
 
     <issue
-        id="UnknownIssueId"
-        severity="Ignore"
-        message="Unknown issue id &quot;ComposableLambdaParameterNaming&quot;"
-        category="Lint"
-        priority="1"
-        summary="Unknown Lint Issue Id"
-        explanation="Lint will report this issue if it is configured with an issue id it does not recognize in for example Gradle files or `lint.xml` configuration files.">
-        <location
-            file="$SUPPORT/lint-checks/integration-tests/build.gradle"/>
-    </issue>
-
-    <issue
-        id="UnknownIssueId"
-        severity="Ignore"
-        message="Unknown issue id &quot;ComposableLambdaParameterPosition&quot;"
-        category="Lint"
-        priority="1"
-        summary="Unknown Lint Issue Id"
-        explanation="Lint will report this issue if it is configured with an issue id it does not recognize in for example Gradle files or `lint.xml` configuration files.">
-        <location
-            file="$SUPPORT/lint-checks/integration-tests/build.gradle"/>
-    </issue>
-
-    <issue
-        id="UnknownIssueId"
-        severity="Ignore"
-        message="Unknown issue id &quot;ComposableNaming&quot;"
-        category="Lint"
-        priority="1"
-        summary="Unknown Lint Issue Id"
-        explanation="Lint will report this issue if it is configured with an issue id it does not recognize in for example Gradle files or `lint.xml` configuration files.">
-        <location
-            file="$SUPPORT/lint-checks/integration-tests/build.gradle"/>
-    </issue>
-
-    <issue
-        id="UnknownIssueId"
-        severity="Ignore"
-        message="Unknown issue id &quot;CompositionLocalNaming&quot;"
-        category="Lint"
-        priority="1"
-        summary="Unknown Lint Issue Id"
-        explanation="Lint will report this issue if it is configured with an issue id it does not recognize in for example Gradle files or `lint.xml` configuration files.">
-        <location
-            file="$SUPPORT/lint-checks/integration-tests/build.gradle"/>
-    </issue>
-
-    <issue
         id="BanConcurrentHashMap"
         severity="Error"
         message="Detected ConcurrentHashMap usage."
@@ -60,28 +12,12 @@
         errorLine1="import java.util.concurrent.ConcurrentHashMap;"
         errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
-            file="$SUPPORT/lint-checks/integration-tests/src/main/java/Sample.java"
+            file="$SUPPORT/lint-checks/integration-tests/src/main/java/androidx/Sample.java"
             line="19"
             column="1"/>
     </issue>
 
     <issue
-        id="UnnecessaryLambdaCreation"
-        severity="Error"
-        message="Creating an unnecessary lambda to emit a captured lambda"
-        category="Performance"
-        priority="5"
-        summary="Creating an unnecessary lambda to emit a captured lambda"
-        explanation="Creating this extra lambda instead of just passing the already captured lambda means that during code generation the Compose compiler will insert code around this lambda to track invalidations. This adds some extra runtime cost so you should instead just directly pass the lambda as a parameter to the function."
-        errorLine1="        lambda()"
-        errorLine2="        ~~~~~~">
-        <location
-            file="$SUPPORT/lint-checks/integration-tests/src/main/java/ComposeSample.kt"
-            line="29"
-            column="9"/>
-    </issue>
-
-    <issue
         id="UnknownNullness"
         severity="Fatal"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
@@ -94,8 +30,8 @@
         errorLine1="    public static Sample confirmIntrinisicLintChecksRun() {"
         errorLine2="                  ~~~~~~">
         <location
-            file="$SUPPORT/lint-checks/integration-tests/src/main/java/Sample.java"
-            line="28"
+            file="$SUPPORT/lint-checks/integration-tests/src/main/java/androidx/Sample.java"
+            line="32"
             column="19"/>
     </issue>
 
@@ -112,8 +48,8 @@
         errorLine1="    public static void confirmCustomAndroidXChecksRun(ConcurrentHashMap m) {"
         errorLine2="                                                      ~~~~~~~~~~~~~~~~~">
         <location
-            file="$SUPPORT/lint-checks/integration-tests/src/main/java/Sample.java"
-            line="37"
+            file="$SUPPORT/lint-checks/integration-tests/src/main/java/androidx/Sample.java"
+            line="41"
             column="55"/>
     </issue>
 
diff --git a/lint-checks/integration-tests/src/main/java/ComposeSample.kt b/lint-checks/integration-tests/src/main/java/androidx/ComposeSample.kt
similarity index 96%
rename from lint-checks/integration-tests/src/main/java/ComposeSample.kt
rename to lint-checks/integration-tests/src/main/java/androidx/ComposeSample.kt
index c3e2c63..ca74815 100644
--- a/lint-checks/integration-tests/src/main/java/ComposeSample.kt
+++ b/lint-checks/integration-tests/src/main/java/androidx/ComposeSample.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:Suppress("unused")
+
 package androidx
 
 fun lambdaFunction(lambda: () -> Unit) {
diff --git a/lint-checks/integration-tests/src/main/java/Sample.java b/lint-checks/integration-tests/src/main/java/androidx/Sample.java
similarity index 88%
rename from lint-checks/integration-tests/src/main/java/Sample.java
rename to lint-checks/integration-tests/src/main/java/androidx/Sample.java
index 5d2ba27..7c396ca 100644
--- a/lint-checks/integration-tests/src/main/java/Sample.java
+++ b/lint-checks/integration-tests/src/main/java/androidx/Sample.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -18,6 +18,10 @@
 
 import java.util.concurrent.ConcurrentHashMap;
 
+/**
+ * Sample class used to verify that ConcurrentHashMap lint check is running.
+ */
+@SuppressWarnings("unused")
 public class Sample {
 
     /**
diff --git a/lint-checks/integration-tests/src/main/java/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt b/lint-checks/integration-tests/src/main/java/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt
new file mode 100644
index 0000000..ded25ef
--- /dev/null
+++ b/lint-checks/integration-tests/src/main/java/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("unused")
+
+package androidx.sample.consumer
+
+import sample.annotation.provider.ExperimentalSampleAnnotation
+import sample.annotation.provider.ExperimentalSampleAnnotationJava
+
+class OutsideGroupExperimentalAnnotatedClass {
+    @ExperimentalSampleAnnotationJava
+    @ExperimentalSampleAnnotation
+    fun invalidAnnotatedMethod() {
+        // Nothing to see here.
+    }
+}
diff --git a/lint-checks/integration-tests/src/main/java/ComposeSample.kt b/lint-checks/integration-tests/src/main/java/sample/annotation/provider/ExperimentalSampleAnnotation.kt
similarity index 62%
copy from lint-checks/integration-tests/src/main/java/ComposeSample.kt
copy to lint-checks/integration-tests/src/main/java/sample/annotation/provider/ExperimentalSampleAnnotation.kt
index c3e2c63..265455c 100644
--- a/lint-checks/integration-tests/src/main/java/ComposeSample.kt
+++ b/lint-checks/integration-tests/src/main/java/sample/annotation/provider/ExperimentalSampleAnnotation.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,18 +14,9 @@
  * limitations under the License.
  */
 
-package androidx
+package sample.annotation.provider
 
-fun lambdaFunction(lambda: () -> Unit) {
-    lambda()
-}
-
-/**
- * This function uses an unnecessary lambda and should trigger UnnecessaryLambdaCreationDetector
- */
-fun confirmCustomComposeLintChecksRun() {
-    val lambda = {}
-    lambdaFunction {
-        lambda()
-    }
-}
+@RequiresOptIn
+@Retention(AnnotationRetention.BINARY)
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+annotation class ExperimentalSampleAnnotation
diff --git a/lint-checks/integration-tests/src/main/java/sample/annotation/provider/ExperimentalSampleAnnotationJava.java b/lint-checks/integration-tests/src/main/java/sample/annotation/provider/ExperimentalSampleAnnotationJava.java
new file mode 100644
index 0000000..cf47d87
--- /dev/null
+++ b/lint-checks/integration-tests/src/main/java/sample/annotation/provider/ExperimentalSampleAnnotationJava.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package sample.annotation.provider;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import kotlin.RequiresOptIn;
+
+@RequiresOptIn
+@Retention(CLASS)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface ExperimentalSampleAnnotationJava {}
diff --git a/lint-checks/integration-tests/src/main/java/ComposeSample.kt b/lint-checks/integration-tests/src/main/java/sample/annotation/provider/WithinGroupExperimentalAnnotatedClass.kt
similarity index 63%
copy from lint-checks/integration-tests/src/main/java/ComposeSample.kt
copy to lint-checks/integration-tests/src/main/java/sample/annotation/provider/WithinGroupExperimentalAnnotatedClass.kt
index c3e2c63..f36c34fd 100644
--- a/lint-checks/integration-tests/src/main/java/ComposeSample.kt
+++ b/lint-checks/integration-tests/src/main/java/sample/annotation/provider/WithinGroupExperimentalAnnotatedClass.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,18 +14,13 @@
  * limitations under the License.
  */
 
-package androidx
+@file:Suppress("unused")
 
-fun lambdaFunction(lambda: () -> Unit) {
-    lambda()
-}
+package sample.annotation.provider
 
-/**
- * This function uses an unnecessary lambda and should trigger UnnecessaryLambdaCreationDetector
- */
-fun confirmCustomComposeLintChecksRun() {
-    val lambda = {}
-    lambdaFunction {
-        lambda()
+class WithinGroupExperimentalAnnotatedClass {
+    @ExperimentalSampleAnnotation
+    fun validAnnotatedMethod() {
+        // Nothing to see here.
     }
 }
diff --git a/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt b/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
index e596f53..5bc2e75 100644
--- a/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
@@ -14,9 +14,11 @@
  * limitations under the License.
  */
 
+@file:Suppress("UnstableApiUsage")
+
 package androidx.build.lint
 
-import com.android.tools.lint.detector.api.AnnotationUsageType
+import com.android.tools.lint.client.api.UElementHandler
 import com.android.tools.lint.detector.api.Category
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Implementation
@@ -24,107 +26,93 @@
 import com.android.tools.lint.detector.api.JavaContext
 import com.android.tools.lint.detector.api.Scope
 import com.android.tools.lint.detector.api.Severity
-import com.android.tools.lint.detector.api.SourceCodeScanner
-import com.intellij.psi.PsiElement
-import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiCompiledElement
+import org.jetbrains.uast.UAnnotated
 import org.jetbrains.uast.UAnnotation
 import org.jetbrains.uast.UElement
-import org.jetbrains.uast.getContainingUClass
+import org.jetbrains.uast.resolveToUElement
 
-class BanInappropriateExperimentalUsage : Detector(), SourceCodeScanner {
-    override fun applicableAnnotations(): List<String>? = listOf(
-        JAVA_EXPERIMENTAL_ANNOTATION,
-        KOTLIN_OPT_IN_ANNOTATION,
-        KOTLIN_EXPERIMENTAL_ANNOTATION
-    )
+/**
+ * Prevents usage of experimental annotations outside the groups in which they were defined.
+ */
+class BanInappropriateExperimentalUsage : Detector(), Detector.UastScanner {
 
-    override fun visitAnnotationUsage(
-        context: JavaContext,
-        usage: UElement,
-        type: AnnotationUsageType,
-        annotation: UAnnotation,
-        qualifiedName: String,
-        method: PsiMethod?,
-        referenced: PsiElement?,
-        annotations: List<UAnnotation>,
-        allMemberAnnotations: List<UAnnotation>,
-        allClassAnnotations: List<UAnnotation>,
-        allPackageAnnotations: List<UAnnotation>
-    ) {
-        when (qualifiedName) {
-            JAVA_EXPERIMENTAL_ANNOTATION,
-            JAVA_OPT_IN_ANNOTATION,
-            KOTLIN_EXPERIMENTAL_ANNOTATION,
-            KOTLIN_OPT_IN_ANNOTATION -> {
-                verifyExperimentalOrOptInUsageIsWithinSameGroup(
-                    context, usage, annotation
-                )
+    override fun getApplicableUastTypes() = listOf(UAnnotation::class.java)
+
+    override fun createUastHandler(context: JavaContext): UElementHandler {
+        return AnnotationChecker(context)
+    }
+
+    private inner class AnnotationChecker(val context: JavaContext) : UElementHandler() {
+        override fun visitAnnotation(node: UAnnotation) {
+            val annotation = node.resolveToUElement()
+            if (annotation is UAnnotated) {
+                val annotations = context.evaluator.getAllAnnotations(annotation, false)
+                val isOptIn = annotations.any { APPLICABLE_ANNOTATIONS.contains(it.qualifiedName) }
+                if (isOptIn) {
+                    verifyUsageOfElementIsWithinSameGroup(context, node, annotation, ISSUE)
+                }
             }
         }
     }
 
-    fun verifyExperimentalOrOptInUsageIsWithinSameGroup(
+    fun verifyUsageOfElementIsWithinSameGroup(
         context: JavaContext,
         usage: UElement,
-        annotation: UAnnotation
+        annotation: UElement,
+        issue: Issue,
     ) {
-        val declaringGroup = getApproximateAnnotationMavenGroup(annotation)
-        val usingGroup = getApproximateUsageSiteMavenGroup(usage)
-        // Don't flag if group is null for some reason (for now at least)
-        // Also exclude sample for now, since it doesn't work well with our workaround (includes
-        // class)
-        if (declaringGroup != null && usingGroup != null && declaringGroup != usingGroup &&
-            usingGroup != "sample"
-        ) {
+        val evaluator = context.evaluator
+        val usageCoordinates = evaluator.getLibrary(usage) ?: context.project.mavenCoordinate
+        val annotationCoordinates = evaluator.getLibrary(annotation) ?: run {
+            // Is the annotation defined in source code?
+            if (usageCoordinates != null && annotation !is PsiCompiledElement) {
+                annotation.sourcePsi?.let { sourcePsi ->
+                    evaluator.getProject(sourcePsi)?.mavenCoordinate
+                }
+            } else {
+                null
+            }
+        }
+        val usageGroupId = usageCoordinates?.groupId
+        val annotationGroupId = annotationCoordinates?.groupId
+        if (annotationGroupId != usageGroupId && annotationGroupId != null) {
             context.report(
-                BanInappropriateExperimentalUsage.ISSUE, usage, context.getNameLocation(usage),
-                "`Experimental`/`OptIn` APIs should only be used from within the same library " +
-                    "or libraries within the same requireSameVersion group"
+                issue, usage, context.getNameLocation(usage),
+                "`Experimental` and `RequiresOptIn` APIs may only be used within the same-version" +
+                    " group where they were defined."
             )
         }
     }
 
-    fun getApproximateAnnotationMavenGroup(annotation: UAnnotation): String? {
-        if (annotation.getContainingUClass() == null || annotation.getContainingUClass()!!
-            .qualifiedName == null
-        ) {
-            return null
-        }
-        return annotation.getContainingUClass()!!.qualifiedName!!.split(".").subList(0, 2)
-            .joinToString(".")
-    }
-
-    fun getApproximateUsageSiteMavenGroup(usage: UElement): String? {
-        if (usage.getContainingUClass() == null || usage.getContainingUClass()!!
-            .qualifiedName == null
-        ) {
-            return null
-        }
-        return usage.getContainingUClass()!!.qualifiedName!!.split(".").subList(0, 2)
-            .joinToString(".")
-    }
-
     companion object {
-
         private const val KOTLIN_EXPERIMENTAL_ANNOTATION = "kotlin.Experimental"
-
+        private const val KOTLIN_REQUIRES_OPT_IN_ANNOTATION = "kotlin.RequiresOptIn"
         private const val JAVA_EXPERIMENTAL_ANNOTATION =
             "androidx.annotation.experimental.Experimental"
+        private const val JAVA_REQUIRES_OPT_IN_ANNOTATION =
+            "androidx.annotation.RequiresOptIn"
 
-        private const val KOTLIN_OPT_IN_ANNOTATION =
-            "kotlin.OptIn"
-
-        private const val JAVA_OPT_IN_ANNOTATION =
-            "androidx.annotation.OptIn"
+        private val APPLICABLE_ANNOTATIONS = listOf(
+            JAVA_EXPERIMENTAL_ANNOTATION,
+            KOTLIN_EXPERIMENTAL_ANNOTATION,
+            JAVA_REQUIRES_OPT_IN_ANNOTATION,
+            KOTLIN_REQUIRES_OPT_IN_ANNOTATION,
+        )
 
         val ISSUE = Issue.create(
-            "IllegalExperimentalApiUsage",
-            "Using experimental api from separately versioned library",
-            "APIs annotated with `@RequiresOptIn` or `@Experimental` are considered alpha." +
-                "A caller from another library may not use them unless that the two libraries " +
-                "are part of the same maven group and that group specifies requireSameVersion",
-            Category.CORRECTNESS, 5, Severity.ERROR,
-            Implementation(BanInappropriateExperimentalUsage::class.java, Scope.JAVA_FILE_SCOPE)
+            id = "IllegalExperimentalApiUsage",
+            briefDescription = "Using experimental API from separately versioned library",
+            explanation = "Annotations meta-annotated with `@RequiresOptIn` or `@Experimental` " +
+                "may only be referenced from within the same-version group in which they were " +
+                "defined.",
+            category = Category.CORRECTNESS,
+            priority = 5,
+            severity = Severity.ERROR,
+            implementation = Implementation(
+                BanInappropriateExperimentalUsage::class.java,
+                Scope.JAVA_FILE_SCOPE,
+            ),
         )
     }
-}
+}
\ No newline at end of file
diff --git a/lint-checks/src/test/java/androidx/build/lint/BanInappropriateExperimentalUsageTest.kt b/lint-checks/src/test/java/androidx/build/lint/BanInappropriateExperimentalUsageTest.kt
new file mode 100644
index 0000000..cd7a7b3
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/BanInappropriateExperimentalUsageTest.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.lint
+
+import com.android.tools.lint.checks.infrastructure.ProjectDescription
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.checks.infrastructure.TestFiles.gradle
+import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@Suppress("UnstableApiUsage")
+@RunWith(JUnit4::class)
+class BanInappropriateExperimentalUsageTest {
+
+    @Test
+    fun `Test within-module Experimental usage via Gradle model`() {
+        val provider = project()
+            .name("provider")
+            .files(
+                ktSample("sample.annotation.provider.WithinGroupExperimentalAnnotatedClass"),
+                ktSample("sample.annotation.provider.ExperimentalSampleAnnotation"),
+                gradle(
+                    """
+                    apply plugin: 'com.android.library'
+                    group=sample.annotation.provider
+                    """
+                ).indented(),
+                OPT_IN_KT,
+            )
+
+        lint()
+            .projects(provider)
+            .issues(BanInappropriateExperimentalUsage.ISSUE)
+            .run()
+            .expect(
+                """
+                No warnings.
+                """.trimIndent()
+            )
+    }
+
+    @Test
+    fun `Test cross-module Experimental usage via Gradle model`() {
+        val provider = project()
+            .name("provider")
+            .report(false)
+            .files(
+                ktSample("sample.annotation.provider.ExperimentalSampleAnnotation"),
+                javaSample("sample.annotation.provider.ExperimentalSampleAnnotationJava"),
+                gradle(
+                    """
+                    apply plugin: 'com.android.library'
+                    group=sample.annotation.provider
+                    """
+                ).indented(),
+                OPT_IN_KT,
+            )
+
+        val consumer = project()
+            .name("consumer")
+            .dependsOn(provider)
+            .files(
+                ktSample("androidx.sample.consumer.OutsideGroupExperimentalAnnotatedClass"),
+                gradle(
+                    """
+                    apply plugin: 'com.android.library'
+                    group=androidx.sample.consumer
+                    """
+                ).indented()
+            )
+
+        lint()
+            .projects(provider, consumer)
+            .issues(BanInappropriateExperimentalUsage.ISSUE)
+            .run()
+            .expect(
+                """
+                consumer/src/main/kotlin/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt:25: Error: Experimental and RequiresOptIn APIs may only be used within the same-version group where they were defined. [IllegalExperimentalApiUsage]
+                    @ExperimentalSampleAnnotationJava
+                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                1 errors, 0 warnings
+                """.trimIndent()
+            )
+    }
+
+    private fun project(): ProjectDescription = ProjectDescription()
+
+    /**
+     * Loads a [TestFile] from Java source code included in the JAR resources.
+     */
+    private fun javaSample(className: String): TestFile = TestFiles.java(
+        javaClass.getResource("/java/${className.replace('.', '/')}.java").readText()
+    )
+
+    /**
+     * Loads a [TestFile] from Kotlin source code included in the JAR resources.
+     */
+    private fun ktSample(className: String): TestFile = TestFiles.kotlin(
+        javaClass.getResource("/java/${className.replace('.', '/')}.kt").readText()
+    )
+}
+
+/* ktlint-disable max-line-length */
+
+/**
+ * [TestFile] containing OptIn.kt from the Kotlin standard library.
+ *
+ * This is a workaround for the Kotlin standard library used by the Lint test harness not
+ * including the Experimental annotation by default.
+ */
+private val OPT_IN_KT: TestFile = TestFiles.kotlin(
+    """
+    package kotlin
+
+    import kotlin.annotation.AnnotationRetention.BINARY
+    import kotlin.annotation.AnnotationRetention.SOURCE
+    import kotlin.annotation.AnnotationTarget.*
+    import kotlin.internal.RequireKotlin
+    import kotlin.internal.RequireKotlinVersionKind
+    import kotlin.reflect.KClass
+
+    @Target(ANNOTATION_CLASS)
+    @Retention(BINARY)
+    @SinceKotlin("1.3")
+    @RequireKotlin("1.3.70", versionKind = RequireKotlinVersionKind.COMPILER_VERSION)
+    public annotation class RequiresOptIn(
+        val message: String = "",
+        val level: Level = Level.ERROR
+    ) {
+        public enum class Level {
+            WARNING,
+            ERROR,
+        }
+    }
+
+    @Target(
+        CLASS, PROPERTY, LOCAL_VARIABLE, VALUE_PARAMETER, CONSTRUCTOR, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, EXPRESSION, FILE, TYPEALIAS
+    )
+    @Retention(SOURCE)
+    @SinceKotlin("1.3")
+    @RequireKotlin("1.3.70", versionKind = RequireKotlinVersionKind.COMPILER_VERSION)
+    public annotation class OptIn(
+        vararg val markerClass: KClass<out Annotation>
+    )
+    """.trimIndent()
+)
+
+/* ktlint-enable max-line-length */
diff --git a/media/version-compat-tests/previous/client/src/androidTest/java/android/support/mediacompat/client/RemoteUserInfoWithMediaControllerCompatTest.java b/media/version-compat-tests/previous/client/src/androidTest/java/android/support/mediacompat/client/RemoteUserInfoWithMediaControllerCompatTest.java
index 0567306..91fb0c3 100644
--- a/media/version-compat-tests/previous/client/src/androidTest/java/android/support/mediacompat/client/RemoteUserInfoWithMediaControllerCompatTest.java
+++ b/media/version-compat-tests/previous/client/src/androidTest/java/android/support/mediacompat/client/RemoteUserInfoWithMediaControllerCompatTest.java
@@ -48,6 +48,7 @@
 import android.view.KeyEvent;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.FlakyTest;
 import androidx.test.filters.LargeTest;
 
 import org.junit.After;
@@ -64,6 +65,7 @@
  * {@link MediaControllerCompat} methods.
  */
 @RunWith(AndroidJUnit4.class)
+@FlakyTest(bugId = 182271958)
 @LargeTest
 public class RemoteUserInfoWithMediaControllerCompatTest {
     private static final String TAG = "RemoteUserInfoCompat";
diff --git a/media2/media2-session/version-compat-tests/previous/client/src/androidTest/java/androidx/media2/test/client/tests/MediaBrowserCallbackTest.java b/media2/media2-session/version-compat-tests/previous/client/src/androidTest/java/androidx/media2/test/client/tests/MediaBrowserCallbackTest.java
index 38a4296..1ddc1c0 100644
--- a/media2/media2-session/version-compat-tests/previous/client/src/androidTest/java/androidx/media2/test/client/tests/MediaBrowserCallbackTest.java
+++ b/media2/media2-session/version-compat-tests/previous/client/src/androidTest/java/androidx/media2/test/client/tests/MediaBrowserCallbackTest.java
@@ -60,6 +60,7 @@
 import androidx.test.filters.LargeTest;
 import androidx.versionedparcelable.ParcelUtils;
 
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -154,6 +155,7 @@
     }
 
     @Test
+    @Ignore("Throwing fatal exceptions: b/178708442")
     public void getItem_nullResult() throws Exception {
         prepareLooper();
         final String mediaId = MediaBrowserConstants.MEDIA_ID_GET_NULL_ITEM;
@@ -175,6 +177,7 @@
     }
 
     @Test
+    @Ignore("Throwing fatal exceptions: b/178708442")
     public void getItem_invalidResult() throws Exception {
         prepareLooper();
         final String mediaId = MediaBrowserConstants.MEDIA_ID_GET_INVALID_ITEM;
diff --git a/playground-common/playground-include-settings.gradle b/playground-common/playground-include-settings.gradle
index 4f2e033..994668d 100644
--- a/playground-common/playground-include-settings.gradle
+++ b/playground-common/playground-include-settings.gradle
@@ -65,6 +65,8 @@
     }
     settings.includeBuild(new File(supportRoot, "androidx-plugin"))
     settings.includeProject(":lint-checks", new File(supportRoot, "lint-checks"))
+    settings.includeProject(":lint-checks:integration-tests",
+            new File(supportRoot, "lint-checks/integration-tests"))
     settings.includeProject(":fakeannotations", new File(supportRoot,"fakeannotations"))
     settings.includeProject(":internal-testutils-common",
             new File(supportRoot, "testutils/testutils-common"))
diff --git a/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt b/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
index 8deb8c2..474c81d 100644
--- a/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
+++ b/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
@@ -23,6 +23,7 @@
 import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
 import androidx.testutils.assertThrows
 import com.google.common.truth.Truth.assertThat
@@ -130,6 +131,7 @@
     }
 
     @Test
+    @FlakyTest(bugId = 182343970)
     public fun dbNotClosedWithRefCountIncremented() {
         autoCloser.incrementCountAndEnsureDbIsOpen()
 
diff --git a/settings.gradle b/settings.gradle
index e4207b1..a728444 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -305,6 +305,8 @@
 includeProject(":compose:ui:ui-test", "compose/ui/ui-test", [BuildType.COMPOSE])
 includeProject(":compose:ui:ui-test-font", "compose/ui/ui-test-font", [BuildType.COMPOSE])
 includeProject(":compose:ui:ui-test-junit4", "compose/ui/ui-test-junit4", [BuildType.COMPOSE])
+includeProject(":compose:ui:ui-test-manifest", "compose/ui/ui-test-manifest", [BuildType.COMPOSE])
+includeProject(":compose:ui:ui-test-manifest:integration-tests:testapp", "compose/ui/ui-test-manifest/integration-tests/testapp", [BuildType.COMPOSE])
 includeProject(":compose:ui:ui-text", "compose/ui/ui-text", [BuildType.COMPOSE])
 includeProject(":compose:ui:ui-text:ui-text-benchmark", "compose/ui/ui-text/benchmark", [BuildType.COMPOSE])
 includeProject(":compose:ui:ui-text:ui-text-samples", "compose/ui/ui-text/samples", [BuildType.COMPOSE])
@@ -422,8 +424,8 @@
 includeProject(":lifecycle:lifecycle-viewmodel-ktx", "lifecycle/lifecycle-viewmodel-ktx", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":lifecycle:lifecycle-viewmodel-savedstate", "lifecycle/lifecycle-viewmodel-savedstate", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR])
 includeProject(":lint-checks", "lint-checks")
-includeProject(":lint-checks:integration-tests", "lint-checks/integration-tests", [BuildType.COMPOSE])
-includeProject(":lint-checks:tests", "lint-checks/tests", [BuildType.MAIN])
+includeProject(":lint-checks:integration-tests", "lint-checks/integration-tests")
+includeProject(":lint-checks:tests", "lint-checks/tests")
 includeProject(":lint-demos:lint-demo-appcompat", "lint-demos/lint-demo-appcompat", [BuildType.MAIN])
 includeProject(":loader:loader", "loader/loader", [BuildType.MAIN])
 includeProject(":loader:loader-ktx", "loader/loader-ktx", [BuildType.MAIN])
@@ -568,7 +570,6 @@
 includeProject(":wear:wear-phone-interactions", "wear/wear-phone-interactions", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":wear:wear-remote-interactions", "wear/wear-remote-interactions", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":wear:wear-tiles", "wear/wear-tiles", [BuildType.MAIN, BuildType.WEAR])
-includeProject(":wear:wear-tiles-data", "wear/wear-tiles-data", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":wear:wear-tiles-proto", "wear/wear-tiles-proto", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":wear:wear-tiles-renderer", "wear/wear-tiles-renderer", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":wear:wear-watchface", "wear/wear-watchface", [BuildType.MAIN, BuildType.WEAR])
diff --git a/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java b/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
index 7890aa4..3afe39e 100644
--- a/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
+++ b/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
@@ -1704,7 +1704,7 @@
      * @return A pair of rects define the position of the split, or {@null} if there is no split
      */
     private ArrayList<Rect> splitViewPositions() {
-        if (mFoldingFeature == null || !isValidFoldStateForSplit(mFoldingFeature)) {
+        if (mFoldingFeature == null || !mFoldingFeature.isSeparating()) {
             return null;
         }
 
@@ -1748,24 +1748,6 @@
         return foldRectInView;
     }
 
-    private static boolean isValidFoldStateForSplit(@NonNull FoldingFeature foldingFeature) {
-        // Only a fold or a hinge can split the view
-        if (foldingFeature.getType() != FoldingFeature.TYPE_FOLD
-                && foldingFeature.getType() != FoldingFeature.TYPE_HINGE) {
-            return false;
-        }
-        // A foldable device with hinge is always separating
-        if (foldingFeature.getType() == FoldingFeature.TYPE_HINGE) {
-            return true;
-        }
-        // Split the view when a foldable device is in half opened state or flipped state
-        if (foldingFeature.getState() != FoldingFeature.STATE_HALF_OPENED
-                && foldingFeature.getState() != FoldingFeature.STATE_FLIPPED) {
-            return false;
-        }
-        return true;
-    }
-
     /**
      * A device folding feature observer is used to notify listener when there is a folding feature
      * change.
diff --git a/wear/wear-tiles-data/build.gradle b/wear/wear-tiles-data/build.gradle
deleted file mode 100644
index faf2576..0000000
--- a/wear/wear-tiles-data/build.gradle
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import static androidx.build.dependencies.DependenciesKt.*
-import androidx.build.LibraryGroups
-import androidx.build.LibraryType
-import androidx.build.LibraryVersions
-import androidx.build.RunApiTasks
-
-plugins {
-    id("AndroidXPlugin")
-    id("com.android.library")
-}
-
-dependencies {
-    api("androidx.annotation:annotation:1.1.0")
-    api(GUAVA_LISTENABLE_FUTURE)
-    implementation(PROTOBUF_LITE)
-    implementation("androidx.annotation:annotation:1.2.0-alpha01")
-
-    testImplementation(ANDROIDX_TEST_EXT_JUNIT)
-    testImplementation(ANDROIDX_TEST_CORE)
-    testImplementation(ANDROIDX_TEST_RUNNER)
-    testImplementation(ANDROIDX_TEST_RULES)
-    testImplementation(ROBOLECTRIC)
-    testImplementation(MOCKITO_CORE)
-}
-
-android {
-    defaultConfig {
-        minSdkVersion 25
-    }
-
-    buildFeatures {
-        aidl = true
-    }
-
-    // Use Robolectric 4.+
-    testOptions.unitTests.includeAndroidResources = true
-}
-
-androidx {
-    name = "Android Wear Tiles Data"
-    type = LibraryType.PUBLISHED_LIBRARY
-    mavenGroup = LibraryGroups.WEAR
-    mavenVersion = LibraryVersions.WEAR_TILES_DATA
-    inceptionYear = "2020"
-    description = "Android Wear Tiles Internal Data Classes"
-    runApiTasks = new RunApiTasks.No("API tracking disabled while the package is empty")
-}
diff --git a/wear/wear-tiles-data/lint-baseline.xml b/wear/wear-tiles-data/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/wear/wear-tiles-data/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/wear/wear-tiles-data/src/main/AndroidManifest.xml b/wear/wear-tiles-data/src/main/AndroidManifest.xml
deleted file mode 100644
index e26eded..0000000
--- a/wear/wear-tiles-data/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="androidx.wear.tiles">
-</manifest>
diff --git a/wear/wear-tiles-renderer/src/main/java/androidx/wear/tiles/renderer/ResourceAccessors.java b/wear/wear-tiles-renderer/src/main/java/androidx/wear/tiles/renderer/ResourceAccessors.java
index 5bc9bd6..4a297d3 100644
--- a/wear/wear-tiles-renderer/src/main/java/androidx/wear/tiles/renderer/ResourceAccessors.java
+++ b/wear/wear-tiles-renderer/src/main/java/androidx/wear/tiles/renderer/ResourceAccessors.java
@@ -122,7 +122,7 @@
             this.mProtoResources = protoResources;
         }
 
-        /** Set the resource loader for {@link AndroidImageResourceByResId} resources. */
+        /** Set the resource loader for {@link AndroidImageResourceByResIdAccessor} resources. */
         @NonNull
         @SuppressLint("MissingGetterMatchingBuilder")
         public Builder setAndroidImageResourceByResIdAccessor(
diff --git a/wear/wear-tiles/src/main/java/androidx/wear/tiles/TileProviderService.java b/wear/wear-tiles/src/main/java/androidx/wear/tiles/TileProviderService.java
index ab51044..10dfabc 100644
--- a/wear/wear-tiles/src/main/java/androidx/wear/tiles/TileProviderService.java
+++ b/wear/wear-tiles/src/main/java/androidx/wear/tiles/TileProviderService.java
@@ -82,7 +82,7 @@
      * Called when the system is requesting a new timeline from this Tile Provider. Note that this
      * may be called from a background thread.
      *
-     * @param requestParams Parameters about the request. See {@link TileRequestData} for more info.
+     * @param requestParams Parameters about the request. See {@link TileRequest} for more info.
      */
     @MainThread
     @NonNull
@@ -92,8 +92,8 @@
      * Called when the system is requesting a resource bundle from this Tile Provider. Note that
      * this may be called from a background thread.
      *
-     * @param requestParams Parameters about the request. See {@link ResourcesRequestData} for more
-     *     info.
+     * @param requestParams Parameters about the request. See {@link ResourcesRequest} for more
+     *                      info.
      */
     @MainThread
     @NonNull
@@ -104,7 +104,7 @@
      * Called when a tile provided by this Tile Provider is added to the carousel. Note that this
      * may be called from a background thread.
      *
-     * @param requestParams Parameters about the request. See {@link TileAddEventData} for more
+     * @param requestParams Parameters about the request. See {@link TileAddEvent} for more
      *     info.
      */
     @MainThread
@@ -114,7 +114,7 @@
      * Called when a tile provided by this Tile Provider is removed from the carousel. Note that
      * this may be called from a background thread.
      *
-     * @param requestParams Parameters about the request. See {@link TileRemoveEventData} for more
+     * @param requestParams Parameters about the request. See {@link TileRemoveEvent} for more
      *     info.
      */
     @MainThread
@@ -124,7 +124,7 @@
      * Called when a tile provided by this Tile Provider becomes into view, on screen. Note that
      * this may be called from a background thread.
      *
-     * @param requestParams Parameters about the request. See {@link TileEnterEventData} for more
+     * @param requestParams Parameters about the request. See {@link TileEnterEvent} for more
      *     info.
      */
     @MainThread
@@ -134,7 +134,7 @@
      * Called when a tile provided by this Tile Provider goes out of view, on screen. Note that this
      * may be called from a background thread.
      *
-     * @param requestParams Parameters about the request. See {@link TileLeaveEventData} for more
+     * @param requestParams Parameters about the request. See {@link TileLeaveEvent} for more
      *     info.
      */
     @MainThread
diff --git a/wear/wear-tiles/src/main/java/androidx/wear/tiles/builders/LayoutElementBuilders.java b/wear/wear-tiles/src/main/java/androidx/wear/tiles/builders/LayoutElementBuilders.java
index eea253a..3f1f665 100644
--- a/wear/wear-tiles/src/main/java/androidx/wear/tiles/builders/LayoutElementBuilders.java
+++ b/wear/wear-tiles/src/main/java/androidx/wear/tiles/builders/LayoutElementBuilders.java
@@ -1812,8 +1812,7 @@
             }
 
             /**
-             * Sets how to align the contents of this container relative to anchor_angle. See the
-             * descriptions of options in {@link ArcAnchorType} for more information. If not
+             * Sets how to align the contents of this container relative to anchor_angle. If not
              * defined, defaults to ARC_ANCHOR_CENTER.
              */
             @SuppressLint("MissingGetterMatchingBuilder")
diff --git a/wear/wear-tiles/src/main/java/androidx/wear/tiles/readers/EventReaders.java b/wear/wear-tiles/src/main/java/androidx/wear/tiles/readers/EventReaders.java
index 8f2324e..2ae5a0a 100644
--- a/wear/wear-tiles/src/main/java/androidx/wear/tiles/readers/EventReaders.java
+++ b/wear/wear-tiles/src/main/java/androidx/wear/tiles/readers/EventReaders.java
@@ -31,7 +31,7 @@
 public class EventReaders {
     private EventReaders() {}
 
-    /** Reader for a {@link TileAddEventData} instance. */
+    /** Reader for Tile add event parameters. */
     public static class TileAddEvent {
         private final EventProto.TileAddEvent mProto;
 
@@ -64,7 +64,7 @@
         }
     }
 
-    /** Reader for a {@link TileRemoveEventData} instance. */
+    /** Reader for Tile remove event parameters. */
     public static class TileRemoveEvent {
         private final EventProto.TileRemoveEvent mProto;
 
@@ -97,7 +97,7 @@
         }
     }
 
-    /** Reader for a {@link TileEnterEventData} instance. */
+    /** Reader for Tile enter event parameters. */
     public static class TileEnterEvent {
         private final EventProto.TileEnterEvent mProto;
 
@@ -130,7 +130,7 @@
         }
     }
 
-    /** Reader for a {@link TileLeaveEventData} instance. */
+    /** Reader for a Tile leave event parameters. */
     public static class TileLeaveEvent {
         private final EventProto.TileLeaveEvent mProto;
 
diff --git a/wear/wear-tiles/src/main/java/androidx/wear/tiles/readers/RequestReaders.java b/wear/wear-tiles/src/main/java/androidx/wear/tiles/readers/RequestReaders.java
index 0584c8c..d3a50de6 100644
--- a/wear/wear-tiles/src/main/java/androidx/wear/tiles/readers/RequestReaders.java
+++ b/wear/wear-tiles/src/main/java/androidx/wear/tiles/readers/RequestReaders.java
@@ -33,7 +33,7 @@
 public class RequestReaders {
     private RequestReaders() {}
 
-    /** Reader for a {@link TileRequestData} instance. */
+    /** Reader for Tile request parameters. */
     public static class TileRequest {
         private final RequestProto.TileRequest mProto;
         private final int mTileId;
@@ -75,7 +75,7 @@
         }
     }
 
-    /** Reader for a {@link ResourcesRequestData} instance. */
+    /** Reader for resource request parameters. */
     public static class ResourcesRequest {
         private final RequestProto.ResourcesRequest mProto;
         private final int mTileId;
@@ -105,15 +105,15 @@
             return mTileId;
         }
 
-        /** Get the resource version requested by this {@link ResourcesRequestData}. */
+        /** Get the requested resource version. */
         @NonNull
         public String getVersion() {
             return mProto.getVersion();
         }
 
         /**
-         * Get the resource IDs requested by this {@link ResourcesRequestData}. May be empty, in
-         * which case all resources should be returned.
+         * Get the requested resource IDs. May be empty, in which case all resources should be
+         * returned.
          */
         @NonNull
         public List<String> getResourceIds() {
diff --git a/wear/wear-watchface-client/guava/src/androidTest/java/androidx/wear/watchface/ListenableWatchFaceControlClientTest.kt b/wear/wear-watchface-client/guava/src/androidTest/java/androidx/wear/watchface/ListenableWatchFaceControlClientTest.kt
index c3dec98..12aa604 100644
--- a/wear/wear-watchface-client/guava/src/androidTest/java/androidx/wear/watchface/ListenableWatchFaceControlClientTest.kt
+++ b/wear/wear-watchface-client/guava/src/androidTest/java/androidx/wear/watchface/ListenableWatchFaceControlClientTest.kt
@@ -20,9 +20,11 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.graphics.Rect
 import android.os.Handler
 import android.os.IBinder
 import android.os.Looper
+import android.view.Surface
 import android.view.SurfaceHolder
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -34,8 +36,12 @@
 import androidx.wear.watchface.samples.createExampleCanvasAnalogWatchFaceBuilder
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertNull
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
 import java.util.concurrent.TimeUnit
 
 private const val TIMEOUT_MS = 500L
@@ -44,6 +50,19 @@
 @MediumTest
 public class ListenableWatchFaceControlClientTest {
 
+    @Mock
+    private lateinit var surfaceHolder: SurfaceHolder
+    @Mock
+    private lateinit var surface: Surface
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        Mockito.`when`(surfaceHolder.surfaceFrame)
+            .thenReturn(Rect(0, 0, 400, 400))
+        Mockito.`when`(surfaceHolder.surface).thenReturn(surface)
+    }
+
     @Test
     public fun headlessSchemaSettingIds() {
         val context = ApplicationProvider.getApplicationContext<Context>()
@@ -125,7 +144,12 @@
                 attachBaseContext(context)
             }
         }
-        service.onCreateEngine()
+        service.onCreateEngine().onSurfaceChanged(
+            surfaceHolder,
+            0,
+            surfaceHolder.surfaceFrame.width(),
+            surfaceHolder.surfaceFrame.height()
+        )
 
         val interactiveInstance = interactiveInstanceFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
         assertThat(interactiveInstance.userStyleSchema.userStyleSettings.map { it.id })
@@ -215,7 +239,12 @@
                 attachBaseContext(context)
             }
         }
-        service.onCreateEngine()
+        service.onCreateEngine().onSurfaceChanged(
+            surfaceHolder,
+            0,
+            surfaceHolder.surfaceFrame.width(),
+            surfaceHolder.surfaceFrame.height()
+        )
 
         val interactiveInstance = interactiveInstanceFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
         val headlessInstance1 = client.createHeadlessWatchFaceClient(
diff --git a/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index cd927f1..6548a2e 100644
--- a/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -24,6 +24,7 @@
 import android.os.Handler
 import android.os.Looper
 import android.service.wallpaper.WallpaperService
+import android.view.Surface
 import android.view.SurfaceHolder
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -88,6 +89,8 @@
 
     @Mock
     private lateinit var surfaceHolder: SurfaceHolder
+    @Mock
+    private lateinit var surface: Surface
     private lateinit var engine: WallpaperService.Engine
     private val handler = Handler(Looper.getMainLooper())
     private val engineLatch = CountDownLatch(1)
@@ -100,6 +103,7 @@
 
         Mockito.`when`(surfaceHolder.surfaceFrame)
             .thenReturn(Rect(0, 0, 400, 400))
+        Mockito.`when`(surfaceHolder.surface).thenReturn(surface)
     }
 
     @After
@@ -141,6 +145,12 @@
     private fun createEngine() {
         handler.post {
             engine = wallpaperService.onCreateEngine()
+            engine.onSurfaceChanged(
+                surfaceHolder,
+                0,
+                surfaceHolder.surfaceFrame.width(),
+                surfaceHolder.surfaceFrame.height()
+            )
             engineLatch.countDown()
         }
         engineLatch.await(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
@@ -537,7 +547,15 @@
         assertFalse(deferredExistingInstance.isCompleted)
 
         // We don't want to leave a pending request or it'll mess up subsequent tests.
-        handler.post { engine = wallpaperService.onCreateEngine() }
+        handler.post {
+            engine = wallpaperService.onCreateEngine()
+            engine.onSurfaceChanged(
+                surfaceHolder,
+                0,
+                surfaceHolder.surfaceFrame.width(),
+                surfaceHolder.surfaceFrame.height()
+            )
+        }
         runBlocking {
             withTimeout(CONNECT_TIMEOUT_MILLIS) {
                 deferredExistingInstance.await()
diff --git a/wear/wear-watchface/api/current.txt b/wear/wear-watchface/api/current.txt
index 289dc0a..193d83b 100644
--- a/wear/wear-watchface/api/current.txt
+++ b/wear/wear-watchface/api/current.txt
@@ -79,12 +79,12 @@
 
   public final class ComplicationOutlineRenderer {
     ctor public ComplicationOutlineRenderer();
-    method public static void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
+    method public static void drawComplicationOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
     field public static final androidx.wear.watchface.ComplicationOutlineRenderer.Companion Companion;
   }
 
   public static final class ComplicationOutlineRenderer.Companion {
-    method public void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
+    method public void drawComplicationOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
   }
 
   public final class ComplicationsManager {
diff --git a/wear/wear-watchface/api/public_plus_experimental_current.txt b/wear/wear-watchface/api/public_plus_experimental_current.txt
index 289dc0a..193d83b 100644
--- a/wear/wear-watchface/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface/api/public_plus_experimental_current.txt
@@ -79,12 +79,12 @@
 
   public final class ComplicationOutlineRenderer {
     ctor public ComplicationOutlineRenderer();
-    method public static void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
+    method public static void drawComplicationOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
     field public static final androidx.wear.watchface.ComplicationOutlineRenderer.Companion Companion;
   }
 
   public static final class ComplicationOutlineRenderer.Companion {
-    method public void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
+    method public void drawComplicationOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
   }
 
   public final class ComplicationsManager {
diff --git a/wear/wear-watchface/api/restricted_current.txt b/wear/wear-watchface/api/restricted_current.txt
index eb61cbf..2584372 100644
--- a/wear/wear-watchface/api/restricted_current.txt
+++ b/wear/wear-watchface/api/restricted_current.txt
@@ -79,12 +79,12 @@
 
   public final class ComplicationOutlineRenderer {
     ctor public ComplicationOutlineRenderer();
-    method public static void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
+    method public static void drawComplicationOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
     field public static final androidx.wear.watchface.ComplicationOutlineRenderer.Companion Companion;
   }
 
   public static final class ComplicationOutlineRenderer.Companion {
-    method public void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
+    method public void drawComplicationOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
   }
 
   public final class ComplicationsManager {
diff --git a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
index 929dadc..ab591af 100644
--- a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
+++ b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
@@ -194,6 +194,12 @@
 
         engineWrapper =
             canvasAnalogWatchFaceService.onCreateEngine() as WatchFaceService.EngineWrapper
+        engineWrapper.onSurfaceChanged(
+            surfaceHolder,
+            0,
+            surfaceHolder.surfaceFrame.width(),
+            surfaceHolder.surfaceFrame.height()
+        )
     }
 
     private fun initGles2WatchFace() {
@@ -214,6 +220,12 @@
         setPendingWallpaperInteractiveWatchFaceInstance()
 
         engineWrapper = glesWatchFaceService.onCreateEngine() as WatchFaceService.EngineWrapper
+        engineWrapper.onSurfaceChanged(
+            surfaceHolder,
+            0,
+            surfaceHolder.surfaceFrame.width(),
+            surfaceHolder.surfaceFrame.height()
+        )
     }
 
     private fun setPendingWallpaperInteractiveWatchFaceInstance() {
@@ -556,6 +568,12 @@
             )
 
             engineWrapper = service.onCreateEngine() as WatchFaceService.EngineWrapper
+            engineWrapper.onSurfaceChanged(
+                surfaceHolder,
+                0,
+                surfaceHolder.surfaceFrame.width(),
+                surfaceHolder.surfaceFrame.height()
+            )
             handler.post { engineWrapper.draw() }
         }
 
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
index acfc804..4c55fd3 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
@@ -200,7 +200,7 @@
         @ColorInt color: Int
     ) {
         if (!attachedComplication!!.fixedComplicationProvider) {
-            ComplicationOutlineRenderer.drawComplicationSelectOutline(
+            ComplicationOutlineRenderer.drawComplicationOutline(
                 canvas,
                 bounds,
                 color
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt
index befcb84..2467a50 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt
@@ -44,7 +44,7 @@
 
         /** Draws a thick line around the complication with the given bounds. */
         @JvmStatic
-        public fun drawComplicationSelectOutline(
+        public fun drawComplicationOutline(
             canvas: Canvas,
             bounds: Rect,
             @ColorInt color: Int
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 978e628..2c5d45d 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -352,6 +352,7 @@
         private var pendingVisibilityChanged: Boolean? = null
         private var pendingComplicationDataUpdates = ArrayList<PendingComplicationData>()
         private var complicationsActivated = false
+        private var watchFaceInitStarted = false
 
         // Only valid after onSetBinder has been called.
         private var systemApiVersion = -1
@@ -363,7 +364,7 @@
         internal var lastActiveComplications: IntArray? = null
         internal var lastA11yLabels: Array<ContentDescriptionLabel>? = null
 
-        private var watchFaceInitStarted = false
+        private var firstOnSurfaceChangedReceived = false
         private var asyncWatchFaceConstructionPending = false
 
         private var initialUserStyle: UserStyleWireFormat? = null
@@ -371,17 +372,14 @@
 
         private var createdBy = "?"
 
-        init {
-            TraceEvent("EngineWrapper.init").use {
-                // If this is a headless instance then we don't want to create a WCS instance.
-                if (!mutableWatchState.isHeadless) {
-                    maybeCreateWCSApi()
-                }
-            }
-        }
-
+        /** Note this function should only be called once. */
         @SuppressWarnings("NewApi")
-        private fun maybeCreateWCSApi() {
+        private fun maybeCreateWCSApi(): Unit = TraceEvent("EngineWrapper.maybeCreateWCSApi").use {
+            // If this is a headless instance then we don't want to create a WCS instance.
+            if (mutableWatchState.isHeadless) {
+                return
+            }
+
             val pendingWallpaperInstance =
                 InteractiveInstanceManager.takePendingWallpaperInteractiveWatchFaceInstance()
 
@@ -754,6 +752,23 @@
             )
         }
 
+        override fun onSurfaceChanged(
+            holder: SurfaceHolder?,
+            format: Int,
+            width: Int,
+            height: Int
+        ): Unit = TraceEvent("EngineWrapper.onSurfaceChanged").use {
+            super.onSurfaceChanged(holder, format, width, height)
+
+            // We can only call maybeCreateWCSApi once. For OpenGL watch faces we need to wait for
+            // onSurfaceChanged before bootstrapping because the surface isn't valid for creating
+            // an EGL context until then.
+            if (!firstOnSurfaceChangedReceived) {
+                maybeCreateWCSApi()
+                firstOnSurfaceChangedReceived = true
+            }
+        }
+
         override fun onDestroy(): Unit = TraceEvent("EngineWrapper.onDestroy").use {
             destroyed = true
             uiThreadHandler.removeCallbacks(invalidateRunnable)
@@ -1292,6 +1307,7 @@
                 }
             }
             writer.println("createdBy=$createdBy")
+            writer.println("firstOnSurfaceChanged=$firstOnSurfaceChangedReceived")
             writer.println("watchFaceInitStarted=$watchFaceInitStarted")
             writer.println("asyncWatchFaceConstructionPending=$asyncWatchFaceConstructionPending")
             writer.println("ignoreNextOnVisibilityChanged=$ignoreNextOnVisibilityChanged")
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
index 718dd4c..719ede7 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
@@ -91,6 +91,7 @@
 @RunWith(WatchFaceTestRunner::class)
 public class AsyncWatchFaceInitTest {
     private val handler = mock<Handler>()
+    private val surfaceHolder = mock<SurfaceHolder>()
     private var looperTimeMillis = 0L
     private val pendingTasks = PriorityQueue<Task>()
     private val userStyleRepository = UserStyleRepository(UserStyleSchema(emptyList()))
@@ -176,6 +177,7 @@
         )
 
         val engineWrapper = service.onCreateEngine() as WatchFaceService.EngineWrapper
+        engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
 
         runPostedTasksFor(0)
 
@@ -215,7 +217,8 @@
             initParams
         )
 
-        service.onCreateEngine() as WatchFaceService.EngineWrapper
+        val engineWrapper = service.onCreateEngine() as WatchFaceService.EngineWrapper
+        engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
         runPostedTasksFor(0)
 
         var pendingInteractiveWatchFaceWcs: IInteractiveWatchFaceWCS? = null
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index c7457c6..5e7c56a 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -2279,7 +2279,7 @@
             )
         )
 
-        service.onCreateEngine()
+        service.onCreateEngine().onSurfaceChanged(surfaceHolder, 0, 100, 100)
 
         runPendingPostedDispatchedContinuationTasks()
 
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt b/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt
index 54988dd..47b72bb 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt
@@ -46,6 +46,7 @@
 import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
 import androidx.test.espresso.matcher.ViewMatchers.withId
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.screenshot.AndroidXScreenshotTestRule
@@ -633,6 +634,7 @@
         testEventsFast("touch_fast_screenshot", views)
     }
 
+    @FlakyTest // b/182268136
     @Test(timeout = 5000)
     fun testMarginTouch() {
         val views = createTwoArcsWithMargin()
diff --git a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java
index e39e5ca..bc7e5b5 100644
--- a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java
+++ b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java
@@ -101,7 +101,7 @@
     private final int mState;
 
     public ExtensionFoldingFeature(@NonNull Rect bounds, @Type int type, @State int state) {
-        validateFeatureBounds(bounds, type);
+        validateFeatureBounds(bounds);
         mBounds = new Rect(bounds);
         mType = type;
         mState = state;
@@ -129,26 +129,13 @@
     /**
      * Verifies the bounds of the folding feature.
      */
-    private static void validateFeatureBounds(@NonNull Rect bounds, int type) {
+    private static void validateFeatureBounds(@NonNull Rect bounds) {
         if (bounds.width() == 0 && bounds.height() == 0) {
             throw new IllegalArgumentException("Bounds must be non zero");
         }
-        if (type == TYPE_FOLD) {
-            if (bounds.width() != 0 && bounds.height() != 0) {
-                throw new IllegalArgumentException("Bounding rectangle must be either zero-wide "
-                        + "or zero-high for features of type " + typeToString(type));
-            }
-
-            if ((bounds.width() != 0 && bounds.left != 0)
-                    || (bounds.height() != 0 && bounds.top != 0)) {
-                throw new IllegalArgumentException("Bounding rectangle must span the entire "
-                        + "window space for features of type " + typeToString(type));
-            }
-        } else if (type == TYPE_HINGE) {
-            if (bounds.left != 0 && bounds.top != 0) {
-                throw new IllegalArgumentException("Bounding rectangle must span the entire "
-                        + "window space for features of type " + typeToString(type));
-            }
+        if (bounds.left != 0 && bounds.top != 0) {
+            throw new IllegalArgumentException("Bounding rectangle must start at the top or "
+                    + "left window edge for folding features");
         }
     }
 
diff --git a/window/window-samples/src/main/java/androidx/window/sample/DisplayFeaturesActivity.kt b/window/window-samples/src/main/java/androidx/window/sample/DisplayFeaturesActivity.kt
index 264cd13..7dee369 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/DisplayFeaturesActivity.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/DisplayFeaturesActivity.kt
@@ -22,7 +22,6 @@
 import android.widget.FrameLayout
 import android.widget.TextView
 import androidx.core.util.Consumer
-import androidx.window.FoldingFeature
 import androidx.window.WindowLayoutInfo
 import androidx.window.WindowManager
 import java.text.SimpleDateFormat
@@ -94,12 +93,7 @@
                 }
 
                 val featureView = View(this)
-                val foldFeature = displayFeature as? FoldingFeature
-                val color = when (foldFeature?.type) {
-                    FoldingFeature.TYPE_FOLD -> getColor(R.color.colorFeatureFold)
-                    FoldingFeature.TYPE_HINGE -> getColor(R.color.colorFeatureHinge)
-                    else -> getColor(R.color.colorFeatureUnknown)
-                }
+                val color = getColor(R.color.colorFeatureFold)
                 featureView.foreground = ColorDrawable(color)
 
                 rootLayout.addView(featureView, lp)
diff --git a/window/window-samples/src/main/java/androidx/window/sample/SplitLayout.kt b/window/window-samples/src/main/java/androidx/window/sample/SplitLayout.kt
index cc0b341..583b46e 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/SplitLayout.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/SplitLayout.kt
@@ -25,8 +25,6 @@
 import android.widget.FrameLayout
 import androidx.window.DisplayFeature
 import androidx.window.FoldingFeature
-import androidx.window.FoldingFeature.TYPE_FOLD
-import androidx.window.FoldingFeature.TYPE_HINGE
 import androidx.window.WindowLayoutInfo
 
 /**
@@ -195,7 +193,6 @@
 
     private fun isValidFoldFeature(displayFeature: DisplayFeature): Boolean {
         val feature = displayFeature as? FoldingFeature ?: return false
-        return (feature.type == TYPE_FOLD || feature.type == TYPE_HINGE) &&
-            getFeaturePositionInViewRect(feature, this) != null
+        return getFeaturePositionInViewRect(feature, this) != null
     }
 }
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt b/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt
index 3ea6946..af7dbca 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt
@@ -57,22 +57,11 @@
     }
 
     /**
-     * @return {@link DeviceState} with an unknown posture.
-     * @deprecated Will be removed when method is removed from {@link WindowBackend}
-     */
-    @Deprecated("Added for compatibility with WindowBackend in sample")
-    override fun getDeviceState(): DeviceState {
-        return DeviceState.Builder().setPosture(DeviceState.POSTURE_OPENED).build()
-    }
-
-    /**
      * @param activity Currently running {@link Activity}.
      * @return A fake {@link WindowLayoutInfo} with a fold in the middle matching the {@link
      * FoldAxis}.
-     * @deprecated Visibility will be reduced when method is removed from {@link WindowBackend}.
      */
-    @Deprecated("Exposed for compatibility with WindowBackend in sample")
-    override fun getWindowLayoutInfo(activity: Activity): WindowLayoutInfo {
+    private fun getWindowLayoutInfo(activity: Activity): WindowLayoutInfo {
         val windowSize = activity.calculateWindowSizeExt()
         val featureRect = foldRect(windowSize)
 
@@ -88,11 +77,6 @@
     }
 
     @Deprecated("Added for compatibility with WindowBackend in sample")
-    override fun getWindowLayoutInfo(context: Context): WindowLayoutInfo {
-        return WindowLayoutInfo.Builder().setDisplayFeatures(emptyList()).build()
-    }
-
-    @Deprecated("Added for compatibility with WindowBackend in sample")
     override fun registerLayoutChangeCallback(
         context: Context,
         executor: Executor,
diff --git a/window/window-samples/src/main/res/layout/activity_display_features.xml b/window/window-samples/src/main/res/layout/activity_display_features.xml
index cb3c351..cabe1f4 100644
--- a/window/window-samples/src/main/res/layout/activity_display_features.xml
+++ b/window/window-samples/src/main/res/layout/activity_display_features.xml
@@ -63,44 +63,6 @@
                 android:text="@string/fold" />
         </LinearLayout>
 
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:orientation="horizontal">
-
-            <ImageView
-                android:id="@+id/hingeColorImageView"
-                android:layout_width="20dp"
-                android:layout_height="20dp"
-                android:foreground="@color/colorFeatureHinge" />
-
-            <TextView
-                android:id="@+id/hingeColorTextView"
-                android:layout_width="0dp"
-                android:layout_height="wrap_content"
-                android:layout_weight="1"
-                android:text="@string/hinge" />
-        </LinearLayout>
-
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:orientation="horizontal">
-
-            <ImageView
-                android:id="@+id/unknownColorImageView"
-                android:layout_width="20dp"
-                android:layout_height="20dp"
-                android:foreground="@color/colorFeatureUnknown" />
-
-            <TextView
-                android:id="@+id/unknownColorTextView"
-                android:layout_width="0dp"
-                android:layout_height="wrap_content"
-                android:layout_weight="1"
-                android:text="@string/unknown" />
-        </LinearLayout>
-
     </LinearLayout>
 
     <TextView
diff --git a/window/window-samples/src/main/res/values/colors.xml b/window/window-samples/src/main/res/values/colors.xml
index 483f736..41a72b2 100644
--- a/window/window-samples/src/main/res/values/colors.xml
+++ b/window/window-samples/src/main/res/values/colors.xml
@@ -21,8 +21,6 @@
     <color name="colorAccent">#03DAC5</color>
 
     <color name="colorFeatureFold">#7700FF00</color>
-    <color name="colorFeatureHinge">#77FF0000</color>
-    <color name="colorFeatureUnknown">#77000000</color>
 
     <color name="colorSplitContentBackground">#3B6BDB4C</color>
     <color name="colorSplitControlsBackground">#475ABFF3</color>
diff --git a/window/window-samples/src/main/res/values/strings.xml b/window/window-samples/src/main/res/values/strings.xml
index e5ad108..9dd6a26 100644
--- a/window/window-samples/src/main/res/values/strings.xml
+++ b/window/window-samples/src/main/res/values/strings.xml
@@ -20,8 +20,6 @@
     <string name="stateUpdateLog">State update log</string>
     <string name="deviceState">Device state</string>
     <string name="windowLayout">Window layout</string>
-    <string name="hinge">Hinge</string>
-    <string name="unknown">Unknown</string>
     <string name="fold">Fold</string>
     <string name="legend">Legend:</string>
     <string name="content_title">Content title</string>
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index 52c9682..e8f74ed 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -33,8 +33,15 @@
   public class FoldingFeature implements androidx.window.DisplayFeature {
     ctor public FoldingFeature(android.graphics.Rect, int, int);
     method public android.graphics.Rect getBounds();
+    method public int getOcclusionMode();
+    method public int getOrientation();
     method public int getState();
-    method public int getType();
+    method @Deprecated public int getType();
+    method public boolean isSeparating();
+    field public static final int OCCLUSION_FULL = 1; // 0x1
+    field public static final int OCCLUSION_NONE = 0; // 0x0
+    field public static final int ORIENTATION_HORIZONTAL = 1; // 0x1
+    field public static final int ORIENTATION_VERTICAL = 0; // 0x0
     field public static final int STATE_FLAT = 1; // 0x1
     field public static final int STATE_FLIPPED = 3; // 0x3
     field public static final int STATE_HALF_OPENED = 2; // 0x2
@@ -43,9 +50,6 @@
   }
 
   public interface WindowBackend {
-    method @Deprecated public androidx.window.DeviceState getDeviceState();
-    method @Deprecated public androidx.window.WindowLayoutInfo getWindowLayoutInfo(android.app.Activity);
-    method @Deprecated public androidx.window.WindowLayoutInfo getWindowLayoutInfo(android.content.Context);
     method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void registerLayoutChangeCallback(android.app.Activity, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
     method @Deprecated public void registerLayoutChangeCallback(android.content.Context, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
@@ -65,14 +69,10 @@
 
   public final class WindowManager {
     ctor public WindowManager(android.content.Context);
-    ctor @Deprecated public WindowManager(android.content.Context, androidx.window.WindowBackend?);
+    ctor public WindowManager(android.content.Context, androidx.window.WindowBackend);
     method public androidx.window.WindowMetrics getCurrentWindowMetrics();
-    method @Deprecated public androidx.window.DeviceState getDeviceState();
     method public androidx.window.WindowMetrics getMaximumWindowMetrics();
-    method @Deprecated public androidx.window.WindowLayoutInfo getWindowLayoutInfo();
-    method @Deprecated public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void registerLayoutChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
-    method @Deprecated public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
   }
 
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index 52c9682..e8f74ed 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -33,8 +33,15 @@
   public class FoldingFeature implements androidx.window.DisplayFeature {
     ctor public FoldingFeature(android.graphics.Rect, int, int);
     method public android.graphics.Rect getBounds();
+    method public int getOcclusionMode();
+    method public int getOrientation();
     method public int getState();
-    method public int getType();
+    method @Deprecated public int getType();
+    method public boolean isSeparating();
+    field public static final int OCCLUSION_FULL = 1; // 0x1
+    field public static final int OCCLUSION_NONE = 0; // 0x0
+    field public static final int ORIENTATION_HORIZONTAL = 1; // 0x1
+    field public static final int ORIENTATION_VERTICAL = 0; // 0x0
     field public static final int STATE_FLAT = 1; // 0x1
     field public static final int STATE_FLIPPED = 3; // 0x3
     field public static final int STATE_HALF_OPENED = 2; // 0x2
@@ -43,9 +50,6 @@
   }
 
   public interface WindowBackend {
-    method @Deprecated public androidx.window.DeviceState getDeviceState();
-    method @Deprecated public androidx.window.WindowLayoutInfo getWindowLayoutInfo(android.app.Activity);
-    method @Deprecated public androidx.window.WindowLayoutInfo getWindowLayoutInfo(android.content.Context);
     method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void registerLayoutChangeCallback(android.app.Activity, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
     method @Deprecated public void registerLayoutChangeCallback(android.content.Context, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
@@ -65,14 +69,10 @@
 
   public final class WindowManager {
     ctor public WindowManager(android.content.Context);
-    ctor @Deprecated public WindowManager(android.content.Context, androidx.window.WindowBackend?);
+    ctor public WindowManager(android.content.Context, androidx.window.WindowBackend);
     method public androidx.window.WindowMetrics getCurrentWindowMetrics();
-    method @Deprecated public androidx.window.DeviceState getDeviceState();
     method public androidx.window.WindowMetrics getMaximumWindowMetrics();
-    method @Deprecated public androidx.window.WindowLayoutInfo getWindowLayoutInfo();
-    method @Deprecated public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void registerLayoutChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
-    method @Deprecated public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
   }
 
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index 52c9682..e8f74ed 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -33,8 +33,15 @@
   public class FoldingFeature implements androidx.window.DisplayFeature {
     ctor public FoldingFeature(android.graphics.Rect, int, int);
     method public android.graphics.Rect getBounds();
+    method public int getOcclusionMode();
+    method public int getOrientation();
     method public int getState();
-    method public int getType();
+    method @Deprecated public int getType();
+    method public boolean isSeparating();
+    field public static final int OCCLUSION_FULL = 1; // 0x1
+    field public static final int OCCLUSION_NONE = 0; // 0x0
+    field public static final int ORIENTATION_HORIZONTAL = 1; // 0x1
+    field public static final int ORIENTATION_VERTICAL = 0; // 0x0
     field public static final int STATE_FLAT = 1; // 0x1
     field public static final int STATE_FLIPPED = 3; // 0x3
     field public static final int STATE_HALF_OPENED = 2; // 0x2
@@ -43,9 +50,6 @@
   }
 
   public interface WindowBackend {
-    method @Deprecated public androidx.window.DeviceState getDeviceState();
-    method @Deprecated public androidx.window.WindowLayoutInfo getWindowLayoutInfo(android.app.Activity);
-    method @Deprecated public androidx.window.WindowLayoutInfo getWindowLayoutInfo(android.content.Context);
     method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void registerLayoutChangeCallback(android.app.Activity, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
     method @Deprecated public void registerLayoutChangeCallback(android.content.Context, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
@@ -65,14 +69,10 @@
 
   public final class WindowManager {
     ctor public WindowManager(android.content.Context);
-    ctor @Deprecated public WindowManager(android.content.Context, androidx.window.WindowBackend?);
+    ctor public WindowManager(android.content.Context, androidx.window.WindowBackend);
     method public androidx.window.WindowMetrics getCurrentWindowMetrics();
-    method @Deprecated public androidx.window.DeviceState getDeviceState();
     method public androidx.window.WindowMetrics getMaximumWindowMetrics();
-    method @Deprecated public androidx.window.WindowLayoutInfo getWindowLayoutInfo();
-    method @Deprecated public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void registerLayoutChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
-    method @Deprecated public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
   }
 
diff --git a/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java b/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java
index c0d318e..e78d762 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java
@@ -17,9 +17,8 @@
 package androidx.window;
 
 import static androidx.window.ExtensionInterfaceCompat.ExtensionCallbackInterface;
-import static androidx.window.TestBoundsUtil.invalidFoldBounds;
-import static androidx.window.TestBoundsUtil.invalidHingeBounds;
-import static androidx.window.TestBoundsUtil.validFoldBound;
+import static androidx.window.TestFoldingFeatureUtil.invalidFoldBounds;
+import static androidx.window.TestFoldingFeatureUtil.validFoldBound;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -160,7 +159,6 @@
 
         FoldingFeature foldingFeature = (FoldingFeature) capturedDisplayFeature;
         assertNotNull(foldingFeature);
-        assertEquals(FoldingFeature.TYPE_HINGE, foldingFeature.getType());
         assertEquals(bounds, capturedDisplayFeature.getBounds());
     }
 
@@ -272,7 +270,7 @@
                         ExtensionFoldingFeature.STATE_FLAT));
             }
 
-            for (Rect malformedBound : invalidHingeBounds(WINDOW_BOUNDS)) {
+            for (Rect malformedBound : invalidFoldBounds(WINDOW_BOUNDS)) {
                 malformedFeatures.add(new ExtensionFoldingFeature(malformedBound,
                         ExtensionFoldingFeature.TYPE_HINGE,
                         ExtensionFoldingFeature.STATE_FLAT));
diff --git a/window/window/src/androidTest/java/androidx/window/ExtensionTest.java b/window/window/src/androidTest/java/androidx/window/ExtensionTest.java
index a34be4a..f9db374 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionTest.java
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionTest.java
@@ -299,10 +299,6 @@
             return false;
         }
         FoldingFeature feature = (FoldingFeature) displayFeature;
-        int featureType = feature.getType();
-        if (featureType != FoldingFeature.TYPE_FOLD && featureType != FoldingFeature.TYPE_HINGE) {
-            return false;
-        }
 
         Rect featureRect = feature.getBounds();
         WindowMetrics windowMetrics = new WindowManager(activity).getCurrentWindowMetrics();
diff --git a/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java b/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java
index 9339492..fcf70d6 100644
--- a/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java
+++ b/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java
@@ -16,23 +16,39 @@
 
 package androidx.window;
 
+import static androidx.window.FoldingFeature.OCCLUSION_FULL;
+import static androidx.window.FoldingFeature.OCCLUSION_NONE;
+import static androidx.window.FoldingFeature.ORIENTATION_HORIZONTAL;
+import static androidx.window.FoldingFeature.ORIENTATION_VERTICAL;
+import static androidx.window.FoldingFeature.OcclusionType;
+import static androidx.window.FoldingFeature.Orientation;
 import static androidx.window.FoldingFeature.STATE_FLAT;
 import static androidx.window.FoldingFeature.STATE_FLIPPED;
 import static androidx.window.FoldingFeature.STATE_HALF_OPENED;
 import static androidx.window.FoldingFeature.TYPE_FOLD;
 import static androidx.window.FoldingFeature.TYPE_HINGE;
+import static androidx.window.FoldingFeature.occlusionTypeToString;
+import static androidx.window.FoldingFeature.orientationToString;
+import static androidx.window.TestFoldingFeatureUtil.allFoldStates;
+import static androidx.window.TestFoldingFeatureUtil.allFoldingFeatureTypeAndStates;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
 
 import android.graphics.Rect;
 
+import androidx.annotation.NonNull;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /** Tests for {@link FoldingFeature} class. */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -44,11 +60,6 @@
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void testFoldWithNonZeroArea() {
-        new FoldingFeature(new Rect(0, 0, 20, 30), TYPE_FOLD, STATE_FLIPPED);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
     public void testHorizontalHingeWithNonZeroOrigin() {
         new FoldingFeature(new Rect(1, 10, 20, 10), TYPE_HINGE, STATE_FLIPPED);
     }
@@ -68,7 +79,18 @@
         new FoldingFeature(new Rect(10, 1, 10, 20), TYPE_FOLD, STATE_FLIPPED);
     }
 
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidType() {
+        new FoldingFeature(new Rect(0, 10, 30, 10), -1, STATE_HALF_OPENED);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidState() {
+        new FoldingFeature(new Rect(0, 10, 30, 10), TYPE_FOLD, -1);
+    }
+
     @Test
+    @SuppressWarnings("deprecation") // TODO(b/173739071) remove when getType is package private
     public void testSetBoundsAndType() {
         Rect bounds = new Rect(0, 10, 30, 10);
         int type = TYPE_HINGE;
@@ -144,4 +166,116 @@
         assertEquals(original, matching);
         assertEquals(original.hashCode(), matching.hashCode());
     }
+
+    @Test
+    public void testIsSeparating_trueForHinge() {
+        Rect bounds = new Rect(1, 0, 1, 10);
+
+        for (FoldingFeature feature : allFoldStates(bounds, TYPE_HINGE)) {
+            assertTrue(separatingModeErrorMessage(true, feature), feature.isSeparating());
+        }
+    }
+
+    @Test
+    public void testIsSeparating_falseForFlatFold() {
+        Rect bounds = new Rect(1, 0, 1, 10);
+
+        FoldingFeature feature = new FoldingFeature(bounds, TYPE_FOLD, STATE_FLAT);
+
+        assertFalse(separatingModeErrorMessage(false, feature), feature.isSeparating());
+    }
+
+    @Test
+    public void testIsSeparating_trueForNotFlatFold() {
+        Rect bounds = new Rect(1, 0, 1, 10);
+
+        List<FoldingFeature> nonFlatFeatures = new ArrayList<>();
+        for (FoldingFeature feature : allFoldStates(bounds, TYPE_FOLD)) {
+            if (feature.getState() != STATE_FLAT) {
+                nonFlatFeatures.add(feature);
+            }
+        }
+
+        for (FoldingFeature feature : nonFlatFeatures) {
+            assertTrue(separatingModeErrorMessage(true, feature), feature.isSeparating());
+        }
+    }
+
+    @Test
+    public void testOcclusionTypeNone_emptyFeature() {
+        Rect bounds = new Rect(0, 100, 100, 100);
+
+        for (FoldingFeature feature: allFoldingFeatureTypeAndStates(bounds)) {
+            assertEquals(occlusionTypeErrorMessage(OCCLUSION_NONE, feature),
+                    OCCLUSION_NONE, feature.getOcclusionMode());
+        }
+    }
+
+    @Test
+    public void testOcclusionTypeFull_nonEmptyHingeFeature() {
+        Rect bounds = new Rect(0, 100, 100, 101);
+
+        for (FoldingFeature feature: allFoldStates(bounds, TYPE_HINGE)) {
+            assertEquals(occlusionTypeErrorMessage(OCCLUSION_FULL, feature),
+                    OCCLUSION_FULL, feature.getOcclusionMode());
+        }
+    }
+
+    @Test
+    public void testGetFeatureOrientation_isHorizontalWhenWidthIsGreaterThanHeight() {
+        Rect bounds = new Rect(0, 100, 200, 100);
+
+        for (FoldingFeature feature: allFoldingFeatureTypeAndStates(bounds)) {
+            assertEquals(featureOrientationErrorMessage(ORIENTATION_HORIZONTAL, feature),
+                    ORIENTATION_HORIZONTAL, feature.getOrientation());
+        }
+    }
+
+    @Test
+    public void testGetFeatureOrientation_isVerticalWhenHeightIsGreaterThanWidth() {
+        Rect bounds = new Rect(100, 0, 100, 200);
+
+        for (FoldingFeature feature: allFoldingFeatureTypeAndStates(bounds)) {
+            assertEquals(featureOrientationErrorMessage(ORIENTATION_VERTICAL, feature),
+                    ORIENTATION_VERTICAL, feature.getOrientation());
+        }
+    }
+
+    @Test
+    public void testGetFeatureOrientation_isVerticalWhenHeightIsEqualToWidth() {
+        Rect bounds = new Rect(0, 0, 100, 100);
+
+        for (FoldingFeature feature: allFoldingFeatureTypeAndStates(bounds)) {
+            assertEquals(featureOrientationErrorMessage(ORIENTATION_VERTICAL, feature),
+                    ORIENTATION_VERTICAL, feature.getOrientation());
+        }
+    }
+
+    @NonNull
+    private String separatingModeErrorMessage(boolean expected, @NonNull FoldingFeature feature) {
+        return errorMessage(FoldingFeature.class.getSimpleName(), "isSeparating",
+                Boolean.toString(expected), Boolean.toString(feature.isSeparating()), feature);
+    }
+
+    @NonNull
+    private static String occlusionTypeErrorMessage(@OcclusionType int expected,
+            FoldingFeature feature) {
+        return errorMessage(FoldingFeature.class.getSimpleName(), "getOcclusionMode",
+                occlusionTypeToString(expected),
+                occlusionTypeToString(feature.getOcclusionMode()), feature);
+    }
+
+    @NonNull
+    private static String featureOrientationErrorMessage(@Orientation int expected,
+            FoldingFeature feature) {
+        return errorMessage(FoldingFeature.class.getSimpleName(), "getFeatureOrientation",
+                orientationToString(expected),
+                orientationToString(feature.getOrientation()), feature);
+    }
+
+    private static String errorMessage(String className, String methodName, String expected,
+            String actual, Object value) {
+        return String.format("%s#%s was expected to be %s but was %s. %s: %s", className,
+                methodName, expected, actual, className, value.toString());
+    }
 }
diff --git a/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java b/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java
index d2552fc..99e5f35 100644
--- a/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java
+++ b/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java
@@ -19,9 +19,8 @@
 import static androidx.window.SidecarAdapter.getSidecarDisplayFeatures;
 import static androidx.window.SidecarAdapter.setSidecarDevicePosture;
 import static androidx.window.SidecarAdapter.setSidecarDisplayFeatures;
-import static androidx.window.TestBoundsUtil.invalidFoldBounds;
-import static androidx.window.TestBoundsUtil.invalidHingeBounds;
-import static androidx.window.TestBoundsUtil.validFoldBound;
+import static androidx.window.TestFoldingFeatureUtil.invalidFoldBounds;
+import static androidx.window.TestFoldingFeatureUtil.validFoldBound;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -299,7 +298,6 @@
         DisplayFeature capturedDisplayFeature = capturedLayout.getDisplayFeatures().get(0);
         FoldingFeature foldingFeature = (FoldingFeature) capturedDisplayFeature;
         assertNotNull(foldingFeature);
-        assertEquals(FoldingFeature.TYPE_HINGE, foldingFeature.getType());
         assertEquals(bounds, capturedDisplayFeature.getBounds());
     }
 
@@ -561,7 +559,7 @@
                         SidecarDisplayFeature.TYPE_FOLD));
             }
 
-            for (Rect malformedBound : invalidHingeBounds(WINDOW_BOUNDS)) {
+            for (Rect malformedBound : invalidFoldBounds(WINDOW_BOUNDS)) {
                 malformedFeatures.add(newDisplayFeature(malformedBound,
                         SidecarDisplayFeature.TYPE_HINGE));
             }
diff --git a/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java b/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java
index 50caab8..4730074 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java
+++ b/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java
@@ -51,18 +51,14 @@
     @Test
     public void testFakeWindowBackend() {
         WindowLayoutInfo windowLayoutInfo = newTestWindowLayout();
-        DeviceState deviceState = newTestDeviceState();
-        WindowBackend windowBackend = new FakeWindowBackend(windowLayoutInfo, deviceState);
+        WindowBackend windowBackend = new FakeWindowBackend(windowLayoutInfo);
         TestActivity activity = mActivityTestRule.launchActivity(new Intent());
         WindowManager wm = new WindowManager(activity, windowBackend);
         Consumer<WindowLayoutInfo> layoutInfoConsumer = mock(Consumer.class);
-        Consumer<DeviceState> stateConsumer = mock(Consumer.class);
 
         wm.registerLayoutChangeCallback(MoreExecutors.directExecutor(), layoutInfoConsumer);
-        wm.registerDeviceStateChangeCallback(MoreExecutors.directExecutor(), stateConsumer);
 
         verify(layoutInfoConsumer).accept(windowLayoutInfo);
-        verify(stateConsumer).accept(deviceState);
     }
 
     private WindowLayoutInfo newTestWindowLayout() {
@@ -74,54 +70,11 @@
         return new WindowLayoutInfo(displayFeatureList);
     }
 
-    private DeviceState newTestDeviceState() {
-        return new DeviceState(DeviceState.POSTURE_OPENED);
-    }
-
     private static class FakeWindowBackend implements WindowBackend {
         private WindowLayoutInfo mWindowLayoutInfo;
-        private DeviceState mDeviceState;
 
-        private FakeWindowBackend(@NonNull WindowLayoutInfo windowLayoutInfo,
-                @NonNull DeviceState deviceState) {
+        private FakeWindowBackend(@NonNull WindowLayoutInfo windowLayoutInfo) {
             mWindowLayoutInfo = windowLayoutInfo;
-            mDeviceState = deviceState;
-        }
-
-
-        /**
-         * @deprecated will be removed in next alpha
-         * @return nothing, throws an exception.
-         */
-        @Override
-        @NonNull
-        @Deprecated // TODO(b/173739071) Remove in next alpha.
-        public DeviceState getDeviceState() {
-            throw new RuntimeException("Deprecated method");
-        }
-
-        /**
-         * @deprecated will be removed in next alpha
-         * @param activity any {@link Activity}
-         * @return nothing, throws an exception since this is depredcated
-         */
-        @Override
-        @NonNull
-        @Deprecated // TODO(b/173739071) Remove in next alpha.
-        public WindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) {
-            throw new RuntimeException("Deprecated method");
-        }
-
-        /**
-         * @deprecated will be removed in next alpha
-         * @param context any {@link Context}
-         * @return nothing, throws an exception since this is deprecated.
-         */
-        @NonNull
-        @Override
-        @Deprecated // TODO(b/173739071) Remove in next alpha.
-        public WindowLayoutInfo getWindowLayoutInfo(@NonNull Context context) {
-            throw new RuntimeException("Deprecated method");
         }
 
         /**
@@ -152,12 +105,12 @@
         @Override
         public void registerDeviceStateChangeCallback(@NonNull Executor executor,
                 @NonNull Consumer<DeviceState> callback) {
-            executor.execute(() -> callback.accept(mDeviceState));
+            throw new UnsupportedOperationException("Deprecated method");
         }
 
         @Override
         public void unregisterDeviceStateChangeCallback(@NonNull Consumer<DeviceState> callback) {
-            // Empty
+            throw new UnsupportedOperationException("Deprecated method");
         }
     }
 }
diff --git a/window/window/src/androidTest/java/androidx/window/WindowManagerTest.java b/window/window/src/androidTest/java/androidx/window/WindowManagerTest.java
index 83782d0..8b8e142 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowManagerTest.java
+++ b/window/window/src/androidTest/java/androidx/window/WindowManagerTest.java
@@ -86,21 +86,6 @@
     }
 
     @Test
-    public void testRegisterDeviceStateChangeCallback() {
-        WindowBackend backend = mock(WindowBackend.class);
-        Activity activity = mock(Activity.class);
-        WindowManager wm = new WindowManager(activity, backend);
-
-        Executor executor = MoreExecutors.directExecutor();
-        Consumer<DeviceState> consumer = mock(Consumer.class);
-        wm.registerDeviceStateChangeCallback(executor, consumer);
-        verify(backend).registerDeviceStateChangeCallback(executor, consumer);
-
-        wm.unregisterDeviceStateChangeCallback(consumer);
-        verify(backend).unregisterDeviceStateChangeCallback(eq(consumer));
-    }
-
-    @Test
     public void testGetCurrentWindowMetrics() {
         WindowBackend backend = mock(WindowBackend.class);
         Activity activity = mock(Activity.class);
diff --git a/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java b/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java
index 3379725..692e3c7 100644
--- a/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java
+++ b/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java
@@ -32,7 +32,6 @@
 import androidx.window.extensions.ExtensionInterface;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.Executor;
@@ -96,56 +95,6 @@
         return sInstance;
     }
 
-    /**
-     * @deprecated will be removed in the next alpha.
-     * @return {@link DeviceState} when Sidecar is present and an unknown {@link DeviceState}
-     * otherwise.
-     */
-    @Override
-    @NonNull
-    @Deprecated
-    public DeviceState getDeviceState() {
-        synchronized (sLock) {
-            if (mWindowExtension instanceof SidecarCompat) {
-                SidecarCompat sidecarCompat = (SidecarCompat) mWindowExtension;
-                return sidecarCompat.getDeviceState();
-            }
-            return new DeviceState(DeviceState.POSTURE_UNKNOWN);
-        }
-    }
-
-    /**
-     * @deprecated will be removed in the next alpha.
-     * @param activity that is running.
-     * @return {@link WindowLayoutInfo} for the window containing the {@link Activity} when
-     * Sidecar is present and an empty info otherwise
-     */
-    @NonNull
-    @Override
-    @Deprecated
-    public WindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) {
-        synchronized (sLock) {
-            if (mWindowExtension instanceof SidecarCompat) {
-                SidecarCompat sidecarCompat = (SidecarCompat) mWindowExtension;
-                return sidecarCompat.getWindowLayoutInfo(activity);
-            }
-            return new WindowLayoutInfo(Collections.emptyList());
-        }
-    }
-
-    /**
-     *
-     * @param context with an associated {@link Activity}
-     * @return the {@link WindowLayoutInfo}
-     * @deprecated use an {@link Activity} instead of {@link Context}
-     */
-    @NonNull
-    @Override
-    @Deprecated
-    public WindowLayoutInfo getWindowLayoutInfo(@NonNull Context context) {
-        return getWindowLayoutInfo(assertActivityContext(context));
-    }
-
     @Override
     public void registerLayoutChangeCallback(@NonNull Context context, @NonNull Executor executor,
             @NonNull Consumer<WindowLayoutInfo> callback) {
diff --git a/window/window/src/main/java/androidx/window/FoldingFeature.java b/window/window/src/main/java/androidx/window/FoldingFeature.java
index 34f2b68..388cff7 100644
--- a/window/window/src/main/java/androidx/window/FoldingFeature.java
+++ b/window/window/src/main/java/androidx/window/FoldingFeature.java
@@ -72,6 +72,46 @@
      */
     public static final int STATE_FLIPPED = 3;
 
+    /**
+     * The {@link FoldingFeature} does not occlude the content in any way. One example is a flat
+     * continuous fold where content can stretch across the fold. Another example is a hinge that
+     * has width or height equal to 0. In this case the content is physically split across both
+     * displays, but fully visible.
+     */
+    public static final int OCCLUSION_NONE = 0;
+
+    /**
+     * The {@link FoldingFeature} occludes all content. One example is a hinge that is considered to
+     * be part of the window, so that part of the UI is not visible to the user. Any content shown
+     * in the same area as the hinge may not be accessible in any way. Fully occluded areas should
+     * always be avoided when placing interactive UI elements and text.
+     */
+    public static final int OCCLUSION_FULL = 1;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            OCCLUSION_NONE,
+            OCCLUSION_FULL
+    })
+    @interface OcclusionType {}
+
+    /**
+     * The height of the {@link FoldingFeature} is greater than or equal to the width.
+     */
+    public static final int ORIENTATION_VERTICAL = 0;
+
+    /**
+     * The width of the {@link FoldingFeature} is greater than the height.
+     */
+    public static final int ORIENTATION_HORIZONTAL = 1;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            ORIENTATION_HORIZONTAL,
+            ORIENTATION_VERTICAL
+    })
+    @interface Orientation {}
+
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({
             STATE_HALF_OPENED,
@@ -100,7 +140,9 @@
     private final int mState;
 
     public FoldingFeature(@NonNull Rect bounds, @Type int type, @State int state) {
-        validateFeatureBounds(bounds, type);
+        validateState(state);
+        validateType(type);
+        validateFeatureBounds(bounds);
         mBounds = new Rect(bounds);
         mType = type;
         mState = state;
@@ -112,7 +154,13 @@
         return new Rect(mBounds);
     }
 
+    /**
+     * Returns type that is either {@link FoldingFeature#TYPE_FOLD} or
+     * {@link FoldingFeature#TYPE_HINGE}
+     * @deprecated visibility will be reduced.
+     */
     @Type
+    @Deprecated
     public int getType() {
         return mType;
     }
@@ -123,28 +171,127 @@
     }
 
     /**
+     * Calculates if a {@link FoldingFeature} should be thought of as splitting the window into
+     * multiple physical areas that can be seen by users as logically separate. Display panels
+     * connected by a hinge are always separated. Folds on flexible screens should be treated as
+     * separating when they are not {@link FoldingFeature#STATE_FLAT}.
+     *
+     * Apps may use this to determine if content should lay out around the {@link FoldingFeature}.
+     * Developers should consider the placement of interactive elements. Similar to the case of
+     * {@link FoldingFeature#OCCLUSION_FULL}, when a feature is separating then consider laying
+     * out the controls around the {@link FoldingFeature}.
+     *
+     * An example use case is to determine if the UI should be split into two logical areas. A media
+     * app where there is some auxiliary content, such as comments or description of a video, may
+     * need to adapt the layout. The media can be put on one side of the {@link FoldingFeature} and
+     * the auxiliary content can be placed on the other side.
+     *
+     * @return {@code true} if the feature splits the display into two areas, {@code false}
+     * otherwise.
+     */
+    public boolean isSeparating() {
+        if (mType == TYPE_HINGE) {
+            return true;
+        }
+        if (mType == TYPE_FOLD && (mState == STATE_FLIPPED || mState == STATE_HALF_OPENED)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Calculates the occlusion mode to determine if a {@link FoldingFeature} occludes a part of
+     * the window. This flag is useful for determining if UI elements need to be moved
+     * around so that the user can access them. For some devices occluded elements can not be
+     * accessed by the user at all.
+     *
+     * For occlusion type {@link FoldingFeature#OCCLUSION_NONE} the feature can be treated as a
+     * guideline. One example would be for a continuously folding screen. For occlusion type
+     * {@link FoldingFeature#OCCLUSION_FULL} the feature should be avoided completely since content
+     * will not be visible or touchable, like a hinge device with two displays.
+     *
+     * The occlusion mode is useful to determine if the UI needs to adapt to the
+     * {@link FoldingFeature}. For example, full screen games should consider avoiding anything in
+     * the occluded region if it negatively affects the gameplay.  The user can not tap
+     * on the occluded interactive UI elements nor can they see important information.
+     *
+     * @return {@link FoldingFeature#OCCLUSION_NONE} if the {@link FoldingFeature} has empty
+     * bounds.
+     */
+    @OcclusionType
+    public int getOcclusionMode() {
+        if (mBounds.width() == 0 || mBounds.height() == 0) {
+            return OCCLUSION_NONE;
+        }
+        return OCCLUSION_FULL;
+    }
+
+    /**
+     * Returns {@link FoldingFeature#ORIENTATION_HORIZONTAL} if the width is greater than the
+     * height, {@link FoldingFeature#ORIENTATION_VERTICAL} otherwise.
+     */
+    @Orientation
+    public int getOrientation() {
+        return mBounds.width() > mBounds.height()
+                ? ORIENTATION_HORIZONTAL
+                : ORIENTATION_VERTICAL;
+    }
+
+    static String occlusionTypeToString(@OcclusionType int type) {
+        switch (type) {
+            case OCCLUSION_NONE:
+                return "OCCLUSION_NONE";
+            case OCCLUSION_FULL:
+                return "OCCLUSION_FULL";
+            default:
+                return "UNKNOWN";
+        }
+    }
+
+    static String orientationToString(@Orientation int direction) {
+        switch (direction) {
+            case ORIENTATION_HORIZONTAL:
+                return "ORIENTATION_HORIZONTAL";
+            case ORIENTATION_VERTICAL:
+                return "ORIENTATION_VERTICAL";
+            default:
+                return "UNKNOWN";
+        }
+    }
+
+    /**
+     * Verifies the state is {@link FoldingFeature#STATE_FLAT},
+     * {@link FoldingFeature#STATE_HALF_OPENED} or {@link FoldingFeature#STATE_FLIPPED}.
+     */
+    private static void validateState(int state) {
+        if (state != STATE_FLAT && state != STATE_HALF_OPENED && state != STATE_FLIPPED) {
+            throw new IllegalArgumentException("State must be either " + stateToString(STATE_FLAT)
+                    + ", " + stateToString(STATE_HALF_OPENED) + ", or "
+                    + stateToString(STATE_FLIPPED));
+        }
+    }
+
+    /**
+     * Verifies the type is either {@link FoldingFeature#TYPE_HINGE} or
+     * {@link FoldingFeature#TYPE_FOLD}
+     */
+    private static void validateType(int type) {
+        if (type != TYPE_FOLD && type != TYPE_HINGE) {
+            throw new IllegalArgumentException("Type must be either " + typeToString(TYPE_FOLD)
+                    + " or " + typeToString(TYPE_HINGE));
+        }
+    }
+
+    /**
      * Verifies the bounds of the folding feature.
      */
-    private static void validateFeatureBounds(@NonNull Rect bounds, int type) {
+    private static void validateFeatureBounds(@NonNull Rect bounds) {
         if (bounds.width() == 0 && bounds.height() == 0) {
             throw new IllegalArgumentException("Bounds must be non zero");
         }
-        if (type == TYPE_FOLD) {
-            if (bounds.width() != 0 && bounds.height() != 0) {
-                throw new IllegalArgumentException("Bounding rectangle must be either zero-wide "
-                        + "or zero-high for features of type " + typeToString(type));
-            }
-
-            if ((bounds.width() != 0 && bounds.left != 0)
-                    || (bounds.height() != 0 && bounds.top != 0)) {
-                throw new IllegalArgumentException("Bounding rectangle must span the entire "
-                        + "window space for features of type " + typeToString(type));
-            }
-        } else if (type == TYPE_HINGE) {
-            if (bounds.left != 0 && bounds.top != 0) {
-                throw new IllegalArgumentException("Bounding rectangle must span the entire "
-                        + "window space for features of type " + typeToString(type));
-            }
+        if (bounds.left != 0 && bounds.top != 0) {
+            throw new IllegalArgumentException("Bounding rectangle must start at the top or "
+                    + "left window edge for folding features");
         }
     }
 
@@ -178,7 +325,7 @@
     @Override
     public String toString() {
         return FoldingFeature.class.getSimpleName() + " { " + mBounds + ", type="
-                + typeToString(getType()) + ", state=" + stateToString(mState) + " }";
+                + typeToString(mType) + ", state=" + stateToString(mState) + " }";
     }
 
     @Override
diff --git a/window/window/src/main/java/androidx/window/SidecarCompat.java b/window/window/src/main/java/androidx/window/SidecarCompat.java
index 52c686c..3659a84 100644
--- a/window/window/src/main/java/androidx/window/SidecarCompat.java
+++ b/window/window/src/main/java/androidx/window/SidecarCompat.java
@@ -69,10 +69,6 @@
         }
     }
 
-    DeviceState getDeviceState() {
-        return mSidecarAdapter.translate(mSidecar.getDeviceState());
-    }
-
     @VisibleForTesting
     SidecarCompat(@NonNull SidecarInterface sidecar, SidecarAdapter sidecarAdapter) {
         // Empty implementation to avoid null checks.
diff --git a/window/window/src/main/java/androidx/window/WindowBackend.java b/window/window/src/main/java/androidx/window/WindowBackend.java
index 1a874f3..b45817b 100644
--- a/window/window/src/main/java/androidx/window/WindowBackend.java
+++ b/window/window/src/main/java/androidx/window/WindowBackend.java
@@ -31,33 +31,6 @@
 public interface WindowBackend {
 
     /**
-     * @return the {@link DeviceState} when the Sidecar library is present and a {@link DeviceState}
-     * with {@code POSTURE_UNKNOWN} otherwise.
-     * @deprecated will be removed in the next alpha
-     */
-    @Deprecated
-    @NonNull
-    DeviceState getDeviceState();
-
-    /**
-     * @return the {@link WindowLayoutInfo} when Sidecar library is present and an empty info
-     * otherwise
-     * @deprecated will be removed in the next alpha
-     */
-    @Deprecated
-    @NonNull
-    WindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity);
-
-    /**
-     * @return the {@link WindowLayoutInfo} when Sidecar library is present and an empty info
-     * otherwise
-     * @deprecated will be removed in the next alpha
-     */
-    @Deprecated
-    @NonNull
-    WindowLayoutInfo getWindowLayoutInfo(@NonNull Context context);
-
-    /**
      * Registers a callback for layout changes of the window for the supplied {@link Activity}.
      * Must be called only after the it is attached to the window.
      */
diff --git a/window/window/src/main/java/androidx/window/WindowManager.java b/window/window/src/main/java/androidx/window/WindowManager.java
index c36bb46..df22d0f 100644
--- a/window/window/src/main/java/androidx/window/WindowManager.java
+++ b/window/window/src/main/java/androidx/window/WindowManager.java
@@ -40,11 +40,11 @@
      * from the {@link androidx.window.sidecar.SidecarInterface} or is passed directly to the
      * {@link ExtensionInterface}.
      */
-    private Activity mActivity;
+    private final Activity mActivity;
     /**
      * The backend that supplies the information through this class.
      */
-    private WindowBackend mWindowBackend;
+    private final WindowBackend mWindowBackend;
 
     /**
      * Gets an instance of the class initialized with and connected to the provided {@link Context}.
@@ -68,10 +68,8 @@
      *                      around one, to use for initialization.
      * @param windowBackend Backing server class that will provide information for this instance.
      *                      Pass a custom {@link WindowBackend} implementation for testing.
-     * @deprecated WindowBackend will be a required argument in the next implementation.
      */
-    @Deprecated
-    public WindowManager(@NonNull Context context, @Nullable WindowBackend windowBackend) {
+    public WindowManager(@NonNull Context context, @NonNull WindowBackend windowBackend) {
         Activity activity = getActivityFromContext(context);
         if (activity == null) {
             throw new IllegalArgumentException("Used non-visual Context to obtain an instance of "
@@ -102,46 +100,6 @@
     }
 
     /**
-     * @deprecated will be removed in the next alpha
-     * @return the current {@link DeviceState} if Sidecar is present and an empty info otherwise
-     */
-    @Deprecated
-    @NonNull
-    public DeviceState getDeviceState() {
-        return mWindowBackend.getDeviceState();
-    }
-
-    /**
-     * @deprecated will be removed in the next alpha
-     * @return the current {@link WindowLayoutInfo} when Sidecar is present and an empty info
-     * otherwise
-     */
-    @Deprecated
-    @NonNull
-    public WindowLayoutInfo getWindowLayoutInfo() {
-        return mWindowBackend.getWindowLayoutInfo(mActivity);
-    }
-
-    /**
-     * @deprecated {@link DeviceState} information has been merged into {@link WindowLayoutInfo}
-     * Registers a callback for device state changes.
-     */
-    @Deprecated
-    public void registerDeviceStateChangeCallback(@NonNull Executor executor,
-            @NonNull Consumer<DeviceState> callback) {
-        mWindowBackend.registerDeviceStateChangeCallback(executor, callback);
-    }
-
-    /**
-     * @deprecated {@link DeviceState} information has been merged into {@link WindowLayoutInfo}
-     * Unregisters a callback for device state changes.
-     */
-    @Deprecated
-    public void unregisterDeviceStateChangeCallback(@NonNull Consumer<DeviceState> callback) {
-        mWindowBackend.unregisterDeviceStateChangeCallback(callback);
-    }
-
-    /**
      * Returns the {@link WindowMetrics} according to the current system state.
      * <p>
      * The metrics describe the size of the area the window would occupy with
diff --git a/window/window/src/test/java/androidx/window/SidecarCompatUnitTest.java b/window/window/src/test/java/androidx/window/SidecarCompatUnitTest.java
index 752b99d..63183ac 100644
--- a/window/window/src/test/java/androidx/window/SidecarCompatUnitTest.java
+++ b/window/window/src/test/java/androidx/window/SidecarCompatUnitTest.java
@@ -17,9 +17,8 @@
 package androidx.window;
 
 import static androidx.window.ActivityUtil.getActivityWindowToken;
-import static androidx.window.TestBoundsUtil.invalidFoldBounds;
-import static androidx.window.TestBoundsUtil.invalidHingeBounds;
-import static androidx.window.TestBoundsUtil.validFoldBound;
+import static androidx.window.TestFoldingFeatureUtil.invalidFoldBounds;
+import static androidx.window.TestFoldingFeatureUtil.validFoldBound;
 
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
@@ -373,7 +372,7 @@
                         SidecarDisplayFeature.TYPE_FOLD));
             }
 
-            for (Rect malformedBound : invalidHingeBounds(WINDOW_BOUNDS)) {
+            for (Rect malformedBound : invalidFoldBounds(WINDOW_BOUNDS)) {
                 malformedFeatures.add(newDisplayFeature(malformedBound,
                         SidecarDisplayFeature.TYPE_HINGE));
             }
diff --git a/window/window/src/testUtil/java/androidx/window/TestBoundsUtil.java b/window/window/src/testUtil/java/androidx/window/TestBoundsUtil.java
deleted file mode 100644
index 3f5ab44..0000000
--- a/window/window/src/testUtil/java/androidx/window/TestBoundsUtil.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.window;
-
-import android.graphics.Rect;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A class containing static methods for creating different window bound types. Test methods are
- * shared between the unit tests and the instrumentation tests.
- */
-final class TestBoundsUtil {
-
-    private TestBoundsUtil() { }
-
-    /**
-     * @param windowBounds the bounds for a window contain a valid fold.
-     * @return {@link Rect} that is a valid fold bound within the given window.
-     */
-    public static Rect validFoldBound(Rect windowBounds) {
-        int verticalMid = windowBounds.height() / 2;
-        return new Rect(0, verticalMid, windowBounds.width(), verticalMid);
-    }
-
-    /**
-     * @return {@link Rect} containing the invalid zero bounds.
-     */
-    static Rect invalidZeroBound() {
-        return new Rect();
-    }
-
-    /**
-     * @param windowBounds the bounds for a window contain an invalid fold.
-     * @return {@link Rect} for bounds where the width is shorter than the window width.
-     */
-    static Rect invalidBoundShortWidth(Rect windowBounds) {
-        return new Rect(0, 0, windowBounds.width() / 2, 0);
-    }
-
-    /**
-     * @param windowBounds the bounds for a window contain an invalid fold.
-     * @return {@link Rect} for bounds where the height is shorter than the window height.
-     */
-    static Rect invalidBoundShortHeight(Rect windowBounds) {
-        return new Rect(0, 0, 0, windowBounds.height() / 2);
-    }
-
-    /**
-     * @param windowBounds the bounds for a window contain an invalid fold.
-     * @return a {@link List} of {@link Rect} of invalid bounds for fold features
-     */
-    static List<Rect> invalidFoldBounds(Rect windowBounds) {
-        List<Rect> badBounds = invalidHingeBounds(windowBounds);
-        Rect nonEmptySmallRect = new Rect(0, 0, 1, 1);
-        badBounds.add(nonEmptySmallRect);
-        return badBounds;
-    }
-
-    /**
-     * @param windowBounds the bounds for a window contain an invalid fold.
-     * @return a {@link List} of {@link Rect} of invalid bounds for hinge features
-     */
-    static List<Rect> invalidHingeBounds(Rect windowBounds) {
-        List<Rect> badBounds = new ArrayList<>();
-
-        badBounds.add(invalidZeroBound());
-        badBounds.add(invalidBoundShortWidth(windowBounds));
-        badBounds.add(invalidBoundShortHeight(windowBounds));
-
-        return badBounds;
-    }
-}
diff --git a/window/window/src/testUtil/java/androidx/window/TestFoldingFeatureUtil.java b/window/window/src/testUtil/java/androidx/window/TestFoldingFeatureUtil.java
new file mode 100644
index 0000000..bc14ba6
--- /dev/null
+++ b/window/window/src/testUtil/java/androidx/window/TestFoldingFeatureUtil.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window;
+
+import static androidx.window.FoldingFeature.STATE_FLAT;
+import static androidx.window.FoldingFeature.STATE_FLIPPED;
+import static androidx.window.FoldingFeature.STATE_HALF_OPENED;
+import static androidx.window.FoldingFeature.TYPE_FOLD;
+import static androidx.window.FoldingFeature.TYPE_HINGE;
+
+import android.graphics.Rect;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class containing static methods for creating different window bound types. Test methods are
+ * shared between the unit tests and the instrumentation tests.
+ */
+final class TestFoldingFeatureUtil {
+
+    private TestFoldingFeatureUtil() {
+    }
+
+    /**
+     * @param windowBounds the bounds of the window.
+     * @return {@link Rect} that is a valid fold bound within the given window.
+     */
+    static Rect validFoldBound(Rect windowBounds) {
+        int verticalMid = windowBounds.height() / 2;
+        return new Rect(0, verticalMid, windowBounds.width(), verticalMid);
+    }
+
+    /**
+     * @return {@link Rect} containing the invalid zero bounds.
+     */
+    static Rect invalidZeroBound() {
+        return new Rect();
+    }
+
+    /**
+     * @param windowBounds the bounds of the window.
+     * @return {@link Rect} for bounds where the width is shorter than the window width.
+     */
+    static Rect invalidBoundShortWidth(Rect windowBounds) {
+        return new Rect(0, 0, windowBounds.width() / 2, 0);
+    }
+
+    /**
+     * @param windowBounds the bounds of the window.
+     * @return {@link Rect} for bounds where the height is shorter than the window height.
+     */
+    static Rect invalidBoundShortHeight(Rect windowBounds) {
+        return new Rect(0, 0, 0, windowBounds.height() / 2);
+    }
+
+    /**
+     * @param windowBounds the bounds of the window.
+     * @return a {@link List} of {@link Rect} of invalid bounds for folding features
+     */
+    static List<Rect> invalidFoldBounds(Rect windowBounds) {
+        List<Rect> badBounds = new ArrayList<>();
+
+        badBounds.add(invalidZeroBound());
+        badBounds.add(invalidBoundShortWidth(windowBounds));
+        badBounds.add(invalidBoundShortHeight(windowBounds));
+
+        return badBounds;
+    }
+
+    /**
+     * @param bounds for the test {@link FoldingFeature}
+     * @param type   of the {@link FoldingFeature}
+     * @return {@link List} of {@link FoldingFeature} containing all the possible states for the
+     * given type.
+     */
+    static List<FoldingFeature> allFoldStates(Rect bounds, @FoldingFeature.Type int type) {
+        List<FoldingFeature> states = new ArrayList<>();
+
+        states.add(new FoldingFeature(bounds, type, STATE_FLAT));
+        states.add(new FoldingFeature(bounds, type, STATE_HALF_OPENED));
+        states.add(new FoldingFeature(bounds, type, STATE_FLIPPED));
+
+        return states;
+    }
+
+    /**
+     * @param bounds for the test {@link FoldingFeature}
+     * @return {@link List} of {@link FoldingFeature} containing all the possible states and
+     * types.
+     */
+    static List<FoldingFeature> allFoldingFeatureTypeAndStates(Rect bounds) {
+        List<FoldingFeature> features = new ArrayList<>();
+        features.addAll(allFoldStates(bounds, TYPE_HINGE));
+        features.addAll(allFoldStates(bounds, TYPE_FOLD));
+        return features;
+    }
+}