[go: nahoru, domu]

Merge "Migrate from AndroidManifest to build.gradle for namespace" into androidx-main
diff --git a/car/app/OWNERS b/car/app/OWNERS
index 040e49d..7cdc4ee 100644
--- a/car/app/OWNERS
+++ b/car/app/OWNERS
@@ -9,3 +9,4 @@
 per-file app-testing/api/*=file:/car/app/API_OWNERS
 
 # Feature owners
+per-file app/*=kodlee@google.com
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongSparseArray.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongSparseArray.kt
new file mode 100644
index 0000000..e891076
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongSparseArray.kt
@@ -0,0 +1,605 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import androidx.collection.internal.binarySearch
+import androidx.collection.internal.idealLongArraySize
+import kotlin.DeprecationLevel.HIDDEN
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+import kotlin.jvm.JvmSynthetic
+
+private val DELETED = Any()
+
+/**
+ * SparseArray mapping longs to Objects. Unlike a normal array of Objects, there can be gaps in the
+ * indices. It is intended to be more memory efficient than using a HashMap to map Longs to Objects,
+ * both because it avoids auto-boxing keys and its data structure doesn't rely on an extra entry
+ * object for each mapping.
+ *
+ * Note that this container keeps its mappings in an array data structure, using a binary search to
+ * find keys. The implementation is not intended to be appropriate for data structures that may
+ * contain large numbers of items. It is generally slower than a traditional HashMap, since lookups
+ * require a binary search and adds and removes require inserting and deleting entries in the array.
+ * For containers holding up to hundreds of items, the performance difference is not significant,
+ * less than 50%.
+ *
+ * To help with performance, the container includes an optimization when removing keys: instead of
+ * compacting its array immediately, it leaves the removed entry marked as deleted. The entry can
+ * then be re-used for the same key, or compacted later in a single garbage collection step of all
+ * removed entries. This garbage collection will need to be performed at any time the array needs to
+ * be grown or the map size or entry values are retrieved.
+ *
+ * It is possible to iterate over the items in this container using [keyAt] and [valueAt]. Iterating
+ * over the keys using [keyAt] with ascending values of the index will return the keys in ascending
+ * order, or the values corresponding to the keys in ascending order in the case of [valueAt].
+ *
+ * @constructor Creates a new [LongSparseArray] containing no mappings that will not require any
+ * additional memory allocation to store the specified number of mappings. If you supply an initial
+ * capacity of 0, the sparse array will be initialized with a light-weight representation not
+ * requiring any additional array allocations.
+ */
+public expect open class LongSparseArray<E>
+@JvmOverloads public constructor(initialCapacity: Int = 10) {
+    @JvmSynthetic // Hide from Java callers.
+    @JvmField
+    internal var garbage: Boolean
+
+    @JvmSynthetic // Hide from Java callers.
+    @JvmField
+    internal var keys: LongArray
+
+    @JvmSynthetic // Hide from Java callers.
+    @JvmField
+    internal var values: Array<Any?>
+
+    @JvmSynthetic // Hide from Java callers.
+    @JvmField
+    internal var size: Int
+
+    /**
+     * Gets the value mapped from the specified [key], or `null` if no such mapping has been made.
+     */
+    public open operator fun get(key: Long): E?
+
+    /**
+     * Gets the value mapped from the specified [key], or [defaultValue] if no such mapping has been
+     * made.
+     */
+    @Suppress("KotlinOperator") // Avoid confusion with matrix access syntax.
+    public open fun get(key: Long, defaultValue: E): E
+
+    /**
+     * Removes the mapping from the specified [key], if there was any.
+     */
+    @Deprecated("Alias for `remove(key)`.", ReplaceWith("remove(key)"))
+    public open fun delete(key: Long): Unit
+
+    /**
+     * Removes the mapping from the specified [key], if there was any.
+     */
+    public open fun remove(key: Long): Unit
+
+    /**
+     * Remove an existing key from the array map only if it is currently mapped to [value].
+     *
+     * @param key The key of the mapping to remove.
+     * @param value The value expected to be mapped to the key.
+     * @return Returns `true` if the mapping was removed.
+     */
+    public open fun remove(key: Long, value: E): Boolean
+
+    /**
+     * Removes the mapping at the specified [index].
+     */
+    public open fun removeAt(index: Int): Unit
+
+    /**
+     * Replace the mapping for [key] only if it is already mapped to a value.
+     *
+     * @param key The key of the mapping to replace.
+     * @param value The value to store for the given key.
+     * @return Returns the previous mapped value or `null`.
+     */
+    public open fun replace(key: Long, value: E): E?
+
+    /**
+     * Replace the mapping for [key] only if it is already mapped to a value.
+     *
+     * @param key The key of the mapping to replace.
+     * @param oldValue The value expected to be mapped to the key.
+     * @param newValue The value to store for the given key.
+     * @return Returns `true` if the value was replaced.
+     */
+    public open fun replace(key: Long, oldValue: E, newValue: E): Boolean
+
+    /**
+     * Adds a mapping from the specified key to the specified value, replacing the previous mapping
+     * from the specified key if there was one.
+     */
+    public open fun put(key: Long, value: E): Unit
+
+    /**
+     * Copies all of the mappings from [other] to this map. The effect of this call is equivalent to
+     * that of calling [put] on this map once for each mapping from key to value in [other].
+     */
+    public open fun putAll(other: LongSparseArray<out E>): Unit
+
+    /**
+     * Add a new value to the array map only if the key does not already have a value or it is
+     * mapped to `null`.
+     *
+     * @param key The key under which to store the value.
+     * @param value The value to store for the given key.
+     * @return Returns the value that was stored for the given key, or `null` if there was no such
+     * key.
+     */
+    public open fun putIfAbsent(key: Long, value: E): E?
+
+    /**
+     * Returns the number of key-value mappings that this [LongSparseArray] currently stores.
+     */
+    public open fun size(): Int
+
+    /**
+     * Return `true` if [size] is 0.
+     *
+     * @return `true` if [size] is 0.
+     */
+    public open fun isEmpty(): Boolean
+
+    /**
+     * Given an index in the range `0...size()-1`, returns the key from the `index`th key-value
+     * mapping that this [LongSparseArray] stores.
+     *
+     * The keys corresponding to indices in ascending order are guaranteed to be in ascending order,
+     * e.g., `keyAt(0)` will return the smallest key and `keyAt(size()-1)` will return the largest
+     * key.
+     *
+     * @throws IllegalArgumentException if [index] is not in the range `0...size()-1`
+     */
+    public open fun keyAt(index: Int): Long
+
+    /**
+     * Given an index in the range `0...size()-1`, returns the value from the `index`th key-value
+     * mapping that this [LongSparseArray] stores.
+     *
+     * The values corresponding to indices in ascending order are guaranteed to be associated with
+     * keys in ascending order, e.g., `valueAt(0)` will return the value associated with the
+     * smallest key and `valueAt(size()-1)` will return the value associated with the largest key.
+     *
+     * @throws IllegalArgumentException if [index] is not in the range `0...size()-1`
+     */
+    public open fun valueAt(index: Int): E
+
+    /**
+     * Given an index in the range `0...size()-1`, sets a new value for the `index`th key-value
+     * mapping that this [LongSparseArray] stores.
+     *
+     * @throws IllegalArgumentException if [index] is not in the range `0...size()-1`
+     */
+    public open fun setValueAt(index: Int, value: E): Unit
+
+    /**
+     * Returns the index for which [keyAt] would return the
+     * specified key, or a negative number if the specified
+     * key is not mapped.
+     */
+    public open fun indexOfKey(key: Long): Int
+
+    /**
+     * Returns an index for which [valueAt] would return the specified key, or a negative number if
+     * no keys map to the specified [value].
+     *
+     * Beware that this is a linear search, unlike lookups by key, and that multiple keys can map to
+     * the same value and this will find only one of them.
+     */
+    public open fun indexOfValue(value: E): Int
+
+    /** Returns `true` if the specified [key] is mapped. */
+    public open fun containsKey(key: Long): Boolean
+
+    /** Returns `true` if the specified [value] is mapped from any key. */
+    public open fun containsValue(value: E): Boolean
+
+    /**
+     * Removes all key-value mappings from this [LongSparseArray].
+     */
+    public open fun clear(): Unit
+
+    /**
+     * Puts a key/value pair into the array, optimizing for the case where the key is greater than
+     * all existing keys in the array.
+     */
+    public open fun append(key: Long, value: E): Unit
+
+    /**
+     * Returns a string representation of the object.
+     *
+     * This implementation composes a string by iterating over its mappings. If this map contains
+     * itself as a value, the string "(this Map)" will appear in its place.
+     */
+    override fun toString(): String
+}
+
+// TODO(KT-20427): Move these into the expect once support is added for default implementations.
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonGet(key: Long): E? {
+    return commonGetInternal(key, null)
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonGet(key: Long, defaultValue: E): E {
+    return commonGetInternal(key, defaultValue)
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <T : E?, E> LongSparseArray<E>.commonGetInternal(
+    key: Long,
+    defaultValue: T
+): T {
+    val i = binarySearch(keys, size, key)
+    return if (i < 0 || values[i] === DELETED) {
+        defaultValue
+    } else {
+        @Suppress("UNCHECKED_CAST")
+        values[i] as T
+    }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonRemove(key: Long) {
+    val i = binarySearch(keys, size, key)
+    if (i >= 0) {
+        if (values[i] !== DELETED) {
+            values[i] = DELETED
+            garbage = true
+        }
+    }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonRemove(key: Long, value: E): Boolean {
+    val index = indexOfKey(key)
+    if (index >= 0) {
+        val mapValue = valueAt(index)
+        if (value == mapValue) {
+            removeAt(index)
+            return true
+        }
+    }
+    return false
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonRemoveAt(index: Int) {
+    if (values[index] !== DELETED) {
+        values[index] = DELETED
+        garbage = true
+    }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonReplace(key: Long, value: E): E? {
+    val index = indexOfKey(key)
+    if (index >= 0) {
+        @Suppress("UNCHECKED_CAST")
+        val oldValue = values[index] as E?
+        values[index] = value
+        return oldValue
+    }
+    return null
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonReplace(
+    key: Long,
+    oldValue: E,
+    newValue: E
+): Boolean {
+    val index = indexOfKey(key)
+    if (index >= 0) {
+        val mapValue = values[index]
+        if (mapValue == oldValue) {
+            values[index] = newValue
+            return true
+        }
+    }
+    return false
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonGc() {
+    val n = size
+    var newSize = 0
+    val keys = keys
+    val values = values
+    for (i in 0 until n) {
+        val value = values[i]
+        if (value !== DELETED) {
+            if (i != newSize) {
+                keys[newSize] = keys[i]
+                values[newSize] = value
+                values[i] = null
+            }
+            newSize++
+        }
+    }
+    garbage = false
+    size = newSize
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonPut(key: Long, value: E) {
+    var index = binarySearch(keys, size, key)
+    if (index >= 0) {
+        values[index] = value
+    } else {
+        index = index.inv()
+        if (index < size && values[index] === DELETED) {
+            keys[index] = key
+            values[index] = value
+            return
+        }
+        if (garbage && size >= keys.size) {
+            commonGc()
+
+            // Search again because indices may have changed.
+            index = binarySearch(keys, size, key).inv()
+        }
+        if (size >= keys.size) {
+            val newSize = idealLongArraySize(size + 1)
+            keys = keys.copyOf(newSize)
+            values = values.copyOf(newSize)
+        }
+        if (size - index != 0) {
+            keys.copyInto(
+                keys,
+                destinationOffset = index + 1,
+                startIndex = index,
+                endIndex = size
+            )
+            values.copyInto(
+                values,
+                destinationOffset = index + 1,
+                startIndex = index,
+                endIndex = size
+            )
+        }
+        keys[index] = key
+        values[index] = value
+        size++
+    }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonPutAll(other: LongSparseArray<out E>) {
+    val size = other.size()
+    repeat(size) { i ->
+        put(other.keyAt(i), other.valueAt(i))
+    }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonPutIfAbsent(key: Long, value: E): E? {
+    val mapValue = get(key)
+    if (mapValue == null) {
+        put(key, value)
+    }
+    return mapValue
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonSize(): Int {
+    if (garbage) {
+        commonGc()
+    }
+    return size
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonIsEmpty(): Boolean = size() == 0
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonKeyAt(index: Int): Long {
+    require(index in 0 until size) {
+        "Expected index to be within 0..size()-1, but was $index"
+    }
+
+    if (garbage) {
+        commonGc()
+    }
+    return keys[index]
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonValueAt(index: Int): E {
+    require(index in 0 until size) {
+        "Expected index to be within 0..size()-1, but was $index"
+    }
+
+    if (garbage) {
+        commonGc()
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    return values[index] as E
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonSetValueAt(index: Int, value: E) {
+    require(index in 0 until size) {
+        "Expected index to be within 0..size()-1, but was $index"
+    }
+
+    if (garbage) {
+        commonGc()
+    }
+    values[index] = value
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonIndexOfKey(key: Long): Int {
+    if (garbage) {
+        commonGc()
+    }
+    return binarySearch(keys, size, key)
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonIndexOfValue(value: E): Int {
+    if (garbage) {
+        commonGc()
+    }
+    repeat(size) { i ->
+        if (values[i] === value) {
+            return i
+        }
+    }
+    return -1
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonContainsKey(key: Long): Boolean {
+    return indexOfKey(key) >= 0
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonContainsValue(value: E): Boolean {
+    return indexOfValue(value) >= 0
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonClear() {
+    val n = size
+    val values = values
+    for (i in 0 until n) {
+        values[i] = null
+    }
+    size = 0
+    garbage = false
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonAppend(key: Long, value: E) {
+    if (size != 0 && key <= keys[size - 1]) {
+        put(key, value)
+        return
+    }
+    if (garbage && size >= keys.size) {
+        commonGc()
+    }
+    val pos = size
+    if (pos >= keys.size) {
+        val newSize = idealLongArraySize(pos + 1)
+        keys = keys.copyOf(newSize)
+        values = values.copyOf(newSize)
+    }
+    keys[pos] = key
+    values[pos] = value
+    size = pos + 1
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> LongSparseArray<E>.commonToString(): String {
+    if (size() <= 0) {
+        return "{}"
+    }
+    return buildString(size * 28) {
+        append('{')
+        for (i in 0 until size) {
+            if (i > 0) {
+                append(", ")
+            }
+            val key = keyAt(i)
+            append(key)
+            append('=')
+            val value: Any? = valueAt(i)
+            if (value !== this) {
+                append(value)
+            } else {
+                append("(this Map)")
+            }
+        }
+        append('}')
+    }
+}
+
+/** Returns the number of key/value pairs in the collection. */
+@Suppress("NOTHING_TO_INLINE")
+public inline val <T> LongSparseArray<T>.size: Int get() = size()
+
+/** Returns true if the collection contains [key]. */
+@Suppress("NOTHING_TO_INLINE")
+public inline operator fun <T> LongSparseArray<T>.contains(key: Long): Boolean = containsKey(key)
+
+/** Allows the use of the index operator for storing values in the collection. */
+@Suppress("NOTHING_TO_INLINE")
+public inline operator fun <T> LongSparseArray<T>.set(key: Long, value: T): Unit = put(key, value)
+
+/** Creates a new collection by adding or replacing entries from [other]. */
+public operator fun <T> LongSparseArray<T>.plus(other: LongSparseArray<T>): LongSparseArray<T> {
+    val new = LongSparseArray<T>(size() + other.size())
+    new.putAll(this)
+    new.putAll(other)
+    return new
+}
+
+/** Return the value corresponding to [key], or [defaultValue] when not present. */
+@Suppress("NOTHING_TO_INLINE")
+public inline fun <T> LongSparseArray<T>.getOrDefault(key: Long, defaultValue: T): T =
+    get(key, defaultValue)
+
+/** Return the value corresponding to [key], or from [defaultValue] when not present. */
+@Suppress("NOTHING_TO_INLINE")
+public inline fun <T> LongSparseArray<T>.getOrElse(key: Long, defaultValue: () -> T): T =
+    get(key) ?: defaultValue()
+
+/** Return true when the collection contains elements. */
+@Suppress("NOTHING_TO_INLINE")
+public inline fun <T> LongSparseArray<T>.isNotEmpty(): Boolean = !isEmpty()
+
+/** Removes the entry for [key] only if it is mapped to [value]. */
+@Suppress("EXTENSION_SHADOWED_BY_MEMBER") // Binary API compatibility.
+@Deprecated(
+    message = "Replaced with member function. Remove extension import!",
+    level = HIDDEN
+)
+public fun <T> LongSparseArray<T>.remove(key: Long, value: T): Boolean = remove(key, value)
+
+/** Performs the given [action] for each key/value entry. */
+@Suppress("NOTHING_TO_INLINE")
+public inline fun <T> LongSparseArray<T>.forEach(action: (key: Long, value: T) -> Unit) {
+    for (index in 0 until size()) {
+        action(keyAt(index), valueAt(index))
+    }
+}
+
+/** Return an iterator over the collection's keys. */
+public fun <T> LongSparseArray<T>.keyIterator(): LongIterator = object : LongIterator() {
+    var index = 0
+    override fun hasNext() = index < size()
+    override fun nextLong() = keyAt(index++)
+}
+
+/** Return an iterator over the collection's values. */
+public fun <T> LongSparseArray<T>.valueIterator(): Iterator<T> = object : Iterator<T> {
+    var index = 0
+    override fun hasNext() = index < size()
+    override fun next() = valueAt(index++)
+}
diff --git a/collection/collection/src/jvmTest/kotlin/androidx/collection/LongSparseArrayTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongSparseArrayTest.kt
similarity index 91%
rename from collection/collection/src/jvmTest/kotlin/androidx/collection/LongSparseArrayTest.kt
rename to collection/collection/src/commonTest/kotlin/androidx/collection/LongSparseArrayTest.kt
index 8ee9605..f12e234 100644
--- a/collection/collection/src/jvmTest/kotlin/androidx/collection/LongSparseArrayTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongSparseArrayTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018 The Android Open Source Project
+ * Copyright 2022 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,16 +15,12 @@
  */
 package androidx.collection
 
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertNotSame
-import org.junit.Assert.assertNull
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
 
-@RunWith(JUnit4::class)
 internal class LongSparseArrayTest {
     @Test
     fun getOrDefaultPrefersStoredValue() {
@@ -270,17 +266,4 @@
         assertEquals(1L, dest[1L])
         assertEquals("two", dest[2L])
     }
-
-    @Test
-    fun cloning() {
-        val source = LongSparseArray<String>()
-        source.put(10L, "hello")
-        source.put(20L, "world")
-        val dest = source.clone()
-        assertNotSame(source, dest)
-        repeat(source.size()) { i ->
-            assertEquals(source.keyAt(i), dest.keyAt(i))
-            assertEquals(source.valueAt(i), dest.valueAt(i))
-        }
-    }
 }
\ No newline at end of file
diff --git a/collection/collection/src/jvmMain/kotlin/androidx/collection/LongSparseArray.jvm.kt b/collection/collection/src/jvmMain/kotlin/androidx/collection/LongSparseArray.jvm.kt
new file mode 100644
index 0000000..a3c602f
--- /dev/null
+++ b/collection/collection/src/jvmMain/kotlin/androidx/collection/LongSparseArray.jvm.kt
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_LONGS
+import androidx.collection.internal.EMPTY_OBJECTS
+import androidx.collection.internal.idealLongArraySize
+
+/**
+ * SparseArray mapping longs to Objects. Unlike a normal array of Objects, there can be gaps in the
+ * indices. It is intended to be more memory efficient than using a HashMap to map Longs to Objects,
+ * both because it avoids auto-boxing keys and its data structure doesn't rely on an extra entry
+ * object for each mapping.
+ *
+ * Note that this container keeps its mappings in an array data structure, using a binary search to
+ * find keys. The implementation is not intended to be appropriate for data structures that may
+ * contain large numbers of items. It is generally slower than a traditional HashMap, since lookups
+ * require a binary search and adds and removes require inserting and deleting entries in the array.
+ * For containers holding up to hundreds of items, the performance difference is not significant,
+ * less than 50%.
+ *
+ * To help with performance, the container includes an optimization when removing keys: instead of
+ * compacting its array immediately, it leaves the removed entry marked as deleted. The entry can
+ * then be re-used for the same key, or compacted later in a single garbage collection step of all
+ * removed entries. This garbage collection will need to be performed at any time the array needs to
+ * be grown or the map size or entry values are retrieved.
+ *
+ * It is possible to iterate over the items in this container using [keyAt] and [valueAt]. Iterating
+ * over the keys using [keyAt] with ascending values of the index will return the keys in ascending
+ * order, or the values corresponding to the keys in ascending order in the case of [valueAt].
+ *
+ * @constructor Creates a new [LongSparseArray] containing no mappings that will not require any
+ * additional memory allocation to store the specified number of mappings. If you supply an initial
+ * capacity of 0, the sparse array will be initialized with a light-weight representation not
+ * requiring any additional array allocations.
+ */
+public actual open class LongSparseArray<E>
+
+// TODO(b/237405792): Default value for optional argument is required here to workaround Metalava's
+//  lack of support for expect / actual.
+@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
+// TODO(b/237405286): @JvmOverloads is redundant in this actual, but is necessary here to workaround
+//  Metalava's lack of support for expect / actual.
+@JvmOverloads public actual constructor(initialCapacity: Int = 10) : Cloneable {
+    @JvmSynthetic // Hide from Java callers.
+    @JvmField
+    internal actual var garbage = false
+
+    @JvmSynthetic // Hide from Java callers.
+    @JvmField
+    internal actual var keys: LongArray
+
+    @JvmSynthetic // Hide from Java callers.
+    @JvmField
+    internal actual var values: Array<Any?>
+
+    @JvmSynthetic // Hide from Java callers.
+    @JvmField
+    internal actual var size = 0
+
+    init {
+        if (initialCapacity == 0) {
+            keys = EMPTY_LONGS
+            values = EMPTY_OBJECTS
+        } else {
+            val idealCapacity = idealLongArraySize(initialCapacity)
+            keys = LongArray(idealCapacity)
+            values = arrayOfNulls(idealCapacity)
+        }
+    }
+
+    public override fun clone(): LongSparseArray<E> {
+        @Suppress("UNCHECKED_CAST")
+        val clone: LongSparseArray<E> = super.clone() as LongSparseArray<E>
+        clone.keys = keys.clone()
+        clone.values = values.clone()
+        return clone
+    }
+
+    /**
+     * Gets the value mapped from the specified [key], or `null` if no such mapping has been made.
+     */
+    public actual open operator fun get(key: Long): E? = commonGet(key)
+
+    /**
+     * Gets the value mapped from the specified [key], or [defaultValue] if no such mapping has been
+     * made.
+     */
+    @Suppress("KotlinOperator") // Avoid confusion with matrix access syntax.
+    public actual open fun get(key: Long, defaultValue: E): E = commonGet(key, defaultValue)
+
+    /**
+     * Removes the mapping from the specified [key], if there was any.
+     */
+    @Deprecated("Alias for `remove(key)`.", ReplaceWith("remove(key)"))
+    public actual open fun delete(key: Long): Unit = commonRemove(key)
+
+    /**
+     * Removes the mapping from the specified [key], if there was any.
+     */
+    public actual open fun remove(key: Long): Unit = commonRemove(key)
+
+    /**
+     * Remove an existing key from the array map only if it is currently mapped to [value].
+     *
+     * @param key The key of the mapping to remove.
+     * @param value The value expected to be mapped to the key.
+     * @return Returns true if the mapping was removed.
+     */
+    public actual open fun remove(key: Long, value: E): Boolean = commonRemove(key, value)
+
+    /**
+     * Removes the mapping at the specified index.
+     */
+    public actual open fun removeAt(index: Int): Unit = commonRemoveAt(index)
+
+    /**
+     * Replace the mapping for [key] only if it is already mapped to a value.
+     *
+     * @param key The key of the mapping to replace.
+     * @param value The value to store for the given key.
+     * @return Returns the previous mapped value or `null`.
+     */
+    public actual open fun replace(key: Long, value: E): E? = commonReplace(key, value)
+
+    /**
+     * Replace the mapping for [key] only if it is already mapped to a value.
+     *
+     * @param key The key of the mapping to replace.
+     * @param oldValue The value expected to be mapped to the key.
+     * @param newValue The value to store for the given key.
+     * @return Returns `true` if the value was replaced.
+     */
+    public actual open fun replace(key: Long, oldValue: E, newValue: E): Boolean =
+        commonReplace(key, oldValue, newValue)
+
+    /**
+     * Adds a mapping from the specified key to the specified value, replacing the previous mapping
+     * from the specified key if there was one.
+     */
+    public actual open fun put(key: Long, value: E): Unit = commonPut(key, value)
+
+    /**
+     * Copies all of the mappings from [other] to this map. The effect of this call is equivalent to
+     * that of calling [put] on this map once for each mapping from key to value in [other].
+     */
+    public actual open fun putAll(other: LongSparseArray<out E>): Unit = commonPutAll(other)
+
+    /**
+     * Add a new value to the array map only if the key does not already have a value or it is
+     * mapped to `null`.
+     *
+     * @param key The key under which to store the value.
+     * @param value The value to store for the given key.
+     * @return Returns the value that was stored for the given key, or `null` if there was no such
+     * key.
+     */
+    public actual open fun putIfAbsent(key: Long, value: E): E? = commonPutIfAbsent(key, value)
+
+    /**
+     * Returns the number of key-value mappings that this [LongSparseArray] currently stores.
+     */
+    public actual open fun size(): Int = commonSize()
+
+    /**
+     * Return `true` if [size] is 0.
+     *
+     * @return `true` if [size] is 0.
+     */
+    public actual open fun isEmpty(): Boolean = commonIsEmpty()
+
+    /**
+     * Given an index in the range `0...size()-1`, returns the key from the `index`th key-value
+     * mapping that this [LongSparseArray] stores.
+     *
+     * The keys corresponding to indices in ascending order are guaranteed to be in ascending order,
+     * e.g., `keyAt(0)` will return the smallest key and `keyAt(size()-1)` will return the largest
+     * key.
+     *
+     * @throws IllegalArgumentException if [index] is not in the range `0...size()-1`
+     */
+    public actual open fun keyAt(index: Int): Long = commonKeyAt(index)
+
+    /**
+     * Given an index in the range `0...size()-1`, returns the value from the `index`th key-value
+     * mapping that this [LongSparseArray] stores.
+     *
+     * The values corresponding to indices in ascending order are guaranteed to be associated with
+     * keys in ascending order, e.g., `valueAt(0)` will return the value associated with the
+     * smallest key and `valueAt(size()-1)` will return the value associated with the largest key.
+     *
+     * @throws IllegalArgumentException if [index] is not in the range `0...size()-1`
+     */
+    public actual open fun valueAt(index: Int): E = commonValueAt(index)
+
+    /**
+     * Given an index in the range `0...size()-1`, sets a new value for the `index`th key-value
+     * mapping that this [LongSparseArray] stores.
+     *
+     * @throws IllegalArgumentException if [index] is not in the range `0...size()-1`
+     */
+    public actual open fun setValueAt(index: Int, value: E): Unit = commonSetValueAt(index, value)
+
+    /**
+     * Returns the index for which [keyAt] would return the
+     * specified key, or a negative number if the specified
+     * key is not mapped.
+     */
+    public actual open fun indexOfKey(key: Long): Int = commonIndexOfKey(key)
+
+    /**
+     * Returns an index for which [valueAt] would return the specified key, or a negative number if
+     * no keys map to the specified [value].
+     *
+     * Beware that this is a linear search, unlike lookups by key, and that multiple keys can map to
+     * the same value and this will find only one of them.
+     */
+    public actual open fun indexOfValue(value: E): Int = commonIndexOfValue(value)
+
+    /** Returns `true` if the specified [key] is mapped. */
+    public actual open fun containsKey(key: Long): Boolean = commonContainsKey(key)
+
+    /** Returns `true` if the specified [value] is mapped from any key. */
+    public actual open fun containsValue(value: E): Boolean = commonContainsValue(value)
+
+    /**
+     * Removes all key-value mappings from this [LongSparseArray].
+     */
+    public actual open fun clear(): Unit = commonClear()
+
+    /**
+     * Puts a key/value pair into the array, optimizing for the case where the key is greater than
+     * all existing keys in the array.
+     */
+    public actual open fun append(key: Long, value: E): Unit = commonAppend(key, value)
+
+    /**
+     * Returns a string representation of the object.
+     *
+     * This implementation composes a string by iterating over its mappings. If this map contains
+     * itself as a value, the string "(this Map)" will appear in its place.
+     */
+    actual override fun toString(): String = commonToString()
+}
\ No newline at end of file
diff --git a/collection/collection/src/jvmMain/kotlin/androidx/collection/LongSparseArray.kt b/collection/collection/src/jvmMain/kotlin/androidx/collection/LongSparseArray.kt
deleted file mode 100644
index c30e713..0000000
--- a/collection/collection/src/jvmMain/kotlin/androidx/collection/LongSparseArray.kt
+++ /dev/null
@@ -1,541 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.collection
-
-import androidx.collection.internal.EMPTY_LONGS
-import androidx.collection.internal.EMPTY_OBJECTS
-import androidx.collection.internal.binarySearch
-import androidx.collection.internal.idealLongArraySize
-import kotlin.DeprecationLevel.HIDDEN
-
-private val DELETED = Any()
-
-/**
- * SparseArray mapping longs to Objects. Unlike a normal array of Objects, there can be gaps in the
- * indices. It is intended to be more memory efficient than using a HashMap to map Longs to Objects,
- * both because it avoids auto-boxing keys and its data structure doesn't rely on an extra entry
- * object for each mapping.
- *
- * Note that this container keeps its mappings in an array data structure, using a binary search to
- * find keys. The implementation is not intended to be appropriate for data structures that may
- * contain large numbers of items. It is generally slower than a traditional HashMap, since lookups
- * require a binary search and adds and removes require inserting and deleting entries in the array.
- * For containers holding up to hundreds of items, the performance difference is not significant,
- * less than 50%.
- *
- * To help with performance, the container includes an optimization when removing keys: instead of
- * compacting its array immediately, it leaves the removed entry marked as deleted. The entry can
- * then be re-used for the same key, or compacted later in a single garbage collection step of all
- * removed entries. This garbage collection will need to be performed at any time the array needs to
- * be grown or the map size or entry values are retrieved.
- *
- * It is possible to iterate over the items in this container using [keyAt] and [valueAt]. Iterating
- * over the keys using [keyAt] with ascending values of the index will return the keys in ascending
- * order, or the values corresponding to the keys in ascending order in the case of [valueAt].
- *
- * @constructor Creates a new [LongSparseArray] containing no mappings that will not require any
- * additional memory allocation to store the specified number of mappings. If you supply an initial
- * capacity of 0, the sparse array will be initialized with a light-weight representation not
- * requiring any additional array allocations.
- */
-public open class LongSparseArray<E>
-@JvmOverloads public constructor(initialCapacity: Int = 10) : Cloneable {
-    private var garbage = false
-    private var keys: LongArray
-    private var values: Array<Any?>
-    private var size = 0
-
-    init {
-        if (initialCapacity == 0) {
-            keys = EMPTY_LONGS
-            values = EMPTY_OBJECTS
-        } else {
-            val idealCapacity = idealLongArraySize(initialCapacity)
-            keys = LongArray(idealCapacity)
-            values = arrayOfNulls(idealCapacity)
-        }
-    }
-
-    public override fun clone(): LongSparseArray<E> {
-        @Suppress("UNCHECKED_CAST")
-        val clone: LongSparseArray<E> = super.clone() as LongSparseArray<E>
-        clone.keys = keys.clone()
-        clone.values = values.clone()
-        return clone
-    }
-
-    /**
-     * Gets the value mapped from the specified [key], or `null` if no such mapping has been made.
-     */
-    public open operator fun get(key: Long): E? {
-        return getInternal(key, null)
-    }
-
-    /**
-     * Gets the value mapped from the specified [key], or [defaultValue] if no such mapping has been
-     * made.
-     */
-    @Suppress("KotlinOperator") // Avoid confusion with matrix access syntax.
-    public open fun get(key: Long, defaultValue: E): E {
-        return getInternal(key, defaultValue)
-    }
-
-    @Suppress("NOTHING_TO_INLINE")
-    private inline fun <T : E?> getInternal(key: Long, defaultValue: T): T {
-        val i = binarySearch(keys, size, key)
-        return if (i < 0 || values[i] === DELETED) {
-            defaultValue
-        } else {
-            @Suppress("UNCHECKED_CAST")
-            values[i] as T
-        }
-    }
-
-    /**
-     * Removes the mapping from the specified [key], if there was any.
-     */
-    @Deprecated("Alias for `remove(key)`.", ReplaceWith("remove(key)"))
-    public open fun delete(key: Long) {
-        remove(key)
-    }
-
-    /**
-     * Removes the mapping from the specified [key], if there was any.
-     */
-    public open fun remove(key: Long) {
-        val i = binarySearch(keys, size, key)
-        if (i >= 0) {
-            if (values[i] !== DELETED) {
-                values[i] = DELETED
-                garbage = true
-            }
-        }
-    }
-
-    /**
-     * Remove an existing key from the array map only if it is currently mapped to [value].
-     *
-     * @param key The key of the mapping to remove.
-     * @param value The value expected to be mapped to the key.
-     * @return Returns true if the mapping was removed.
-     */
-    public open fun remove(key: Long, value: E): Boolean {
-        val index = indexOfKey(key)
-        if (index >= 0) {
-            val mapValue = valueAt(index)
-            if (value == mapValue) {
-                removeAt(index)
-                return true
-            }
-        }
-        return false
-    }
-
-    /**
-     * Removes the mapping at the specified index.
-     */
-    public open fun removeAt(index: Int) {
-        if (values[index] !== DELETED) {
-            values[index] = DELETED
-            garbage = true
-        }
-    }
-
-    /**
-     * Replace the mapping for [key] only if it is already mapped to a value.
-     *
-     * @param key The key of the mapping to replace.
-     * @param value The value to store for the given key.
-     * @return Returns the previous mapped value or `null`.
-     */
-    public open fun replace(key: Long, value: E): E? {
-        val index = indexOfKey(key)
-        if (index >= 0) {
-            @Suppress("UNCHECKED_CAST")
-            val oldValue = values[index] as E?
-            values[index] = value
-            return oldValue
-        }
-        return null
-    }
-
-    /**
-     * Replace the mapping for [key] only if it is already mapped to a value.
-     *
-     * @param key The key of the mapping to replace.
-     * @param oldValue The value expected to be mapped to the key.
-     * @param newValue The value to store for the given key.
-     * @return Returns `true` if the value was replaced.
-     */
-    public open fun replace(key: Long, oldValue: E, newValue: E): Boolean {
-        val index = indexOfKey(key)
-        if (index >= 0) {
-            val mapValue = values[index]
-            if (mapValue == oldValue) {
-                values[index] = newValue
-                return true
-            }
-        }
-        return false
-    }
-
-    private fun gc() {
-        // Log.e("SparseArray", "gc start with " + mSize);
-        val n = size
-        var newSize = 0
-        val keys = keys
-        val values = values
-        for (i in 0 until n) {
-            val value = values[i]
-            if (value !== DELETED) {
-                if (i != newSize) {
-                    keys[newSize] = keys[i]
-                    values[newSize] = value
-                    values[i] = null
-                }
-                newSize++
-            }
-        }
-        garbage = false
-        size = newSize
-
-        // Log.e("SparseArray", "gc end with " + mSize);
-    }
-
-    /**
-     * Adds a mapping from the specified key to the specified value, replacing the previous mapping
-     * from the specified key if there was one.
-     */
-    public open fun put(key: Long, value: E) {
-        var i = binarySearch(keys, size, key)
-        if (i >= 0) {
-            values[i] = value
-        } else {
-            i = i.inv()
-            if (i < size && values[i] === DELETED) {
-                keys[i] = key
-                values[i] = value
-                return
-            }
-            if (garbage && size >= keys.size) {
-                gc()
-
-                // Search again because indices may have changed.
-                i = binarySearch(keys, size, key).inv()
-            }
-            if (size >= keys.size) {
-                val n = idealLongArraySize(size + 1)
-                val nkeys = LongArray(n)
-                val nvalues = arrayOfNulls<Any>(n)
-
-                // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
-                System.arraycopy(keys, 0, nkeys, 0, keys.size)
-                System.arraycopy(values, 0, nvalues, 0, values.size)
-                keys = nkeys
-                values = nvalues
-            }
-            if (size - i != 0) {
-                // Log.e("SparseArray", "move " + (mSize - i));
-                System.arraycopy(keys, i, keys, i + 1, size - i)
-                System.arraycopy(values, i, values, i + 1, size - i)
-            }
-            keys[i] = key
-            values[i] = value
-            size++
-        }
-    }
-
-    /**
-     * Copies all of the mappings from [other] to this map. The effect of this call is equivalent to
-     * that of calling [put] on this map once for each mapping from key to value in [other].
-     */
-    public open fun putAll(other: LongSparseArray<out E>) {
-        val size = other.size()
-        repeat(size) { i ->
-            put(other.keyAt(i), other.valueAt(i))
-        }
-    }
-
-    /**
-     * Add a new value to the array map only if the key does not already have a value or it is
-     * mapped to `null`.
-     *
-     * @param key The key under which to store the value.
-     * @param value The value to store for the given key.
-     * @return Returns the value that was stored for the given key, or `null` if there was no such
-     * key.
-     */
-    public open fun putIfAbsent(key: Long, value: E): E? {
-        val mapValue = get(key)
-        if (mapValue == null) {
-            put(key, value)
-        }
-        return mapValue
-    }
-
-    /**
-     * Returns the number of key-value mappings that this [LongSparseArray] currently stores.
-     */
-    public open fun size(): Int {
-        if (garbage) {
-            gc()
-        }
-        return size
-    }
-
-    /**
-     * Return `true` if [size] is 0.
-     *
-     * @return `true` if [size] is 0.
-     */
-    public open fun isEmpty(): Boolean = size() == 0
-
-    /**
-     * Given an index in the range `0...size()-1`, returns the key from the `index`th key-value
-     * mapping that this [LongSparseArray] stores.
-     *
-     * The keys corresponding to indices in ascending order are guaranteed to be in ascending order,
-     * e.g., `keyAt(0)` will return the smallest key and `keyAt(size()-1)` will return the largest
-     * key.
-     *
-     * @throws IllegalArgumentException if [index] is not in the range `0...size()-1`
-     */
-    public open fun keyAt(index: Int): Long {
-        require(index in 0 until size) {
-            "Expected index to be within 0..size()-1, but was $index"
-        }
-
-        if (garbage) {
-            gc()
-        }
-        return keys[index]
-    }
-
-    /**
-     * Given an index in the range `0...size()-1`, returns the value from the `index`th key-value
-     * mapping that this [LongSparseArray] stores.
-     *
-     * The values corresponding to indices in ascending order are guaranteed to be associated with
-     * keys in ascending order, e.g., `valueAt(0)` will return the value associated with the
-     * smallest key and `valueAt(size()-1)` will return the value associated with the largest key.
-     *
-     * @throws IllegalArgumentException if [index] is not in the range `0...size()-1`
-     */
-    public open fun valueAt(index: Int): E {
-        require(index in 0 until size) {
-            "Expected index to be within 0..size()-1, but was $index"
-        }
-
-        if (garbage) {
-            gc()
-        }
-
-        @Suppress("UNCHECKED_CAST")
-        return values[index] as E
-    }
-
-    /**
-     * Given an index in the range `0...size()-1`, sets a new value for the `index`th key-value
-     * mapping that this [LongSparseArray] stores.
-     *
-     * @throws IllegalArgumentException if [index] is not in the range `0...size()-1`
-     */
-    public open fun setValueAt(index: Int, value: E) {
-        require(index in 0 until size) {
-            "Expected index to be within 0..size()-1, but was $index"
-        }
-
-        if (garbage) {
-            gc()
-        }
-        values[index] = value
-    }
-
-    /**
-     * Returns the index for which [keyAt] would return the
-     * specified key, or a negative number if the specified
-     * key is not mapped.
-     */
-    public open fun indexOfKey(key: Long): Int {
-        if (garbage) {
-            gc()
-        }
-        return binarySearch(keys, size, key)
-    }
-
-    /**
-     * Returns an index for which [valueAt] would return the specified key, or a negative number if
-     * no keys map to the specified [value].
-     *
-     * Beware that this is a linear search, unlike lookups by key, and that multiple keys can map to
-     * the same value and this will find only one of them.
-     */
-    public open fun indexOfValue(value: E): Int {
-        if (garbage) {
-            gc()
-        }
-        repeat(size) { i ->
-            if (values[i] === value) {
-                return i
-            }
-        }
-        return -1
-    }
-
-    /** Returns `true` if the specified [key] is mapped. */
-    public open fun containsKey(key: Long): Boolean {
-        return indexOfKey(key) >= 0
-    }
-
-    /** Returns `true` if the specified [value] is mapped from any key. */
-    public open fun containsValue(value: E): Boolean {
-        return indexOfValue(value) >= 0
-    }
-
-    /**
-     * Removes all key-value mappings from this [LongSparseArray].
-     */
-    public open fun clear() {
-        val n = size
-        val values = values
-        for (i in 0 until n) {
-            values[i] = null
-        }
-        size = 0
-        garbage = false
-    }
-
-    /**
-     * Puts a key/value pair into the array, optimizing for the case where the key is greater than
-     * all existing keys in the array.
-     */
-    public open fun append(key: Long, value: E) {
-        if (size != 0 && key <= keys[size - 1]) {
-            put(key, value)
-            return
-        }
-        if (garbage && size >= keys.size) {
-            gc()
-        }
-        val pos = size
-        if (pos >= keys.size) {
-            val n = idealLongArraySize(pos + 1)
-            val nkeys = LongArray(n)
-            val nvalues = arrayOfNulls<Any>(n)
-
-            // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
-            System.arraycopy(keys, 0, nkeys, 0, keys.size)
-            System.arraycopy(values, 0, nvalues, 0, values.size)
-            keys = nkeys
-            values = nvalues
-        }
-        keys[pos] = key
-        values[pos] = value
-        size = pos + 1
-    }
-
-    /**
-     * Returns a string representation of the object.
-     *
-     * This implementation composes a string by iterating over its mappings. If this map contains
-     * itself as a value, the string "(this Map)" will appear in its place.
-     */
-    override fun toString(): String {
-        if (size() <= 0) {
-            return "{}"
-        }
-        return buildString(size * 28) {
-            append('{')
-            for (i in 0 until size) {
-                if (i > 0) {
-                    append(", ")
-                }
-                val key = keyAt(i)
-                append(key)
-                append('=')
-                val value: Any? = valueAt(i)
-                if (value !== this) {
-                    append(value)
-                } else {
-                    append("(this Map)")
-                }
-            }
-            append('}')
-        }
-    }
-}
-
-/** Returns the number of key/value pairs in the collection. */
-public inline val <T> LongSparseArray<T>.size: Int get() = size()
-
-/** Returns true if the collection contains [key]. */
-@Suppress("NOTHING_TO_INLINE")
-public inline operator fun <T> LongSparseArray<T>.contains(key: Long): Boolean = containsKey(key)
-
-/** Allows the use of the index operator for storing values in the collection. */
-@Suppress("NOTHING_TO_INLINE")
-public inline operator fun <T> LongSparseArray<T>.set(key: Long, value: T): Unit = put(key, value)
-
-/** Creates a new collection by adding or replacing entries from [other]. */
-public operator fun <T> LongSparseArray<T>.plus(other: LongSparseArray<T>): LongSparseArray<T> {
-    val new = LongSparseArray<T>(size() + other.size())
-    new.putAll(this)
-    new.putAll(other)
-    return new
-}
-
-/** Return the value corresponding to [key], or [defaultValue] when not present. */
-@Suppress("NOTHING_TO_INLINE")
-public inline fun <T> LongSparseArray<T>.getOrDefault(key: Long, defaultValue: T): T =
-    get(key, defaultValue)
-
-/** Return the value corresponding to [key], or from [defaultValue] when not present. */
-public inline fun <T> LongSparseArray<T>.getOrElse(key: Long, defaultValue: () -> T): T =
-    get(key) ?: defaultValue()
-
-/** Return true when the collection contains elements. */
-@Suppress("NOTHING_TO_INLINE")
-public inline fun <T> LongSparseArray<T>.isNotEmpty(): Boolean = !isEmpty()
-
-/** Removes the entry for [key] only if it is mapped to [value]. */
-@Suppress("EXTENSION_SHADOWED_BY_MEMBER") // Binary API compatibility.
-@Deprecated(
-    message = "Replaced with member function. Remove extension import!",
-    level = HIDDEN
-)
-public fun <T> LongSparseArray<T>.remove(key: Long, value: T): Boolean = remove(key, value)
-
-/** Performs the given [action] for each key/value entry. */
-public inline fun <T> LongSparseArray<T>.forEach(action: (key: Long, value: T) -> Unit) {
-    for (index in 0 until size()) {
-        action(keyAt(index), valueAt(index))
-    }
-}
-
-/** Return an iterator over the collection's keys. */
-public fun <T> LongSparseArray<T>.keyIterator(): LongIterator = object : LongIterator() {
-    var index = 0
-    override fun hasNext() = index < size()
-    override fun nextLong() = keyAt(index++)
-}
-
-/** Return an iterator over the collection's values. */
-public fun <T> LongSparseArray<T>.valueIterator(): Iterator<T> = object : Iterator<T> {
-    var index = 0
-    override fun hasNext() = index < size()
-    override fun next() = valueAt(index++)
-}
diff --git a/collection/collection/src/jvmTest/kotlin/androidx/collection/LongSparseArrayJvmTest.kt b/collection/collection/src/jvmTest/kotlin/androidx/collection/LongSparseArrayJvmTest.kt
new file mode 100644
index 0000000..df45728
--- /dev/null
+++ b/collection/collection/src/jvmTest/kotlin/androidx/collection/LongSparseArrayJvmTest.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.assertEquals
+import kotlin.test.assertNotSame
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+internal class LongSparseArrayJvmTest {
+    @Test
+    fun cloning() {
+        val source = LongSparseArray<String>()
+        source.put(10L, "hello")
+        source.put(20L, "world")
+        val dest = source.clone()
+        assertNotSame(source, dest)
+        repeat(source.size()) { i ->
+            assertEquals(source.keyAt(i), dest.keyAt(i))
+            assertEquals(source.valueAt(i), dest.valueAt(i))
+        }
+    }
+}
\ No newline at end of file
diff --git a/collection/collection/src/nativeMain/kotlin/androidx/collection/LongSparseArray.native.kt b/collection/collection/src/nativeMain/kotlin/androidx/collection/LongSparseArray.native.kt
new file mode 100644
index 0000000..1c6afd7
--- /dev/null
+++ b/collection/collection/src/nativeMain/kotlin/androidx/collection/LongSparseArray.native.kt
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_LONGS
+import androidx.collection.internal.EMPTY_OBJECTS
+import androidx.collection.internal.idealLongArraySize
+
+/**
+ * SparseArray mapping longs to Objects. Unlike a normal array of Objects, there can be gaps in the
+ * indices. It is intended to be more memory efficient than using a HashMap to map Longs to Objects,
+ * both because it avoids auto-boxing keys and its data structure doesn't rely on an extra entry
+ * object for each mapping.
+ *
+ * Note that this container keeps its mappings in an array data structure, using a binary search to
+ * find keys. The implementation is not intended to be appropriate for data structures that may
+ * contain large numbers of items. It is generally slower than a traditional HashMap, since lookups
+ * require a binary search and adds and removes require inserting and deleting entries in the array.
+ * For containers holding up to hundreds of items, the performance difference is not significant,
+ * less than 50%.
+ *
+ * To help with performance, the container includes an optimization when removing keys: instead of
+ * compacting its array immediately, it leaves the removed entry marked as deleted. The entry can
+ * then be re-used for the same key, or compacted later in a single garbage collection step of all
+ * removed entries. This garbage collection will need to be performed at any time the array needs to
+ * be grown or the map size or entry values are retrieved.
+ *
+ * It is possible to iterate over the items in this container using [keyAt] and [valueAt]. Iterating
+ * over the keys using [keyAt] with ascending values of the index will return the keys in ascending
+ * order, or the values corresponding to the keys in ascending order in the case of [valueAt].
+ *
+ * @constructor Creates a new [LongSparseArray] containing no mappings that will not require any
+ * additional memory allocation to store the specified number of mappings. If you supply an initial
+ * capacity of 0, the sparse array will be initialized with a light-weight representation not
+ * requiring any additional array allocations.
+ */
+public actual open class LongSparseArray<E>
+public actual constructor(initialCapacity: Int) {
+    internal actual var garbage = false
+    internal actual var keys: LongArray
+    internal actual var values: Array<Any?>
+    internal actual var size = 0
+
+    init {
+        if (initialCapacity == 0) {
+            keys = EMPTY_LONGS
+            values = EMPTY_OBJECTS
+        } else {
+            val idealCapacity = idealLongArraySize(initialCapacity)
+            keys = LongArray(idealCapacity)
+            values = arrayOfNulls(idealCapacity)
+        }
+    }
+
+    /**
+     * Gets the value mapped from the specified [key], or `null` if no such mapping has been made.
+     */
+    public actual open operator fun get(key: Long): E? = commonGet(key)
+
+    /**
+     * Gets the value mapped from the specified [key], or [defaultValue] if no such mapping has been
+     * made.
+     */
+    @Suppress("KotlinOperator") // Avoid confusion with matrix access syntax.
+    public actual open fun get(key: Long, defaultValue: E): E = commonGet(key, defaultValue)
+
+    /**
+     * Removes the mapping from the specified [key], if there was any.
+     */
+    @Deprecated("Alias for `remove(key)`.", ReplaceWith("remove(key)"))
+    public actual open fun delete(key: Long): Unit = commonRemove(key)
+
+    /**
+     * Removes the mapping from the specified [key], if there was any.
+     */
+    public actual open fun remove(key: Long): Unit = commonRemove(key)
+
+    /**
+     * Remove an existing key from the array map only if it is currently mapped to [value].
+     *
+     * @param key The key of the mapping to remove.
+     * @param value The value expected to be mapped to the key.
+     * @return Returns true if the mapping was removed.
+     */
+    public actual open fun remove(key: Long, value: E): Boolean = commonRemove(key, value)
+
+    /**
+     * Removes the mapping at the specified index.
+     */
+    public actual open fun removeAt(index: Int): Unit = commonRemoveAt(index)
+
+    /**
+     * Replace the mapping for [key] only if it is already mapped to a value.
+     *
+     * @param key The key of the mapping to replace.
+     * @param value The value to store for the given key.
+     * @return Returns the previous mapped value or `null`.
+     */
+    public actual open fun replace(key: Long, value: E): E? = commonReplace(key, value)
+
+    /**
+     * Replace the mapping for [key] only if it is already mapped to a value.
+     *
+     * @param key The key of the mapping to replace.
+     * @param oldValue The value expected to be mapped to the key.
+     * @param newValue The value to store for the given key.
+     * @return Returns `true` if the value was replaced.
+     */
+    public actual open fun replace(key: Long, oldValue: E, newValue: E): Boolean =
+        commonReplace(key, oldValue, newValue)
+
+    /**
+     * Adds a mapping from the specified key to the specified value, replacing the previous mapping
+     * from the specified key if there was one.
+     */
+    public actual open fun put(key: Long, value: E): Unit = commonPut(key, value)
+
+    /**
+     * Copies all of the mappings from [other] to this map. The effect of this call is equivalent to
+     * that of calling [put] on this map once for each mapping from key to value in [other].
+     */
+    public actual open fun putAll(other: LongSparseArray<out E>): Unit = commonPutAll(other)
+
+    /**
+     * Add a new value to the array map only if the key does not already have a value or it is
+     * mapped to `null`.
+     *
+     * @param key The key under which to store the value.
+     * @param value The value to store for the given key.
+     * @return Returns the value that was stored for the given key, or `null` if there was no such
+     * key.
+     */
+    public actual open fun putIfAbsent(key: Long, value: E): E? = commonPutIfAbsent(key, value)
+
+    /**
+     * Returns the number of key-value mappings that this [LongSparseArray] currently stores.
+     */
+    public actual open fun size(): Int = commonSize()
+
+    /**
+     * Return `true` if [size] is 0.
+     *
+     * @return `true` if [size] is 0.
+     */
+    public actual open fun isEmpty(): Boolean = commonIsEmpty()
+
+    /**
+     * Given an index in the range `0...size()-1`, returns the key from the `index`th key-value
+     * mapping that this [LongSparseArray] stores.
+     *
+     * The keys corresponding to indices in ascending order are guaranteed to be in ascending order,
+     * e.g., `keyAt(0)` will return the smallest key and `keyAt(size()-1)` will return the largest
+     * key.
+     *
+     * @throws IllegalArgumentException if [index] is not in the range `0...size()-1`
+     */
+    public actual open fun keyAt(index: Int): Long = commonKeyAt(index)
+
+    /**
+     * Given an index in the range `0...size()-1`, returns the value from the `index`th key-value
+     * mapping that this [LongSparseArray] stores.
+     *
+     * The values corresponding to indices in ascending order are guaranteed to be associated with
+     * keys in ascending order, e.g., `valueAt(0)` will return the value associated with the
+     * smallest key and `valueAt(size()-1)` will return the value associated with the largest key.
+     *
+     * @throws IllegalArgumentException if [index] is not in the range `0...size()-1`
+     */
+    public actual open fun valueAt(index: Int): E = commonValueAt(index)
+
+    /**
+     * Given an index in the range `0...size()-1`, sets a new value for the `index`th key-value
+     * mapping that this [LongSparseArray] stores.
+     *
+     * @throws IllegalArgumentException if [index] is not in the range `0...size()-1`
+     */
+    public actual open fun setValueAt(index: Int, value: E): Unit = commonSetValueAt(index, value)
+
+    /**
+     * Returns the index for which [keyAt] would return the
+     * specified key, or a negative number if the specified
+     * key is not mapped.
+     */
+    public actual open fun indexOfKey(key: Long): Int = commonIndexOfKey(key)
+
+    /**
+     * Returns an index for which [valueAt] would return the specified key, or a negative number if
+     * no keys map to the specified [value].
+     *
+     * Beware that this is a linear search, unlike lookups by key, and that multiple keys can map to
+     * the same value and this will find only one of them.
+     */
+    public actual open fun indexOfValue(value: E): Int = commonIndexOfValue(value)
+
+    /** Returns `true` if the specified [key] is mapped. */
+    public actual open fun containsKey(key: Long): Boolean = commonContainsKey(key)
+
+    /** Returns `true` if the specified [value] is mapped from any key. */
+    public actual open fun containsValue(value: E): Boolean = commonContainsValue(value)
+
+    /**
+     * Removes all key-value mappings from this [LongSparseArray].
+     */
+    public actual open fun clear(): Unit = commonClear()
+
+    /**
+     * Puts a key/value pair into the array, optimizing for the case where the key is greater than
+     * all existing keys in the array.
+     */
+    public actual open fun append(key: Long, value: E): Unit = commonAppend(key, value)
+
+    /**
+     * Returns a string representation of the object.
+     *
+     * This implementation composes a string by iterating over its mappings. If this map contains
+     * itself as a value, the string "(this Map)" will appear in its place.
+     */
+    actual override fun toString(): String = commonToString()
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt
index 0f25426..05baa8c 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt
@@ -1323,6 +1323,67 @@
         """
     )
 
+    @Test
+    fun testInferringTargetFromAncestorMethod() = verifyComposeIrTransform(
+        source = """
+            import androidx.compose.runtime.Composable
+            import androidx.compose.runtime.ComposableTarget
+            import androidx.compose.runtime.ComposableOpenTarget
+
+            @Composable @ComposableOpenTarget(0) fun OpenTarget() { }
+
+            abstract class Base {
+              @Composable @ComposableTarget("N") abstract fun Compose()
+            }
+
+            class Valid : Base () {
+              @Composable override fun Compose() {
+                OpenTarget()
+              }
+            }
+        """,
+        expectedTransformed = """
+            @Composable
+            @ComposableOpenTarget(index = 0)
+            fun OpenTarget(%composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>)
+              sourceInformation(%composer, "C(OpenTarget):Test.kt")
+              if (%changed !== 0 || !%composer.skipping) {
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                OpenTarget(%composer, %changed or 0b0001)
+              }
+            }
+            @StabilityInferred(parameters = 0)
+            abstract class Base {
+              @Composable
+              @ComposableTarget(applier = "N")
+              abstract fun Compose(%composer: Composer?, %changed: Int)
+              static val %stable: Int = 0
+            }
+            @StabilityInferred(parameters = 0)
+            class Valid : Base {
+              @Composable
+              override fun Compose(%composer: Composer?, %changed: Int) {
+                %composer = %composer.startRestartGroup(<>)
+                sourceInformation(%composer, "C(Compose)<OpenTa...>:Test.kt")
+                if (%changed and 0b0001 !== 0 || !%composer.skipping) {
+                  OpenTarget(%composer, 0)
+                } else {
+                  %composer.skipToGroupEnd()
+                }
+                val tmp0_rcvr = <this>
+                %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                  tmp0_rcvr.Compose(%composer, %changed or 0b0001)
+                }
+              }
+              static val %stable: Int = 0
+            }
+        """
+        )
+
     private fun verify(source: String, expected: String) =
         verifyComposeIrTransform(source, expected, baseDefinition)
 
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableTargetCheckerTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableTargetCheckerTests.kt
index 7063bcbc..d9f2f9e 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableTargetCheckerTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableTargetCheckerTests.kt
@@ -417,4 +417,73 @@
         }
         """
     )
+
+    fun testOpenOverrideAttributesInheritTarget() = check(
+        """
+        import androidx.compose.runtime.Composable
+        import androidx.compose.runtime.ComposableTarget
+
+        @Composable @ComposableTarget("N") fun N() { }
+        @Composable @ComposableTarget("M") fun M() { }
+
+        abstract class Base {
+          @Composable @ComposableTarget("N") abstract fun Compose()
+        }
+
+        class Invalid : Base() {
+          @Composable override fun Compose() {
+            <!COMPOSE_APPLIER_CALL_MISMATCH!>M<!>()
+          }
+        }
+
+        class Valid : Base () {
+          @Composable override fun Compose() {
+            N()
+          }
+        }
+        """
+    )
+
+    fun testOpenOverrideTargetsMustAgree() = check(
+        """
+        import androidx.compose.runtime.Composable
+        import androidx.compose.runtime.ComposableTarget
+
+        @Composable @ComposableTarget("N") fun N() { }
+        @Composable @ComposableTarget("M") fun M() { }
+
+        abstract class Base {
+          @Composable @ComposableTarget("N") abstract fun Compose()
+        }
+
+        class Invalid : Base() {
+          <!COMPOSE_APPLIER_DECLARATION_MISMATCH!>@Composable @ComposableTarget("M") override fun Compose() { }<!>
+        }
+
+        class Valid : Base () {
+          @Composable override fun Compose() {
+            N()
+          }
+        }
+        """
+    )
+
+    fun testOpenOverrideInferredToAgree() = check(
+        """
+        import androidx.compose.runtime.Composable
+        import androidx.compose.runtime.ComposableTarget
+
+        @Composable @ComposableTarget("N") fun N() { }
+        @Composable @ComposableTarget("M") fun M() { }
+
+        abstract class Base {
+          @Composable @ComposableTarget("N") abstract fun Compose()
+        }
+
+        class Invalid : Base() {
+          @Composable override fun Compose() {
+            N()
+          }
+        }
+        """)
 }
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableDeclarationChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableDeclarationChecker.kt
index 30952f6..6f2e72c 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableDeclarationChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableDeclarationChecker.kt
@@ -85,6 +85,10 @@
                         listOf(descriptor, override)
                     )
                 )
+            } else if (!descriptor.toScheme(null).canOverride(override.toScheme(null))) {
+                context.trace.report(
+                    ComposeErrors.COMPOSE_APPLIER_DECLARATION_MISMATCH.on(declaration)
+                )
             }
 
             descriptor.valueParameters.forEach { valueParameter ->
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableTargetChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableTargetChecker.kt
index 9dcd05c..bd9e032 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableTargetChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableTargetChecker.kt
@@ -428,13 +428,13 @@
 
 private fun Annotated.scheme(): Scheme? = compositionScheme()?.let { deserializeScheme(it) }
 
-private fun CallableDescriptor.toScheme(callContext: CallCheckerContext): Scheme =
+internal fun CallableDescriptor.toScheme(callContext: CallCheckerContext?): Scheme =
     scheme()
         ?: Scheme(
             target = schemeItem().let {
                 // The item is unspecified see if the containing has an annotation we can use
                 if (it.isUnspecified) {
-                    val target = fileScopeTarget(callContext)
+                    val target = callContext?.let { context -> fileScopeTarget(context) }
                     if (target != null) return@let target
                 }
                 it
@@ -444,15 +444,15 @@
             }.map {
                 it.samComposableOrNull()?.toScheme(callContext) ?: it.type.toScheme()
             }
-        )
+        ).mergeWith(overriddenDescriptors.map { it.toScheme(null) })
 
 private fun CallableDescriptor.fileScopeTarget(callContext: CallCheckerContext): Item? =
     (psiElement?.containingFile as? KtFile)?.let {
         for (entry in it.annotationEntries) {
             val annotationDescriptor =
                 callContext.trace.bindingContext[BindingContext.ANNOTATION, entry]
-            annotationDescriptor?.compositionTarget()?.let {
-                return Token(it)
+            annotationDescriptor?.compositionTarget()?.let { token ->
+                return Token(token)
             }
         }
         null
@@ -470,3 +470,24 @@
 
 private fun ValueParameterDescriptor.isSamComposable() =
     samComposableOrNull()?.hasComposableAnnotation() == true
+
+internal fun Scheme.mergeWith(schemes: List<Scheme>): Scheme {
+    if (schemes.isEmpty()) return this
+
+    val lazyScheme = LazyScheme(this)
+    val bindings = lazyScheme.bindings
+
+    fun unifySchemes(a: LazyScheme, b: LazyScheme) {
+        bindings.unify(a.target, b.target)
+        for ((ap, bp) in a.parameters.zip(b.parameters)) {
+            unifySchemes(ap, bp)
+        }
+    }
+
+    schemes.forEach {
+        val overrideScheme = LazyScheme(it, bindings = lazyScheme.bindings)
+        unifySchemes(lazyScheme, overrideScheme)
+    }
+
+    return lazyScheme.toScheme()
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrorMessages.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrorMessages.kt
index 726ae5b..0398542 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrorMessages.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrorMessages.kt
@@ -120,5 +120,9 @@
             Renderers.TO_STRING,
             Renderers.TO_STRING
         )
+        MAP.put(
+            ComposeErrors.COMPOSE_APPLIER_DECLARATION_MISMATCH,
+            "The composition target of an override must match the ancestor target"
+        )
     }
 }
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrors.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrors.kt
index 3bfd5b6..3b3f96dd 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrors.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrors.kt
@@ -148,6 +148,12 @@
             Severity.WARNING
         )
 
+    @JvmField
+    val COMPOSE_APPLIER_DECLARATION_MISMATCH =
+        DiagnosticFactory0.create<PsiElement>(
+            Severity.WARNING
+        )
+
     init {
         Errors.Initializer.initializeFactoryNamesAndDefaultErrorMessages(
             ComposeErrors::class.java,
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/inference/Scheme.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/inference/Scheme.kt
index 67e3a5e..b1a6a30 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/inference/Scheme.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/inference/Scheme.kt
@@ -112,8 +112,22 @@
         return this.alphaRename().simpleEquals(o.alphaRename())
     }
 
+    fun canOverride(other: Scheme): Boolean = alphaRename().simpleCanOverride(other.alphaRename())
+
     override fun hashCode(): Int = alphaRename().simpleHashCode()
 
+    private fun simpleCanOverride(other: Scheme): Boolean {
+        return if (other.target is Open) {
+            target is Open && other.target.index == target.index
+        } else {
+            target.isUnspecified || target == other.target
+        } && parameters.zip(other.parameters).all { (a, b) -> a.simpleCanOverride(b) } &&
+            (
+                result == other.result ||
+                    (other.result != null && result != null && result.canOverride((other.result)))
+            )
+    }
+
     private fun simpleEquals(other: Scheme) =
         target == other.target && parameters.zip(other.parameters).all { (a, b) -> a == b } &&
             result == result
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTargetAnnotationsTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTargetAnnotationsTransformer.kt
index ae67ced..24e0a00 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTargetAnnotationsTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTargetAnnotationsTransformer.kt
@@ -33,6 +33,7 @@
 import androidx.compose.compiler.plugins.kotlin.inference.Token
 import androidx.compose.compiler.plugins.kotlin.inference.deserializeScheme
 import androidx.compose.compiler.plugins.kotlin.irTrace
+import androidx.compose.compiler.plugins.kotlin.mergeWith
 import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
 import org.jetbrains.kotlin.descriptors.ClassKind
 import org.jetbrains.kotlin.descriptors.Modality
@@ -43,6 +44,7 @@
 import org.jetbrains.kotlin.ir.declarations.IrFunction
 import org.jetbrains.kotlin.ir.declarations.IrLocalDelegatedProperty
 import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
+import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
 import org.jetbrains.kotlin.ir.declarations.IrValueParameter
 import org.jetbrains.kotlin.ir.declarations.IrVariable
 import org.jetbrains.kotlin.ir.declarations.name
@@ -60,6 +62,7 @@
 import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin
 import org.jetbrains.kotlin.ir.expressions.IrTypeOperatorCall
 import org.jetbrains.kotlin.ir.expressions.impl.IrConstructorCallImpl
+import org.jetbrains.kotlin.ir.interpreter.getLastOverridden
 import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
 import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
 import org.jetbrains.kotlin.ir.symbols.IrSymbol
@@ -660,33 +663,41 @@
     }
 
     override fun toDeclaredScheme(defaultTarget: Item): Scheme = with(transformer) {
-        function.scheme
-            ?: run {
-                val target = function.annotations.target.let { target ->
-                    if (target.isUnspecified && function.body == null) {
-                        defaultTarget
-                    } else if (target.isUnspecified) {
-                        // Default to the target specified at the file scope, if one.
-                        function.file.annotations.target
-                    } else target
-                }
-                val effectiveDefault =
-                    if (function.body == null) defaultTarget
-                    else Open(-1, isUnspecified = true)
-                val result = function.returnType.let { resultType ->
-                    if (resultType.isOrHasComposableLambda)
-                        resultType.toScheme(effectiveDefault)
-                    else null
-                }
-
-                Scheme(
-                    target,
-                    parameters().map { it.toDeclaredScheme(effectiveDefault) },
-                    result
-                )
-            }
+        function.scheme ?: function.toScheme(defaultTarget)
     }
 
+    private fun IrFunction.toScheme(defaultTarget: Item): Scheme = with(transformer) {
+        val target = function.annotations.target.let { target ->
+            if (target.isUnspecified && function.body == null) {
+                defaultTarget
+            } else if (target.isUnspecified) {
+                // Default to the target specified at the file scope, if one.
+                function.file.annotations.target
+            } else target
+        }
+        val effectiveDefault =
+            if (function.body == null) defaultTarget
+            else Open(-1, isUnspecified = true)
+        val result = function.returnType.let { resultType ->
+            if (resultType.isOrHasComposableLambda)
+                resultType.toScheme(effectiveDefault)
+            else null
+        }
+
+        Scheme(
+            target,
+            parameters().map { it.toDeclaredScheme(effectiveDefault) },
+            result
+        ).let { scheme ->
+            ancestorScheme(defaultTarget)?.let { scheme.mergeWith(listOf(it)) } ?: scheme
+        }
+    }
+
+    private fun IrFunction.ancestorScheme(defaultTarget: Item): Scheme? =
+        if (this is IrSimpleFunction && this.overriddenSymbols.isNotEmpty()) {
+            getLastOverridden().toScheme(defaultTarget)
+        } else null
+
     override fun hashCode(): Int = function.hashCode() * 31
     override fun equals(other: Any?) =
         other is InferenceFunctionDeclaration && other.function == function
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt
index 23b522c..a85fd4f 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt
@@ -17,6 +17,7 @@
 package androidx.compose.material
 
 import android.os.Build
+import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.PaddingValues
@@ -27,7 +28,9 @@
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.shadow
 import androidx.compose.ui.geometry.Offset
@@ -40,6 +43,7 @@
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -418,6 +422,66 @@
     }
 
     @Test
+    fun scaffold_geometry_animated_fabSize() {
+        val fabTestTag = "FAB TAG"
+        lateinit var showFab: MutableState<Boolean>
+        var actualFabSize: IntSize = IntSize.Zero
+        var actualFabPlacement: FabPlacement? = null
+        rule.setContent {
+            showFab = remember { mutableStateOf(true) }
+            val animatedFab = @Composable {
+                AnimatedVisibility(visible = showFab.value) {
+                    FloatingActionButton(
+                        modifier = Modifier.onGloballyPositioned { positioned ->
+                            actualFabSize = positioned.size
+                        }.testTag(fabTestTag),
+                        >
+                    ) {
+                        Icon(Icons.Filled.Favorite, null)
+                    }
+                }
+            }
+            Scaffold(
+                floatingActionButton = animatedFab,
+                floatingActionButtonPosition = FabPosition.End,
+                bottomBar = {
+                    actualFabPlacement = LocalFabPlacement.current
+                }
+            ) {
+                Text("body")
+            }
+        }
+
+        val fabNode = rule.onNodeWithTag(fabTestTag)
+
+        fabNode.assertIsDisplayed()
+
+        rule.runOnIdle {
+            assertThat(actualFabPlacement?.width).isEqualTo(actualFabSize.width)
+            assertThat(actualFabPlacement?.height).isEqualTo(actualFabSize.height)
+            actualFabSize = IntSize.Zero
+            actualFabPlacement = null
+            showFab.value = false
+        }
+
+        fabNode.assertDoesNotExist()
+
+        rule.runOnIdle {
+            assertThat(actualFabPlacement).isNull()
+            actualFabSize = IntSize.Zero
+            actualFabPlacement = null
+            showFab.value = true
+        }
+
+        fabNode.assertIsDisplayed()
+
+        rule.runOnIdle {
+            assertThat(actualFabPlacement?.width).isEqualTo(actualFabSize.width)
+            assertThat(actualFabPlacement?.height).isEqualTo(actualFabSize.height)
+        }
+    }
+
+    @Test
     fun scaffold_innerPadding_lambdaParam() {
         var bottomBarSize: IntSize = IntSize.Zero
         lateinit var innerPadding: PaddingValues
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
index d8cd090..c49d3c7 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
@@ -254,30 +254,34 @@
             val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
 
             val fabPlaceables =
-                subcompose(ScaffoldLayoutContent.Fab, fab).mapNotNull { measurable ->
-                    measurable.measure(looseConstraints).takeIf { it.height != 0 && it.width != 0 }
+                subcompose(ScaffoldLayoutContent.Fab, fab).fastMap { measurable ->
+                    measurable.measure(looseConstraints)
                 }
 
             val fabPlacement = if (fabPlaceables.isNotEmpty()) {
-                val fabWidth = fabPlaceables.fastMaxBy { it.width }!!.width
-                val fabHeight = fabPlaceables.fastMaxBy { it.height }!!.height
+                val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0
+                val fabHeight = fabPlaceables.fastMaxBy { it.height }?.height ?: 0
                 // FAB distance from the left of the layout, taking into account LTR / RTL
-                val fabLeftOffset = if (fabPosition == FabPosition.End) {
-                    if (layoutDirection == LayoutDirection.Ltr) {
-                        layoutWidth - FabSpacing.roundToPx() - fabWidth
+                if (fabWidth != 0 && fabHeight != 0) {
+                    val fabLeftOffset = if (fabPosition == FabPosition.End) {
+                        if (layoutDirection == LayoutDirection.Ltr) {
+                            layoutWidth - FabSpacing.roundToPx() - fabWidth
+                        } else {
+                            FabSpacing.roundToPx()
+                        }
                     } else {
-                        FabSpacing.roundToPx()
+                        (layoutWidth - fabWidth) / 2
                     }
-                } else {
-                    (layoutWidth - fabWidth) / 2
-                }
 
-                FabPlacement(
-                    isDocked = isFabDocked,
-                    left = fabLeftOffset,
-                    width = fabWidth,
-                    height = fabHeight
-                )
+                    FabPlacement(
+                        isDocked = isFabDocked,
+                        left = fabLeftOffset,
+                        width = fabWidth,
+                        height = fabHeight
+                    )
+                } else {
+                    null
+                }
             } else {
                 null
             }
@@ -334,10 +338,8 @@
                 it.place(0, layoutHeight - bottomBarHeight)
             }
             // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
-            fabPlacement?.let { placement ->
-                fabPlaceables.fastForEach {
-                    it.place(placement.left, layoutHeight - fabOffsetFromBottom!!)
-                }
+            fabPlaceables.fastForEach {
+                it.place(fabPlacement?.left ?: 0, layoutHeight - (fabOffsetFromBottom ?: 0))
             }
         }
     }
diff --git a/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/ComposeTestCase.kt b/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/ComposeTestCase.kt
index 28460d1..5d5e032 100644
--- a/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/ComposeTestCase.kt
+++ b/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/ComposeTestCase.kt
@@ -17,6 +17,7 @@
 package androidx.compose.testutils
 
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.UiComposable
 
 /**
  * To be implemented to provide a test case that is then executed by [ComposeTestRule] or can be
@@ -72,6 +73,7 @@
      * The lifecycle rules for this method are same as for [Content]
      */
     @Composable
+    @UiComposable
     open fun ContentWrappers(content: @Composable () -> Unit) {
         content()
     }
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index a5944e3..d230019 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -2223,7 +2223,7 @@
     ctor public AbstractComposeView(android.content.Context context, optional android.util.AttributeSet? attrs, optional int defStyleAttr);
     ctor public AbstractComposeView(android.content.Context context, optional android.util.AttributeSet? attrs);
     ctor public AbstractComposeView(android.content.Context context);
-    method @androidx.compose.runtime.Composable public abstract void Content();
+    method @androidx.compose.runtime.Composable @androidx.compose.ui.UiComposable public abstract void Content();
     method public final void createComposition();
     method public final void disposeComposition();
     method public final boolean getHasComposition();
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 0458028..2eeed57 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -2392,7 +2392,7 @@
     ctor public AbstractComposeView(android.content.Context context, optional android.util.AttributeSet? attrs, optional int defStyleAttr);
     ctor public AbstractComposeView(android.content.Context context, optional android.util.AttributeSet? attrs);
     ctor public AbstractComposeView(android.content.Context context);
-    method @androidx.compose.runtime.Composable public abstract void Content();
+    method @androidx.compose.runtime.Composable @androidx.compose.ui.UiComposable public abstract void Content();
     method public final void createComposition();
     method public final void disposeComposition();
     method public final boolean getHasComposition();
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index ced5b76..3896d4a 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -2258,7 +2258,7 @@
     ctor public AbstractComposeView(android.content.Context context, optional android.util.AttributeSet? attrs, optional int defStyleAttr);
     ctor public AbstractComposeView(android.content.Context context, optional android.util.AttributeSet? attrs);
     ctor public AbstractComposeView(android.content.Context context);
-    method @androidx.compose.runtime.Composable public abstract void Content();
+    method @androidx.compose.runtime.Composable @androidx.compose.ui.UiComposable public abstract void Content();
     method public final void createComposition();
     method public final void disposeComposition();
     method public final boolean getHasComposition();
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ComposeView.android.kt
index 4787b78..0ccde49 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ComposeView.android.kt
@@ -27,6 +27,7 @@
 import androidx.compose.runtime.Recomposer
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.InternalComposeUiApi
+import androidx.compose.ui.UiComposable
 import androidx.compose.ui.node.InternalCoreApi
 import androidx.compose.ui.node.Owner
 import androidx.lifecycle.Lifecycle
@@ -170,6 +171,7 @@
      * whichever comes first.
      */
     @Composable
+    @UiComposable
     abstract fun Content()
 
     /**
diff --git a/datastore/datastore-multiprocess/build.gradle b/datastore/datastore-multiprocess/build.gradle
index 1b5e4313..30b35a7 100644
--- a/datastore/datastore-multiprocess/build.gradle
+++ b/datastore/datastore-multiprocess/build.gradle
@@ -25,6 +25,8 @@
 dependencies {
     api(libs.kotlinStdlib)
     api(libs.kotlinCoroutinesCore)
+    api("androidx.annotation:annotation:1.2.0")
+    api(project(":datastore:datastore-core"))
 
     androidTestImplementation(libs.junit)
     androidTestImplementation(libs.kotlinCoroutinesTest)
@@ -32,6 +34,8 @@
     androidTestImplementation(project(":internal-testutils-truth"))
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.testCore)
+    androidTestImplementation(project(":datastore:datastore-core"))
+    androidTestImplementation(project(":datastore:datastore-proto"))
 }
 
 android {
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt
new file mode 100644
index 0000000..51e4717
--- /dev/null
+++ b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt
@@ -0,0 +1,447 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.multiprocess
+
+import androidx.annotation.GuardedBy
+import androidx.datastore.core.CorruptionException
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.Serializer
+import androidx.datastore.multiprocess.handlers.NoOpCorruptionHandler
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+import java.io.IOException
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.coroutineContext
+import kotlinx.coroutines.completeWith
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.dropWhile
+import kotlinx.coroutines.flow.emitAll
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
+
+/**
+ * Multi process implementation of DataStore. It is multi-process safe.
+ */
+// TODO(zhiyuanwang): copied straight from {@link androidx.datastore.core.SingleProcessDataStore},
+// replace with the real multi process implementation
+internal class MultiProcessDataStore<T>(
+    private val produceFile: () -> File,
+    private val serializer: Serializer<T>,
+    /**
+     * The list of initialization tasks to perform. These tasks will be completed before any data
+     * is published to the data and before any read-modify-writes execute in updateData.  If
+     * any of the tasks fail, the tasks will be run again the next time data is collected or
+     * updateData is called. Init tasks should not wait on results from data - this will
+     * result in deadlock.
+     */
+    initTasksList: List<suspend (api: InitializerApi<T>) -> Unit> = emptyList(),
+    private val corruptionHandler: CorruptionHandler<T> = NoOpCorruptionHandler<T>(),
+    private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+) : DataStore<T> {
+
+    override val data: Flow<T> = flow {
+        /**
+         * If downstream flow is UnInitialized, no data has been read yet, we need to trigger a new
+         * read then start emitting values once we have seen a new value (or exception).
+         *
+         * If downstream flow has a ReadException, there was an exception last time we tried to read
+         * data. We need to trigger a new read then start emitting values once we have seen a new
+         * value (or exception).
+         *
+         * If downstream flow has Data, we should just start emitting from downstream flow.
+         *
+         * If Downstream flow is Final, the scope has been cancelled so the data store is no
+         * longer usable. We should just propagate this exception.
+         *
+         * State always starts at null. null can transition to ReadException, Data or
+         * Final. ReadException can transition to another ReadException, Data or Final.
+         * Data can transition to another Data or Final. Final will not change.
+         */
+
+        val currentDownStreamFlowState = downstreamFlow.value
+
+        if (currentDownStreamFlowState !is Data) {
+            // We need to send a read request because we don't have data yet.
+            actor.offer(Message.Read(currentDownStreamFlowState))
+        }
+
+        emitAll(
+            downstreamFlow.dropWhile {
+                if (currentDownStreamFlowState is Data<T> ||
+                    currentDownStreamFlowState is Final<T>
+                ) {
+                    // We don't need to drop any Data or Final values.
+                    false
+                } else {
+                    // we need to drop the last seen state since it was either an exception or
+                    // wasn't yet initialized. Since we sent a message to actor, we *will* see a
+                    // new value.
+                    it === currentDownStreamFlowState
+                }
+            }.map {
+                when (it) {
+                    is ReadException<T> -> throw it.readException
+                    is Final<T> -> throw it.finalException
+                    is Data<T> -> it.value
+                    is UnInitialized -> error(
+                        "This is a bug in DataStore. Please file a bug at: " +
+                            "https://issuetracker.google.com/issues/new?" +
+                            "component=907884&template=1466542"
+                    )
+                }
+            }
+        )
+    }
+
+    override suspend fun updateData(transform: suspend (t: T) -> T): T {
+        /**
+         * The states here are the same as the states for reads. Additionally we send an ack that
+         * the actor *must* respond to (even if it is cancelled).
+         */
+        val ack = CompletableDeferred<T>()
+        val currentDownStreamFlowState = downstreamFlow.value
+
+        val updateMsg =
+            Message.Update(transform, ack, currentDownStreamFlowState, coroutineContext)
+
+        actor.offer(updateMsg)
+
+        return ack.await()
+    }
+
+    private val SCRATCH_SUFFIX = ".tmp"
+
+    private val file: File by lazy {
+        val file = produceFile()
+
+        file.absolutePath.let {
+            synchronized(activeFilesLock) {
+                check(!activeFiles.contains(it)) {
+                    "There are multiple DataStores active for the same file: $file. You should " +
+                        "either maintain your DataStore as a singleton or confirm that there is " +
+                        "no two DataStore's active on the same file (by confirming that the scope" +
+                        " is cancelled)."
+                }
+                activeFiles.add(it)
+            }
+        }
+
+        file
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    private val downstreamFlow = MutableStateFlow(UnInitialized as State<T>)
+
+    private var initTasks: List<suspend (api: InitializerApi<T>) -> Unit>? =
+        initTasksList.toList()
+
+    /** The actions for the actor. */
+    private sealed class Message<T> {
+        abstract val lastState: State<T>?
+
+        /**
+         * Represents a read operation. If the data is already cached, this is a no-op. If data
+         * has not been cached, it triggers a new read to the specified dataChannel.
+         */
+        class Read<T>(
+            override val lastState: State<T>?
+        ) : Message<T>()
+
+        /** Represents an update operation. */
+        class Update<T>(
+            val transform: suspend (t: T) -> T,
+            /**
+             * Used to signal (un)successful completion of the update to the caller.
+             */
+            val ack: CompletableDeferred<T>,
+            override val lastState: State<T>?,
+            val callerContext: CoroutineContext
+        ) : Message<T>()
+    }
+
+    private val actor = SimpleActor<Message<T>>(
+        scope = scope,
+        >
+            it?.let {
+                downstreamFlow.value = Final(it)
+            }
+            // We expect it to always be non-null but we will leave the alternative as a no-op
+            // just in case.
+
+            synchronized(activeFilesLock) {
+                activeFiles.remove(file.absolutePath)
+            }
+        },
+         msg, ex ->
+            if (msg is Message.Update) {
+                // TODO(rohitsat): should we instead use scope.ensureActive() to get the original
+                //  cancellation cause? Should we instead have something like
+                //  UndeliveredElementException?
+                msg.ack.completeExceptionally(
+                    ex ?: CancellationException(
+                        "DataStore scope was cancelled before updateData could complete"
+                    )
+                )
+            }
+        }
+    ) { msg ->
+        when (msg) {
+            is Message.Read -> {
+                handleRead(msg)
+            }
+            is Message.Update -> {
+                handleUpdate(msg)
+            }
+        }
+    }
+
+    private suspend fun handleRead(read: Message.Read<T>) {
+        when (val currentState = downstreamFlow.value) {
+            is Data -> {
+                // We already have data so just return...
+            }
+            is ReadException -> {
+                if (currentState === read.lastState) {
+                    readAndInitOrPropagateFailure()
+                }
+
+                // Someone else beat us but also failed. The collector has already
+                // been signalled so we don't need to do anything.
+            }
+            UnInitialized -> {
+                readAndInitOrPropagateFailure()
+            }
+            is Final -> error("Can't read in final state.") // won't happen
+        }
+    }
+
+    private suspend fun handleUpdate(update: Message.Update<T>) {
+        // All branches of this *must* complete ack either successfully or exceptionally.
+        // We must *not* throw an exception, just propagate it to the ack.
+        update.ack.completeWith(
+            runCatching {
+
+                when (val currentState = downstreamFlow.value) {
+                    is Data -> {
+                        // We are already initialized, we just need to perform the update
+                        transformAndWrite(update.transform, update.callerContext)
+                    }
+                    is ReadException, is UnInitialized -> {
+                        if (currentState === update.lastState) {
+                            // we need to try to read again
+                            readAndInitOrPropagateAndThrowFailure()
+
+                            // We've successfully read, now we need to perform the update
+                            transformAndWrite(update.transform, update.callerContext)
+                        } else {
+                            // Someone else beat us to read but also failed. We just need to
+                            // signal the writer that is waiting on ack.
+                            // This cast is safe because we can't be in the UnInitialized
+                            // state if the state has changed.
+                            throw (currentState as ReadException).readException
+                        }
+                    }
+
+                    is Final -> throw currentState.finalException // won't happen
+                }
+            }
+        )
+    }
+
+    private suspend fun readAndInitOrPropagateAndThrowFailure() {
+        try {
+            readAndInit()
+        } catch (throwable: Throwable) {
+            downstreamFlow.value = ReadException(throwable)
+            throw throwable
+        }
+    }
+
+    private suspend fun readAndInitOrPropagateFailure() {
+        try {
+            readAndInit()
+        } catch (throwable: Throwable) {
+            downstreamFlow.value = ReadException(throwable)
+        }
+    }
+
+    private suspend fun readAndInit() {
+        // This should only be called if we don't already have cached data.
+        check(downstreamFlow.value == UnInitialized || downstreamFlow.value is ReadException)
+
+        val updateLock = Mutex()
+        var initData = readDataOrHandleCorruption()
+
+        var initializationComplete: Boolean = false
+
+        // TODO(b/151635324): Consider using Context Element to throw an error on re-entrance.
+        val api = object : InitializerApi<T> {
+            override suspend fun updateData(transform: suspend (t: T) -> T): T {
+                return updateLock.withLock() {
+                    if (initializationComplete) {
+                        throw IllegalStateException(
+                            "InitializerApi.updateData should not be " +
+                                "called after initialization is complete."
+                        )
+                    }
+
+                    val newData = transform(initData)
+                    if (newData != initData) {
+                        writeData(newData)
+                        initData = newData
+                    }
+
+                    initData
+                }
+            }
+        }
+
+        initTasks?.forEach { it(api) }
+        initTasks = null // Init tasks have run successfully, we don't need them anymore.
+        updateLock.withLock {
+            initializationComplete = true
+        }
+
+        downstreamFlow.value = Data(initData, initData.hashCode(), /* unused */ version = 0)
+    }
+
+    private suspend fun readDataOrHandleCorruption(): T {
+        try {
+            return readData()
+        } catch (ex: CorruptionException) {
+
+            val newData: T = corruptionHandler.handleCorruption(ex)
+
+            try {
+                writeData(newData)
+            } catch (writeEx: IOException) {
+                // If we fail to write the handled data, add the new exception as a suppressed
+                // exception.
+                ex.addSuppressed(writeEx)
+                throw ex
+            }
+
+            // If we reach this point, we've successfully replaced the data on disk with newData.
+            return newData
+        }
+    }
+
+    private suspend fun readData(): T {
+        try {
+            FileInputStream(file).use { stream ->
+                return serializer.readFrom(stream)
+            }
+        } catch (ex: FileNotFoundException) {
+            if (file.exists()) {
+                throw ex
+            }
+            return serializer.defaultValue
+        }
+    }
+
+    // downstreamFlow.value must be successfully set to data before calling this
+    private suspend fun transformAndWrite(
+        transform: suspend (t: T) -> T,
+        callerContext: CoroutineContext
+    ): T {
+        // value is not null or an exception because we must have the value set by now so this cast
+        // is safe.
+        val curDataAndHash = downstreamFlow.value as Data<T>
+        curDataAndHash.checkHashCode()
+
+        val curData = curDataAndHash.value
+        val newData = withContext(callerContext) { transform(curData) }
+
+        // Check that curData has not changed...
+        curDataAndHash.checkHashCode()
+
+        return if (curData == newData) {
+            curData
+        } else {
+            writeData(newData)
+            downstreamFlow.value = Data(newData, newData.hashCode(), /* unused */ version = 0)
+            newData
+        }
+    }
+
+    /**
+     * Internal only to prevent creation of synthetic accessor function. Do not call this from
+     * outside this class.
+     */
+    internal suspend fun writeData(newData: T) {
+        file.createParentDirectories()
+
+        val scratchFile = File(file.absolutePath + SCRATCH_SUFFIX)
+        try {
+            FileOutputStream(scratchFile).use { stream ->
+                serializer.writeTo(newData, UncloseableOutputStream(stream))
+                stream.fd.sync()
+                // TODO(b/151635324): fsync the directory, otherwise a badly timed crash could
+                //  result in reverting to a previous state.
+            }
+
+            if (!scratchFile.renameTo(file)) {
+                throw IOException(
+                    "Unable to rename $scratchFile." +
+                        "This likely means that there are multiple instances of DataStore " +
+                        "for this file. Ensure that you are only creating a single instance of " +
+                        "datastore for this file."
+                )
+            }
+        } catch (ex: IOException) {
+            if (scratchFile.exists()) {
+                scratchFile.delete() // Swallow failure to delete
+            }
+            throw ex
+        }
+    }
+
+    private fun File.createParentDirectories() {
+        val parent: File? = canonicalFile.parentFile
+
+        parent?.let {
+            it.mkdirs()
+            if (!it.isDirectory) {
+                throw IOException("Unable to create parent directories of $this")
+            }
+        }
+    }
+
+    internal companion object {
+        /**
+         * Active files should contain the absolute path for which there are currently active
+         * DataStores. A DataStore is active until the scope it was created with has been
+         * cancelled. Files aren't added to this list until the first read/write because the file
+         * path is computed asynchronously.
+         */
+        @GuardedBy("activeFilesLock")
+        internal val activeFiles = mutableSetOf<String>()
+
+        internal val activeFilesLock = Any()
+    }
+}
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/handlers/NoOpCorruptionHandler.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/handlers/NoOpCorruptionHandler.kt
new file mode 100644
index 0000000..c92e910
--- /dev/null
+++ b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/handlers/NoOpCorruptionHandler.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.datastore.multiprocess.handlers
+
+import androidx.datastore.core.CorruptionException
+import androidx.datastore.multiprocess.CorruptionHandler
+
+/**
+ * Default corruption handler which does nothing but rethrow the exception.
+ */
+internal class NoOpCorruptionHandler<T> : CorruptionHandler<T> {
+
+    @Throws(CorruptionException::class)
+    override suspend fun handleCorruption(ex: CorruptionException): T {
+        throw ex
+    }
+}
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/CorruptionHandler.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/CorruptionHandler.kt
new file mode 100644
index 0000000..1eacf02
--- /dev/null
+++ b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/CorruptionHandler.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.multiprocess
+
+import androidx.datastore.core.CorruptionException
+
+/**
+ * CorruptionHandlers allow recovery from corruption that prevents reading data from the file (as
+ * indicated by a CorruptionException).
+ */
+internal interface CorruptionHandler<T> {
+    /**
+     * This function will be called by DataStore when it encounters corruption. If the
+     * implementation of this function throws an exception, it will be propagated to the original
+     * call to DataStore. Otherwise, the returned data will be written to disk.
+     *
+     * This function should not interact with any DataStore API - doing so can result in a deadlock.
+     *
+     * @param ex is the exception encountered when attempting to deserialize data from disk.
+     * @return The value that DataStore should attempt to write to disk.
+     **/
+    public suspend fun handleCorruption(ex: CorruptionException): T
+}
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/InitializerApi.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/InitializerApi.kt
new file mode 100644
index 0000000..c7eaa410
--- /dev/null
+++ b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/InitializerApi.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.multiprocess
+
+/**
+ * The initializer API allows changes to be made to store before data is accessed through
+ * data or updateData.
+ *
+ * Initializers are executed in the order in which they are added. They must be idempotent
+ * since they are run each time the DataStore starts, and they may be run multiple times by a
+ * single instance if a downstream initializer fails.
+ *
+ * Note: Initializers are internal only. Instead, see [DataMigration].
+ */
+internal interface InitializerApi<T> {
+    suspend fun updateData(transform: suspend (t: T) -> T): T
+}
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/Message.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/Message.kt
new file mode 100644
index 0000000..b3ae931
--- /dev/null
+++ b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/Message.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.multiprocess
+
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CompletableDeferred
+
+/** The actions for the actor. */
+internal sealed class Message<T> {
+    abstract val lastState: State<T>?
+
+    /**
+     * Represents a read operation. If the data is already cached, this is a no-op. If data
+     * has not been cached, it triggers a new read to the specified dataChannel.
+     */
+    class Read<T>(
+        override val lastState: State<T>?,
+        val isBlocking: Boolean = false
+    ) : Message<T>()
+
+    /** Represents an update operation. */
+    class Update<T>(
+        val transform: suspend (t: T) -> T,
+        /**
+         * Used to signal (un)successful completion of the update to the caller.
+         */
+        val ack: CompletableDeferred<T>,
+        override val lastState: State<T>?,
+        val callerContext: CoroutineContext
+    ) : Message<T>()
+}
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/SimpleActor.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/SimpleActor.kt
new file mode 100644
index 0000000..39f3a38
--- /dev/null
+++ b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/SimpleActor.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.multiprocess
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
+import kotlinx.coroutines.channels.ClosedSendChannelException
+import kotlinx.coroutines.channels.onClosed
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.launch
+import java.util.concurrent.atomic.AtomicInteger
+
+internal class SimpleActor<T>(
+    /**
+     * The scope in which to consume messages.
+     */
+    private val scope: CoroutineScope,
+    /**
+     * Function that will be called when scope is cancelled. Should *not* throw exceptions.
+     */
+    onComplete: (Throwable?) -> Unit,
+    /**
+     * Function that will be called for each element when the scope is cancelled. Should *not*
+     * throw exceptions.
+     */
+    onUndeliveredElement: (T, Throwable?) -> Unit,
+    /**
+     * Function that will be called once for each message.
+     *
+     * Must *not* throw an exception (other than CancellationException if scope is cancelled).
+     */
+    private val consumeMessage: suspend (T) -> Unit
+) {
+    private val messageQueue = Channel<T>(capacity = UNLIMITED)
+
+    /**
+     * Count of the number of remaining messages to process. When the messageQueue is closed,
+     * this is no longer used.
+     */
+    private val remainingMessages = AtomicInteger(0)
+
+    init {
+        // If the scope doesn't have a job, it won't be cancelled, so we don't need to register a
+        // callback.
+        scope.coroutineContext[Job]?.invokeOnCompletion { ex ->
+            onComplete(ex)
+
+            // TODO(rohitsat): replace this with Channel(onUndeliveredElement) when it
+            // is fixed: https://github.com/Kotlin/kotlinx.coroutines/issues/2435
+
+            messageQueue.close(ex)
+
+            while (true) {
+                messageQueue.tryReceive().getOrNull()?.let { msg ->
+                    onUndeliveredElement(msg, ex)
+                } ?: break
+            }
+        }
+    }
+
+    /**
+     * Sends a message to a message queue to be processed by [consumeMessage] in [scope].
+     *
+     * If [offer] completes successfully, the msg *will* be processed either by
+     * consumeMessage or
+     * onUndeliveredElement. If [offer] throws an exception, the message may or may not be
+     * processed.
+     */
+    fun offer(msg: T) {
+        /**
+         * Possible states:
+         * 1) remainingMessages = 0
+         *   All messages have been consumed, so there is no active consumer
+         * 2) remainingMessages > 0, no active consumer
+         *   One of the senders is responsible for triggering the consumer
+         * 3) remainingMessages > 0, active consumer
+         *   Consumer will continue to consume until remainingMessages is 0
+         * 4) messageQueue is closed, there are remaining messages to consume
+         *   Attempts to offer messages will fail, onComplete() will consume remaining messages
+         *   with onUndelivered. The Consumer has already completed since close() is called by
+         *   onComplete().
+         * 5) messageQueue is closed, there are no remaining messages to consume
+         *   Attempts to offer messages will fail.
+         */
+
+        // should never return false bc the channel capacity is unlimited
+        check(
+            messageQueue.trySend(msg)
+                .onClosed { throw it ?: ClosedSendChannelException("Channel was closed normally") }
+                .isSuccess
+        )
+
+        // If the number of remaining messages was 0, there is no active consumer, since it quits
+        // consuming once remaining messages hits 0. We must kick off a new consumer.
+        if (remainingMessages.getAndIncrement() == 0) {
+            scope.launch {
+                // We shouldn't have started a new consumer unless there are remaining messages...
+                check(remainingMessages.get() > 0)
+
+                do {
+                    // We don't want to try to consume a new message unless we are still active.
+                    // If ensureActive throws, the scope is no longer active, so it doesn't
+                    // matter that we have remaining messages.
+                    scope.ensureActive()
+
+                    consumeMessage(messageQueue.receive())
+                } while (remainingMessages.decrementAndGet() != 0)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/State.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/State.kt
new file mode 100644
index 0000000..c83819b
--- /dev/null
+++ b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/State.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.multiprocess
+
+/**
+ * Represents the current state of the DataStore.
+ */
+internal sealed class State<T>
+
+internal object UnInitialized : State<Any>()
+
+/**
+ * A read from disk has succeeded, value represents the current on disk state.
+ */
+internal class Data<T>(val value: T, val hashCode: Int, val version: Int) : State<T>() {
+    fun checkHashCode() {
+        check(value.hashCode() == hashCode) {
+            "Data in DataStore was mutated but DataStore is only compatible with Immutable types."
+        }
+    }
+}
+
+/**
+ * A read from disk has failed. ReadException is the exception that was thrown.
+ */
+internal class ReadException<T>(val readException: Throwable) : State<T>()
+
+/**
+ * The scope has been cancelled. This DataStore cannot process any new reads or writes.
+ */
+internal class Final<T>(val finalException: Throwable) : State<T>()
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/UncloseableOutputStream.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/UncloseableOutputStream.kt
new file mode 100644
index 0000000..45b1c81
--- /dev/null
+++ b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/UncloseableOutputStream.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.multiprocess
+
+import java.io.FileOutputStream
+import java.io.OutputStream
+
+/**
+ * Wrapper on FileOutputStream to prevent closing the underlying OutputStream.
+ */
+internal class UncloseableOutputStream(val fileOutputStream: FileOutputStream) : OutputStream() {
+
+    override fun write(b: Int) {
+        fileOutputStream.write(b)
+    }
+
+    override fun write(b: ByteArray) {
+        fileOutputStream.write(b)
+    }
+
+    override fun write(bytes: ByteArray, off: Int, len: Int) {
+        fileOutputStream.write(bytes, off, len)
+    }
+
+    override fun close() {
+        // We will not close the underlying FileOutputStream until after we're done syncing
+        // the fd. This is useful for things like b/173037611.
+    }
+
+    override fun flush() {
+        fileOutputStream.flush()
+    }
+}
\ No newline at end of file
diff --git a/development/gradleRemoteCache/.gitignore b/development/gradleRemoteCache/.gitignore
deleted file mode 100644
index f23b948..0000000
--- a/development/gradleRemoteCache/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*.jar
\ No newline at end of file
diff --git a/development/gradleRemoteCache/OWNERS b/development/gradleRemoteCache/OWNERS
deleted file mode 100644
index 3235a23..0000000
--- a/development/gradleRemoteCache/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-rahulrav@google.com
\ No newline at end of file
diff --git a/development/gradleRemoteCache/README.md b/development/gradleRemoteCache/README.md
deleted file mode 100644
index 328b00f..0000000
--- a/development/gradleRemoteCache/README.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# Setting up the Gradle Build Cache Node on Google Cloud Platform.
-
-To setup the [Gradle Remote Cache](https://docs.gradle.com/build-cache-node) you need to do the following:
-
-## Create a new Instance
-
-* Open the Cloud Platform [console](https://console.cloud.google.com/home/dashboard?project=fetch-licenses).
-
-* In the search box type in and select `VM Instances`.
-
-* Click on an existing node to see details page, then use `Create Similar` to create a new node.
-  *Note*: This node has to be tagged with a network tag called `gradle-remote-cache-node`
-  for it to be picked up by the load balancer. Make sure you create the node in the zone `us-east-1-b`.
-
-* Click `Allow HTTP Traffic` and `Allow HTTPs Traffic`. By doing do, you are allowing UberProxy access
-  to the remote cache. The load balancer is only available when you are on a corp network.
-
-* Connect to the newly created node using an SSH session. You can use the `gcloud` CLI for this.
-  *Note*: Use the `external` IP of the newly created node to SSH.
-
-```bash
-# Note: To switch projects use `gcloud config set project fetch-licenses`
-# Will show the newly created instance
-gcloud compute instances list
-# Will setup ssh configurations
-gcloud compute config-ssh
-ssh 123.123.123.123
-```
-
-## Starting the Gradle Remote Cache Node
-
-```bash
-# Install some prerequisite packages
-sudo apt update
-sudo apt upgrade
-sudo apt install openjdk-11-jdk tmux wget
-# Create a folder `Workspace` in the home directory.
-mkdir Workspace
-cd Workspace
-mkdir -p data/conf
-# using the template in this checkout create config.yaml
-vi data/conf/config.yaml
-# using the template in this checkout create run_node, replace YOURUSERNAME with your username
-vi run_node
-chmod +x run_node
-mkdir gradle-node
-wget https://docs.gradle.com/build-cache-node/jar/build-cache-node-11.1.jar -P gradle-node
-# Create a `tmux` session
-tmux new -s gradle
-sudo ./run_node &
-# Detach from the tmux session ctrl+b then d
-exit
-```
-
-## Update the `gradle-remote-cache-group` instance group.
-
-* Open `Instance groups` in gcloud console
-* Click on `gradle-remote-cache-group` and select `Edit Group`.
-* Select the new node(s), from the drop-down list.
-* Remove old nodes from the list
-* Click `Save`.
diff --git a/development/gradleRemoteCache/config.yaml b/development/gradleRemoteCache/config.yaml
deleted file mode 100644
index 5c5a832..0000000
--- a/development/gradleRemoteCache/config.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
----
-version: 3
-uiAccess:
-  type: "open"
-cache:
-  accessControl:
-    anonymousLevel: "readwrite"
-  targetSize: 150000
-  maxArtifactSize: 2500
diff --git a/development/gradleRemoteCache/data/.empty b/development/gradleRemoteCache/data/.empty
deleted file mode 100644
index e69de29..0000000
--- a/development/gradleRemoteCache/data/.empty
+++ /dev/null
diff --git a/development/gradleRemoteCache/gradle-node/.empty b/development/gradleRemoteCache/gradle-node/.empty
deleted file mode 100644
index e69de29..0000000
--- a/development/gradleRemoteCache/gradle-node/.empty
+++ /dev/null
diff --git a/development/gradleRemoteCache/run_node b/development/gradleRemoteCache/run_node
deleted file mode 100644
index 0b50c0f..0000000
--- a/development/gradleRemoteCache/run_node
+++ /dev/null
@@ -1 +0,0 @@
-java -jar /home/YOURUSERNAME/Workspace/gradle-node/build-cache-node-11.1.jar start --data-dir /home/YOURUSERNAME/Workspace/data --port 80 --no-warn-anon-cache-write --no-warn-anon-ui-access
\ No newline at end of file
diff --git a/development/importMaven/README.md b/development/importMaven/README.md
index 8b3de07..9ec5b89 100644
--- a/development/importMaven/README.md
+++ b/development/importMaven/README.md
@@ -2,13 +2,21 @@
 
 It can download maven artifacts or KMP prebuilts.
 
+By default, arguments passed into it the script that do not immediately
+follow an option (e.g. --optionName value) are evaluated to be maven
+artficat coordinates.
+
 # Quickstart
 
 ## download single artifact
-`./importMaven.sh import-artifact --artifacts "androidx.room:room-runtime:2.4.2"`
+`./importMaven.sh androidx.room:room-runtime:2.4.2`
+`./importMaven.sh --artifacts androidx.room:room-runtime:2.4.2`
 
 ## download multiple artifacts
-`./importMaven.sh import-artifact --artifacts "androidx.room:room-runtime:2.4.2,com.squareup.okio:okio:3.0.0"`
+`./importMaven.sh androidx.room:room-runtime:2.4.2 com.squareup.okio:okio:3.0.0`
+
+## download multiple artifacts with explicit argument
+`./importMaven.sh --artifacts androidx.room:room-runtime:2.4.2,com.squareup.okio:okio:3.0.0`
 
 ## download konan prebuilts needed for kotlin native
 `./importMaven.sh import-konan-binaries --konan-compiler-version 1.6.1`
@@ -17,20 +25,19 @@
 `./importMaven.sh import-toml`
 
 ## download an androidx prebuilt (via androidx.dev)
-`./importMaven.sh import-artifact --androidx-build-id 123 --artifacts "androidx.room:room-runtime:2.5.0-SNAPSHOT"`
+`./importMaven.sh --androidx-build-id 123 androidx.room:room-runtime:2.5.0-SNAPSHOT androidx.room:room-compiler:2.5.0-SNAPSHOT`
 
 ## download metalava
-`./importMaven.sh import-artifact --metalava-build-id 8660637 --redownload  --artifacts "com.android.tools.metalava:metalava:1.0.0-alpha06"`
+`./importMaven.sh --metalava-build-id 8660637 --redownload  --artifacts com.android.tools.metalava:metalava:1.0.0-alpha06`
 
 ## verbose logging
-`./importMaven.sh import-artifact --verbose --artifacts "androidx.room:room-runtime:2.4.2"`
+`./importMaven.sh --verbose androidx.room:room-runtime:2.4.2`
 
 # More Help:
 
 For full list of options, please execute one of the commands with `--help`
 ```
 ./importMaven.sh --help
-./importMaven.sh import-artifact --help
 ./importMaven.sh import-konan-prebuilts --help
 ./importMaven.sh import-toml --help
 ```
\ No newline at end of file
diff --git a/development/importMaven/import_maven_artifacts.py b/development/importMaven/import_maven_artifacts.py
index c7e11a8..c3f6947 100755
--- a/development/importMaven/import_maven_artifacts.py
+++ b/development/importMaven/import_maven_artifacts.py
@@ -66,7 +66,7 @@
     parse_result = parser.parse_args()
     artifact_name = parse_result.name
 
-    command = 'importMaven.sh import-artifact --artifacts %s' % (artifact_name)
+    command = 'importMaven.sh %s' % (artifact_name)
     # AndroidX Build Id
     androidx_build_id = parse_result.androidx_build_id
     if (androidx_build_id):
diff --git a/development/importMaven/src/main/kotlin/androidx/build/importMaven/ArtifactResolver.kt b/development/importMaven/src/main/kotlin/androidx/build/importMaven/ArtifactResolver.kt
index 8a26560..531fad3 100644
--- a/development/importMaven/src/main/kotlin/androidx/build/importMaven/ArtifactResolver.kt
+++ b/development/importMaven/src/main/kotlin/androidx/build/importMaven/ArtifactResolver.kt
@@ -117,7 +117,7 @@
             logger.info {
                 """
                     Starting artifact resolution
-                    Resolving artifacts: ${artifacts.joinToString(" ")}"
+                    Resolving artifacts: ${artifacts.joinToString(" ")}
                     Local repositories: ${localRepositories.joinToString(" ")}
                     High priority repositories: ${additionalPriorityRepositories.joinToString(" ")}
                 """.trimIndent()
@@ -214,10 +214,11 @@
                     val copy = configuration.copyRecursive().also {
                         it.resolutionStrategy.disableDependencyVerification()
                     }
-                    logger.warn(verificationException) {
+                    logger.warn {
                         """
                             Failed key verification for public servers, will retry without
                             verification.
+                            ${verificationException.message}
                         """.trimIndent()
                     }
                     resolveArtifacts(copy, disableVerificationOnFailure = false)
diff --git a/development/importMaven/src/main/kotlin/androidx/build/importMaven/Main.kt b/development/importMaven/src/main/kotlin/androidx/build/importMaven/Main.kt
index 8d3d852..05e083b 100644
--- a/development/importMaven/src/main/kotlin/androidx/build/importMaven/Main.kt
+++ b/development/importMaven/src/main/kotlin/androidx/build/importMaven/Main.kt
@@ -16,8 +16,13 @@
 package androidx.build.importMaven
 
 import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.clikt.core.Context
+import com.github.ajalt.clikt.core.UsageError
 import com.github.ajalt.clikt.core.subcommands
+import com.github.ajalt.clikt.parameters.arguments.argument
+import com.github.ajalt.clikt.parameters.arguments.multiple
 import com.github.ajalt.clikt.parameters.options.convert
+import com.github.ajalt.clikt.parameters.options.default
 import com.github.ajalt.clikt.parameters.options.flag
 import com.github.ajalt.clikt.parameters.options.option
 import com.github.ajalt.clikt.parameters.options.required
@@ -28,21 +33,24 @@
 import org.apache.logging.log4j.kotlin.logger
 import kotlin.system.exitProcess
 
-internal class Cli : CliktCommand() {
-    override fun run() = Unit
-}
-
 /**
  * Base class for all commands which only reads the support repo folder.
  */
 internal abstract class BaseCommand(
-    help: String
-) : CliktCommand(help) {
+    help: String,
+    treatUnknownOptionsAsArgs: Boolean = false,
+    invokeWithoutSubcommand: Boolean = false,
+) : CliktCommand(
+    help = help,
+    invokeWithoutSubcommand = invokeWithoutSubcommand,
+    treatUnknownOptionsAsArgs = treatUnknownOptionsAsArgs
+) {
+    private var interceptor: ((Context) -> Unit)? = null
     protected val logger by lazy {
         // make this lazy so that it can be created after root logger config is changed.
         logger("main")
     }
-    private val supportRepoFolder by option(
+    internal val supportRepoFolder by option(
         help = """
             Path to the support repository (frameworks/support).
             By default, it is inherited from the build of import maven itself.
@@ -50,7 +58,7 @@
         envvar = "SUPPORT_REPO"
     )
 
-    private val verbose by option(
+    internal val verbose by option(
         names = arrayOf("-v", "--verbose"),
         help = """
             Enables verbose logging
@@ -73,13 +81,27 @@
         }
     }
 
+    /**
+     * Disables executing the command, which is useful for testing.
+     */
+    fun intercept(interceptor: (Context) -> Unit) {
+        this.interceptor = interceptor
+        registeredSubcommands().forEach {
+            (it as BaseCommand).intercept(interceptor)
+        }
+    }
+
     final override fun run() {
         if (verbose) {
             enableVerboseLogs()
         } else {
             enableInfoLogs()
         }
-        execute()
+        if (interceptor != null) {
+            interceptor!!.invoke(currentContext)
+        } else {
+            execute()
+        }
     }
 
     abstract fun execute()
@@ -89,42 +111,47 @@
  * Base class to import maven artifacts.
  */
 internal abstract class BaseImportMavenCommand(
+    invokeWithoutSubcommand: Boolean = false,
     help: String
-) : BaseCommand(help) {
-    private val prebuiltsFolder by option(
+) : BaseCommand(
+    help = help,
+    invokeWithoutSubcommand = invokeWithoutSubcommand,
+    treatUnknownOptionsAsArgs = true,
+) {
+    internal val prebuiltsFolder by option(
         help = """
             Path to the prebuilts folder. Can be relative to the current working
             directory.
             By default, inherited from the support-repo root folder.
         """.trimIndent()
     )
-    private val androidXBuildId by option(
+    internal val androidXBuildId by option(
         names = arrayOf("--androidx-build-id"),
         help = """
             The build id of https://ci.android.com/builds/branches/aosp-androidx-main/grid?
             to use for fetching androidx prebuilts.
         """.trimIndent()
     ).int()
-    private val metalavaBuildId by option(
+    internal val metalavaBuildId by option(
         help = """
             The build id of https://androidx.dev/metalava/builds to fetch metalava from.
         """.trimIndent()
     ).int()
-    private val allowJetbrainsDev by option(
+    internal val allowJetbrainsDev by option(
         help = """
             Whether or not to allow artifacts to be fetched from Jetbrains' dev repository
             E.g. https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev
         """.trimIndent()
     ).flag()
 
-    private val redownload by option(
+    internal val redownload by option(
         help = """
             If set to true, local repositories will be ignored while resolving artifacts so
             all of them will be redownloaded.
         """.trimIndent()
     ).flag(default = false)
 
-    private val repositories by option(
+    internal val repositories by option(
         help = """
             Comma separated list of additional repositories.
         """.trimIndent()
@@ -132,7 +159,7 @@
         it.split(',')
     }
 
-    private val cleanLocalRepo by option(
+    internal val cleanLocalRepo by option(
         help = """
             This flag tries to remove unnecessary / bad files from the local maven repository.
             It must be used with the `redownload` flag.
@@ -141,7 +168,7 @@
         """.trimIndent()
     ).flag(default = false)
 
-    private val explicitlyFetchInheritedDependencies by option(
+    internal val explicitlyFetchInheritedDependencies by option(
         help = """
             If set, all inherited dependencies will be fetched individually, with their own
             dependencies.
@@ -164,6 +191,11 @@
     abstract fun artifacts(): List<String>
 
     override fun execute() {
+        if (currentContext.invokedSubcommand != null) {
+            // skip, invoking a sub command instead
+            return
+        }
+        val artifactsToBeResolved = artifacts()
         val extraRepositories = mutableListOf<String>()
         androidXBuildId?.let {
             extraRepositories.add(ArtifactResolver.createAndroidXRepo(it))
@@ -194,7 +226,7 @@
             extraRepositories.addAll(it)
         }
         val resolvedArtifacts = ArtifactResolver.resolveArtifacts(
-            artifacts = artifacts(),
+            artifacts = artifactsToBeResolved,
             additionalRepositories = extraRepositories,
             explicitlyFetchInheritedDependencies = explicitlyFetchInheritedDependencies,
             localRepositories = if (redownload) {
@@ -239,17 +271,52 @@
  * Imports the maven artifacts in the [artifacts] parameter.
  */
 internal class ImportArtifact : BaseImportMavenCommand(
-    help = "Imports given artifacts"
+    help = "Imports given artifacts",
+    invokeWithoutSubcommand = true
 ) {
+    private val args by argument(
+        help = """
+            The dependency notation of the artifact you want to add to the prebuilts folder.
+            Can be passed multiple times.
+            E.g. android.arch.work:work-runtime-ktx:1.0.0-alpha07
+        """.trimIndent()
+    ).multiple(
+        required = false,
+        default = emptyList()
+    )
     private val artifacts by option(
         help = """
             The dependency notation of the artifact you want to add to the prebuilts folder.
             E.g. android.arch.work:work-runtime-ktx:1.0.0-alpha07
             Multiple artifacts can be provided with a `,` in between them.
         """.trimIndent()
-    ).required()
+    ).default("")
 
-    override fun artifacts(): List<String> = artifacts.split(',')
+    override fun artifacts(): List<String> {
+        // artifacts passed via --artifacts
+        val optionArtifacts = artifacts.split(',')
+        // artficats passed as command line argument
+        val argArtifacts = args.flatMap { it.split(',') }
+        val artifactsToBeResolved = (optionArtifacts + argArtifacts).distinct()
+            .filter {
+                it.isNotBlank()
+            }
+        if (artifactsToBeResolved.isEmpty()) {
+            // since we run this command as the default one, we cannot enforce arguments.
+            // instead, we check them in first access
+            throw UsageError(
+                text = """
+                        Missing artifact coordinates.
+                        You can either pass them as arguments or explicitly via --artifacts option.
+                        e.g. ./importMaven.sh foo:bar:baz:123
+                             ./importMaven.sh --artifacts foo:bar:baz:123
+                        help:
+                        ${getFormattedHelp()}
+                    """.trimIndent()
+            )
+        }
+        return artifactsToBeResolved
+    }
 }
 
 /**
@@ -258,14 +325,14 @@
 internal class ImportKonanBinariesCommand : BaseCommand(
     help = "Downloads konan binaries"
 ) {
-    private val konanPrebuiltsFolder by option(
+    internal val konanPrebuiltsFolder by option(
         help = """
             Path to the prebuilts folder. Can be relative to the current working
             directory.
             By default, inherited from the support-repo root folder.
         """.trimIndent()
     )
-    private val konanCompilerVersion by option(
+    internal val konanCompilerVersion by option(
         help = """
             Konan compiler version to download. This is usually your kotlin version.
         """.trimIndent()
@@ -291,7 +358,7 @@
 internal class ImportToml : BaseImportMavenCommand(
     help = "Downloads all artifacts declared in the project's toml file"
 ) {
-    private val tomlFile by option(
+    internal val tomlFile by option(
         help = """
             Path to the toml file. If not provided, main androidx toml file is obtained from the
             supportRepoFolder argument.
@@ -309,9 +376,12 @@
     }
 }
 
+internal fun createCliCommands() = ImportArtifact()
+    .subcommands(
+        ImportKonanBinariesCommand(), ImportToml()
+    )
+
 fun main(args: Array<String>) {
-    Cli()
-        .subcommands(ImportArtifact(), ImportKonanBinariesCommand(), ImportToml())
-        .main(args)
+    createCliCommands().main(args)
     exitProcess(0)
 }
\ No newline at end of file
diff --git a/development/importMaven/src/test/kotlin/androidx/build/importMaven/CliCommandParserTest.kt b/development/importMaven/src/test/kotlin/androidx/build/importMaven/CliCommandParserTest.kt
new file mode 100644
index 0000000..dfcc22a
--- /dev/null
+++ b/development/importMaven/src/test/kotlin/androidx/build/importMaven/CliCommandParserTest.kt
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.build.importMaven
+
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+
+class CliCommandParserTest {
+    @Test
+    fun artifactCoordinatesViaArguments() {
+        runCommand<ImportArtifact>("foo:bar:123") { cmd ->
+            assertThat(
+                cmd.artifacts()
+            ).containsExactly("foo:bar:123")
+        }
+    }
+
+    @Test
+    fun multipleArtifactCoordinatesViaArguments() {
+        runCommand<ImportArtifact>("foo:123", "bar:2.3.4") { cmd ->
+            assertThat(
+                cmd.artifacts()
+            ).containsExactly("foo:123", "bar:2.3.4")
+        }
+    }
+
+    @Test
+    fun mixedArtifactAndArgumentInputs() {
+        runCommand<ImportArtifact>("--artifacts", "foo:123,foo:345", "bar:2.3.4") { cmd ->
+            assertThat(
+                cmd.artifacts()
+            ).containsExactly("foo:123", "foo:345", "bar:2.3.4")
+        }
+    }
+
+    @Test
+    fun artifactCoordinatesAsCommaSeparatedArguments() {
+        runCommand<ImportArtifact>("foo:bar,bar:baz") { cmd ->
+            assertThat(
+                cmd.artifacts()
+            ).containsExactly(
+                "foo:bar", "bar:baz"
+            )
+        }
+    }
+
+    @Test
+    fun importArtifactParameters() {
+        val allSupportedImportMavenBaseArguments = listOf(
+            "--verbose",
+            "--androidx-build-id", "123",
+            "--metalava-build-id", "345",
+            "--support-repo-folder", "support/repo/path",
+            "--allow-jetbrains-dev",
+            "--redownload",
+            "--repositories", "http://a.com,http://b.com",
+            "--clean-local-repo",
+            "--explicitly-fetch-inherited-dependencies"
+        )
+        val validateCommonArguments = { cmd: BaseImportMavenCommand ->
+            assertThat(
+                cmd.androidXBuildId
+            ).isEqualTo(123)
+            assertThat(
+                cmd.verbose
+            ).isTrue()
+            assertThat(
+                cmd.redownload
+            ).isTrue()
+            assertThat(
+                cmd.metalavaBuildId
+            ).isEqualTo(345)
+            assertThat(
+                cmd.supportRepoFolder
+            ).isEqualTo("support/repo/path")
+            assertThat(
+                cmd.allowJetbrainsDev
+            ).isTrue()
+            assertThat(
+                cmd.repositories
+            ).containsExactly(
+                "http://a.com", "http://b.com"
+            )
+            assertThat(
+                cmd.cleanLocalRepo
+            ).isTrue()
+            assertThat(
+                cmd.explicitlyFetchInheritedDependencies
+            ).isTrue()
+        }
+        val importArtifactArgs = allSupportedImportMavenBaseArguments + listOf(
+            "foo:bar",
+            "foo2:bar2",
+            "--artifacts",
+            "bar:baz,bar2:baz2"
+        )
+        runCommand<BaseImportMavenCommand>(
+            *importArtifactArgs.toTypedArray()
+        ) { cmd ->
+            assertThat(
+                cmd.artifacts()
+            ).containsExactly(
+                "foo:bar", "foo2:bar2", "bar:baz", "bar2:baz2"
+            )
+            validateCommonArguments(cmd)
+        }
+        val importTomlArgs = listOf("import-toml") + allSupportedImportMavenBaseArguments
+        runCommand<ImportToml>(
+            *importTomlArgs.toTypedArray()
+        ) { cmd ->
+            validateCommonArguments(cmd)
+        }
+    }
+
+    @Test
+    fun noArguments() {
+        val result = kotlin.runCatching {
+            runCommand<ImportArtifact>() { cmd ->
+                cmd.artifacts()
+            }
+        }
+        assertThat(
+            result.exceptionOrNull()
+        ).hasMessageThat().contains(
+            "Missing artifact coordinates"
+        )
+    }
+
+    @Test
+    fun importToml() {
+        runCommand<ImportToml>("import-toml") { cmd ->
+            assertThat(cmd.tomlFile).isNull()
+        }
+    }
+
+    @Test
+    fun importKonanArtifacts_missingKotlinVersion() {
+        val exception = runInvalidCommand<ImportKonanBinariesCommand>("import-konan-binaries")
+        assertThat(exception).hasMessageThat().contains(
+            "Missing option \"--konan-compiler-version\""
+        )
+    }
+
+    private inline fun <reified T : BaseCommand> runInvalidCommand(
+        vararg args: String
+    ): Throwable {
+        val result = kotlin.runCatching {
+            runCommand<T>(*args) { _ -> }
+        }
+        val exception = result.exceptionOrNull()
+        assertWithMessage("expected the commmand to fail")
+            .that(exception)
+            .isNotNull()
+        return exception!!
+    }
+
+    private inline fun <reified T : BaseCommand> runCommand(
+        vararg args: String,
+        crossinline block: (T) -> Unit
+    ) = createCliCommands().also {
+        var intercepted = false
+        it.intercept { context ->
+            if (context.invokedSubcommand == null) {
+                val cmd = context.command
+                if (T::class.isInstance(cmd)) {
+                    block(cmd as T)
+                } else {
+                    throw AssertionError(
+                        """
+                    Expected to invoke command type of ${T::class} but invoked ${cmd::class}
+                """.trimIndent()
+                    )
+                }
+                intercepted = true
+            }
+        }
+        it.parse(argv = args.toList(), parentContext = null)
+        assertWithMessage(
+            "Expected to intercept execution"
+        ).that(
+            intercepted
+        ).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/health/health-connect-client/api/current.txt b/health/health-connect-client/api/current.txt
index a4038e0..3a5df3a 100644
--- a/health/health-connect-client/api/current.txt
+++ b/health/health-connect-client/api/current.txt
@@ -1064,37 +1064,37 @@
     field public static final String UNKNOWN = "unknown";
   }
 
-  public final class Speed {
-    ctor public Speed(java.time.Instant time, @FloatRange(from=0.0, to=1000000.0) double metersPerSecond);
-    method public double getMetersPerSecond();
-    method public java.time.Instant getTime();
-    property public final double metersPerSecond;
-    property public final java.time.Instant time;
-  }
-
   public final class SpeedRecord implements androidx.health.connect.client.records.Record {
-    ctor public SpeedRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.Speed> samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public SpeedRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.SpeedRecord.Sample> samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.util.List<androidx.health.connect.client.records.Speed> getSamples();
+    method public java.util.List<androidx.health.connect.client.records.SpeedRecord.Sample> getSamples();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.util.List<androidx.health.connect.client.records.Speed> samples;
+    property public java.util.List<androidx.health.connect.client.records.SpeedRecord.Sample> samples;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
     field public static final androidx.health.connect.client.records.SpeedRecord.Companion Companion;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SPEED_AVG;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SPEED_MAX;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SPEED_MIN;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Velocity> SPEED_AVG;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Velocity> SPEED_MAX;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Velocity> SPEED_MIN;
   }
 
   public static final class SpeedRecord.Companion {
   }
 
+  public static final class SpeedRecord.Sample {
+    ctor public SpeedRecord.Sample(java.time.Instant time, androidx.health.connect.client.units.Velocity speed);
+    method public androidx.health.connect.client.units.Velocity getSpeed();
+    method public java.time.Instant getTime();
+    property public final androidx.health.connect.client.units.Velocity speed;
+    property public final java.time.Instant time;
+  }
+
   public final class StepsCadence {
     ctor public StepsCadence(java.time.Instant time, @FloatRange(from=0.0, to=10000.0) double rate);
     method public double getRate();
@@ -1556,6 +1556,29 @@
   public final class TemperatureKt {
   }
 
+  public final class Velocity implements java.lang.Comparable<androidx.health.connect.client.units.Velocity> {
+    method public int compareTo(androidx.health.connect.client.units.Velocity other);
+    method public double getKilometersPerHour();
+    method public double getMetersPerSecond();
+    method public double getMilesPerHour();
+    method public static androidx.health.connect.client.units.Velocity kilometersPerHour(double value);
+    method public static androidx.health.connect.client.units.Velocity metersPerSecond(double value);
+    method public static androidx.health.connect.client.units.Velocity milesPerHour(double value);
+    property public final double inKilometersPerHour;
+    property public final double inMetersPerSecond;
+    property public final double inMilesPerHour;
+    field public static final androidx.health.connect.client.units.Velocity.Companion Companion;
+  }
+
+  public static final class Velocity.Companion {
+    method public androidx.health.connect.client.units.Velocity kilometersPerHour(double value);
+    method public androidx.health.connect.client.units.Velocity metersPerSecond(double value);
+    method public androidx.health.connect.client.units.Velocity milesPerHour(double value);
+  }
+
+  public final class VelocityKt {
+  }
+
   public final class Volume implements java.lang.Comparable<androidx.health.connect.client.units.Volume> {
     method public int compareTo(androidx.health.connect.client.units.Volume other);
     method public double getLiters();
diff --git a/health/health-connect-client/api/public_plus_experimental_current.txt b/health/health-connect-client/api/public_plus_experimental_current.txt
index a4038e0..3a5df3a 100644
--- a/health/health-connect-client/api/public_plus_experimental_current.txt
+++ b/health/health-connect-client/api/public_plus_experimental_current.txt
@@ -1064,37 +1064,37 @@
     field public static final String UNKNOWN = "unknown";
   }
 
-  public final class Speed {
-    ctor public Speed(java.time.Instant time, @FloatRange(from=0.0, to=1000000.0) double metersPerSecond);
-    method public double getMetersPerSecond();
-    method public java.time.Instant getTime();
-    property public final double metersPerSecond;
-    property public final java.time.Instant time;
-  }
-
   public final class SpeedRecord implements androidx.health.connect.client.records.Record {
-    ctor public SpeedRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.Speed> samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public SpeedRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.SpeedRecord.Sample> samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.util.List<androidx.health.connect.client.records.Speed> getSamples();
+    method public java.util.List<androidx.health.connect.client.records.SpeedRecord.Sample> getSamples();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.util.List<androidx.health.connect.client.records.Speed> samples;
+    property public java.util.List<androidx.health.connect.client.records.SpeedRecord.Sample> samples;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
     field public static final androidx.health.connect.client.records.SpeedRecord.Companion Companion;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SPEED_AVG;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SPEED_MAX;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SPEED_MIN;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Velocity> SPEED_AVG;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Velocity> SPEED_MAX;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Velocity> SPEED_MIN;
   }
 
   public static final class SpeedRecord.Companion {
   }
 
+  public static final class SpeedRecord.Sample {
+    ctor public SpeedRecord.Sample(java.time.Instant time, androidx.health.connect.client.units.Velocity speed);
+    method public androidx.health.connect.client.units.Velocity getSpeed();
+    method public java.time.Instant getTime();
+    property public final androidx.health.connect.client.units.Velocity speed;
+    property public final java.time.Instant time;
+  }
+
   public final class StepsCadence {
     ctor public StepsCadence(java.time.Instant time, @FloatRange(from=0.0, to=10000.0) double rate);
     method public double getRate();
@@ -1556,6 +1556,29 @@
   public final class TemperatureKt {
   }
 
+  public final class Velocity implements java.lang.Comparable<androidx.health.connect.client.units.Velocity> {
+    method public int compareTo(androidx.health.connect.client.units.Velocity other);
+    method public double getKilometersPerHour();
+    method public double getMetersPerSecond();
+    method public double getMilesPerHour();
+    method public static androidx.health.connect.client.units.Velocity kilometersPerHour(double value);
+    method public static androidx.health.connect.client.units.Velocity metersPerSecond(double value);
+    method public static androidx.health.connect.client.units.Velocity milesPerHour(double value);
+    property public final double inKilometersPerHour;
+    property public final double inMetersPerSecond;
+    property public final double inMilesPerHour;
+    field public static final androidx.health.connect.client.units.Velocity.Companion Companion;
+  }
+
+  public static final class Velocity.Companion {
+    method public androidx.health.connect.client.units.Velocity kilometersPerHour(double value);
+    method public androidx.health.connect.client.units.Velocity metersPerSecond(double value);
+    method public androidx.health.connect.client.units.Velocity milesPerHour(double value);
+  }
+
+  public final class VelocityKt {
+  }
+
   public final class Volume implements java.lang.Comparable<androidx.health.connect.client.units.Volume> {
     method public int compareTo(androidx.health.connect.client.units.Volume other);
     method public double getLiters();
diff --git a/health/health-connect-client/api/restricted_current.txt b/health/health-connect-client/api/restricted_current.txt
index c3912ad..ed5ecdb 100644
--- a/health/health-connect-client/api/restricted_current.txt
+++ b/health/health-connect-client/api/restricted_current.txt
@@ -1087,37 +1087,37 @@
     field public static final String UNKNOWN = "unknown";
   }
 
-  public final class Speed {
-    ctor public Speed(java.time.Instant time, @FloatRange(from=0.0, to=1000000.0) double metersPerSecond);
-    method public double getMetersPerSecond();
-    method public java.time.Instant getTime();
-    property public final double metersPerSecond;
-    property public final java.time.Instant time;
-  }
-
-  public final class SpeedRecord implements androidx.health.connect.client.records.SeriesRecord<androidx.health.connect.client.records.Speed> {
-    ctor public SpeedRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.Speed> samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+  public final class SpeedRecord implements androidx.health.connect.client.records.SeriesRecord<androidx.health.connect.client.records.SpeedRecord.Sample> {
+    ctor public SpeedRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.SpeedRecord.Sample> samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.util.List<androidx.health.connect.client.records.Speed> getSamples();
+    method public java.util.List<androidx.health.connect.client.records.SpeedRecord.Sample> getSamples();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.util.List<androidx.health.connect.client.records.Speed> samples;
+    property public java.util.List<androidx.health.connect.client.records.SpeedRecord.Sample> samples;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
     field public static final androidx.health.connect.client.records.SpeedRecord.Companion Companion;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SPEED_AVG;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SPEED_MAX;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SPEED_MIN;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Velocity> SPEED_AVG;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Velocity> SPEED_MAX;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Velocity> SPEED_MIN;
   }
 
   public static final class SpeedRecord.Companion {
   }
 
+  public static final class SpeedRecord.Sample {
+    ctor public SpeedRecord.Sample(java.time.Instant time, androidx.health.connect.client.units.Velocity speed);
+    method public androidx.health.connect.client.units.Velocity getSpeed();
+    method public java.time.Instant getTime();
+    property public final androidx.health.connect.client.units.Velocity speed;
+    property public final java.time.Instant time;
+  }
+
   public final class StepsCadence {
     ctor public StepsCadence(java.time.Instant time, @FloatRange(from=0.0, to=10000.0) double rate);
     method public double getRate();
@@ -1579,6 +1579,29 @@
   public final class TemperatureKt {
   }
 
+  public final class Velocity implements java.lang.Comparable<androidx.health.connect.client.units.Velocity> {
+    method public int compareTo(androidx.health.connect.client.units.Velocity other);
+    method public double getKilometersPerHour();
+    method public double getMetersPerSecond();
+    method public double getMilesPerHour();
+    method public static androidx.health.connect.client.units.Velocity kilometersPerHour(double value);
+    method public static androidx.health.connect.client.units.Velocity metersPerSecond(double value);
+    method public static androidx.health.connect.client.units.Velocity milesPerHour(double value);
+    property public final double inKilometersPerHour;
+    property public final double inMetersPerSecond;
+    property public final double inMilesPerHour;
+    field public static final androidx.health.connect.client.units.Velocity.Companion Companion;
+  }
+
+  public static final class Velocity.Companion {
+    method public androidx.health.connect.client.units.Velocity kilometersPerHour(double value);
+    method public androidx.health.connect.client.units.Velocity metersPerSecond(double value);
+    method public androidx.health.connect.client.units.Velocity milesPerHour(double value);
+  }
+
+  public final class VelocityKt {
+  }
+
   public final class Volume implements java.lang.Comparable<androidx.health.connect.client.units.Volume> {
     method public int compareTo(androidx.health.connect.client.units.Volume other);
     method public double getLiters();
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
index aaf2034..f767c8b 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
@@ -63,7 +63,6 @@
 import androidx.health.connect.client.records.SexualActivityRecord
 import androidx.health.connect.client.records.SleepSessionRecord
 import androidx.health.connect.client.records.SleepStageRecord
-import androidx.health.connect.client.records.Speed
 import androidx.health.connect.client.records.SpeedRecord
 import androidx.health.connect.client.records.StepsCadence
 import androidx.health.connect.client.records.StepsCadenceRecord
@@ -81,6 +80,7 @@
 import androidx.health.connect.client.units.kilograms
 import androidx.health.connect.client.units.liters
 import androidx.health.connect.client.units.meters
+import androidx.health.connect.client.units.metersPerSecond
 import androidx.health.connect.client.units.millimetersOfMercury
 import androidx.health.connect.client.units.percent
 import androidx.health.connect.client.units.watts
@@ -342,9 +342,9 @@
                     endZoneOffset = endZoneOffset,
                     samples =
                         seriesValuesList.map { value ->
-                            Speed(
+                            SpeedRecord.Sample(
                                 time = Instant.ofEpochMilli(value.instantTimeMillis),
-                                metersPerSecond = value.getDouble("speed"),
+                                speed = value.getDouble("speed").metersPerSecond,
                             )
                         },
                     metadata = metadata,
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
index 7a76ce7..30199e8 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
@@ -254,7 +254,7 @@
         is SpeedRecord ->
             toProto(dataTypeName = "SpeedSeries") { sample ->
                 DataProto.SeriesValue.newBuilder()
-                    .putValues("speed", doubleVal(sample.metersPerSecond))
+                    .putValues("speed", doubleVal(sample.speed.inMetersPerSecond))
                     .setInstantTimeMillis(sample.time.toEpochMilli())
                     .build()
             }
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/SpeedRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/SpeedRecord.kt
index 4725ab0a..e14bc6f 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/SpeedRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/SpeedRecord.kt
@@ -15,9 +15,9 @@
  */
 package androidx.health.connect.client.records
 
-import androidx.annotation.FloatRange
 import androidx.health.connect.client.aggregate.AggregateMetric
 import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.units.Velocity
 import java.time.Instant
 import java.time.ZoneOffset
 
@@ -30,9 +30,9 @@
     override val startZoneOffset: ZoneOffset?,
     override val endTime: Instant,
     override val endZoneOffset: ZoneOffset?,
-    override val samples: List<Speed>,
+    override val samples: List<Sample>,
     override val metadata: Metadata = Metadata.EMPTY,
-) : SeriesRecord<Speed> {
+) : SeriesRecord<SpeedRecord.Sample> {
 
     /*
      * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
@@ -73,11 +73,12 @@
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val SPEED_AVG: AggregateMetric<Double> =
+        val SPEED_AVG: AggregateMetric<Velocity> =
             AggregateMetric.doubleMetric(
-                SPEED_TYPE_NAME,
-                AggregateMetric.AggregationType.AVERAGE,
-                SPEED_FIELD_NAME
+                dataTypeName = SPEED_TYPE_NAME,
+                aggregationType = AggregateMetric.AggregationType.AVERAGE,
+                fieldName = SPEED_FIELD_NAME,
+                mapper = Velocity::metersPerSecond,
             )
 
         /**
@@ -85,11 +86,12 @@
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val SPEED_MIN: AggregateMetric<Double> =
+        val SPEED_MIN: AggregateMetric<Velocity> =
             AggregateMetric.doubleMetric(
-                SPEED_TYPE_NAME,
-                AggregateMetric.AggregationType.MINIMUM,
-                SPEED_FIELD_NAME
+                dataTypeName = SPEED_TYPE_NAME,
+                aggregationType = AggregateMetric.AggregationType.MINIMUM,
+                fieldName = SPEED_FIELD_NAME,
+                mapper = Velocity::metersPerSecond,
             )
 
         /**
@@ -97,51 +99,46 @@
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val SPEED_MAX: AggregateMetric<Double> =
+        val SPEED_MAX: AggregateMetric<Velocity> =
             AggregateMetric.doubleMetric(
-                SPEED_TYPE_NAME,
-                AggregateMetric.AggregationType.MAXIMUM,
-                SPEED_FIELD_NAME
+                dataTypeName = SPEED_TYPE_NAME,
+                aggregationType = AggregateMetric.AggregationType.MAXIMUM,
+                fieldName = SPEED_FIELD_NAME,
+                mapper = Velocity::metersPerSecond,
             )
     }
-}
 
-/**
- * Represents a single measurement of the speed, a scalar magnitude.
- *
- * @param time The point in time when the measurement was taken.
- * @param metersPerSecond Speed in meters per second. Valid range: 0-1000000.
- *
- * @see SpeedRecord
- */
-public class Speed(
-    val time: Instant,
-    @FloatRange(from = 0.0, to = 1_000_000.0) val metersPerSecond: Double,
-) {
-
-    init {
-        requireNonNegative(value = metersPerSecond, name = "metersPerSecond")
-    }
-
-    /*
-     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+    /**
+     * Represents a single measurement of the speed, a scalar magnitude.
+     *
+     * @param time The point in time when the measurement was taken.
+     * @param speed Speed in [Velocity] unit. Valid range: 0-1000000 meters/sec.
+     *
+     * @see SpeedRecord
      */
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is Speed) return false
+    public class Sample(
+        val time: Instant,
+        val speed: Velocity,
+    ) {
 
-        if (time != other.time) return false
-        if (metersPerSecond != other.metersPerSecond) return false
+        init {
+            speed.requireNotLess(other = speed.zero(), name = "speed")
+        }
 
-        return true
-    }
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is Sample) return false
 
-    /*
-     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
-     */
-    override fun hashCode(): Int {
-        var result = time.hashCode()
-        result = 31 * result + metersPerSecond.hashCode()
-        return result
+            if (time != other.time) return false
+            if (speed != other.speed) return false
+
+            return true
+        }
+
+        override fun hashCode(): Int {
+            var result = time.hashCode()
+            result = 31 * result + speed.hashCode()
+            return result
+        }
     }
 }
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/units/Velocity.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/units/Velocity.kt
new file mode 100644
index 0000000..9a15e36
--- /dev/null
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/units/Velocity.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.units
+
+/**
+ * Represents a unit of speed. Supported units:
+ *
+ * - metersPerSecond - see [Velocity.metersPerSecond], [Double.metersPerSecond]
+ * - kilometersPerHour - see [Velocity.kilometersPerHour], [Double.kilometersPerHour]
+ * - milesPerHour - see [Velocity.milesPerHour], [Double.milesPerHour]
+ */
+class Velocity private constructor(
+    private val value: Double,
+    private val type: Type,
+) : Comparable<Velocity> {
+
+    /** Returns the velocity in meters per second. */
+    @get:JvmName("getMetersPerSecond")
+    val inMetersPerSecond: Double
+        get() = value * type.metersPerSecondPerUnit
+
+    /** Returns the velocity in kilometers per hour. */
+    @get:JvmName("getKilometersPerHour")
+    val inKilometersPerHour: Double
+        get() = get(type = Type.KILOMETERS_PER_HOUR)
+
+    /** Returns the velocity in miles per hour. */
+    @get:JvmName("getMilesPerHour")
+    val inMilesPerHour: Double
+        get() = get(type = Type.MILES_PER_HOUR)
+
+    private fun get(type: Type): Double =
+        if (this.type == type) value else inMetersPerSecond / type.metersPerSecondPerUnit
+
+    /** Returns zero [Velocity] of the same [Type]. */
+    internal fun zero(): Velocity = ZEROS.getValue(type)
+
+    override fun compareTo(other: Velocity): Int =
+        if (type == other.type) {
+            value.compareTo(other.value)
+        } else {
+            inMetersPerSecond.compareTo(other.inMetersPerSecond)
+        }
+
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Velocity) return false
+
+        if (value != other.value) return false
+        if (type != other.type) return false
+
+        return true
+    }
+
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
+    override fun hashCode(): Int {
+        var result = value.hashCode()
+        result = 31 * result + type.hashCode()
+        return result
+    }
+
+    override fun toString(): String = "$value ${type.title}"
+
+    companion object {
+        private val ZEROS = Type.values().associateWith { Velocity(value = 0.0, type = it) }
+
+        /** Creates [Velocity] with the specified value in meters per second. */
+        @JvmStatic
+        fun metersPerSecond(value: Double): Velocity = Velocity(value, Type.METERS_PER_SECOND)
+
+        /** Creates [Velocity] with the specified value in kilometers per hour. */
+        @JvmStatic
+        fun kilometersPerHour(value: Double): Velocity = Velocity(value, Type.KILOMETERS_PER_HOUR)
+
+        /** Creates [Velocity] with the specified value in miles per hour. */
+        @JvmStatic
+        fun milesPerHour(value: Double): Velocity = Velocity(value, Type.MILES_PER_HOUR)
+    }
+
+    private enum class Type {
+        METERS_PER_SECOND {
+            override val metersPerSecondPerUnit: Double = 1.0
+            override val title: String = "meters/sec"
+        },
+        KILOMETERS_PER_HOUR {
+            override val metersPerSecondPerUnit: Double = 1.0 / 3.6
+            override val title: String = "km/h"
+        },
+        MILES_PER_HOUR {
+            override val metersPerSecondPerUnit: Double = 0.447040357632
+            override val title: String = "miles/h"
+        };
+
+        abstract val metersPerSecondPerUnit: Double
+        abstract val title: String
+    }
+}
+
+/** Creates [Velocity] with the specified value in meters per second. */
+@get:JvmSynthetic
+val Double.metersPerSecond: Velocity
+    get() = Velocity.metersPerSecond(value = this)
+
+/** Creates [Velocity] with the specified value in meters per second. */
+@get:JvmSynthetic
+val Long.metersPerSecond: Velocity
+    get() = toDouble().metersPerSecond
+
+/** Creates [Velocity] with the specified value in meters per second. */
+@get:JvmSynthetic
+val Float.metersPerSecond: Velocity
+    get() = toDouble().metersPerSecond
+
+/** Creates [Velocity] with the specified value in meters per second. */
+@get:JvmSynthetic
+val Int.metersPerSecond: Velocity
+    get() = toDouble().metersPerSecond
+
+/** Creates [Velocity] with the specified value in kilometers per hour. */
+@get:JvmSynthetic
+val Double.kilometersPerHour: Velocity
+    get() = Velocity.kilometersPerHour(value = this)
+
+/** Creates [Velocity] with the specified value in kilometers per hour. */
+@get:JvmSynthetic
+val Long.kilometersPerHour: Velocity
+    get() = toDouble().kilometersPerHour
+
+/** Creates [Velocity] with the specified value in kilometers per hour. */
+@get:JvmSynthetic
+val Float.kilometersPerHour: Velocity
+    get() = toDouble().kilometersPerHour
+
+/** Creates [Velocity] with the specified value in kilometers per hour. */
+@get:JvmSynthetic
+val Int.kilometersPerHour: Velocity
+    get() = toDouble().kilometersPerHour
+
+/** Creates [Velocity] with the specified value in miles per hour. */
+@get:JvmSynthetic
+val Double.milesPerHour: Velocity
+    get() = Velocity.milesPerHour(value = this)
+
+/** Creates [Velocity] with the specified value in miles per hour. */
+@get:JvmSynthetic
+val Long.milesPerHour: Velocity
+    get() = toDouble().milesPerHour
+
+/** Creates [Velocity] with the specified value in miles per hour. */
+@get:JvmSynthetic
+val Float.milesPerHour: Velocity
+    get() = toDouble().milesPerHour
+
+/** Creates [Velocity] with the specified value in miles per hour. */
+@get:JvmSynthetic
+val Int.milesPerHour: Velocity
+    get() = toDouble().milesPerHour
diff --git a/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt b/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
index 4e9203a..2679086d 100644
--- a/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
+++ b/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
@@ -69,7 +69,6 @@
 import androidx.health.connect.client.records.SleepSessionRecord
 import androidx.health.connect.client.records.SleepStageRecord
 import androidx.health.connect.client.records.SleepStageRecord.StageType
-import androidx.health.connect.client.records.Speed
 import androidx.health.connect.client.records.SpeedRecord
 import androidx.health.connect.client.records.StepsCadence
 import androidx.health.connect.client.records.StepsCadenceRecord
@@ -91,6 +90,7 @@
 import androidx.health.connect.client.units.kilograms
 import androidx.health.connect.client.units.liters
 import androidx.health.connect.client.units.meters
+import androidx.health.connect.client.units.metersPerSecond
 import androidx.health.connect.client.units.millimetersOfMercury
 import androidx.health.connect.client.units.percent
 import androidx.health.connect.client.units.watts
@@ -611,13 +611,13 @@
                 endZoneOffset = END_ZONE_OFFSET,
                 samples =
                     listOf(
-                        Speed(
+                        SpeedRecord.Sample(
                             time = START_TIME,
-                            metersPerSecond = 1.0,
+                            speed = 1.metersPerSecond,
                         ),
-                        Speed(
+                        SpeedRecord.Sample(
                             time = START_TIME,
-                            metersPerSecond = 2.0,
+                            speed = 2.metersPerSecond,
                         ),
                     ),
                 metadata = TEST_METADATA,
diff --git a/lint-checks/src/main/java/androidx/build/lint/BanConcurrentHashMap.kt b/lint-checks/src/main/java/androidx/build/lint/BanConcurrentHashMap.kt
index b5529a3..41191a3 100644
--- a/lint-checks/src/main/java/androidx/build/lint/BanConcurrentHashMap.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/BanConcurrentHashMap.kt
@@ -32,7 +32,6 @@
 import org.jetbrains.uast.UElement
 import org.jetbrains.uast.UImportStatement
 import org.jetbrains.uast.UQualifiedReferenceExpression
-import org.jetbrains.uast.USimpleNameReferenceExpression
 
 class BanConcurrentHashMap : Detector(), Detector.UastScanner {
 
@@ -44,41 +43,62 @@
     override fun createUastHandler(context: JavaContext): UElementHandler = object :
         UElementHandler() {
 
-        // Detect fully qualified reference if not imported.
+        /**
+         * Detect map construction using fully qualified reference if not imported.
+         * This specifically flags the constructor, and not usages of the map after it is created.
+         */
         override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression) {
-            if (node.selector is USimpleNameReferenceExpression) {
-                val name = node.selector as USimpleNameReferenceExpression
-                if (CONCURRENT_HASHMAP == name.identifier) {
-                    val incident = Incident(context)
-                        .issue(ISSUE)
-                        .location(context.getLocation(node))
-                        .message("Detected ConcurrentHashMap usage.")
-                        .scope(node)
-                    context.report(incident)
+            val resolved = node.resolve()
+            // In Kotlin, the resolved node will be a method with name ConcurrentHashMap
+            // In Java, it will be the class itself
+            if ((resolved is PsiMethod && resolved.isConcurrentHashMapConstructor()) ||
+                (resolved is PsiClass && resolved.isConcurrentHashMap())) {
+                reportIncidentForNode(node)
+            }
+        }
+
+        /**
+         * Detect import.
+         */
+        override fun visitImportStatement(node: UImportStatement) {
+            if (node.importReference != null) {
+                var resolved = node.resolve()
+                if (resolved is PsiField) {
+                    resolved = resolved.containingClass
+                } else if (resolved is PsiMethod) {
+                    resolved = resolved.containingClass
+                }
+
+                if (resolved is PsiClass && resolved.isConcurrentHashMap()) {
+                    reportIncidentForNode(node)
                 }
             }
         }
 
-        // Detect import.
-        override fun visitImportStatement(node: UImportStatement) {
-            if (node.importReference != null) {
-                var resolved = node.resolve()
-                if (node.resolve() is PsiField) {
-                    resolved = (resolved as PsiField).containingClass
-                } else if (resolved is PsiMethod) {
-                    resolved = resolved.containingClass
-                }
-                if (resolved is PsiClass &&
-                    CONCURRENT_HASHMAP_QUALIFIED_NAME == resolved.qualifiedName
-                ) {
-                    val incident = Incident(context)
-                        .issue(ISSUE)
-                        .location(context.getLocation(node))
-                        .message("Detected ConcurrentHashMap usage.")
-                        .scope(node)
-                    context.report(incident)
-                }
-            }
+        /**
+         * Reports an error for ConcurrentHashMap usage at the node's location.
+         */
+        private fun reportIncidentForNode(node: UElement) {
+            val incident = Incident(context)
+                .issue(ISSUE)
+                .location(context.getLocation(node))
+                .message("Detected ConcurrentHashMap usage.")
+                .scope(node)
+            context.report(incident)
+        }
+
+        /**
+         * Check if the method is the constructor for ConcurrentHashMap (applicable for Kotlin).
+         */
+        private fun PsiMethod.isConcurrentHashMapConstructor(): Boolean {
+            return name == CONCURRENT_HASHMAP && (containingClass?.isConcurrentHashMap() ?: false)
+        }
+
+        /**
+         * Checks if the class is ConcurrentHashMap.
+         */
+        private fun PsiClass.isConcurrentHashMap(): Boolean {
+            return qualifiedName == CONCURRENT_HASHMAP_QUALIFIED_NAME
         }
     }
 
diff --git a/lint-checks/src/test/java/androidx/build/lint/BanConcurrentHashMapTest.kt b/lint-checks/src/test/java/androidx/build/lint/BanConcurrentHashMapTest.kt
index bb9acbb..cc2edd6 100644
--- a/lint-checks/src/test/java/androidx/build/lint/BanConcurrentHashMapTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/BanConcurrentHashMapTest.kt
@@ -30,19 +30,17 @@
 ) {
 
     @Test
-    fun `Detection of ConcurrentHashMap usage in Java sources`() {
+    fun `Detection of ConcurrentHashMap import in Java sources`() {
         val input = java(
-            "src/androidx/ConcurrentHashMapUsageJava.java",
+            "src/androidx/ConcurrentHashMapImportJava.java",
             """
                 import androidx.annotation.NonNull;
                 import java.util.Map;
                 import java.util.concurrent.ConcurrentHashMap;
 
                 public class ConcurrentHashMapUsageJava {
+                    private final Map<?, ?> mMap = new ConcurrentHashMap<>();
 
-                    private final ConcurrentHashMap<?, ?> mMap = new ConcurrentHashMap<>();
-
-                    @NonNull
                     public <V, K> Map<V, K> createMap() {
                         return new ConcurrentHashMap<>();
                     }
@@ -52,7 +50,7 @@
 
         /* ktlint-disable max-line-length */
         val expected = """
-src/androidx/ConcurrentHashMapUsageJava.java:3: Error: Detected ConcurrentHashMap usage. [BanConcurrentHashMap]
+src/androidx/ConcurrentHashMapImportJava.java:3: Error: Detected ConcurrentHashMap usage. [BanConcurrentHashMap]
 import java.util.concurrent.ConcurrentHashMap;
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 1 errors, 0 warnings
@@ -63,17 +61,50 @@
     }
 
     @Test
-    fun `Detection of ConcurrentHashMap usage in Kotlin sources`() {
+    fun `Detection of ConcurrentHashMap fully-qualified usage in Java sources`() {
+        val input = java(
+            "src/androidx/ConcurrentHashMapUsageJava.java",
+            """
+                import androidx.annotation.NonNull;
+                import java.util.Map;
+
+                public class ConcurrentHashMapUsageJava {
+                    private final Map<?, ?> mMap = new java.util.concurrent.ConcurrentHashMap<>();
+
+                    public <V, K> Map<V, K> createMap() {
+                        return new java.util.concurrent.ConcurrentHashMap<>();
+                    }
+                }
+            """.trimIndent()
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+src/androidx/ConcurrentHashMapUsageJava.java:5: Error: Detected ConcurrentHashMap usage. [BanConcurrentHashMap]
+    private final Map<?, ?> mMap = new java.util.concurrent.ConcurrentHashMap<>();
+                                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+src/androidx/ConcurrentHashMapUsageJava.java:8: Error: Detected ConcurrentHashMap usage. [BanConcurrentHashMap]
+        return new java.util.concurrent.ConcurrentHashMap<>();
+                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+2 errors, 0 warnings
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+        check(input).expect(expected)
+    }
+
+    @Test
+    fun `Detection of ConcurrentHashMap import in Kotlin sources`() {
         val input = kotlin(
-            "src/androidx/ConcurrentHashMapUsageKotlin.kt",
+            "src/androidx/ConcurrentHashMapImportKotlin.kt",
             """
                 package androidx
 
                 import java.util.concurrent.ConcurrentHashMap
 
-                @Suppress("unused")
                 class ConcurrentHashMapUsageKotlin {
                     private val mMap: ConcurrentHashMap<*, *> = ConcurrentHashMap<Any, Any>()
+
                     fun <V, K> createMap(): Map<V, K> {
                         return ConcurrentHashMap()
                     }
@@ -83,7 +114,7 @@
 
         /* ktlint-disable max-line-length */
         val expected = """
-src/androidx/ConcurrentHashMapUsageKotlin.kt:3: Error: Detected ConcurrentHashMap usage. [BanConcurrentHashMap]
+src/androidx/ConcurrentHashMapImportKotlin.kt:3: Error: Detected ConcurrentHashMap usage. [BanConcurrentHashMap]
 import java.util.concurrent.ConcurrentHashMap
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 1 errors, 0 warnings
@@ -95,7 +126,88 @@
                 *stubs,
                 input
             )
-            .skipTestModes(TestMode.IMPORT_ALIAS) // b/203124716
+            // This fails in IMPORT_ALIAS mode because changing the import line changes the error.
+            // It fails in FULLY_QUALIFIED mode because more errors occur when the fully-qualified
+            // class is used. These cases are tested separately.
+            .skipTestModes(TestMode.IMPORT_ALIAS, TestMode.FULLY_QUALIFIED)
+            .run()
+            .expect(expected)
+    }
+
+    @Test
+    fun `Detection of ConcurrentHashMap fully-qualified usage in Kotlin sources`() {
+        val input = kotlin(
+            "src/androidx/ConcurrentHashMapUsageKotlin.kt",
+            """
+                package androidx
+
+                class ConcurrentHashMapUsageKotlin {
+                    private val mMap: Map<*, *> = java.util.concurrent.ConcurrentHashMap<Any, Any>()
+
+                    fun <V, K> createMap(): Map<V, K> {
+                        return java.util.concurrent.ConcurrentHashMap()
+                    }
+                }
+            """.trimIndent()
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+src/androidx/ConcurrentHashMapUsageKotlin.kt:4: Error: Detected ConcurrentHashMap usage. [BanConcurrentHashMap]
+    private val mMap: Map<*, *> = java.util.concurrent.ConcurrentHashMap<Any, Any>()
+                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+src/androidx/ConcurrentHashMapUsageKotlin.kt:7: Error: Detected ConcurrentHashMap usage. [BanConcurrentHashMap]
+        return java.util.concurrent.ConcurrentHashMap()
+               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+2 errors, 0 warnings
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+        lint()
+            .files(
+                *stubs,
+                input
+            )
+            .run()
+            .expect(expected)
+    }
+
+    @Test
+    fun `Detection of ConcurrentHashMap import alias in Kotlin sources`() {
+        val input = kotlin(
+            "src/androidx/ConcurrentHashMapUsageAliasKotlin.kt",
+            """
+                package androidx
+
+                import java.util.concurrent.ConcurrentHashMap as NewClassName
+
+                class ConcurrentHashMapUsageAliasKotlin {
+                    private val mMap: Map<*, *> = NewClassName<Any, Any>()
+
+                    fun <V, K> createMap(): Map<V, K> {
+                        return NewClassName()
+                    }
+                }
+            """.trimIndent()
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+src/androidx/ConcurrentHashMapUsageAliasKotlin.kt:3: Error: Detected ConcurrentHashMap usage. [BanConcurrentHashMap]
+import java.util.concurrent.ConcurrentHashMap as NewClassName
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1 errors, 0 warnings
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+        lint()
+            .files(
+                *stubs,
+                input
+            )
+            // In FULLY_QUALIFIED test mode, more errors occur when the fully-qualified class is
+            // used. This case is tested separately.
+            .skipTestModes(TestMode.FULLY_QUALIFIED)
             .run()
             .expect(expected)
     }
diff --git a/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/ui/DefaultProgressFragmentTest.kt b/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/ui/DefaultProgressFragmentTest.kt
index 39dcd18..57d781b 100644
--- a/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/ui/DefaultProgressFragmentTest.kt
+++ b/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/ui/DefaultProgressFragmentTest.kt
@@ -19,6 +19,7 @@
 import androidx.navigation.dynamicfeatures.fragment.R as mainR
 import androidx.navigation.dynamicfeatures.fragment.test.R as testR
 import android.widget.TextView
+import androidx.lifecycle.Observer
 import androidx.lifecycle.ViewModelProvider
 import androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment
 import androidx.navigation.dynamicfeatures.fragment.NavigationActivity
@@ -27,6 +28,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.testutils.withActivity
+import com.google.android.play.core.splitinstall.SplitInstallSessionState
 import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.CountDownLatch
@@ -68,11 +70,16 @@
                 // the test to wait for the failure signal from the splitInstall session. To do that
                 // we observe the livedata of the DefaultProgressFragment's viewModel, and wait for
                 // it to fail before we check for test failure.
-                viewModel.installMonitor!!.status.observe(defaultProgressFragment) {
-                    if (it.status() == SplitInstallSessionStatus.FAILED) {
-                        failureCountdownLatch.countDown()
+                val liveData = viewModel.installMonitor!!.status
+                val observer = object : Observer<SplitInstallSessionState> {
+                    override fun onChanged(state: SplitInstallSessionState) {
+                        if (state.status() == SplitInstallSessionStatus.FAILED) {
+                            liveData.removeObserver(this)
+                            failureCountdownLatch.countDown()
+                        }
                     }
                 }
+                liveData.observe(defaultProgressFragment, observer)
             }
 
             assertThat(failureCountdownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspJvmTypeResolver.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspJvmTypeResolver.kt
index 39de2f1..489f6fa 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspJvmTypeResolver.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspJvmTypeResolver.kt
@@ -45,13 +45,19 @@
             KSTypeVarianceResolver.WildcardMode.PREFERRED
         }
 
-        // use the jvm type of the declaration so that it also gets its jvm wildcards resolved.
-        val declarationJvmType = (scope.findDeclarationType() as? KspType)?.jvmWildcardTypeOrSelf
-
+        val declarationType = scope.findDeclarationType() as? KspType
         return env.resolveWildcards(
             ksType = delegate.ksType,
             wildcardMode = wildcardMode,
-            declarationType = declarationJvmType?.ksType
+            // See KSTypeVarianceResolver#applyTypeVariance: "If the ksType is from the original
+            // declaration, declarationType should be null".
+            declarationType = if (declarationType == delegate) {
+                null
+            } else {
+                // use the jvm type of the declaration so that it also gets its jvm wildcards
+                // resolved.
+                declarationType?.jvmWildcardTypeOrSelf?.ksType
+            }
         ).let {
             env.wrap(
                 ksType = it,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt
index f0186ad..b833951 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt
@@ -93,15 +93,16 @@
      * If this method is declared in the containing class (or in a file), it will be null.
      */
     val declarationMethodType: XMethodType? by lazy {
-        val declaredIn = declaration.closestClassDeclaration()
-        if (declaredIn == null || declaredIn == containing.declaration) {
-            null
-        } else {
-            create(
-                env = env,
-                containing = env.wrapClassDeclaration(declaredIn),
-                declaration = declaration
-            ).executableType
+        declaration.closestClassDeclaration()?.let { declaredIn ->
+            if (declaredIn == containing.declaration) {
+                executableType
+            } else {
+                create(
+                    env = env,
+                    containing = env.wrapClassDeclaration(declaredIn),
+                    declaration = declaration
+                ).executableType
+            }
         }
     }
 
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TypeInheritanceTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TypeInheritanceTest.kt
index 4a0cb98..9599c6c 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TypeInheritanceTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TypeInheritanceTest.kt
@@ -73,19 +73,18 @@
     private fun XTestInvocation.assertParamType(
         methodName: String,
         paramName: String,
-        subExpectedTypeName: String,
-        baseExpectedTypeName: String = subExpectedTypeName
+        expectedTypeName: String,
     ) {
         val sub = processingEnv.requireTypeElement("SubClass")
         val subMethod = sub.getMethodByJvmName(methodName)
         val subParam = subMethod.getParameter(paramName)
-        assertThat(subParam.type.typeName.toString()).isEqualTo(subExpectedTypeName)
+        assertThat(subParam.type.typeName.toString()).isEqualTo(expectedTypeName)
 
         val base = processingEnv.requireTypeElement("BaseClass")
         val baseMethod = base.getMethodByJvmName(methodName).asMemberOf(sub.type)
         val paramIndex = subMethod.parameters.indexOf(subParam)
         assertThat(baseMethod.parameterTypes[paramIndex].typeName.toString())
-            .isEqualTo(baseExpectedTypeName)
+            .isEqualTo(expectedTypeName)
     }
 
     private fun XTestInvocation.assertReturnType(methodName: String, expectedTypeName: String) {
@@ -288,6 +287,7 @@
             invocation.assertFieldType("varFieldT1", "Foo<Bar<Baz>>")
             invocation.assertParamType("method", "param", "Foo<Bar<Baz>>")
             invocation.assertParamType("method", "paramT1", "Foo<Bar<Baz>>")
+            invocation.assertParamType("method", "paramT2", "Foo<? extends Bar<Baz>>")
             invocation.assertReturnType("methodReturn", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturnT1", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturnT2", "Foo<Bar<Baz>>")
@@ -295,12 +295,8 @@
             // TODO(b/237280547): Make KSP type name match KAPT.
             if (invocation.isKsp) {
                 invocation.assertFieldType("varFieldT2", "Foo<Bar<Baz>>")
-                invocation.assertParamType(
-                    "method", "paramT2", "Foo<? extends Bar<Baz>>", "Foo<Bar<Baz>>"
-                )
             } else {
                 invocation.assertFieldType("varFieldT2", "Foo<? extends Bar<Baz>>")
-                invocation.assertParamType("method", "paramT2", "Foo<? extends Bar<Baz>>")
             }
         }
     }
@@ -319,6 +315,7 @@
             invocation.assertFieldType("varFieldT1", "Foo<Bar<Baz>>")
             invocation.assertParamType("method", "param", "Foo<Bar<Baz>>")
             invocation.assertParamType("method", "paramT1", "Foo<Bar<Baz>>")
+            invocation.assertParamType("method", "paramT2", "Foo<? extends Bar<Baz>>")
             invocation.assertReturnType("methodReturn", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturnT1", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturnT2", "Foo<Bar<Baz>>")
@@ -326,12 +323,8 @@
             // TODO(b/237280547): Make KSP type name match KAPT.
             if (invocation.isKsp) {
                 invocation.assertFieldType("varFieldT2", "Foo<Bar<Baz>>")
-                invocation.assertParamType(
-                    "method", "paramT2", "Foo<? extends Bar<Baz>>", "Foo<Bar<Baz>>"
-                )
             } else {
                 invocation.assertFieldType("varFieldT2", "Foo<? extends Bar<Baz>>")
-                invocation.assertParamType("method", "paramT2", "Foo<? extends Bar<Baz>>")
             }
         }
     }
@@ -380,6 +373,7 @@
             invocation.assertFieldType("varFieldT1", "Foo<Bar<Baz>>")
             invocation.assertParamType("method", "param", "Foo<Bar<Baz>>")
             invocation.assertParamType("method", "paramT1", "Foo<Bar<Baz>>")
+            invocation.assertParamType("method", "paramT2", "Foo<? extends Bar<Baz>>")
             invocation.assertReturnType("methodReturn", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturnT1", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturnT2", "Foo<Bar<Baz>>")
@@ -387,12 +381,8 @@
             // TODO(b/237280547): Make KSP type name match KAPT.
             if (invocation.isKsp) {
                 invocation.assertFieldType("varFieldT2", "Foo<Bar<Baz>>")
-                invocation.assertParamType(
-                    "method", "paramT2", "Foo<? extends Bar<Baz>>", "Foo<Bar<Baz>>"
-                )
             } else {
                 invocation.assertFieldType("varFieldT2", "Foo<? extends Bar<Baz>>")
-                invocation.assertParamType("method", "paramT2", "Foo<? extends Bar<Baz>>")
             }
         }
     }
@@ -441,6 +431,7 @@
             invocation.assertFieldType("varFieldT1", "Foo<Bar<Baz>>")
             invocation.assertParamType("method", "param", "Foo<Bar<Baz>>")
             invocation.assertParamType("method", "paramT1", "Foo<Bar<Baz>>")
+            invocation.assertParamType("method", "paramT2", "Foo<? extends Bar<Baz>>")
             invocation.assertReturnType("methodReturn", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturnT1", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturnT2", "Foo<Bar<Baz>>")
@@ -448,12 +439,8 @@
             // TODO(b/237280547): Make KSP type name match KAPT.
             if (invocation.isKsp) {
                 invocation.assertFieldType("varFieldT2", "Foo<Bar<Baz>>")
-                invocation.assertParamType(
-                    "method", "paramT2", "Foo<? extends Bar<Baz>>", "Foo<Bar<Baz>>"
-                )
             } else {
                 invocation.assertFieldType("varFieldT2", "Foo<? extends Bar<Baz>>")
-                invocation.assertParamType("method", "paramT2", "Foo<? extends Bar<Baz>>")
             }
         }
     }
@@ -530,6 +517,7 @@
             invocation.assertFieldType("varField", "Foo<Bar<Baz>>")
             invocation.assertFieldType("varFieldT1", "Foo<Bar<Baz>>")
             invocation.assertParamType("method", "param", "Foo<Bar<Baz>>")
+            invocation.assertParamType("method", "paramT2", "Foo<Bar<? extends Baz>>")
             invocation.assertReturnType("methodReturn", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturnT1", "Foo<Bar<Baz>>")
 
@@ -537,18 +525,12 @@
             if (invocation.isKsp) {
                 invocation.assertFieldType("valFieldT2", "Foo<Bar<Baz>>")
                 invocation.assertFieldType("varFieldT2", "Foo<Bar<Baz>>")
-                invocation.assertParamType(
-                    "method", "paramT1", "Foo<Bar<? extends Baz>>", "Foo<Bar<Baz>>"
-                )
-                invocation.assertParamType(
-                    "method", "paramT2", "Foo<Bar<? extends Baz>>", "Foo<Bar<Baz>>"
-                )
+                invocation.assertParamType("method", "paramT1", "Foo<Bar<? extends Baz>>")
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<Baz>>")
             } else {
                 invocation.assertFieldType("valFieldT2", "Foo<Bar<? extends Baz>>")
                 invocation.assertFieldType("varFieldT2", "Foo<Bar<? extends Baz>>")
                 invocation.assertParamType("method", "paramT1", "Foo<Bar<Baz>>")
-                invocation.assertParamType("method", "paramT2", "Foo<Bar<? extends Baz>>")
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<? extends Baz>>")
             }
         }
@@ -566,6 +548,7 @@
             invocation.assertFieldType("varField", "Foo<Bar<Baz>>")
             invocation.assertFieldType("varFieldT1", "Foo<Bar<Baz>>")
             invocation.assertParamType("method", "param", "Foo<Bar<Baz>>")
+            invocation.assertParamType("method", "paramT2", "Foo<Bar<? extends Baz>>")
             invocation.assertReturnType("methodReturn", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturnT1", "Foo<Bar<Baz>>")
 
@@ -573,18 +556,12 @@
             if (invocation.isKsp) {
                 invocation.assertFieldType("valFieldT2", "Foo<Bar<Baz>>")
                 invocation.assertFieldType("varFieldT2", "Foo<Bar<Baz>>")
-                invocation.assertParamType(
-                    "method", "paramT1", "Foo<Bar<? extends Baz>>", "Foo<Bar<Baz>>"
-                )
-                invocation.assertParamType(
-                    "method", "paramT2", "Foo<Bar<? extends Baz>>", "Foo<Bar<Baz>>"
-                )
+                invocation.assertParamType("method", "paramT1", "Foo<Bar<? extends Baz>>")
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<Baz>>")
             } else {
                 invocation.assertFieldType("valFieldT2", "Foo<Bar<? extends Baz>>")
                 invocation.assertFieldType("varFieldT2", "Foo<Bar<? extends Baz>>")
                 invocation.assertParamType("method", "paramT1", "Foo<Bar<Baz>>")
-                invocation.assertParamType("method", "paramT2", "Foo<Bar<? extends Baz>>")
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<? extends Baz>>")
             }
         }
@@ -602,6 +579,7 @@
             invocation.assertFieldType("varField", "Foo<Bar<Baz>>")
             invocation.assertFieldType("varFieldT1", "Foo<Bar<Baz>>")
             invocation.assertParamType("method", "param", "Foo<Bar<Baz>>")
+            invocation.assertParamType("method", "paramT2", "Foo<Bar<? extends Baz>>")
             invocation.assertReturnType("methodReturn", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturnT1", "Foo<Bar<Baz>>")
 
@@ -609,18 +587,12 @@
             if (invocation.isKsp) {
                 invocation.assertFieldType("valFieldT2", "Foo<Bar<Baz>>")
                 invocation.assertFieldType("varFieldT2", "Foo<Bar<Baz>>")
-                invocation.assertParamType(
-                    "method", "paramT1", "Foo<Bar<? extends Baz>>", "Foo<Bar<Baz>>"
-                )
-                invocation.assertParamType(
-                    "method", "paramT2", "Foo<Bar<? extends Baz>>", "Foo<Bar<Baz>>"
-                )
+                invocation.assertParamType("method", "paramT1", "Foo<Bar<? extends Baz>>")
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<Baz>>")
             } else {
                 invocation.assertFieldType("valFieldT2", "Foo<Bar<? extends Baz>>")
                 invocation.assertFieldType("varFieldT2", "Foo<Bar<? extends Baz>>")
                 invocation.assertParamType("method", "paramT1", "Foo<Bar<Baz>>")
-                invocation.assertParamType("method", "paramT2", "Foo<Bar<? extends Baz>>")
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<? extends Baz>>")
             }
         }
@@ -670,6 +642,7 @@
             invocation.assertFieldType("varField", "Foo<Bar<Baz>>")
             invocation.assertFieldType("varFieldT1", "Foo<Bar<Baz>>")
             invocation.assertParamType("method", "param", "Foo<Bar<Baz>>")
+            invocation.assertParamType("method", "paramT2", "Foo<Bar<? extends Baz>>")
             invocation.assertReturnType("methodReturn", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturnT1", "Foo<Bar<Baz>>")
 
@@ -677,18 +650,12 @@
             if (invocation.isKsp) {
                 invocation.assertFieldType("valFieldT2", "Foo<Bar<Baz>>")
                 invocation.assertFieldType("varFieldT2", "Foo<Bar<Baz>>")
-                invocation.assertParamType(
-                    "method", "paramT1", "Foo<Bar<? extends Baz>>", "Foo<Bar<Baz>>"
-                )
-                invocation.assertParamType(
-                    "method", "paramT2", "Foo<Bar<? extends Baz>>", "Foo<Bar<Baz>>"
-                )
+                invocation.assertParamType("method", "paramT1", "Foo<Bar<? extends Baz>>")
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<Baz>>")
             } else {
                 invocation.assertFieldType("valFieldT2", "Foo<Bar<? extends Baz>>")
                 invocation.assertFieldType("varFieldT2", "Foo<Bar<? extends Baz>>")
                 invocation.assertParamType("method", "paramT1", "Foo<Bar<Baz>>")
-                invocation.assertParamType("method", "paramT2", "Foo<Bar<? extends Baz>>")
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<? extends Baz>>")
             }
         }
@@ -792,6 +759,7 @@
             invocation.assertFieldType("valFieldT1", "Foo<Bar<Baz>>")
             invocation.assertFieldType("varField", "Foo<Bar<Baz>>")
             invocation.assertParamType("method", "param", "Foo<Bar<Baz>>")
+            invocation.assertParamType("method", "paramT2", "Foo<? extends Bar<? extends Baz>>")
             invocation.assertReturnType("methodReturn", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturnT1", "Foo<Bar<Baz>>")
 
@@ -800,12 +768,7 @@
                 invocation.assertFieldType("valFieldT2", "Foo<Bar<Baz>>")
                 invocation.assertFieldType("varFieldT1", "Foo<Bar<Baz>>")
                 invocation.assertFieldType("varFieldT2", "Foo<Bar<Baz>>")
-                invocation.assertParamType(
-                    "method", "paramT1", "Foo<Bar<? extends Baz>>", "Foo<Bar<Baz>>"
-                )
-                invocation.assertParamType(
-                    "method", "paramT2", "Foo<? extends Bar<? extends Baz>>", "Foo<Bar<Baz>>"
-                )
+                invocation.assertParamType("method", "paramT1", "Foo<Bar<? extends Baz>>")
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<Baz>>")
             } else {
                 invocation.assertFieldType("valFieldT2", "Foo<Bar<? extends Baz>>")
@@ -814,9 +777,6 @@
                 invocation.assertParamType(
                     "method", "paramT1", "Foo<? extends Bar<? extends Baz>>"
                 )
-                invocation.assertParamType(
-                    "method", "paramT2", "Foo<? extends Bar<? extends Baz>>"
-                )
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<? extends Baz>>")
             }
         }
@@ -833,6 +793,7 @@
             invocation.assertFieldType("valFieldT1", "Foo<Bar<Baz>>")
             invocation.assertFieldType("varField", "Foo<Bar<Baz>>")
             invocation.assertParamType("method", "param", "Foo<Bar<Baz>>")
+            invocation.assertParamType("method", "paramT2", "Foo<? extends Bar<? extends Baz>>")
             invocation.assertReturnType("methodReturn", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturnT1", "Foo<Bar<Baz>>")
 
@@ -842,10 +803,7 @@
                 invocation.assertFieldType("varFieldT1", "Foo<Bar<Baz>>")
                 invocation.assertFieldType("varFieldT2", "Foo<Bar<Baz>>")
                 invocation.assertParamType(
-                    "method", "paramT1", "Foo<Bar<? extends Baz>>", "Foo<Bar<Baz>>"
-                )
-                invocation.assertParamType(
-                    "method", "paramT2", "Foo<? extends Bar<? extends Baz>>", "Foo<Bar<Baz>>"
+                    "method", "paramT1", "Foo<Bar<? extends Baz>>"
                 )
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<Baz>>")
             } else {
@@ -855,9 +813,6 @@
                 invocation.assertParamType(
                     "method", "paramT1", "Foo<? extends Bar<? extends Baz>>"
                 )
-                invocation.assertParamType(
-                    "method", "paramT2", "Foo<? extends Bar<? extends Baz>>"
-                )
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<? extends Baz>>")
             }
         }
@@ -874,6 +829,8 @@
             invocation.assertFieldType("valFieldT1", "Foo<Bar<Baz>>")
 
             invocation.assertParamType("method", "param", "Foo<? extends Bar<Baz>>")
+            invocation.assertParamType("method", "paramT1", "Foo<? extends Bar<? extends Baz>>")
+            invocation.assertParamType("method", "paramT2", "Foo<? extends Bar<? extends Baz>>")
 
             invocation.assertReturnType("methodReturn", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturnT1", "Foo<Bar<Baz>>")
@@ -884,26 +841,12 @@
                 invocation.assertFieldType("varField", "Foo<Bar<Baz>>")
                 invocation.assertFieldType("varFieldT1", "Foo<Bar<Baz>>")
                 invocation.assertFieldType("varFieldT2", "Foo<Bar<Baz>>")
-                invocation.assertParamType(
-                    "method",
-                    "paramT1",
-                    "Foo<? extends Bar<? extends Baz>>",
-                    "Foo<? extends Bar<Baz>>"
-                )
-                invocation.assertParamType(
-                    "method",
-                    "paramT2",
-                    "Foo<? extends Bar<? extends Baz>>",
-                    "Foo<? extends Bar<Baz>>"
-                )
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<Baz>>")
             } else {
                 invocation.assertFieldType("valFieldT2", "Foo<Bar<? extends Baz>>")
                 invocation.assertFieldType("varField", "Foo<? extends Bar<Baz>>")
                 invocation.assertFieldType("varFieldT1", "Foo<? extends Bar<? extends Baz>>")
                 invocation.assertFieldType("varFieldT2", "Foo<? extends Bar<? extends Baz>>")
-                invocation.assertParamType("method", "paramT1", "Foo<? extends Bar<? extends Baz>>")
-                invocation.assertParamType("method", "paramT2", "Foo<? extends Bar<? extends Baz>>")
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<? extends Baz>>")
             }
         }
@@ -920,6 +863,7 @@
             invocation.assertFieldType("valFieldT1", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturn", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturnT1", "Foo<Bar<Baz>>")
+            invocation.assertParamType("method", "paramT2", "Foo<? extends Bar<? extends Baz>>")
 
             // TODO(b/237280547): Make KSP type name match KAPT.
             if (invocation.isKsp) {
@@ -929,12 +873,6 @@
                 invocation.assertFieldType("varFieldT2", "Foo<Bar<Baz>>")
                 invocation.assertParamType("method", "param", "Foo<Bar<? extends Baz>>")
                 invocation.assertParamType("method", "paramT1", "Foo<Bar<? extends Baz>>")
-                invocation.assertParamType(
-                    "method",
-                    "paramT2",
-                    "Foo<? extends Bar<? extends Baz>>",
-                    "Foo<Bar<? extends Baz>>"
-                )
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<Baz>>")
             } else {
                 invocation.assertFieldType("valFieldT2", "Foo<Bar<? extends Baz>>")
@@ -945,9 +883,6 @@
                 invocation.assertParamType(
                     "method", "paramT1", "Foo<? extends Bar<? extends Baz>>"
                 )
-                invocation.assertParamType(
-                    "method", "paramT2", "Foo<? extends Bar<? extends Baz>>"
-                )
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<? extends Baz>>")
             }
         }
@@ -963,6 +898,8 @@
             invocation.assertFieldType("valField", "Foo<Bar<Baz>>")
             invocation.assertFieldType("valFieldT1", "Foo<Bar<Baz>>")
             invocation.assertParamType("method", "param", "Foo<? extends Bar<Baz>>")
+            invocation.assertParamType("method", "paramT1", "Foo<? extends Bar<? extends Baz>>")
+            invocation.assertParamType("method", "paramT2", "Foo<? extends Bar<? extends Baz>>")
             invocation.assertReturnType("methodReturn", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturnT1", "Foo<Bar<Baz>>")
 
@@ -972,30 +909,12 @@
                 invocation.assertFieldType("varField", "Foo<Bar<Baz>>")
                 invocation.assertFieldType("varFieldT1", "Foo<Bar<Baz>>")
                 invocation.assertFieldType("varFieldT2", "Foo<Bar<Baz>>")
-                invocation.assertParamType(
-                    "method",
-                    "paramT1",
-                    "Foo<? extends Bar<? extends Baz>>",
-                    "Foo<? extends Bar<Baz>>"
-                )
-                invocation.assertParamType(
-                    "method",
-                    "paramT2",
-                    "Foo<? extends Bar<? extends Baz>>",
-                    "Foo<? extends Bar<Baz>>"
-                )
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<Baz>>")
             } else {
                 invocation.assertFieldType("valFieldT2", "Foo<Bar<? extends Baz>>")
                 invocation.assertFieldType("varField", "Foo<? extends Bar<Baz>>")
                 invocation.assertFieldType("varFieldT1", "Foo<? extends Bar<? extends Baz>>")
                 invocation.assertFieldType("varFieldT2", "Foo<? extends Bar<? extends Baz>>")
-                invocation.assertParamType(
-                    "method", "paramT1", "Foo<? extends Bar<? extends Baz>>"
-                )
-                invocation.assertParamType(
-                    "method", "paramT2", "Foo<? extends Bar<? extends Baz>>"
-                )
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<? extends Baz>>")
             }
         }
@@ -1010,6 +929,7 @@
         ) { invocation ->
             invocation.assertFieldType("valField", "Foo<Bar<Baz>>")
             invocation.assertFieldType("valFieldT1", "Foo<Bar<Baz>>")
+            invocation.assertParamType("method", "paramT2", "Foo<? extends Bar<? extends Baz>>")
             invocation.assertReturnType("methodReturn", "Foo<Bar<Baz>>")
             invocation.assertReturnType("methodReturnT1", "Foo<Bar<Baz>>")
 
@@ -1021,12 +941,6 @@
                 invocation.assertFieldType("varFieldT2", "Foo<Bar<Baz>>")
                 invocation.assertParamType("method", "param", "Foo<Bar<? extends Baz>>")
                 invocation.assertParamType("method", "paramT1", "Foo<Bar<? extends Baz>>")
-                invocation.assertParamType(
-                    "method",
-                    "paramT2",
-                    "Foo<? extends Bar<? extends Baz>>",
-                    "Foo<Bar<? extends Baz>>"
-                )
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<Baz>>")
             } else {
                 invocation.assertFieldType("valFieldT2", "Foo<Bar<? extends Baz>>")
@@ -1035,7 +949,6 @@
                 invocation.assertFieldType("varFieldT2", "Foo<? extends Bar<? extends Baz>>")
                 invocation.assertParamType("method", "param", "Foo<? extends Bar<? extends Baz>>")
                 invocation.assertParamType("method", "paramT1", "Foo<? extends Bar<? extends Baz>>")
-                invocation.assertParamType("method", "paramT2", "Foo<? extends Bar<? extends Baz>>")
                 invocation.assertReturnType("methodReturnT2", "Foo<Bar<? extends Baz>>")
             }
         }
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BySelectorTests.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BySelectorTests.java
index 6fa00bb..69a2db2 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BySelectorTests.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BySelectorTests.java
@@ -82,32 +82,46 @@
     }
 
     @Test
-    public void testDescSetFromResource() {
+    public void testDesc() {
         launchTestActivity(BySelectorTestDescActivity.class);
 
-        // Content Description from resource
-        assertNotNull(mDevice.findObject(By.desc("Content Description Set From Layout")));
+        // Content description from source code.
+        assertNotNull(mDevice.findObject(By.desc("This button desc contains some text.")));
+
+        // Content description set at runtime.
+        assertNotNull(mDevice.findObject(By.desc("Content description set at runtime.")));
+
+        // No element has this content description.
+        assertNull(mDevice.findObject(By.desc("No element has this content description.")));
+
+        // Pattern of the content description.
+        assertNotNull(mDevice.findObject(By.desc(Pattern.compile(".*contains.*"))));
+        assertNull(mDevice.findObject(By.desc(Pattern.compile(".*NonExistent.*"))));
     }
 
     @Test
-    public void testDescSetAtRuntime() {
+    public void testDescContains() {
         launchTestActivity(BySelectorTestDescActivity.class);
 
-        // Content Description set at runtime
-        assertNotNull(mDevice.findObject(By.desc("Content Description Set At Runtime")));
+        assertNotNull(mDevice.findObject(By.descContains("contains")));
+        assertNull(mDevice.findObject(By.descContains("not-containing")));
     }
 
     @Test
-    public void testDescNotFound() {
+    public void testDescStartsWith() {
         launchTestActivity(BySelectorTestDescActivity.class);
 
-        // No element has this content description
-        assertNull(mDevice.findObject(By.desc("No element has this Content Description")));
+        assertNotNull(mDevice.findObject(By.descStartsWith("This")));
+        assertNull(mDevice.findObject(By.descStartsWith("NotThis")));
     }
 
-    // TODO(b/235841286): Implement these for desc():
-    // 1. Patterns
-    // 2. Runtime Widgets
+    @Test
+    public void testDescEndsWith() {
+        launchTestActivity(BySelectorTestDescActivity.class);
+
+        assertNotNull(mDevice.findObject(By.descEndsWith(" text.")));
+        assertNull(mDevice.findObject(By.descEndsWith(" not.")));
+    }
 
     @Test
     public void testPackage() {
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Tests.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Tests.java
index 7aeb18d..a98b5bb 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Tests.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Tests.java
@@ -18,8 +18,10 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
 import android.graphics.Point;
@@ -37,6 +39,7 @@
 
 public class UiObject2Tests extends BaseTest {
     private static final int TIMEOUT_MS = 10_000;
+    private static final int SPEED_MS = 100;
 
     @Test
     public void testClear() {
@@ -83,6 +86,18 @@
     }
 
     @Test
+    public void testEquals() {
+        launchTestActivity(MainActivity.class);
+
+        // Get the same textView object via different methods.
+        UiObject2 textView1 = mDevice.findObject(By.res(TEST_APP, "example_id"));
+        UiObject2 textView2 = mDevice.findObject(By.text("TextView with an id"));
+        assertTrue(textView1.equals(textView2));
+        UiObject2 linearLayout = mDevice.findObject(By.res(TEST_APP, "nested_elements"));
+        assertFalse(textView1.equals(linearLayout));
+    }
+
+    @Test
     public void testFindObject() {
         launchTestActivity(MainActivity.class);
 
@@ -238,6 +253,21 @@
     }
 
     @Test
+    public void testHashCode() {
+        launchTestActivity(MainActivity.class);
+
+        // Get the same textView object via different methods.
+        // The same object should have the same hash code.
+        UiObject2 textView1 = mDevice.findObject(By.res(TEST_APP, "example_id"));
+        UiObject2 textView2 = mDevice.findObject(By.text("TextView with an id"));
+        assertEquals(textView1.hashCode(), textView2.hashCode());
+
+        // Different objects should have different hash codes.
+        UiObject2 linearLayout = mDevice.findObject(By.res(TEST_APP, "nested_elements"));
+        assertNotEquals(textView1.hashCode(), linearLayout.hashCode());
+    }
+
+    @Test
     public void testHasObject() {
         launchTestActivity(MainActivity.class);
 
@@ -362,47 +392,70 @@
     }
 
     @Test
-    public void testPinchIn100Percent() {
+    public void testPinchClose() {
         launchTestActivity(UiObject2TestPinchActivity.class);
 
-        // Find the area to pinch
         UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
         UiObject2 scaleText = pinchArea.findObject(By.res(TEST_APP, "scale_factor"));
-        pinchArea.pinchClose(1.0f, 100);
-        scaleText.wait(Until.textNotEquals("1.0f"), 1000);
+        pinchArea.pinchClose(1f);
+        scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
+        float scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        assertTrue(String.format("Expected scale value to be less than 1f after pinchClose(), "
+                        + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch < 1f);
     }
 
     @Test
-    public void testPinchIn75Percent() {
+    public void testPinchClose_withSpeed() {
         launchTestActivity(UiObject2TestPinchActivity.class);
 
-        // Find the area to pinch
         UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
         UiObject2 scaleText = pinchArea.findObject(By.res(TEST_APP, "scale_factor"));
-        pinchArea.pinchClose(.75f, 100);
-        scaleText.wait(Until.textNotEquals("1.0f"), 1000);
+        pinchArea.pinchClose(.75f, SPEED_MS);
+        scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
+        float scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        assertTrue(String.format("Expected scale value to be less than 1f after pinchClose(), "
+                + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch < 1f);
     }
 
     @Test
-    public void testPinchIn50Percent() {
+    public void testPinchOpen() {
         launchTestActivity(UiObject2TestPinchActivity.class);
 
-        // Find the area to pinch
         UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
         UiObject2 scaleText = pinchArea.findObject(By.res(TEST_APP, "scale_factor"));
-        pinchArea.pinchClose(.5f, 100);
-        scaleText.wait(Until.textNotEquals("1.0f"), 1000);
+        pinchArea.pinchOpen(.5f);
+        scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
+        float scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        assertTrue(String.format("Expected scale text to be greater than 1f after pinchOpen(), "
+                + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch > 1f);
     }
 
     @Test
-    public void testPinchIn25Percent() {
+    public void testPinchOpen_withSpeed() {
         launchTestActivity(UiObject2TestPinchActivity.class);
 
-        // Find the area to pinch
         UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
         UiObject2 scaleText = pinchArea.findObject(By.res(TEST_APP, "scale_factor"));
-        pinchArea.pinchClose(.25f, 100);
-        scaleText.wait(Until.textNotEquals("1.0f"), 1000);
+        pinchArea.pinchOpen(.25f, SPEED_MS);
+        scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
+        float scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        assertTrue(String.format("Expected scale text to be greater than 1f after pinchOpen(), "
+                + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch > 1f);
+    }
+
+    @Test
+    public void testRecycle() {
+        launchTestActivity(MainActivity.class);
+
+        UiObject2 textView = mDevice.findObject(By.text("Sample text"));
+        textView.recycle();
+        // Attributes of a recycled object cannot be accessed.
+        IllegalStateException e = assertThrows(
+                "Expected testView.getText() to throw IllegalStateException, but it didn't.",
+                IllegalStateException.class,
+                () -> textView.getText()
+        );
+        assertEquals("This object has already been recycled", e.getMessage());
     }
 
     @Test
@@ -470,6 +523,62 @@
     }
 
     @Test
+    public void testSetGestureMargin() {
+        launchTestActivity(UiObject2TestPinchActivity.class);
+
+        UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
+        UiObject2 scaleText = pinchArea.findObject(By.res(TEST_APP, "scale_factor"));
+
+        // Set the gesture's margins to a large number (greater than the width or height of the UI
+        // object's visible bounds).
+        // The gesture's bounds cannot form a rectangle and no action can be performed.
+        pinchArea.setGestureMargin(1_000);
+        pinchArea.pinchClose(1f);
+        scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
+        float scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        assertEquals(String.format("Expected scale value to be equal to 1f after pinchClose(), "
+                + "but got [%f]", scaleValueAfterPinch), 1f, scaleValueAfterPinch, 0f);
+
+        // Set the gesture's margins to a small number (smaller than the width or height of the UI
+        // object's visible bounds).
+        // The gesture's bounds form a rectangle and action can be performed.
+        pinchArea.setGestureMargin(1);
+        pinchArea.pinchClose(1f);
+        scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
+        scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        assertTrue(String.format("Expected scale value to be less than 1f after pinchClose(), "
+                + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch < 1f);
+    }
+
+    @Test
+    public void testSetGestureMargins() {
+        launchTestActivity(UiObject2TestPinchActivity.class);
+
+        UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
+        UiObject2 scaleText = pinchArea.findObject(By.res(TEST_APP, "scale_factor"));
+
+        // Set the gesture's margins to large numbers (greater than the width or height of the UI
+        // object's visible bounds).
+        // The gesture's bounds cannot form a rectangle and no action can be performed.
+        pinchArea.setGestureMargins(1, 1, 1_000, 1_000);
+        pinchArea.pinchClose(1f);
+        scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
+        float scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        assertEquals(String.format("Expected scale value to be equal to 1f after pinchClose(), "
+                + "but got [%f]", scaleValueAfterPinch), 1f, scaleValueAfterPinch, 0f);
+
+        // Set the gesture's margins to small numbers (smaller than the width or height of the UI
+        // object's visible bounds).
+        // The gesture's bounds form a rectangle and action can be performed.
+        pinchArea.setGestureMargins(1, 1, 1, 1);
+        pinchArea.pinchClose(1f);
+        scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
+        scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        assertTrue(String.format("Expected scale value to be less than 1f after pinchClose(), "
+                + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch < 1f);
+    }
+
+    @Test
     public void testSetText() {
         launchTestActivity(UiObject2TestClearTextActivity.class);
 
@@ -484,10 +593,10 @@
     /* TODO(b/235841473): Implement these tests
     public void testDrag() {}
 
-    public void testEquals() {}
-
     public void testFling() {}
 
+    public void testSwipe() {}
+
     public void testWaitForExists() {}
 
     public void testWaitForGone() {}
diff --git a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/BySelectorTestDescActivity.java b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/BySelectorTestDescActivity.java
index 9b559c5..1bfc504 100644
--- a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/BySelectorTestDescActivity.java
+++ b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/BySelectorTestDescActivity.java
@@ -18,7 +18,7 @@
 
 import android.app.Activity;
 import android.os.Bundle;
-import android.widget.Button;
+import android.widget.RatingBar;
 
 import androidx.annotation.Nullable;
 
@@ -29,7 +29,7 @@
 
         setContentView(R.layout.byselector_testdesc_activity);
 
-        Button button = (Button)findViewById(R.id.button_with_runtime_description);
-        button.setContentDescription("Content Description Set At Runtime");
+        RatingBar rating_bar = (RatingBar) findViewById(R.id.rating_bar);
+        rating_bar.setContentDescription("Content description set at runtime.");
     }
 }
diff --git a/test/uiautomator/integration-tests/testapp/src/main/res/layout/byselector_testdesc_activity.xml b/test/uiautomator/integration-tests/testapp/src/main/res/layout/byselector_testdesc_activity.xml
index c09010f..2f5325f 100644
--- a/test/uiautomator/integration-tests/testapp/src/main/res/layout/byselector_testdesc_activity.xml
+++ b/test/uiautomator/integration-tests/testapp/src/main/res/layout/byselector_testdesc_activity.xml
@@ -21,13 +21,13 @@
         android:orientation="vertical"
         tools:context=".BySelectorTestDescActivity">
 
-    <Button android:id="@+id/button_with_layout_description"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:contentDescription="Content Description Set From Layout" />
+    <Button android:id="@+id/button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:contentDescription="This button desc contains some text." />
 
-    <Button android:id="@+id/button_with_runtime_description"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content" />
+    <RatingBar android:id="@+id/rating_bar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
 
 </LinearLayout>
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
index a17297c..0e44a89 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
@@ -202,7 +202,7 @@
     }
 
     /** Helper method used to evaluate a {@link Pattern} criteria if it is set. */
-    static private boolean checkCriteria(Pattern criteria, CharSequence value) {
+    static boolean checkCriteria(Pattern criteria, CharSequence value) {
         if (criteria == null) {
             return true;
         }
@@ -210,7 +210,7 @@
     }
 
     /** Helper method used to evaluate a {@link Boolean} criteria if it is set. */
-    static private boolean checkCriteria(Boolean criteria, boolean value) {
+    static boolean checkCriteria(Boolean criteria, boolean value) {
         if (criteria == null) {
             return true;
         }
@@ -348,7 +348,7 @@
      */
     private static class SinglyLinkedList<T> implements Iterable<T> {
 
-        private final Node<T> mHead;
+        final Node<T> mHead;
 
         /** Constructs an empty list. */
         public SinglyLinkedList() {
diff --git a/text/text/build.gradle b/text/text/build.gradle
index 231df11..331866c 100644
--- a/text/text/build.gradle
+++ b/text/text/build.gradle
@@ -25,6 +25,7 @@
 
 dependencies {
     implementation(libs.kotlinStdlib)
+    implementation("androidx.core:core:1.7.0")
 
     api "androidx.annotation:annotation:1.2.0"
 
@@ -32,7 +33,6 @@
     testImplementation(libs.testRunner)
     testImplementation(libs.junit)
 
-    androidTestImplementation("androidx.core:core:1.5.0-rc02")
     androidTestImplementation(project(":compose:ui:ui-test-font"))
     androidTestImplementation(libs.testRules)
     androidTestImplementation(libs.testRunner)
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebMessageAdapter.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebMessageAdapter.java
index 7496c34..aad2697 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebMessageAdapter.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebMessageAdapter.java
@@ -37,6 +37,7 @@
         this.mWebMessageCompat = webMessage;
     }
 
+    @SuppressWarnings("deprecation")
     @Override
     @Nullable
     public String getData() {