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() {