Merge "[BottomSheet] Horizontal Edge to Edge" into androidx-main
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
index b514854..8161c1c 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
@@ -18,8 +18,6 @@
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertThrows;
-
import android.os.Bundle;
import android.os.Parcel;
@@ -107,48 +105,4 @@
assertThat(outDoc.getPropertyDocument("propDocument").getPropertyBytesArray("propBytes"))
.isEqualTo(new byte[][]{{3, 4}});
}
-
- @Test
- public void testPropertyParcel_onePropertySet_success() {
- String[] stringValues = {"a", "b"};
- long[] longValues = {1L, 2L};
- double[] doubleValues = {1.0, 2.0};
- boolean[] booleanValues = {true, false};
- byte[][] bytesValues = {new byte[1]};
- Bundle[] bundleValues = {new Bundle()};
-
- assertThat(new PropertyParcel.Builder("name").setStringValues(
- stringValues).build().getStringValues()).isEqualTo(
- Arrays.copyOf(stringValues, stringValues.length));
- assertThat(new PropertyParcel.Builder("name").setLongValues(
- longValues).build().getLongValues()).isEqualTo(
- Arrays.copyOf(longValues, longValues.length));
- assertThat(new PropertyParcel.Builder("name").setDoubleValues(
- doubleValues).build().getDoubleValues()).isEqualTo(
- Arrays.copyOf(doubleValues, doubleValues.length));
- assertThat(new PropertyParcel.Builder("name").setBooleanValues(
- booleanValues).build().getBooleanValues()).isEqualTo(
- Arrays.copyOf(booleanValues, booleanValues.length));
- assertThat(new PropertyParcel.Builder("name").setBytesValues(
- bytesValues).build().getBytesValues()).isEqualTo(
- Arrays.copyOf(bytesValues, bytesValues.length));
- assertThat(new PropertyParcel.Builder("name").setDocumentValues(
- bundleValues).build().getDocumentValues()).isEqualTo(
- Arrays.copyOf(bundleValues, bundleValues.length));
- }
-
- @Test
- public void testPropertyParcel_moreThanOnePropertySet_exceptionThrown() {
- String[] stringValues = {"a", "b"};
- long[] longValues = {1L, 2L};
- PropertyParcel.Builder propertyParcelBuilder =
- new PropertyParcel.Builder("name")
- .setStringValues(stringValues)
- .setLongValues(longValues);
-
- IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
- () -> propertyParcelBuilder.build());
-
- assertThat(exception.getMessage()).contains("One and only one type array");
- }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/safeparcel/GenericDocumentParcelTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/safeparcel/GenericDocumentParcelTest.java
new file mode 100644
index 0000000..47df245
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/safeparcel/GenericDocumentParcelTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.app.safeparcel;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Map;
+
+/** Tests for {@link androidx.appsearch.app.GenericDocument} related SafeParcels. */
+public class GenericDocumentParcelTest {
+ @Test
+ public void testPropertyParcel_onePropertySet_success() {
+ String[] stringValues = {"a", "b"};
+ long[] longValues = {1L, 2L};
+ double[] doubleValues = {1.0, 2.0};
+ boolean[] booleanValues = {true, false};
+ byte[][] bytesValues = {new byte[1]};
+ GenericDocumentParcel[] docValues = {(new GenericDocumentParcel.Builder(
+ "namespace", "id", "schemaType")).build()};
+
+ assertThat(new PropertyParcel.Builder("name").setStringValues(
+ stringValues).build().getStringValues()).isEqualTo(
+ Arrays.copyOf(stringValues, stringValues.length));
+ assertThat(new PropertyParcel.Builder("name").setLongValues(
+ longValues).build().getLongValues()).isEqualTo(
+ Arrays.copyOf(longValues, longValues.length));
+ assertThat(new PropertyParcel.Builder("name").setDoubleValues(
+ doubleValues).build().getDoubleValues()).isEqualTo(
+ Arrays.copyOf(doubleValues, doubleValues.length));
+ assertThat(new PropertyParcel.Builder("name").setBooleanValues(
+ booleanValues).build().getBooleanValues()).isEqualTo(
+ Arrays.copyOf(booleanValues, booleanValues.length));
+ assertThat(new PropertyParcel.Builder("name").setBytesValues(
+ bytesValues).build().getBytesValues()).isEqualTo(
+ Arrays.copyOf(bytesValues, bytesValues.length));
+ assertThat(new PropertyParcel.Builder("name").setDocumentValues(
+ docValues).build().getDocumentValues()).isEqualTo(
+ Arrays.copyOf(docValues, docValues.length));
+ }
+
+ @Test
+ public void testPropertyParcel_moreThanOnePropertySet_exceptionThrown() {
+ String[] stringValues = {"a", "b"};
+ long[] longValues = {1L, 2L};
+ PropertyParcel.Builder propertyParcelBuilder =
+ new PropertyParcel.Builder("name")
+ .setStringValues(stringValues)
+ .setLongValues(longValues);
+
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> propertyParcelBuilder.build());
+
+ assertThat(exception.getMessage()).contains("One and only one type array");
+ }
+
+ @Test
+ public void testGenericDocumentParcel_propertiesGeneratedCorrectly() {
+ GenericDocumentParcel.Builder builder =
+ new GenericDocumentParcel.Builder(
+ /*namespace=*/ "namespace",
+ /*id=*/ "id",
+ /*schemaType=*/ "schemaType");
+ long[] longArray = new long[]{1L, 2L, 3L};
+ String[] stringArray = new String[]{"hello", "world", "!"};
+ builder.putInPropertyMap(/*name=*/ "longArray", /*values=*/ longArray);
+ builder.putInPropertyMap(/*name=*/ "stringArray", /*values=*/ stringArray);
+ GenericDocumentParcel genericDocumentParcel = builder.build();
+
+ PropertyParcel[] properties = genericDocumentParcel.getProperties();
+ Map<String, PropertyParcel> propertyMap = genericDocumentParcel.getPropertyMap();
+ PropertyParcel longArrayProperty = new PropertyParcel.Builder(
+ /*name=*/ "longArray").setLongValues(longArray).build();
+ PropertyParcel stringArrayProperty = new PropertyParcel.Builder(
+ /*name=*/ "stringArray").setStringValues(stringArray).build();
+
+ assertThat(properties).asList().containsExactly(longArrayProperty, stringArrayProperty);
+ assertThat(propertyMap).containsExactly("longArray", longArrayProperty,
+ "stringArray", stringArrayProperty);
+ }
+
+ @Test
+ public void testGenericDocumentParcel_buildFromAnotherDocumentParcelCorrectly() {
+ GenericDocumentParcel.Builder builder =
+ new GenericDocumentParcel.Builder(
+ /*namespace=*/ "namespace",
+ /*id=*/ "id",
+ /*schemaType=*/ "schemaType");
+ long[] longArray = new long[]{1L, 2L, 3L};
+ String[] stringArray = new String[]{"hello", "world", "!"};
+ builder.putInPropertyMap(/*name=*/ "longArray", /*values=*/ longArray);
+ builder.putInPropertyMap(/*name=*/ "stringArray", /*values=*/ stringArray);
+ GenericDocumentParcel genericDocumentParcel = builder.build();
+
+ GenericDocumentParcel genericDocumentParcelCopy =
+ new GenericDocumentParcel.Builder(genericDocumentParcel).build();
+
+ assertThat(genericDocumentParcelCopy.getNamespace()).isEqualTo(
+ genericDocumentParcel.getNamespace());
+ assertThat(genericDocumentParcelCopy.getId()).isEqualTo(genericDocumentParcel.getId());
+ assertThat(genericDocumentParcelCopy.getSchemaType()).isEqualTo(
+ genericDocumentParcel.getSchemaType());
+ assertThat(genericDocumentParcelCopy.getCreationTimestampMillis()).isEqualTo(
+ genericDocumentParcel.getCreationTimestampMillis());
+ assertThat(genericDocumentParcelCopy.getTtlMillis()).isEqualTo(
+ genericDocumentParcel.getTtlMillis());
+ assertThat(genericDocumentParcelCopy.getScore()).isEqualTo(
+ genericDocumentParcel.getScore());
+ // Check it is a copy.
+ assertThat(genericDocumentParcelCopy).isNotSameInstanceAs(genericDocumentParcel);
+ assertThat(genericDocumentParcelCopy.getProperties()).isEqualTo(
+ genericDocumentParcel.getProperties());
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/safeparcel/GenericDocumentParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/safeparcel/GenericDocumentParcel.java
new file mode 100644
index 0000000..3d92c02
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/safeparcel/GenericDocumentParcel.java
@@ -0,0 +1,494 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.app.safeparcel;
+
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.GenericDocumentParcelCreator;
+import androidx.collection.ArrayMap;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Holds data for a {@link GenericDocument}.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SafeParcelable.Class(creator = "GenericDocumentParcelCreator")
+public final class GenericDocumentParcel extends AbstractSafeParcelable {
+ @NonNull
+ public static final GenericDocumentParcelCreator CREATOR =
+ new GenericDocumentParcelCreator();
+
+ /** The default score of document. */
+ private static final int DEFAULT_SCORE = 0;
+
+ /** The default time-to-live in millisecond of a document, which is infinity. */
+ private static final long DEFAULT_TTL_MILLIS = 0L;
+
+ /** Default but invalid value for {@code mCreationTimestampMillis}. */
+ private static final long INVALID_CREATION_TIMESTAMP_MILLIS = -1L;
+
+ @Field(id = 1, getter = "getNamespace")
+ @NonNull
+ private final String mNamespace;
+
+ @Field(id = 2, getter = "getId")
+ @NonNull
+ private final String mId;
+
+ @Field(id = 3, getter = "getSchemaType")
+ @NonNull
+ private final String mSchemaType;
+
+ @Field(id = 4, getter = "getCreationTimestampMillis")
+ private final long mCreationTimestampMillis;
+
+ @Field(id = 5, getter = "getTtlMillis")
+ private final long mTtlMillis;
+
+ @Field(id = 6, getter = "getScore")
+ private final int mScore;
+
+ /**
+ * Contains all properties in {@link GenericDocument} in a list.
+ *
+ * <p>Unfortunately SafeParcelable doesn't support map type so we have to use a list here.
+ */
+ @Field(id = 7, getter = "getProperties")
+ @NonNull
+ private final PropertyParcel[] mProperties;
+
+ /**
+ * Contains all properties in {@link GenericDocument} to support getting properties via name
+ *
+ * <p>This map is created for quick looking up property by name.
+ */
+ @NonNull
+ private final Map<String, PropertyParcel> mPropertyMap;
+
+ @Nullable
+ private Integer mHashCode;
+
+ /**
+ * The constructor taking the property list, and create map internally from this list.
+ *
+ * <p> This will be used in createFromParcel, so creating the property map can not be avoided
+ * in this constructor.
+ */
+ @Constructor
+ GenericDocumentParcel(
+ @Param(id = 1) @NonNull String namespace,
+ @Param(id = 2) @NonNull String id,
+ @Param(id = 3) @NonNull String schemaType,
+ @Param(id = 4) long creationTimestampMillis,
+ @Param(id = 5) long ttlMillis,
+ @Param(id = 6) int score,
+ @Param(id = 7) @NonNull PropertyParcel[] properties) {
+ this(namespace, id, schemaType, creationTimestampMillis, ttlMillis, score,
+ properties, createPropertyMapFromPropertyArray(properties));
+ }
+
+ /**
+ * A constructor taking both property list and property map.
+ *
+ * <p>Caller needs to make sure property list and property map
+ * matches(map is generated from list, or list generated from map).
+ */
+ GenericDocumentParcel(
+ @NonNull String namespace,
+ @NonNull String id,
+ @NonNull String schemaType,
+ long creationTimestampMillis,
+ long ttlMillis,
+ int score,
+ @NonNull PropertyParcel[] properties,
+ @NonNull Map<String, PropertyParcel> propertyMap) {
+ mNamespace = Objects.requireNonNull(namespace);
+ mId = Objects.requireNonNull(id);
+ mSchemaType = Objects.requireNonNull(schemaType);
+ mCreationTimestampMillis = creationTimestampMillis;
+ mTtlMillis = ttlMillis;
+ mScore = score;
+ mProperties = Objects.requireNonNull(properties);
+ mPropertyMap = Objects.requireNonNull(propertyMap);
+ }
+
+ private static Map<String, PropertyParcel> createPropertyMapFromPropertyArray(
+ @NonNull PropertyParcel[] properties) {
+ Objects.requireNonNull(properties);
+ Map<String, PropertyParcel> propertyMap = new ArrayMap<>(properties.length);
+ for (int i = 0; i < properties.length; ++i) {
+ PropertyParcel property = properties[i];
+ propertyMap.put(property.getPropertyName(), property);
+ }
+ return propertyMap;
+ }
+
+ /** Returns the unique identifier of the {@link GenericDocument}. */
+ @NonNull
+ public String getId() {
+ return mId;
+ }
+
+ /** Returns the namespace of the {@link GenericDocument}. */
+ @NonNull
+ public String getNamespace() {
+ return mNamespace;
+ }
+
+ /** Returns the {@link AppSearchSchema} type of the {@link GenericDocument}. */
+ @NonNull
+ public String getSchemaType() {
+ return mSchemaType;
+ }
+
+ /** Returns the creation timestamp of the {@link GenericDocument}, in milliseconds. */
+ /*@exportToFramework:CurrentTimeMillisLong*/
+ public long getCreationTimestampMillis() {
+ return mCreationTimestampMillis;
+ }
+
+ /** Returns the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds. */
+ public long getTtlMillis() {
+ return mTtlMillis;
+ }
+
+ /** Returns the score of the {@link GenericDocument}. */
+ public int getScore() {
+ return mScore;
+ }
+
+ /** Returns the names of all properties defined in this document. */
+ @NonNull
+ public Set<String> getPropertyNames() {
+ return mPropertyMap.keySet();
+ }
+
+ /** Returns all the properties the document has. */
+ @NonNull
+ public PropertyParcel[] getProperties() {
+ return mProperties;
+ }
+
+ /** Returns the property map the document has. */
+ @NonNull
+ public Map<String, PropertyParcel> getPropertyMap() {
+ return mPropertyMap;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof GenericDocumentParcel)) {
+ return false;
+ }
+ GenericDocumentParcel otherDocument = (GenericDocumentParcel) other;
+ return mNamespace.equals(otherDocument.mNamespace)
+ && mId.equals(otherDocument.mId)
+ && mSchemaType.equals(otherDocument.mSchemaType)
+ && mTtlMillis == otherDocument.mTtlMillis
+ && mCreationTimestampMillis == otherDocument.mCreationTimestampMillis
+ && mScore == otherDocument.mScore
+ && Arrays.equals(mProperties, otherDocument.mProperties)
+ && Objects.equals(mPropertyMap, otherDocument.mPropertyMap);
+ }
+
+ @Override
+ public int hashCode() {
+ if (mHashCode == null) {
+ mHashCode = Objects.hash(
+ mNamespace,
+ mId,
+ mSchemaType,
+ mTtlMillis,
+ mScore,
+ mCreationTimestampMillis,
+ Arrays.hashCode(mProperties),
+ mPropertyMap.hashCode());
+ }
+ return mHashCode;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ GenericDocumentParcelCreator.writeToParcel(this, dest, flags);
+ }
+
+ /** The builder class for {@link GenericDocumentParcel}. */
+ public static final class Builder {
+ private String mNamespace;
+ private String mId;
+ private String mSchemaType;
+ private long mCreationTimestampMillis;
+ private long mTtlMillis;
+ private int mScore;
+ private Map<String, PropertyParcel> mPropertyMap;
+ private boolean mBuilt = false;
+
+ /**
+ * Creates a new {@link GenericDocument.Builder}.
+ *
+ * <p>Document IDs are unique within a namespace.
+ *
+ * <p>The number of namespaces per app should be kept small for efficiency reasons.
+ */
+ public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) {
+ mNamespace = Objects.requireNonNull(namespace);
+ mId = Objects.requireNonNull(id);
+ mSchemaType = Objects.requireNonNull(schemaType);
+ mCreationTimestampMillis = INVALID_CREATION_TIMESTAMP_MILLIS;
+ mTtlMillis = DEFAULT_TTL_MILLIS;
+ mScore = DEFAULT_SCORE;
+ mPropertyMap = new ArrayMap<>();
+ }
+
+ /**
+ * Creates a new {@link GenericDocumentParcel.Builder} from the given
+ * {@link GenericDocumentParcel}.
+ */
+ Builder(@NonNull GenericDocumentParcel documentSafeParcel) {
+ Objects.requireNonNull(documentSafeParcel);
+
+ mNamespace = documentSafeParcel.mNamespace;
+ mId = documentSafeParcel.mId;
+ mSchemaType = documentSafeParcel.mSchemaType;
+ mCreationTimestampMillis = documentSafeParcel.mCreationTimestampMillis;
+ mTtlMillis = documentSafeParcel.mTtlMillis;
+ mScore = documentSafeParcel.mScore;
+
+ // Create a shallow copy of the map so we won't change the original one.
+ Map<String, PropertyParcel> propertyMap = documentSafeParcel.mPropertyMap;
+ mPropertyMap = new ArrayMap<>(propertyMap.size());
+ for (PropertyParcel value : propertyMap.values()) {
+ mPropertyMap.put(value.getPropertyName(), value);
+ }
+ }
+
+ /**
+ * Sets the app-defined namespace this document resides in, changing the value provided in
+ * the constructor. No special values are reserved or understood by the infrastructure.
+ *
+ * <p>Document IDs are unique within a namespace.
+ *
+ * <p>The number of namespaces per app should be kept small for efficiency reasons.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setNamespace(@NonNull String namespace) {
+ Objects.requireNonNull(namespace);
+ resetIfBuilt();
+ mNamespace = namespace;
+ return this;
+ }
+
+ /**
+ * Sets the ID of this document, changing the value provided in the constructor. No special
+ * values are reserved or understood by the infrastructure.
+ *
+ * <p>Document IDs are unique within a namespace.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setId(@NonNull String id) {
+ Objects.requireNonNull(id);
+ resetIfBuilt();
+ mId = id;
+ return this;
+ }
+
+ /**
+ * Sets the schema type of this document, changing the value provided in the constructor.
+ *
+ * <p>To successfully index a document, the schema type must match the name of an {@link
+ * AppSearchSchema} object previously provided to {@link AppSearchSession#setSchema}.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setSchemaType(@NonNull String schemaType) {
+ Objects.requireNonNull(schemaType);
+ resetIfBuilt();
+ mSchemaType = schemaType;
+ return this;
+ }
+
+ /** Sets the score of the parent {@link GenericDocument}. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setScore(int score) {
+ resetIfBuilt();
+ mScore = score;
+ return this;
+ }
+
+ /**
+ * Sets the creation timestamp of the {@link GenericDocument}, in milliseconds.
+ *
+ * <p>This should be set using a value obtained from the {@link System#currentTimeMillis}
+ * time base.
+ *
+ * <p>If this method is not called, this will be set to the time the object is built.
+ *
+ * @param creationTimestampMillis a creation timestamp in milliseconds.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setCreationTimestampMillis(
+ /*@exportToFramework:CurrentTimeMillisLong*/ long creationTimestampMillis) {
+ resetIfBuilt();
+ mCreationTimestampMillis = creationTimestampMillis;
+ return this;
+ }
+
+ /**
+ * Sets the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds.
+ *
+ * <p>The TTL is measured against {@link #getCreationTimestampMillis}. At the timestamp of
+ * {@code creationTimestampMillis + ttlMillis}, measured in the {@link
+ * System#currentTimeMillis} time base, the document will be auto-deleted.
+ *
+ * <p>The default value is 0, which means the document is permanent and won't be
+ * auto-deleted until the app is uninstalled or {@link AppSearchSession#remove} is called.
+ *
+ * @param ttlMillis a non-negative duration in milliseconds.
+ * @throws IllegalArgumentException if ttlMillis is negative.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setTtlMillis(long ttlMillis) {
+ if (ttlMillis < 0) {
+ throw new IllegalArgumentException("Document ttlMillis cannot be negative.");
+ }
+ resetIfBuilt();
+ mTtlMillis = ttlMillis;
+ return this;
+ }
+
+ /**
+ * Clears the value for the property with the given name.
+ *
+ * <p>Note that this method does not support property paths.
+ *
+ * @param name The name of the property to clear.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearProperty(@NonNull String name) {
+ Objects.requireNonNull(name);
+ resetIfBuilt();
+ mPropertyMap.remove(name);
+ return this;
+ }
+
+ /** puts an array of {@link String} in property map. */
+ @NonNull
+ public Builder putInPropertyMap(@NonNull String name, @NonNull String[] values)
+ throws IllegalArgumentException {
+ mPropertyMap.put(name,
+ new PropertyParcel.Builder(name).setStringValues(values).build());
+ return this;
+ }
+
+ /** puts an array of boolean in property map. */
+ @NonNull
+ public Builder putInPropertyMap(@NonNull String name, @NonNull boolean[] values) {
+ mPropertyMap.put(name,
+ new PropertyParcel.Builder(name).setBooleanValues(values).build());
+ return this;
+ }
+
+ /** puts an array of double in property map. */
+ @NonNull
+ public Builder putInPropertyMap(@NonNull String name, @NonNull double[] values) {
+ mPropertyMap.put(name,
+ new PropertyParcel.Builder(name).setDoubleValues(values).build());
+ return this;
+ }
+
+ /** puts an array of long in property map. */
+ @NonNull
+ public Builder putInPropertyMap(@NonNull String name, @NonNull long[] values) {
+ mPropertyMap.put(name,
+ new PropertyParcel.Builder(name).setLongValues(values).build());
+ return this;
+ }
+
+ /**
+ * Converts and saves a byte[][] into {@link #mProperties}.
+ */
+ @NonNull
+ public Builder putInPropertyMap(@NonNull String name, @NonNull byte[][] values) {
+ mPropertyMap.put(name,
+ new PropertyParcel.Builder(name).setBytesValues(values).build());
+ return this;
+ }
+
+ /** puts an array of {@link GenericDocumentParcel} in property map. */
+ @NonNull
+ public Builder putInPropertyMap(@NonNull String name,
+ @NonNull GenericDocumentParcel[] values) {
+ mPropertyMap.put(name,
+ new PropertyParcel.Builder(name).setDocumentValues(values).build());
+ return this;
+ }
+
+ /** Builds the {@link GenericDocument} object. */
+ @NonNull
+ public GenericDocumentParcel build() {
+ mBuilt = true;
+ // Set current timestamp for creation timestamp by default.
+ if (mCreationTimestampMillis == INVALID_CREATION_TIMESTAMP_MILLIS) {
+ mCreationTimestampMillis = System.currentTimeMillis();
+ }
+ return new GenericDocumentParcel(
+ mNamespace,
+ mId,
+ mSchemaType,
+ mCreationTimestampMillis,
+ mTtlMillis,
+ mScore,
+ mPropertyMap.values().toArray(new PropertyParcel[0]));
+ }
+
+ void resetIfBuilt() {
+ if (mBuilt) {
+ Map<String, PropertyParcel> propertyMap = mPropertyMap;
+ mPropertyMap = new ArrayMap<>(propertyMap.size());
+ for (PropertyParcel value : propertyMap.values()) {
+ // PropertyParcel is not deep copied since it is not mutable.
+ mPropertyMap.put(value.getPropertyName(), value);
+ }
+ mBuilt = false;
+ }
+ }
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PropertyParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/safeparcel/PropertyParcel.java
similarity index 88%
rename from appsearch/appsearch/src/main/java/androidx/appsearch/app/PropertyParcel.java
rename to appsearch/appsearch/src/main/java/androidx/appsearch/app/safeparcel/PropertyParcel.java
index 7e6fa88..f9034a9 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PropertyParcel.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/safeparcel/PropertyParcel.java
@@ -14,10 +14,9 @@
* limitations under the License.
*/
-package androidx.appsearch.app;
+package androidx.appsearch.app.safeparcel;
-import android.os.Bundle;
import android.os.Parcel;
import androidx.annotation.NonNull;
@@ -26,7 +25,6 @@
import androidx.appsearch.safeparcel.AbstractSafeParcelable;
import androidx.appsearch.safeparcel.SafeParcelable;
import androidx.appsearch.safeparcel.stub.StubCreators.PropertyParcelCreator;
-import androidx.appsearch.util.BundleUtil;
import java.util.Arrays;
import java.util.Objects;
@@ -67,10 +65,9 @@
@Field(id = 6, getter = "getBytesValues")
private final byte[][] mBytesValues;
- // TODO(b/24205844) Change it to GenericDocumentParcel once it is added.
@Nullable
@Field(id = 7, getter = "getDocumentValues")
- private final Bundle[] mDocumentValues;
+ private final GenericDocumentParcel[] mDocumentValues;
@Nullable private Integer mHashCode;
@@ -82,7 +79,7 @@
@Param(id = 4) @Nullable double[] doubleValues,
@Param(id = 5) @Nullable boolean[] booleanValues,
@Param(id = 6) @Nullable byte[][] bytesValues,
- @Param(id = 7) @Nullable Bundle[] documentValues) {
+ @Param(id = 7) @Nullable GenericDocumentParcel[] documentValues) {
mPropertyName = Objects.requireNonNull(propertyName);
mStringValues = stringValues;
mLongValues = longValues;
@@ -129,9 +126,9 @@
return mBytesValues;
}
- /** Returns {@link Bundle} in an array. */
+ /** Returns {@link GenericDocumentParcel}s in an array. */
@Nullable
- public Bundle[] getDocumentValues() {
+ public GenericDocumentParcel[] getDocumentValues() {
return mDocumentValues;
}
@@ -209,13 +206,7 @@
} else if (mBytesValues != null) {
hashCode = Arrays.deepHashCode(mBytesValues);
} else if (mDocumentValues != null) {
- // TODO(b/24205844) change those to Arrays.hashCode() as well once we replace
- // this Bundle[] with GenericDocumentParcel[].
- int[] innerHashCodes = new int[mDocumentValues.length];
- for (int i = 0; i < mDocumentValues.length; ++i) {
- innerHashCodes[i] = BundleUtil.deepHashCode(mDocumentValues[i]);
- }
- hashCode = Arrays.hashCode(innerHashCodes);
+ hashCode = Arrays.hashCode(mDocumentValues);
}
mHashCode = Objects.hash(mPropertyName, hashCode);
}
@@ -239,9 +230,7 @@
&& Arrays.equals(mDoubleValues, otherPropertyParcel.mDoubleValues)
&& Arrays.equals(mBooleanValues, otherPropertyParcel.mBooleanValues)
&& Arrays.deepEquals(mBytesValues, otherPropertyParcel.mBytesValues)
- // TODO(b/24205844) Change it to Arrays.equals once GenericDocumentParcel is added.
- && BundleUtil.bundleValueEquals(
- mDocumentValues, otherPropertyParcel.mDocumentValues);
+ && Arrays.equals(mDocumentValues, otherPropertyParcel.mDocumentValues);
}
/** Builder for {@link PropertyParcel}. */
@@ -252,7 +241,7 @@
private double[] mDoubleValues;
private boolean[] mBooleanValues;
private byte[][] mBytesValues;
- private Bundle[] mDocumentValues;
+ private GenericDocumentParcel[] mDocumentValues;
public Builder(@NonNull String propertyName) {
mPropertyName = Objects.requireNonNull(propertyName);
@@ -295,7 +284,7 @@
/** Sets document values. */
@NonNull
- public Builder setDocumentValues(@NonNull Bundle[] documentValues) {
+ public Builder setDocumentValues(@NonNull GenericDocumentParcel[] documentValues) {
mDocumentValues = Objects.requireNonNull(documentValues);
return this;
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java
index 5b36ecd..c4f9241 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java
@@ -16,7 +16,9 @@
package androidx.appsearch.safeparcel.stub;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.safeparcel.GenericDocumentParcel;
import androidx.appsearch.app.safeparcel.PropertyConfigParcel;
+import androidx.appsearch.app.safeparcel.PropertyParcel;
/**
* Stub creators for any classes extending
@@ -33,7 +35,7 @@
public static class StorageInfoCreator extends AbstractCreator {
}
- /** Stub creator for {@link androidx.appsearch.app.PropertyParcel}. */
+ /** Stub creator for {@link PropertyParcel}. */
public static class PropertyParcelCreator extends AbstractCreator {
}
@@ -68,4 +70,8 @@
*/
public static class DocumentIndexingConfigParcelCreator extends AbstractCreator {
}
+
+ /** Stub creator for {@link GenericDocumentParcel}. */
+ public static class GenericDocumentParcelCreator extends AbstractCreator {
+ }
}
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt
index b4fdbc1..6c67eab 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt
@@ -421,8 +421,6 @@
// supported by a device. That is why we need to search from most specific to
// least specific. For e.g. emulators claim to support aarch64, when in reality
// they can only support x86 or x86_64.
- // Note: Cuttlefish is x86 but claims support for x86_64
- Build.MODEL.contains("Cuttlefish") -> "x86" // TODO(204892353): handle properly
Build.SUPPORTED_64_BIT_ABIS.any { it.startsWith("x86_64") } -> "x86_64"
Build.SUPPORTED_32_BIT_ABIS.any { it.startsWith("x86") } -> "x86"
Build.SUPPORTED_64_BIT_ABIS.any { it.startsWith("arm64") } -> "aarch64"
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 73b8580..5fe690c 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -460,6 +460,9 @@
it.artRewritingWorkaround()
}
}
+
+ project.buildOnServerDependsOnAssembleRelease()
+ project.buildOnServerDependsOnLint()
}
private fun configureWithTestPlugin(project: Project, androidXExtension: AndroidXExtension) {
@@ -473,6 +476,30 @@
project.addToProjectMap(androidXExtension)
}
+ private fun Project.buildOnServerDependsOnAssembleRelease() {
+ project.addToBuildOnServer("assembleRelease")
+ }
+
+ private fun Project.buildOnServerDependsOnLint() {
+ if (!project.usingMaxDepVersions()) {
+ project.agpVariants.all { variant ->
+ // in AndroidX, release and debug variants are essentially the same,
+ // so we don't run the lintRelease task on the build server
+ if (!variant.name.lowercase(Locale.getDefault()).contains("release")) {
+ val taskName =
+ "lint${variant.name.replaceFirstChar {
+ if (it.isLowerCase()) {
+ it.titlecase(Locale.getDefault())
+ } else {
+ it.toString()
+ }
+ }}"
+ project.addToBuildOnServer(taskName)
+ }
+ }
+ }
+ }
+
private fun HasAndroidTest.configureTests() {
configureLicensePackaging()
excludeVersionFilesFromTestApks()
@@ -595,6 +622,9 @@
)
project.addToProjectMap(androidXExtension)
+
+ project.buildOnServerDependsOnAssembleRelease()
+ project.buildOnServerDependsOnLint()
}
private fun configureWithJavaPlugin(project: Project, extension: AndroidXExtension) {
@@ -657,6 +687,8 @@
configuration.resolutionStrategy.preferProjectModules()
}
+ project.addToBuildOnServer("jar")
+
project.addToProjectMap(extension)
}
@@ -792,14 +824,20 @@
File(project.buildDir, "../nativeBuildStaging")
}
+ @Suppress("UnstableApiUsage") // finalizeDsl, minCompileSdkExtension
private fun LibraryExtension.configureAndroidLibraryOptions(
project: Project,
androidXExtension: AndroidXExtension
) {
- // Note, this should really match COMPILE_SDK_VERSION, however
- // this API takes an integer and we are unable to set it to a
- // pre-release SDK.
- defaultConfig.aarMetadata.minCompileSdk = project.defaultAndroidConfig.targetSdk
+ // Propagate the compileSdk value into minCompileSdk. Don't propagate compileSdkExtension,
+ // since only one library actually depends on the extension APIs and they can explicitly
+ // declare that in their build.gradle. Note that when we're using a preview SDK, the value
+ // for compileSdk will be null and the resulting AAR metadata won't have a minCompileSdk --
+ // this is okay because AGP automatically embeds forceCompileSdkPreview in the AAR metadata
+ // and uses it instead of minCompileSdk.
+ project.extensions.findByType<LibraryAndroidComponentsExtension>()!!.finalizeDsl {
+ it.defaultConfig.aarMetadata.minCompileSdk = it.compileSdk
+ }
// The full Guava artifact is very large, so they split off a special artifact containing a
// standalone version of the commonly-used ListenableFuture interface. However, they also
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
index fc33853..9eb83d2 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -29,16 +29,12 @@
import androidx.build.uptodatedness.TaskUpToDateValidator
import androidx.build.uptodatedness.cacheEvenIfNoOutputs
import com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
-import com.android.build.gradle.AppPlugin
-import com.android.build.gradle.LibraryPlugin
import java.io.File
-import java.util.Locale
import java.util.concurrent.ConcurrentHashMap
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.component.ModuleComponentSelector
-import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.plugins.JvmEcosystemPlugin
import org.gradle.api.tasks.bundling.Zip
import org.gradle.api.tasks.bundling.ZipEntryCompression
@@ -101,37 +97,6 @@
}
extra.set("projects", ConcurrentHashMap<String, String>())
- subprojects { project ->
- project.afterEvaluate {
- if (
- project.plugins.hasPlugin(LibraryPlugin::class.java) ||
- project.plugins.hasPlugin(AppPlugin::class.java)
- ) {
-
- buildOnServerTask.dependsOn("${project.path}:assembleRelease")
- if (!project.usingMaxDepVersions()) {
- project.agpVariants.all { variant ->
- // in AndroidX, release and debug variants are essentially the same,
- // so we don't run the lintRelease task on the build server
- if (!variant.name.lowercase(Locale.getDefault()).contains("release")) {
- val taskName =
- "lint${variant.name.replaceFirstChar {
- if (it.isLowerCase()) {
- it.titlecase(Locale.getDefault())
- } else {
- it.toString()
- }
- }}"
- buildOnServerTask.dependsOn("${project.path}:$taskName")
- }
- }
- }
- }
- }
- project.plugins.withType(JavaPlugin::class.java) {
- buildOnServerTask.dependsOn("${project.path}:jar")
- }
- }
// NOTE: this task is used by the Github CI as well. If you make any changes here,
// please update the .github/workflows files as well, if necessary.
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt b/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt
index 669aa02..7b470d9 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt
@@ -239,7 +239,7 @@
task.taskExtension.set(
object : DefaultSpdxSbomTaskExtension() {
- override fun mapRepoUri(repoUri: URI, artifact: ModuleVersionIdentifier): URI {
+ override fun mapRepoUri(repoUri: URI?, artifact: ModuleVersionIdentifier): URI {
val uriString = repoUri.toString()
for (repo in repos) {
val ourRepoUrl = repo.key
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/BuildOnServer.kt b/buildSrc/public/src/main/kotlin/androidx/build/BuildOnServer.kt
index c29f465..99ed273 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/BuildOnServer.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/BuildOnServer.kt
@@ -22,12 +22,12 @@
const val BUILD_ON_SERVER_TASK = "buildOnServer"
-/** Configures the root project's buildOnServer task to run the specified task. */
+/** Configures the project's buildOnServer task to run the specified task. */
fun <T : Task> Project.addToBuildOnServer(taskProvider: TaskProvider<T>) {
tasks.named(BUILD_ON_SERVER_TASK).configure { it.dependsOn(taskProvider) }
}
-/** Configures the root project's buildOnServer task to run the specified task. */
-fun <T : Task> Project.addToBuildOnServer(taskPath: String) {
+/** Configures the project's buildOnServer task to run the specified task. */
+fun Project.addToBuildOnServer(taskPath: String) {
tasks.named(BUILD_ON_SERVER_TASK).configure { it.dependsOn(taskPath) }
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
index f8d6362..6fd951c 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -210,6 +210,14 @@
return setOf(SDR)
}
+ override fun isPreviewStabilizationSupported(): Boolean {
+ return false
+ }
+
+ override fun isVideoStabilizationSupported(): Boolean {
+ return false
+ }
+
private fun profileSetToDynamicRangeSet(profileSet: Set<Long>): Set<DynamicRange> {
return profileSet.map { profileToDynamicRange(it) }.toSet()
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
index 401c134..420f747 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
@@ -178,6 +178,14 @@
override fun getSupportedDynamicRanges(): MutableSet<DynamicRange> {
throw NotImplementedError("Not used in testing")
}
+
+ override fun isPreviewStabilizationSupported(): Boolean {
+ throw NotImplementedError("Not used in testing")
+ }
+
+ override fun isVideoStabilizationSupported(): Boolean {
+ throw NotImplementedError("Not used in testing")
+ }
}
Camera2CameraInfo.from(wrongCameraInfo)
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt
index ec0be0e..7274b3b 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt
@@ -36,8 +36,7 @@
* This allows all fields to be accessed and return reasonable values on all OS versions.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-internal class Camera2CameraMetadata
-constructor(
+internal class Camera2CameraMetadata(
override val camera: CameraId,
override val isRedacted: Boolean,
private val characteristics: CameraCharacteristics,
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt
index e7187fe..ba3953d7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt
@@ -17,7 +17,9 @@
package androidx.camera.camera2.pipe.compat
import android.content.Context
+import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
+import android.os.Build
import android.util.ArrayMap
import androidx.annotation.GuardedBy
import androidx.annotation.RequiresApi
@@ -105,7 +107,13 @@
// Merge the camera specific and global cache blocklists together.
// this will prevent these values from being cached after first access.
- val cameraBlocklist = cameraMetadataConfig.cameraCacheBlocklist[cameraId]
+ val cameraBlocklist =
+ if (shouldBlockSensorOrientationCache(characteristics)) {
+ (cameraMetadataConfig.cameraCacheBlocklist[cameraId] ?: emptySet()) +
+ CameraCharacteristics.SENSOR_ORIENTATION
+ } else {
+ cameraMetadataConfig.cameraCacheBlocklist[cameraId]
+ }
val cacheBlocklist =
if (cameraBlocklist == null) {
cameraMetadataConfig.cacheBlocklist
@@ -146,4 +154,9 @@
}
private fun isMetadataRedacted(): Boolean = !permissions.hasCameraPermission
+
+ private fun shouldBlockSensorOrientationCache(characteristics: CameraCharacteristics): Boolean {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2 &&
+ characteristics[CameraCharacteristics.INFO_DEVICE_STATE_SENSOR_ORIENTATION_MAP] != null
+ }
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index e6da980..edcf484 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -55,6 +55,7 @@
import androidx.camera.core.ExposureState;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.Logger;
+import androidx.camera.core.PreviewCapabilities;
import androidx.camera.core.ZoomState;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraInfoInternal;
@@ -507,6 +508,12 @@
}
}
+ @NonNull
+ @Override
+ public PreviewCapabilities getPreviewCapabilities() {
+ return Camera2PreviewCapabilities.from(this);
+ }
+
@Override
public boolean isVideoStabilizationSupported() {
int[] availableVideoStabilizationModes =
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2PreviewCapabilities.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2PreviewCapabilities.java
new file mode 100644
index 0000000..aa25edd
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2PreviewCapabilities.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.PreviewCapabilities;
+import androidx.camera.core.impl.CameraInfoInternal;
+
+/**
+ * Camera2 implementation of {@link PreviewCapabilities}.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class Camera2PreviewCapabilities implements PreviewCapabilities {
+
+ private boolean mIsStabilizationSupported = false;
+
+ Camera2PreviewCapabilities(@NonNull CameraInfoInternal cameraInfoInternal) {
+ mIsStabilizationSupported = cameraInfoInternal.isPreviewStabilizationSupported();
+ }
+
+
+ @NonNull
+ static Camera2PreviewCapabilities from(@NonNull CameraInfo cameraInfo) {
+ return new Camera2PreviewCapabilities((CameraInfoInternal) cameraInfo);
+ }
+
+
+ @Override
+ public boolean isStabilizationSupported() {
+ return mIsStabilizationSupported;
+ }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index 5908b96..8a876ca 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -687,7 +687,7 @@
init(/* hasAvailableCapabilities = */ false);
// Camera0
- CameraInfo cameraInfo0 = new Camera2CameraInfoImpl(CAMERA0_ID,
+ Camera2CameraInfoImpl cameraInfo0 = new Camera2CameraInfoImpl(CAMERA0_ID,
mCameraManagerCompat);
@@ -699,14 +699,14 @@
assertThat(cameraInfo0.isVideoStabilizationSupported()).isTrue();
// Camera1
- CameraInfo cameraInfo1 = new Camera2CameraInfoImpl(CAMERA1_ID,
+ Camera2CameraInfoImpl cameraInfo1 = new Camera2CameraInfoImpl(CAMERA1_ID,
mCameraManagerCompat);
assertThat(cameraInfo1.isPreviewStabilizationSupported()).isFalse();
assertThat(cameraInfo0.isVideoStabilizationSupported()).isTrue();
// Camera2
- CameraInfo cameraInfo2 = new Camera2CameraInfoImpl(CAMERA2_ID,
+ Camera2CameraInfoImpl cameraInfo2 = new Camera2CameraInfoImpl(CAMERA2_ID,
mCameraManagerCompat);
assertThat(cameraInfo2.isPreviewStabilizationSupported()).isFalse();
assertThat(cameraInfo2.isVideoStabilizationSupported()).isFalse();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
index 8c0750e..4d7519d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
@@ -298,23 +298,14 @@
}
/**
- * Returns if video stabilization is supported on the device.
+ * Returns {@link PreviewCapabilities} to query preview stream related device capability.
*
- * @return true if supported, otherwise false.
+ * @return {@link PreviewCapabilities}
*/
+ @NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
- default boolean isVideoStabilizationSupported() {
- return false;
- }
-
- /**
- * Returns if preview stabilization is supported on the device.
- *
- * @return true if supported, otherwise false.
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- default boolean isPreviewStabilizationSupported() {
- return false;
+ default PreviewCapabilities getPreviewCapabilities() {
+ return PreviewCapabilities.EMPTY;
}
/**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/PreviewCapabilities.java b/camera/camera-core/src/main/java/androidx/camera/core/PreviewCapabilities.java
new file mode 100644
index 0000000..283b013
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/PreviewCapabilities.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ * PreviewCapabilities is used to query preview stream capabilities on the device.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public interface PreviewCapabilities {
+
+ /**
+ * Returns if preview stabilization is supported on the device.
+ *
+ * @return true if
+ * {@link CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION} is supported,
+ * otherwise false.
+ *
+ * @see CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE
+ */
+ boolean isStabilizationSupported();
+
+
+ /** An empty implementation. */
+ @NonNull
+ PreviewCapabilities EMPTY = new PreviewCapabilities() {
+ @Override
+ public boolean isStabilizationSupported() {
+ return false;
+ }
+ };
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
index e6a1e9b..d78c73a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
@@ -18,6 +18,7 @@
import android.graphics.ImageFormat;
import android.graphics.PixelFormat;
+import android.hardware.camera2.CaptureRequest;
import android.util.Size;
import androidx.annotation.NonNull;
@@ -106,6 +107,27 @@
Set<DynamicRange> getSupportedDynamicRanges();
/**
+ * Returns if preview stabilization is supported on the device.
+ *
+ * @return true if
+ * {@link CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION} is supported,
+ * otherwise false.
+ *
+ * @see CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE
+ */
+ boolean isPreviewStabilizationSupported();
+
+ /**
+ * Returns if video stabilization is supported on the device.
+ *
+ * @return true if {@link CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE_ON} is supported,
+ * otherwise false.
+ *
+ * @see CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE
+ */
+ boolean isVideoStabilizationSupported();
+
+ /**
* Gets the underlying implementation instance which could be cast into an implementation
* specific class for further use in implementation module. Returns <code>this</code> if this
* instance is the implementation instance.
@@ -115,7 +137,6 @@
return this;
}
-
/** {@inheritDoc} */
@NonNull
@Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java
index ab1314e..be3b3b8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java
@@ -46,8 +46,8 @@
* Returns whether this configuration contains the supplied option.
*
* @param id The {@link Option} to search for in this configuration.
- * @return <code>true</code> if this configuration contains the supplied option; <code>false
- * </code> otherwise.
+ * @return {@code true} if this configuration contains the supplied option; {@code false}
+ * otherwise.
*/
boolean containsOption(@NonNull Option<?> id);
@@ -77,7 +77,7 @@
* @param valueIfMissing The value to return if the specified {@link Option} does not exist in
* this configuration.
* @param <ValueT> The type for the value associated with the supplied {@link Option}.
- * @return The value stored in this configuration, or <code>valueIfMissing</code> if it does
+ * @return The value stored in this configuration, or {@code valueIfMissing} if it does
* not exist.
*/
@Nullable
@@ -116,12 +116,10 @@
* option such as \"<code>
* camerax.core.example</code>\".
* @param matcher A callback used to receive results of the search. Results will be
- * sent to
- * {@link OptionMatcher#onOptionMatched(Option)} in the order in which
- * they are found inside
- * this configuration. Subsequent results will continue to be sent as
- * long as {@link
- * OptionMatcher#onOptionMatched(Option)} returns <code>true</code>.
+ * sent to {@link OptionMatcher#onOptionMatched(Option)} in the order
+ * in which they are found inside this configuration. Subsequent
+ * results will continue to be sent as long as {@link
+ * OptionMatcher#onOptionMatched(Option)} returns {@code true}.
*/
void findOptions(@NonNull String idSearchString, @NonNull OptionMatcher matcher);
@@ -199,8 +197,7 @@
* @param valueClass The class of the value stored by this option.
* @param <T> The type of the value stored by this option.
* @param token An optional, type-erased object for storing more context for this
- * specific
- * option. Generally this object should have static scope and be
+ * specific option. Generally this object should have static scope and be
* immutable.
* @return An {@link Option} object which can be used to store/retrieve values from a {@link
* Config}.
@@ -250,11 +247,14 @@
*/
enum OptionPriority {
/**
- * Should only be used externally by apps. It takes precedence over any other option
- * values at the risk of causing unexpected behavior.
+ * It takes precedence over any other option values at the risk of causing unexpected
+ * behavior.
*
- * <p>This should not used internally in CameraX. It conflicts when merging different
- * values set to ALWAY_OVERRIDE.
+ * <p>If the same option is already set, the option with this priority will overwrite the
+ * value.
+ *
+ * <p>This priority should only be used to explicitly specify an option, such as used by
+ * {@code Camera2Interop} or {@code Camera2CameraControl}, and should be used with caution.
*/
ALWAYS_OVERRIDE,
@@ -262,15 +262,17 @@
* It's a required option value in order to achieve expected CameraX behavior. It takes
* precedence over {@link #OPTIONAL} option values.
*
- * <p>If apps set ALWAYS_OVERRIDE options, it'll override REQUIRED option values and can
- * potentially cause unexpected behaviors. It conflicts when merging different values set
- * to REQUIRED.
+ * <p>If two values are set to the same option, the value with {@link #ALWAYS_OVERRIDE}
+ * priority will overwrite this priority and can potentially cause unexpected behaviors.
+ *
+ * <p>If two values are set to the same option with this priority, it might indicate a
+ * programming error internally and an exception will be thrown when merging the configs.
*/
REQUIRED,
/**
* The lowest priority, it can be overridden by any other option value. When two option
- * values are set as OPTIONAL, the newer value takes precedence over the old one.
+ * values are set with this priority, the newer value takes precedence over the old one.
*/
OPTIONAL
}
@@ -278,35 +280,25 @@
/**
* Returns if values with these {@link OptionPriority} conflict or not.
*
- * Currently it is not allowed to have different values with same ALWAYS_OVERRIDE
- * priority or to have different values with same REQUIRED priority.
+ * <p>Currently it is not allowed the same option to have different values with priority
+ * {@link OptionPriority#REQUIRED}.
*/
static boolean hasConflict(@NonNull OptionPriority priority1,
@NonNull OptionPriority priority2) {
- if (priority1 == OptionPriority.ALWAYS_OVERRIDE
- && priority2 == OptionPriority.ALWAYS_OVERRIDE) {
- return true;
- }
-
- if (priority1 == OptionPriority.REQUIRED
- && priority2 == OptionPriority.REQUIRED) {
- return true;
- }
-
- return false;
+ return priority1 == OptionPriority.REQUIRED
+ && priority2 == OptionPriority.REQUIRED;
}
/**
- * Merges two configs
+ * Merges two configs.
*
* @param extendedConfig the extended config. The options in the extendedConfig will be applied
* on top of the baseConfig based on the option priorities.
- * @param baseConfig the base config
- * @return a {@link MutableOptionsBundle} of the merged config
+ * @param baseConfig the base config.
+ * @return a {@link MutableOptionsBundle} of the merged config.
*/
@NonNull
- static Config mergeConfigs(@Nullable Config extendedConfig,
- @Nullable Config baseConfig) {
+ static Config mergeConfigs(@Nullable Config extendedConfig, @Nullable Config baseConfig) {
if (extendedConfig == null && baseConfig == null) {
return OptionsBundle.emptyBundle();
}
@@ -333,12 +325,12 @@
/**
* Merges a specific option value from two configs.
*
- * @param mergedConfig the final output config
+ * @param mergedConfig the final output config.
* @param baseConfig the base config contains the option value which might be overridden by
* the corresponding option value in the extend config.
* @param extendedConfig the extended config contains the option value which might override
* the corresponding option value in the base config.
- * @param opt the option to merge
+ * @param opt the option to merge.
*/
static void mergeOptionValue(@NonNull MutableOptionsBundle mergedConfig,
@NonNull Config baseConfig,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
index c389a1d..0bd54ec 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
@@ -27,6 +27,7 @@
import androidx.camera.core.ExperimentalZeroShutterLag;
import androidx.camera.core.ExposureState;
import androidx.camera.core.FocusMeteringAction;
+import androidx.camera.core.PreviewCapabilities;
import androidx.camera.core.ZoomState;
import androidx.lifecycle.LiveData;
@@ -192,4 +193,20 @@
public CameraSelector getCameraSelector() {
return mCameraInfoInternal.getCameraSelector();
}
+
+ @Override
+ public boolean isPreviewStabilizationSupported() {
+ return mCameraInfoInternal.isPreviewStabilizationSupported();
+ }
+
+ @Override
+ public boolean isVideoStabilizationSupported() {
+ return mCameraInfoInternal.isVideoStabilizationSupported();
+ }
+
+ @NonNull
+ @Override
+ public PreviewCapabilities getPreviewCapabilities() {
+ return mCameraInfoInternal.getPreviewCapabilities();
+ }
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/LargeJpegImageQuirk.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/LargeJpegImageQuirk.java
index c6ad529..471095e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/LargeJpegImageQuirk.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/LargeJpegImageQuirk.java
@@ -28,16 +28,16 @@
/**
* <p>QuirkSummary
- * Bug Id: 288828159
+ * Bug Id: 288828159, 299069235
* Description: Quirk required to check whether the captured JPEG image contains redundant
* 0's padding data. For example, Samsung A5 (2017) series devices have the
* problem and result in the output JPEG image to be extremely large (about 32 MB).
- * Device(s): Samsung Galaxy A5 (2017), A52, A70, A72 and S7 series devices
+ * Device(s): Samsung Galaxy A5 (2017), A52, A70, A72, S7 series devices and Vivo S16 device
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public final class LargeJpegImageQuirk implements Quirk {
- private static final Set<String> DEVICE_MODELS = new HashSet<>(Arrays.asList(
+ private static final Set<String> SAMSUNG_DEVICE_MODELS = new HashSet<>(Arrays.asList(
// Samsung Galaxy A5 series devices
"SM-A520F",
"SM-A520L",
@@ -70,7 +70,22 @@
"SM-S906B"
));
+ private static final Set<String> VIVO_DEVICE_MODELS = new HashSet<>(Arrays.asList(
+ // Vivo S16
+ "V2244A"
+ ));
+
static boolean load() {
- return DEVICE_MODELS.contains(Build.MODEL.toUpperCase(Locale.US));
+ return isSamsungProblematicDevice() || isVivoProblematicDevice();
+ }
+
+ private static boolean isSamsungProblematicDevice() {
+ return "Samsung".equalsIgnoreCase(Build.BRAND) && SAMSUNG_DEVICE_MODELS.contains(
+ Build.MODEL.toUpperCase(Locale.US));
+ }
+
+ private static boolean isVivoProblematicDevice() {
+ return "Vivo".equalsIgnoreCase(Build.BRAND) && VIVO_DEVICE_MODELS.contains(
+ Build.MODEL.toUpperCase(Locale.US));
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParser.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParser.java
index a9b122a..4a0d527 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParser.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParser.java
@@ -18,6 +18,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
import androidx.camera.core.internal.compat.quirk.DeviceQuirks;
import androidx.camera.core.internal.compat.quirk.LargeJpegImageQuirk;
@@ -41,12 +42,23 @@
return bytes.length;
}
+ int jfifEoiMarkEndPosition = getJfifEoiMarkEndPosition(bytes);
+
+ return jfifEoiMarkEndPosition != -1 ? jfifEoiMarkEndPosition : bytes.length;
+ }
+
+ /**
+ * Returns the end position of JFIF EOI mark. Returns -1 while JFIF EOI mark can't be found
+ * in the provided byte array.
+ */
+ @VisibleForTesting
+ public static int getJfifEoiMarkEndPosition(@NonNull byte[] bytes) {
// Parses the JFIF segments from the start of the JPEG image data
int markPosition = 0x2;
while (true) {
// Breaks the while-loop and return null if the mark byte can't be correctly found.
if (markPosition + 4 > bytes.length || bytes[markPosition] != ((byte) 0xff)) {
- return bytes.length;
+ return -1;
}
int segmentLength =
@@ -65,7 +77,7 @@
while (true) {
// Breaks the while-loop and return null if EOI mark can't be found
if (eoiPosition + 2 > bytes.length) {
- return bytes.length;
+ return -1;
}
if (bytes[eoiPosition] == ((byte) 0xff) && bytes[eoiPosition + 1] == ((byte) 0xd9)) {
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/ConfigTest.java b/camera/camera-core/src/test/java/androidx/camera/core/impl/ConfigTest.java
index 96b086a..e78d689f 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/ConfigTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/ConfigTest.java
@@ -89,8 +89,8 @@
}
@Test
- public void hasConflict_whenTwoValueAreALWAYSOVERRIDE() {
- assertThat(Config.hasConflict(ALWAYS_OVERRIDE, ALWAYS_OVERRIDE)).isTrue();
+ public void noConflict_whenTwoValueAreALWAYSOVERRIDE() {
+ assertThat(Config.hasConflict(ALWAYS_OVERRIDE, ALWAYS_OVERRIDE)).isFalse();
}
@Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/MutableOptionsBundleTest.java b/camera/camera-core/src/test/java/androidx/camera/core/impl/MutableOptionsBundleTest.java
index 1db4883..be0e91c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/MutableOptionsBundleTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/MutableOptionsBundleTest.java
@@ -108,13 +108,18 @@
assertThat(config2.retrieveOptionWithPriority(OPTION_2, OPTIONAL)).isEqualTo(VALUE_1);
}
- @Test(expected = IllegalArgumentException.class)
+ @Test
public void insertOption_ALWAYSOVERRIDE_ALWAYSOVERRIDE() {
MutableOptionsBundle mutOpts = MutableOptionsBundle.create();
mutOpts.insertOption(OPTION_1, ALWAYS_OVERRIDE, VALUE_1);
- // should throw an Error
mutOpts.insertOption(OPTION_1, ALWAYS_OVERRIDE, VALUE_2);
+
+ assertThat(mutOpts.retrieveOption(OPTION_1)).isEqualTo(VALUE_2);
+ Config.OptionPriority highestPriority = Collections.min(mutOpts.getPriorities(OPTION_1));
+ assertThat(highestPriority).isEqualTo(ALWAYS_OVERRIDE);
+ assertThat(mutOpts.retrieveOptionWithPriority(OPTION_1, highestPriority))
+ .isEqualTo(VALUE_2);
}
@Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParserTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParserTest.kt
index cdd2576..127b136 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParserTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParserTest.kt
@@ -73,31 +73,34 @@
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class InvalidJpegDataParserTest(
+ private val brand: String,
private val model: String,
private val data: ByteArray,
private val validDataLength: Int,
- ) {
+) {
companion object {
@JvmStatic
- @ParameterizedRobolectricTestRunner.Parameters(name = "model={0}, data={1}, length={2}")
+ @ParameterizedRobolectricTestRunner.Parameters(
+ name = "brand={0}, model={1}, data={2}, length={3}")
fun data() = mutableListOf<Array<Any?>>().apply {
- add(arrayOf("SM-A520F", problematicJpegByteArray, 18))
- add(arrayOf("SM-A520F", problematicJpegByteArray2, 18))
- add(arrayOf("SM-A520F", correctJpegByteArray1, 18))
- add(arrayOf("SM-A520F", correctJpegByteArray2, 18))
- add(arrayOf("SM-A520F", invalidVeryShortData, 2))
- add(arrayOf("SM-A520F", invalidNoSosData, 28))
- add(arrayOf("SM-A520F", invalidNoEoiData, 28))
- add(arrayOf("fake-model", problematicJpegByteArray, 42))
- add(arrayOf("fake-model", problematicJpegByteArray2, 64))
- add(arrayOf("fake-model", correctJpegByteArray1, 28))
- add(arrayOf("fake-model", correctJpegByteArray2, 18))
+ add(arrayOf("SAMSUNG", "SM-A520F", problematicJpegByteArray, 18))
+ add(arrayOf("SAMSUNG", "SM-A520F", problematicJpegByteArray2, 18))
+ add(arrayOf("SAMSUNG", "SM-A520F", correctJpegByteArray1, 18))
+ add(arrayOf("SAMSUNG", "SM-A520F", correctJpegByteArray2, 18))
+ add(arrayOf("SAMSUNG", "SM-A520F", invalidVeryShortData, 2))
+ add(arrayOf("SAMSUNG", "SM-A520F", invalidNoSosData, 28))
+ add(arrayOf("SAMSUNG", "SM-A520F", invalidNoEoiData, 28))
+ add(arrayOf("fake-brand", "fake-model", problematicJpegByteArray, 42))
+ add(arrayOf("fake-brand", "fake-model", problematicJpegByteArray2, 64))
+ add(arrayOf("fake-brand", "fake-model", correctJpegByteArray1, 28))
+ add(arrayOf("fake-brand", "fake-model", correctJpegByteArray2, 18))
}
}
@Test
fun canGetValidJpegDataLength() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", brand)
ReflectionHelpers.setStaticField(Build::class.java, "MODEL", model)
assertThat(InvalidJpegDataParser().getValidDataLength(data)).isEqualTo(validDataLength)
}
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheckTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheckTest.kt
index abd3bda..9bc767e 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheckTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheckTest.kt
@@ -17,6 +17,7 @@
package androidx.camera.extensions.internal.compat.workaround
import androidx.camera.extensions.internal.compat.workaround.OnEnableDisableSessionDurationCheck.MIN_DURATION_FOR_ENABLE_DISABLE_SESSION
+import androidx.camera.testing.impl.AndroidUtil
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
@@ -24,6 +25,8 @@
import kotlin.system.measureTimeMillis
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
+import org.junit.Assume.assumeFalse
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -35,6 +38,11 @@
const val TOLERANCE = 60L
}
+ @Before
+ fun setUp() {
+ assumeFalse(AndroidUtil.isEmulatorAndAPI21())
+ }
+
@Test
fun enabled_ensureMinimalDuration() = runBlocking {
// Arrange
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
index 7a4b53a..8b28e6c 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
@@ -299,6 +299,16 @@
return mIntrinsicZoomRatio;
}
+ @Override
+ public boolean isPreviewStabilizationSupported() {
+ return false;
+ }
+
+ @Override
+ public boolean isVideoStabilizationSupported() {
+ return false;
+ }
+
/** Adds a quirk to the list of this camera's quirks. */
@SuppressWarnings("unused")
public void addCameraQuirk(@NonNull final Quirk quirk) {
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/E2ETestUtil.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/FileUtil.kt
similarity index 60%
rename from camera/camera-testing/src/main/java/androidx/camera/testing/impl/E2ETestUtil.kt
rename to camera/camera-testing/src/main/java/androidx/camera/testing/impl/FileUtil.kt
index e454f702..b482047 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/E2ETestUtil.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/FileUtil.kt
@@ -18,10 +18,12 @@
import android.content.ContentResolver
import android.content.ContentValues
+import android.net.Uri
import android.os.Environment.DIRECTORY_DOCUMENTS
import android.os.Environment.DIRECTORY_MOVIES
import android.os.Environment.getExternalStoragePublicDirectory
import android.provider.MediaStore
+import android.util.Log
import androidx.annotation.RequiresApi
import androidx.camera.core.Logger
import androidx.camera.video.FileOutputOptions
@@ -32,12 +34,12 @@
import java.io.FileOutputStream
import java.io.OutputStreamWriter
-private const val TAG = "E2ETestUtil"
+private const val TAG = "FileUtil"
private const val EXTENSION_MP4 = "mp4"
private const val EXTENSION_TEXT = "txt"
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-object E2ETestUtil {
+object FileUtil {
/**
* Write the given text to the external storage.
@@ -101,7 +103,7 @@
): FileOutputOptions {
val fileNameWithExtension = "$fileName.$extension"
val folder = getExternalStoragePublicDirectory(DIRECTORY_MOVIES)
- if (!folder.exists() && !folder.mkdirs()) {
+ if (!createFolder(folder)) {
Logger.e(TAG, "Failed to create directory: $folder")
}
return FileOutputOptions.Builder(File(folder, fileNameWithExtension)).build()
@@ -119,14 +121,10 @@
fun generateVideoMediaStoreOptions(
contentResolver: ContentResolver,
fileName: String
- ): MediaStoreOutputOptions {
- val contentValues = generateVideoContentValues(fileName)
-
- return MediaStoreOutputOptions.Builder(
- contentResolver,
- MediaStore.Video.Media.EXTERNAL_CONTENT_URI
- ).setContentValues(contentValues).build()
- }
+ ): MediaStoreOutputOptions = MediaStoreOutputOptions.Builder(
+ contentResolver,
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+ ).setContentValues(generateVideoContentValues(fileName)).build()
/**
* Check if the given file name string is valid.
@@ -145,14 +143,79 @@
return !fileName.isNullOrBlank()
}
- private fun generateVideoContentValues(fileName: String): ContentValues {
- val res = ContentValues()
- res.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
- res.put(MediaStore.Video.Media.TITLE, fileName)
- res.put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
- res.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000)
- res.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis())
+ /**
+ * Creates parent folder for the input file path.
+ *
+ * @param filePath the input file path to create its parent folder.
+ * @return `true` if the parent folder already exists or is created successfully.
+ * `false` if the existing parent folder path is not a folder or failed to create.
+ */
+ @JvmStatic
+ fun createParentFolder(filePath: String): Boolean {
+ return createParentFolder(File(filePath))
+ }
- return res
+ /**
+ * Creates parent folder for the input file.
+ *
+ * @param file the input file to create its parent folder
+ * @return `true` if the parent folder already exists or is created successfully.
+ * `false` if the existing parent folder path is not a folder or failed to create.
+ */
+ @JvmStatic
+ fun createParentFolder(file: File): Boolean = file.parentFile?.let {
+ createFolder(it)
+ } ?: false
+
+ /**
+ * Creates folder for the input file.
+ *
+ * @param file the input file to create folder
+ * @return `true` if the folder already exists or is created successfully.
+ * `false` if the existing folder path is not a folder or failed to create.
+ */
+ @JvmStatic
+ fun createFolder(file: File): Boolean = if (file.exists()) {
+ file.isDirectory
+ } else {
+ file.mkdirs()
+ }
+
+ /**
+ * Gets the absolute path from a Uri.
+ *
+ * @param resolver the content resolver.
+ * @param contentUri the content uri.
+ * @return the file path of the content uri or null if not found.
+ */
+ @JvmStatic
+ fun getAbsolutePathFromUri(resolver: ContentResolver, contentUri: Uri): String? {
+ // MediaStore.Video.Media.DATA was deprecated in API level 29.
+ val column = MediaStore.Video.Media.DATA
+ try {
+ resolver.query(contentUri, arrayOf(column), null, null, null)!!.use { cursor ->
+ val columnIndex = cursor.getColumnIndexOrThrow(column)
+ cursor.moveToFirst()
+ return cursor.getString(columnIndex)
+ }
+ } catch (e: RuntimeException) {
+ Log.e(
+ TAG,
+ String.format(
+ "Failed in getting absolute path for Uri %s with Exception %s",
+ contentUri, e
+ ), e
+ )
+ return null
+ }
+ }
+
+ private fun generateVideoContentValues(fileName: String) = ContentValues().apply {
+ put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
+ put(MediaStore.Video.Media.TITLE, fileName)
+ put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
+ val currentTimeMs = System.currentTimeMillis()
+ put(MediaStore.Video.Media.DATE_ADDED, currentTimeMs / 1000)
+ put(MediaStore.Video.Media.DATE_TAKEN, currentTimeMs)
}
}
diff --git a/camera/camera-video/api/current.txt b/camera/camera-video/api/current.txt
index b9cc77a..7f1314e 100644
--- a/camera/camera-video/api/current.txt
+++ b/camera/camera-video/api/current.txt
@@ -2,10 +2,12 @@
package androidx.camera.video {
@RequiresApi(21) @com.google.auto.value.AutoValue public abstract class AudioStats {
+ method public double getAudioAmplitude();
method public abstract int getAudioState();
method public abstract Throwable? getErrorCause();
method public boolean hasAudio();
method public boolean hasError();
+ field public static final double AUDIO_AMPLITUDE_NONE = 0.0;
field public static final int AUDIO_STATE_ACTIVE = 0; // 0x0
field public static final int AUDIO_STATE_DISABLED = 1; // 0x1
field public static final int AUDIO_STATE_ENCODER_ERROR = 3; // 0x3
@@ -14,6 +16,9 @@
field public static final int AUDIO_STATE_SOURCE_SILENCED = 2; // 0x2
}
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAudioApi {
+ }
+
@SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalPersistentRecording {
}
diff --git a/camera/camera-video/api/restricted_current.txt b/camera/camera-video/api/restricted_current.txt
index b9cc77a..7f1314e 100644
--- a/camera/camera-video/api/restricted_current.txt
+++ b/camera/camera-video/api/restricted_current.txt
@@ -2,10 +2,12 @@
package androidx.camera.video {
@RequiresApi(21) @com.google.auto.value.AutoValue public abstract class AudioStats {
+ method public double getAudioAmplitude();
method public abstract int getAudioState();
method public abstract Throwable? getErrorCause();
method public boolean hasAudio();
method public boolean hasError();
+ field public static final double AUDIO_AMPLITUDE_NONE = 0.0;
field public static final int AUDIO_STATE_ACTIVE = 0; // 0x0
field public static final int AUDIO_STATE_DISABLED = 1; // 0x1
field public static final int AUDIO_STATE_ENCODER_ERROR = 3; // 0x3
@@ -14,6 +16,9 @@
field public static final int AUDIO_STATE_SOURCE_SILENCED = 2; // 0x2
}
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAudioApi {
+ }
+
@SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalPersistentRecording {
}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
index 42f8ae0..3e39ad6 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
@@ -570,6 +570,10 @@
val profiles = createFakeEncoderProfilesProxy(size.width, size.height)
return VideoValidatedEncoderProfilesProxy.from(profiles)
}
+
+ override fun isStabilizationSupported(): Boolean {
+ return false
+ }
}
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java b/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java
index 7f171f5..dde69be 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java
@@ -104,7 +104,6 @@
* Should audio recording be disabled, any attempts to retrieve the amplitude will
* return this value.
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY)
public static final double AUDIO_AMPLITUDE_NONE = 0;
@IntDef({AUDIO_STATE_ACTIVE, AUDIO_STATE_DISABLED, AUDIO_STATE_SOURCE_SILENCED,
@@ -168,8 +167,8 @@
public abstract Throwable getErrorCause();
/**
- * Returns the maximum absolute amplitude of the audio most recently sampled. Returns
- * {@link #AUDIO_AMPLITUDE_NONE} if audio is disabled.
+ * Returns the maximum absolute amplitude of the audio most recently sampled in the past 2
+ * nanoseconds
*
* <p>The amplitude is the maximum absolute value over all channels which the audio was
* most recently sampled from.
@@ -177,10 +176,11 @@
* <p>Amplitude is a relative measure of the maximum sound pressure/voltage range of the device
* microphone.
*
+ * <p>Returns {@link #AUDIO_AMPLITUDE_NONE} if audio is disabled.
* <p>The amplitude value returned will be a double between {@code 0} and {@code 1}.
+ *
*/
@OptIn(markerClass = ExperimentalAudioApi.class)
- @RestrictTo(RestrictTo.Scope.LIBRARY)
public double getAudioAmplitude() {
if (getAudioState() == AUDIO_STATE_DISABLED) {
return AUDIO_AMPLITUDE_NONE;
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/ExperimentalAudioApi.java b/camera/camera-video/src/main/java/androidx/camera/video/ExperimentalAudioApi.java
index bf07a6e..99de642 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/ExperimentalAudioApi.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/ExperimentalAudioApi.java
@@ -19,16 +19,14 @@
import static java.lang.annotation.RetentionPolicy.CLASS;
import androidx.annotation.RequiresOptIn;
-import androidx.annotation.RestrictTo;
import java.lang.annotation.Retention;
/**
- * Denotes that the methods on retrieving audio amplitude data are experimental and may
+ * Denotes that the annotated element relates to an experimental audio feature and may
* change in a future release.
*/
@Retention(CLASS)
@RequiresOptIn
-@RestrictTo(RestrictTo.Scope.LIBRARY)
public @interface ExperimentalAudioApi {
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java b/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java
index 7b78e91..b9e560e 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java
@@ -76,6 +76,8 @@
private final EncoderProfilesProvider mProfilesProvider;
+ private boolean mIsStabilizationSupported = false;
+
// Mappings of DynamicRange to recording capability information. The mappings are divided
// into two collections based on the key's (DynamicRange) category, one for specified
// DynamicRange and one for others. Specified DynamicRange means that its bit depth and
@@ -130,6 +132,9 @@
mCapabilitiesMapForFullySpecifiedDynamicRange.put(dynamicRange, capabilities);
}
}
+
+ // Video stabilization
+ mIsStabilizationSupported = cameraInfoInternal.isVideoStabilizationSupported();
}
/**
@@ -166,6 +171,11 @@
return capabilities != null && capabilities.isQualitySupported(quality);
}
+ @Override
+ public boolean isStabilizationSupported() {
+ return mIsStabilizationSupported;
+ }
+
@Nullable
@Override
public VideoValidatedEncoderProfilesProxy getProfiles(@NonNull Quality quality,
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java
index 02b857e..8f9fd80 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java
@@ -16,6 +16,7 @@
package androidx.camera.video;
+import android.hardware.camera2.CaptureRequest;
import android.util.Size;
import androidx.annotation.NonNull;
@@ -114,6 +115,17 @@
boolean isQualitySupported(@NonNull Quality quality, @NonNull DynamicRange dynamicRange);
/**
+ * Returns if video stabilization is supported on the device.
+ *
+ * @return true if {@link CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE_ON} is supported,
+ * otherwise false.
+ *
+ * @see CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ boolean isStabilizationSupported();
+
+ /**
* Gets the corresponding {@link VideoValidatedEncoderProfilesProxy} of the input quality and
* dynamic range.
*
@@ -193,5 +205,10 @@
@NonNull DynamicRange dynamicRange) {
return false;
}
+
+ @Override
+ public boolean isStabilizationSupported() {
+ return false;
+ }
};
}
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/QualitySelectorTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/QualitySelectorTest.kt
index 9fef206..78c7b9a 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/QualitySelectorTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/QualitySelectorTest.kt
@@ -438,6 +438,10 @@
override fun isQualitySupported(quality: Quality, dynamicRange: DynamicRange): Boolean {
throw UnsupportedOperationException("Not supported.")
}
+
+ override fun isStabilizationSupported(): Boolean {
+ return false
+ }
}
}
}
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
index 141fe23..734deea 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -1716,6 +1716,10 @@
return videoCapabilitiesMap[dynamicRange]?.isQualitySupported(quality) ?: false
}
+ override fun isStabilizationSupported(): Boolean {
+ return false
+ }
+
override fun getProfiles(
quality: Quality,
dynamicRange: DynamicRange
diff --git a/camera/integration-tests/coretestapp/lint-baseline.xml b/camera/integration-tests/coretestapp/lint-baseline.xml
index 4cbca68..78ea985 100644
--- a/camera/integration-tests/coretestapp/lint-baseline.xml
+++ b/camera/integration-tests/coretestapp/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-alpha15" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha15)" variant="all" version="8.2.0-alpha15">
+<issues format="6" by="lint 8.2.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-beta01)" variant="all" version="8.2.0-beta01">
<issue
id="BanThreadSleep"
@@ -12,108 +12,90 @@
<issue
id="RestrictedApiAndroidX"
- message="CameraInfoInternal can only be accessed from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" if (cameraInfo instanceof CameraInfoInternal) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ message="FileUtil.canDeviceWriteToMediaStore can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" if (!canDeviceWriteToMediaStore()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="DeviceQuirks.getAll can only be called from within the same library (androidx.camera:camera-video)"
- errorLine1=" Quirks deviceQuirks = DeviceQuirks.getAll();"
- errorLine2=" ~~~~~~">
+ message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" this, generateVideoFileOutputOptions(fileName, extension));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="CameraInfoInternal can only be accessed from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" Quirks cameraQuirks = ((CameraInfoInternal) cameraInfo).getCameraQuirks();"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" this, generateVideoFileOutputOptions(fileName, extension));"
+ errorLine2=" ~~~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="CameraInfoInternal.getCameraQuirks can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" Quirks cameraQuirks = ((CameraInfoInternal) cameraInfo).getCameraQuirks();"
- errorLine2=" ~~~~~~~~~~~~~~~">
+ message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" this, generateVideoFileOutputOptions(fileName, extension));"
+ errorLine2=" ~~~~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="Quirks.contains can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" if (deviceQuirks.contains(CrashWhenTakingPhotoWithAutoFlashAEModeQuirk.class)"
- errorLine2=" ~~~~~~~~">
+ message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" generateVideoMediaStoreOptions(getContentResolver(), fileName));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="Quirks.contains can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" if (deviceQuirks.contains(CrashWhenTakingPhotoWithAutoFlashAEModeQuirk.class)"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" generateVideoMediaStoreOptions(getContentResolver(), fileName));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="Quirks.contains can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" || cameraQuirks.contains(ImageCaptureFailWithAutoFlashQuirk.class)"
- errorLine2=" ~~~~~~~~">
+ message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" generateVideoMediaStoreOptions(getContentResolver(), fileName));"
+ errorLine2=" ~~~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="Quirks.contains can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" || cameraQuirks.contains(ImageCaptureFailWithAutoFlashQuirk.class)"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" videoFilePath = getAbsolutePathFromUri("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="Quirks.contains can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" || cameraQuirks.contains(ImageCaptureFlashNotFireQuirk.class)) {"
- errorLine2=" ~~~~~~~~">
+ message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" getApplicationContext().getContentResolver(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="Quirks.contains can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" || cameraQuirks.contains(ImageCaptureFlashNotFireQuirk.class)) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
- </issue>
-
- <issue
- id="RestrictedApiAndroidX"
- message="DeviceQuirks.get can only be called from within the same library (androidx.camera:camera-video)"
- errorLine1=" if (DeviceQuirks.get(MediaStoreVideoCannotWrite.class) != null) {"
- errorLine2=" ~~~">
- <location
- file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
- </issue>
-
- <issue
- id="RestrictedApiAndroidX"
- message="DeviceQuirks.get can only be called from within the same library (androidx.camera:camera-video)"
- errorLine1=" if (DeviceQuirks.get(MediaStoreVideoCannotWrite.class) != null) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" uri"
+ errorLine2=" ~~~">
<location
file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
</issue>
@@ -183,6 +165,69 @@
<issue
id="RestrictedApiAndroidX"
+ message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" if (createParentFolder(pictureFolder)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+ </issue>
+
+ <issue
+ id="RestrictedApiAndroidX"
+ message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" if (createParentFolder(pictureFolder)) {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+ </issue>
+
+ <issue
+ id="RestrictedApiAndroidX"
+ message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" getAbsolutePathFromUri(getApplicationContext().getContentResolver(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+ </issue>
+
+ <issue
+ id="RestrictedApiAndroidX"
+ message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" getAbsolutePathFromUri(getApplicationContext().getContentResolver(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+ </issue>
+
+ <issue
+ id="RestrictedApiAndroidX"
+ message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" MediaStore.Video.Media.EXTERNAL_CONTENT_URI);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+ </issue>
+
+ <issue
+ id="RestrictedApiAndroidX"
+ message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" if (videoFilePath == null || !createParentFolder(videoFilePath)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+ </issue>
+
+ <issue
+ id="RestrictedApiAndroidX"
+ message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" if (videoFilePath == null || !createParentFolder(videoFilePath)) {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+ </issue>
+
+ <issue
+ id="RestrictedApiAndroidX"
message="CameraXExecutors.mainThreadExecutor can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
errorLine1=" CameraXExecutors.mainThreadExecutor());"
errorLine2=" ~~~~~~~~~~~~~~~~~~">
@@ -192,6 +237,24 @@
<issue
id="RestrictedApiAndroidX"
+ message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" if (createParentFolder(pictureFolder)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+ </issue>
+
+ <issue
+ id="RestrictedApiAndroidX"
+ message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" if (createParentFolder(pictureFolder)) {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+ </issue>
+
+ <issue
+ id="RestrictedApiAndroidX"
message="TransformationInfo.hasCameraTransform can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
errorLine1=" mHasCameraTransform = transformationInfo.hasCameraTransform();"
errorLine2=" ~~~~~~~~~~~~~~~~~~">
@@ -264,34 +327,34 @@
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.canDeviceWriteToMediaStore can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" if (E2ETestUtil.canDeviceWriteToMediaStore()) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ message="FileUtil.canDeviceWriteToMediaStore can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" if (FileUtil.canDeviceWriteToMediaStore()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" E2ETestUtil.generateVideoMediaStoreOptions(this.getContentResolver(),"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" FileUtil.generateVideoMediaStoreOptions(this.getContentResolver(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" E2ETestUtil.generateVideoMediaStoreOptions(this.getContentResolver(),"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" FileUtil.generateVideoMediaStoreOptions(this.getContentResolver(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
errorLine1=" videoFileName));"
errorLine2=" ~~~~~~~~~~~~~">
<location
@@ -300,52 +363,52 @@
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" E2ETestUtil.generateVideoFileOutputOptions(videoFileName, "mp4"));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" FileUtil.generateVideoFileOutputOptions(videoFileName, "mp4"));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" E2ETestUtil.generateVideoFileOutputOptions(videoFileName, "mp4"));"
- errorLine2=" ~~~~~~~~~~~~~">
+ message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" FileUtil.generateVideoFileOutputOptions(videoFileName, "mp4"));"
+ errorLine2=" ~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" E2ETestUtil.generateVideoFileOutputOptions(videoFileName, "mp4"));"
- errorLine2=" ~~~~~">
+ message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" FileUtil.generateVideoFileOutputOptions(videoFileName, "mp4"));"
+ errorLine2=" ~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" E2ETestUtil.writeTextToExternalFile(information,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" FileUtil.writeTextToExternalFile(information,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" E2ETestUtil.writeTextToExternalFile(information,"
- errorLine2=" ~~~~~~~~~~~">
+ message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" FileUtil.writeTextToExternalFile(information,"
+ errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
errorLine1=" generateFileName(INFO_FILE_PREFIX, false), "txt");"
errorLine2=" ~~~~~~~~~~~~~~~~">
<location
@@ -354,7 +417,7 @@
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
errorLine1=" generateFileName(INFO_FILE_PREFIX, false), "txt");"
errorLine2=" ~~~~~">
<location
@@ -363,36 +426,36 @@
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" if (!isUnique && !E2ETestUtil.isFileNameValid(prefix)) {"
- errorLine2=" ~~~~~~~~~~~~~~~">
+ message="FileUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" if (!isUnique && !FileUtil.isFileNameValid(prefix)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" if (!isUnique && !E2ETestUtil.isFileNameValid(prefix)) {"
- errorLine2=" ~~~~~~">
+ message="FileUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" if (!isUnique && !FileUtil.isFileNameValid(prefix)) {"
+ errorLine2=" ~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" if (E2ETestUtil.isFileNameValid(prefix)) {"
- errorLine2=" ~~~~~~~~~~~~~~~">
+ message="FileUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" if (FileUtil.isFileNameValid(prefix)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
</issue>
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
- errorLine1=" if (E2ETestUtil.isFileNameValid(prefix)) {"
- errorLine2=" ~~~~~~">
+ message="FileUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ errorLine1=" if (FileUtil.isFileNameValid(prefix)) {"
+ errorLine2=" ~~~~~~">
<location
file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
</issue>
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXServiceTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXServiceTest.kt
index 0810339..df79650 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXServiceTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXServiceTest.kt
@@ -16,6 +16,7 @@
package androidx.camera.integration.core
+import android.Manifest
import android.app.ActivityManager
import android.app.Service
import android.content.ComponentName
@@ -32,6 +33,7 @@
import androidx.camera.core.ImageCapture
import androidx.camera.core.UseCase
import androidx.camera.integration.core.CameraXService.ACTION_BIND_USE_CASES
+import androidx.camera.integration.core.CameraXService.ACTION_TAKE_PICTURE
import androidx.camera.integration.core.CameraXService.EXTRA_IMAGE_ANALYSIS_ENABLED
import androidx.camera.integration.core.CameraXService.EXTRA_IMAGE_CAPTURE_ENABLED
import androidx.camera.integration.core.CameraXService.EXTRA_VIDEO_CAPTURE_ENABLED
@@ -45,6 +47,7 @@
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
+import androidx.test.rule.GrantPermissionRule
import androidx.testutils.LifecycleOwnerUtils
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.TimeUnit
@@ -72,6 +75,12 @@
)
@get:Rule
+ val permissionRule: GrantPermissionRule =
+ GrantPermissionRule.grant(
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ )
+
+ @get:Rule
val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
active = implName == CameraPipeConfig::class.simpleName,
)
@@ -105,6 +114,7 @@
@After
fun tearDown() {
if (this::service.isInitialized) {
+ service.deleteSavedMediaFiles()
context.unbindService(serviceConnection)
context.stopService(createServiceIntent())
@@ -165,6 +175,21 @@
assertThat(latch.await(15, TimeUnit.SECONDS)).isTrue()
}
+ @Test
+ fun canTakePicture() = runBlocking {
+ // Arrange.
+ context.startService(createServiceIntent(ACTION_BIND_USE_CASES).apply {
+ putExtra(EXTRA_IMAGE_CAPTURE_ENABLED, true)
+ })
+
+ // Act.
+ val latch = service.acquireTakePictureCountDownLatch()
+ context.startService(createServiceIntent(ACTION_TAKE_PICTURE))
+
+ // Assert.
+ assertThat(latch.await(15, TimeUnit.SECONDS)).isTrue()
+ }
+
private fun createServiceIntent(action: String? = null) =
Intent(context, CameraXService::class.java).apply {
action?.let { setAction(it) }
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
index abb5eaed..e10ff38 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -64,6 +64,7 @@
import androidx.camera.core.impl.utils.CameraOrientationUtil
import androidx.camera.core.impl.utils.Exif
import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability
+import androidx.camera.core.internal.compat.workaround.InvalidJpegDataParser
import androidx.camera.core.resolutionselector.AspectRatioStrategy
import androidx.camera.core.resolutionselector.ResolutionFilter
import androidx.camera.core.resolutionselector.ResolutionSelector
@@ -1802,6 +1803,89 @@
assertThat(imageProperties.size).isEqualTo(maxHighResolutionOutputSize)
}
+ /**
+ * See b/288828159 for the detailed info of the issue
+ */
+ @Test
+ fun jpegImageZeroPaddingDataDetectionTest(): Unit = runBlocking {
+ val imageCapture = ImageCapture.Builder().build()
+
+ withContext(Dispatchers.Main) {
+ cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, imageCapture)
+ }
+
+ val latch = CountdownDeferred(1)
+ var errors: Exception? = null
+
+ val callback = object : ImageCapture.OnImageCapturedCallback() {
+ override fun onCaptureSuccess(image: ImageProxy) {
+ val planes = image.planes
+ val buffer = planes[0].buffer
+ val data = ByteArray(buffer.capacity())
+ buffer.rewind()
+ buffer[data]
+
+ image.close()
+
+ val invalidJpegDataParser = InvalidJpegDataParser()
+
+ // Only checks the unnecessary zero padding data when the device is not included in
+ // the LargeJpegImageQuirk device list. InvalidJpegDataParser#getValidDataLength()
+ // should have returned the valid data length to avoid the extremely large JPEG
+ // file issue.
+ if (invalidJpegDataParser.getValidDataLength(data) == data.size &&
+ containsZeroPaddingDataAfterEoi(data)
+ ) {
+ errors = Exception("UNNECESSARY_JPEG_ZERO_PADDING_DATA_DETECTED!")
+ }
+
+ latch.countDown()
+ }
+
+ override fun onError(exception: ImageCaptureException) {
+ errors = exception
+ latch.countDown()
+ }
+ }
+
+ imageCapture.takePicture(mainExecutor, callback)
+
+ // Wait for the signal that the image has been captured.
+ assertThat(withTimeoutOrNull(CAPTURE_TIMEOUT) {
+ latch.await()
+ }).isNotNull()
+ assertThat(errors).isNull()
+ }
+
+ /**
+ * This util function is only used to detect the unnecessary zero padding data after EOI. It
+ * will directly return false when it fails to parse the JPEG byte array data.
+ */
+ private fun containsZeroPaddingDataAfterEoi(bytes: ByteArray): Boolean {
+ val jfifEoiMarkEndPosition = InvalidJpegDataParser.getJfifEoiMarkEndPosition(bytes)
+
+ // Directly returns false when EOI mark can't be found.
+ if (jfifEoiMarkEndPosition == -1) {
+ return false
+ }
+
+ // Will check 1mb data to know whether unnecessary zero padding data exists or not.
+ // Directly returns false when the data length is long enough
+ val dataLengthToDetect = 1_000_000
+ if (jfifEoiMarkEndPosition + dataLengthToDetect > bytes.size) {
+ return false
+ }
+
+ // Checks that there are at least continuous 1mb of unnecessary zero padding data after EOI
+ for (position in jfifEoiMarkEndPosition..jfifEoiMarkEndPosition + dataLengthToDetect) {
+ if (bytes[position] != 0x00.toByte()) {
+ return false
+ }
+ }
+
+ return true
+ }
+
private fun createNonRotatedConfiguration(): ImageCaptureConfig {
// Create a configuration with target rotation that matches the sensor rotation.
// This assumes a back-facing camera (facing away from screen)
diff --git a/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml b/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
index 4954bfb..3fd027e 100644
--- a/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
@@ -83,6 +83,7 @@
android:label="CameraX Service">
<intent-filter>
<action android:name="androidx.camera.integration.core.intent.action.BIND_USE_CASES" />
+ <action android:name="androidx.camera.integration.core.intent.action.TAKE_PICTURE" />
</intent-filter>
</service>
</application>
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index f89839e..b304a9a 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -25,21 +25,25 @@
import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
import static androidx.camera.core.ImageCapture.FLASH_MODE_ON;
import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
+import static androidx.camera.testing.impl.FileUtil.canDeviceWriteToMediaStore;
+import static androidx.camera.testing.impl.FileUtil.createParentFolder;
+import static androidx.camera.testing.impl.FileUtil.generateVideoFileOutputOptions;
+import static androidx.camera.testing.impl.FileUtil.generateVideoMediaStoreOptions;
+import static androidx.camera.testing.impl.FileUtil.getAbsolutePathFromUri;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_INSUFFICIENT_STORAGE;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE;
+import static java.util.Objects.requireNonNull;
+
import android.Manifest;
import android.annotation.SuppressLint;
-import android.content.ContentResolver;
import android.content.ContentValues;
-import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
-import android.database.Cursor;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.display.DisplayManager;
import android.media.MediaScannerConnection;
@@ -126,8 +130,6 @@
import androidx.camera.video.VideoCapabilities;
import androidx.camera.video.VideoCapture;
import androidx.camera.video.VideoRecordEvent;
-import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
-import androidx.camera.video.internal.compat.quirk.MediaStoreVideoCannotWrite;
import androidx.core.content.ContextCompat;
import androidx.core.math.MathUtils;
import androidx.core.util.Consumer;
@@ -357,6 +359,9 @@
@Override
public void onSuccess(@Nullable Integer result) {
+ if (result == null) {
+ return;
+ }
CameraInfo cameraInfo = getCameraInfo();
if (cameraInfo != null) {
ExposureState exposureState = cameraInfo.getExposureState();
@@ -478,7 +483,7 @@
@VisibleForTesting
public void resetViewIdlingResource() {
mPreviewFrameCount.set(0);
- // Make the view idling resource non-idle, until required framecount achieved.
+ // Make the view idling resource non-idle, until required frame count achieved.
if (mViewIdlingResource.isIdleNow()) {
mViewIdlingResource.increment();
}
@@ -596,14 +601,17 @@
case IDLE:
createDefaultVideoFolderIfNotExist();
final PendingRecording pendingRecording;
- if (DeviceQuirks.get(MediaStoreVideoCannotWrite.class) != null) {
- // Use FileOutputOption for devices in MediaStoreVideoCannotWrite Quirk.
- pendingRecording = getVideoCapture().getOutput().prepareRecording(
- this, getNewVideoFileOutputOptions());
- } else {
+ String fileName = "video_" + System.currentTimeMillis();
+ String extension = "mp4";
+ if (canDeviceWriteToMediaStore()) {
// Use MediaStoreOutputOptions for public share media storage.
pendingRecording = getVideoCapture().getOutput().prepareRecording(
- this, getNewVideoOutputMediaStoreOptions());
+ this,
+ generateVideoMediaStoreOptions(getContentResolver(), fileName));
+ } else {
+ // Use FileOutputOption for devices in MediaStoreVideoCannotWrite Quirk.
+ pendingRecording = getVideoCapture().getOutput().prepareRecording(
+ this, generateVideoFileOutputOptions(fileName, extension));
}
resetVideoSavedIdlingResource();
@@ -806,32 +814,6 @@
}
}
- @NonNull
- private MediaStoreOutputOptions getNewVideoOutputMediaStoreOptions() {
- String videoFileName = "video_" + System.currentTimeMillis();
- ContentValues contentValues = new ContentValues();
- contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
- contentValues.put(MediaStore.Video.Media.TITLE, videoFileName);
- contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, videoFileName);
- contentValues.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
- contentValues.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis());
- return new MediaStoreOutputOptions.Builder(getContentResolver(),
- MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
- .setContentValues(contentValues)
- .build();
- }
-
- @NonNull
- private FileOutputOptions getNewVideoFileOutputOptions() {
- String videoFileName = "video_" + System.currentTimeMillis() + ".mp4";
- File videoFolder = Environment.getExternalStoragePublicDirectory(
- Environment.DIRECTORY_MOVIES);
- if (!videoFolder.exists() && !videoFolder.mkdirs()) {
- Log.e(TAG, "Failed to create directory: " + videoFolder);
- }
- return new FileOutputOptions.Builder(new File(videoFolder, videoFileName)).build();
- }
-
private void updateRecordingStats(@NonNull RecordingStats stats) {
double durationMs = TimeUnit.NANOSECONDS.toMillis(stats.getRecordedDurationNanos());
// Show megabytes in International System of Units (SI)
@@ -890,7 +872,7 @@
Toast.LENGTH_SHORT).show());
if (mSessionImagesUriSet != null) {
mSessionImagesUriSet.add(
- Objects.requireNonNull(
+ requireNonNull(
outputFileResults.getSavedUri()));
}
}
@@ -956,7 +938,7 @@
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
}
} catch (IllegalArgumentException e) {
- Toast.makeText(this, "Failed to swich Camera. Error:" + e.getMessage(),
+ Toast.makeText(this, "Failed to switch Camera. Error:" + e.getMessage(),
Toast.LENGTH_SHORT).show();
}
});
@@ -1008,8 +990,8 @@
private void setUpTorchButton() {
mTorchButton.setOnClickListener(v -> {
- Objects.requireNonNull(getCameraInfo());
- Objects.requireNonNull(getCameraControl());
+ requireNonNull(getCameraInfo());
+ requireNonNull(getCameraControl());
Integer torchState = getCameraInfo().getTorchState().getValue();
boolean toggledState = !Objects.equals(torchState, TorchState.ON);
Log.d(TAG, "Set camera torch: " + toggledState);
@@ -1029,8 +1011,8 @@
private void setUpEVButton() {
mPlusEV.setOnClickListener(v -> {
- Objects.requireNonNull(getCameraInfo());
- Objects.requireNonNull(getCameraControl());
+ requireNonNull(getCameraInfo());
+ requireNonNull(getCameraControl());
ExposureState exposureState = getCameraInfo().getExposureState();
Range<Integer> range = exposureState.getExposureCompensationRange();
@@ -1048,8 +1030,8 @@
});
mDecEV.setOnClickListener(v -> {
- Objects.requireNonNull(getCameraInfo());
- Objects.requireNonNull(getCameraControl());
+ requireNonNull(getCameraInfo());
+ requireNonNull(getCameraControl());
ExposureState exposureState = getCameraInfo().getExposureState();
Range<Integer> range = exposureState.getExposureCompensationRange();
@@ -1120,12 +1102,12 @@
ViewGroup.LayoutParams lp = viewFinderStub.getLayoutParams();
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
lp.width = displayMetrics.widthPixels;
- lp.height = (int) (displayMetrics.widthPixels / ratio.getDenominator()
- * ratio.getNumerator());
+ lp.height = displayMetrics.widthPixels / ratio.getDenominator()
+ * ratio.getNumerator();
} else {
lp.height = displayMetrics.heightPixels;
- lp.width = (int) (displayMetrics.heightPixels / ratio.getDenominator()
- * ratio.getNumerator());
+ lp.width = displayMetrics.heightPixels / ratio.getDenominator()
+ * ratio.getNumerator();
}
viewFinderStub.setLayoutParams(lp);
}
@@ -1329,7 +1311,7 @@
};
DisplayManager dpyMgr =
- Objects.requireNonNull((DisplayManager) getSystemService(Context.DISPLAY_SERVICE));
+ requireNonNull(ContextCompat.getSystemService(this, DisplayManager.class));
dpyMgr.registerDisplayListener(mDisplayListener, new Handler(Looper.getMainLooper()));
StrictMode.VmPolicy vmPolicy =
@@ -1446,7 +1428,7 @@
public void onDestroy() {
super.onDestroy();
DisplayManager dpyMgr =
- Objects.requireNonNull((DisplayManager) getSystemService(Context.DISPLAY_SERVICE));
+ requireNonNull(ContextCompat.getSystemService(this, DisplayManager.class));
dpyMgr.unregisterDisplayListener(mDisplayListener);
mPreviewRenderer.shutdown();
mImageCaptureExecutorService.shutdown();
@@ -1577,12 +1559,9 @@
// Remove ImageAnalysis to check whether the new use cases combination can be supported.
if (mAnalysisToggle.isChecked()) {
mAnalysisToggle.setChecked(false);
- if (isCheckedUseCasesCombinationSupported()) {
- return;
- }
+ // No need to do further use case combination check since Preview + ImageCapture
+ // should be always supported.
}
-
- // Preview + ImageCapture should be always supported.
}
/**
@@ -1687,7 +1666,7 @@
new ActivityResultContracts.RequestMultiplePermissions(),
result -> {
for (String permission : REQUIRED_PERMISSIONS) {
- if (!Objects.requireNonNull(result.get(permission))) {
+ if (!requireNonNull(result.get(permission))) {
Toast.makeText(getApplicationContext(),
"Camera permission denied.",
Toast.LENGTH_SHORT)
@@ -1720,10 +1699,8 @@
void createDefaultPictureFolderIfNotExist() {
File pictureFolder = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES);
- if (!pictureFolder.exists()) {
- if (!pictureFolder.mkdir()) {
- Log.e(TAG, "Failed to create directory: " + pictureFolder);
- }
+ if (createParentFolder(pictureFolder)) {
+ Log.e(TAG, "Failed to create directory: " + pictureFolder);
}
}
@@ -1732,17 +1709,8 @@
String videoFilePath =
getAbsolutePathFromUri(getApplicationContext().getContentResolver(),
MediaStore.Video.Media.EXTERNAL_CONTENT_URI);
-
- // If cannot get the video path, just skip checking and create folder.
- if (videoFilePath == null) {
- return;
- }
- File videoFile = new File(videoFilePath);
-
- if (videoFile.getParentFile() != null && !videoFile.getParentFile().exists()) {
- if (!videoFile.getParentFile().mkdir()) {
- Log.e(TAG, "Failed to create directory: " + videoFile);
- }
+ if (videoFilePath == null || !createParentFolder(videoFilePath)) {
+ Log.e(TAG, "Failed to create parent directory for: " + videoFilePath);
}
}
@@ -1778,13 +1746,14 @@
ScaleGestureDetector.SimpleOnScaleGestureListener mScaleGestureListener =
new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
- public boolean onScale(ScaleGestureDetector detector) {
+ public boolean onScale(@NonNull ScaleGestureDetector detector) {
if (mCamera == null) {
return true;
}
CameraInfo cameraInfo = mCamera.getCameraInfo();
- float newZoom = cameraInfo.getZoomState().getValue().getZoomRatio()
+ float newZoom =
+ requireNonNull(cameraInfo.getZoomState().getValue()).getZoomRatio()
* detector.getScaleFactor();
setZoomRatio(newZoom);
return true;
@@ -1794,7 +1763,7 @@
GestureDetector.OnGestureListener onTapGestureListener =
new GestureDetector.SimpleOnGestureListener() {
@Override
- public boolean onSingleTapUp(MotionEvent e) {
+ public boolean onSingleTapUp(@NonNull MotionEvent e) {
if (mCamera == null) {
return false;
}
@@ -1834,7 +1803,8 @@
mZoomSeekBar.setMax(MAX_SEEKBAR_VALUE);
mZoomSeekBar.setProgress(
- (int) (cameraInfo.getZoomState().getValue().getLinearZoom() * MAX_SEEKBAR_VALUE));
+ (int) (requireNonNull(cameraInfo.getZoomState().getValue()).getLinearZoom()
+ * MAX_SEEKBAR_VALUE));
mZoomSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
@@ -1884,7 +1854,7 @@
private boolean is2XZoomSupported() {
CameraInfo cameraInfo = getCameraInfo();
return cameraInfo != null
- && cameraInfo.getZoomState().getValue().getMaxZoomRatio() >= 2.0f;
+ && requireNonNull(cameraInfo.getZoomState().getValue()).getMaxZoomRatio() >= 2.0f;
}
private void setUpZoomButton() {
@@ -1901,7 +1871,7 @@
CameraInfo cameraInfo = mCamera.getCameraInfo();
CameraControl cameraControl = mCamera.getCameraControl();
float clampedNewZoom = MathUtils.clamp(newZoom,
- cameraInfo.getZoomState().getValue().getMinZoomRatio(),
+ requireNonNull(cameraInfo.getZoomState().getValue()).getMinZoomRatio(),
cameraInfo.getZoomState().getValue().getMaxZoomRatio());
Log.d(TAG, "setZoomRatio ratio: " + clampedNewZoom);
@@ -1932,34 +1902,6 @@
});
}
- /** Gets the absolute path from a Uri. */
- @Nullable
- public String getAbsolutePathFromUri(@NonNull ContentResolver resolver,
- @NonNull Uri contentUri) {
- Cursor cursor = null;
- try {
- // We should include in any Media collections.
- String[] proj;
- int columnIndex;
- // MediaStore.Video.Media.DATA was deprecated in API level 29.
- proj = new String[]{MediaStore.Video.Media.DATA};
- cursor = resolver.query(contentUri, proj, null, null, null);
- columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA);
-
- cursor.moveToFirst();
- return cursor.getString(columnIndex);
- } catch (RuntimeException e) {
- Log.e(TAG, String.format(
- "Failed in getting absolute path for Uri %s with Exception %s",
- contentUri, e));
- return "";
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- }
-
private class SessionMediaUriSet {
private final Set<Uri> mSessionMediaUris;
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXService.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXService.java
index 45f7201..4e5333c 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXService.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXService.java
@@ -17,15 +17,21 @@
package androidx.camera.integration.core;
import static androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA;
+import static androidx.camera.testing.impl.FileUtil.createParentFolder;
import static com.google.common.base.Preconditions.checkNotNull;
import android.app.NotificationChannel;
import android.app.NotificationManager;
+import android.content.ContentValues;
import android.content.Intent;
+import android.net.Uri;
import android.os.Binder;
import android.os.Build;
+import android.os.Environment;
import android.os.IBinder;
+import android.os.SystemClock;
+import android.provider.MediaStore;
import android.util.Log;
import androidx.annotation.DoNotInline;
@@ -35,6 +41,7 @@
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.UseCase;
import androidx.camera.core.UseCaseGroup;
import androidx.camera.lifecycle.ProcessCameraProvider;
@@ -47,9 +54,18 @@
import com.google.common.util.concurrent.ListenableFuture;
+import java.io.File;
+import java.text.Format;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
@@ -64,6 +80,8 @@
// Actions
public static final String ACTION_BIND_USE_CASES =
"androidx.camera.integration.core.intent.action.BIND_USE_CASES";
+ public static final String ACTION_TAKE_PICTURE =
+ "androidx.camera.integration.core.intent.action.TAKE_PICTURE";
// Extras
public static final String EXTRA_VIDEO_CAPTURE_ENABLED = "EXTRA_VIDEO_CAPTURE_ENABLED";
@@ -73,12 +91,22 @@
private final IBinder mBinder = new CameraXServiceBinder();
////////////////////////////////////////////////////////////////////////////////////////////////
+ // Members only accessed on main thread //
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ private final Map<Class<?>, UseCase> mBoundUseCases = new HashMap<>();
+ //--------------------------------------------------------------------------------------------//
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
// Members for testing //
////////////////////////////////////////////////////////////////////////////////////////////////
+ private final Set<Uri> mSavedImageUri = new HashSet<>();
+
@Nullable
private Consumer<Collection<UseCase>> mOnUseCaseBoundCallback;
@Nullable
private CountDownLatch mAnalysisFrameLatch;
+ @Nullable
+ private CountDownLatch mTakePictureLatch;
//--------------------------------------------------------------------------------------------//
@Override
@@ -101,6 +129,8 @@
Log.d(TAG, "onStartCommand: action = " + action + ", extras = " + intent.getExtras());
if (ACTION_BIND_USE_CASES.equals(action)) {
bindToLifecycle(intent);
+ } else if (ACTION_TAKE_PICTURE.equals(action)) {
+ takePicture();
}
}
return super.onStartCommand(intent, flags, startId);
@@ -125,6 +155,7 @@
throw new IllegalStateException(e);
}
cameraProvider.unbindAll();
+ mBoundUseCases.clear();
UseCaseGroup useCaseGroup = resolveUseCaseGroup(intent);
List<UseCase> boundUseCases = Collections.emptyList();
if (useCaseGroup != null) {
@@ -136,6 +167,9 @@
}
}
Log.d(TAG, "Bound UseCases: " + boundUseCases);
+ for (UseCase boundUseCase : boundUseCases) {
+ mBoundUseCases.put(boundUseCase.getClass(), boundUseCase);
+ }
if (mOnUseCaseBoundCallback != null) {
mOnUseCaseBoundCallback.accept(boundUseCases);
}
@@ -183,6 +217,61 @@
return checkNotNull(ContextCompat.getSystemService(this, NotificationManager.class));
}
+ @Nullable
+ private ImageCapture getImageCapture() {
+ return (ImageCapture) mBoundUseCases.get(ImageCapture.class);
+ }
+
+ private void takePicture() {
+ ImageCapture imageCapture = getImageCapture();
+ if (imageCapture == null) {
+ Log.w(TAG, "ImageCapture is not bound.");
+ return;
+ }
+ createDefaultPictureFolderIfNotExist();
+ Format formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US);
+ String fileName = "ServiceTestApp-" + formatter.format(Calendar.getInstance().getTime())
+ + ".jpg";
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
+ contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
+ ImageCapture.OutputFileOptions outputFileOptions =
+ new ImageCapture.OutputFileOptions.Builder(
+ getContentResolver(),
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ contentValues).build();
+ long startTimeMs = SystemClock.elapsedRealtime();
+ imageCapture.takePicture(outputFileOptions,
+ ContextCompat.getMainExecutor(this),
+ new ImageCapture.OnImageSavedCallback() {
+ @Override
+ public void onImageSaved(
+ @NonNull ImageCapture.OutputFileResults outputFileResults) {
+ long durationMs = SystemClock.elapsedRealtime() - startTimeMs;
+ Log.d(TAG, "Saved image " + outputFileResults.getSavedUri()
+ + " (" + durationMs + " ms)");
+ mSavedImageUri.add(outputFileResults.getSavedUri());
+ if (mTakePictureLatch != null) {
+ mTakePictureLatch.countDown();
+ }
+ }
+
+ @Override
+ public void onError(@NonNull ImageCaptureException exception) {
+ Log.e(TAG, "Failed to save image by " + exception.getImageCaptureError(),
+ exception);
+ }
+ });
+ }
+
+ private void createDefaultPictureFolderIfNotExist() {
+ File pictureFolder = Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_PICTURES);
+ if (!createParentFolder(pictureFolder)) {
+ Log.e(TAG, "Failed to create directory: " + pictureFolder);
+ }
+ }
+
private final ImageAnalysis.Analyzer mAnalyzer = image -> {
if (mAnalysisFrameLatch != null) {
mAnalysisFrameLatch.countDown();
@@ -217,11 +306,34 @@
}
@VisibleForTesting
+ @NonNull
CountDownLatch acquireAnalysisFrameCountDownLatch() {
mAnalysisFrameLatch = new CountDownLatch(3);
return mAnalysisFrameLatch;
}
+ @VisibleForTesting
+ @NonNull
+ CountDownLatch acquireTakePictureCountDownLatch() {
+ mTakePictureLatch = new CountDownLatch(1);
+ return mTakePictureLatch;
+ }
+
+ @VisibleForTesting
+ void deleteSavedMediaFiles() {
+ deleteUriSet(mSavedImageUri);
+ }
+
+ private void deleteUriSet(@NonNull Set<Uri> uriSet) {
+ for (Uri uri : uriSet) {
+ try {
+ getContentResolver().delete(uri, null, null);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Unable to delete uri: " + uri, e);
+ }
+ }
+ }
+
class CameraXServiceBinder extends Binder {
@NonNull
CameraXService getService() {
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java
index 61a1f09..620f7e0 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java
@@ -107,7 +107,7 @@
@OptIn(markerClass = ExperimentalCameraProviderConfiguration.class)
@MainThread
public static boolean isCameraProviderUnInitializedOrSameAsParameter(
- @NonNull String cameraImplementation) {
+ @Nullable String cameraImplementation) {
if (sConfiguredCameraXCameraImplementation == null) {
return true;
@@ -116,11 +116,7 @@
sConfiguredCameraXCameraImplementation);
cameraImplementation = getCameraProviderName(cameraImplementation);
- if (currentCameraProvider.equals(cameraImplementation)) {
- return true;
- }
-
- return false;
+ return currentCameraProvider.equals(cameraImplementation);
}
/**
@@ -129,7 +125,7 @@
*/
@OptIn(markerClass = ExperimentalCameraProviderConfiguration.class)
@MainThread
- private static String getCameraProviderName(String mCameraProvider) {
+ private static String getCameraProviderName(@Nullable String mCameraProvider) {
if (mCameraProvider == null) {
mCameraProvider = CAMERA2_IMPLEMENTATION_OPTION;
}
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java
index c775b3d..976b5f3 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java
@@ -33,7 +33,7 @@
import androidx.camera.core.Logger;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
-import androidx.camera.testing.impl.E2ETestUtil;
+import androidx.camera.testing.impl.FileUtil;
import androidx.camera.video.ExperimentalPersistentRecording;
import androidx.camera.video.PendingRecording;
import androidx.camera.video.Recorder;
@@ -232,14 +232,14 @@
final String videoFileName = generateFileName(VIDEO_FILE_PREFIX, true);
final PendingRecording pendingRecording;
- if (E2ETestUtil.canDeviceWriteToMediaStore()) {
+ if (FileUtil.canDeviceWriteToMediaStore()) {
// Use MediaStoreOutputOptions for public share media storage.
pendingRecording = mVideoCapture.getOutput().prepareRecording(this,
- E2ETestUtil.generateVideoMediaStoreOptions(this.getContentResolver(),
+ FileUtil.generateVideoMediaStoreOptions(this.getContentResolver(),
videoFileName));
} else {
pendingRecording = mVideoCapture.getOutput().prepareRecording(this,
- E2ETestUtil.generateVideoFileOutputOptions(videoFileName, "mp4"));
+ FileUtil.generateVideoFileOutputOptions(videoFileName, "mp4"));
}
mRecording = pendingRecording
.asPersistentRecording() // Perform the recording as a persistent recording.
@@ -274,17 +274,17 @@
private void exportTestInformation() {
String information = KEY_DEVICE_ORIENTATION + ": " + mDeviceOrientation;
- E2ETestUtil.writeTextToExternalFile(information,
+ FileUtil.writeTextToExternalFile(information,
generateFileName(INFO_FILE_PREFIX, false), "txt");
}
@NonNull
private String generateFileName(@Nullable String prefix, boolean isUnique) {
- if (!isUnique && !E2ETestUtil.isFileNameValid(prefix)) {
+ if (!isUnique && !FileUtil.isFileNameValid(prefix)) {
throw new IllegalArgumentException("Invalid arguments for generating file name.");
}
StringBuilder fileName = new StringBuilder();
- if (E2ETestUtil.isFileNameValid(prefix)) {
+ if (FileUtil.isFileNameValid(prefix)) {
fileName.append(prefix);
if (isUnique) {
fileName.append("_");
diff --git a/camera/integration-tests/viewtestapp/lint-baseline.xml b/camera/integration-tests/viewtestapp/lint-baseline.xml
index acea7110..cfacbf5 100644
--- a/camera/integration-tests/viewtestapp/lint-baseline.xml
+++ b/camera/integration-tests/viewtestapp/lint-baseline.xml
@@ -264,7 +264,7 @@
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.canDeviceWriteToMediaStore can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ message="FileUtil.canDeviceWriteToMediaStore can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
errorLine1=" return if (canDeviceWriteToMediaStore()) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -273,7 +273,7 @@
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
errorLine1=" generateVideoMediaStoreOptions(context.contentResolver, fileName)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -282,7 +282,7 @@
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
errorLine1=" generateVideoMediaStoreOptions(context.contentResolver, fileName)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -291,7 +291,7 @@
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
errorLine1=" generateVideoMediaStoreOptions(context.contentResolver, fileName)"
errorLine2=" ~~~~~~~~">
<location
@@ -300,7 +300,7 @@
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
errorLine1=" recorder.prepareRecording(context, generateVideoFileOutputOptions(fileName))"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -309,7 +309,7 @@
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
errorLine1=" recorder.prepareRecording(context, generateVideoFileOutputOptions(fileName))"
errorLine2=" ~~~~~~~~">
<location
@@ -318,7 +318,7 @@
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
errorLine1=" writeTextToExternalFile(information, fileName)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -327,7 +327,7 @@
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
errorLine1=" writeTextToExternalFile(information, fileName)"
errorLine2=" ~~~~~~~~~~~">
<location
@@ -336,7 +336,7 @@
<issue
id="RestrictedApiAndroidX"
- message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+ message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
errorLine1=" writeTextToExternalFile(information, fileName)"
errorLine2=" ~~~~~~~~">
<location
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/StreamSharingActivity.kt b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/StreamSharingActivity.kt
index a593ec0..6577ce6 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/StreamSharingActivity.kt
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/StreamSharingActivity.kt
@@ -33,10 +33,10 @@
import androidx.camera.core.UseCase
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.lifecycle.ProcessCameraProvider
-import androidx.camera.testing.impl.E2ETestUtil.canDeviceWriteToMediaStore
-import androidx.camera.testing.impl.E2ETestUtil.generateVideoFileOutputOptions
-import androidx.camera.testing.impl.E2ETestUtil.generateVideoMediaStoreOptions
-import androidx.camera.testing.impl.E2ETestUtil.writeTextToExternalFile
+import androidx.camera.testing.impl.FileUtil.canDeviceWriteToMediaStore
+import androidx.camera.testing.impl.FileUtil.generateVideoFileOutputOptions
+import androidx.camera.testing.impl.FileUtil.generateVideoMediaStoreOptions
+import androidx.camera.testing.impl.FileUtil.writeTextToExternalFile
import androidx.camera.video.PendingRecording
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
diff --git a/collection/collection/api/current.txt b/collection/collection/api/current.txt
index 5bcab89..ec619f8 100644
--- a/collection/collection/api/current.txt
+++ b/collection/collection/api/current.txt
@@ -94,6 +94,70 @@
property public final int last;
}
+ public abstract sealed class FloatFloatMap {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final operator boolean contains(float key);
+ method public final boolean containsKey(float key);
+ method public final boolean containsValue(float value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+ method public final operator float get(float key);
+ method public final int getCapacity();
+ method public final float getOrDefault(float key, float defaultValue);
+ method public final inline float getOrElse(float key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ }
+
+ public final class FloatFloatMapKt {
+ method public static androidx.collection.FloatFloatMap emptyFloatFloatMap();
+ method public static androidx.collection.FloatFloatMap floatFloatMapOf();
+ method public static androidx.collection.FloatFloatMap floatFloatMapOf(kotlin.Pair<java.lang.Float,java.lang.Float>... pairs);
+ method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf();
+ method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf(kotlin.Pair<java.lang.Float,java.lang.Float>... pairs);
+ }
+
+ public abstract sealed class FloatIntMap {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final operator boolean contains(float key);
+ method public final boolean containsKey(float key);
+ method public final boolean containsValue(int value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final operator int get(float key);
+ method public final int getCapacity();
+ method public final int getOrDefault(float key, int defaultValue);
+ method public final inline int getOrElse(float key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ }
+
+ public final class FloatIntMapKt {
+ method public static androidx.collection.FloatIntMap emptyFloatIntMap();
+ method public static androidx.collection.FloatIntMap floatIntMapOf();
+ method public static androidx.collection.FloatIntMap floatIntMapOf(kotlin.Pair<java.lang.Float,java.lang.Integer>... pairs);
+ method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf();
+ method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf(kotlin.Pair<java.lang.Float,java.lang.Integer>... pairs);
+ }
+
public abstract sealed class FloatList {
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
@@ -146,6 +210,70 @@
method public static inline androidx.collection.MutableFloatList mutableFloatListOf(float... elements);
}
+ public abstract sealed class FloatLongMap {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final operator boolean contains(float key);
+ method public final boolean containsKey(float key);
+ method public final boolean containsValue(long value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+ method public final operator long get(float key);
+ method public final int getCapacity();
+ method public final long getOrDefault(float key, long defaultValue);
+ method public final inline long getOrElse(float key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ }
+
+ public final class FloatLongMapKt {
+ method public static androidx.collection.FloatLongMap emptyFloatLongMap();
+ method public static androidx.collection.FloatLongMap floatLongMapOf();
+ method public static androidx.collection.FloatLongMap floatLongMapOf(kotlin.Pair<java.lang.Float,java.lang.Long>... pairs);
+ method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf();
+ method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf(kotlin.Pair<java.lang.Float,java.lang.Long>... pairs);
+ }
+
+ public abstract sealed class FloatObjectMap<V> {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+ method public final operator boolean contains(float key);
+ method public final boolean containsKey(float key);
+ method public final boolean containsValue(V value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super V,kotlin.Unit> block);
+ method public final operator V? get(float key);
+ method public final int getCapacity();
+ method public final V getOrDefault(float key, V defaultValue);
+ method public final inline V getOrElse(float key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ }
+
+ public final class FloatObjectMapKt {
+ method public static <V> androidx.collection.FloatObjectMap<V> emptyFloatObjectMap();
+ method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf();
+ method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf(kotlin.Pair<java.lang.Float,? extends V>... pairs);
+ method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf();
+ method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf(kotlin.Pair<java.lang.Float,? extends V>... pairs);
+ }
+
public abstract sealed class FloatSet {
method public final inline boolean all(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
method public final boolean any();
@@ -179,6 +307,70 @@
method public static androidx.collection.MutableFloatSet mutableFloatSetOf(float... elements);
}
+ public abstract sealed class IntFloatMap {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final operator boolean contains(int key);
+ method public final boolean containsKey(int key);
+ method public final boolean containsValue(float value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+ method public final operator float get(int key);
+ method public final int getCapacity();
+ method public final float getOrDefault(int key, float defaultValue);
+ method public final inline float getOrElse(int key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ }
+
+ public final class IntFloatMapKt {
+ method public static androidx.collection.IntFloatMap emptyIntFloatMap();
+ method public static androidx.collection.IntFloatMap intFloatMapOf();
+ method public static androidx.collection.IntFloatMap intFloatMapOf(kotlin.Pair<java.lang.Integer,java.lang.Float>... pairs);
+ method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf();
+ method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf(kotlin.Pair<java.lang.Integer,java.lang.Float>... pairs);
+ }
+
+ public abstract sealed class IntIntMap {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final operator boolean contains(int key);
+ method public final boolean containsKey(int key);
+ method public final boolean containsValue(int value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final operator int get(int key);
+ method public final int getCapacity();
+ method public final int getOrDefault(int key, int defaultValue);
+ method public final inline int getOrElse(int key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ }
+
+ public final class IntIntMapKt {
+ method public static androidx.collection.IntIntMap emptyIntIntMap();
+ method public static androidx.collection.IntIntMap intIntMapOf();
+ method public static androidx.collection.IntIntMap intIntMapOf(kotlin.Pair<java.lang.Integer,java.lang.Integer>... pairs);
+ method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf();
+ method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf(kotlin.Pair<java.lang.Integer,java.lang.Integer>... pairs);
+ }
+
public abstract sealed class IntList {
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
@@ -231,6 +423,70 @@
method public static inline androidx.collection.MutableIntList mutableIntListOf(int... elements);
}
+ public abstract sealed class IntLongMap {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final operator boolean contains(int key);
+ method public final boolean containsKey(int key);
+ method public final boolean containsValue(long value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+ method public final operator long get(int key);
+ method public final int getCapacity();
+ method public final long getOrDefault(int key, long defaultValue);
+ method public final inline long getOrElse(int key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ }
+
+ public final class IntLongMapKt {
+ method public static androidx.collection.IntLongMap emptyIntLongMap();
+ method public static androidx.collection.IntLongMap intLongMapOf();
+ method public static androidx.collection.IntLongMap intLongMapOf(kotlin.Pair<java.lang.Integer,java.lang.Long>... pairs);
+ method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf();
+ method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf(kotlin.Pair<java.lang.Integer,java.lang.Long>... pairs);
+ }
+
+ public abstract sealed class IntObjectMap<V> {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+ method public final operator boolean contains(int key);
+ method public final boolean containsKey(int key);
+ method public final boolean containsValue(V value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super V,kotlin.Unit> block);
+ method public final operator V? get(int key);
+ method public final int getCapacity();
+ method public final V getOrDefault(int key, V defaultValue);
+ method public final inline V getOrElse(int key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ }
+
+ public final class IntObjectMapKt {
+ method public static <V> androidx.collection.IntObjectMap<V> emptyIntObjectMap();
+ method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf();
+ method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf(kotlin.Pair<java.lang.Integer,? extends V>... pairs);
+ method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf();
+ method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf(kotlin.Pair<java.lang.Integer,? extends V>... pairs);
+ }
+
public abstract sealed class IntSet {
method public final inline boolean all(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
method public final boolean any();
@@ -264,6 +520,70 @@
method public static androidx.collection.MutableIntSet mutableIntSetOf(int... elements);
}
+ public abstract sealed class LongFloatMap {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final operator boolean contains(long key);
+ method public final boolean containsKey(long key);
+ method public final boolean containsValue(float value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+ method public final operator float get(long key);
+ method public final int getCapacity();
+ method public final float getOrDefault(long key, float defaultValue);
+ method public final inline float getOrElse(long key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ }
+
+ public final class LongFloatMapKt {
+ method public static androidx.collection.LongFloatMap emptyLongFloatMap();
+ method public static androidx.collection.LongFloatMap longFloatMapOf();
+ method public static androidx.collection.LongFloatMap longFloatMapOf(kotlin.Pair<java.lang.Long,java.lang.Float>... pairs);
+ method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf();
+ method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf(kotlin.Pair<java.lang.Long,java.lang.Float>... pairs);
+ }
+
+ public abstract sealed class LongIntMap {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final operator boolean contains(long key);
+ method public final boolean containsKey(long key);
+ method public final boolean containsValue(int value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final operator int get(long key);
+ method public final int getCapacity();
+ method public final int getOrDefault(long key, int defaultValue);
+ method public final inline int getOrElse(long key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ }
+
+ public final class LongIntMapKt {
+ method public static androidx.collection.LongIntMap emptyLongIntMap();
+ method public static androidx.collection.LongIntMap longIntMapOf();
+ method public static androidx.collection.LongIntMap longIntMapOf(kotlin.Pair<java.lang.Long,java.lang.Integer>... pairs);
+ method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf();
+ method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf(kotlin.Pair<java.lang.Long,java.lang.Integer>... pairs);
+ }
+
public abstract sealed class LongList {
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
@@ -316,6 +636,70 @@
method public static inline androidx.collection.MutableLongList mutableLongListOf(long... elements);
}
+ public abstract sealed class LongLongMap {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final operator boolean contains(long key);
+ method public final boolean containsKey(long key);
+ method public final boolean containsValue(long value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+ method public final operator long get(long key);
+ method public final int getCapacity();
+ method public final long getOrDefault(long key, long defaultValue);
+ method public final inline long getOrElse(long key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ }
+
+ public final class LongLongMapKt {
+ method public static androidx.collection.LongLongMap emptyLongLongMap();
+ method public static androidx.collection.LongLongMap longLongMapOf();
+ method public static androidx.collection.LongLongMap longLongMapOf(kotlin.Pair<java.lang.Long,java.lang.Long>... pairs);
+ method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf();
+ method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf(kotlin.Pair<java.lang.Long,java.lang.Long>... pairs);
+ }
+
+ public abstract sealed class LongObjectMap<V> {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+ method public final operator boolean contains(long key);
+ method public final boolean containsKey(long key);
+ method public final boolean containsValue(V value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super V,kotlin.Unit> block);
+ method public final operator V? get(long key);
+ method public final int getCapacity();
+ method public final V getOrDefault(long key, V defaultValue);
+ method public final inline V getOrElse(long key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ }
+
+ public final class LongObjectMapKt {
+ method public static <V> androidx.collection.LongObjectMap<V> emptyLongObjectMap();
+ method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf();
+ method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf(kotlin.Pair<java.lang.Long,? extends V>... pairs);
+ method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf();
+ method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf(kotlin.Pair<java.lang.Long,? extends V>... pairs);
+ }
+
public abstract sealed class LongSet {
method public final inline boolean all(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
method public final boolean any();
@@ -416,6 +800,48 @@
method public static inline <K, V> androidx.collection.LruCache<K,V> lruCache(int maxSize, optional kotlin.jvm.functions.Function2<? super K,? super V,java.lang.Integer> sizeOf, optional kotlin.jvm.functions.Function1<? super K,? extends V> create, optional kotlin.jvm.functions.Function4<? super java.lang.Boolean,? super K,? super V,? super V,kotlin.Unit> onEntryRemoved);
}
+ public final class MutableFloatFloatMap extends androidx.collection.FloatFloatMap {
+ ctor public MutableFloatFloatMap(optional int initialCapacity);
+ method public void clear();
+ method public inline float getOrPut(float key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.FloatList keys);
+ method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+ method public inline operator void minusAssign(float key);
+ method public inline operator void minusAssign(float[] keys);
+ method public inline operator void plusAssign(androidx.collection.FloatFloatMap from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Float> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Float>![] pairs);
+ method public void put(float key, float value);
+ method public void putAll(androidx.collection.FloatFloatMap from);
+ method public void putAll(kotlin.Pair<java.lang.Float,java.lang.Float>![] pairs);
+ method public void remove(float key);
+ method public boolean remove(float key, float value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public operator void set(float key, float value);
+ method public int trim();
+ }
+
+ public final class MutableFloatIntMap extends androidx.collection.FloatIntMap {
+ ctor public MutableFloatIntMap(optional int initialCapacity);
+ method public void clear();
+ method public inline int getOrPut(float key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.FloatList keys);
+ method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+ method public inline operator void minusAssign(float key);
+ method public inline operator void minusAssign(float[] keys);
+ method public inline operator void plusAssign(androidx.collection.FloatIntMap from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Integer> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Integer>![] pairs);
+ method public void put(float key, int value);
+ method public void putAll(androidx.collection.FloatIntMap from);
+ method public void putAll(kotlin.Pair<java.lang.Float,java.lang.Integer>![] pairs);
+ method public void remove(float key);
+ method public boolean remove(float key, int value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public operator void set(float key, int value);
+ method public int trim();
+ }
+
public final class MutableFloatList extends androidx.collection.FloatList {
ctor public MutableFloatList(optional int initialCapacity);
method public boolean add(float element);
@@ -447,6 +873,48 @@
property public final inline int capacity;
}
+ public final class MutableFloatLongMap extends androidx.collection.FloatLongMap {
+ ctor public MutableFloatLongMap(optional int initialCapacity);
+ method public void clear();
+ method public inline long getOrPut(float key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.FloatList keys);
+ method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+ method public inline operator void minusAssign(float key);
+ method public inline operator void minusAssign(float[] keys);
+ method public inline operator void plusAssign(androidx.collection.FloatLongMap from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Long> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Long>![] pairs);
+ method public void put(float key, long value);
+ method public void putAll(androidx.collection.FloatLongMap from);
+ method public void putAll(kotlin.Pair<java.lang.Float,java.lang.Long>![] pairs);
+ method public void remove(float key);
+ method public boolean remove(float key, long value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public operator void set(float key, long value);
+ method public int trim();
+ }
+
+ public final class MutableFloatObjectMap<V> extends androidx.collection.FloatObjectMap<V> {
+ ctor public MutableFloatObjectMap(optional int initialCapacity);
+ method public void clear();
+ method public inline V getOrPut(float key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.FloatList keys);
+ method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+ method public inline operator void minusAssign(float key);
+ method public inline operator void minusAssign(float[] keys);
+ method public inline operator void plusAssign(androidx.collection.FloatObjectMap<V> from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,? extends V> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,? extends V>![] pairs);
+ method public V? put(float key, V value);
+ method public void putAll(androidx.collection.FloatObjectMap<V> from);
+ method public void putAll(kotlin.Pair<java.lang.Float,? extends V>![] pairs);
+ method public V? remove(float key);
+ method public boolean remove(float key, V value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+ method public operator void set(float key, V value);
+ method public int trim();
+ }
+
public final class MutableFloatSet extends androidx.collection.FloatSet {
ctor public MutableFloatSet(optional int initialCapacity);
method public boolean add(float element);
@@ -465,6 +933,48 @@
method @IntRange(from=0L) public int trim();
}
+ public final class MutableIntFloatMap extends androidx.collection.IntFloatMap {
+ ctor public MutableIntFloatMap(optional int initialCapacity);
+ method public void clear();
+ method public inline float getOrPut(int key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.IntList keys);
+ method public inline operator void minusAssign(androidx.collection.IntSet keys);
+ method public inline operator void minusAssign(int key);
+ method public inline operator void minusAssign(int[] keys);
+ method public inline operator void plusAssign(androidx.collection.IntFloatMap from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Float> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Float>![] pairs);
+ method public void put(int key, float value);
+ method public void putAll(androidx.collection.IntFloatMap from);
+ method public void putAll(kotlin.Pair<java.lang.Integer,java.lang.Float>![] pairs);
+ method public void remove(int key);
+ method public boolean remove(int key, float value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public operator void set(int key, float value);
+ method public int trim();
+ }
+
+ public final class MutableIntIntMap extends androidx.collection.IntIntMap {
+ ctor public MutableIntIntMap(optional int initialCapacity);
+ method public void clear();
+ method public inline int getOrPut(int key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.IntList keys);
+ method public inline operator void minusAssign(androidx.collection.IntSet keys);
+ method public inline operator void minusAssign(int key);
+ method public inline operator void minusAssign(int[] keys);
+ method public inline operator void plusAssign(androidx.collection.IntIntMap from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Integer> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Integer>![] pairs);
+ method public void put(int key, int value);
+ method public void putAll(androidx.collection.IntIntMap from);
+ method public void putAll(kotlin.Pair<java.lang.Integer,java.lang.Integer>![] pairs);
+ method public void remove(int key);
+ method public boolean remove(int key, int value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public operator void set(int key, int value);
+ method public int trim();
+ }
+
public final class MutableIntList extends androidx.collection.IntList {
ctor public MutableIntList(optional int initialCapacity);
method public boolean add(int element);
@@ -496,6 +1006,48 @@
property public final inline int capacity;
}
+ public final class MutableIntLongMap extends androidx.collection.IntLongMap {
+ ctor public MutableIntLongMap(optional int initialCapacity);
+ method public void clear();
+ method public inline long getOrPut(int key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.IntList keys);
+ method public inline operator void minusAssign(androidx.collection.IntSet keys);
+ method public inline operator void minusAssign(int key);
+ method public inline operator void minusAssign(int[] keys);
+ method public inline operator void plusAssign(androidx.collection.IntLongMap from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Long> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Long>![] pairs);
+ method public void put(int key, long value);
+ method public void putAll(androidx.collection.IntLongMap from);
+ method public void putAll(kotlin.Pair<java.lang.Integer,java.lang.Long>![] pairs);
+ method public void remove(int key);
+ method public boolean remove(int key, long value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public operator void set(int key, long value);
+ method public int trim();
+ }
+
+ public final class MutableIntObjectMap<V> extends androidx.collection.IntObjectMap<V> {
+ ctor public MutableIntObjectMap(optional int initialCapacity);
+ method public void clear();
+ method public inline V getOrPut(int key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.IntList keys);
+ method public inline operator void minusAssign(androidx.collection.IntSet keys);
+ method public inline operator void minusAssign(int key);
+ method public inline operator void minusAssign(int[] keys);
+ method public inline operator void plusAssign(androidx.collection.IntObjectMap<V> from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,? extends V> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,? extends V>![] pairs);
+ method public V? put(int key, V value);
+ method public void putAll(androidx.collection.IntObjectMap<V> from);
+ method public void putAll(kotlin.Pair<java.lang.Integer,? extends V>![] pairs);
+ method public V? remove(int key);
+ method public boolean remove(int key, V value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+ method public operator void set(int key, V value);
+ method public int trim();
+ }
+
public final class MutableIntSet extends androidx.collection.IntSet {
ctor public MutableIntSet(optional int initialCapacity);
method public boolean add(int element);
@@ -514,6 +1066,48 @@
method @IntRange(from=0L) public int trim();
}
+ public final class MutableLongFloatMap extends androidx.collection.LongFloatMap {
+ ctor public MutableLongFloatMap(optional int initialCapacity);
+ method public void clear();
+ method public inline float getOrPut(long key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.LongList keys);
+ method public inline operator void minusAssign(androidx.collection.LongSet keys);
+ method public inline operator void minusAssign(long key);
+ method public inline operator void minusAssign(long[] keys);
+ method public inline operator void plusAssign(androidx.collection.LongFloatMap from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Float> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Float>![] pairs);
+ method public void put(long key, float value);
+ method public void putAll(androidx.collection.LongFloatMap from);
+ method public void putAll(kotlin.Pair<java.lang.Long,java.lang.Float>![] pairs);
+ method public void remove(long key);
+ method public boolean remove(long key, float value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public operator void set(long key, float value);
+ method public int trim();
+ }
+
+ public final class MutableLongIntMap extends androidx.collection.LongIntMap {
+ ctor public MutableLongIntMap(optional int initialCapacity);
+ method public void clear();
+ method public inline int getOrPut(long key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.LongList keys);
+ method public inline operator void minusAssign(androidx.collection.LongSet keys);
+ method public inline operator void minusAssign(long key);
+ method public inline operator void minusAssign(long[] keys);
+ method public inline operator void plusAssign(androidx.collection.LongIntMap from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Integer> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Integer>![] pairs);
+ method public void put(long key, int value);
+ method public void putAll(androidx.collection.LongIntMap from);
+ method public void putAll(kotlin.Pair<java.lang.Long,java.lang.Integer>![] pairs);
+ method public void remove(long key);
+ method public boolean remove(long key, int value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public operator void set(long key, int value);
+ method public int trim();
+ }
+
public final class MutableLongList extends androidx.collection.LongList {
ctor public MutableLongList(optional int initialCapacity);
method public void add(@IntRange(from=0L) int index, long element);
@@ -545,6 +1139,48 @@
property public final inline int capacity;
}
+ public final class MutableLongLongMap extends androidx.collection.LongLongMap {
+ ctor public MutableLongLongMap(optional int initialCapacity);
+ method public void clear();
+ method public inline long getOrPut(long key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.LongList keys);
+ method public inline operator void minusAssign(androidx.collection.LongSet keys);
+ method public inline operator void minusAssign(long key);
+ method public inline operator void minusAssign(long[] keys);
+ method public inline operator void plusAssign(androidx.collection.LongLongMap from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Long> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Long>![] pairs);
+ method public void put(long key, long value);
+ method public void putAll(androidx.collection.LongLongMap from);
+ method public void putAll(kotlin.Pair<java.lang.Long,java.lang.Long>![] pairs);
+ method public void remove(long key);
+ method public boolean remove(long key, long value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public operator void set(long key, long value);
+ method public int trim();
+ }
+
+ public final class MutableLongObjectMap<V> extends androidx.collection.LongObjectMap<V> {
+ ctor public MutableLongObjectMap(optional int initialCapacity);
+ method public void clear();
+ method public inline V getOrPut(long key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.LongList keys);
+ method public inline operator void minusAssign(androidx.collection.LongSet keys);
+ method public inline operator void minusAssign(long key);
+ method public inline operator void minusAssign(long[] keys);
+ method public inline operator void plusAssign(androidx.collection.LongObjectMap<V> from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,? extends V> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,? extends V>![] pairs);
+ method public V? put(long key, V value);
+ method public void putAll(androidx.collection.LongObjectMap<V> from);
+ method public void putAll(kotlin.Pair<java.lang.Long,? extends V>![] pairs);
+ method public V? remove(long key);
+ method public boolean remove(long key, V value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+ method public operator void set(long key, V value);
+ method public int trim();
+ }
+
public final class MutableLongSet extends androidx.collection.LongSet {
ctor public MutableLongSet(optional int initialCapacity);
method public boolean add(long element);
@@ -563,6 +1199,124 @@
method @IntRange(from=0L) public int trim();
}
+ public final class MutableObjectFloatMap<K> extends androidx.collection.ObjectFloatMap<K> {
+ ctor public MutableObjectFloatMap(optional int initialCapacity);
+ method public void clear();
+ method public inline float getOrPut(K key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
+ method public inline operator void minusAssign(Iterable<? extends K> keys);
+ method public inline operator void minusAssign(K key);
+ method public inline operator void minusAssign(K![] keys);
+ method public inline operator void minusAssign(kotlin.sequences.Sequence<? extends K> keys);
+ method public inline operator void plusAssign(androidx.collection.ObjectFloatMap<K> from);
+ method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Float> pair);
+ method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Float>![] pairs);
+ method public void put(K key, float value);
+ method public void putAll(androidx.collection.ObjectFloatMap<K> from);
+ method public void putAll(kotlin.Pair<? extends K,java.lang.Float>![] pairs);
+ method public void remove(K key);
+ method public boolean remove(K key, float value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public operator void set(K key, float value);
+ method public int trim();
+ }
+
+ public final class MutableObjectIntMap<K> extends androidx.collection.ObjectIntMap<K> {
+ ctor public MutableObjectIntMap(optional int initialCapacity);
+ method public void clear();
+ method public inline int getOrPut(K key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
+ method public inline operator void minusAssign(Iterable<? extends K> keys);
+ method public inline operator void minusAssign(K key);
+ method public inline operator void minusAssign(K![] keys);
+ method public inline operator void minusAssign(kotlin.sequences.Sequence<? extends K> keys);
+ method public inline operator void plusAssign(androidx.collection.ObjectIntMap<K> from);
+ method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Integer> pair);
+ method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Integer>![] pairs);
+ method public void put(K key, int value);
+ method public void putAll(androidx.collection.ObjectIntMap<K> from);
+ method public void putAll(kotlin.Pair<? extends K,java.lang.Integer>![] pairs);
+ method public void remove(K key);
+ method public boolean remove(K key, int value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public operator void set(K key, int value);
+ method public int trim();
+ }
+
+ public final class MutableObjectList<E> extends androidx.collection.ObjectList<E> {
+ ctor public MutableObjectList(optional int initialCapacity);
+ method public boolean add(E element);
+ method public void add(@IntRange(from=0L) int index, E element);
+ method public boolean addAll(androidx.collection.ObjectList<E> elements);
+ method public boolean addAll(androidx.collection.ScatterSet<E> elements);
+ method public boolean addAll(E![] elements);
+ method public boolean addAll(@IntRange(from=0L) int index, androidx.collection.ObjectList<E> elements);
+ method public boolean addAll(@IntRange(from=0L) int index, E![] elements);
+ method public boolean addAll(@IntRange(from=0L) int index, java.util.Collection<? extends E> elements);
+ method public boolean addAll(Iterable<? extends E> elements);
+ method public boolean addAll(java.util.List<? extends E> elements);
+ method public boolean addAll(kotlin.sequences.Sequence<? extends E> elements);
+ method public java.util.List<E> asList();
+ method public java.util.List<E> asMutableList();
+ method public void clear();
+ method public void ensureCapacity(int capacity);
+ method public inline int getCapacity();
+ method public operator void minusAssign(androidx.collection.ObjectList<E> elements);
+ method public operator void minusAssign(androidx.collection.ScatterSet<E> elements);
+ method public inline operator void minusAssign(E element);
+ method public operator void minusAssign(E![] elements);
+ method public operator void minusAssign(Iterable<? extends E> elements);
+ method public operator void minusAssign(java.util.List<? extends E> elements);
+ method public operator void minusAssign(kotlin.sequences.Sequence<? extends E> elements);
+ method public operator void plusAssign(androidx.collection.ObjectList<E> elements);
+ method public operator void plusAssign(androidx.collection.ScatterSet<E> elements);
+ method public inline operator void plusAssign(E element);
+ method public operator void plusAssign(E![] elements);
+ method public operator void plusAssign(Iterable<? extends E> elements);
+ method public operator void plusAssign(java.util.List<? extends E> elements);
+ method public operator void plusAssign(kotlin.sequences.Sequence<? extends E> elements);
+ method public boolean remove(E element);
+ method public boolean removeAll(androidx.collection.ObjectList<E> elements);
+ method public boolean removeAll(androidx.collection.ScatterSet<E> elements);
+ method public boolean removeAll(E![] elements);
+ method public boolean removeAll(Iterable<? extends E> elements);
+ method public boolean removeAll(java.util.List<? extends E> elements);
+ method public boolean removeAll(kotlin.sequences.Sequence<? extends E> elements);
+ method public E removeAt(@IntRange(from=0L) int index);
+ method public inline void removeIf(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ method public void removeRange(@IntRange(from=0L) int start, @IntRange(from=0L) int end);
+ method public boolean retainAll(androidx.collection.ObjectList<E> elements);
+ method public boolean retainAll(E![] elements);
+ method public boolean retainAll(Iterable<? extends E> elements);
+ method public boolean retainAll(java.util.Collection<? extends E> elements);
+ method public boolean retainAll(kotlin.sequences.Sequence<? extends E> elements);
+ method public operator E set(@IntRange(from=0L) int index, E element);
+ method public void trim(optional int minCapacity);
+ property public final inline int capacity;
+ }
+
+ public final class MutableObjectLongMap<K> extends androidx.collection.ObjectLongMap<K> {
+ ctor public MutableObjectLongMap(optional int initialCapacity);
+ method public void clear();
+ method public inline long getOrPut(K key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
+ method public inline operator void minusAssign(Iterable<? extends K> keys);
+ method public inline operator void minusAssign(K key);
+ method public inline operator void minusAssign(K![] keys);
+ method public inline operator void minusAssign(kotlin.sequences.Sequence<? extends K> keys);
+ method public inline operator void plusAssign(androidx.collection.ObjectLongMap<K> from);
+ method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Long> pair);
+ method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Long>![] pairs);
+ method public void put(K key, long value);
+ method public void putAll(androidx.collection.ObjectLongMap<K> from);
+ method public void putAll(kotlin.Pair<? extends K,java.lang.Long>![] pairs);
+ method public void remove(K key);
+ method public boolean remove(K key, long value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public operator void set(K key, long value);
+ method public int trim();
+ }
+
public final class MutableScatterMap<K, V> extends androidx.collection.ScatterMap<K,V> {
ctor public MutableScatterMap(optional int initialCapacity);
method public java.util.Map<K,V> asMutableMap();
@@ -619,6 +1373,162 @@
method @IntRange(from=0L) public int trim();
}
+ public abstract sealed class ObjectFloatMap<K> {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final operator boolean contains(K key);
+ method public final boolean containsKey(K key);
+ method public final boolean containsValue(float value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super K,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+ method public final operator float get(K key);
+ method public final int getCapacity();
+ method public final float getOrDefault(K key, float defaultValue);
+ method public final inline float getOrElse(K key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ }
+
+ public final class ObjectFloatMapKt {
+ method public static <K> androidx.collection.ObjectFloatMap<K> emptyObjectFloatMap();
+ method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf();
+ method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf(kotlin.Pair<? extends K,java.lang.Float>... pairs);
+ method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMap();
+ method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMapOf(kotlin.Pair<? extends K,java.lang.Float>... pairs);
+ }
+
+ public abstract sealed class ObjectIntMap<K> {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final operator boolean contains(K key);
+ method public final boolean containsKey(K key);
+ method public final boolean containsValue(int value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super K,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final operator int get(K key);
+ method public final int getCapacity();
+ method public final int getOrDefault(K key, int defaultValue);
+ method public final inline int getOrElse(K key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ }
+
+ public final class ObjectIntMapKt {
+ method public static <K> androidx.collection.ObjectIntMap<K> emptyObjectIntMap();
+ method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf();
+ method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf(kotlin.Pair<? extends K,java.lang.Integer>... pairs);
+ method public static <K> androidx.collection.ObjectIntMap<K> objectIntMap();
+ method public static <K> androidx.collection.ObjectIntMap<K> objectIntMapOf(kotlin.Pair<? extends K,java.lang.Integer>... pairs);
+ }
+
+ public abstract sealed class ObjectList<E> {
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ method public abstract java.util.List<E> asList();
+ method public final operator boolean contains(E element);
+ method public final boolean containsAll(androidx.collection.ObjectList<E> elements);
+ method public final boolean containsAll(E![] elements);
+ method public final boolean containsAll(Iterable<? extends E> elements);
+ method public final boolean containsAll(java.util.List<? extends E> elements);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ method public final E elementAt(@IntRange(from=0L) int index);
+ method public final inline E elementAtOrElse(@IntRange(from=0L) int index, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends E> defaultValue);
+ method public final E first();
+ method public final inline E first(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ method public final inline E? firstOrNull();
+ method public final inline E? firstOrNull(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ method public final inline <R> R fold(R initial, kotlin.jvm.functions.Function2<? super R,? super E,? extends R> operation);
+ method public final inline <R> R foldIndexed(R initial, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super R,? super E,? extends R> operation);
+ method public final inline <R> R foldRight(R initial, kotlin.jvm.functions.Function2<? super E,? super R,? extends R> operation);
+ method public final inline <R> R foldRightIndexed(R initial, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super E,? super R,? extends R> operation);
+ method public final inline void forEach(kotlin.jvm.functions.Function1<? super E,kotlin.Unit> block);
+ method public final inline void forEachIndexed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super E,kotlin.Unit> block);
+ method public final inline void forEachReversed(kotlin.jvm.functions.Function1<? super E,kotlin.Unit> block);
+ method public final inline void forEachReversedIndexed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super E,kotlin.Unit> block);
+ method public final operator E get(@IntRange(from=0L) int index);
+ method public final inline kotlin.ranges.IntRange getIndices();
+ method @IntRange(from=-1L) public final inline int getLastIndex();
+ method @IntRange(from=0L) public final int getSize();
+ method public final int indexOf(E element);
+ method public final inline int indexOfFirst(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final E last();
+ method public final inline E last(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ method public final int lastIndexOf(E element);
+ method public final inline E? lastOrNull();
+ method public final inline E? lastOrNull(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ method public final boolean none();
+ method public final inline boolean reversedAny(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ property public final inline kotlin.ranges.IntRange indices;
+ property @IntRange(from=-1L) public final inline int lastIndex;
+ property @IntRange(from=0L) public final int size;
+ }
+
+ public final class ObjectListKt {
+ method public static <E> androidx.collection.ObjectList<E> emptyObjectList();
+ method public static inline <E> androidx.collection.MutableObjectList<E> mutableObjectListOf();
+ method public static <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E element1);
+ method public static <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E element1, E element2);
+ method public static <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E element1, E element2, E element3);
+ method public static inline <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E?... elements);
+ method public static <E> androidx.collection.ObjectList<E> objectListOf();
+ method public static <E> androidx.collection.ObjectList<E> objectListOf(E element1);
+ method public static <E> androidx.collection.ObjectList<E> objectListOf(E element1, E element2);
+ method public static <E> androidx.collection.ObjectList<E> objectListOf(E element1, E element2, E element3);
+ method public static <E> androidx.collection.ObjectList<E> objectListOf(E?... elements);
+ }
+
+ public abstract sealed class ObjectLongMap<K> {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final operator boolean contains(K key);
+ method public final boolean containsKey(K key);
+ method public final boolean containsValue(long value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super K,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+ method public final operator long get(K key);
+ method public final int getCapacity();
+ method public final long getOrDefault(K key, long defaultValue);
+ method public final inline long getOrElse(K key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ }
+
+ public final class ObjectLongMapKt {
+ method public static <K> androidx.collection.ObjectLongMap<K> emptyObjectLongMap();
+ method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf();
+ method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf(kotlin.Pair<? extends K,java.lang.Long>... pairs);
+ method public static <K> androidx.collection.ObjectLongMap<K> objectLongMap();
+ method public static <K> androidx.collection.ObjectLongMap<K> objectLongMapOf(kotlin.Pair<? extends K,java.lang.Long>... pairs);
+ }
+
@kotlin.jvm.JvmInline public final value class PairFloatFloat {
ctor public PairFloatFloat(float first, float second);
method public inline operator float component1();
diff --git a/collection/collection/api/restricted_current.txt b/collection/collection/api/restricted_current.txt
index a46df5c..098c0eb 100644
--- a/collection/collection/api/restricted_current.txt
+++ b/collection/collection/api/restricted_current.txt
@@ -94,6 +94,80 @@
property public final int last;
}
+ public abstract sealed class FloatFloatMap {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final operator boolean contains(float key);
+ method public final boolean containsKey(float key);
+ method public final boolean containsValue(float value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+ method @kotlin.PublishedApi internal final int findKeyIndex(float key);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,kotlin.Unit> block);
+ method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+ method public final operator float get(float key);
+ method public final int getCapacity();
+ method public final float getOrDefault(float key, float defaultValue);
+ method public final inline float getOrElse(float key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ field @kotlin.PublishedApi internal float[] keys;
+ field @kotlin.PublishedApi internal long[] metadata;
+ field @kotlin.PublishedApi internal float[] values;
+ }
+
+ public final class FloatFloatMapKt {
+ method public static androidx.collection.FloatFloatMap emptyFloatFloatMap();
+ method public static androidx.collection.FloatFloatMap floatFloatMapOf();
+ method public static androidx.collection.FloatFloatMap floatFloatMapOf(kotlin.Pair<java.lang.Float,java.lang.Float>... pairs);
+ method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf();
+ method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf(kotlin.Pair<java.lang.Float,java.lang.Float>... pairs);
+ }
+
+ public abstract sealed class FloatIntMap {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final operator boolean contains(float key);
+ method public final boolean containsKey(float key);
+ method public final boolean containsValue(int value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method @kotlin.PublishedApi internal final int findKeyIndex(float key);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,kotlin.Unit> block);
+ method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final operator int get(float key);
+ method public final int getCapacity();
+ method public final int getOrDefault(float key, int defaultValue);
+ method public final inline int getOrElse(float key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ field @kotlin.PublishedApi internal float[] keys;
+ field @kotlin.PublishedApi internal long[] metadata;
+ field @kotlin.PublishedApi internal int[] values;
+ }
+
+ public final class FloatIntMapKt {
+ method public static androidx.collection.FloatIntMap emptyFloatIntMap();
+ method public static androidx.collection.FloatIntMap floatIntMapOf();
+ method public static androidx.collection.FloatIntMap floatIntMapOf(kotlin.Pair<java.lang.Float,java.lang.Integer>... pairs);
+ method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf();
+ method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf(kotlin.Pair<java.lang.Float,java.lang.Integer>... pairs);
+ }
+
public abstract sealed class FloatList {
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
@@ -148,6 +222,79 @@
method public static inline androidx.collection.MutableFloatList mutableFloatListOf(float... elements);
}
+ public abstract sealed class FloatLongMap {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final operator boolean contains(float key);
+ method public final boolean containsKey(float key);
+ method public final boolean containsValue(long value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+ method @kotlin.PublishedApi internal final int findKeyIndex(float key);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,kotlin.Unit> block);
+ method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+ method public final operator long get(float key);
+ method public final int getCapacity();
+ method public final long getOrDefault(float key, long defaultValue);
+ method public final inline long getOrElse(float key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ field @kotlin.PublishedApi internal float[] keys;
+ field @kotlin.PublishedApi internal long[] metadata;
+ field @kotlin.PublishedApi internal long[] values;
+ }
+
+ public final class FloatLongMapKt {
+ method public static androidx.collection.FloatLongMap emptyFloatLongMap();
+ method public static androidx.collection.FloatLongMap floatLongMapOf();
+ method public static androidx.collection.FloatLongMap floatLongMapOf(kotlin.Pair<java.lang.Float,java.lang.Long>... pairs);
+ method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf();
+ method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf(kotlin.Pair<java.lang.Float,java.lang.Long>... pairs);
+ }
+
+ public abstract sealed class FloatObjectMap<V> {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+ method public final operator boolean contains(float key);
+ method public final boolean containsKey(float key);
+ method public final boolean containsValue(V value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,kotlin.Unit> block);
+ method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super V,kotlin.Unit> block);
+ method public final operator V? get(float key);
+ method public final int getCapacity();
+ method public final V getOrDefault(float key, V defaultValue);
+ method public final inline V getOrElse(float key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ field @kotlin.PublishedApi internal float[] keys;
+ field @kotlin.PublishedApi internal long[] metadata;
+ field @kotlin.PublishedApi internal Object![] values;
+ }
+
+ public final class FloatObjectMapKt {
+ method public static <V> androidx.collection.FloatObjectMap<V> emptyFloatObjectMap();
+ method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf();
+ method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf(kotlin.Pair<java.lang.Float,? extends V>... pairs);
+ method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf();
+ method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf(kotlin.Pair<java.lang.Float,? extends V>... pairs);
+ }
+
public abstract sealed class FloatSet {
method public final inline boolean all(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
method public final boolean any();
@@ -184,6 +331,80 @@
method public static androidx.collection.MutableFloatSet mutableFloatSetOf(float... elements);
}
+ public abstract sealed class IntFloatMap {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final operator boolean contains(int key);
+ method public final boolean containsKey(int key);
+ method public final boolean containsValue(float value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+ method @kotlin.PublishedApi internal final int findKeyIndex(int key);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,kotlin.Unit> block);
+ method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+ method public final operator float get(int key);
+ method public final int getCapacity();
+ method public final float getOrDefault(int key, float defaultValue);
+ method public final inline float getOrElse(int key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ field @kotlin.PublishedApi internal int[] keys;
+ field @kotlin.PublishedApi internal long[] metadata;
+ field @kotlin.PublishedApi internal float[] values;
+ }
+
+ public final class IntFloatMapKt {
+ method public static androidx.collection.IntFloatMap emptyIntFloatMap();
+ method public static androidx.collection.IntFloatMap intFloatMapOf();
+ method public static androidx.collection.IntFloatMap intFloatMapOf(kotlin.Pair<java.lang.Integer,java.lang.Float>... pairs);
+ method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf();
+ method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf(kotlin.Pair<java.lang.Integer,java.lang.Float>... pairs);
+ }
+
+ public abstract sealed class IntIntMap {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final operator boolean contains(int key);
+ method public final boolean containsKey(int key);
+ method public final boolean containsValue(int value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method @kotlin.PublishedApi internal final int findKeyIndex(int key);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,kotlin.Unit> block);
+ method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final operator int get(int key);
+ method public final int getCapacity();
+ method public final int getOrDefault(int key, int defaultValue);
+ method public final inline int getOrElse(int key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ field @kotlin.PublishedApi internal int[] keys;
+ field @kotlin.PublishedApi internal long[] metadata;
+ field @kotlin.PublishedApi internal int[] values;
+ }
+
+ public final class IntIntMapKt {
+ method public static androidx.collection.IntIntMap emptyIntIntMap();
+ method public static androidx.collection.IntIntMap intIntMapOf();
+ method public static androidx.collection.IntIntMap intIntMapOf(kotlin.Pair<java.lang.Integer,java.lang.Integer>... pairs);
+ method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf();
+ method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf(kotlin.Pair<java.lang.Integer,java.lang.Integer>... pairs);
+ }
+
public abstract sealed class IntList {
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
@@ -238,6 +459,79 @@
method public static inline androidx.collection.MutableIntList mutableIntListOf(int... elements);
}
+ public abstract sealed class IntLongMap {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final operator boolean contains(int key);
+ method public final boolean containsKey(int key);
+ method public final boolean containsValue(long value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+ method @kotlin.PublishedApi internal final int findKeyIndex(int key);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,kotlin.Unit> block);
+ method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+ method public final operator long get(int key);
+ method public final int getCapacity();
+ method public final long getOrDefault(int key, long defaultValue);
+ method public final inline long getOrElse(int key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ field @kotlin.PublishedApi internal int[] keys;
+ field @kotlin.PublishedApi internal long[] metadata;
+ field @kotlin.PublishedApi internal long[] values;
+ }
+
+ public final class IntLongMapKt {
+ method public static androidx.collection.IntLongMap emptyIntLongMap();
+ method public static androidx.collection.IntLongMap intLongMapOf();
+ method public static androidx.collection.IntLongMap intLongMapOf(kotlin.Pair<java.lang.Integer,java.lang.Long>... pairs);
+ method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf();
+ method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf(kotlin.Pair<java.lang.Integer,java.lang.Long>... pairs);
+ }
+
+ public abstract sealed class IntObjectMap<V> {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+ method public final operator boolean contains(int key);
+ method public final boolean containsKey(int key);
+ method public final boolean containsValue(V value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,kotlin.Unit> block);
+ method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super V,kotlin.Unit> block);
+ method public final operator V? get(int key);
+ method public final int getCapacity();
+ method public final V getOrDefault(int key, V defaultValue);
+ method public final inline V getOrElse(int key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ field @kotlin.PublishedApi internal int[] keys;
+ field @kotlin.PublishedApi internal long[] metadata;
+ field @kotlin.PublishedApi internal Object![] values;
+ }
+
+ public final class IntObjectMapKt {
+ method public static <V> androidx.collection.IntObjectMap<V> emptyIntObjectMap();
+ method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf();
+ method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf(kotlin.Pair<java.lang.Integer,? extends V>... pairs);
+ method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf();
+ method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf(kotlin.Pair<java.lang.Integer,? extends V>... pairs);
+ }
+
public abstract sealed class IntSet {
method public final inline boolean all(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
method public final boolean any();
@@ -274,6 +568,80 @@
method public static androidx.collection.MutableIntSet mutableIntSetOf(int... elements);
}
+ public abstract sealed class LongFloatMap {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final operator boolean contains(long key);
+ method public final boolean containsKey(long key);
+ method public final boolean containsValue(float value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+ method @kotlin.PublishedApi internal final int findKeyIndex(long key);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,kotlin.Unit> block);
+ method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+ method public final operator float get(long key);
+ method public final int getCapacity();
+ method public final float getOrDefault(long key, float defaultValue);
+ method public final inline float getOrElse(long key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ field @kotlin.PublishedApi internal long[] keys;
+ field @kotlin.PublishedApi internal long[] metadata;
+ field @kotlin.PublishedApi internal float[] values;
+ }
+
+ public final class LongFloatMapKt {
+ method public static androidx.collection.LongFloatMap emptyLongFloatMap();
+ method public static androidx.collection.LongFloatMap longFloatMapOf();
+ method public static androidx.collection.LongFloatMap longFloatMapOf(kotlin.Pair<java.lang.Long,java.lang.Float>... pairs);
+ method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf();
+ method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf(kotlin.Pair<java.lang.Long,java.lang.Float>... pairs);
+ }
+
+ public abstract sealed class LongIntMap {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final operator boolean contains(long key);
+ method public final boolean containsKey(long key);
+ method public final boolean containsValue(int value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method @kotlin.PublishedApi internal final int findKeyIndex(long key);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,kotlin.Unit> block);
+ method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final operator int get(long key);
+ method public final int getCapacity();
+ method public final int getOrDefault(long key, int defaultValue);
+ method public final inline int getOrElse(long key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ field @kotlin.PublishedApi internal long[] keys;
+ field @kotlin.PublishedApi internal long[] metadata;
+ field @kotlin.PublishedApi internal int[] values;
+ }
+
+ public final class LongIntMapKt {
+ method public static androidx.collection.LongIntMap emptyLongIntMap();
+ method public static androidx.collection.LongIntMap longIntMapOf();
+ method public static androidx.collection.LongIntMap longIntMapOf(kotlin.Pair<java.lang.Long,java.lang.Integer>... pairs);
+ method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf();
+ method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf(kotlin.Pair<java.lang.Long,java.lang.Integer>... pairs);
+ }
+
public abstract sealed class LongList {
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
@@ -328,6 +696,79 @@
method public static inline androidx.collection.MutableLongList mutableLongListOf(long... elements);
}
+ public abstract sealed class LongLongMap {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final operator boolean contains(long key);
+ method public final boolean containsKey(long key);
+ method public final boolean containsValue(long value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+ method @kotlin.PublishedApi internal final int findKeyIndex(long key);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,kotlin.Unit> block);
+ method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+ method public final operator long get(long key);
+ method public final int getCapacity();
+ method public final long getOrDefault(long key, long defaultValue);
+ method public final inline long getOrElse(long key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ field @kotlin.PublishedApi internal long[] keys;
+ field @kotlin.PublishedApi internal long[] metadata;
+ field @kotlin.PublishedApi internal long[] values;
+ }
+
+ public final class LongLongMapKt {
+ method public static androidx.collection.LongLongMap emptyLongLongMap();
+ method public static androidx.collection.LongLongMap longLongMapOf();
+ method public static androidx.collection.LongLongMap longLongMapOf(kotlin.Pair<java.lang.Long,java.lang.Long>... pairs);
+ method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf();
+ method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf(kotlin.Pair<java.lang.Long,java.lang.Long>... pairs);
+ }
+
+ public abstract sealed class LongObjectMap<V> {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+ method public final operator boolean contains(long key);
+ method public final boolean containsKey(long key);
+ method public final boolean containsValue(V value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,kotlin.Unit> block);
+ method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super V,kotlin.Unit> block);
+ method public final operator V? get(long key);
+ method public final int getCapacity();
+ method public final V getOrDefault(long key, V defaultValue);
+ method public final inline V getOrElse(long key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ field @kotlin.PublishedApi internal long[] keys;
+ field @kotlin.PublishedApi internal long[] metadata;
+ field @kotlin.PublishedApi internal Object![] values;
+ }
+
+ public final class LongObjectMapKt {
+ method public static <V> androidx.collection.LongObjectMap<V> emptyLongObjectMap();
+ method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf();
+ method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf(kotlin.Pair<java.lang.Long,? extends V>... pairs);
+ method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf();
+ method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf(kotlin.Pair<java.lang.Long,? extends V>... pairs);
+ }
+
public abstract sealed class LongSet {
method public final inline boolean all(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
method public final boolean any();
@@ -431,6 +872,48 @@
method public static inline <K, V> androidx.collection.LruCache<K,V> lruCache(int maxSize, optional kotlin.jvm.functions.Function2<? super K,? super V,java.lang.Integer> sizeOf, optional kotlin.jvm.functions.Function1<? super K,? extends V> create, optional kotlin.jvm.functions.Function4<? super java.lang.Boolean,? super K,? super V,? super V,kotlin.Unit> onEntryRemoved);
}
+ public final class MutableFloatFloatMap extends androidx.collection.FloatFloatMap {
+ ctor public MutableFloatFloatMap(optional int initialCapacity);
+ method public void clear();
+ method public inline float getOrPut(float key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.FloatList keys);
+ method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+ method public inline operator void minusAssign(float key);
+ method public inline operator void minusAssign(float[] keys);
+ method public inline operator void plusAssign(androidx.collection.FloatFloatMap from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Float> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Float>![] pairs);
+ method public void put(float key, float value);
+ method public void putAll(androidx.collection.FloatFloatMap from);
+ method public void putAll(kotlin.Pair<java.lang.Float,java.lang.Float>![] pairs);
+ method public void remove(float key);
+ method public boolean remove(float key, float value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public operator void set(float key, float value);
+ method public int trim();
+ }
+
+ public final class MutableFloatIntMap extends androidx.collection.FloatIntMap {
+ ctor public MutableFloatIntMap(optional int initialCapacity);
+ method public void clear();
+ method public inline int getOrPut(float key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.FloatList keys);
+ method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+ method public inline operator void minusAssign(float key);
+ method public inline operator void minusAssign(float[] keys);
+ method public inline operator void plusAssign(androidx.collection.FloatIntMap from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Integer> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Integer>![] pairs);
+ method public void put(float key, int value);
+ method public void putAll(androidx.collection.FloatIntMap from);
+ method public void putAll(kotlin.Pair<java.lang.Float,java.lang.Integer>![] pairs);
+ method public void remove(float key);
+ method public boolean remove(float key, int value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public operator void set(float key, int value);
+ method public int trim();
+ }
+
public final class MutableFloatList extends androidx.collection.FloatList {
ctor public MutableFloatList(optional int initialCapacity);
method public boolean add(float element);
@@ -462,6 +945,48 @@
property public final inline int capacity;
}
+ public final class MutableFloatLongMap extends androidx.collection.FloatLongMap {
+ ctor public MutableFloatLongMap(optional int initialCapacity);
+ method public void clear();
+ method public inline long getOrPut(float key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.FloatList keys);
+ method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+ method public inline operator void minusAssign(float key);
+ method public inline operator void minusAssign(float[] keys);
+ method public inline operator void plusAssign(androidx.collection.FloatLongMap from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Long> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Long>![] pairs);
+ method public void put(float key, long value);
+ method public void putAll(androidx.collection.FloatLongMap from);
+ method public void putAll(kotlin.Pair<java.lang.Float,java.lang.Long>![] pairs);
+ method public void remove(float key);
+ method public boolean remove(float key, long value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public operator void set(float key, long value);
+ method public int trim();
+ }
+
+ public final class MutableFloatObjectMap<V> extends androidx.collection.FloatObjectMap<V> {
+ ctor public MutableFloatObjectMap(optional int initialCapacity);
+ method public void clear();
+ method public inline V getOrPut(float key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.FloatList keys);
+ method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+ method public inline operator void minusAssign(float key);
+ method public inline operator void minusAssign(float[] keys);
+ method public inline operator void plusAssign(androidx.collection.FloatObjectMap<V> from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,? extends V> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,? extends V>![] pairs);
+ method public V? put(float key, V value);
+ method public void putAll(androidx.collection.FloatObjectMap<V> from);
+ method public void putAll(kotlin.Pair<java.lang.Float,? extends V>![] pairs);
+ method public V? remove(float key);
+ method public boolean remove(float key, V value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+ method public operator void set(float key, V value);
+ method public int trim();
+ }
+
public final class MutableFloatSet extends androidx.collection.FloatSet {
ctor public MutableFloatSet(optional int initialCapacity);
method public boolean add(float element);
@@ -480,6 +1005,48 @@
method @IntRange(from=0L) public int trim();
}
+ public final class MutableIntFloatMap extends androidx.collection.IntFloatMap {
+ ctor public MutableIntFloatMap(optional int initialCapacity);
+ method public void clear();
+ method public inline float getOrPut(int key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.IntList keys);
+ method public inline operator void minusAssign(androidx.collection.IntSet keys);
+ method public inline operator void minusAssign(int key);
+ method public inline operator void minusAssign(int[] keys);
+ method public inline operator void plusAssign(androidx.collection.IntFloatMap from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Float> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Float>![] pairs);
+ method public void put(int key, float value);
+ method public void putAll(androidx.collection.IntFloatMap from);
+ method public void putAll(kotlin.Pair<java.lang.Integer,java.lang.Float>![] pairs);
+ method public void remove(int key);
+ method public boolean remove(int key, float value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public operator void set(int key, float value);
+ method public int trim();
+ }
+
+ public final class MutableIntIntMap extends androidx.collection.IntIntMap {
+ ctor public MutableIntIntMap(optional int initialCapacity);
+ method public void clear();
+ method public inline int getOrPut(int key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.IntList keys);
+ method public inline operator void minusAssign(androidx.collection.IntSet keys);
+ method public inline operator void minusAssign(int key);
+ method public inline operator void minusAssign(int[] keys);
+ method public inline operator void plusAssign(androidx.collection.IntIntMap from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Integer> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Integer>![] pairs);
+ method public void put(int key, int value);
+ method public void putAll(androidx.collection.IntIntMap from);
+ method public void putAll(kotlin.Pair<java.lang.Integer,java.lang.Integer>![] pairs);
+ method public void remove(int key);
+ method public boolean remove(int key, int value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public operator void set(int key, int value);
+ method public int trim();
+ }
+
public final class MutableIntList extends androidx.collection.IntList {
ctor public MutableIntList(optional int initialCapacity);
method public boolean add(int element);
@@ -511,6 +1078,48 @@
property public final inline int capacity;
}
+ public final class MutableIntLongMap extends androidx.collection.IntLongMap {
+ ctor public MutableIntLongMap(optional int initialCapacity);
+ method public void clear();
+ method public inline long getOrPut(int key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.IntList keys);
+ method public inline operator void minusAssign(androidx.collection.IntSet keys);
+ method public inline operator void minusAssign(int key);
+ method public inline operator void minusAssign(int[] keys);
+ method public inline operator void plusAssign(androidx.collection.IntLongMap from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Long> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Long>![] pairs);
+ method public void put(int key, long value);
+ method public void putAll(androidx.collection.IntLongMap from);
+ method public void putAll(kotlin.Pair<java.lang.Integer,java.lang.Long>![] pairs);
+ method public void remove(int key);
+ method public boolean remove(int key, long value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public operator void set(int key, long value);
+ method public int trim();
+ }
+
+ public final class MutableIntObjectMap<V> extends androidx.collection.IntObjectMap<V> {
+ ctor public MutableIntObjectMap(optional int initialCapacity);
+ method public void clear();
+ method public inline V getOrPut(int key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.IntList keys);
+ method public inline operator void minusAssign(androidx.collection.IntSet keys);
+ method public inline operator void minusAssign(int key);
+ method public inline operator void minusAssign(int[] keys);
+ method public inline operator void plusAssign(androidx.collection.IntObjectMap<V> from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,? extends V> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,? extends V>![] pairs);
+ method public V? put(int key, V value);
+ method public void putAll(androidx.collection.IntObjectMap<V> from);
+ method public void putAll(kotlin.Pair<java.lang.Integer,? extends V>![] pairs);
+ method public V? remove(int key);
+ method public boolean remove(int key, V value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+ method public operator void set(int key, V value);
+ method public int trim();
+ }
+
public final class MutableIntSet extends androidx.collection.IntSet {
ctor public MutableIntSet(optional int initialCapacity);
method public boolean add(int element);
@@ -529,6 +1138,48 @@
method @IntRange(from=0L) public int trim();
}
+ public final class MutableLongFloatMap extends androidx.collection.LongFloatMap {
+ ctor public MutableLongFloatMap(optional int initialCapacity);
+ method public void clear();
+ method public inline float getOrPut(long key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.LongList keys);
+ method public inline operator void minusAssign(androidx.collection.LongSet keys);
+ method public inline operator void minusAssign(long key);
+ method public inline operator void minusAssign(long[] keys);
+ method public inline operator void plusAssign(androidx.collection.LongFloatMap from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Float> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Float>![] pairs);
+ method public void put(long key, float value);
+ method public void putAll(androidx.collection.LongFloatMap from);
+ method public void putAll(kotlin.Pair<java.lang.Long,java.lang.Float>![] pairs);
+ method public void remove(long key);
+ method public boolean remove(long key, float value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public operator void set(long key, float value);
+ method public int trim();
+ }
+
+ public final class MutableLongIntMap extends androidx.collection.LongIntMap {
+ ctor public MutableLongIntMap(optional int initialCapacity);
+ method public void clear();
+ method public inline int getOrPut(long key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.LongList keys);
+ method public inline operator void minusAssign(androidx.collection.LongSet keys);
+ method public inline operator void minusAssign(long key);
+ method public inline operator void minusAssign(long[] keys);
+ method public inline operator void plusAssign(androidx.collection.LongIntMap from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Integer> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Integer>![] pairs);
+ method public void put(long key, int value);
+ method public void putAll(androidx.collection.LongIntMap from);
+ method public void putAll(kotlin.Pair<java.lang.Long,java.lang.Integer>![] pairs);
+ method public void remove(long key);
+ method public boolean remove(long key, int value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public operator void set(long key, int value);
+ method public int trim();
+ }
+
public final class MutableLongList extends androidx.collection.LongList {
ctor public MutableLongList(optional int initialCapacity);
method public void add(@IntRange(from=0L) int index, long element);
@@ -560,6 +1211,48 @@
property public final inline int capacity;
}
+ public final class MutableLongLongMap extends androidx.collection.LongLongMap {
+ ctor public MutableLongLongMap(optional int initialCapacity);
+ method public void clear();
+ method public inline long getOrPut(long key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.LongList keys);
+ method public inline operator void minusAssign(androidx.collection.LongSet keys);
+ method public inline operator void minusAssign(long key);
+ method public inline operator void minusAssign(long[] keys);
+ method public inline operator void plusAssign(androidx.collection.LongLongMap from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Long> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Long>![] pairs);
+ method public void put(long key, long value);
+ method public void putAll(androidx.collection.LongLongMap from);
+ method public void putAll(kotlin.Pair<java.lang.Long,java.lang.Long>![] pairs);
+ method public void remove(long key);
+ method public boolean remove(long key, long value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public operator void set(long key, long value);
+ method public int trim();
+ }
+
+ public final class MutableLongObjectMap<V> extends androidx.collection.LongObjectMap<V> {
+ ctor public MutableLongObjectMap(optional int initialCapacity);
+ method public void clear();
+ method public inline V getOrPut(long key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.LongList keys);
+ method public inline operator void minusAssign(androidx.collection.LongSet keys);
+ method public inline operator void minusAssign(long key);
+ method public inline operator void minusAssign(long[] keys);
+ method public inline operator void plusAssign(androidx.collection.LongObjectMap<V> from);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,? extends V> pair);
+ method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,? extends V>![] pairs);
+ method public V? put(long key, V value);
+ method public void putAll(androidx.collection.LongObjectMap<V> from);
+ method public void putAll(kotlin.Pair<java.lang.Long,? extends V>![] pairs);
+ method public V? remove(long key);
+ method public boolean remove(long key, V value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+ method public operator void set(long key, V value);
+ method public int trim();
+ }
+
public final class MutableLongSet extends androidx.collection.LongSet {
ctor public MutableLongSet(optional int initialCapacity);
method public boolean add(long element);
@@ -578,6 +1271,124 @@
method @IntRange(from=0L) public int trim();
}
+ public final class MutableObjectFloatMap<K> extends androidx.collection.ObjectFloatMap<K> {
+ ctor public MutableObjectFloatMap(optional int initialCapacity);
+ method public void clear();
+ method public inline float getOrPut(K key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
+ method public inline operator void minusAssign(Iterable<? extends K> keys);
+ method public inline operator void minusAssign(K key);
+ method public inline operator void minusAssign(K![] keys);
+ method public inline operator void minusAssign(kotlin.sequences.Sequence<? extends K> keys);
+ method public inline operator void plusAssign(androidx.collection.ObjectFloatMap<K> from);
+ method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Float> pair);
+ method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Float>![] pairs);
+ method public void put(K key, float value);
+ method public void putAll(androidx.collection.ObjectFloatMap<K> from);
+ method public void putAll(kotlin.Pair<? extends K,java.lang.Float>![] pairs);
+ method public void remove(K key);
+ method public boolean remove(K key, float value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public operator void set(K key, float value);
+ method public int trim();
+ }
+
+ public final class MutableObjectIntMap<K> extends androidx.collection.ObjectIntMap<K> {
+ ctor public MutableObjectIntMap(optional int initialCapacity);
+ method public void clear();
+ method public inline int getOrPut(K key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
+ method public inline operator void minusAssign(Iterable<? extends K> keys);
+ method public inline operator void minusAssign(K key);
+ method public inline operator void minusAssign(K![] keys);
+ method public inline operator void minusAssign(kotlin.sequences.Sequence<? extends K> keys);
+ method public inline operator void plusAssign(androidx.collection.ObjectIntMap<K> from);
+ method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Integer> pair);
+ method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Integer>![] pairs);
+ method public void put(K key, int value);
+ method public void putAll(androidx.collection.ObjectIntMap<K> from);
+ method public void putAll(kotlin.Pair<? extends K,java.lang.Integer>![] pairs);
+ method public void remove(K key);
+ method public boolean remove(K key, int value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public operator void set(K key, int value);
+ method public int trim();
+ }
+
+ public final class MutableObjectList<E> extends androidx.collection.ObjectList<E> {
+ ctor public MutableObjectList(optional int initialCapacity);
+ method public boolean add(E element);
+ method public void add(@IntRange(from=0L) int index, E element);
+ method public boolean addAll(androidx.collection.ObjectList<E> elements);
+ method public boolean addAll(androidx.collection.ScatterSet<E> elements);
+ method public boolean addAll(E![] elements);
+ method public boolean addAll(@IntRange(from=0L) int index, androidx.collection.ObjectList<E> elements);
+ method public boolean addAll(@IntRange(from=0L) int index, E![] elements);
+ method public boolean addAll(@IntRange(from=0L) int index, java.util.Collection<? extends E> elements);
+ method public boolean addAll(Iterable<? extends E> elements);
+ method public boolean addAll(java.util.List<? extends E> elements);
+ method public boolean addAll(kotlin.sequences.Sequence<? extends E> elements);
+ method public java.util.List<E> asList();
+ method public java.util.List<E> asMutableList();
+ method public void clear();
+ method public void ensureCapacity(int capacity);
+ method public inline int getCapacity();
+ method public operator void minusAssign(androidx.collection.ObjectList<E> elements);
+ method public operator void minusAssign(androidx.collection.ScatterSet<E> elements);
+ method public inline operator void minusAssign(E element);
+ method public operator void minusAssign(E![] elements);
+ method public operator void minusAssign(Iterable<? extends E> elements);
+ method public operator void minusAssign(java.util.List<? extends E> elements);
+ method public operator void minusAssign(kotlin.sequences.Sequence<? extends E> elements);
+ method public operator void plusAssign(androidx.collection.ObjectList<E> elements);
+ method public operator void plusAssign(androidx.collection.ScatterSet<E> elements);
+ method public inline operator void plusAssign(E element);
+ method public operator void plusAssign(E![] elements);
+ method public operator void plusAssign(Iterable<? extends E> elements);
+ method public operator void plusAssign(java.util.List<? extends E> elements);
+ method public operator void plusAssign(kotlin.sequences.Sequence<? extends E> elements);
+ method public boolean remove(E element);
+ method public boolean removeAll(androidx.collection.ObjectList<E> elements);
+ method public boolean removeAll(androidx.collection.ScatterSet<E> elements);
+ method public boolean removeAll(E![] elements);
+ method public boolean removeAll(Iterable<? extends E> elements);
+ method public boolean removeAll(java.util.List<? extends E> elements);
+ method public boolean removeAll(kotlin.sequences.Sequence<? extends E> elements);
+ method public E removeAt(@IntRange(from=0L) int index);
+ method public inline void removeIf(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ method public void removeRange(@IntRange(from=0L) int start, @IntRange(from=0L) int end);
+ method public boolean retainAll(androidx.collection.ObjectList<E> elements);
+ method public boolean retainAll(E![] elements);
+ method public boolean retainAll(Iterable<? extends E> elements);
+ method public boolean retainAll(java.util.Collection<? extends E> elements);
+ method public boolean retainAll(kotlin.sequences.Sequence<? extends E> elements);
+ method public operator E set(@IntRange(from=0L) int index, E element);
+ method public void trim(optional int minCapacity);
+ property public final inline int capacity;
+ }
+
+ public final class MutableObjectLongMap<K> extends androidx.collection.ObjectLongMap<K> {
+ ctor public MutableObjectLongMap(optional int initialCapacity);
+ method public void clear();
+ method public inline long getOrPut(K key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+ method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
+ method public inline operator void minusAssign(Iterable<? extends K> keys);
+ method public inline operator void minusAssign(K key);
+ method public inline operator void minusAssign(K![] keys);
+ method public inline operator void minusAssign(kotlin.sequences.Sequence<? extends K> keys);
+ method public inline operator void plusAssign(androidx.collection.ObjectLongMap<K> from);
+ method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Long> pair);
+ method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Long>![] pairs);
+ method public void put(K key, long value);
+ method public void putAll(androidx.collection.ObjectLongMap<K> from);
+ method public void putAll(kotlin.Pair<? extends K,java.lang.Long>![] pairs);
+ method public void remove(K key);
+ method public boolean remove(K key, long value);
+ method public void removeIf(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public operator void set(K key, long value);
+ method public int trim();
+ }
+
public final class MutableScatterMap<K, V> extends androidx.collection.ScatterMap<K,V> {
ctor public MutableScatterMap(optional int initialCapacity);
method public java.util.Map<K,V> asMutableMap();
@@ -635,6 +1446,179 @@
method @IntRange(from=0L) public int trim();
}
+ public abstract sealed class ObjectFloatMap<K> {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final operator boolean contains(K key);
+ method public final boolean containsKey(K key);
+ method public final boolean containsValue(float value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+ method @kotlin.PublishedApi internal final int findKeyIndex(K key);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,kotlin.Unit> block);
+ method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super K,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+ method public final operator float get(K key);
+ method public final int getCapacity();
+ method public final float getOrDefault(K key, float defaultValue);
+ method public final inline float getOrElse(K key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ field @kotlin.PublishedApi internal Object![] keys;
+ field @kotlin.PublishedApi internal long[] metadata;
+ field @kotlin.PublishedApi internal float[] values;
+ }
+
+ public final class ObjectFloatMapKt {
+ method public static <K> androidx.collection.ObjectFloatMap<K> emptyObjectFloatMap();
+ method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf();
+ method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf(kotlin.Pair<? extends K,java.lang.Float>... pairs);
+ method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMap();
+ method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMapOf(kotlin.Pair<? extends K,java.lang.Float>... pairs);
+ }
+
+ public abstract sealed class ObjectIntMap<K> {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final operator boolean contains(K key);
+ method public final boolean containsKey(K key);
+ method public final boolean containsValue(int value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+ method @kotlin.PublishedApi internal final int findKeyIndex(K key);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,kotlin.Unit> block);
+ method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super K,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final operator int get(K key);
+ method public final int getCapacity();
+ method public final int getOrDefault(K key, int defaultValue);
+ method public final inline int getOrElse(K key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ field @kotlin.PublishedApi internal Object![] keys;
+ field @kotlin.PublishedApi internal long[] metadata;
+ field @kotlin.PublishedApi internal int[] values;
+ }
+
+ public final class ObjectIntMapKt {
+ method public static <K> androidx.collection.ObjectIntMap<K> emptyObjectIntMap();
+ method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf();
+ method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf(kotlin.Pair<? extends K,java.lang.Integer>... pairs);
+ method public static <K> androidx.collection.ObjectIntMap<K> objectIntMap();
+ method public static <K> androidx.collection.ObjectIntMap<K> objectIntMapOf(kotlin.Pair<? extends K,java.lang.Integer>... pairs);
+ }
+
+ public abstract sealed class ObjectList<E> {
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ method public abstract java.util.List<E> asList();
+ method public final operator boolean contains(E element);
+ method public final boolean containsAll(androidx.collection.ObjectList<E> elements);
+ method public final boolean containsAll(E![] elements);
+ method public final boolean containsAll(Iterable<? extends E> elements);
+ method public final boolean containsAll(java.util.List<? extends E> elements);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ method public final E elementAt(@IntRange(from=0L) int index);
+ method public final inline E elementAtOrElse(@IntRange(from=0L) int index, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends E> defaultValue);
+ method public final E first();
+ method public final inline E first(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ method public final inline E? firstOrNull();
+ method public final inline E? firstOrNull(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ method public final inline <R> R fold(R initial, kotlin.jvm.functions.Function2<? super R,? super E,? extends R> operation);
+ method public final inline <R> R foldIndexed(R initial, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super R,? super E,? extends R> operation);
+ method public final inline <R> R foldRight(R initial, kotlin.jvm.functions.Function2<? super E,? super R,? extends R> operation);
+ method public final inline <R> R foldRightIndexed(R initial, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super E,? super R,? extends R> operation);
+ method public final inline void forEach(kotlin.jvm.functions.Function1<? super E,kotlin.Unit> block);
+ method public final inline void forEachIndexed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super E,kotlin.Unit> block);
+ method public final inline void forEachReversed(kotlin.jvm.functions.Function1<? super E,kotlin.Unit> block);
+ method public final inline void forEachReversedIndexed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super E,kotlin.Unit> block);
+ method public final operator E get(@IntRange(from=0L) int index);
+ method public final inline kotlin.ranges.IntRange getIndices();
+ method @IntRange(from=-1L) public final inline int getLastIndex();
+ method @IntRange(from=0L) public final int getSize();
+ method public final int indexOf(E element);
+ method public final inline int indexOfFirst(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final E last();
+ method public final inline E last(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ method public final int lastIndexOf(E element);
+ method public final inline E? lastOrNull();
+ method public final inline E? lastOrNull(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ method public final boolean none();
+ method public final inline boolean reversedAny(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+ property public final inline kotlin.ranges.IntRange indices;
+ property @IntRange(from=-1L) public final inline int lastIndex;
+ property @IntRange(from=0L) public final int size;
+ field @kotlin.PublishedApi internal int _size;
+ field @kotlin.PublishedApi internal Object![] content;
+ }
+
+ public final class ObjectListKt {
+ method public static <E> androidx.collection.ObjectList<E> emptyObjectList();
+ method public static inline <E> androidx.collection.MutableObjectList<E> mutableObjectListOf();
+ method public static <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E element1);
+ method public static <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E element1, E element2);
+ method public static <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E element1, E element2, E element3);
+ method public static inline <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E?... elements);
+ method public static <E> androidx.collection.ObjectList<E> objectListOf();
+ method public static <E> androidx.collection.ObjectList<E> objectListOf(E element1);
+ method public static <E> androidx.collection.ObjectList<E> objectListOf(E element1, E element2);
+ method public static <E> androidx.collection.ObjectList<E> objectListOf(E element1, E element2, E element3);
+ method public static <E> androidx.collection.ObjectList<E> objectListOf(E?... elements);
+ }
+
+ public abstract sealed class ObjectLongMap<K> {
+ method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final boolean any();
+ method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final operator boolean contains(K key);
+ method public final boolean containsKey(K key);
+ method public final boolean containsValue(long value);
+ method public final int count();
+ method public final inline int count(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+ method @kotlin.PublishedApi internal final int findKeyIndex(K key);
+ method public final inline void forEach(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,kotlin.Unit> block);
+ method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+ method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super K,kotlin.Unit> block);
+ method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+ method public final operator long get(K key);
+ method public final int getCapacity();
+ method public final long getOrDefault(K key, long defaultValue);
+ method public final inline long getOrElse(K key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+ method public final int getSize();
+ method public final boolean isEmpty();
+ method public final boolean isNotEmpty();
+ method public final boolean none();
+ property public final int capacity;
+ property public final int size;
+ field @kotlin.PublishedApi internal Object![] keys;
+ field @kotlin.PublishedApi internal long[] metadata;
+ field @kotlin.PublishedApi internal long[] values;
+ }
+
+ public final class ObjectLongMapKt {
+ method public static <K> androidx.collection.ObjectLongMap<K> emptyObjectLongMap();
+ method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf();
+ method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf(kotlin.Pair<? extends K,java.lang.Long>... pairs);
+ method public static <K> androidx.collection.ObjectLongMap<K> objectLongMap();
+ method public static <K> androidx.collection.ObjectLongMap<K> objectLongMapOf(kotlin.Pair<? extends K,java.lang.Long>... pairs);
+ }
+
@kotlin.jvm.JvmInline public final value class PairFloatFloat {
ctor public PairFloatFloat(float first, float second);
method public inline operator float component1();
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt
new file mode 100644
index 0000000..bca375a
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyFloatFloatMap = MutableFloatFloatMap(0)
+
+/**
+ * Returns an empty, read-only [FloatFloatMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyFloatFloatMap(): FloatFloatMap = EmptyFloatFloatMap
+
+/**
+ * Returns a new [MutableFloatFloatMap].
+ */
+public fun floatFloatMapOf(): FloatFloatMap = EmptyFloatFloatMap
+
+/**
+ * Returns a new [FloatFloatMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Float] key and [Float] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun floatFloatMapOf(vararg pairs: Pair<Float, Float>): FloatFloatMap =
+ MutableFloatFloatMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * Returns a new [MutableFloatFloatMap].
+ */
+public fun mutableFloatFloatMapOf(): MutableFloatFloatMap = MutableFloatFloatMap()
+
+/**
+ * Returns a new [MutableFloatFloatMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Float] key and [Float] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutableFloatFloatMapOf(vararg pairs: Pair<Float, Float>): MutableFloatFloatMap =
+ MutableFloatFloatMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * [FloatFloatMap] is a container with a [Map]-like interface for
+ * [Float] primitive keys and [Float] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableFloatFloatMap].
+ *
+ * @see [MutableFloatFloatMap]
+ * @see [ScatterMap]
+ */
+public sealed class FloatFloatMap {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` entries, including when
+ // the table is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var keys: FloatArray = EmptyFloatArray
+
+ @PublishedApi
+ @JvmField
+ internal var values: FloatArray = EmptyFloatArray
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of key-value pairs that can be stored in this map
+ * without requiring internal storage reallocation.
+ */
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of key-value pairs in this map.
+ */
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this map has at least one entry.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this map has no entries.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this map is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this map is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the value corresponding to the given [key].
+ * @throws NoSuchElementException if [key] is not in the map
+ */
+ public operator fun get(key: Float): Float {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ throw NoSuchElementException("Cannot find value for key $key")
+ }
+ return values[index]
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * or [defaultValue] if this map contains no mapping for the key.
+ */
+ public fun getOrDefault(key: Float, defaultValue: Float): Float {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ return defaultValue
+ }
+
+ /**
+ * Returns the value for the given [key] if the value is present
+ * and not null. Otherwise, returns the result of the [defaultValue]
+ * function.
+ */
+ public inline fun getOrElse(key: Float, defaultValue: () -> Float): Float {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ return defaultValue()
+ }
+ return values[index]
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ public inline fun forEach(block: (key: Float, value: Float) -> Unit) {
+ val k = keys
+ val v = values
+
+ forEachIndexed { index ->
+ block(k[index], v[index])
+ }
+ }
+
+ /**
+ * Iterates over every key stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachKey(block: (key: Float) -> Unit) {
+ val k = keys
+
+ forEachIndexed { index ->
+ block(k[index])
+ }
+ }
+
+ /**
+ * Iterates over every value stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachValue(block: (value: Float) -> Unit) {
+ val v = values
+
+ forEachIndexed { index ->
+ block(v[index])
+ }
+ }
+
+ /**
+ * Returns true if all entries match the given [predicate].
+ */
+ public inline fun all(predicate: (Float, Float) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (!predicate(key, value)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one entry matches the given [predicate].
+ */
+ public inline fun any(predicate: (Float, Float) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (predicate(key, value)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of entries in this map.
+ */
+ public fun count(): Int = size
+
+ /**
+ * Returns the number of entries matching the given [predicate].
+ */
+ public inline fun count(predicate: (Float, Float) -> Boolean): Int {
+ var count = 0
+ forEach { key, value ->
+ if (predicate(key, value)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public operator fun contains(key: Float): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsKey(key: Float): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [value] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsValue(value: Float): Boolean {
+ forEachValue { v ->
+ if (value == v) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the hash code value for this map. The hash code the sum of the hash
+ * codes of each key/value pair.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { key, value ->
+ hash += key.hashCode() xor value.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this hash map for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [FloatFloatMap]
+ * - Has the same [size] as this map
+ * - Contains key/value pairs equal to this map's pair
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is FloatFloatMap) {
+ return false
+ }
+ if (other.size != size) {
+ return false
+ }
+
+ forEach { key, value ->
+ if (value != other[key]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this map. The map is denoted in the
+ * string by the `{}`. Each key/value pair present in the map is represented
+ * inside '{}` by a substring of the form `key=value`, and pairs are
+ * separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ val s = StringBuilder().append('{')
+ var i = 0
+ forEach { key, value ->
+ s.append(key)
+ s.append("=")
+ s.append(value)
+ i++
+ if (i < _size) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append('}').toString()
+ }
+
+ /**
+ * Scans the hash table to find the index in the backing arrays of the
+ * specified [key]. Returns -1 if the key is not present.
+ */
+ @PublishedApi
+ internal fun findKeyIndex(key: Float): Int {
+ val hash = hash(key)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutableFloatFloatMap] is a container with a [MutableMap]-like interface for
+ * [Float] primitive keys and [Float] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableFloatFloatMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableFloatFloatMap(
+ initialCapacity: Int = DefaultScatterCapacity
+) : FloatFloatMap() {
+ // Number of entries we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ keys = FloatArray(newCapacity)
+ values = FloatArray(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * if the value is present in the map and not `null`. Otherwise,
+ * calls `defaultValue()` and puts the result in the map associated
+ * with [key].
+ */
+ public inline fun getOrPut(key: Float, defaultValue: () -> Float): Float {
+ val index = findKeyIndex(key)
+ return if (index < 0) {
+ val defValue = defaultValue()
+ put(key, defValue)
+ defValue
+ } else {
+ values[index]
+ }
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public operator fun set(key: Float, value: Float) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations. Return the previous value associated with the [key],
+ * or `null` if the key was not present in the map.
+ */
+ public fun put(key: Float, value: Float) {
+ set(key, value)
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Float] key and [Float] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<Float, Float>>) {
+ for ((key, value) in pairs) {
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public fun putAll(from: FloatFloatMap) {
+ from.forEach { key, value ->
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts the key/value mapping from the [pair] in this map, using the first
+ * element as the key, and the second element as the value.
+ *
+ * Note that [pair] allocated and both the [Float] key and [Float] value are
+ * boxed. Use [set] instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(pair: Pair<Float, Float>) {
+ this[pair.first] = pair.second
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Float] key and [Float] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(
+ @Suppress("ArrayReturn") pairs: Array<Pair<Float, Float>>
+ ): Unit = putAll(pairs)
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public inline operator fun plusAssign(from: FloatFloatMap): Unit = putAll(from)
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public fun remove(key: Float) {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ removeValueAt(index)
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map if the
+ * associated value equals [value]. Returns whether the removal happened.
+ */
+ public fun remove(key: Float, value: Float): Boolean {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ if (values[index] == value) {
+ removeValueAt(index)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes any mapping for which the specified [predicate] returns true.
+ */
+ public fun removeIf(predicate: (Float, Float) -> Boolean) {
+ forEachIndexed { index ->
+ if (predicate(keys[index], values[index])) {
+ removeValueAt(index)
+ }
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public inline operator fun minusAssign(key: Float) {
+ remove(key)
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: FloatArray) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: FloatSet) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: FloatList) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ private fun removeValueAt(index: Int) {
+ _size -= 1
+
+ // TODO: We could just mark the entry as empty if there's a group
+ // window around this entry that was already empty
+ writeMetadata(index, Deleted)
+ }
+
+ /**
+ * Removes all mappings from this map.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the hash table to find the index at which we can store a value
+ * for the give [key]. If the key already exists in the table, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the table is full.
+ */
+ private fun findAbsoluteInsertIndex(key: Float): Int {
+ val hash = hash(key)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the table in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutableFloatFloatMap]'s storage so it is sized appropriately
+ * to hold the current mappings.
+ *
+ * Returns the number of empty entries removed from this map's storage.
+ * Returns be 0 if no trimming is necessary or possible.
+ */
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted entries from the table to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_set`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousKeys = keys
+ val previousValues = values
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newKeys = keys
+ val newValues = values
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousKey = previousKeys[i]
+ val hash = hash(previousKey)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newKeys[index] = previousKey
+ newValues[index] = previousValues[i]
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt
new file mode 100644
index 0000000..29adea3
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyFloatIntMap = MutableFloatIntMap(0)
+
+/**
+ * Returns an empty, read-only [FloatIntMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyFloatIntMap(): FloatIntMap = EmptyFloatIntMap
+
+/**
+ * Returns a new [MutableFloatIntMap].
+ */
+public fun floatIntMapOf(): FloatIntMap = EmptyFloatIntMap
+
+/**
+ * Returns a new [FloatIntMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Float] key and [Int] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun floatIntMapOf(vararg pairs: Pair<Float, Int>): FloatIntMap =
+ MutableFloatIntMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * Returns a new [MutableFloatIntMap].
+ */
+public fun mutableFloatIntMapOf(): MutableFloatIntMap = MutableFloatIntMap()
+
+/**
+ * Returns a new [MutableFloatIntMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Float] key and [Int] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutableFloatIntMapOf(vararg pairs: Pair<Float, Int>): MutableFloatIntMap =
+ MutableFloatIntMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * [FloatIntMap] is a container with a [Map]-like interface for
+ * [Float] primitive keys and [Int] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableFloatIntMap].
+ *
+ * @see [MutableFloatIntMap]
+ * @see [ScatterMap]
+ */
+public sealed class FloatIntMap {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` entries, including when
+ // the table is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var keys: FloatArray = EmptyFloatArray
+
+ @PublishedApi
+ @JvmField
+ internal var values: IntArray = EmptyIntArray
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of key-value pairs that can be stored in this map
+ * without requiring internal storage reallocation.
+ */
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of key-value pairs in this map.
+ */
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this map has at least one entry.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this map has no entries.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this map is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this map is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the value corresponding to the given [key].
+ * @throws NoSuchElementException if [key] is not in the map
+ */
+ public operator fun get(key: Float): Int {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ throw NoSuchElementException("Cannot find value for key $key")
+ }
+ return values[index]
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * or [defaultValue] if this map contains no mapping for the key.
+ */
+ public fun getOrDefault(key: Float, defaultValue: Int): Int {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ return defaultValue
+ }
+
+ /**
+ * Returns the value for the given [key] if the value is present
+ * and not null. Otherwise, returns the result of the [defaultValue]
+ * function.
+ */
+ public inline fun getOrElse(key: Float, defaultValue: () -> Int): Int {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ return defaultValue()
+ }
+ return values[index]
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ public inline fun forEach(block: (key: Float, value: Int) -> Unit) {
+ val k = keys
+ val v = values
+
+ forEachIndexed { index ->
+ block(k[index], v[index])
+ }
+ }
+
+ /**
+ * Iterates over every key stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachKey(block: (key: Float) -> Unit) {
+ val k = keys
+
+ forEachIndexed { index ->
+ block(k[index])
+ }
+ }
+
+ /**
+ * Iterates over every value stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachValue(block: (value: Int) -> Unit) {
+ val v = values
+
+ forEachIndexed { index ->
+ block(v[index])
+ }
+ }
+
+ /**
+ * Returns true if all entries match the given [predicate].
+ */
+ public inline fun all(predicate: (Float, Int) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (!predicate(key, value)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one entry matches the given [predicate].
+ */
+ public inline fun any(predicate: (Float, Int) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (predicate(key, value)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of entries in this map.
+ */
+ public fun count(): Int = size
+
+ /**
+ * Returns the number of entries matching the given [predicate].
+ */
+ public inline fun count(predicate: (Float, Int) -> Boolean): Int {
+ var count = 0
+ forEach { key, value ->
+ if (predicate(key, value)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public operator fun contains(key: Float): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsKey(key: Float): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [value] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsValue(value: Int): Boolean {
+ forEachValue { v ->
+ if (value == v) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the hash code value for this map. The hash code the sum of the hash
+ * codes of each key/value pair.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { key, value ->
+ hash += key.hashCode() xor value.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this hash map for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [FloatIntMap]
+ * - Has the same [size] as this map
+ * - Contains key/value pairs equal to this map's pair
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is FloatIntMap) {
+ return false
+ }
+ if (other.size != size) {
+ return false
+ }
+
+ forEach { key, value ->
+ if (value != other[key]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this map. The map is denoted in the
+ * string by the `{}`. Each key/value pair present in the map is represented
+ * inside '{}` by a substring of the form `key=value`, and pairs are
+ * separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ val s = StringBuilder().append('{')
+ var i = 0
+ forEach { key, value ->
+ s.append(key)
+ s.append("=")
+ s.append(value)
+ i++
+ if (i < _size) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append('}').toString()
+ }
+
+ /**
+ * Scans the hash table to find the index in the backing arrays of the
+ * specified [key]. Returns -1 if the key is not present.
+ */
+ @PublishedApi
+ internal fun findKeyIndex(key: Float): Int {
+ val hash = hash(key)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutableFloatIntMap] is a container with a [MutableMap]-like interface for
+ * [Float] primitive keys and [Int] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableFloatIntMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableFloatIntMap(
+ initialCapacity: Int = DefaultScatterCapacity
+) : FloatIntMap() {
+ // Number of entries we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ keys = FloatArray(newCapacity)
+ values = IntArray(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * if the value is present in the map and not `null`. Otherwise,
+ * calls `defaultValue()` and puts the result in the map associated
+ * with [key].
+ */
+ public inline fun getOrPut(key: Float, defaultValue: () -> Int): Int {
+ val index = findKeyIndex(key)
+ return if (index < 0) {
+ val defValue = defaultValue()
+ put(key, defValue)
+ defValue
+ } else {
+ values[index]
+ }
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public operator fun set(key: Float, value: Int) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations. Return the previous value associated with the [key],
+ * or `null` if the key was not present in the map.
+ */
+ public fun put(key: Float, value: Int) {
+ set(key, value)
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Float] key and [Int] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<Float, Int>>) {
+ for ((key, value) in pairs) {
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public fun putAll(from: FloatIntMap) {
+ from.forEach { key, value ->
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts the key/value mapping from the [pair] in this map, using the first
+ * element as the key, and the second element as the value.
+ *
+ * Note that [pair] allocated and both the [Float] key and [Int] value are
+ * boxed. Use [set] instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(pair: Pair<Float, Int>) {
+ this[pair.first] = pair.second
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Float] key and [Int] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(
+ @Suppress("ArrayReturn") pairs: Array<Pair<Float, Int>>
+ ): Unit = putAll(pairs)
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public inline operator fun plusAssign(from: FloatIntMap): Unit = putAll(from)
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public fun remove(key: Float) {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ removeValueAt(index)
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map if the
+ * associated value equals [value]. Returns whether the removal happened.
+ */
+ public fun remove(key: Float, value: Int): Boolean {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ if (values[index] == value) {
+ removeValueAt(index)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes any mapping for which the specified [predicate] returns true.
+ */
+ public fun removeIf(predicate: (Float, Int) -> Boolean) {
+ forEachIndexed { index ->
+ if (predicate(keys[index], values[index])) {
+ removeValueAt(index)
+ }
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public inline operator fun minusAssign(key: Float) {
+ remove(key)
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: FloatArray) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: FloatSet) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: FloatList) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ private fun removeValueAt(index: Int) {
+ _size -= 1
+
+ // TODO: We could just mark the entry as empty if there's a group
+ // window around this entry that was already empty
+ writeMetadata(index, Deleted)
+ }
+
+ /**
+ * Removes all mappings from this map.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the hash table to find the index at which we can store a value
+ * for the give [key]. If the key already exists in the table, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the table is full.
+ */
+ private fun findAbsoluteInsertIndex(key: Float): Int {
+ val hash = hash(key)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the table in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutableFloatIntMap]'s storage so it is sized appropriately
+ * to hold the current mappings.
+ *
+ * Returns the number of empty entries removed from this map's storage.
+ * Returns be 0 if no trimming is necessary or possible.
+ */
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted entries from the table to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_set`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousKeys = keys
+ val previousValues = values
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newKeys = keys
+ val newValues = values
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousKey = previousKeys[i]
+ val hash = hash(previousKey)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newKeys[index] = previousKey
+ newValues[index] = previousValues[i]
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt
index cf0c648..70b5787 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt
@@ -22,6 +22,14 @@
import kotlin.contracts.contract
import kotlin.jvm.JvmField
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
/**
* [FloatList] is a [List]-like collection for [Float] values. It allows retrieving
* the elements without boxing. [FloatList] is always backed by a [MutableFloatList],
@@ -835,10 +843,6 @@
}
}
-// Empty array used when nothing is allocated
-@Suppress("PrivatePropertyName")
-private val EmptyFloatArray = FloatArray(0)
-
private val EmptyFloatList: FloatList = MutableFloatList(0)
/**
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt
new file mode 100644
index 0000000..ac71a3e
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyFloatLongMap = MutableFloatLongMap(0)
+
+/**
+ * Returns an empty, read-only [FloatLongMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyFloatLongMap(): FloatLongMap = EmptyFloatLongMap
+
+/**
+ * Returns a new [MutableFloatLongMap].
+ */
+public fun floatLongMapOf(): FloatLongMap = EmptyFloatLongMap
+
+/**
+ * Returns a new [FloatLongMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Float] key and [Long] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun floatLongMapOf(vararg pairs: Pair<Float, Long>): FloatLongMap =
+ MutableFloatLongMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * Returns a new [MutableFloatLongMap].
+ */
+public fun mutableFloatLongMapOf(): MutableFloatLongMap = MutableFloatLongMap()
+
+/**
+ * Returns a new [MutableFloatLongMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Float] key and [Long] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutableFloatLongMapOf(vararg pairs: Pair<Float, Long>): MutableFloatLongMap =
+ MutableFloatLongMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * [FloatLongMap] is a container with a [Map]-like interface for
+ * [Float] primitive keys and [Long] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableFloatLongMap].
+ *
+ * @see [MutableFloatLongMap]
+ * @see [ScatterMap]
+ */
+public sealed class FloatLongMap {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` entries, including when
+ // the table is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var keys: FloatArray = EmptyFloatArray
+
+ @PublishedApi
+ @JvmField
+ internal var values: LongArray = EmptyLongArray
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of key-value pairs that can be stored in this map
+ * without requiring internal storage reallocation.
+ */
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of key-value pairs in this map.
+ */
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this map has at least one entry.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this map has no entries.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this map is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this map is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the value corresponding to the given [key].
+ * @throws NoSuchElementException if [key] is not in the map
+ */
+ public operator fun get(key: Float): Long {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ throw NoSuchElementException("Cannot find value for key $key")
+ }
+ return values[index]
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * or [defaultValue] if this map contains no mapping for the key.
+ */
+ public fun getOrDefault(key: Float, defaultValue: Long): Long {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ return defaultValue
+ }
+
+ /**
+ * Returns the value for the given [key] if the value is present
+ * and not null. Otherwise, returns the result of the [defaultValue]
+ * function.
+ */
+ public inline fun getOrElse(key: Float, defaultValue: () -> Long): Long {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ return defaultValue()
+ }
+ return values[index]
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ public inline fun forEach(block: (key: Float, value: Long) -> Unit) {
+ val k = keys
+ val v = values
+
+ forEachIndexed { index ->
+ block(k[index], v[index])
+ }
+ }
+
+ /**
+ * Iterates over every key stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachKey(block: (key: Float) -> Unit) {
+ val k = keys
+
+ forEachIndexed { index ->
+ block(k[index])
+ }
+ }
+
+ /**
+ * Iterates over every value stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachValue(block: (value: Long) -> Unit) {
+ val v = values
+
+ forEachIndexed { index ->
+ block(v[index])
+ }
+ }
+
+ /**
+ * Returns true if all entries match the given [predicate].
+ */
+ public inline fun all(predicate: (Float, Long) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (!predicate(key, value)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one entry matches the given [predicate].
+ */
+ public inline fun any(predicate: (Float, Long) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (predicate(key, value)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of entries in this map.
+ */
+ public fun count(): Int = size
+
+ /**
+ * Returns the number of entries matching the given [predicate].
+ */
+ public inline fun count(predicate: (Float, Long) -> Boolean): Int {
+ var count = 0
+ forEach { key, value ->
+ if (predicate(key, value)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public operator fun contains(key: Float): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsKey(key: Float): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [value] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsValue(value: Long): Boolean {
+ forEachValue { v ->
+ if (value == v) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the hash code value for this map. The hash code the sum of the hash
+ * codes of each key/value pair.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { key, value ->
+ hash += key.hashCode() xor value.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this hash map for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [FloatLongMap]
+ * - Has the same [size] as this map
+ * - Contains key/value pairs equal to this map's pair
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is FloatLongMap) {
+ return false
+ }
+ if (other.size != size) {
+ return false
+ }
+
+ forEach { key, value ->
+ if (value != other[key]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this map. The map is denoted in the
+ * string by the `{}`. Each key/value pair present in the map is represented
+ * inside '{}` by a substring of the form `key=value`, and pairs are
+ * separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ val s = StringBuilder().append('{')
+ var i = 0
+ forEach { key, value ->
+ s.append(key)
+ s.append("=")
+ s.append(value)
+ i++
+ if (i < _size) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append('}').toString()
+ }
+
+ /**
+ * Scans the hash table to find the index in the backing arrays of the
+ * specified [key]. Returns -1 if the key is not present.
+ */
+ @PublishedApi
+ internal fun findKeyIndex(key: Float): Int {
+ val hash = hash(key)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutableFloatLongMap] is a container with a [MutableMap]-like interface for
+ * [Float] primitive keys and [Long] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableFloatLongMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableFloatLongMap(
+ initialCapacity: Int = DefaultScatterCapacity
+) : FloatLongMap() {
+ // Number of entries we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ keys = FloatArray(newCapacity)
+ values = LongArray(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * if the value is present in the map and not `null`. Otherwise,
+ * calls `defaultValue()` and puts the result in the map associated
+ * with [key].
+ */
+ public inline fun getOrPut(key: Float, defaultValue: () -> Long): Long {
+ val index = findKeyIndex(key)
+ return if (index < 0) {
+ val defValue = defaultValue()
+ put(key, defValue)
+ defValue
+ } else {
+ values[index]
+ }
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public operator fun set(key: Float, value: Long) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations. Return the previous value associated with the [key],
+ * or `null` if the key was not present in the map.
+ */
+ public fun put(key: Float, value: Long) {
+ set(key, value)
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Float] key and [Long] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<Float, Long>>) {
+ for ((key, value) in pairs) {
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public fun putAll(from: FloatLongMap) {
+ from.forEach { key, value ->
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts the key/value mapping from the [pair] in this map, using the first
+ * element as the key, and the second element as the value.
+ *
+ * Note that [pair] allocated and both the [Float] key and [Long] value are
+ * boxed. Use [set] instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(pair: Pair<Float, Long>) {
+ this[pair.first] = pair.second
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Float] key and [Long] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(
+ @Suppress("ArrayReturn") pairs: Array<Pair<Float, Long>>
+ ): Unit = putAll(pairs)
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public inline operator fun plusAssign(from: FloatLongMap): Unit = putAll(from)
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public fun remove(key: Float) {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ removeValueAt(index)
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map if the
+ * associated value equals [value]. Returns whether the removal happened.
+ */
+ public fun remove(key: Float, value: Long): Boolean {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ if (values[index] == value) {
+ removeValueAt(index)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes any mapping for which the specified [predicate] returns true.
+ */
+ public fun removeIf(predicate: (Float, Long) -> Boolean) {
+ forEachIndexed { index ->
+ if (predicate(keys[index], values[index])) {
+ removeValueAt(index)
+ }
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public inline operator fun minusAssign(key: Float) {
+ remove(key)
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: FloatArray) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: FloatSet) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: FloatList) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ private fun removeValueAt(index: Int) {
+ _size -= 1
+
+ // TODO: We could just mark the entry as empty if there's a group
+ // window around this entry that was already empty
+ writeMetadata(index, Deleted)
+ }
+
+ /**
+ * Removes all mappings from this map.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the hash table to find the index at which we can store a value
+ * for the give [key]. If the key already exists in the table, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the table is full.
+ */
+ private fun findAbsoluteInsertIndex(key: Float): Int {
+ val hash = hash(key)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the table in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutableFloatLongMap]'s storage so it is sized appropriately
+ * to hold the current mappings.
+ *
+ * Returns the number of empty entries removed from this map's storage.
+ * Returns be 0 if no trimming is necessary or possible.
+ */
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted entries from the table to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_set`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousKeys = keys
+ val previousValues = values
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newKeys = keys
+ val newValues = values
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousKey = previousKeys[i]
+ val hash = hash(previousKey)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newKeys[index] = previousKey
+ newValues[index] = previousValues[i]
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt
new file mode 100644
index 0000000..292347f
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt
@@ -0,0 +1,847 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyFloatObjectMap = MutableFloatObjectMap<Nothing>(0)
+
+/**
+ * Returns an empty, read-only [FloatObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> emptyFloatObjectMap(): FloatObjectMap<V> = EmptyFloatObjectMap as FloatObjectMap<V>
+
+/**
+ * Returns an empty, read-only [FloatObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> floatObjectMapOf(): FloatObjectMap<V> = EmptyFloatObjectMap as FloatObjectMap<V>
+
+/**
+ * Returns a new [FloatObjectMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Float] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <V> floatObjectMapOf(vararg pairs: Pair<Float, V>): FloatObjectMap<V> =
+ MutableFloatObjectMap<V>(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * Returns a new [MutableFloatObjectMap].
+ */
+public fun <V> mutableFloatObjectMapOf(): MutableFloatObjectMap<V> = MutableFloatObjectMap()
+
+/**
+ * Returns a new [MutableFloatObjectMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Float] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <V> mutableFloatObjectMapOf(vararg pairs: Pair<Float, V>): MutableFloatObjectMap<V> =
+ MutableFloatObjectMap<V>(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * [FloatObjectMap] is a container with a [Map]-like interface for keys with
+ * [Float] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableFloatObjectMap].
+ *
+ * @see [MutableFloatObjectMap]
+ */
+public sealed class FloatObjectMap<V> {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` entries, including when
+ // the table is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var keys: FloatArray = EmptyFloatArray
+
+ @PublishedApi
+ @JvmField
+ internal var values: Array<Any?> = EMPTY_OBJECTS
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of key-value pairs that can be stored in this map
+ * without requiring internal storage reallocation.
+ */
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of key-value pairs in this map.
+ */
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this map has at least one entry.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this map has no entries.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this map is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this map is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the value corresponding to the given [key], or `null` if such
+ * a key is not present in the map.
+ */
+ public operator fun get(key: Float): V? {
+ val index = findKeyIndex(key)
+ @Suppress("UNCHECKED_CAST")
+ return if (index >= 0) values[index] as V? else null
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * or [defaultValue] if this map contains no mapping for the key.
+ */
+ public fun getOrDefault(key: Float, defaultValue: V): V {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ @Suppress("UNCHECKED_CAST")
+ return values[index] as V
+ }
+ return defaultValue
+ }
+
+ /**
+ * Returns the value for the given [key] if the value is present
+ * and not null. Otherwise, returns the result of the [defaultValue]
+ * function.
+ */
+ public inline fun getOrElse(key: Float, defaultValue: () -> V): V {
+ return get(key) ?: defaultValue()
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ public inline fun forEach(block: (key: Float, value: V) -> Unit) {
+ val k = keys
+ val v = values
+
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ block(k[index], v[index] as V)
+ }
+ }
+
+ /**
+ * Iterates over every key stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachKey(block: (key: Float) -> Unit) {
+ val k = keys
+
+ forEachIndexed { index ->
+ block(k[index])
+ }
+ }
+
+ /**
+ * Iterates over every value stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachValue(block: (value: V) -> Unit) {
+ val v = values
+
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ block(v[index] as V)
+ }
+ }
+
+ /**
+ * Returns true if all entries match the given [predicate].
+ */
+ public inline fun all(predicate: (Float, V) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (!predicate(key, value)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one entry matches the given [predicate].
+ */
+ public inline fun any(predicate: (Float, V) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (predicate(key, value)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of entries in this map.
+ */
+ public fun count(): Int = size
+
+ /**
+ * Returns the number of entries matching the given [predicate].
+ */
+ public inline fun count(predicate: (Float, V) -> Boolean): Int {
+ var count = 0
+ forEach { key, value ->
+ if (predicate(key, value)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public operator fun contains(key: Float): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsKey(key: Float): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [value] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsValue(value: V): Boolean {
+ forEachValue { v ->
+ if (value == v) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the hash code value for this map. The hash code the sum of the hash
+ * codes of each key/value pair.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { key, value ->
+ hash += key.hashCode() xor value.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this hash map for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [FloatObjectMap]
+ * - Has the same [size] as this map
+ * - Contains key/value pairs equal to this map's pair
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is FloatObjectMap<*>) {
+ return false
+ }
+ if (other.size != size) {
+ return false
+ }
+
+ forEach { key, value ->
+ if (value == null) {
+ if (other[key] != null || !other.containsKey(key)) {
+ return false
+ }
+ } else if (value != other[key]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this map. The map is denoted in the
+ * string by the `{}`. Each key/value pair present in the map is represented
+ * inside '{}` by a substring of the form `key=value`, and pairs are
+ * separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ val s = StringBuilder().append('{')
+ var i = 0
+ forEach { key, value ->
+ s.append(key)
+ s.append("=")
+ s.append(if (value === this) "(this)" else value)
+ i++
+ if (i < _size) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append('}').toString()
+ }
+
+ /**
+ * Scans the hash table to find the index in the backing arrays of the
+ * specified [key]. Returns -1 if the key is not present.
+ */
+ internal inline fun findKeyIndex(key: Float): Int {
+ val hash = hash(key)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutableFloatObjectMap] is a container with a [MutableMap]-like interface for keys with
+ * [Float] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableFloatObjectMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see ScatterMap
+ */
+public class MutableFloatObjectMap<V>(
+ initialCapacity: Int = DefaultScatterCapacity
+) : FloatObjectMap<V>() {
+ // Number of entries we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ keys = FloatArray(newCapacity)
+ values = arrayOfNulls(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * if the value is present in the map and not `null`. Otherwise,
+ * calls `defaultValue()` and puts the result in the map associated
+ * with [key].
+ */
+ public inline fun getOrPut(key: Float, defaultValue: () -> V): V {
+ return get(key) ?: defaultValue().also { set(key, it) }
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public operator fun set(key: Float, value: V) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations. Return the previous value associated with the [key],
+ * or `null` if the key was not present in the map.
+ */
+ public fun put(key: Float, value: V): V? {
+ val index = findAbsoluteInsertIndex(key)
+ val oldValue = values[index]
+ keys[index] = key
+ values[index] = value
+
+ @Suppress("UNCHECKED_CAST")
+ return oldValue as V?
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Float] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+ public fun putAll(@Suppress("ArrayReturn") pairs: Array<out Pair<Float, V>>) {
+ for ((key, value) in pairs) {
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public fun putAll(from: FloatObjectMap<V>) {
+ from.forEach { key, value ->
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts the key/value mapping from the [pair] in this map, using the first
+ * element as the key, and the second element as the value.
+ *
+ * Note that the [Pair] is allocated and the [Float] key is boxed.
+ * Use [set] instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(pair: Pair<Float, V>) {
+ this[pair.first] = pair.second
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Float] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+ public inline operator fun plusAssign(
+ @Suppress("ArrayReturn") pairs: Array<out Pair<Float, V>>
+ ): Unit = putAll(pairs)
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public inline operator fun plusAssign(from: FloatObjectMap<V>): Unit = putAll(from)
+
+ /**
+ * Removes the specified [key] and its associated value from the map. If the
+ * [key] was present in the map, this function returns the value that was
+ * present before removal.
+ */
+ public fun remove(key: Float): V? {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return removeValueAt(index)
+ }
+ return null
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map if the
+ * associated value equals [value]. Returns whether the removal happened.
+ */
+ public fun remove(key: Float, value: V): Boolean {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ if (values[index] == value) {
+ removeValueAt(index)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes any mapping for which the specified [predicate] returns true.
+ */
+ public fun removeIf(predicate: (Float, V) -> Boolean) {
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ if (predicate(keys[index], values[index] as V)) {
+ removeValueAt(index)
+ }
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public inline operator fun minusAssign(key: Float) {
+ remove(key)
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: FloatArray) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: FloatSet) {
+ keys.forEach { key ->
+ minusAssign(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: FloatList) {
+ keys.forEach { key ->
+ minusAssign(key)
+ }
+ }
+
+ private fun removeValueAt(index: Int): V? {
+ _size -= 1
+
+ // TODO: We could just mark the entry as empty if there's a group
+ // window around this entry that was already empty
+ writeMetadata(index, Deleted)
+ val oldValue = values[index]
+ values[index] = null
+
+ @Suppress("UNCHECKED_CAST")
+ return oldValue as V?
+ }
+
+ /**
+ * Removes all mappings from this map.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ values.fill(null, 0, _capacity)
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the hash table to find the index at which we can store a value
+ * for the give [key]. If the key already exists in the table, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the table is full.
+ */
+ private fun findAbsoluteInsertIndex(key: Float): Int {
+ val hash = hash(key)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the table in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutableFloatObjectMap]'s storage so it is sized appropriately
+ * to hold the current mappings.
+ *
+ * Returns the number of empty entries removed from this map's storage.
+ * Returns be 0 if no trimming is necessary or possible.
+ */
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted entries from the table to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_set`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousKeys = keys
+ val previousValues = values
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newKeys = keys
+ val newValues = values
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousKey = previousKeys[i]
+ val hash = hash(previousKey)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newKeys[index] = previousKey
+ newValues[index] = previousValues[i]
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
index 278902c4..f08a176 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
@@ -29,13 +29,21 @@
import kotlin.contracts.contract
import kotlin.jvm.JvmField
-// This is a copy of ScatterSet, but with Float elements
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// This is a copy of ScatterSet, but with primitive elements
// Default empty set to avoid allocations
private val EmptyFloatSet = MutableFloatSet(0)
// An empty array of floats
-private val EmptyFloatArray = FloatArray(0)
+internal val EmptyFloatArray = FloatArray(0)
/**
* Returns an empty, read-only [FloatSet].
@@ -770,7 +778,7 @@
* Returns the hash code of [k]. This follows the [HashSet] default behavior on Android
* of returning [Object.hashcode()] with the higher bits of hash spread to the lower bits.
*/
-private inline fun hash(k: Float): Int {
+internal inline fun hash(k: Float): Int {
val hash = k.hashCode()
return hash xor (hash ushr 16)
}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt
new file mode 100644
index 0000000..b9151ff7
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyIntFloatMap = MutableIntFloatMap(0)
+
+/**
+ * Returns an empty, read-only [IntFloatMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyIntFloatMap(): IntFloatMap = EmptyIntFloatMap
+
+/**
+ * Returns a new [MutableIntFloatMap].
+ */
+public fun intFloatMapOf(): IntFloatMap = EmptyIntFloatMap
+
+/**
+ * Returns a new [IntFloatMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Int] key and [Float] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun intFloatMapOf(vararg pairs: Pair<Int, Float>): IntFloatMap =
+ MutableIntFloatMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * Returns a new [MutableIntFloatMap].
+ */
+public fun mutableIntFloatMapOf(): MutableIntFloatMap = MutableIntFloatMap()
+
+/**
+ * Returns a new [MutableIntFloatMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Int] key and [Float] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutableIntFloatMapOf(vararg pairs: Pair<Int, Float>): MutableIntFloatMap =
+ MutableIntFloatMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * [IntFloatMap] is a container with a [Map]-like interface for
+ * [Int] primitive keys and [Float] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableIntFloatMap].
+ *
+ * @see [MutableIntFloatMap]
+ * @see [ScatterMap]
+ */
+public sealed class IntFloatMap {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` entries, including when
+ // the table is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var keys: IntArray = EmptyIntArray
+
+ @PublishedApi
+ @JvmField
+ internal var values: FloatArray = EmptyFloatArray
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of key-value pairs that can be stored in this map
+ * without requiring internal storage reallocation.
+ */
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of key-value pairs in this map.
+ */
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this map has at least one entry.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this map has no entries.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this map is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this map is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the value corresponding to the given [key].
+ * @throws NoSuchElementException if [key] is not in the map
+ */
+ public operator fun get(key: Int): Float {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ throw NoSuchElementException("Cannot find value for key $key")
+ }
+ return values[index]
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * or [defaultValue] if this map contains no mapping for the key.
+ */
+ public fun getOrDefault(key: Int, defaultValue: Float): Float {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ return defaultValue
+ }
+
+ /**
+ * Returns the value for the given [key] if the value is present
+ * and not null. Otherwise, returns the result of the [defaultValue]
+ * function.
+ */
+ public inline fun getOrElse(key: Int, defaultValue: () -> Float): Float {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ return defaultValue()
+ }
+ return values[index]
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ public inline fun forEach(block: (key: Int, value: Float) -> Unit) {
+ val k = keys
+ val v = values
+
+ forEachIndexed { index ->
+ block(k[index], v[index])
+ }
+ }
+
+ /**
+ * Iterates over every key stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachKey(block: (key: Int) -> Unit) {
+ val k = keys
+
+ forEachIndexed { index ->
+ block(k[index])
+ }
+ }
+
+ /**
+ * Iterates over every value stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachValue(block: (value: Float) -> Unit) {
+ val v = values
+
+ forEachIndexed { index ->
+ block(v[index])
+ }
+ }
+
+ /**
+ * Returns true if all entries match the given [predicate].
+ */
+ public inline fun all(predicate: (Int, Float) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (!predicate(key, value)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one entry matches the given [predicate].
+ */
+ public inline fun any(predicate: (Int, Float) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (predicate(key, value)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of entries in this map.
+ */
+ public fun count(): Int = size
+
+ /**
+ * Returns the number of entries matching the given [predicate].
+ */
+ public inline fun count(predicate: (Int, Float) -> Boolean): Int {
+ var count = 0
+ forEach { key, value ->
+ if (predicate(key, value)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public operator fun contains(key: Int): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsKey(key: Int): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [value] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsValue(value: Float): Boolean {
+ forEachValue { v ->
+ if (value == v) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the hash code value for this map. The hash code the sum of the hash
+ * codes of each key/value pair.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { key, value ->
+ hash += key.hashCode() xor value.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this hash map for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [IntFloatMap]
+ * - Has the same [size] as this map
+ * - Contains key/value pairs equal to this map's pair
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is IntFloatMap) {
+ return false
+ }
+ if (other.size != size) {
+ return false
+ }
+
+ forEach { key, value ->
+ if (value != other[key]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this map. The map is denoted in the
+ * string by the `{}`. Each key/value pair present in the map is represented
+ * inside '{}` by a substring of the form `key=value`, and pairs are
+ * separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ val s = StringBuilder().append('{')
+ var i = 0
+ forEach { key, value ->
+ s.append(key)
+ s.append("=")
+ s.append(value)
+ i++
+ if (i < _size) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append('}').toString()
+ }
+
+ /**
+ * Scans the hash table to find the index in the backing arrays of the
+ * specified [key]. Returns -1 if the key is not present.
+ */
+ @PublishedApi
+ internal fun findKeyIndex(key: Int): Int {
+ val hash = hash(key)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutableIntFloatMap] is a container with a [MutableMap]-like interface for
+ * [Int] primitive keys and [Float] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableIntFloatMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableIntFloatMap(
+ initialCapacity: Int = DefaultScatterCapacity
+) : IntFloatMap() {
+ // Number of entries we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ keys = IntArray(newCapacity)
+ values = FloatArray(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * if the value is present in the map and not `null`. Otherwise,
+ * calls `defaultValue()` and puts the result in the map associated
+ * with [key].
+ */
+ public inline fun getOrPut(key: Int, defaultValue: () -> Float): Float {
+ val index = findKeyIndex(key)
+ return if (index < 0) {
+ val defValue = defaultValue()
+ put(key, defValue)
+ defValue
+ } else {
+ values[index]
+ }
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public operator fun set(key: Int, value: Float) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations. Return the previous value associated with the [key],
+ * or `null` if the key was not present in the map.
+ */
+ public fun put(key: Int, value: Float) {
+ set(key, value)
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Int] key and [Float] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<Int, Float>>) {
+ for ((key, value) in pairs) {
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public fun putAll(from: IntFloatMap) {
+ from.forEach { key, value ->
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts the key/value mapping from the [pair] in this map, using the first
+ * element as the key, and the second element as the value.
+ *
+ * Note that [pair] allocated and both the [Int] key and [Float] value are
+ * boxed. Use [set] instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(pair: Pair<Int, Float>) {
+ this[pair.first] = pair.second
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Int] key and [Float] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(
+ @Suppress("ArrayReturn") pairs: Array<Pair<Int, Float>>
+ ): Unit = putAll(pairs)
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public inline operator fun plusAssign(from: IntFloatMap): Unit = putAll(from)
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public fun remove(key: Int) {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ removeValueAt(index)
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map if the
+ * associated value equals [value]. Returns whether the removal happened.
+ */
+ public fun remove(key: Int, value: Float): Boolean {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ if (values[index] == value) {
+ removeValueAt(index)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes any mapping for which the specified [predicate] returns true.
+ */
+ public fun removeIf(predicate: (Int, Float) -> Boolean) {
+ forEachIndexed { index ->
+ if (predicate(keys[index], values[index])) {
+ removeValueAt(index)
+ }
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public inline operator fun minusAssign(key: Int) {
+ remove(key)
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: IntArray) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: IntSet) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: IntList) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ private fun removeValueAt(index: Int) {
+ _size -= 1
+
+ // TODO: We could just mark the entry as empty if there's a group
+ // window around this entry that was already empty
+ writeMetadata(index, Deleted)
+ }
+
+ /**
+ * Removes all mappings from this map.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the hash table to find the index at which we can store a value
+ * for the give [key]. If the key already exists in the table, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the table is full.
+ */
+ private fun findAbsoluteInsertIndex(key: Int): Int {
+ val hash = hash(key)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the table in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutableIntFloatMap]'s storage so it is sized appropriately
+ * to hold the current mappings.
+ *
+ * Returns the number of empty entries removed from this map's storage.
+ * Returns be 0 if no trimming is necessary or possible.
+ */
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted entries from the table to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_set`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousKeys = keys
+ val previousValues = values
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newKeys = keys
+ val newValues = values
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousKey = previousKeys[i]
+ val hash = hash(previousKey)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newKeys[index] = previousKey
+ newValues[index] = previousValues[i]
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt
new file mode 100644
index 0000000..5d288b2
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyIntIntMap = MutableIntIntMap(0)
+
+/**
+ * Returns an empty, read-only [IntIntMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyIntIntMap(): IntIntMap = EmptyIntIntMap
+
+/**
+ * Returns a new [MutableIntIntMap].
+ */
+public fun intIntMapOf(): IntIntMap = EmptyIntIntMap
+
+/**
+ * Returns a new [IntIntMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Int] key and [Int] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun intIntMapOf(vararg pairs: Pair<Int, Int>): IntIntMap =
+ MutableIntIntMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * Returns a new [MutableIntIntMap].
+ */
+public fun mutableIntIntMapOf(): MutableIntIntMap = MutableIntIntMap()
+
+/**
+ * Returns a new [MutableIntIntMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Int] key and [Int] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutableIntIntMapOf(vararg pairs: Pair<Int, Int>): MutableIntIntMap =
+ MutableIntIntMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * [IntIntMap] is a container with a [Map]-like interface for
+ * [Int] primitive keys and [Int] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableIntIntMap].
+ *
+ * @see [MutableIntIntMap]
+ * @see [ScatterMap]
+ */
+public sealed class IntIntMap {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` entries, including when
+ // the table is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var keys: IntArray = EmptyIntArray
+
+ @PublishedApi
+ @JvmField
+ internal var values: IntArray = EmptyIntArray
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of key-value pairs that can be stored in this map
+ * without requiring internal storage reallocation.
+ */
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of key-value pairs in this map.
+ */
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this map has at least one entry.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this map has no entries.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this map is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this map is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the value corresponding to the given [key].
+ * @throws NoSuchElementException if [key] is not in the map
+ */
+ public operator fun get(key: Int): Int {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ throw NoSuchElementException("Cannot find value for key $key")
+ }
+ return values[index]
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * or [defaultValue] if this map contains no mapping for the key.
+ */
+ public fun getOrDefault(key: Int, defaultValue: Int): Int {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ return defaultValue
+ }
+
+ /**
+ * Returns the value for the given [key] if the value is present
+ * and not null. Otherwise, returns the result of the [defaultValue]
+ * function.
+ */
+ public inline fun getOrElse(key: Int, defaultValue: () -> Int): Int {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ return defaultValue()
+ }
+ return values[index]
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ public inline fun forEach(block: (key: Int, value: Int) -> Unit) {
+ val k = keys
+ val v = values
+
+ forEachIndexed { index ->
+ block(k[index], v[index])
+ }
+ }
+
+ /**
+ * Iterates over every key stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachKey(block: (key: Int) -> Unit) {
+ val k = keys
+
+ forEachIndexed { index ->
+ block(k[index])
+ }
+ }
+
+ /**
+ * Iterates over every value stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachValue(block: (value: Int) -> Unit) {
+ val v = values
+
+ forEachIndexed { index ->
+ block(v[index])
+ }
+ }
+
+ /**
+ * Returns true if all entries match the given [predicate].
+ */
+ public inline fun all(predicate: (Int, Int) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (!predicate(key, value)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one entry matches the given [predicate].
+ */
+ public inline fun any(predicate: (Int, Int) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (predicate(key, value)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of entries in this map.
+ */
+ public fun count(): Int = size
+
+ /**
+ * Returns the number of entries matching the given [predicate].
+ */
+ public inline fun count(predicate: (Int, Int) -> Boolean): Int {
+ var count = 0
+ forEach { key, value ->
+ if (predicate(key, value)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public operator fun contains(key: Int): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsKey(key: Int): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [value] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsValue(value: Int): Boolean {
+ forEachValue { v ->
+ if (value == v) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the hash code value for this map. The hash code the sum of the hash
+ * codes of each key/value pair.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { key, value ->
+ hash += key.hashCode() xor value.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this hash map for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [IntIntMap]
+ * - Has the same [size] as this map
+ * - Contains key/value pairs equal to this map's pair
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is IntIntMap) {
+ return false
+ }
+ if (other.size != size) {
+ return false
+ }
+
+ forEach { key, value ->
+ if (value != other[key]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this map. The map is denoted in the
+ * string by the `{}`. Each key/value pair present in the map is represented
+ * inside '{}` by a substring of the form `key=value`, and pairs are
+ * separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ val s = StringBuilder().append('{')
+ var i = 0
+ forEach { key, value ->
+ s.append(key)
+ s.append("=")
+ s.append(value)
+ i++
+ if (i < _size) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append('}').toString()
+ }
+
+ /**
+ * Scans the hash table to find the index in the backing arrays of the
+ * specified [key]. Returns -1 if the key is not present.
+ */
+ @PublishedApi
+ internal fun findKeyIndex(key: Int): Int {
+ val hash = hash(key)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutableIntIntMap] is a container with a [MutableMap]-like interface for
+ * [Int] primitive keys and [Int] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableIntIntMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableIntIntMap(
+ initialCapacity: Int = DefaultScatterCapacity
+) : IntIntMap() {
+ // Number of entries we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ keys = IntArray(newCapacity)
+ values = IntArray(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * if the value is present in the map and not `null`. Otherwise,
+ * calls `defaultValue()` and puts the result in the map associated
+ * with [key].
+ */
+ public inline fun getOrPut(key: Int, defaultValue: () -> Int): Int {
+ val index = findKeyIndex(key)
+ return if (index < 0) {
+ val defValue = defaultValue()
+ put(key, defValue)
+ defValue
+ } else {
+ values[index]
+ }
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public operator fun set(key: Int, value: Int) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations. Return the previous value associated with the [key],
+ * or `null` if the key was not present in the map.
+ */
+ public fun put(key: Int, value: Int) {
+ set(key, value)
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Int] key and [Int] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<Int, Int>>) {
+ for ((key, value) in pairs) {
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public fun putAll(from: IntIntMap) {
+ from.forEach { key, value ->
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts the key/value mapping from the [pair] in this map, using the first
+ * element as the key, and the second element as the value.
+ *
+ * Note that [pair] allocated and both the [Int] key and [Int] value are
+ * boxed. Use [set] instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(pair: Pair<Int, Int>) {
+ this[pair.first] = pair.second
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Int] key and [Int] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(
+ @Suppress("ArrayReturn") pairs: Array<Pair<Int, Int>>
+ ): Unit = putAll(pairs)
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public inline operator fun plusAssign(from: IntIntMap): Unit = putAll(from)
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public fun remove(key: Int) {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ removeValueAt(index)
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map if the
+ * associated value equals [value]. Returns whether the removal happened.
+ */
+ public fun remove(key: Int, value: Int): Boolean {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ if (values[index] == value) {
+ removeValueAt(index)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes any mapping for which the specified [predicate] returns true.
+ */
+ public fun removeIf(predicate: (Int, Int) -> Boolean) {
+ forEachIndexed { index ->
+ if (predicate(keys[index], values[index])) {
+ removeValueAt(index)
+ }
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public inline operator fun minusAssign(key: Int) {
+ remove(key)
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: IntArray) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: IntSet) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: IntList) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ private fun removeValueAt(index: Int) {
+ _size -= 1
+
+ // TODO: We could just mark the entry as empty if there's a group
+ // window around this entry that was already empty
+ writeMetadata(index, Deleted)
+ }
+
+ /**
+ * Removes all mappings from this map.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the hash table to find the index at which we can store a value
+ * for the give [key]. If the key already exists in the table, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the table is full.
+ */
+ private fun findAbsoluteInsertIndex(key: Int): Int {
+ val hash = hash(key)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the table in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutableIntIntMap]'s storage so it is sized appropriately
+ * to hold the current mappings.
+ *
+ * Returns the number of empty entries removed from this map's storage.
+ * Returns be 0 if no trimming is necessary or possible.
+ */
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted entries from the table to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_set`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousKeys = keys
+ val previousValues = values
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newKeys = keys
+ val newValues = values
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousKey = previousKeys[i]
+ val hash = hash(previousKey)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newKeys[index] = previousKey
+ newValues[index] = previousValues[i]
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt
index 8c6122db..4260def 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt
@@ -22,6 +22,14 @@
import kotlin.contracts.contract
import kotlin.jvm.JvmField
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
/**
* [IntList] is a [List]-like collection for [Int] values. It allows retrieving
* the elements without boxing. [IntList] is always backed by a [MutableIntList],
@@ -835,10 +843,6 @@
}
}
-// Empty array used when nothing is allocated
-@Suppress("PrivatePropertyName")
-private val EmptyIntArray = IntArray(0)
-
private val EmptyIntList: IntList = MutableIntList(0)
/**
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt
new file mode 100644
index 0000000..ce4455a
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyIntLongMap = MutableIntLongMap(0)
+
+/**
+ * Returns an empty, read-only [IntLongMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyIntLongMap(): IntLongMap = EmptyIntLongMap
+
+/**
+ * Returns a new [MutableIntLongMap].
+ */
+public fun intLongMapOf(): IntLongMap = EmptyIntLongMap
+
+/**
+ * Returns a new [IntLongMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Int] key and [Long] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun intLongMapOf(vararg pairs: Pair<Int, Long>): IntLongMap =
+ MutableIntLongMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * Returns a new [MutableIntLongMap].
+ */
+public fun mutableIntLongMapOf(): MutableIntLongMap = MutableIntLongMap()
+
+/**
+ * Returns a new [MutableIntLongMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Int] key and [Long] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutableIntLongMapOf(vararg pairs: Pair<Int, Long>): MutableIntLongMap =
+ MutableIntLongMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * [IntLongMap] is a container with a [Map]-like interface for
+ * [Int] primitive keys and [Long] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableIntLongMap].
+ *
+ * @see [MutableIntLongMap]
+ * @see [ScatterMap]
+ */
+public sealed class IntLongMap {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` entries, including when
+ // the table is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var keys: IntArray = EmptyIntArray
+
+ @PublishedApi
+ @JvmField
+ internal var values: LongArray = EmptyLongArray
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of key-value pairs that can be stored in this map
+ * without requiring internal storage reallocation.
+ */
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of key-value pairs in this map.
+ */
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this map has at least one entry.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this map has no entries.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this map is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this map is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the value corresponding to the given [key].
+ * @throws NoSuchElementException if [key] is not in the map
+ */
+ public operator fun get(key: Int): Long {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ throw NoSuchElementException("Cannot find value for key $key")
+ }
+ return values[index]
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * or [defaultValue] if this map contains no mapping for the key.
+ */
+ public fun getOrDefault(key: Int, defaultValue: Long): Long {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ return defaultValue
+ }
+
+ /**
+ * Returns the value for the given [key] if the value is present
+ * and not null. Otherwise, returns the result of the [defaultValue]
+ * function.
+ */
+ public inline fun getOrElse(key: Int, defaultValue: () -> Long): Long {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ return defaultValue()
+ }
+ return values[index]
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ public inline fun forEach(block: (key: Int, value: Long) -> Unit) {
+ val k = keys
+ val v = values
+
+ forEachIndexed { index ->
+ block(k[index], v[index])
+ }
+ }
+
+ /**
+ * Iterates over every key stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachKey(block: (key: Int) -> Unit) {
+ val k = keys
+
+ forEachIndexed { index ->
+ block(k[index])
+ }
+ }
+
+ /**
+ * Iterates over every value stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachValue(block: (value: Long) -> Unit) {
+ val v = values
+
+ forEachIndexed { index ->
+ block(v[index])
+ }
+ }
+
+ /**
+ * Returns true if all entries match the given [predicate].
+ */
+ public inline fun all(predicate: (Int, Long) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (!predicate(key, value)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one entry matches the given [predicate].
+ */
+ public inline fun any(predicate: (Int, Long) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (predicate(key, value)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of entries in this map.
+ */
+ public fun count(): Int = size
+
+ /**
+ * Returns the number of entries matching the given [predicate].
+ */
+ public inline fun count(predicate: (Int, Long) -> Boolean): Int {
+ var count = 0
+ forEach { key, value ->
+ if (predicate(key, value)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public operator fun contains(key: Int): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsKey(key: Int): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [value] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsValue(value: Long): Boolean {
+ forEachValue { v ->
+ if (value == v) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the hash code value for this map. The hash code the sum of the hash
+ * codes of each key/value pair.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { key, value ->
+ hash += key.hashCode() xor value.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this hash map for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [IntLongMap]
+ * - Has the same [size] as this map
+ * - Contains key/value pairs equal to this map's pair
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is IntLongMap) {
+ return false
+ }
+ if (other.size != size) {
+ return false
+ }
+
+ forEach { key, value ->
+ if (value != other[key]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this map. The map is denoted in the
+ * string by the `{}`. Each key/value pair present in the map is represented
+ * inside '{}` by a substring of the form `key=value`, and pairs are
+ * separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ val s = StringBuilder().append('{')
+ var i = 0
+ forEach { key, value ->
+ s.append(key)
+ s.append("=")
+ s.append(value)
+ i++
+ if (i < _size) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append('}').toString()
+ }
+
+ /**
+ * Scans the hash table to find the index in the backing arrays of the
+ * specified [key]. Returns -1 if the key is not present.
+ */
+ @PublishedApi
+ internal fun findKeyIndex(key: Int): Int {
+ val hash = hash(key)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutableIntLongMap] is a container with a [MutableMap]-like interface for
+ * [Int] primitive keys and [Long] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableIntLongMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableIntLongMap(
+ initialCapacity: Int = DefaultScatterCapacity
+) : IntLongMap() {
+ // Number of entries we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ keys = IntArray(newCapacity)
+ values = LongArray(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * if the value is present in the map and not `null`. Otherwise,
+ * calls `defaultValue()` and puts the result in the map associated
+ * with [key].
+ */
+ public inline fun getOrPut(key: Int, defaultValue: () -> Long): Long {
+ val index = findKeyIndex(key)
+ return if (index < 0) {
+ val defValue = defaultValue()
+ put(key, defValue)
+ defValue
+ } else {
+ values[index]
+ }
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public operator fun set(key: Int, value: Long) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations. Return the previous value associated with the [key],
+ * or `null` if the key was not present in the map.
+ */
+ public fun put(key: Int, value: Long) {
+ set(key, value)
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Int] key and [Long] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<Int, Long>>) {
+ for ((key, value) in pairs) {
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public fun putAll(from: IntLongMap) {
+ from.forEach { key, value ->
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts the key/value mapping from the [pair] in this map, using the first
+ * element as the key, and the second element as the value.
+ *
+ * Note that [pair] allocated and both the [Int] key and [Long] value are
+ * boxed. Use [set] instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(pair: Pair<Int, Long>) {
+ this[pair.first] = pair.second
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Int] key and [Long] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(
+ @Suppress("ArrayReturn") pairs: Array<Pair<Int, Long>>
+ ): Unit = putAll(pairs)
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public inline operator fun plusAssign(from: IntLongMap): Unit = putAll(from)
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public fun remove(key: Int) {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ removeValueAt(index)
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map if the
+ * associated value equals [value]. Returns whether the removal happened.
+ */
+ public fun remove(key: Int, value: Long): Boolean {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ if (values[index] == value) {
+ removeValueAt(index)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes any mapping for which the specified [predicate] returns true.
+ */
+ public fun removeIf(predicate: (Int, Long) -> Boolean) {
+ forEachIndexed { index ->
+ if (predicate(keys[index], values[index])) {
+ removeValueAt(index)
+ }
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public inline operator fun minusAssign(key: Int) {
+ remove(key)
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: IntArray) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: IntSet) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: IntList) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ private fun removeValueAt(index: Int) {
+ _size -= 1
+
+ // TODO: We could just mark the entry as empty if there's a group
+ // window around this entry that was already empty
+ writeMetadata(index, Deleted)
+ }
+
+ /**
+ * Removes all mappings from this map.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the hash table to find the index at which we can store a value
+ * for the give [key]. If the key already exists in the table, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the table is full.
+ */
+ private fun findAbsoluteInsertIndex(key: Int): Int {
+ val hash = hash(key)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the table in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutableIntLongMap]'s storage so it is sized appropriately
+ * to hold the current mappings.
+ *
+ * Returns the number of empty entries removed from this map's storage.
+ * Returns be 0 if no trimming is necessary or possible.
+ */
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted entries from the table to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_set`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousKeys = keys
+ val previousValues = values
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newKeys = keys
+ val newValues = values
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousKey = previousKeys[i]
+ val hash = hash(previousKey)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newKeys[index] = previousKey
+ newValues[index] = previousValues[i]
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt
new file mode 100644
index 0000000..1626d89
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt
@@ -0,0 +1,847 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyIntObjectMap = MutableIntObjectMap<Nothing>(0)
+
+/**
+ * Returns an empty, read-only [IntObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> emptyIntObjectMap(): IntObjectMap<V> = EmptyIntObjectMap as IntObjectMap<V>
+
+/**
+ * Returns an empty, read-only [IntObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> intObjectMapOf(): IntObjectMap<V> = EmptyIntObjectMap as IntObjectMap<V>
+
+/**
+ * Returns a new [IntObjectMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Int] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <V> intObjectMapOf(vararg pairs: Pair<Int, V>): IntObjectMap<V> =
+ MutableIntObjectMap<V>(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * Returns a new [MutableIntObjectMap].
+ */
+public fun <V> mutableIntObjectMapOf(): MutableIntObjectMap<V> = MutableIntObjectMap()
+
+/**
+ * Returns a new [MutableIntObjectMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Int] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <V> mutableIntObjectMapOf(vararg pairs: Pair<Int, V>): MutableIntObjectMap<V> =
+ MutableIntObjectMap<V>(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * [IntObjectMap] is a container with a [Map]-like interface for keys with
+ * [Int] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableIntObjectMap].
+ *
+ * @see [MutableIntObjectMap]
+ */
+public sealed class IntObjectMap<V> {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` entries, including when
+ // the table is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var keys: IntArray = EmptyIntArray
+
+ @PublishedApi
+ @JvmField
+ internal var values: Array<Any?> = EMPTY_OBJECTS
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of key-value pairs that can be stored in this map
+ * without requiring internal storage reallocation.
+ */
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of key-value pairs in this map.
+ */
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this map has at least one entry.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this map has no entries.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this map is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this map is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the value corresponding to the given [key], or `null` if such
+ * a key is not present in the map.
+ */
+ public operator fun get(key: Int): V? {
+ val index = findKeyIndex(key)
+ @Suppress("UNCHECKED_CAST")
+ return if (index >= 0) values[index] as V? else null
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * or [defaultValue] if this map contains no mapping for the key.
+ */
+ public fun getOrDefault(key: Int, defaultValue: V): V {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ @Suppress("UNCHECKED_CAST")
+ return values[index] as V
+ }
+ return defaultValue
+ }
+
+ /**
+ * Returns the value for the given [key] if the value is present
+ * and not null. Otherwise, returns the result of the [defaultValue]
+ * function.
+ */
+ public inline fun getOrElse(key: Int, defaultValue: () -> V): V {
+ return get(key) ?: defaultValue()
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ public inline fun forEach(block: (key: Int, value: V) -> Unit) {
+ val k = keys
+ val v = values
+
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ block(k[index], v[index] as V)
+ }
+ }
+
+ /**
+ * Iterates over every key stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachKey(block: (key: Int) -> Unit) {
+ val k = keys
+
+ forEachIndexed { index ->
+ block(k[index])
+ }
+ }
+
+ /**
+ * Iterates over every value stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachValue(block: (value: V) -> Unit) {
+ val v = values
+
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ block(v[index] as V)
+ }
+ }
+
+ /**
+ * Returns true if all entries match the given [predicate].
+ */
+ public inline fun all(predicate: (Int, V) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (!predicate(key, value)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one entry matches the given [predicate].
+ */
+ public inline fun any(predicate: (Int, V) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (predicate(key, value)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of entries in this map.
+ */
+ public fun count(): Int = size
+
+ /**
+ * Returns the number of entries matching the given [predicate].
+ */
+ public inline fun count(predicate: (Int, V) -> Boolean): Int {
+ var count = 0
+ forEach { key, value ->
+ if (predicate(key, value)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public operator fun contains(key: Int): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsKey(key: Int): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [value] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsValue(value: V): Boolean {
+ forEachValue { v ->
+ if (value == v) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the hash code value for this map. The hash code the sum of the hash
+ * codes of each key/value pair.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { key, value ->
+ hash += key.hashCode() xor value.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this hash map for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [IntObjectMap]
+ * - Has the same [size] as this map
+ * - Contains key/value pairs equal to this map's pair
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is IntObjectMap<*>) {
+ return false
+ }
+ if (other.size != size) {
+ return false
+ }
+
+ forEach { key, value ->
+ if (value == null) {
+ if (other[key] != null || !other.containsKey(key)) {
+ return false
+ }
+ } else if (value != other[key]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this map. The map is denoted in the
+ * string by the `{}`. Each key/value pair present in the map is represented
+ * inside '{}` by a substring of the form `key=value`, and pairs are
+ * separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ val s = StringBuilder().append('{')
+ var i = 0
+ forEach { key, value ->
+ s.append(key)
+ s.append("=")
+ s.append(if (value === this) "(this)" else value)
+ i++
+ if (i < _size) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append('}').toString()
+ }
+
+ /**
+ * Scans the hash table to find the index in the backing arrays of the
+ * specified [key]. Returns -1 if the key is not present.
+ */
+ internal inline fun findKeyIndex(key: Int): Int {
+ val hash = hash(key)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutableIntObjectMap] is a container with a [MutableMap]-like interface for keys with
+ * [Int] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableIntObjectMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see ScatterMap
+ */
+public class MutableIntObjectMap<V>(
+ initialCapacity: Int = DefaultScatterCapacity
+) : IntObjectMap<V>() {
+ // Number of entries we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ keys = IntArray(newCapacity)
+ values = arrayOfNulls(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * if the value is present in the map and not `null`. Otherwise,
+ * calls `defaultValue()` and puts the result in the map associated
+ * with [key].
+ */
+ public inline fun getOrPut(key: Int, defaultValue: () -> V): V {
+ return get(key) ?: defaultValue().also { set(key, it) }
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public operator fun set(key: Int, value: V) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations. Return the previous value associated with the [key],
+ * or `null` if the key was not present in the map.
+ */
+ public fun put(key: Int, value: V): V? {
+ val index = findAbsoluteInsertIndex(key)
+ val oldValue = values[index]
+ keys[index] = key
+ values[index] = value
+
+ @Suppress("UNCHECKED_CAST")
+ return oldValue as V?
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Int] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+ public fun putAll(@Suppress("ArrayReturn") pairs: Array<out Pair<Int, V>>) {
+ for ((key, value) in pairs) {
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public fun putAll(from: IntObjectMap<V>) {
+ from.forEach { key, value ->
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts the key/value mapping from the [pair] in this map, using the first
+ * element as the key, and the second element as the value.
+ *
+ * Note that the [Pair] is allocated and the [Int] key is boxed.
+ * Use [set] instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(pair: Pair<Int, V>) {
+ this[pair.first] = pair.second
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Int] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+ public inline operator fun plusAssign(
+ @Suppress("ArrayReturn") pairs: Array<out Pair<Int, V>>
+ ): Unit = putAll(pairs)
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public inline operator fun plusAssign(from: IntObjectMap<V>): Unit = putAll(from)
+
+ /**
+ * Removes the specified [key] and its associated value from the map. If the
+ * [key] was present in the map, this function returns the value that was
+ * present before removal.
+ */
+ public fun remove(key: Int): V? {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return removeValueAt(index)
+ }
+ return null
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map if the
+ * associated value equals [value]. Returns whether the removal happened.
+ */
+ public fun remove(key: Int, value: V): Boolean {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ if (values[index] == value) {
+ removeValueAt(index)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes any mapping for which the specified [predicate] returns true.
+ */
+ public fun removeIf(predicate: (Int, V) -> Boolean) {
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ if (predicate(keys[index], values[index] as V)) {
+ removeValueAt(index)
+ }
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public inline operator fun minusAssign(key: Int) {
+ remove(key)
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: IntArray) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: IntSet) {
+ keys.forEach { key ->
+ minusAssign(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: IntList) {
+ keys.forEach { key ->
+ minusAssign(key)
+ }
+ }
+
+ private fun removeValueAt(index: Int): V? {
+ _size -= 1
+
+ // TODO: We could just mark the entry as empty if there's a group
+ // window around this entry that was already empty
+ writeMetadata(index, Deleted)
+ val oldValue = values[index]
+ values[index] = null
+
+ @Suppress("UNCHECKED_CAST")
+ return oldValue as V?
+ }
+
+ /**
+ * Removes all mappings from this map.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ values.fill(null, 0, _capacity)
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the hash table to find the index at which we can store a value
+ * for the give [key]. If the key already exists in the table, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the table is full.
+ */
+ private fun findAbsoluteInsertIndex(key: Int): Int {
+ val hash = hash(key)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the table in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutableIntObjectMap]'s storage so it is sized appropriately
+ * to hold the current mappings.
+ *
+ * Returns the number of empty entries removed from this map's storage.
+ * Returns be 0 if no trimming is necessary or possible.
+ */
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted entries from the table to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_set`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousKeys = keys
+ val previousValues = values
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newKeys = keys
+ val newValues = values
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousKey = previousKeys[i]
+ val hash = hash(previousKey)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newKeys[index] = previousKey
+ newValues[index] = previousValues[i]
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
index 2db0f7f..282c94d 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
@@ -29,13 +29,21 @@
import kotlin.contracts.contract
import kotlin.jvm.JvmField
-// This is a copy of ScatterSet, but with Int elements
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// This is a copy of ScatterSet, but with primitive elements
// Default empty set to avoid allocations
private val EmptyIntSet = MutableIntSet(0)
// An empty array of ints
-private val EmptyIntArray = IntArray(0)
+internal val EmptyIntArray = IntArray(0)
/**
* Returns an empty, read-only [IntSet].
@@ -770,7 +778,7 @@
* Returns the hash code of [k]. This follows the [HashSet] default behavior on Android
* of returning [Object.hashcode()] with the higher bits of hash spread to the lower bits.
*/
-private inline fun hash(k: Int): Int {
+internal inline fun hash(k: Int): Int {
val hash = k.hashCode()
return hash xor (hash ushr 16)
}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt
new file mode 100644
index 0000000..797b7fb
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyLongFloatMap = MutableLongFloatMap(0)
+
+/**
+ * Returns an empty, read-only [LongFloatMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyLongFloatMap(): LongFloatMap = EmptyLongFloatMap
+
+/**
+ * Returns a new [MutableLongFloatMap].
+ */
+public fun longFloatMapOf(): LongFloatMap = EmptyLongFloatMap
+
+/**
+ * Returns a new [LongFloatMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Long] key and [Float] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun longFloatMapOf(vararg pairs: Pair<Long, Float>): LongFloatMap =
+ MutableLongFloatMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * Returns a new [MutableLongFloatMap].
+ */
+public fun mutableLongFloatMapOf(): MutableLongFloatMap = MutableLongFloatMap()
+
+/**
+ * Returns a new [MutableLongFloatMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Long] key and [Float] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutableLongFloatMapOf(vararg pairs: Pair<Long, Float>): MutableLongFloatMap =
+ MutableLongFloatMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * [LongFloatMap] is a container with a [Map]-like interface for
+ * [Long] primitive keys and [Float] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableLongFloatMap].
+ *
+ * @see [MutableLongFloatMap]
+ * @see [ScatterMap]
+ */
+public sealed class LongFloatMap {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` entries, including when
+ // the table is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var keys: LongArray = EmptyLongArray
+
+ @PublishedApi
+ @JvmField
+ internal var values: FloatArray = EmptyFloatArray
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of key-value pairs that can be stored in this map
+ * without requiring internal storage reallocation.
+ */
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of key-value pairs in this map.
+ */
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this map has at least one entry.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this map has no entries.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this map is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this map is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the value corresponding to the given [key].
+ * @throws NoSuchElementException if [key] is not in the map
+ */
+ public operator fun get(key: Long): Float {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ throw NoSuchElementException("Cannot find value for key $key")
+ }
+ return values[index]
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * or [defaultValue] if this map contains no mapping for the key.
+ */
+ public fun getOrDefault(key: Long, defaultValue: Float): Float {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ return defaultValue
+ }
+
+ /**
+ * Returns the value for the given [key] if the value is present
+ * and not null. Otherwise, returns the result of the [defaultValue]
+ * function.
+ */
+ public inline fun getOrElse(key: Long, defaultValue: () -> Float): Float {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ return defaultValue()
+ }
+ return values[index]
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ public inline fun forEach(block: (key: Long, value: Float) -> Unit) {
+ val k = keys
+ val v = values
+
+ forEachIndexed { index ->
+ block(k[index], v[index])
+ }
+ }
+
+ /**
+ * Iterates over every key stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachKey(block: (key: Long) -> Unit) {
+ val k = keys
+
+ forEachIndexed { index ->
+ block(k[index])
+ }
+ }
+
+ /**
+ * Iterates over every value stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachValue(block: (value: Float) -> Unit) {
+ val v = values
+
+ forEachIndexed { index ->
+ block(v[index])
+ }
+ }
+
+ /**
+ * Returns true if all entries match the given [predicate].
+ */
+ public inline fun all(predicate: (Long, Float) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (!predicate(key, value)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one entry matches the given [predicate].
+ */
+ public inline fun any(predicate: (Long, Float) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (predicate(key, value)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of entries in this map.
+ */
+ public fun count(): Int = size
+
+ /**
+ * Returns the number of entries matching the given [predicate].
+ */
+ public inline fun count(predicate: (Long, Float) -> Boolean): Int {
+ var count = 0
+ forEach { key, value ->
+ if (predicate(key, value)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public operator fun contains(key: Long): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsKey(key: Long): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [value] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsValue(value: Float): Boolean {
+ forEachValue { v ->
+ if (value == v) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the hash code value for this map. The hash code the sum of the hash
+ * codes of each key/value pair.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { key, value ->
+ hash += key.hashCode() xor value.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this hash map for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [LongFloatMap]
+ * - Has the same [size] as this map
+ * - Contains key/value pairs equal to this map's pair
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is LongFloatMap) {
+ return false
+ }
+ if (other.size != size) {
+ return false
+ }
+
+ forEach { key, value ->
+ if (value != other[key]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this map. The map is denoted in the
+ * string by the `{}`. Each key/value pair present in the map is represented
+ * inside '{}` by a substring of the form `key=value`, and pairs are
+ * separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ val s = StringBuilder().append('{')
+ var i = 0
+ forEach { key, value ->
+ s.append(key)
+ s.append("=")
+ s.append(value)
+ i++
+ if (i < _size) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append('}').toString()
+ }
+
+ /**
+ * Scans the hash table to find the index in the backing arrays of the
+ * specified [key]. Returns -1 if the key is not present.
+ */
+ @PublishedApi
+ internal fun findKeyIndex(key: Long): Int {
+ val hash = hash(key)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutableLongFloatMap] is a container with a [MutableMap]-like interface for
+ * [Long] primitive keys and [Float] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableLongFloatMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableLongFloatMap(
+ initialCapacity: Int = DefaultScatterCapacity
+) : LongFloatMap() {
+ // Number of entries we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ keys = LongArray(newCapacity)
+ values = FloatArray(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * if the value is present in the map and not `null`. Otherwise,
+ * calls `defaultValue()` and puts the result in the map associated
+ * with [key].
+ */
+ public inline fun getOrPut(key: Long, defaultValue: () -> Float): Float {
+ val index = findKeyIndex(key)
+ return if (index < 0) {
+ val defValue = defaultValue()
+ put(key, defValue)
+ defValue
+ } else {
+ values[index]
+ }
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public operator fun set(key: Long, value: Float) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations. Return the previous value associated with the [key],
+ * or `null` if the key was not present in the map.
+ */
+ public fun put(key: Long, value: Float) {
+ set(key, value)
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Long] key and [Float] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<Long, Float>>) {
+ for ((key, value) in pairs) {
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public fun putAll(from: LongFloatMap) {
+ from.forEach { key, value ->
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts the key/value mapping from the [pair] in this map, using the first
+ * element as the key, and the second element as the value.
+ *
+ * Note that [pair] allocated and both the [Long] key and [Float] value are
+ * boxed. Use [set] instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(pair: Pair<Long, Float>) {
+ this[pair.first] = pair.second
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Long] key and [Float] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(
+ @Suppress("ArrayReturn") pairs: Array<Pair<Long, Float>>
+ ): Unit = putAll(pairs)
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public inline operator fun plusAssign(from: LongFloatMap): Unit = putAll(from)
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public fun remove(key: Long) {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ removeValueAt(index)
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map if the
+ * associated value equals [value]. Returns whether the removal happened.
+ */
+ public fun remove(key: Long, value: Float): Boolean {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ if (values[index] == value) {
+ removeValueAt(index)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes any mapping for which the specified [predicate] returns true.
+ */
+ public fun removeIf(predicate: (Long, Float) -> Boolean) {
+ forEachIndexed { index ->
+ if (predicate(keys[index], values[index])) {
+ removeValueAt(index)
+ }
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public inline operator fun minusAssign(key: Long) {
+ remove(key)
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: LongArray) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: LongSet) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: LongList) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ private fun removeValueAt(index: Int) {
+ _size -= 1
+
+ // TODO: We could just mark the entry as empty if there's a group
+ // window around this entry that was already empty
+ writeMetadata(index, Deleted)
+ }
+
+ /**
+ * Removes all mappings from this map.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the hash table to find the index at which we can store a value
+ * for the give [key]. If the key already exists in the table, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the table is full.
+ */
+ private fun findAbsoluteInsertIndex(key: Long): Int {
+ val hash = hash(key)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the table in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutableLongFloatMap]'s storage so it is sized appropriately
+ * to hold the current mappings.
+ *
+ * Returns the number of empty entries removed from this map's storage.
+ * Returns be 0 if no trimming is necessary or possible.
+ */
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted entries from the table to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_set`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousKeys = keys
+ val previousValues = values
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newKeys = keys
+ val newValues = values
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousKey = previousKeys[i]
+ val hash = hash(previousKey)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newKeys[index] = previousKey
+ newValues[index] = previousValues[i]
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt
new file mode 100644
index 0000000..c9c5ef6
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyLongIntMap = MutableLongIntMap(0)
+
+/**
+ * Returns an empty, read-only [LongIntMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyLongIntMap(): LongIntMap = EmptyLongIntMap
+
+/**
+ * Returns a new [MutableLongIntMap].
+ */
+public fun longIntMapOf(): LongIntMap = EmptyLongIntMap
+
+/**
+ * Returns a new [LongIntMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Long] key and [Int] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun longIntMapOf(vararg pairs: Pair<Long, Int>): LongIntMap =
+ MutableLongIntMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * Returns a new [MutableLongIntMap].
+ */
+public fun mutableLongIntMapOf(): MutableLongIntMap = MutableLongIntMap()
+
+/**
+ * Returns a new [MutableLongIntMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Long] key and [Int] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutableLongIntMapOf(vararg pairs: Pair<Long, Int>): MutableLongIntMap =
+ MutableLongIntMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * [LongIntMap] is a container with a [Map]-like interface for
+ * [Long] primitive keys and [Int] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableLongIntMap].
+ *
+ * @see [MutableLongIntMap]
+ * @see [ScatterMap]
+ */
+public sealed class LongIntMap {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` entries, including when
+ // the table is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var keys: LongArray = EmptyLongArray
+
+ @PublishedApi
+ @JvmField
+ internal var values: IntArray = EmptyIntArray
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of key-value pairs that can be stored in this map
+ * without requiring internal storage reallocation.
+ */
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of key-value pairs in this map.
+ */
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this map has at least one entry.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this map has no entries.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this map is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this map is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the value corresponding to the given [key].
+ * @throws NoSuchElementException if [key] is not in the map
+ */
+ public operator fun get(key: Long): Int {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ throw NoSuchElementException("Cannot find value for key $key")
+ }
+ return values[index]
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * or [defaultValue] if this map contains no mapping for the key.
+ */
+ public fun getOrDefault(key: Long, defaultValue: Int): Int {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ return defaultValue
+ }
+
+ /**
+ * Returns the value for the given [key] if the value is present
+ * and not null. Otherwise, returns the result of the [defaultValue]
+ * function.
+ */
+ public inline fun getOrElse(key: Long, defaultValue: () -> Int): Int {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ return defaultValue()
+ }
+ return values[index]
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ public inline fun forEach(block: (key: Long, value: Int) -> Unit) {
+ val k = keys
+ val v = values
+
+ forEachIndexed { index ->
+ block(k[index], v[index])
+ }
+ }
+
+ /**
+ * Iterates over every key stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachKey(block: (key: Long) -> Unit) {
+ val k = keys
+
+ forEachIndexed { index ->
+ block(k[index])
+ }
+ }
+
+ /**
+ * Iterates over every value stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachValue(block: (value: Int) -> Unit) {
+ val v = values
+
+ forEachIndexed { index ->
+ block(v[index])
+ }
+ }
+
+ /**
+ * Returns true if all entries match the given [predicate].
+ */
+ public inline fun all(predicate: (Long, Int) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (!predicate(key, value)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one entry matches the given [predicate].
+ */
+ public inline fun any(predicate: (Long, Int) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (predicate(key, value)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of entries in this map.
+ */
+ public fun count(): Int = size
+
+ /**
+ * Returns the number of entries matching the given [predicate].
+ */
+ public inline fun count(predicate: (Long, Int) -> Boolean): Int {
+ var count = 0
+ forEach { key, value ->
+ if (predicate(key, value)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public operator fun contains(key: Long): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsKey(key: Long): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [value] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsValue(value: Int): Boolean {
+ forEachValue { v ->
+ if (value == v) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the hash code value for this map. The hash code the sum of the hash
+ * codes of each key/value pair.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { key, value ->
+ hash += key.hashCode() xor value.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this hash map for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [LongIntMap]
+ * - Has the same [size] as this map
+ * - Contains key/value pairs equal to this map's pair
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is LongIntMap) {
+ return false
+ }
+ if (other.size != size) {
+ return false
+ }
+
+ forEach { key, value ->
+ if (value != other[key]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this map. The map is denoted in the
+ * string by the `{}`. Each key/value pair present in the map is represented
+ * inside '{}` by a substring of the form `key=value`, and pairs are
+ * separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ val s = StringBuilder().append('{')
+ var i = 0
+ forEach { key, value ->
+ s.append(key)
+ s.append("=")
+ s.append(value)
+ i++
+ if (i < _size) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append('}').toString()
+ }
+
+ /**
+ * Scans the hash table to find the index in the backing arrays of the
+ * specified [key]. Returns -1 if the key is not present.
+ */
+ @PublishedApi
+ internal fun findKeyIndex(key: Long): Int {
+ val hash = hash(key)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutableLongIntMap] is a container with a [MutableMap]-like interface for
+ * [Long] primitive keys and [Int] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableLongIntMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableLongIntMap(
+ initialCapacity: Int = DefaultScatterCapacity
+) : LongIntMap() {
+ // Number of entries we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ keys = LongArray(newCapacity)
+ values = IntArray(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * if the value is present in the map and not `null`. Otherwise,
+ * calls `defaultValue()` and puts the result in the map associated
+ * with [key].
+ */
+ public inline fun getOrPut(key: Long, defaultValue: () -> Int): Int {
+ val index = findKeyIndex(key)
+ return if (index < 0) {
+ val defValue = defaultValue()
+ put(key, defValue)
+ defValue
+ } else {
+ values[index]
+ }
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public operator fun set(key: Long, value: Int) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations. Return the previous value associated with the [key],
+ * or `null` if the key was not present in the map.
+ */
+ public fun put(key: Long, value: Int) {
+ set(key, value)
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Long] key and [Int] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<Long, Int>>) {
+ for ((key, value) in pairs) {
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public fun putAll(from: LongIntMap) {
+ from.forEach { key, value ->
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts the key/value mapping from the [pair] in this map, using the first
+ * element as the key, and the second element as the value.
+ *
+ * Note that [pair] allocated and both the [Long] key and [Int] value are
+ * boxed. Use [set] instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(pair: Pair<Long, Int>) {
+ this[pair.first] = pair.second
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Long] key and [Int] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(
+ @Suppress("ArrayReturn") pairs: Array<Pair<Long, Int>>
+ ): Unit = putAll(pairs)
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public inline operator fun plusAssign(from: LongIntMap): Unit = putAll(from)
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public fun remove(key: Long) {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ removeValueAt(index)
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map if the
+ * associated value equals [value]. Returns whether the removal happened.
+ */
+ public fun remove(key: Long, value: Int): Boolean {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ if (values[index] == value) {
+ removeValueAt(index)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes any mapping for which the specified [predicate] returns true.
+ */
+ public fun removeIf(predicate: (Long, Int) -> Boolean) {
+ forEachIndexed { index ->
+ if (predicate(keys[index], values[index])) {
+ removeValueAt(index)
+ }
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public inline operator fun minusAssign(key: Long) {
+ remove(key)
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: LongArray) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: LongSet) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: LongList) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ private fun removeValueAt(index: Int) {
+ _size -= 1
+
+ // TODO: We could just mark the entry as empty if there's a group
+ // window around this entry that was already empty
+ writeMetadata(index, Deleted)
+ }
+
+ /**
+ * Removes all mappings from this map.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the hash table to find the index at which we can store a value
+ * for the give [key]. If the key already exists in the table, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the table is full.
+ */
+ private fun findAbsoluteInsertIndex(key: Long): Int {
+ val hash = hash(key)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the table in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutableLongIntMap]'s storage so it is sized appropriately
+ * to hold the current mappings.
+ *
+ * Returns the number of empty entries removed from this map's storage.
+ * Returns be 0 if no trimming is necessary or possible.
+ */
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted entries from the table to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_set`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousKeys = keys
+ val previousValues = values
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newKeys = keys
+ val newValues = values
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousKey = previousKeys[i]
+ val hash = hash(previousKey)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newKeys[index] = previousKey
+ newValues[index] = previousValues[i]
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt
index 94dfd82..4f40b5b 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt
@@ -22,6 +22,14 @@
import kotlin.contracts.contract
import kotlin.jvm.JvmField
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
/**
* [LongList] is a [List]-like collection for [Long] values. It allows retrieving
* the elements without boxing. [LongList] is always backed by a [MutableLongList],
@@ -835,10 +843,6 @@
}
}
-// Empty array used when nothing is allocated
-@Suppress("PrivatePropertyName")
-private val EmptyLongArray = LongArray(0)
-
private val EmptyLongList: LongList = MutableLongList(0)
/**
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt
new file mode 100644
index 0000000..e61a7a0
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyLongLongMap = MutableLongLongMap(0)
+
+/**
+ * Returns an empty, read-only [LongLongMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyLongLongMap(): LongLongMap = EmptyLongLongMap
+
+/**
+ * Returns a new [MutableLongLongMap].
+ */
+public fun longLongMapOf(): LongLongMap = EmptyLongLongMap
+
+/**
+ * Returns a new [LongLongMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Long] key and [Long] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun longLongMapOf(vararg pairs: Pair<Long, Long>): LongLongMap =
+ MutableLongLongMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * Returns a new [MutableLongLongMap].
+ */
+public fun mutableLongLongMapOf(): MutableLongLongMap = MutableLongLongMap()
+
+/**
+ * Returns a new [MutableLongLongMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Long] key and [Long] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutableLongLongMapOf(vararg pairs: Pair<Long, Long>): MutableLongLongMap =
+ MutableLongLongMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * [LongLongMap] is a container with a [Map]-like interface for
+ * [Long] primitive keys and [Long] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableLongLongMap].
+ *
+ * @see [MutableLongLongMap]
+ * @see [ScatterMap]
+ */
+public sealed class LongLongMap {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` entries, including when
+ // the table is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var keys: LongArray = EmptyLongArray
+
+ @PublishedApi
+ @JvmField
+ internal var values: LongArray = EmptyLongArray
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of key-value pairs that can be stored in this map
+ * without requiring internal storage reallocation.
+ */
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of key-value pairs in this map.
+ */
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this map has at least one entry.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this map has no entries.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this map is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this map is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the value corresponding to the given [key].
+ * @throws NoSuchElementException if [key] is not in the map
+ */
+ public operator fun get(key: Long): Long {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ throw NoSuchElementException("Cannot find value for key $key")
+ }
+ return values[index]
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * or [defaultValue] if this map contains no mapping for the key.
+ */
+ public fun getOrDefault(key: Long, defaultValue: Long): Long {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ return defaultValue
+ }
+
+ /**
+ * Returns the value for the given [key] if the value is present
+ * and not null. Otherwise, returns the result of the [defaultValue]
+ * function.
+ */
+ public inline fun getOrElse(key: Long, defaultValue: () -> Long): Long {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ return defaultValue()
+ }
+ return values[index]
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ public inline fun forEach(block: (key: Long, value: Long) -> Unit) {
+ val k = keys
+ val v = values
+
+ forEachIndexed { index ->
+ block(k[index], v[index])
+ }
+ }
+
+ /**
+ * Iterates over every key stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachKey(block: (key: Long) -> Unit) {
+ val k = keys
+
+ forEachIndexed { index ->
+ block(k[index])
+ }
+ }
+
+ /**
+ * Iterates over every value stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachValue(block: (value: Long) -> Unit) {
+ val v = values
+
+ forEachIndexed { index ->
+ block(v[index])
+ }
+ }
+
+ /**
+ * Returns true if all entries match the given [predicate].
+ */
+ public inline fun all(predicate: (Long, Long) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (!predicate(key, value)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one entry matches the given [predicate].
+ */
+ public inline fun any(predicate: (Long, Long) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (predicate(key, value)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of entries in this map.
+ */
+ public fun count(): Int = size
+
+ /**
+ * Returns the number of entries matching the given [predicate].
+ */
+ public inline fun count(predicate: (Long, Long) -> Boolean): Int {
+ var count = 0
+ forEach { key, value ->
+ if (predicate(key, value)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public operator fun contains(key: Long): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsKey(key: Long): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [value] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsValue(value: Long): Boolean {
+ forEachValue { v ->
+ if (value == v) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the hash code value for this map. The hash code the sum of the hash
+ * codes of each key/value pair.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { key, value ->
+ hash += key.hashCode() xor value.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this hash map for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [LongLongMap]
+ * - Has the same [size] as this map
+ * - Contains key/value pairs equal to this map's pair
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is LongLongMap) {
+ return false
+ }
+ if (other.size != size) {
+ return false
+ }
+
+ forEach { key, value ->
+ if (value != other[key]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this map. The map is denoted in the
+ * string by the `{}`. Each key/value pair present in the map is represented
+ * inside '{}` by a substring of the form `key=value`, and pairs are
+ * separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ val s = StringBuilder().append('{')
+ var i = 0
+ forEach { key, value ->
+ s.append(key)
+ s.append("=")
+ s.append(value)
+ i++
+ if (i < _size) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append('}').toString()
+ }
+
+ /**
+ * Scans the hash table to find the index in the backing arrays of the
+ * specified [key]. Returns -1 if the key is not present.
+ */
+ @PublishedApi
+ internal fun findKeyIndex(key: Long): Int {
+ val hash = hash(key)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutableLongLongMap] is a container with a [MutableMap]-like interface for
+ * [Long] primitive keys and [Long] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableLongLongMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableLongLongMap(
+ initialCapacity: Int = DefaultScatterCapacity
+) : LongLongMap() {
+ // Number of entries we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ keys = LongArray(newCapacity)
+ values = LongArray(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * if the value is present in the map and not `null`. Otherwise,
+ * calls `defaultValue()` and puts the result in the map associated
+ * with [key].
+ */
+ public inline fun getOrPut(key: Long, defaultValue: () -> Long): Long {
+ val index = findKeyIndex(key)
+ return if (index < 0) {
+ val defValue = defaultValue()
+ put(key, defValue)
+ defValue
+ } else {
+ values[index]
+ }
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public operator fun set(key: Long, value: Long) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations. Return the previous value associated with the [key],
+ * or `null` if the key was not present in the map.
+ */
+ public fun put(key: Long, value: Long) {
+ set(key, value)
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Long] key and [Long] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<Long, Long>>) {
+ for ((key, value) in pairs) {
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public fun putAll(from: LongLongMap) {
+ from.forEach { key, value ->
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts the key/value mapping from the [pair] in this map, using the first
+ * element as the key, and the second element as the value.
+ *
+ * Note that [pair] allocated and both the [Long] key and [Long] value are
+ * boxed. Use [set] instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(pair: Pair<Long, Long>) {
+ this[pair.first] = pair.second
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Long] key and [Long] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(
+ @Suppress("ArrayReturn") pairs: Array<Pair<Long, Long>>
+ ): Unit = putAll(pairs)
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public inline operator fun plusAssign(from: LongLongMap): Unit = putAll(from)
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public fun remove(key: Long) {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ removeValueAt(index)
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map if the
+ * associated value equals [value]. Returns whether the removal happened.
+ */
+ public fun remove(key: Long, value: Long): Boolean {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ if (values[index] == value) {
+ removeValueAt(index)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes any mapping for which the specified [predicate] returns true.
+ */
+ public fun removeIf(predicate: (Long, Long) -> Boolean) {
+ forEachIndexed { index ->
+ if (predicate(keys[index], values[index])) {
+ removeValueAt(index)
+ }
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public inline operator fun minusAssign(key: Long) {
+ remove(key)
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: LongArray) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: LongSet) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: LongList) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ private fun removeValueAt(index: Int) {
+ _size -= 1
+
+ // TODO: We could just mark the entry as empty if there's a group
+ // window around this entry that was already empty
+ writeMetadata(index, Deleted)
+ }
+
+ /**
+ * Removes all mappings from this map.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the hash table to find the index at which we can store a value
+ * for the give [key]. If the key already exists in the table, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the table is full.
+ */
+ private fun findAbsoluteInsertIndex(key: Long): Int {
+ val hash = hash(key)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the table in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutableLongLongMap]'s storage so it is sized appropriately
+ * to hold the current mappings.
+ *
+ * Returns the number of empty entries removed from this map's storage.
+ * Returns be 0 if no trimming is necessary or possible.
+ */
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted entries from the table to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_set`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousKeys = keys
+ val previousValues = values
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newKeys = keys
+ val newValues = values
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousKey = previousKeys[i]
+ val hash = hash(previousKey)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newKeys[index] = previousKey
+ newValues[index] = previousValues[i]
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt
new file mode 100644
index 0000000..f01c754
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt
@@ -0,0 +1,847 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyLongObjectMap = MutableLongObjectMap<Nothing>(0)
+
+/**
+ * Returns an empty, read-only [LongObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> emptyLongObjectMap(): LongObjectMap<V> = EmptyLongObjectMap as LongObjectMap<V>
+
+/**
+ * Returns an empty, read-only [LongObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> longObjectMapOf(): LongObjectMap<V> = EmptyLongObjectMap as LongObjectMap<V>
+
+/**
+ * Returns a new [LongObjectMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Long] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <V> longObjectMapOf(vararg pairs: Pair<Long, V>): LongObjectMap<V> =
+ MutableLongObjectMap<V>(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * Returns a new [MutableLongObjectMap].
+ */
+public fun <V> mutableLongObjectMapOf(): MutableLongObjectMap<V> = MutableLongObjectMap()
+
+/**
+ * Returns a new [MutableLongObjectMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Long] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <V> mutableLongObjectMapOf(vararg pairs: Pair<Long, V>): MutableLongObjectMap<V> =
+ MutableLongObjectMap<V>(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * [LongObjectMap] is a container with a [Map]-like interface for keys with
+ * [Long] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableLongObjectMap].
+ *
+ * @see [MutableLongObjectMap]
+ */
+public sealed class LongObjectMap<V> {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` entries, including when
+ // the table is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var keys: LongArray = EmptyLongArray
+
+ @PublishedApi
+ @JvmField
+ internal var values: Array<Any?> = EMPTY_OBJECTS
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of key-value pairs that can be stored in this map
+ * without requiring internal storage reallocation.
+ */
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of key-value pairs in this map.
+ */
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this map has at least one entry.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this map has no entries.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this map is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this map is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the value corresponding to the given [key], or `null` if such
+ * a key is not present in the map.
+ */
+ public operator fun get(key: Long): V? {
+ val index = findKeyIndex(key)
+ @Suppress("UNCHECKED_CAST")
+ return if (index >= 0) values[index] as V? else null
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * or [defaultValue] if this map contains no mapping for the key.
+ */
+ public fun getOrDefault(key: Long, defaultValue: V): V {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ @Suppress("UNCHECKED_CAST")
+ return values[index] as V
+ }
+ return defaultValue
+ }
+
+ /**
+ * Returns the value for the given [key] if the value is present
+ * and not null. Otherwise, returns the result of the [defaultValue]
+ * function.
+ */
+ public inline fun getOrElse(key: Long, defaultValue: () -> V): V {
+ return get(key) ?: defaultValue()
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ public inline fun forEach(block: (key: Long, value: V) -> Unit) {
+ val k = keys
+ val v = values
+
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ block(k[index], v[index] as V)
+ }
+ }
+
+ /**
+ * Iterates over every key stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachKey(block: (key: Long) -> Unit) {
+ val k = keys
+
+ forEachIndexed { index ->
+ block(k[index])
+ }
+ }
+
+ /**
+ * Iterates over every value stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachValue(block: (value: V) -> Unit) {
+ val v = values
+
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ block(v[index] as V)
+ }
+ }
+
+ /**
+ * Returns true if all entries match the given [predicate].
+ */
+ public inline fun all(predicate: (Long, V) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (!predicate(key, value)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one entry matches the given [predicate].
+ */
+ public inline fun any(predicate: (Long, V) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (predicate(key, value)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of entries in this map.
+ */
+ public fun count(): Int = size
+
+ /**
+ * Returns the number of entries matching the given [predicate].
+ */
+ public inline fun count(predicate: (Long, V) -> Boolean): Int {
+ var count = 0
+ forEach { key, value ->
+ if (predicate(key, value)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public operator fun contains(key: Long): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsKey(key: Long): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [value] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsValue(value: V): Boolean {
+ forEachValue { v ->
+ if (value == v) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the hash code value for this map. The hash code the sum of the hash
+ * codes of each key/value pair.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { key, value ->
+ hash += key.hashCode() xor value.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this hash map for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [LongObjectMap]
+ * - Has the same [size] as this map
+ * - Contains key/value pairs equal to this map's pair
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is LongObjectMap<*>) {
+ return false
+ }
+ if (other.size != size) {
+ return false
+ }
+
+ forEach { key, value ->
+ if (value == null) {
+ if (other[key] != null || !other.containsKey(key)) {
+ return false
+ }
+ } else if (value != other[key]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this map. The map is denoted in the
+ * string by the `{}`. Each key/value pair present in the map is represented
+ * inside '{}` by a substring of the form `key=value`, and pairs are
+ * separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ val s = StringBuilder().append('{')
+ var i = 0
+ forEach { key, value ->
+ s.append(key)
+ s.append("=")
+ s.append(if (value === this) "(this)" else value)
+ i++
+ if (i < _size) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append('}').toString()
+ }
+
+ /**
+ * Scans the hash table to find the index in the backing arrays of the
+ * specified [key]. Returns -1 if the key is not present.
+ */
+ internal inline fun findKeyIndex(key: Long): Int {
+ val hash = hash(key)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutableLongObjectMap] is a container with a [MutableMap]-like interface for keys with
+ * [Long] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableLongObjectMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see ScatterMap
+ */
+public class MutableLongObjectMap<V>(
+ initialCapacity: Int = DefaultScatterCapacity
+) : LongObjectMap<V>() {
+ // Number of entries we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ keys = LongArray(newCapacity)
+ values = arrayOfNulls(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * if the value is present in the map and not `null`. Otherwise,
+ * calls `defaultValue()` and puts the result in the map associated
+ * with [key].
+ */
+ public inline fun getOrPut(key: Long, defaultValue: () -> V): V {
+ return get(key) ?: defaultValue().also { set(key, it) }
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public operator fun set(key: Long, value: V) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations. Return the previous value associated with the [key],
+ * or `null` if the key was not present in the map.
+ */
+ public fun put(key: Long, value: V): V? {
+ val index = findAbsoluteInsertIndex(key)
+ val oldValue = values[index]
+ keys[index] = key
+ values[index] = value
+
+ @Suppress("UNCHECKED_CAST")
+ return oldValue as V?
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Long] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+ public fun putAll(@Suppress("ArrayReturn") pairs: Array<out Pair<Long, V>>) {
+ for ((key, value) in pairs) {
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public fun putAll(from: LongObjectMap<V>) {
+ from.forEach { key, value ->
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts the key/value mapping from the [pair] in this map, using the first
+ * element as the key, and the second element as the value.
+ *
+ * Note that the [Pair] is allocated and the [Long] key is boxed.
+ * Use [set] instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(pair: Pair<Long, V>) {
+ this[pair.first] = pair.second
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Long] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+ public inline operator fun plusAssign(
+ @Suppress("ArrayReturn") pairs: Array<out Pair<Long, V>>
+ ): Unit = putAll(pairs)
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public inline operator fun plusAssign(from: LongObjectMap<V>): Unit = putAll(from)
+
+ /**
+ * Removes the specified [key] and its associated value from the map. If the
+ * [key] was present in the map, this function returns the value that was
+ * present before removal.
+ */
+ public fun remove(key: Long): V? {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return removeValueAt(index)
+ }
+ return null
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map if the
+ * associated value equals [value]. Returns whether the removal happened.
+ */
+ public fun remove(key: Long, value: V): Boolean {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ if (values[index] == value) {
+ removeValueAt(index)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes any mapping for which the specified [predicate] returns true.
+ */
+ public fun removeIf(predicate: (Long, V) -> Boolean) {
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ if (predicate(keys[index], values[index] as V)) {
+ removeValueAt(index)
+ }
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public inline operator fun minusAssign(key: Long) {
+ remove(key)
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: LongArray) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: LongSet) {
+ keys.forEach { key ->
+ minusAssign(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: LongList) {
+ keys.forEach { key ->
+ minusAssign(key)
+ }
+ }
+
+ private fun removeValueAt(index: Int): V? {
+ _size -= 1
+
+ // TODO: We could just mark the entry as empty if there's a group
+ // window around this entry that was already empty
+ writeMetadata(index, Deleted)
+ val oldValue = values[index]
+ values[index] = null
+
+ @Suppress("UNCHECKED_CAST")
+ return oldValue as V?
+ }
+
+ /**
+ * Removes all mappings from this map.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ values.fill(null, 0, _capacity)
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the hash table to find the index at which we can store a value
+ * for the give [key]. If the key already exists in the table, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the table is full.
+ */
+ private fun findAbsoluteInsertIndex(key: Long): Int {
+ val hash = hash(key)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the table in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutableLongObjectMap]'s storage so it is sized appropriately
+ * to hold the current mappings.
+ *
+ * Returns the number of empty entries removed from this map's storage.
+ * Returns be 0 if no trimming is necessary or possible.
+ */
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted entries from the table to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_set`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousKeys = keys
+ val previousValues = values
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newKeys = keys
+ val newValues = values
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousKey = previousKeys[i]
+ val hash = hash(previousKey)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newKeys[index] = previousKey
+ newValues[index] = previousValues[i]
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
index f292716..45734c8 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
@@ -29,13 +29,21 @@
import kotlin.contracts.contract
import kotlin.jvm.JvmField
-// This is a copy of ScatterSet, but with Long elements
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// This is a copy of ScatterSet, but with primitive elements
// Default empty set to avoid allocations
private val EmptyLongSet = MutableLongSet(0)
// An empty array of longs
-private val EmptyLongArray = LongArray(0)
+internal val EmptyLongArray = LongArray(0)
/**
* Returns an empty, read-only [LongSet].
@@ -770,7 +778,7 @@
* Returns the hash code of [k]. This follows the [HashSet] default behavior on Android
* of returning [Object.hashcode()] with the higher bits of hash spread to the lower bits.
*/
-private inline fun hash(k: Long): Int {
+internal inline fun hash(k: Long): Int {
val hash = k.hashCode()
return hash xor (hash ushr 16)
}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt
new file mode 100644
index 0000000..95c1a3d
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt
@@ -0,0 +1,855 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyObjectFloatMap = MutableObjectFloatMap<Any?>(0)
+
+/**
+ * Returns an empty, read-only [ObjectFloatMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> emptyObjectFloatMap(): ObjectFloatMap<K> =
+ EmptyObjectFloatMap as ObjectFloatMap<K>
+
+/**
+ * Returns an empty, read-only [ObjectFloatMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> objectFloatMap(): ObjectFloatMap<K> =
+ EmptyObjectFloatMap as ObjectFloatMap<K>
+
+/**
+ * Returns a new [MutableObjectFloatMap].
+ */
+public fun <K> mutableObjectFloatMapOf(): MutableObjectFloatMap<K> = MutableObjectFloatMap()
+
+/**
+ * Returns a new [MutableObjectFloatMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Float] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <K> objectFloatMapOf(vararg pairs: Pair<K, Float>): ObjectFloatMap<K> =
+ MutableObjectFloatMap<K>(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * Returns a new [MutableObjectFloatMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Float] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <K> mutableObjectFloatMapOf(vararg pairs: Pair<K, Float>): MutableObjectFloatMap<K> =
+ MutableObjectFloatMap<K>(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * [ObjectFloatMap] is a container with a [Map]-like interface for keys with
+ * reference types and [Float] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableObjectFloatMap].
+ *
+ * @see [MutableObjectFloatMap]
+ * @see ScatterMap
+ */
+public sealed class ObjectFloatMap<K> {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` entries, including when
+ // the table is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var keys: Array<Any?> = EMPTY_OBJECTS
+
+ @PublishedApi
+ @JvmField
+ internal var values: FloatArray = EmptyFloatArray
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of key-value pairs that can be stored in this map
+ * without requiring internal storage reallocation.
+ */
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of key-value pairs in this map.
+ */
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this map has at least one entry.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this map has no entries.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this map is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this map is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the value corresponding to the given [key], or `null` if such
+ * a key is not present in the map.
+ * @throws NoSuchElementException when [key] is not found
+ */
+ public operator fun get(key: K): Float {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ throw NoSuchElementException("There is no key $key in the map")
+ }
+ return values[index]
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * or [defaultValue] if this map contains no mapping for the key.
+ */
+ public fun getOrDefault(key: K, defaultValue: Float): Float {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ return defaultValue
+ }
+
+ /**
+ * Returns the value for the given [key] if the value is present
+ * and not null. Otherwise, returns the result of the [defaultValue]
+ * function.
+ */
+ public inline fun getOrElse(key: K, defaultValue: () -> Float): Float {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ return defaultValue()
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ public inline fun forEach(block: (key: K, value: Float) -> Unit) {
+ val k = keys
+ val v = values
+
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ block(k[index] as K, v[index])
+ }
+ }
+
+ /**
+ * Iterates over every key stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachKey(block: (key: K) -> Unit) {
+ val k = keys
+
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ block(k[index] as K)
+ }
+ }
+
+ /**
+ * Iterates over every value stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachValue(block: (value: Float) -> Unit) {
+ val v = values
+
+ forEachIndexed { index ->
+ block(v[index])
+ }
+ }
+
+ /**
+ * Returns true if all entries match the given [predicate].
+ */
+ public inline fun all(predicate: (K, Float) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (!predicate(key, value)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one entry matches the given [predicate].
+ */
+ public inline fun any(predicate: (K, Float) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (predicate(key, value)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of entries in this map.
+ */
+ public fun count(): Int = size
+
+ /**
+ * Returns the number of entries matching the given [predicate].
+ */
+ public inline fun count(predicate: (K, Float) -> Boolean): Int {
+ var count = 0
+ forEach { key, value ->
+ if (predicate(key, value)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public operator fun contains(key: K): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsKey(key: K): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [value] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsValue(value: Float): Boolean {
+ forEachValue { v ->
+ if (value == v) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the hash code value for this map. The hash code the sum of the hash
+ * codes of each key/value pair.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { key, value ->
+ hash += key.hashCode() xor value.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this hash map for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [ObjectFloatMap]
+ * - Has the same [size] as this map
+ * - Contains key/value pairs equal to this map's pair
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is ObjectFloatMap<*>) {
+ return false
+ }
+ if (other.size != size) {
+ return false
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ val o = other as ObjectFloatMap<Any?>
+
+ forEach { key, value ->
+ if (value != o[key]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this map. The map is denoted in the
+ * string by the `{}`. Each key/value pair present in the map is represented
+ * inside '{}` by a substring of the form `key=value`, and pairs are
+ * separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ val s = StringBuilder().append('{')
+ var i = 0
+ forEach { key, value ->
+ s.append(if (key === this) "(this)" else key)
+ s.append("=")
+ s.append(value)
+ i++
+ if (i < _size) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append('}').toString()
+ }
+
+ /**
+ * Scans the hash table to find the index in the backing arrays of the
+ * specified [key]. Returns -1 if the key is not present.
+ */
+ @PublishedApi
+ internal fun findKeyIndex(key: K): Int {
+ val hash = hash(key)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutableObjectFloatMap] is a container with a [MutableMap]-like interface for keys with
+ * reference types and [Float] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableObjectFloatMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableObjectFloatMap<K>(
+ initialCapacity: Int = DefaultScatterCapacity
+) : ObjectFloatMap<K>() {
+ // Number of entries we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ keys = arrayOfNulls(newCapacity)
+ values = FloatArray(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * if the value is present in the map and not `null`. Otherwise,
+ * calls `defaultValue()` and puts the result in the map associated
+ * with [key].
+ */
+ public inline fun getOrPut(key: K, defaultValue: () -> Float): Float {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ val value = defaultValue()
+ set(key, value)
+ return value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public operator fun set(key: K, value: Float) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public fun put(key: K, value: Float) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Float] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+ public fun putAll(@Suppress("ArrayReturn") pairs: Array<out Pair<K, Float>>) {
+ for ((key, value) in pairs) {
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public fun putAll(from: ObjectFloatMap<K>) {
+ from.forEach { key, value ->
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts the key/value mapping from the [pair] in this map, using the first
+ * element as the key, and the second element as the value.
+ *
+ * Note that the [Pair] is allocated and the [Float] value is boxed.
+ * Use [set] instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(pair: Pair<K, Float>) {
+ this[pair.first] = pair.second
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Float] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+ public inline operator fun plusAssign(
+ @Suppress("ArrayReturn") pairs: Array<out Pair<K, Float>>
+ ): Unit = putAll(pairs)
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public inline operator fun plusAssign(from: ObjectFloatMap<K>): Unit = putAll(from)
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public fun remove(key: K) {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ removeValueAt(index)
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map if the
+ * associated value equals [value]. Returns whether the removal happened.
+ */
+ public fun remove(key: K, value: Float): Boolean {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ if (values[index] == value) {
+ removeValueAt(index)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes any mapping for which the specified [predicate] returns true.
+ */
+ public fun removeIf(predicate: (K, Float) -> Boolean) {
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ if (predicate(keys[index] as K, values[index])) {
+ removeValueAt(index)
+ }
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public inline operator fun minusAssign(key: K) {
+ remove(key)
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: Array<out K>) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: Iterable<K>) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: Sequence<K>) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: ScatterSet<K>) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ private fun removeValueAt(index: Int) {
+ _size -= 1
+
+ // TODO: We could just mark the entry as empty if there's a group
+ // window around this entry that was already empty
+ writeMetadata(index, Deleted)
+ keys[index] = null
+ }
+
+ /**
+ * Removes all mappings from this map.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ keys.fill(null, 0, _capacity)
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the hash table to find the index at which we can store a value
+ * for the give [key]. If the key already exists in the table, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the table is full.
+ */
+ private fun findAbsoluteInsertIndex(key: K): Int {
+ val hash = hash(key)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the table in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutableObjectFloatMap]'s storage so it is sized appropriately
+ * to hold the current mappings.
+ *
+ * Returns the number of empty entries removed from this map's storage.
+ * Returns be 0 if no trimming is necessary or possible.
+ */
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted entries from the table to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_set`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousKeys = keys
+ val previousValues = values
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newKeys = keys
+ val newValues = values
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousKey = previousKeys[i]
+ val hash = hash(previousKey)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newKeys[index] = previousKey
+ newValues[index] = previousValues[i]
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt
new file mode 100644
index 0000000..697f1b0
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt
@@ -0,0 +1,855 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyObjectIntMap = MutableObjectIntMap<Any?>(0)
+
+/**
+ * Returns an empty, read-only [ObjectIntMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> emptyObjectIntMap(): ObjectIntMap<K> =
+ EmptyObjectIntMap as ObjectIntMap<K>
+
+/**
+ * Returns an empty, read-only [ObjectIntMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> objectIntMap(): ObjectIntMap<K> =
+ EmptyObjectIntMap as ObjectIntMap<K>
+
+/**
+ * Returns a new [MutableObjectIntMap].
+ */
+public fun <K> mutableObjectIntMapOf(): MutableObjectIntMap<K> = MutableObjectIntMap()
+
+/**
+ * Returns a new [MutableObjectIntMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Int] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <K> objectIntMapOf(vararg pairs: Pair<K, Int>): ObjectIntMap<K> =
+ MutableObjectIntMap<K>(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * Returns a new [MutableObjectIntMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Int] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <K> mutableObjectIntMapOf(vararg pairs: Pair<K, Int>): MutableObjectIntMap<K> =
+ MutableObjectIntMap<K>(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * [ObjectIntMap] is a container with a [Map]-like interface for keys with
+ * reference types and [Int] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableObjectIntMap].
+ *
+ * @see [MutableObjectIntMap]
+ * @see ScatterMap
+ */
+public sealed class ObjectIntMap<K> {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` entries, including when
+ // the table is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var keys: Array<Any?> = EMPTY_OBJECTS
+
+ @PublishedApi
+ @JvmField
+ internal var values: IntArray = EmptyIntArray
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of key-value pairs that can be stored in this map
+ * without requiring internal storage reallocation.
+ */
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of key-value pairs in this map.
+ */
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this map has at least one entry.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this map has no entries.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this map is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this map is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the value corresponding to the given [key], or `null` if such
+ * a key is not present in the map.
+ * @throws NoSuchElementException when [key] is not found
+ */
+ public operator fun get(key: K): Int {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ throw NoSuchElementException("There is no key $key in the map")
+ }
+ return values[index]
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * or [defaultValue] if this map contains no mapping for the key.
+ */
+ public fun getOrDefault(key: K, defaultValue: Int): Int {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ return defaultValue
+ }
+
+ /**
+ * Returns the value for the given [key] if the value is present
+ * and not null. Otherwise, returns the result of the [defaultValue]
+ * function.
+ */
+ public inline fun getOrElse(key: K, defaultValue: () -> Int): Int {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ return defaultValue()
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ public inline fun forEach(block: (key: K, value: Int) -> Unit) {
+ val k = keys
+ val v = values
+
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ block(k[index] as K, v[index])
+ }
+ }
+
+ /**
+ * Iterates over every key stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachKey(block: (key: K) -> Unit) {
+ val k = keys
+
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ block(k[index] as K)
+ }
+ }
+
+ /**
+ * Iterates over every value stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachValue(block: (value: Int) -> Unit) {
+ val v = values
+
+ forEachIndexed { index ->
+ block(v[index])
+ }
+ }
+
+ /**
+ * Returns true if all entries match the given [predicate].
+ */
+ public inline fun all(predicate: (K, Int) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (!predicate(key, value)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one entry matches the given [predicate].
+ */
+ public inline fun any(predicate: (K, Int) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (predicate(key, value)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of entries in this map.
+ */
+ public fun count(): Int = size
+
+ /**
+ * Returns the number of entries matching the given [predicate].
+ */
+ public inline fun count(predicate: (K, Int) -> Boolean): Int {
+ var count = 0
+ forEach { key, value ->
+ if (predicate(key, value)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public operator fun contains(key: K): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsKey(key: K): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [value] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsValue(value: Int): Boolean {
+ forEachValue { v ->
+ if (value == v) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the hash code value for this map. The hash code the sum of the hash
+ * codes of each key/value pair.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { key, value ->
+ hash += key.hashCode() xor value.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this hash map for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [ObjectIntMap]
+ * - Has the same [size] as this map
+ * - Contains key/value pairs equal to this map's pair
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is ObjectIntMap<*>) {
+ return false
+ }
+ if (other.size != size) {
+ return false
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ val o = other as ObjectIntMap<Any?>
+
+ forEach { key, value ->
+ if (value != o[key]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this map. The map is denoted in the
+ * string by the `{}`. Each key/value pair present in the map is represented
+ * inside '{}` by a substring of the form `key=value`, and pairs are
+ * separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ val s = StringBuilder().append('{')
+ var i = 0
+ forEach { key, value ->
+ s.append(if (key === this) "(this)" else key)
+ s.append("=")
+ s.append(value)
+ i++
+ if (i < _size) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append('}').toString()
+ }
+
+ /**
+ * Scans the hash table to find the index in the backing arrays of the
+ * specified [key]. Returns -1 if the key is not present.
+ */
+ @PublishedApi
+ internal fun findKeyIndex(key: K): Int {
+ val hash = hash(key)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutableObjectIntMap] is a container with a [MutableMap]-like interface for keys with
+ * reference types and [Int] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableObjectIntMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableObjectIntMap<K>(
+ initialCapacity: Int = DefaultScatterCapacity
+) : ObjectIntMap<K>() {
+ // Number of entries we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ keys = arrayOfNulls(newCapacity)
+ values = IntArray(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * if the value is present in the map and not `null`. Otherwise,
+ * calls `defaultValue()` and puts the result in the map associated
+ * with [key].
+ */
+ public inline fun getOrPut(key: K, defaultValue: () -> Int): Int {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ val value = defaultValue()
+ set(key, value)
+ return value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public operator fun set(key: K, value: Int) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public fun put(key: K, value: Int) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Int] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+ public fun putAll(@Suppress("ArrayReturn") pairs: Array<out Pair<K, Int>>) {
+ for ((key, value) in pairs) {
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public fun putAll(from: ObjectIntMap<K>) {
+ from.forEach { key, value ->
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts the key/value mapping from the [pair] in this map, using the first
+ * element as the key, and the second element as the value.
+ *
+ * Note that the [Pair] is allocated and the [Int] value is boxed.
+ * Use [set] instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(pair: Pair<K, Int>) {
+ this[pair.first] = pair.second
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Int] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+ public inline operator fun plusAssign(
+ @Suppress("ArrayReturn") pairs: Array<out Pair<K, Int>>
+ ): Unit = putAll(pairs)
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public inline operator fun plusAssign(from: ObjectIntMap<K>): Unit = putAll(from)
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public fun remove(key: K) {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ removeValueAt(index)
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map if the
+ * associated value equals [value]. Returns whether the removal happened.
+ */
+ public fun remove(key: K, value: Int): Boolean {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ if (values[index] == value) {
+ removeValueAt(index)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes any mapping for which the specified [predicate] returns true.
+ */
+ public fun removeIf(predicate: (K, Int) -> Boolean) {
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ if (predicate(keys[index] as K, values[index])) {
+ removeValueAt(index)
+ }
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public inline operator fun minusAssign(key: K) {
+ remove(key)
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: Array<out K>) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: Iterable<K>) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: Sequence<K>) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: ScatterSet<K>) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ private fun removeValueAt(index: Int) {
+ _size -= 1
+
+ // TODO: We could just mark the entry as empty if there's a group
+ // window around this entry that was already empty
+ writeMetadata(index, Deleted)
+ keys[index] = null
+ }
+
+ /**
+ * Removes all mappings from this map.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ keys.fill(null, 0, _capacity)
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the hash table to find the index at which we can store a value
+ * for the give [key]. If the key already exists in the table, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the table is full.
+ */
+ private fun findAbsoluteInsertIndex(key: K): Int {
+ val hash = hash(key)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the table in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutableObjectIntMap]'s storage so it is sized appropriately
+ * to hold the current mappings.
+ *
+ * Returns the number of empty entries removed from this map's storage.
+ * Returns be 0 if no trimming is necessary or possible.
+ */
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted entries from the table to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_set`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousKeys = keys
+ val previousValues = values
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newKeys = keys
+ val newValues = values
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousKey = previousKeys[i]
+ val hash = hash(previousKey)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newKeys[index] = previousKey
+ newValues[index] = previousValues[i]
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectList.kt
new file mode 100644
index 0000000..105ebaf
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectList.kt
@@ -0,0 +1,1560 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:Suppress("NOTHING_TO_INLINE", "RedundantVisibilityModifier", "UNCHECKED_CAST")
+@file:OptIn(ExperimentalContracts::class)
+
+package androidx.collection
+
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+import kotlin.jvm.JvmField
+
+/**
+ * [ObjectList] is a [List]-like collection for reference types. It is optimized for fast
+ * access, avoiding virtual and interface method access. Methods avoid allocation whenever
+ * possible. For example [forEach] does not need allocate an [Iterator].
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the list (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. It is also not safe to mutate during reentrancy --
+ * in the middle of a [forEach], for example. However, concurrent reads are safe.
+ *
+ * **Note** [List] access is available through [asList] when developers need access to the
+ * common API.
+ *
+ * @see MutableObjectList
+ * @see FloatList
+ * @see IntList
+ * @eee LongList
+ */
+public sealed class ObjectList<E>(initialCapacity: Int) {
+ @JvmField
+ @PublishedApi
+ internal var content: Array<Any?> = if (initialCapacity == 0) {
+ EmptyArray
+ } else {
+ arrayOfNulls(initialCapacity)
+ }
+
+ @Suppress("PropertyName")
+ @JvmField
+ @PublishedApi
+ internal var _size: Int = 0
+
+ /**
+ * The number of elements in the [ObjectList].
+ */
+ @get:androidx.annotation.IntRange(from = 0)
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns the last valid index in the [ObjectList]. This can be `-1` when the list is empty.
+ */
+ @get:androidx.annotation.IntRange(from = -1)
+ public inline val lastIndex: Int get() = _size - 1
+
+ /**
+ * Returns an [IntRange] of the valid indices for this [ObjectList].
+ */
+ public inline val indices: IntRange get() = 0 until _size
+
+ /**
+ * Returns `true` if the collection has no elements in it.
+ */
+ public fun none(): Boolean {
+ return isEmpty()
+ }
+
+ /**
+ * Returns `true` if there's at least one element in the collection.
+ */
+ public fun any(): Boolean {
+ return isNotEmpty()
+ }
+
+ /**
+ * Returns `true` if any of the elements give a `true` return value for [predicate].
+ */
+ public inline fun any(predicate: (element: E) -> Boolean): Boolean {
+ contract { callsInPlace(predicate) }
+ forEach {
+ if (predicate(it)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Returns `true` if any of the elements give a `true` return value for [predicate] while
+ * iterating in the reverse order.
+ */
+ public inline fun reversedAny(predicate: (element: E) -> Boolean): Boolean {
+ contract { callsInPlace(predicate) }
+ forEachReversed {
+ if (predicate(it)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Returns `true` if the [ObjectList] contains [element] or `false` otherwise.
+ */
+ public operator fun contains(element: E): Boolean {
+ forEach {
+ if (it == element) {
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Returns `true` if the [ObjectList] contains all elements in [elements] or `false` if
+ * one or more are missing.
+ */
+ public fun containsAll(@Suppress("ArrayReturn") elements: Array<E>): Boolean {
+ for (i in elements.indices) {
+ if (!contains(elements[i])) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns `true` if the [ObjectList] contains all elements in [elements] or `false` if
+ * one or more are missing.
+ */
+ public fun containsAll(elements: List<E>): Boolean {
+ for (i in elements.indices) {
+ if (!contains(elements[i])) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns `true` if the [ObjectList] contains all elements in [elements] or `false` if
+ * one or more are missing.
+ */
+ public fun containsAll(elements: Iterable<E>): Boolean {
+ elements.forEach { element ->
+ if (!contains(element)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns `true` if the [ObjectList] contains all elements in [elements] or `false` if
+ * one or more are missing.
+ */
+ public fun containsAll(elements: ObjectList<E>): Boolean {
+ elements.forEach { element ->
+ if (!contains(element)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns the number of elements in this list.
+ */
+ public fun count(): Int = _size
+
+ /**
+ * Counts the number of elements matching [predicate].
+ * @return The number of elements in this list for which [predicate] returns true.
+ */
+ public inline fun count(predicate: (element: E) -> Boolean): Int {
+ contract { callsInPlace(predicate) }
+ var count = 0
+ forEach { if (predicate(it)) count++ }
+ return count
+ }
+
+ /**
+ * Returns the first element in the [ObjectList] or throws a [NoSuchElementException] if
+ * it [isEmpty].
+ */
+ public fun first(): E {
+ if (isEmpty()) {
+ throw NoSuchElementException("ObjectList is empty.")
+ }
+ return content[0] as E
+ }
+
+ /**
+ * Returns the first element in the [ObjectList] for which [predicate] returns `true` or
+ * throws [NoSuchElementException] if nothing matches.
+ * @see indexOfFirst
+ * @see firstOrNull
+ */
+ public inline fun first(predicate: (element: E) -> Boolean): E {
+ contract { callsInPlace(predicate) }
+ forEach { element ->
+ if (predicate(element)) return element
+ }
+ throw NoSuchElementException("ObjectList contains no element matching the predicate.")
+ }
+
+ /**
+ * Returns the first element in the [ObjectList] or `null` if it [isEmpty].
+ */
+ public inline fun firstOrNull(): E? = if (isEmpty()) null else get(0)
+
+ /**
+ * Returns the first element in the [ObjectList] for which [predicate] returns `true` or
+ * `null` if nothing matches.
+ * @see indexOfFirst
+ */
+ public inline fun firstOrNull(predicate: (element: E) -> Boolean): E? {
+ contract { callsInPlace(predicate) }
+ forEach { element ->
+ if (predicate(element)) return element
+ }
+ return null
+ }
+
+ /**
+ * Accumulates values, starting with [initial], and applying [operation] to each element
+ * in the [ObjectList] in order.
+ * @param initial The value of `acc` for the first call to [operation] or return value if
+ * there are no elements in this list.
+ * @param operation function that takes current accumulator value and an element, and
+ * calculates the next accumulator value.
+ */
+ public inline fun <R> fold(initial: R, operation: (acc: R, element: E) -> R): R {
+ contract { callsInPlace(operation) }
+ var acc = initial
+ forEach { element ->
+ acc = operation(acc, element)
+ }
+ return acc
+ }
+
+ /**
+ * Accumulates values, starting with [initial], and applying [operation] to each element
+ * in the [ObjectList] in order.
+ */
+ public inline fun <R> foldIndexed(
+ initial: R,
+ operation: (index: Int, acc: R, element: E) -> R
+ ): R {
+ contract { callsInPlace(operation) }
+ var acc = initial
+ forEachIndexed { i, element ->
+ acc = operation(i, acc, element)
+ }
+ return acc
+ }
+
+ /**
+ * Accumulates values, starting with [initial], and applying [operation] to each element
+ * in the [ObjectList] in reverse order.
+ * @param initial The value of `acc` for the first call to [operation] or return value if
+ * there are no elements in this list.
+ * @param operation function that takes an element and the current accumulator value, and
+ * calculates the next accumulator value.
+ */
+ public inline fun <R> foldRight(initial: R, operation: (element: E, acc: R) -> R): R {
+ contract { callsInPlace(operation) }
+ var acc = initial
+ forEachReversed { element ->
+ acc = operation(element, acc)
+ }
+ return acc
+ }
+
+ /**
+ * Accumulates values, starting with [initial], and applying [operation] to each element
+ * in the [ObjectList] in reverse order.
+ */
+ public inline fun <R> foldRightIndexed(
+ initial: R,
+ operation: (index: Int, element: E, acc: R) -> R
+ ): R {
+ contract { callsInPlace(operation) }
+ var acc = initial
+ forEachReversedIndexed { i, element ->
+ acc = operation(i, element, acc)
+ }
+ return acc
+ }
+
+ /**
+ * Calls [block] for each element in the [ObjectList], in order.
+ * @param block will be executed for every element in the list, accepting an element from
+ * the list
+ */
+ public inline fun forEach(block: (element: E) -> Unit) {
+ contract { callsInPlace(block) }
+ val content = content
+ for (i in 0 until _size) {
+ block(content[i] as E)
+ }
+ }
+
+ /**
+ * Calls [block] for each element in the [ObjectList] along with its index, in order.
+ * @param block will be executed for every element in the list, accepting the index and
+ * the element at that index.
+ */
+ public inline fun forEachIndexed(block: (index: Int, element: E) -> Unit) {
+ contract { callsInPlace(block) }
+ val content = content
+ for (i in 0 until _size) {
+ block(i, content[i] as E)
+ }
+ }
+
+ /**
+ * Calls [block] for each element in the [ObjectList] in reverse order.
+ * @param block will be executed for every element in the list, accepting an element from
+ * the list
+ */
+ public inline fun forEachReversed(block: (element: E) -> Unit) {
+ contract { callsInPlace(block) }
+ val content = content
+ for (i in _size - 1 downTo 0) {
+ block(content[i] as E)
+ }
+ }
+
+ /**
+ * Calls [block] for each element in the [ObjectList] along with its index, in reverse
+ * order.
+ * @param block will be executed for every element in the list, accepting the index and
+ * the element at that index.
+ */
+ public inline fun forEachReversedIndexed(block: (index: Int, element: E) -> Unit) {
+ contract { callsInPlace(block) }
+ val content = content
+ for (i in _size - 1 downTo 0) {
+ block(i, content[i] as E)
+ }
+ }
+
+ /**
+ * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+ * the [index] is out of bounds of this collection.
+ */
+ public operator fun get(@androidx.annotation.IntRange(from = 0) index: Int): E {
+ if (index !in 0 until _size) {
+ throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ }
+ return content[index] as E
+ }
+
+ /**
+ * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+ * the [index] is out of bounds of this collection.
+ */
+ public fun elementAt(@androidx.annotation.IntRange(from = 0) index: Int): E {
+ if (index !in 0 until _size) {
+ throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ }
+ return content[index] as E
+ }
+
+ /**
+ * Returns the element at the given [index] or [defaultValue] if [index] is out of bounds
+ * of the collection.
+ * @param index The index of the element whose value should be returned
+ * @param defaultValue A lambda to call with [index] as a parameter to return a value at
+ * an index not in the list.
+ */
+ public inline fun elementAtOrElse(
+ @androidx.annotation.IntRange(from = 0) index: Int,
+ defaultValue: (index: Int) -> E
+ ): E {
+ if (index !in 0 until _size) {
+ return defaultValue(index)
+ }
+ return content[index] as E
+ }
+
+ /**
+ * Returns the index of [element] in the [ObjectList] or `-1` if [element] is not there.
+ */
+ public fun indexOf(element: E): Int {
+ forEachIndexed { i, item ->
+ if (element == item) {
+ return i
+ }
+ }
+ return -1
+ }
+
+ /**
+ * Returns the index if the first element in the [ObjectList] for which [predicate]
+ * returns `true` or -1 if there was no element for which predicate returned `true`.
+ */
+ public inline fun indexOfFirst(predicate: (element: E) -> Boolean): Int {
+ contract { callsInPlace(predicate) }
+ forEachIndexed { i, element ->
+ if (predicate(element)) {
+ return i
+ }
+ }
+ return -1
+ }
+
+ /**
+ * Returns the index if the last element in the [ObjectList] for which [predicate]
+ * returns `true` or -1 if there was no element for which predicate returned `true`.
+ */
+ public inline fun indexOfLast(predicate: (element: E) -> Boolean): Int {
+ contract { callsInPlace(predicate) }
+ forEachReversedIndexed { i, element ->
+ if (predicate(element)) {
+ return i
+ }
+ }
+ return -1
+ }
+
+ /**
+ * Returns `true` if the [ObjectList] has no elements in it or `false` otherwise.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if there are elements in the [ObjectList] or `false` if it is empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the last element in the [ObjectList] or throws a [NoSuchElementException] if
+ * it [isEmpty].
+ */
+ public fun last(): E {
+ if (isEmpty()) {
+ throw NoSuchElementException("ObjectList is empty.")
+ }
+ return content[lastIndex] as E
+ }
+
+ /**
+ * Returns the last element in the [ObjectList] for which [predicate] returns `true` or
+ * throws [NoSuchElementException] if nothing matches.
+ * @see indexOfLast
+ * @see lastOrNull
+ */
+ public inline fun last(predicate: (element: E) -> Boolean): E {
+ contract { callsInPlace(predicate) }
+ forEachReversed { element ->
+ if (predicate(element)) {
+ return element
+ }
+ }
+ throw NoSuchElementException("ObjectList contains no element matching the predicate.")
+ }
+
+ /**
+ * Returns the last element in the [ObjectList] or `null` if it [isEmpty].
+ */
+ public inline fun lastOrNull(): E? = if (isEmpty()) null else content[lastIndex] as E
+
+ /**
+ * Returns the last element in the [ObjectList] for which [predicate] returns `true` or
+ * `null` if nothing matches.
+ * @see indexOfLast
+ */
+ public inline fun lastOrNull(predicate: (element: E) -> Boolean): E? {
+ contract { callsInPlace(predicate) }
+ forEachReversed { element ->
+ if (predicate(element)) {
+ return element
+ }
+ }
+ return null
+ }
+
+ /**
+ * Returns the index of the last element in the [ObjectList] that is the same as
+ * [element] or `-1` if no elements match.
+ */
+ public fun lastIndexOf(element: E): Int {
+ forEachReversedIndexed { i, item ->
+ if (element == item) {
+ return i
+ }
+ }
+ return -1
+ }
+
+ /**
+ * Returns a [List] view into the [ObjectList]. All access to the collection will be
+ * less efficient and abides by the allocation requirements of the [List]. For example,
+ * [List.forEach] will allocate an iterator. All access will go through the more expensive
+ * interface calls. Critical performance areas should use the [ObjectList] API rather than
+ * [List] API, when possible.
+ */
+ public abstract fun asList(): List<E>
+
+ /**
+ * Returns a hash code based on the contents of the [ObjectList].
+ */
+ override fun hashCode(): Int {
+ var hashCode = 0
+ forEach { element ->
+ hashCode += 31 * element.hashCode()
+ }
+ return hashCode
+ }
+
+ /**
+ * Returns `true` if [other] is a [ObjectList] and the contents of this and [other] are the
+ * same.
+ */
+ override fun equals(other: Any?): Boolean {
+ if (other !is ObjectList<*> || other._size != _size) {
+ return false
+ }
+ val content = content
+ val otherContent = other.content
+ for (i in indices) {
+ if (content[i] != otherContent[i]) {
+ return false
+ }
+ }
+ return true
+ }
+
+ /**
+ * Returns a String representation of the list, surrounded by "[]" and each element
+ * separated by ", ".
+ */
+ override fun toString(): String {
+ if (isEmpty()) {
+ return "[]"
+ }
+ val last = lastIndex
+ return buildString {
+ append('[')
+ val content = content
+ for (i in 0 until last) {
+ append(content[i])
+ append(',')
+ append(' ')
+ }
+ append(content[last])
+ append(']')
+ }
+ }
+}
+
+/**
+ * [MutableObjectList] is a [MutableList]-like collection for reference types. It is optimized
+ * for fast access, avoiding virtual and interface method access. Methods avoid allocation
+ * whenever possible. For example [forEach] does not need allocate an [Iterator].
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the list (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. It is also not safe to mutate during reentrancy --
+ * in the middle of a [forEach], for example. However, concurrent reads are safe.
+ *
+ * **Note** [List] access is available through [asList] when developers need access to the
+ * common API.
+
+ * **Note** [MutableList] access is available through [asMutableList] when developers need
+ * access to the common API.
+ *
+ * @see ObjectList
+ * @see MutableFloatList
+ * @see MutableIntList
+ * @eee MutableLongList
+ */
+public class MutableObjectList<E>(
+ initialCapacity: Int = 16
+) : ObjectList<E>(initialCapacity) {
+ private var list: ObjectListMutableList<E>? = null
+
+ /**
+ * Returns the total number of elements that can be held before the [MutableObjectList] must
+ * grow.
+ *
+ * @see ensureCapacity
+ */
+ public inline val capacity: Int
+ get() = content.size
+
+ /**
+ * Adds [element] to the [MutableObjectList] and returns `true`.
+ */
+ public fun add(element: E): Boolean {
+ ensureCapacity(_size + 1)
+ content[_size] = element
+ _size++
+ return true
+ }
+
+ /**
+ * Adds [element] to the [MutableObjectList] at the given [index], shifting over any
+ * elements at [index] and after, if any.
+ * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+ */
+ public fun add(@androidx.annotation.IntRange(from = 0) index: Int, element: E) {
+ if (index !in 0.._size) {
+ throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+ }
+ ensureCapacity(_size + 1)
+ val content = content
+ if (index != _size) {
+ content.copyInto(
+ destination = content,
+ destinationOffset = index + 1,
+ startIndex = index,
+ endIndex = _size
+ )
+ }
+ content[index] = element
+ _size++
+ }
+
+ /**
+ * Adds all [elements] to the [MutableObjectList] at the given [index], shifting over any
+ * elements at [index] and after, if any.
+ * @return `true` if the [MutableObjectList] was changed or `false` if [elements] was empty
+ * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive.
+ */
+ public fun addAll(
+ @androidx.annotation.IntRange(from = 0) index: Int,
+ @Suppress("ArrayReturn") elements: Array<E>
+ ): Boolean {
+ if (index !in 0.._size) {
+ throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+ }
+ if (elements.isEmpty()) return false
+ ensureCapacity(_size + elements.size)
+ val content = content
+ if (index != _size) {
+ content.copyInto(
+ destination = content,
+ destinationOffset = index + elements.size,
+ startIndex = index,
+ endIndex = _size
+ )
+ }
+ elements.copyInto(content, index)
+ _size += elements.size
+ return true
+ }
+
+ /**
+ * Adds all [elements] to the [MutableObjectList] at the given [index], shifting over any
+ * elements at [index] and after, if any.
+ * @return `true` if the [MutableObjectList] was changed or `false` if [elements] was empty
+ * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive.
+ */
+ public fun addAll(
+ @androidx.annotation.IntRange(from = 0) index: Int,
+ elements: Collection<E>
+ ): Boolean {
+ if (index !in 0.._size) {
+ throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+ }
+ if (elements.isEmpty()) return false
+ ensureCapacity(_size + elements.size)
+ val content = content
+ if (index != _size) {
+ content.copyInto(
+ destination = content,
+ destinationOffset = index + elements.size,
+ startIndex = index,
+ endIndex = _size
+ )
+ }
+ elements.forEachIndexed { i, element ->
+ content[index + i] = element
+ }
+ _size += elements.size
+ return true
+ }
+
+ /**
+ * Adds all [elements] to the [MutableObjectList] at the given [index], shifting over any
+ * elements at [index] and after, if any.
+ * @return `true` if the [MutableObjectList] was changed or `false` if [elements] was empty
+ * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+ */
+ public fun addAll(
+ @androidx.annotation.IntRange(from = 0) index: Int,
+ elements: ObjectList<E>
+ ): Boolean {
+ if (index !in 0.._size) {
+ throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+ }
+ if (elements.isEmpty()) return false
+ ensureCapacity(_size + elements._size)
+ val content = content
+ if (index != _size) {
+ content.copyInto(
+ destination = content,
+ destinationOffset = index + elements._size,
+ startIndex = index,
+ endIndex = _size
+ )
+ }
+ elements.content.copyInto(
+ destination = content,
+ destinationOffset = index,
+ startIndex = 0,
+ endIndex = elements._size
+ )
+ _size += elements._size
+ return true
+ }
+
+ /**
+ * Adds all [elements] to the end of the [MutableObjectList] and returns `true` if the
+ * [MutableObjectList] was changed or `false` if [elements] was empty.
+ */
+ public fun addAll(elements: ObjectList<E>): Boolean {
+ val oldSize = _size
+ plusAssign(elements)
+ return oldSize != _size
+ }
+
+ /**
+ * Adds all [elements] to the end of the [MutableObjectList] and returns `true` if the
+ * [MutableObjectList] was changed or `false` if [elements] was empty.
+ */
+ public fun addAll(elements: ScatterSet<E>): Boolean {
+ val oldSize = _size
+ plusAssign(elements)
+ return oldSize != _size
+ }
+
+ /**
+ * Adds all [elements] to the end of the [MutableObjectList] and returns `true` if the
+ * [MutableObjectList] was changed or `false` if [elements] was empty.
+ */
+ public fun addAll(@Suppress("ArrayReturn") elements: Array<E>): Boolean {
+ val oldSize = _size
+ plusAssign(elements)
+ return oldSize != _size
+ }
+
+ /**
+ * Adds all [elements] to the end of the [MutableObjectList] and returns `true` if the
+ * [MutableObjectList] was changed or `false` if [elements] was empty.
+ */
+ public fun addAll(elements: List<E>): Boolean {
+ val oldSize = _size
+ plusAssign(elements)
+ return oldSize != _size
+ }
+
+ /**
+ * Adds all [elements] to the end of the [MutableObjectList] and returns `true` if the
+ * [MutableObjectList] was changed or `false` if [elements] was empty.
+ */
+ public fun addAll(elements: Iterable<E>): Boolean {
+ val oldSize = _size
+ plusAssign(elements)
+ return oldSize != _size
+ }
+
+ /**
+ * Adds all [elements] to the end of the [MutableObjectList] and returns `true` if the
+ * [MutableObjectList] was changed or `false` if [elements] was empty.
+ */
+ public fun addAll(elements: Sequence<E>): Boolean {
+ val oldSize = _size
+ plusAssign(elements)
+ return oldSize != _size
+ }
+
+ /**
+ * Adds all [elements] to the end of the [MutableObjectList].
+ */
+ public operator fun plusAssign(elements: ObjectList<E>) {
+ if (elements.isEmpty()) return
+ ensureCapacity(_size + elements._size)
+ val content = content
+ elements.content.copyInto(
+ destination = content,
+ destinationOffset = _size,
+ startIndex = 0,
+ endIndex = elements._size
+ )
+ _size += elements._size
+ }
+
+ /**
+ * Adds all [elements] to the end of the [MutableObjectList].
+ */
+ public operator fun plusAssign(elements: ScatterSet<E>) {
+ if (elements.isEmpty()) return
+ ensureCapacity(_size + elements.size)
+ elements.forEach { element ->
+ plusAssign(element)
+ }
+ }
+
+ /**
+ * Adds all [elements] to the end of the [MutableObjectList].
+ */
+ public operator fun plusAssign(@Suppress("ArrayReturn") elements: Array<E>) {
+ if (elements.isEmpty()) return
+ ensureCapacity(_size + elements.size)
+ val content = content
+ elements.copyInto(content, _size)
+ _size += elements.size
+ }
+
+ /**
+ * Adds all [elements] to the end of the [MutableObjectList].
+ */
+ public operator fun plusAssign(elements: List<E>) {
+ if (elements.isEmpty()) return
+ val size = _size
+ ensureCapacity(size + elements.size)
+ val content = content
+ for (i in elements.indices) {
+ content[i + size] = elements[i]
+ }
+ _size += elements.size
+ }
+
+ /**
+ * Adds all [elements] to the end of the [MutableObjectList].
+ */
+ public operator fun plusAssign(elements: Iterable<E>) {
+ elements.forEach { element ->
+ plusAssign(element)
+ }
+ }
+
+ /**
+ * Adds all [elements] to the end of the [MutableObjectList].
+ */
+ public operator fun plusAssign(elements: Sequence<E>) {
+ elements.forEach { element ->
+ plusAssign(element)
+ }
+ }
+
+ /**
+ * Removes all elements in the [MutableObjectList]. The storage isn't released.
+ * @see trim
+ */
+ public fun clear() {
+ content.fill(null, fromIndex = 0, toIndex = _size)
+ _size = 0
+ }
+
+ /**
+ * Reduces the internal storage. If [capacity] is greater than [minCapacity] and [size], the
+ * internal storage is reduced to the maximum of [size] and [minCapacity].
+ * @see ensureCapacity
+ */
+ public fun trim(minCapacity: Int = _size) {
+ val minSize = maxOf(minCapacity, _size)
+ if (capacity > minSize) {
+ content = content.copyOf(minSize)
+ }
+ }
+
+ /**
+ * Ensures that there is enough space to store [capacity] elements in the [MutableObjectList].
+ * @see trim
+ */
+ public fun ensureCapacity(capacity: Int) {
+ val oldContent = content
+ if (oldContent.size < capacity) {
+ val newSize = maxOf(capacity, oldContent.size * 3 / 2)
+ content = oldContent.copyOf(newSize)
+ }
+ }
+
+ /**
+ * [add] [element] to the [MutableObjectList].
+ */
+ public inline operator fun plusAssign(element: E) {
+ add(element)
+ }
+
+ /**
+ * [remove] [element] from the [MutableObjectList]
+ */
+ public inline operator fun minusAssign(element: E) {
+ remove(element)
+ }
+
+ /**
+ * Removes [element] from the [MutableObjectList]. If [element] was in the [MutableObjectList]
+ * and was removed, `true` will be returned, or `false` will be returned if the element
+ * was not found.
+ */
+ public fun remove(element: E): Boolean {
+ val index = indexOf(element)
+ if (index >= 0) {
+ removeAt(index)
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Removes all elements in this list for which [predicate] returns `true`.
+ */
+ public inline fun removeIf(predicate: (element: E) -> Boolean) {
+ var gap = 0
+ val size = _size
+ val content = content
+ for (i in indices) {
+ content[i - gap] = content[i]
+ if (predicate(content[i] as E)) {
+ gap++
+ }
+ }
+ content.fill(null, fromIndex = size - gap, toIndex = size)
+ _size -= gap
+ }
+
+ /**
+ * Removes all [elements] from the [MutableObjectList] and returns `true` if anything was removed.
+ */
+ public fun removeAll(@Suppress("ArrayReturn") elements: Array<E>): Boolean {
+ val initialSize = _size
+ for (i in elements.indices) {
+ remove(elements[i])
+ }
+ return initialSize != _size
+ }
+
+ /**
+ * Removes all [elements] from the [MutableObjectList] and returns `true` if anything was removed.
+ */
+ public fun removeAll(elements: ObjectList<E>): Boolean {
+ val initialSize = _size
+ minusAssign(elements)
+ return initialSize != _size
+ }
+
+ /**
+ * Removes all [elements] from the [MutableObjectList] and returns `true` if anything was removed.
+ */
+ public fun removeAll(elements: ScatterSet<E>): Boolean {
+ val initialSize = _size
+ minusAssign(elements)
+ return initialSize != _size
+ }
+
+ /**
+ * Removes all [elements] from the [MutableObjectList] and returns `true` if anything was removed.
+ */
+ public fun removeAll(elements: List<E>): Boolean {
+ val initialSize = _size
+ minusAssign(elements)
+ return initialSize != _size
+ }
+
+ /**
+ * Removes all [elements] from the [MutableObjectList] and returns `true` if anything was removed.
+ */
+ public fun removeAll(elements: Iterable<E>): Boolean {
+ val initialSize = _size
+ minusAssign(elements)
+ return initialSize != _size
+ }
+
+ /**
+ * Removes all [elements] from the [MutableObjectList] and returns `true` if anything was removed.
+ */
+ public fun removeAll(elements: Sequence<E>): Boolean {
+ val initialSize = _size
+ minusAssign(elements)
+ return initialSize != _size
+ }
+
+ /**
+ * Removes all [elements] from the [MutableObjectList].
+ */
+ public operator fun minusAssign(@Suppress("ArrayReturn") elements: Array<E>) {
+ elements.forEach { element ->
+ minusAssign(element)
+ }
+ }
+
+ /**
+ * Removes all [elements] from the [MutableObjectList].
+ */
+ public operator fun minusAssign(elements: ObjectList<E>) {
+ elements.forEach { element ->
+ minusAssign(element)
+ }
+ }
+
+ /**
+ * Removes all [elements] from the [MutableObjectList].
+ */
+ public operator fun minusAssign(elements: ScatterSet<E>) {
+ elements.forEach { element ->
+ minusAssign(element)
+ }
+ }
+
+ /**
+ * Removes all [elements] from the [MutableObjectList].
+ */
+ public operator fun minusAssign(elements: List<E>) {
+ for (i in elements.indices) {
+ minusAssign(elements[i])
+ }
+ }
+
+ /**
+ * Removes all [elements] from the [MutableObjectList].
+ */
+ public operator fun minusAssign(elements: Iterable<E>) {
+ elements.forEach { element ->
+ minusAssign(element)
+ }
+ }
+
+ /**
+ * Removes all [elements] from the [MutableObjectList].
+ */
+ public operator fun minusAssign(elements: Sequence<E>) {
+ elements.forEach { element ->
+ minusAssign(element)
+ }
+ }
+
+ /**
+ * Removes the element at the given [index] and returns it.
+ * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
+ */
+ public fun removeAt(@androidx.annotation.IntRange(from = 0) index: Int): E {
+ if (index !in 0 until _size) {
+ throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ }
+ val content = content
+ val element = content[index]
+ if (index != lastIndex) {
+ content.copyInto(
+ destination = content,
+ destinationOffset = index,
+ startIndex = index + 1,
+ endIndex = _size
+ )
+ }
+ _size--
+ content[_size] = null
+ return element as E
+ }
+
+ /**
+ * Removes elements from index [start] (inclusive) to [end] (exclusive).
+ * @throws IndexOutOfBoundsException if [start] or [end] isn't between 0 and [size], inclusive
+ * @throws IllegalArgumentException if [start] is greater than [end]
+ */
+ public fun removeRange(
+ @androidx.annotation.IntRange(from = 0) start: Int,
+ @androidx.annotation.IntRange(from = 0) end: Int
+ ) {
+ if (start !in 0.._size || end !in 0.._size) {
+ throw IndexOutOfBoundsException("Start ($start) and end ($end) must be in 0..$_size")
+ }
+ if (end < start) {
+ throw IllegalArgumentException("Start ($start) is more than end ($end)")
+ }
+ if (end != start) {
+ if (end < _size) {
+ content.copyInto(
+ destination = content,
+ destinationOffset = start,
+ startIndex = end,
+ endIndex = _size
+ )
+ }
+ val newSize = _size - (end - start)
+ content.fill(null, fromIndex = newSize, toIndex = _size)
+ _size = newSize
+ }
+ }
+
+ /**
+ * Keeps only [elements] in the [MutableObjectList] and removes all other values.
+ * @return `true` if the [MutableObjectList] has changed.
+ */
+ public fun retainAll(@Suppress("ArrayReturn") elements: Array<E>): Boolean {
+ val initialSize = _size
+ val content = content
+ for (i in lastIndex downTo 0) {
+ val element = content[i]
+ if (elements.indexOfFirst { it == element } < 0) {
+ removeAt(i)
+ }
+ }
+ return initialSize != _size
+ }
+
+ /**
+ * Keeps only [elements] in the [MutableObjectList] and removes all other values.
+ * @return `true` if the [MutableObjectList] has changed.
+ */
+ public fun retainAll(elements: ObjectList<E>): Boolean {
+ val initialSize = _size
+ val content = content
+ for (i in lastIndex downTo 0) {
+ val element = content[i] as E
+ if (element !in elements) {
+ removeAt(i)
+ }
+ }
+ return initialSize != _size
+ }
+
+ /**
+ * Keeps only [elements] in the [MutableObjectList] and removes all other values.
+ * @return `true` if the [MutableObjectList] has changed.
+ */
+ public fun retainAll(elements: Collection<E>): Boolean {
+ val initialSize = _size
+ val content = content
+ for (i in lastIndex downTo 0) {
+ val element = content[i] as E
+ if (element !in elements) {
+ removeAt(i)
+ }
+ }
+ return initialSize != _size
+ }
+
+ /**
+ * Keeps only [elements] in the [MutableObjectList] and removes all other values.
+ * @return `true` if the [MutableObjectList] has changed.
+ */
+ public fun retainAll(elements: Iterable<E>): Boolean {
+ val initialSize = _size
+ val content = content
+ for (i in lastIndex downTo 0) {
+ val element = content[i] as E
+ if (element !in elements) {
+ removeAt(i)
+ }
+ }
+ return initialSize != _size
+ }
+
+ /**
+ * Keeps only [elements] in the [MutableObjectList] and removes all other values.
+ * @return `true` if the [MutableObjectList] has changed.
+ */
+ public fun retainAll(elements: Sequence<E>): Boolean {
+ val initialSize = _size
+ val content = content
+ for (i in lastIndex downTo 0) {
+ val element = content[i] as E
+ if (element !in elements) {
+ removeAt(i)
+ }
+ }
+ return initialSize != _size
+ }
+
+ /**
+ * Sets the value at [index] to [element].
+ * @return the previous value set at [index]
+ * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
+ */
+ public operator fun set(
+ @androidx.annotation.IntRange(from = 0) index: Int,
+ element: E
+ ): E {
+ if (index !in 0 until _size) {
+ throw IndexOutOfBoundsException("set index $index must be between 0 .. $lastIndex")
+ }
+ val content = content
+ val old = content[index]
+ content[index] = element
+ return old as E
+ }
+
+ override fun asList(): List<E> = asMutableList()
+
+ /**
+ * Returns a [MutableList] view into the [MutableObjectList]. All access to the collection
+ * will be less efficient and abides by the allocation requirements of the
+ * [MutableList]. For example, [MutableList.forEach] will allocate an iterator.
+ * All access will go through the more expensive interface calls. Critical performance
+ * areas should use the [MutableObjectList] API rather than [MutableList] API, when possible.
+ */
+ public fun asMutableList(): MutableList<E> = list ?: ObjectListMutableList(this).also {
+ list = it
+ }
+
+ private class MutableObjectListIterator<T>(
+ private val list: MutableList<T>,
+ private var index: Int
+ ) : MutableListIterator<T> {
+ override fun hasNext(): Boolean {
+ return index < list.size
+ }
+
+ override fun next(): T {
+ return list[index++]
+ }
+
+ override fun remove() {
+ index--
+ list.removeAt(index)
+ }
+
+ override fun hasPrevious(): Boolean {
+ return index > 0
+ }
+
+ override fun nextIndex(): Int {
+ return index
+ }
+
+ override fun previous(): T {
+ index--
+ return list[index]
+ }
+
+ override fun previousIndex(): Int {
+ return index - 1
+ }
+
+ override fun add(element: T) {
+ list.add(index, element)
+ index++
+ }
+
+ override fun set(element: T) {
+ list[index] = element
+ }
+ }
+
+ /**
+ * [MutableList] implementation for a [MutableObjectList], used in [asMutableList].
+ */
+ private class ObjectListMutableList<T>(
+ private val objectList: MutableObjectList<T>
+ ) : MutableList<T> {
+ override val size: Int
+ get() = objectList.size
+
+ override fun contains(element: T): Boolean = objectList.contains(element)
+
+ override fun containsAll(elements: Collection<T>): Boolean =
+ objectList.containsAll(elements)
+
+ override fun get(index: Int): T {
+ checkIndex(index)
+ return objectList[index]
+ }
+
+ override fun indexOf(element: T): Int = objectList.indexOf(element)
+
+ override fun isEmpty(): Boolean = objectList.isEmpty()
+
+ override fun iterator(): MutableIterator<T> = MutableObjectListIterator(this, 0)
+
+ override fun lastIndexOf(element: T): Int = objectList.lastIndexOf(element)
+
+ override fun add(element: T): Boolean = objectList.add(element)
+
+ override fun add(index: Int, element: T) = objectList.add(index, element)
+
+ override fun addAll(index: Int, elements: Collection<T>): Boolean =
+ objectList.addAll(index, elements)
+
+ override fun addAll(elements: Collection<T>): Boolean = objectList.addAll(elements)
+
+ override fun clear() = objectList.clear()
+
+ override fun listIterator(): MutableListIterator<T> = MutableObjectListIterator(this, 0)
+
+ override fun listIterator(index: Int): MutableListIterator<T> =
+ MutableObjectListIterator(this, index)
+
+ override fun remove(element: T): Boolean = objectList.remove(element)
+
+ override fun removeAll(elements: Collection<T>): Boolean = objectList.removeAll(elements)
+
+ override fun removeAt(index: Int): T {
+ checkIndex(index)
+ return objectList.removeAt(index)
+ }
+
+ override fun retainAll(elements: Collection<T>): Boolean = objectList.retainAll(elements)
+
+ override fun set(index: Int, element: T): T {
+ checkIndex(index)
+ return objectList.set(index, element)
+ }
+
+ override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> {
+ checkSubIndex(fromIndex, toIndex)
+ return SubList(this, fromIndex, toIndex)
+ }
+ }
+
+ /**
+ * A view into an underlying [MutableList] that directly accesses the underlying [MutableList].
+ * This is important for the implementation of [List.subList]. A change to the [SubList]
+ * also changes the referenced [MutableList].
+ */
+ private class SubList<T>(
+ private val list: MutableList<T>,
+ private val start: Int,
+ private var end: Int
+ ) : MutableList<T> {
+ override val size: Int
+ get() = end - start
+
+ override fun contains(element: T): Boolean {
+ for (i in start until end) {
+ if (list[i] == element) {
+ return true
+ }
+ }
+ return false
+ }
+
+ override fun containsAll(elements: Collection<T>): Boolean {
+ elements.forEach {
+ if (!contains(it)) {
+ return false
+ }
+ }
+ return true
+ }
+
+ override fun get(index: Int): T {
+ checkIndex(index)
+ return list[index + start]
+ }
+
+ override fun indexOf(element: T): Int {
+ for (i in start until end) {
+ if (list[i] == element) {
+ return i - start
+ }
+ }
+ return -1
+ }
+
+ override fun isEmpty(): Boolean = end == start
+
+ override fun iterator(): MutableIterator<T> = MutableObjectListIterator(this, 0)
+
+ override fun lastIndexOf(element: T): Int {
+ for (i in end - 1 downTo start) {
+ if (list[i] == element) {
+ return i - start
+ }
+ }
+ return -1
+ }
+
+ override fun add(element: T): Boolean {
+ list.add(end++, element)
+ return true
+ }
+
+ override fun add(index: Int, element: T) {
+ list.add(index + start, element)
+ end++
+ }
+
+ override fun addAll(index: Int, elements: Collection<T>): Boolean {
+ list.addAll(index + start, elements)
+ end += elements.size
+ return elements.size > 0
+ }
+
+ override fun addAll(elements: Collection<T>): Boolean {
+ list.addAll(end, elements)
+ end += elements.size
+ return elements.size > 0
+ }
+
+ override fun clear() {
+ for (i in end - 1 downTo start) {
+ list.removeAt(i)
+ }
+ end = start
+ }
+
+ override fun listIterator(): MutableListIterator<T> = MutableObjectListIterator(this, 0)
+
+ override fun listIterator(index: Int): MutableListIterator<T> =
+ MutableObjectListIterator(this, index)
+
+ override fun remove(element: T): Boolean {
+ for (i in start until end) {
+ if (list[i] == element) {
+ list.removeAt(i)
+ end--
+ return true
+ }
+ }
+ return false
+ }
+
+ override fun removeAll(elements: Collection<T>): Boolean {
+ val originalEnd = end
+ elements.forEach {
+ remove(it)
+ }
+ return originalEnd != end
+ }
+
+ override fun removeAt(index: Int): T {
+ checkIndex(index)
+ val element = list.removeAt(index + start)
+ end--
+ return element
+ }
+
+ override fun retainAll(elements: Collection<T>): Boolean {
+ val originalEnd = end
+ for (i in end - 1 downTo start) {
+ val element = list[i]
+ if (element !in elements) {
+ list.removeAt(i)
+ end--
+ }
+ }
+ return originalEnd != end
+ }
+
+ override fun set(index: Int, element: T): T {
+ checkIndex(index)
+ return list.set(index + start, element)
+ }
+
+ override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> {
+ checkSubIndex(fromIndex, toIndex)
+ return SubList(this, fromIndex, toIndex)
+ }
+ }
+}
+
+private fun List<*>.checkIndex(index: Int) {
+ val size = size
+ if (index < 0 || index >= size) {
+ throw IndexOutOfBoundsException("Index $index is out of bounds. " +
+ "The list has $size elements.")
+ }
+}
+
+private fun List<*>.checkSubIndex(fromIndex: Int, toIndex: Int) {
+ val size = size
+ if (fromIndex > toIndex) {
+ throw IllegalArgumentException("Indices are out of order. fromIndex ($fromIndex) is " +
+ "greater than toIndex ($toIndex).")
+ }
+ if (fromIndex < 0) {
+ throw IndexOutOfBoundsException("fromIndex ($fromIndex) is less than 0.")
+ }
+ if (toIndex > size) {
+ throw IndexOutOfBoundsException(
+ "toIndex ($toIndex) is more than than the list size ($size)"
+ )
+ }
+}
+
+// Empty array used when nothing is allocated
+private val EmptyArray = arrayOfNulls<Any>(0)
+
+private val EmptyObjectList: ObjectList<Any?> = MutableObjectList(0)
+
+/**
+ * @return a read-only [ObjectList] with nothing in it.
+ */
+public fun <E> emptyObjectList(): ObjectList<E> = EmptyObjectList as ObjectList<E>
+
+/**
+ * @return a read-only [ObjectList] with nothing in it.
+ */
+public fun <E> objectListOf(): ObjectList<E> = EmptyObjectList as ObjectList<E>
+
+/**
+ * @return a new read-only [ObjectList] with [element1] as the only element in the list.
+ */
+public fun <E> objectListOf(element1: E): ObjectList<E> = mutableObjectListOf(element1)
+
+/**
+ * @return a new read-only [ObjectList] with 2 elements, [element1] and [element2], in order.
+ */
+public fun <E> objectListOf(element1: E, element2: E): ObjectList<E> =
+ mutableObjectListOf(element1, element2)
+
+/**
+ * @return a new read-only [ObjectList] with 3 elements, [element1], [element2], and [element3],
+ * in order.
+ */
+public fun <E> objectListOf(element1: E, element2: E, element3: E): ObjectList<E> =
+ mutableObjectListOf(element1, element2, element3)
+
+/**
+ * @return a new read-only [ObjectList] with [elements] in order.
+ */
+public fun <E> objectListOf(vararg elements: E): ObjectList<E> =
+ MutableObjectList<E>(elements.size).apply { plusAssign(elements as Array<E>) }
+
+/**
+ * @return a new empty [MutableObjectList] with the default capacity.
+ */
+public inline fun <E> mutableObjectListOf(): MutableObjectList<E> = MutableObjectList()
+
+/**
+ * @return a new [MutableObjectList] with [element1] as the only element in the list.
+ */
+public fun <E> mutableObjectListOf(element1: E): MutableObjectList<E> {
+ val list = MutableObjectList<E>(1)
+ list += element1
+ return list
+}
+
+/**
+ * @return a new [MutableObjectList] with 2 elements, [element1] and [element2], in order.
+ */
+public fun <E> mutableObjectListOf(element1: E, element2: E): MutableObjectList<E> {
+ val list = MutableObjectList<E>(2)
+ list += element1
+ list += element2
+ return list
+}
+
+/**
+ * @return a new [MutableObjectList] with 3 elements, [element1], [element2], and [element3],
+ * in order.
+ */
+public fun <E> mutableObjectListOf(element1: E, element2: E, element3: E): MutableObjectList<E> {
+ val list = MutableObjectList<E>(3)
+ list += element1
+ list += element2
+ list += element3
+ return list
+}
+
+/**
+ * @return a new [MutableObjectList] with the given elements, in order.
+ */
+public inline fun <E> mutableObjectListOf(vararg elements: E): MutableObjectList<E> =
+ MutableObjectList<E>(elements.size).apply { plusAssign(elements as Array<E>) }
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt
new file mode 100644
index 0000000..092855b
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt
@@ -0,0 +1,855 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyObjectLongMap = MutableObjectLongMap<Any?>(0)
+
+/**
+ * Returns an empty, read-only [ObjectLongMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> emptyObjectLongMap(): ObjectLongMap<K> =
+ EmptyObjectLongMap as ObjectLongMap<K>
+
+/**
+ * Returns an empty, read-only [ObjectLongMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> objectLongMap(): ObjectLongMap<K> =
+ EmptyObjectLongMap as ObjectLongMap<K>
+
+/**
+ * Returns a new [MutableObjectLongMap].
+ */
+public fun <K> mutableObjectLongMapOf(): MutableObjectLongMap<K> = MutableObjectLongMap()
+
+/**
+ * Returns a new [MutableObjectLongMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Long] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <K> objectLongMapOf(vararg pairs: Pair<K, Long>): ObjectLongMap<K> =
+ MutableObjectLongMap<K>(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * Returns a new [MutableObjectLongMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Long] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <K> mutableObjectLongMapOf(vararg pairs: Pair<K, Long>): MutableObjectLongMap<K> =
+ MutableObjectLongMap<K>(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * [ObjectLongMap] is a container with a [Map]-like interface for keys with
+ * reference types and [Long] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableObjectLongMap].
+ *
+ * @see [MutableObjectLongMap]
+ * @see ScatterMap
+ */
+public sealed class ObjectLongMap<K> {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` entries, including when
+ // the table is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var keys: Array<Any?> = EMPTY_OBJECTS
+
+ @PublishedApi
+ @JvmField
+ internal var values: LongArray = EmptyLongArray
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of key-value pairs that can be stored in this map
+ * without requiring internal storage reallocation.
+ */
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of key-value pairs in this map.
+ */
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this map has at least one entry.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this map has no entries.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this map is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this map is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the value corresponding to the given [key], or `null` if such
+ * a key is not present in the map.
+ * @throws NoSuchElementException when [key] is not found
+ */
+ public operator fun get(key: K): Long {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ throw NoSuchElementException("There is no key $key in the map")
+ }
+ return values[index]
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * or [defaultValue] if this map contains no mapping for the key.
+ */
+ public fun getOrDefault(key: K, defaultValue: Long): Long {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ return defaultValue
+ }
+
+ /**
+ * Returns the value for the given [key] if the value is present
+ * and not null. Otherwise, returns the result of the [defaultValue]
+ * function.
+ */
+ public inline fun getOrElse(key: K, defaultValue: () -> Long): Long {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ return defaultValue()
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ public inline fun forEach(block: (key: K, value: Long) -> Unit) {
+ val k = keys
+ val v = values
+
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ block(k[index] as K, v[index])
+ }
+ }
+
+ /**
+ * Iterates over every key stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachKey(block: (key: K) -> Unit) {
+ val k = keys
+
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ block(k[index] as K)
+ }
+ }
+
+ /**
+ * Iterates over every value stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachValue(block: (value: Long) -> Unit) {
+ val v = values
+
+ forEachIndexed { index ->
+ block(v[index])
+ }
+ }
+
+ /**
+ * Returns true if all entries match the given [predicate].
+ */
+ public inline fun all(predicate: (K, Long) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (!predicate(key, value)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one entry matches the given [predicate].
+ */
+ public inline fun any(predicate: (K, Long) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (predicate(key, value)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of entries in this map.
+ */
+ public fun count(): Int = size
+
+ /**
+ * Returns the number of entries matching the given [predicate].
+ */
+ public inline fun count(predicate: (K, Long) -> Boolean): Int {
+ var count = 0
+ forEach { key, value ->
+ if (predicate(key, value)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public operator fun contains(key: K): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsKey(key: K): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [value] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsValue(value: Long): Boolean {
+ forEachValue { v ->
+ if (value == v) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the hash code value for this map. The hash code the sum of the hash
+ * codes of each key/value pair.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { key, value ->
+ hash += key.hashCode() xor value.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this hash map for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [ObjectLongMap]
+ * - Has the same [size] as this map
+ * - Contains key/value pairs equal to this map's pair
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is ObjectLongMap<*>) {
+ return false
+ }
+ if (other.size != size) {
+ return false
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ val o = other as ObjectLongMap<Any?>
+
+ forEach { key, value ->
+ if (value != o[key]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this map. The map is denoted in the
+ * string by the `{}`. Each key/value pair present in the map is represented
+ * inside '{}` by a substring of the form `key=value`, and pairs are
+ * separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ val s = StringBuilder().append('{')
+ var i = 0
+ forEach { key, value ->
+ s.append(if (key === this) "(this)" else key)
+ s.append("=")
+ s.append(value)
+ i++
+ if (i < _size) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append('}').toString()
+ }
+
+ /**
+ * Scans the hash table to find the index in the backing arrays of the
+ * specified [key]. Returns -1 if the key is not present.
+ */
+ @PublishedApi
+ internal fun findKeyIndex(key: K): Int {
+ val hash = hash(key)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutableObjectLongMap] is a container with a [MutableMap]-like interface for keys with
+ * reference types and [Long] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableObjectLongMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableObjectLongMap<K>(
+ initialCapacity: Int = DefaultScatterCapacity
+) : ObjectLongMap<K>() {
+ // Number of entries we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ keys = arrayOfNulls(newCapacity)
+ values = LongArray(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * if the value is present in the map and not `null`. Otherwise,
+ * calls `defaultValue()` and puts the result in the map associated
+ * with [key].
+ */
+ public inline fun getOrPut(key: K, defaultValue: () -> Long): Long {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ val value = defaultValue()
+ set(key, value)
+ return value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public operator fun set(key: K, value: Long) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public fun put(key: K, value: Long) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Long] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+ public fun putAll(@Suppress("ArrayReturn") pairs: Array<out Pair<K, Long>>) {
+ for ((key, value) in pairs) {
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public fun putAll(from: ObjectLongMap<K>) {
+ from.forEach { key, value ->
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts the key/value mapping from the [pair] in this map, using the first
+ * element as the key, and the second element as the value.
+ *
+ * Note that the [Pair] is allocated and the [Long] value is boxed.
+ * Use [set] instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(pair: Pair<K, Long>) {
+ this[pair.first] = pair.second
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Long] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+ public inline operator fun plusAssign(
+ @Suppress("ArrayReturn") pairs: Array<out Pair<K, Long>>
+ ): Unit = putAll(pairs)
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public inline operator fun plusAssign(from: ObjectLongMap<K>): Unit = putAll(from)
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public fun remove(key: K) {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ removeValueAt(index)
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map if the
+ * associated value equals [value]. Returns whether the removal happened.
+ */
+ public fun remove(key: K, value: Long): Boolean {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ if (values[index] == value) {
+ removeValueAt(index)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes any mapping for which the specified [predicate] returns true.
+ */
+ public fun removeIf(predicate: (K, Long) -> Boolean) {
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ if (predicate(keys[index] as K, values[index])) {
+ removeValueAt(index)
+ }
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public inline operator fun minusAssign(key: K) {
+ remove(key)
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: Array<out K>) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: Iterable<K>) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: Sequence<K>) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: ScatterSet<K>) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ private fun removeValueAt(index: Int) {
+ _size -= 1
+
+ // TODO: We could just mark the entry as empty if there's a group
+ // window around this entry that was already empty
+ writeMetadata(index, Deleted)
+ keys[index] = null
+ }
+
+ /**
+ * Removes all mappings from this map.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ keys.fill(null, 0, _capacity)
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the hash table to find the index at which we can store a value
+ * for the give [key]. If the key already exists in the table, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the table is full.
+ */
+ private fun findAbsoluteInsertIndex(key: K): Int {
+ val hash = hash(key)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the table in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutableObjectLongMap]'s storage so it is sized appropriately
+ * to hold the current mappings.
+ *
+ * Returns the number of empty entries removed from this map's storage.
+ * Returns be 0 if no trimming is necessary or possible.
+ */
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted entries from the table to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_set`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousKeys = keys
+ val previousValues = values
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newKeys = keys
+ val newValues = values
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousKey = previousKeys[i]
+ val hash = hash(previousKey)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newKeys[index] = previousKey
+ newValues[index] = previousValues[i]
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatFloatMapTest.kt
new file mode 100644
index 0000000..fa56a25
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatFloatMapTest.kt
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class FloatFloatMapTest {
+ @Test
+ fun floatFloatMap() {
+ val map = MutableFloatFloatMap()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun testEmptyFloatFloatMap() {
+ val map = emptyFloatFloatMap()
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+
+ assertSame(emptyFloatFloatMap(), map)
+ }
+
+ @Test
+ fun floatFloatMapFunction() {
+ val map = mutableFloatFloatMapOf()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun zeroCapacityHashMap() {
+ val map = MutableFloatFloatMap(0)
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun floatFloatMapWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val map = MutableFloatFloatMap(1800)
+ assertEquals(4095, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun floatFloatMapPairsFunction() {
+ val map = mutableFloatFloatMapOf(
+ 1f to 1f,
+ 2f to 2f
+ )
+ assertEquals(2, map.size)
+ assertEquals(1f, map[1f])
+ assertEquals(2f, map[2f])
+ }
+
+ @Test
+ fun addToMap() {
+ val map = MutableFloatFloatMap()
+ map[1f] = 1f
+
+ assertEquals(1, map.size)
+ assertEquals(1f, map[1f])
+ }
+
+ @Test
+ fun addToSizedMap() {
+ val map = MutableFloatFloatMap(12)
+ map[1f] = 1f
+
+ assertEquals(1, map.size)
+ assertEquals(1f, map[1f])
+ }
+
+ @Test
+ fun addToSmallMap() {
+ val map = MutableFloatFloatMap(2)
+ map[1f] = 1f
+
+ assertEquals(1, map.size)
+ assertEquals(7, map.capacity)
+ assertEquals(1f, map[1f])
+ }
+
+ @Test
+ fun addToZeroCapacityMap() {
+ val map = MutableFloatFloatMap(0)
+ map[1f] = 1f
+
+ assertEquals(1, map.size)
+ assertEquals(1f, map[1f])
+ }
+
+ @Test
+ fun replaceExistingKey() {
+ val map = MutableFloatFloatMap()
+ map[1f] = 1f
+ map[1f] = 2f
+
+ assertEquals(1, map.size)
+ assertEquals(2f, map[1f])
+ }
+
+ @Test
+ fun put() {
+ val map = MutableFloatFloatMap()
+
+ map.put(1f, 1f)
+ assertEquals(1f, map[1f])
+ map.put(1f, 2f)
+ assertEquals(2f, map[1f])
+ }
+
+ @Test
+ fun putAllArray() {
+ val map = MutableFloatFloatMap()
+ map[1f] = 1f
+ map[2f] = 2f
+
+ map.putAll(arrayOf(3f to 3f, 7f to 7f))
+
+ assertEquals(4, map.size)
+ assertEquals(3f, map[3f])
+ assertEquals(7f, map[7f])
+ }
+
+ @Test
+ fun plus() {
+ val map = MutableFloatFloatMap()
+ map += 1f to 1f
+
+ assertEquals(1, map.size)
+ assertEquals(1f, map[1f])
+ }
+
+ @Test
+ fun plusArray() {
+ val map = MutableFloatFloatMap()
+ map += arrayOf(3f to 3f, 7f to 7f)
+
+ assertEquals(2, map.size)
+ assertEquals(3f, map[3f])
+ assertEquals(7f, map[7f])
+ }
+
+ @Test
+ fun findNonExistingKey() {
+ val map = MutableFloatFloatMap()
+ map[1f] = 1f
+
+ assertFailsWith<NoSuchElementException> {
+ map[2f]
+ }
+ }
+
+ @Test
+ fun getOrDefault() {
+ val map = MutableFloatFloatMap()
+ map[1f] = 1f
+
+ assertEquals(2f, map.getOrDefault(2f, 2f))
+ }
+
+ @Test
+ fun getOrElse() {
+ val map = MutableFloatFloatMap()
+ map[1f] = 1f
+
+ assertEquals(3f, map.getOrElse(3f) { 3f })
+ }
+
+ @Test
+ fun getOrPut() {
+ val map = MutableFloatFloatMap()
+ map[1f] = 1f
+
+ var counter = 0
+ map.getOrPut(1f) {
+ counter++
+ 2f
+ }
+ assertEquals(1f, map[1f])
+ assertEquals(0, counter)
+
+ map.getOrPut(2f) {
+ counter++
+ 2f
+ }
+ assertEquals(2f, map[2f])
+ assertEquals(1, counter)
+
+ map.getOrPut(2f) {
+ counter++
+ 3f
+ }
+ assertEquals(2f, map[2f])
+ assertEquals(1, counter)
+
+ map.getOrPut(3f) {
+ counter++
+ 3f
+ }
+ assertEquals(3f, map[3f])
+ assertEquals(2, counter)
+ }
+
+ @Test
+ fun remove() {
+ val map = MutableFloatFloatMap()
+ map.remove(1f)
+
+ map[1f] = 1f
+ map.remove(1f)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val map = MutableFloatFloatMap(6)
+ map[1f] = 1f
+ map[2f] = 2f
+ map[3f] = 3f
+ map[4f] = 4f
+ map[5f] = 5f
+ map[6f] = 6f
+
+ // Removing all the entries will mark the medata as deleted
+ map.remove(1f)
+ map.remove(2f)
+ map.remove(3f)
+ map.remove(4f)
+ map.remove(5f)
+ map.remove(6f)
+
+ assertEquals(0, map.size)
+
+ val capacity = map.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ map[7f] = 7f
+
+ assertEquals(1, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun removeIf() {
+ val map = MutableFloatFloatMap()
+ map[1f] = 1f
+ map[2f] = 2f
+ map[3f] = 3f
+ map[4f] = 4f
+ map[5f] = 5f
+ map[6f] = 6f
+
+ map.removeIf { key, _ -> key == 1f || key == 3f }
+
+ assertEquals(4, map.size)
+ assertEquals(2f, map[2f])
+ assertEquals(4f, map[4f])
+ assertEquals(5f, map[5f])
+ assertEquals(6f, map[6f])
+ }
+
+ @Test
+ fun minus() {
+ val map = MutableFloatFloatMap()
+ map[1f] = 1f
+ map[2f] = 2f
+ map[3f] = 3f
+
+ map -= 1f
+
+ assertEquals(2, map.size)
+ assertFalse(1f in map)
+ }
+
+ @Test
+ fun minusArray() {
+ val map = MutableFloatFloatMap()
+ map[1f] = 1f
+ map[2f] = 2f
+ map[3f] = 3f
+
+ map -= floatArrayOf(3f, 2f)
+
+ assertEquals(1, map.size)
+ assertFalse(3f in map)
+ assertFalse(2f in map)
+ }
+
+ @Test
+ fun minusSet() {
+ val map = MutableFloatFloatMap()
+ map[1f] = 1f
+ map[2f] = 2f
+ map[3f] = 3f
+
+ map -= floatSetOf(3f, 2f)
+
+ assertEquals(1, map.size)
+ assertFalse(3f in map)
+ assertFalse(2f in map)
+ }
+
+ @Test
+ fun minusList() {
+ val map = MutableFloatFloatMap()
+ map[1f] = 1f
+ map[2f] = 2f
+ map[3f] = 3f
+
+ map -= floatListOf(3f, 2f)
+
+ assertEquals(1, map.size)
+ assertFalse(3f in map)
+ assertFalse(2f in map)
+ }
+
+ @Test
+ fun conditionalRemove() {
+ val map = MutableFloatFloatMap()
+ assertFalse(map.remove(1f, 1f))
+
+ map[1f] = 1f
+ assertTrue(map.remove(1f, 1f))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val map = MutableFloatFloatMap()
+
+ for (i in 0 until 1700) {
+ map[i.toFloat()] = i.toFloat()
+ }
+
+ assertEquals(1700, map.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val map = MutableFloatFloatMap()
+
+ for (j in 0 until i) {
+ map[j.toFloat()] = j.toFloat()
+ }
+
+ var counter = 0
+ map.forEach { key, value ->
+ assertEquals(key, value.toFloat())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachKey() {
+ for (i in 0..48) {
+ val map = MutableFloatFloatMap()
+
+ for (j in 0 until i) {
+ map[j.toFloat()] = j.toFloat()
+ }
+
+ var counter = 0
+ val keys = BooleanArray(map.size)
+ map.forEachKey { key ->
+ keys[key.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ keys.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun forEachValue() {
+ for (i in 0..48) {
+ val map = MutableFloatFloatMap()
+
+ for (j in 0 until i) {
+ map[j.toFloat()] = j.toFloat()
+ }
+
+ var counter = 0
+ val values = BooleanArray(map.size)
+ map.forEachValue { value ->
+ values[value.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ values.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun clear() {
+ val map = MutableFloatFloatMap()
+
+ for (i in 0 until 32) {
+ map[i.toFloat()] = i.toFloat()
+ }
+
+ val capacity = map.capacity
+ map.clear()
+
+ assertEquals(0, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun string() {
+ val map = MutableFloatFloatMap()
+ assertEquals("{}", map.toString())
+
+ map[1f] = 1f
+ map[2f] = 2f
+ val oneValueString = 1f.toString()
+ val twoValueString = 2f.toString()
+ val oneKeyString = 1f.toString()
+ val twoKeyString = 2f.toString()
+ assertTrue(
+ "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+ "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+ )
+ }
+
+ @Test
+ fun equals() {
+ val map = MutableFloatFloatMap()
+ map[1f] = 1f
+
+ assertFalse(map.equals(null))
+ assertEquals(map, map)
+
+ val map2 = MutableFloatFloatMap()
+ assertNotEquals(map, map2)
+
+ map2[1f] = 1f
+ assertEquals(map, map2)
+ }
+
+ @Test
+ fun containsKey() {
+ val map = MutableFloatFloatMap()
+ map[1f] = 1f
+
+ assertTrue(map.containsKey(1f))
+ assertFalse(map.containsKey(2f))
+ }
+
+ @Test
+ fun contains() {
+ val map = MutableFloatFloatMap()
+ map[1f] = 1f
+
+ assertTrue(1f in map)
+ assertFalse(2f in map)
+ }
+
+ @Test
+ fun containsValue() {
+ val map = MutableFloatFloatMap()
+ map[1f] = 1f
+
+ assertTrue(map.containsValue(1f))
+ assertFalse(map.containsValue(3f))
+ }
+
+ @Test
+ fun empty() {
+ val map = MutableFloatFloatMap()
+ assertTrue(map.isEmpty())
+ assertFalse(map.isNotEmpty())
+ assertTrue(map.none())
+ assertFalse(map.any())
+
+ map[1f] = 1f
+
+ assertFalse(map.isEmpty())
+ assertTrue(map.isNotEmpty())
+ assertTrue(map.any())
+ assertFalse(map.none())
+ }
+
+ @Test
+ fun count() {
+ val map = MutableFloatFloatMap()
+ assertEquals(0, map.count())
+
+ map[1f] = 1f
+ assertEquals(1, map.count())
+
+ map[2f] = 2f
+ map[3f] = 3f
+ map[4f] = 4f
+ map[5f] = 5f
+ map[6f] = 6f
+
+ assertEquals(2, map.count { key, _ -> key <= 2f })
+ assertEquals(0, map.count { key, _ -> key < 0f })
+ }
+
+ @Test
+ fun any() {
+ val map = MutableFloatFloatMap()
+ map[1f] = 1f
+ map[2f] = 2f
+ map[3f] = 3f
+ map[4f] = 4f
+ map[5f] = 5f
+ map[6f] = 6f
+
+ assertTrue(map.any { key, _ -> key == 4f })
+ assertFalse(map.any { key, _ -> key < 0f })
+ }
+
+ @Test
+ fun all() {
+ val map = MutableFloatFloatMap()
+ map[1f] = 1f
+ map[2f] = 2f
+ map[3f] = 3f
+ map[4f] = 4f
+ map[5f] = 5f
+ map[6f] = 6f
+
+ assertTrue(map.all { key, value -> key > 0f && value >= 1f })
+ assertFalse(map.all { key, _ -> key < 6f })
+ }
+
+ @Test
+ fun trim() {
+ val map = MutableFloatFloatMap()
+ assertEquals(7, map.trim())
+
+ map[1f] = 1f
+ map[3f] = 3f
+
+ assertEquals(0, map.trim())
+
+ for (i in 0 until 1700) {
+ map[i.toFloat()] = i.toFloat()
+ }
+
+ assertEquals(2047, map.capacity)
+
+ // After removing these items, our capacity needs should go
+ // from 2047 down to 1023
+ for (i in 0 until 1700) {
+ if (i and 0x1 == 0x0) {
+ val s = i.toFloat()
+ map.remove(s)
+ }
+ }
+
+ assertEquals(1024, map.trim())
+ assertEquals(0, map.trim())
+ }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatIntMapTest.kt
new file mode 100644
index 0000000..9d24ffc
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatIntMapTest.kt
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class FloatIntMapTest {
+ @Test
+ fun floatIntMap() {
+ val map = MutableFloatIntMap()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun testEmptyFloatIntMap() {
+ val map = emptyFloatIntMap()
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+
+ assertSame(emptyFloatIntMap(), map)
+ }
+
+ @Test
+ fun floatIntMapFunction() {
+ val map = mutableFloatIntMapOf()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun zeroCapacityHashMap() {
+ val map = MutableFloatIntMap(0)
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun floatIntMapWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val map = MutableFloatIntMap(1800)
+ assertEquals(4095, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun floatIntMapPairsFunction() {
+ val map = mutableFloatIntMapOf(
+ 1f to 1,
+ 2f to 2
+ )
+ assertEquals(2, map.size)
+ assertEquals(1, map[1f])
+ assertEquals(2, map[2f])
+ }
+
+ @Test
+ fun addToMap() {
+ val map = MutableFloatIntMap()
+ map[1f] = 1
+
+ assertEquals(1, map.size)
+ assertEquals(1, map[1f])
+ }
+
+ @Test
+ fun addToSizedMap() {
+ val map = MutableFloatIntMap(12)
+ map[1f] = 1
+
+ assertEquals(1, map.size)
+ assertEquals(1, map[1f])
+ }
+
+ @Test
+ fun addToSmallMap() {
+ val map = MutableFloatIntMap(2)
+ map[1f] = 1
+
+ assertEquals(1, map.size)
+ assertEquals(7, map.capacity)
+ assertEquals(1, map[1f])
+ }
+
+ @Test
+ fun addToZeroCapacityMap() {
+ val map = MutableFloatIntMap(0)
+ map[1f] = 1
+
+ assertEquals(1, map.size)
+ assertEquals(1, map[1f])
+ }
+
+ @Test
+ fun replaceExistingKey() {
+ val map = MutableFloatIntMap()
+ map[1f] = 1
+ map[1f] = 2
+
+ assertEquals(1, map.size)
+ assertEquals(2, map[1f])
+ }
+
+ @Test
+ fun put() {
+ val map = MutableFloatIntMap()
+
+ map.put(1f, 1)
+ assertEquals(1, map[1f])
+ map.put(1f, 2)
+ assertEquals(2, map[1f])
+ }
+
+ @Test
+ fun putAllArray() {
+ val map = MutableFloatIntMap()
+ map[1f] = 1
+ map[2f] = 2
+
+ map.putAll(arrayOf(3f to 3, 7f to 7))
+
+ assertEquals(4, map.size)
+ assertEquals(3, map[3f])
+ assertEquals(7, map[7f])
+ }
+
+ @Test
+ fun plus() {
+ val map = MutableFloatIntMap()
+ map += 1f to 1
+
+ assertEquals(1, map.size)
+ assertEquals(1, map[1f])
+ }
+
+ @Test
+ fun plusArray() {
+ val map = MutableFloatIntMap()
+ map += arrayOf(3f to 3, 7f to 7)
+
+ assertEquals(2, map.size)
+ assertEquals(3, map[3f])
+ assertEquals(7, map[7f])
+ }
+
+ @Test
+ fun findNonExistingKey() {
+ val map = MutableFloatIntMap()
+ map[1f] = 1
+
+ assertFailsWith<NoSuchElementException> {
+ map[2f]
+ }
+ }
+
+ @Test
+ fun getOrDefault() {
+ val map = MutableFloatIntMap()
+ map[1f] = 1
+
+ assertEquals(2, map.getOrDefault(2f, 2))
+ }
+
+ @Test
+ fun getOrElse() {
+ val map = MutableFloatIntMap()
+ map[1f] = 1
+
+ assertEquals(3, map.getOrElse(3f) { 3 })
+ }
+
+ @Test
+ fun getOrPut() {
+ val map = MutableFloatIntMap()
+ map[1f] = 1
+
+ var counter = 0
+ map.getOrPut(1f) {
+ counter++
+ 2
+ }
+ assertEquals(1, map[1f])
+ assertEquals(0, counter)
+
+ map.getOrPut(2f) {
+ counter++
+ 2
+ }
+ assertEquals(2, map[2f])
+ assertEquals(1, counter)
+
+ map.getOrPut(2f) {
+ counter++
+ 3
+ }
+ assertEquals(2, map[2f])
+ assertEquals(1, counter)
+
+ map.getOrPut(3f) {
+ counter++
+ 3
+ }
+ assertEquals(3, map[3f])
+ assertEquals(2, counter)
+ }
+
+ @Test
+ fun remove() {
+ val map = MutableFloatIntMap()
+ map.remove(1f)
+
+ map[1f] = 1
+ map.remove(1f)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val map = MutableFloatIntMap(6)
+ map[1f] = 1
+ map[2f] = 2
+ map[3f] = 3
+ map[4f] = 4
+ map[5f] = 5
+ map[6f] = 6
+
+ // Removing all the entries will mark the medata as deleted
+ map.remove(1f)
+ map.remove(2f)
+ map.remove(3f)
+ map.remove(4f)
+ map.remove(5f)
+ map.remove(6f)
+
+ assertEquals(0, map.size)
+
+ val capacity = map.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ map[7f] = 7
+
+ assertEquals(1, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun removeIf() {
+ val map = MutableFloatIntMap()
+ map[1f] = 1
+ map[2f] = 2
+ map[3f] = 3
+ map[4f] = 4
+ map[5f] = 5
+ map[6f] = 6
+
+ map.removeIf { key, _ -> key == 1f || key == 3f }
+
+ assertEquals(4, map.size)
+ assertEquals(2, map[2f])
+ assertEquals(4, map[4f])
+ assertEquals(5, map[5f])
+ assertEquals(6, map[6f])
+ }
+
+ @Test
+ fun minus() {
+ val map = MutableFloatIntMap()
+ map[1f] = 1
+ map[2f] = 2
+ map[3f] = 3
+
+ map -= 1f
+
+ assertEquals(2, map.size)
+ assertFalse(1f in map)
+ }
+
+ @Test
+ fun minusArray() {
+ val map = MutableFloatIntMap()
+ map[1f] = 1
+ map[2f] = 2
+ map[3f] = 3
+
+ map -= floatArrayOf(3f, 2f)
+
+ assertEquals(1, map.size)
+ assertFalse(3f in map)
+ assertFalse(2f in map)
+ }
+
+ @Test
+ fun minusSet() {
+ val map = MutableFloatIntMap()
+ map[1f] = 1
+ map[2f] = 2
+ map[3f] = 3
+
+ map -= floatSetOf(3f, 2f)
+
+ assertEquals(1, map.size)
+ assertFalse(3f in map)
+ assertFalse(2f in map)
+ }
+
+ @Test
+ fun minusList() {
+ val map = MutableFloatIntMap()
+ map[1f] = 1
+ map[2f] = 2
+ map[3f] = 3
+
+ map -= floatListOf(3f, 2f)
+
+ assertEquals(1, map.size)
+ assertFalse(3f in map)
+ assertFalse(2f in map)
+ }
+
+ @Test
+ fun conditionalRemove() {
+ val map = MutableFloatIntMap()
+ assertFalse(map.remove(1f, 1))
+
+ map[1f] = 1
+ assertTrue(map.remove(1f, 1))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val map = MutableFloatIntMap()
+
+ for (i in 0 until 1700) {
+ map[i.toFloat()] = i.toInt()
+ }
+
+ assertEquals(1700, map.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val map = MutableFloatIntMap()
+
+ for (j in 0 until i) {
+ map[j.toFloat()] = j.toInt()
+ }
+
+ var counter = 0
+ map.forEach { key, value ->
+ assertEquals(key, value.toFloat())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachKey() {
+ for (i in 0..48) {
+ val map = MutableFloatIntMap()
+
+ for (j in 0 until i) {
+ map[j.toFloat()] = j.toInt()
+ }
+
+ var counter = 0
+ val keys = BooleanArray(map.size)
+ map.forEachKey { key ->
+ keys[key.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ keys.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun forEachValue() {
+ for (i in 0..48) {
+ val map = MutableFloatIntMap()
+
+ for (j in 0 until i) {
+ map[j.toFloat()] = j.toInt()
+ }
+
+ var counter = 0
+ val values = BooleanArray(map.size)
+ map.forEachValue { value ->
+ values[value.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ values.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun clear() {
+ val map = MutableFloatIntMap()
+
+ for (i in 0 until 32) {
+ map[i.toFloat()] = i.toInt()
+ }
+
+ val capacity = map.capacity
+ map.clear()
+
+ assertEquals(0, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun string() {
+ val map = MutableFloatIntMap()
+ assertEquals("{}", map.toString())
+
+ map[1f] = 1
+ map[2f] = 2
+ val oneValueString = 1.toString()
+ val twoValueString = 2.toString()
+ val oneKeyString = 1f.toString()
+ val twoKeyString = 2f.toString()
+ assertTrue(
+ "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+ "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+ )
+ }
+
+ @Test
+ fun equals() {
+ val map = MutableFloatIntMap()
+ map[1f] = 1
+
+ assertFalse(map.equals(null))
+ assertEquals(map, map)
+
+ val map2 = MutableFloatIntMap()
+ assertNotEquals(map, map2)
+
+ map2[1f] = 1
+ assertEquals(map, map2)
+ }
+
+ @Test
+ fun containsKey() {
+ val map = MutableFloatIntMap()
+ map[1f] = 1
+
+ assertTrue(map.containsKey(1f))
+ assertFalse(map.containsKey(2f))
+ }
+
+ @Test
+ fun contains() {
+ val map = MutableFloatIntMap()
+ map[1f] = 1
+
+ assertTrue(1f in map)
+ assertFalse(2f in map)
+ }
+
+ @Test
+ fun containsValue() {
+ val map = MutableFloatIntMap()
+ map[1f] = 1
+
+ assertTrue(map.containsValue(1))
+ assertFalse(map.containsValue(3))
+ }
+
+ @Test
+ fun empty() {
+ val map = MutableFloatIntMap()
+ assertTrue(map.isEmpty())
+ assertFalse(map.isNotEmpty())
+ assertTrue(map.none())
+ assertFalse(map.any())
+
+ map[1f] = 1
+
+ assertFalse(map.isEmpty())
+ assertTrue(map.isNotEmpty())
+ assertTrue(map.any())
+ assertFalse(map.none())
+ }
+
+ @Test
+ fun count() {
+ val map = MutableFloatIntMap()
+ assertEquals(0, map.count())
+
+ map[1f] = 1
+ assertEquals(1, map.count())
+
+ map[2f] = 2
+ map[3f] = 3
+ map[4f] = 4
+ map[5f] = 5
+ map[6f] = 6
+
+ assertEquals(2, map.count { key, _ -> key <= 2f })
+ assertEquals(0, map.count { key, _ -> key < 0f })
+ }
+
+ @Test
+ fun any() {
+ val map = MutableFloatIntMap()
+ map[1f] = 1
+ map[2f] = 2
+ map[3f] = 3
+ map[4f] = 4
+ map[5f] = 5
+ map[6f] = 6
+
+ assertTrue(map.any { key, _ -> key == 4f })
+ assertFalse(map.any { key, _ -> key < 0f })
+ }
+
+ @Test
+ fun all() {
+ val map = MutableFloatIntMap()
+ map[1f] = 1
+ map[2f] = 2
+ map[3f] = 3
+ map[4f] = 4
+ map[5f] = 5
+ map[6f] = 6
+
+ assertTrue(map.all { key, value -> key > 0f && value >= 1 })
+ assertFalse(map.all { key, _ -> key < 6f })
+ }
+
+ @Test
+ fun trim() {
+ val map = MutableFloatIntMap()
+ assertEquals(7, map.trim())
+
+ map[1f] = 1
+ map[3f] = 3
+
+ assertEquals(0, map.trim())
+
+ for (i in 0 until 1700) {
+ map[i.toFloat()] = i.toInt()
+ }
+
+ assertEquals(2047, map.capacity)
+
+ // After removing these items, our capacity needs should go
+ // from 2047 down to 1023
+ for (i in 0 until 1700) {
+ if (i and 0x1 == 0x0) {
+ val s = i.toFloat()
+ map.remove(s)
+ }
+ }
+
+ assertEquals(1024, map.trim())
+ assertEquals(0, map.trim())
+ }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt
index b4e501b..12e6d66 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt
@@ -15,7 +15,6 @@
*/
package androidx.collection
-import kotlin.math.roundToInt
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
@@ -23,6 +22,14 @@
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
class FloatListTest {
private val list: MutableFloatList = mutableFloatListOf(1f, 2f, 3f, 4f, 5f)
@@ -81,7 +88,7 @@
@Test
fun string() {
- assertEquals("[1.0, 2.0, 3.0, 4.0, 5.0]", list.toString())
+ assertEquals("[${1f}, ${2f}, ${3f}, ${4f}, ${5f}]", list.toString())
assertEquals("[]", mutableFloatListOf().toString())
}
@@ -335,7 +342,7 @@
@Test
fun fold() {
- assertEquals("12345", list.fold("") { acc, i -> acc + i.roundToInt().toString() })
+ assertEquals("12345", list.fold("") { acc, i -> acc + i.toInt().toString() })
}
@Test
@@ -343,14 +350,14 @@
assertEquals(
"01-12-23-34-45-",
list.foldIndexed("") { index, acc, i ->
- "$acc$index${i.roundToInt()}-"
+ "$acc$index${i.toInt()}-"
}
)
}
@Test
fun foldRight() {
- assertEquals("54321", list.foldRight("") { i, acc -> acc + i.roundToInt().toString() })
+ assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toInt().toString() })
}
@Test
@@ -358,7 +365,7 @@
assertEquals(
"45-34-23-12-01-",
list.foldRightIndexed("") { index, i, acc ->
- "$acc$index${i.roundToInt()}-"
+ "$acc$index${i.toInt()}-"
}
)
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatLongMapTest.kt
new file mode 100644
index 0000000..cc90959
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatLongMapTest.kt
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class FloatLongMapTest {
+ @Test
+ fun floatLongMap() {
+ val map = MutableFloatLongMap()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun testEmptyFloatLongMap() {
+ val map = emptyFloatLongMap()
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+
+ assertSame(emptyFloatLongMap(), map)
+ }
+
+ @Test
+ fun floatLongMapFunction() {
+ val map = mutableFloatLongMapOf()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun zeroCapacityHashMap() {
+ val map = MutableFloatLongMap(0)
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun floatLongMapWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val map = MutableFloatLongMap(1800)
+ assertEquals(4095, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun floatLongMapPairsFunction() {
+ val map = mutableFloatLongMapOf(
+ 1f to 1L,
+ 2f to 2L
+ )
+ assertEquals(2, map.size)
+ assertEquals(1L, map[1f])
+ assertEquals(2L, map[2f])
+ }
+
+ @Test
+ fun addToMap() {
+ val map = MutableFloatLongMap()
+ map[1f] = 1L
+
+ assertEquals(1, map.size)
+ assertEquals(1L, map[1f])
+ }
+
+ @Test
+ fun addToSizedMap() {
+ val map = MutableFloatLongMap(12)
+ map[1f] = 1L
+
+ assertEquals(1, map.size)
+ assertEquals(1L, map[1f])
+ }
+
+ @Test
+ fun addToSmallMap() {
+ val map = MutableFloatLongMap(2)
+ map[1f] = 1L
+
+ assertEquals(1, map.size)
+ assertEquals(7, map.capacity)
+ assertEquals(1L, map[1f])
+ }
+
+ @Test
+ fun addToZeroCapacityMap() {
+ val map = MutableFloatLongMap(0)
+ map[1f] = 1L
+
+ assertEquals(1, map.size)
+ assertEquals(1L, map[1f])
+ }
+
+ @Test
+ fun replaceExistingKey() {
+ val map = MutableFloatLongMap()
+ map[1f] = 1L
+ map[1f] = 2L
+
+ assertEquals(1, map.size)
+ assertEquals(2L, map[1f])
+ }
+
+ @Test
+ fun put() {
+ val map = MutableFloatLongMap()
+
+ map.put(1f, 1L)
+ assertEquals(1L, map[1f])
+ map.put(1f, 2L)
+ assertEquals(2L, map[1f])
+ }
+
+ @Test
+ fun putAllArray() {
+ val map = MutableFloatLongMap()
+ map[1f] = 1L
+ map[2f] = 2L
+
+ map.putAll(arrayOf(3f to 3L, 7f to 7L))
+
+ assertEquals(4, map.size)
+ assertEquals(3L, map[3f])
+ assertEquals(7L, map[7f])
+ }
+
+ @Test
+ fun plus() {
+ val map = MutableFloatLongMap()
+ map += 1f to 1L
+
+ assertEquals(1, map.size)
+ assertEquals(1L, map[1f])
+ }
+
+ @Test
+ fun plusArray() {
+ val map = MutableFloatLongMap()
+ map += arrayOf(3f to 3L, 7f to 7L)
+
+ assertEquals(2, map.size)
+ assertEquals(3L, map[3f])
+ assertEquals(7L, map[7f])
+ }
+
+ @Test
+ fun findNonExistingKey() {
+ val map = MutableFloatLongMap()
+ map[1f] = 1L
+
+ assertFailsWith<NoSuchElementException> {
+ map[2f]
+ }
+ }
+
+ @Test
+ fun getOrDefault() {
+ val map = MutableFloatLongMap()
+ map[1f] = 1L
+
+ assertEquals(2L, map.getOrDefault(2f, 2L))
+ }
+
+ @Test
+ fun getOrElse() {
+ val map = MutableFloatLongMap()
+ map[1f] = 1L
+
+ assertEquals(3L, map.getOrElse(3f) { 3L })
+ }
+
+ @Test
+ fun getOrPut() {
+ val map = MutableFloatLongMap()
+ map[1f] = 1L
+
+ var counter = 0
+ map.getOrPut(1f) {
+ counter++
+ 2L
+ }
+ assertEquals(1L, map[1f])
+ assertEquals(0, counter)
+
+ map.getOrPut(2f) {
+ counter++
+ 2L
+ }
+ assertEquals(2L, map[2f])
+ assertEquals(1, counter)
+
+ map.getOrPut(2f) {
+ counter++
+ 3L
+ }
+ assertEquals(2L, map[2f])
+ assertEquals(1, counter)
+
+ map.getOrPut(3f) {
+ counter++
+ 3L
+ }
+ assertEquals(3L, map[3f])
+ assertEquals(2, counter)
+ }
+
+ @Test
+ fun remove() {
+ val map = MutableFloatLongMap()
+ map.remove(1f)
+
+ map[1f] = 1L
+ map.remove(1f)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val map = MutableFloatLongMap(6)
+ map[1f] = 1L
+ map[2f] = 2L
+ map[3f] = 3L
+ map[4f] = 4L
+ map[5f] = 5L
+ map[6f] = 6L
+
+ // Removing all the entries will mark the medata as deleted
+ map.remove(1f)
+ map.remove(2f)
+ map.remove(3f)
+ map.remove(4f)
+ map.remove(5f)
+ map.remove(6f)
+
+ assertEquals(0, map.size)
+
+ val capacity = map.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ map[7f] = 7L
+
+ assertEquals(1, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun removeIf() {
+ val map = MutableFloatLongMap()
+ map[1f] = 1L
+ map[2f] = 2L
+ map[3f] = 3L
+ map[4f] = 4L
+ map[5f] = 5L
+ map[6f] = 6L
+
+ map.removeIf { key, _ -> key == 1f || key == 3f }
+
+ assertEquals(4, map.size)
+ assertEquals(2L, map[2f])
+ assertEquals(4L, map[4f])
+ assertEquals(5L, map[5f])
+ assertEquals(6L, map[6f])
+ }
+
+ @Test
+ fun minus() {
+ val map = MutableFloatLongMap()
+ map[1f] = 1L
+ map[2f] = 2L
+ map[3f] = 3L
+
+ map -= 1f
+
+ assertEquals(2, map.size)
+ assertFalse(1f in map)
+ }
+
+ @Test
+ fun minusArray() {
+ val map = MutableFloatLongMap()
+ map[1f] = 1L
+ map[2f] = 2L
+ map[3f] = 3L
+
+ map -= floatArrayOf(3f, 2f)
+
+ assertEquals(1, map.size)
+ assertFalse(3f in map)
+ assertFalse(2f in map)
+ }
+
+ @Test
+ fun minusSet() {
+ val map = MutableFloatLongMap()
+ map[1f] = 1L
+ map[2f] = 2L
+ map[3f] = 3L
+
+ map -= floatSetOf(3f, 2f)
+
+ assertEquals(1, map.size)
+ assertFalse(3f in map)
+ assertFalse(2f in map)
+ }
+
+ @Test
+ fun minusList() {
+ val map = MutableFloatLongMap()
+ map[1f] = 1L
+ map[2f] = 2L
+ map[3f] = 3L
+
+ map -= floatListOf(3f, 2f)
+
+ assertEquals(1, map.size)
+ assertFalse(3f in map)
+ assertFalse(2f in map)
+ }
+
+ @Test
+ fun conditionalRemove() {
+ val map = MutableFloatLongMap()
+ assertFalse(map.remove(1f, 1L))
+
+ map[1f] = 1L
+ assertTrue(map.remove(1f, 1L))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val map = MutableFloatLongMap()
+
+ for (i in 0 until 1700) {
+ map[i.toFloat()] = i.toLong()
+ }
+
+ assertEquals(1700, map.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val map = MutableFloatLongMap()
+
+ for (j in 0 until i) {
+ map[j.toFloat()] = j.toLong()
+ }
+
+ var counter = 0
+ map.forEach { key, value ->
+ assertEquals(key, value.toFloat())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachKey() {
+ for (i in 0..48) {
+ val map = MutableFloatLongMap()
+
+ for (j in 0 until i) {
+ map[j.toFloat()] = j.toLong()
+ }
+
+ var counter = 0
+ val keys = BooleanArray(map.size)
+ map.forEachKey { key ->
+ keys[key.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ keys.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun forEachValue() {
+ for (i in 0..48) {
+ val map = MutableFloatLongMap()
+
+ for (j in 0 until i) {
+ map[j.toFloat()] = j.toLong()
+ }
+
+ var counter = 0
+ val values = BooleanArray(map.size)
+ map.forEachValue { value ->
+ values[value.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ values.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun clear() {
+ val map = MutableFloatLongMap()
+
+ for (i in 0 until 32) {
+ map[i.toFloat()] = i.toLong()
+ }
+
+ val capacity = map.capacity
+ map.clear()
+
+ assertEquals(0, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun string() {
+ val map = MutableFloatLongMap()
+ assertEquals("{}", map.toString())
+
+ map[1f] = 1L
+ map[2f] = 2L
+ val oneValueString = 1L.toString()
+ val twoValueString = 2L.toString()
+ val oneKeyString = 1f.toString()
+ val twoKeyString = 2f.toString()
+ assertTrue(
+ "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+ "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+ )
+ }
+
+ @Test
+ fun equals() {
+ val map = MutableFloatLongMap()
+ map[1f] = 1L
+
+ assertFalse(map.equals(null))
+ assertEquals(map, map)
+
+ val map2 = MutableFloatLongMap()
+ assertNotEquals(map, map2)
+
+ map2[1f] = 1L
+ assertEquals(map, map2)
+ }
+
+ @Test
+ fun containsKey() {
+ val map = MutableFloatLongMap()
+ map[1f] = 1L
+
+ assertTrue(map.containsKey(1f))
+ assertFalse(map.containsKey(2f))
+ }
+
+ @Test
+ fun contains() {
+ val map = MutableFloatLongMap()
+ map[1f] = 1L
+
+ assertTrue(1f in map)
+ assertFalse(2f in map)
+ }
+
+ @Test
+ fun containsValue() {
+ val map = MutableFloatLongMap()
+ map[1f] = 1L
+
+ assertTrue(map.containsValue(1L))
+ assertFalse(map.containsValue(3L))
+ }
+
+ @Test
+ fun empty() {
+ val map = MutableFloatLongMap()
+ assertTrue(map.isEmpty())
+ assertFalse(map.isNotEmpty())
+ assertTrue(map.none())
+ assertFalse(map.any())
+
+ map[1f] = 1L
+
+ assertFalse(map.isEmpty())
+ assertTrue(map.isNotEmpty())
+ assertTrue(map.any())
+ assertFalse(map.none())
+ }
+
+ @Test
+ fun count() {
+ val map = MutableFloatLongMap()
+ assertEquals(0, map.count())
+
+ map[1f] = 1L
+ assertEquals(1, map.count())
+
+ map[2f] = 2L
+ map[3f] = 3L
+ map[4f] = 4L
+ map[5f] = 5L
+ map[6f] = 6L
+
+ assertEquals(2, map.count { key, _ -> key <= 2f })
+ assertEquals(0, map.count { key, _ -> key < 0f })
+ }
+
+ @Test
+ fun any() {
+ val map = MutableFloatLongMap()
+ map[1f] = 1L
+ map[2f] = 2L
+ map[3f] = 3L
+ map[4f] = 4L
+ map[5f] = 5L
+ map[6f] = 6L
+
+ assertTrue(map.any { key, _ -> key == 4f })
+ assertFalse(map.any { key, _ -> key < 0f })
+ }
+
+ @Test
+ fun all() {
+ val map = MutableFloatLongMap()
+ map[1f] = 1L
+ map[2f] = 2L
+ map[3f] = 3L
+ map[4f] = 4L
+ map[5f] = 5L
+ map[6f] = 6L
+
+ assertTrue(map.all { key, value -> key > 0f && value >= 1L })
+ assertFalse(map.all { key, _ -> key < 6f })
+ }
+
+ @Test
+ fun trim() {
+ val map = MutableFloatLongMap()
+ assertEquals(7, map.trim())
+
+ map[1f] = 1L
+ map[3f] = 3L
+
+ assertEquals(0, map.trim())
+
+ for (i in 0 until 1700) {
+ map[i.toFloat()] = i.toLong()
+ }
+
+ assertEquals(2047, map.capacity)
+
+ // After removing these items, our capacity needs should go
+ // from 2047 down to 1023
+ for (i in 0 until 1700) {
+ if (i and 0x1 == 0x0) {
+ val s = i.toFloat()
+ map.remove(s)
+ }
+ }
+
+ assertEquals(1024, map.trim())
+ assertEquals(0, map.trim())
+ }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatObjectMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatObjectMapTest.kt
new file mode 100644
index 0000000..4a10fc7b
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatObjectMapTest.kt
@@ -0,0 +1,632 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class FloatObjectMapTest {
+ @Test
+ fun floatObjectMap() {
+ val map = MutableFloatObjectMap<String>()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun testEmptyFloatObjectMap() {
+ val map = emptyFloatObjectMap<String>()
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+
+ assertSame(emptyFloatObjectMap<String>(), map)
+ }
+
+ @Test
+ fun floatObjectMapFunction() {
+ val map = mutableFloatObjectMapOf<String>()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun zeroCapacityHashMap() {
+ val map = MutableFloatObjectMap<String>(0)
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun floatObjectMapWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val map = MutableFloatObjectMap<String>(1800)
+ assertEquals(4095, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun floatObjectMapPairsFunction() {
+ val map = mutableFloatObjectMapOf(
+ 1f to "World",
+ 2f to "Monde"
+ )
+ assertEquals(2, map.size)
+ assertEquals("World", map[1f])
+ assertEquals("Monde", map[2f])
+ }
+
+ @Test
+ fun addToMap() {
+ val map = MutableFloatObjectMap<String>()
+ map[1f] = "World"
+
+ assertEquals(1, map.size)
+ assertEquals("World", map[1f])
+ }
+
+ @Test
+ fun insertIndex0() {
+ val map = MutableFloatObjectMap<String>()
+ map.put(1f, "World")
+ assertEquals("World", map[1f])
+ }
+
+ @Test
+ fun addToSizedMap() {
+ val map = MutableFloatObjectMap<String>(12)
+ map[1f] = "World"
+
+ assertEquals(1, map.size)
+ assertEquals("World", map[1f])
+ }
+
+ @Test
+ fun addToSmallMap() {
+ val map = MutableFloatObjectMap<String>(2)
+ map[1f] = "World"
+
+ assertEquals(1, map.size)
+ assertEquals(7, map.capacity)
+ assertEquals("World", map[1f])
+ }
+
+ @Test
+ fun addToZeroCapacityMap() {
+ val map = MutableFloatObjectMap<String>(0)
+ map[1f] = "World"
+
+ assertEquals(1, map.size)
+ assertEquals("World", map[1f])
+ }
+
+ @Test
+ fun replaceExistingKey() {
+ val map = MutableFloatObjectMap<String>()
+ map[1f] = "World"
+ map[1f] = "Monde"
+
+ assertEquals(1, map.size)
+ assertEquals("Monde", map[1f])
+ }
+
+ @Test
+ fun put() {
+ val map = MutableFloatObjectMap<String?>()
+
+ assertNull(map.put(1f, "World"))
+ assertEquals("World", map.put(1f, "Monde"))
+ assertNull(map.put(2f, null))
+ assertNull(map.put(2f, "Monde"))
+ }
+
+ @Test
+ fun putAllArray() {
+ val map = MutableFloatObjectMap<String?>()
+ map[1f] = "World"
+ map[2f] = null
+
+ map.putAll(arrayOf(3f to "Welt", 7f to "Mundo"))
+
+ assertEquals(4, map.size)
+ assertEquals("Welt", map[3f])
+ assertEquals("Mundo", map[7f])
+ }
+
+ @Test
+ fun putAllMap() {
+ val map = MutableFloatObjectMap<String?>()
+ map[1f] = "World"
+ map[2f] = null
+
+ map.putAll(mutableFloatObjectMapOf(3f to "Welt", 7f to "Mundo"))
+
+ assertEquals(4, map.size)
+ assertEquals("Welt", map[3f])
+ assertEquals("Mundo", map[7f])
+ }
+
+ @Test
+ fun plus() {
+ val map = MutableFloatObjectMap<String>()
+ map += 1f to "World"
+
+ assertEquals(1, map.size)
+ assertEquals("World", map[1f])
+ }
+
+ @Test
+ fun plusMap() {
+ val map = MutableFloatObjectMap<String>()
+ map += floatObjectMapOf(3f to "Welt", 7f to "Mundo")
+
+ assertEquals(2, map.size)
+ assertEquals("Welt", map[3f])
+ assertEquals("Mundo", map[7f])
+ }
+
+ @Test
+ fun plusArray() {
+ val map = MutableFloatObjectMap<String>()
+ map += arrayOf(3f to "Welt", 7f to "Mundo")
+
+ assertEquals(2, map.size)
+ assertEquals("Welt", map[3f])
+ assertEquals("Mundo", map[7f])
+ }
+
+ @Test
+ fun nullValue() {
+ val map = MutableFloatObjectMap<String?>()
+ map[1f] = null
+
+ assertEquals(1, map.size)
+ assertNull(map[1f])
+ }
+
+ @Test
+ fun findNonExistingKey() {
+ val map = MutableFloatObjectMap<String?>()
+ map[1f] = "World"
+
+ assertNull(map[2f])
+ }
+
+ @Test
+ fun getOrDefault() {
+ val map = MutableFloatObjectMap<String?>()
+ map[1f] = "World"
+
+ assertEquals("Monde", map.getOrDefault(2f, "Monde"))
+ }
+
+ @Test
+ fun getOrElse() {
+ val map = MutableFloatObjectMap<String?>()
+ map[1f] = "World"
+ map[2f] = null
+
+ assertEquals("Monde", map.getOrElse(2f) { "Monde" })
+ assertEquals("Welt", map.getOrElse(3f) { "Welt" })
+ }
+
+ @Test
+ fun getOrPut() {
+ val map = MutableFloatObjectMap<String?>()
+ map[1f] = "World"
+
+ var counter = 0
+ map.getOrPut(1f) {
+ counter++
+ "Monde"
+ }
+ assertEquals("World", map[1f])
+ assertEquals(0, counter)
+
+ map.getOrPut(2f) {
+ counter++
+ "Monde"
+ }
+ assertEquals("Monde", map[2f])
+ assertEquals(1, counter)
+
+ map.getOrPut(2f) {
+ counter++
+ "Welt"
+ }
+ assertEquals("Monde", map[2f])
+ assertEquals(1, counter)
+
+ map.getOrPut(3f) {
+ counter++
+ null
+ }
+ assertNull(map[3f])
+ assertEquals(2, counter)
+
+ map.getOrPut(3f) {
+ counter++
+ "Welt"
+ }
+ assertEquals("Welt", map[3f])
+ assertEquals(3, counter)
+ }
+
+ @Test
+ fun remove() {
+ val map = MutableFloatObjectMap<String?>()
+ assertNull(map.remove(1f))
+
+ map[1f] = "World"
+ assertEquals("World", map.remove(1f))
+ assertEquals(0, map.size)
+
+ map[1f] = null
+ assertNull(map.remove(1f))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val map = MutableFloatObjectMap<String>(6)
+ map[1f] = "World"
+ map[2f] = "Monde"
+ map[3f] = "Welt"
+ map[4f] = "Sekai"
+ map[5f] = "Mondo"
+ map[6f] = "Sesang"
+
+ // Removing all the entries will mark the medata as deleted
+ map.remove(1f)
+ map.remove(2f)
+ map.remove(3f)
+ map.remove(4f)
+ map.remove(5f)
+ map.remove(6f)
+
+ assertEquals(0, map.size)
+
+ val capacity = map.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ map[7f] = "Mundo"
+
+ assertEquals(1, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun removeIf() {
+ val map = MutableFloatObjectMap<String>()
+ map[1f] = "World"
+ map[2f] = "Monde"
+ map[3f] = "Welt"
+ map[4f] = "Sekai"
+ map[5f] = "Mondo"
+ map[6f] = "Sesang"
+
+ map.removeIf { key, value ->
+ key == 1f || key == 3f || value.startsWith('S')
+ }
+
+ assertEquals(2, map.size)
+ assertEquals("Monde", map[2f])
+ assertEquals("Mondo", map[5f])
+ }
+
+ @Test
+ fun minus() {
+ val map = MutableFloatObjectMap<String>()
+ map[1f] = "World"
+ map[2f] = "Monde"
+ map[3f] = "Welt"
+
+ map -= 1f
+
+ assertEquals(2, map.size)
+ assertNull(map[1f])
+ }
+
+ @Test
+ fun minusArray() {
+ val map = MutableFloatObjectMap<String>()
+ map[1f] = "World"
+ map[2f] = "Monde"
+ map[3f] = "Welt"
+
+ map -= floatArrayOf(3f, 2f)
+
+ assertEquals(1, map.size)
+ assertNull(map[3f])
+ assertNull(map[2f])
+ }
+
+ @Test
+ fun minusSet() {
+ val map = MutableFloatObjectMap<String>()
+ map[1f] = "World"
+ map[2f] = "Monde"
+ map[3f] = "Welt"
+
+ map -= floatSetOf(3f, 2f)
+
+ assertEquals(1, map.size)
+ assertNull(map[3f])
+ assertNull(map[2f])
+ }
+
+ @Test
+ fun minusList() {
+ val map = MutableFloatObjectMap<String>()
+ map[1f] = "World"
+ map[2f] = "Monde"
+ map[3f] = "Welt"
+
+ map -= floatListOf(3f, 2f)
+
+ assertEquals(1, map.size)
+ assertNull(map[3f])
+ assertNull(map[2f])
+ }
+
+ @Test
+ fun conditionalRemove() {
+ val map = MutableFloatObjectMap<String?>()
+ assertFalse(map.remove(1f, "World"))
+
+ map[1f] = "World"
+ assertTrue(map.remove(1f, "World"))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val map = MutableFloatObjectMap<String>()
+
+ for (i in 0 until 1700) {
+ map[i.toFloat()] = i.toString()
+ }
+
+ assertEquals(1700, map.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val map = MutableFloatObjectMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toFloat()] = j.toString()
+ }
+
+ var counter = 0
+ map.forEach { key, value ->
+ assertEquals(key.toInt().toString(), value)
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachKey() {
+ for (i in 0..48) {
+ val map = MutableFloatObjectMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toFloat()] = j.toString()
+ }
+
+ var counter = 0
+ map.forEachKey { key ->
+ assertEquals(key.toInt().toString(), map[key])
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachValue() {
+ for (i in 0..48) {
+ val map = MutableFloatObjectMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toFloat()] = j.toString()
+ }
+
+ var counter = 0
+ map.forEachValue { value ->
+ assertNotNull(value.toIntOrNull())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun clear() {
+ val map = MutableFloatObjectMap<String>()
+
+ for (i in 0 until 32) {
+ map[i.toFloat()] = i.toString()
+ }
+
+ val capacity = map.capacity
+ map.clear()
+
+ assertEquals(0, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun string() {
+ val map = MutableFloatObjectMap<String?>()
+ assertEquals("{}", map.toString())
+
+ map[1f] = "World"
+ map[2f] = "Monde"
+ val oneKey = 1f.toString()
+ val twoKey = 2f.toString()
+ assertTrue(
+ "{$oneKey=World, $twoKey=Monde}" == map.toString() ||
+ "{$twoKey=Monde, $oneKey=World}" == map.toString()
+ )
+
+ map.clear()
+ map[1f] = null
+ assertEquals("{$oneKey=null}", map.toString())
+
+ val selfAsValueMap = MutableFloatObjectMap<Any>()
+ selfAsValueMap[1f] = selfAsValueMap
+ assertEquals("{$oneKey=(this)}", selfAsValueMap.toString())
+ }
+
+ @Test
+ fun equals() {
+ val map = MutableFloatObjectMap<String?>()
+ map[1f] = "World"
+ map[2f] = null
+
+ assertFalse(map.equals(null))
+ assertEquals(map, map)
+
+ val map2 = MutableFloatObjectMap<String?>()
+ map2[2f] = null
+
+ assertNotEquals(map, map2)
+
+ map2[1f] = "World"
+ assertEquals(map, map2)
+ }
+
+ @Test
+ fun containsKey() {
+ val map = MutableFloatObjectMap<String?>()
+ map[1f] = "World"
+ map[2f] = null
+
+ assertTrue(map.containsKey(1f))
+ assertFalse(map.containsKey(3f))
+ }
+
+ @Test
+ fun contains() {
+ val map = MutableFloatObjectMap<String?>()
+ map[1f] = "World"
+ map[2f] = null
+
+ assertTrue(1f in map)
+ assertFalse(3f in map)
+ }
+
+ @Test
+ fun containsValue() {
+ val map = MutableFloatObjectMap<String?>()
+ map[1f] = "World"
+ map[2f] = null
+
+ assertTrue(map.containsValue("World"))
+ assertTrue(map.containsValue(null))
+ assertFalse(map.containsValue("Monde"))
+ }
+
+ @Test
+ fun empty() {
+ val map = MutableFloatObjectMap<String?>()
+ assertTrue(map.isEmpty())
+ assertFalse(map.isNotEmpty())
+ assertTrue(map.none())
+ assertFalse(map.any())
+
+ map[1f] = "World"
+
+ assertFalse(map.isEmpty())
+ assertTrue(map.isNotEmpty())
+ assertTrue(map.any())
+ assertFalse(map.none())
+ }
+
+ @Test
+ fun count() {
+ val map = MutableFloatObjectMap<String>()
+ assertEquals(0, map.count())
+
+ map[1f] = "World"
+ assertEquals(1, map.count())
+
+ map[2f] = "Monde"
+ map[3f] = "Welt"
+ map[4f] = "Sekai"
+ map[5f] = "Mondo"
+ map[6f] = "Sesang"
+
+ assertEquals(2, map.count { key, _ -> key < 3f })
+ assertEquals(0, map.count { key, _ -> key < 0f })
+ }
+
+ @Test
+ fun any() {
+ val map = MutableFloatObjectMap<String>()
+ map[1f] = "World"
+ map[2f] = "Monde"
+ map[3f] = "Welt"
+ map[4f] = "Sekai"
+ map[5f] = "Mondo"
+ map[6f] = "Sesang"
+
+ assertTrue(map.any { key, _ -> key > 5f })
+ assertFalse(map.any { key, _ -> key < 0f })
+ }
+
+ @Test
+ fun all() {
+ val map = MutableFloatObjectMap<String>()
+ map[1f] = "World"
+ map[2f] = "Monde"
+ map[3f] = "Welt"
+ map[4f] = "Sekai"
+ map[5f] = "Mondo"
+ map[6f] = "Sesang"
+
+ assertTrue(map.all { key, value -> key < 7f && value.length > 0 })
+ assertFalse(map.all { key, _ -> key < 6f })
+ }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt
index 98f7b59..af07640 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt
@@ -23,6 +23,14 @@
import kotlin.test.assertSame
import kotlin.test.assertTrue
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
class FloatSetTest {
@Test
fun emptyFloatSetConstructor() {
@@ -147,9 +155,9 @@
val set = MutableFloatSet()
set += 1f
set += 2f
- var element: Float = Float.NaN
- var otherElement: Float = Float.NaN
- set.forEach { if (element.isNaN()) element = it else otherElement = it }
+ var element: Float = -1f
+ var otherElement: Float = -1f
+ set.forEach { if (element == -1f) element = it else otherElement = it }
assertEquals(element, set.first())
set -= element
assertEquals(otherElement, set.first())
@@ -333,8 +341,8 @@
set += 1f
set += 5f
assertTrue(
- "[1.0, 5.0]" == set.toString() ||
- "[5.0, 1.0]" == set.toString()
+ "[${1f}, ${5f}]" == set.toString() ||
+ "[${5f}, ${1f}]" == set.toString()
)
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntFloatMapTest.kt
new file mode 100644
index 0000000..94c8ec2
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntFloatMapTest.kt
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class IntFloatMapTest {
+ @Test
+ fun intFloatMap() {
+ val map = MutableIntFloatMap()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun testEmptyIntFloatMap() {
+ val map = emptyIntFloatMap()
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+
+ assertSame(emptyIntFloatMap(), map)
+ }
+
+ @Test
+ fun intFloatMapFunction() {
+ val map = mutableIntFloatMapOf()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun zeroCapacityHashMap() {
+ val map = MutableIntFloatMap(0)
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun intFloatMapWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val map = MutableIntFloatMap(1800)
+ assertEquals(4095, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun intFloatMapPairsFunction() {
+ val map = mutableIntFloatMapOf(
+ 1 to 1f,
+ 2 to 2f
+ )
+ assertEquals(2, map.size)
+ assertEquals(1f, map[1])
+ assertEquals(2f, map[2])
+ }
+
+ @Test
+ fun addToMap() {
+ val map = MutableIntFloatMap()
+ map[1] = 1f
+
+ assertEquals(1, map.size)
+ assertEquals(1f, map[1])
+ }
+
+ @Test
+ fun addToSizedMap() {
+ val map = MutableIntFloatMap(12)
+ map[1] = 1f
+
+ assertEquals(1, map.size)
+ assertEquals(1f, map[1])
+ }
+
+ @Test
+ fun addToSmallMap() {
+ val map = MutableIntFloatMap(2)
+ map[1] = 1f
+
+ assertEquals(1, map.size)
+ assertEquals(7, map.capacity)
+ assertEquals(1f, map[1])
+ }
+
+ @Test
+ fun addToZeroCapacityMap() {
+ val map = MutableIntFloatMap(0)
+ map[1] = 1f
+
+ assertEquals(1, map.size)
+ assertEquals(1f, map[1])
+ }
+
+ @Test
+ fun replaceExistingKey() {
+ val map = MutableIntFloatMap()
+ map[1] = 1f
+ map[1] = 2f
+
+ assertEquals(1, map.size)
+ assertEquals(2f, map[1])
+ }
+
+ @Test
+ fun put() {
+ val map = MutableIntFloatMap()
+
+ map.put(1, 1f)
+ assertEquals(1f, map[1])
+ map.put(1, 2f)
+ assertEquals(2f, map[1])
+ }
+
+ @Test
+ fun putAllArray() {
+ val map = MutableIntFloatMap()
+ map[1] = 1f
+ map[2] = 2f
+
+ map.putAll(arrayOf(3 to 3f, 7 to 7f))
+
+ assertEquals(4, map.size)
+ assertEquals(3f, map[3])
+ assertEquals(7f, map[7])
+ }
+
+ @Test
+ fun plus() {
+ val map = MutableIntFloatMap()
+ map += 1 to 1f
+
+ assertEquals(1, map.size)
+ assertEquals(1f, map[1])
+ }
+
+ @Test
+ fun plusArray() {
+ val map = MutableIntFloatMap()
+ map += arrayOf(3 to 3f, 7 to 7f)
+
+ assertEquals(2, map.size)
+ assertEquals(3f, map[3])
+ assertEquals(7f, map[7])
+ }
+
+ @Test
+ fun findNonExistingKey() {
+ val map = MutableIntFloatMap()
+ map[1] = 1f
+
+ assertFailsWith<NoSuchElementException> {
+ map[2]
+ }
+ }
+
+ @Test
+ fun getOrDefault() {
+ val map = MutableIntFloatMap()
+ map[1] = 1f
+
+ assertEquals(2f, map.getOrDefault(2, 2f))
+ }
+
+ @Test
+ fun getOrElse() {
+ val map = MutableIntFloatMap()
+ map[1] = 1f
+
+ assertEquals(3f, map.getOrElse(3) { 3f })
+ }
+
+ @Test
+ fun getOrPut() {
+ val map = MutableIntFloatMap()
+ map[1] = 1f
+
+ var counter = 0
+ map.getOrPut(1) {
+ counter++
+ 2f
+ }
+ assertEquals(1f, map[1])
+ assertEquals(0, counter)
+
+ map.getOrPut(2) {
+ counter++
+ 2f
+ }
+ assertEquals(2f, map[2])
+ assertEquals(1, counter)
+
+ map.getOrPut(2) {
+ counter++
+ 3f
+ }
+ assertEquals(2f, map[2])
+ assertEquals(1, counter)
+
+ map.getOrPut(3) {
+ counter++
+ 3f
+ }
+ assertEquals(3f, map[3])
+ assertEquals(2, counter)
+ }
+
+ @Test
+ fun remove() {
+ val map = MutableIntFloatMap()
+ map.remove(1)
+
+ map[1] = 1f
+ map.remove(1)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val map = MutableIntFloatMap(6)
+ map[1] = 1f
+ map[2] = 2f
+ map[3] = 3f
+ map[4] = 4f
+ map[5] = 5f
+ map[6] = 6f
+
+ // Removing all the entries will mark the medata as deleted
+ map.remove(1)
+ map.remove(2)
+ map.remove(3)
+ map.remove(4)
+ map.remove(5)
+ map.remove(6)
+
+ assertEquals(0, map.size)
+
+ val capacity = map.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ map[7] = 7f
+
+ assertEquals(1, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun removeIf() {
+ val map = MutableIntFloatMap()
+ map[1] = 1f
+ map[2] = 2f
+ map[3] = 3f
+ map[4] = 4f
+ map[5] = 5f
+ map[6] = 6f
+
+ map.removeIf { key, _ -> key == 1 || key == 3 }
+
+ assertEquals(4, map.size)
+ assertEquals(2f, map[2])
+ assertEquals(4f, map[4])
+ assertEquals(5f, map[5])
+ assertEquals(6f, map[6])
+ }
+
+ @Test
+ fun minus() {
+ val map = MutableIntFloatMap()
+ map[1] = 1f
+ map[2] = 2f
+ map[3] = 3f
+
+ map -= 1
+
+ assertEquals(2, map.size)
+ assertFalse(1 in map)
+ }
+
+ @Test
+ fun minusArray() {
+ val map = MutableIntFloatMap()
+ map[1] = 1f
+ map[2] = 2f
+ map[3] = 3f
+
+ map -= intArrayOf(3, 2)
+
+ assertEquals(1, map.size)
+ assertFalse(3 in map)
+ assertFalse(2 in map)
+ }
+
+ @Test
+ fun minusSet() {
+ val map = MutableIntFloatMap()
+ map[1] = 1f
+ map[2] = 2f
+ map[3] = 3f
+
+ map -= intSetOf(3, 2)
+
+ assertEquals(1, map.size)
+ assertFalse(3 in map)
+ assertFalse(2 in map)
+ }
+
+ @Test
+ fun minusList() {
+ val map = MutableIntFloatMap()
+ map[1] = 1f
+ map[2] = 2f
+ map[3] = 3f
+
+ map -= intListOf(3, 2)
+
+ assertEquals(1, map.size)
+ assertFalse(3 in map)
+ assertFalse(2 in map)
+ }
+
+ @Test
+ fun conditionalRemove() {
+ val map = MutableIntFloatMap()
+ assertFalse(map.remove(1, 1f))
+
+ map[1] = 1f
+ assertTrue(map.remove(1, 1f))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val map = MutableIntFloatMap()
+
+ for (i in 0 until 1700) {
+ map[i.toInt()] = i.toFloat()
+ }
+
+ assertEquals(1700, map.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val map = MutableIntFloatMap()
+
+ for (j in 0 until i) {
+ map[j.toInt()] = j.toFloat()
+ }
+
+ var counter = 0
+ map.forEach { key, value ->
+ assertEquals(key, value.toInt())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachKey() {
+ for (i in 0..48) {
+ val map = MutableIntFloatMap()
+
+ for (j in 0 until i) {
+ map[j.toInt()] = j.toFloat()
+ }
+
+ var counter = 0
+ val keys = BooleanArray(map.size)
+ map.forEachKey { key ->
+ keys[key.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ keys.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun forEachValue() {
+ for (i in 0..48) {
+ val map = MutableIntFloatMap()
+
+ for (j in 0 until i) {
+ map[j.toInt()] = j.toFloat()
+ }
+
+ var counter = 0
+ val values = BooleanArray(map.size)
+ map.forEachValue { value ->
+ values[value.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ values.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun clear() {
+ val map = MutableIntFloatMap()
+
+ for (i in 0 until 32) {
+ map[i.toInt()] = i.toFloat()
+ }
+
+ val capacity = map.capacity
+ map.clear()
+
+ assertEquals(0, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun string() {
+ val map = MutableIntFloatMap()
+ assertEquals("{}", map.toString())
+
+ map[1] = 1f
+ map[2] = 2f
+ val oneValueString = 1f.toString()
+ val twoValueString = 2f.toString()
+ val oneKeyString = 1.toString()
+ val twoKeyString = 2.toString()
+ assertTrue(
+ "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+ "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+ )
+ }
+
+ @Test
+ fun equals() {
+ val map = MutableIntFloatMap()
+ map[1] = 1f
+
+ assertFalse(map.equals(null))
+ assertEquals(map, map)
+
+ val map2 = MutableIntFloatMap()
+ assertNotEquals(map, map2)
+
+ map2[1] = 1f
+ assertEquals(map, map2)
+ }
+
+ @Test
+ fun containsKey() {
+ val map = MutableIntFloatMap()
+ map[1] = 1f
+
+ assertTrue(map.containsKey(1))
+ assertFalse(map.containsKey(2))
+ }
+
+ @Test
+ fun contains() {
+ val map = MutableIntFloatMap()
+ map[1] = 1f
+
+ assertTrue(1 in map)
+ assertFalse(2 in map)
+ }
+
+ @Test
+ fun containsValue() {
+ val map = MutableIntFloatMap()
+ map[1] = 1f
+
+ assertTrue(map.containsValue(1f))
+ assertFalse(map.containsValue(3f))
+ }
+
+ @Test
+ fun empty() {
+ val map = MutableIntFloatMap()
+ assertTrue(map.isEmpty())
+ assertFalse(map.isNotEmpty())
+ assertTrue(map.none())
+ assertFalse(map.any())
+
+ map[1] = 1f
+
+ assertFalse(map.isEmpty())
+ assertTrue(map.isNotEmpty())
+ assertTrue(map.any())
+ assertFalse(map.none())
+ }
+
+ @Test
+ fun count() {
+ val map = MutableIntFloatMap()
+ assertEquals(0, map.count())
+
+ map[1] = 1f
+ assertEquals(1, map.count())
+
+ map[2] = 2f
+ map[3] = 3f
+ map[4] = 4f
+ map[5] = 5f
+ map[6] = 6f
+
+ assertEquals(2, map.count { key, _ -> key <= 2 })
+ assertEquals(0, map.count { key, _ -> key < 0 })
+ }
+
+ @Test
+ fun any() {
+ val map = MutableIntFloatMap()
+ map[1] = 1f
+ map[2] = 2f
+ map[3] = 3f
+ map[4] = 4f
+ map[5] = 5f
+ map[6] = 6f
+
+ assertTrue(map.any { key, _ -> key == 4 })
+ assertFalse(map.any { key, _ -> key < 0 })
+ }
+
+ @Test
+ fun all() {
+ val map = MutableIntFloatMap()
+ map[1] = 1f
+ map[2] = 2f
+ map[3] = 3f
+ map[4] = 4f
+ map[5] = 5f
+ map[6] = 6f
+
+ assertTrue(map.all { key, value -> key > 0 && value >= 1f })
+ assertFalse(map.all { key, _ -> key < 6 })
+ }
+
+ @Test
+ fun trim() {
+ val map = MutableIntFloatMap()
+ assertEquals(7, map.trim())
+
+ map[1] = 1f
+ map[3] = 3f
+
+ assertEquals(0, map.trim())
+
+ for (i in 0 until 1700) {
+ map[i.toInt()] = i.toFloat()
+ }
+
+ assertEquals(2047, map.capacity)
+
+ // After removing these items, our capacity needs should go
+ // from 2047 down to 1023
+ for (i in 0 until 1700) {
+ if (i and 0x1 == 0x0) {
+ val s = i.toInt()
+ map.remove(s)
+ }
+ }
+
+ assertEquals(1024, map.trim())
+ assertEquals(0, map.trim())
+ }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntIntMapTest.kt
new file mode 100644
index 0000000..933c5ba
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntIntMapTest.kt
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class IntIntMapTest {
+ @Test
+ fun intIntMap() {
+ val map = MutableIntIntMap()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun testEmptyIntIntMap() {
+ val map = emptyIntIntMap()
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+
+ assertSame(emptyIntIntMap(), map)
+ }
+
+ @Test
+ fun intIntMapFunction() {
+ val map = mutableIntIntMapOf()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun zeroCapacityHashMap() {
+ val map = MutableIntIntMap(0)
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun intIntMapWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val map = MutableIntIntMap(1800)
+ assertEquals(4095, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun intIntMapPairsFunction() {
+ val map = mutableIntIntMapOf(
+ 1 to 1,
+ 2 to 2
+ )
+ assertEquals(2, map.size)
+ assertEquals(1, map[1])
+ assertEquals(2, map[2])
+ }
+
+ @Test
+ fun addToMap() {
+ val map = MutableIntIntMap()
+ map[1] = 1
+
+ assertEquals(1, map.size)
+ assertEquals(1, map[1])
+ }
+
+ @Test
+ fun addToSizedMap() {
+ val map = MutableIntIntMap(12)
+ map[1] = 1
+
+ assertEquals(1, map.size)
+ assertEquals(1, map[1])
+ }
+
+ @Test
+ fun addToSmallMap() {
+ val map = MutableIntIntMap(2)
+ map[1] = 1
+
+ assertEquals(1, map.size)
+ assertEquals(7, map.capacity)
+ assertEquals(1, map[1])
+ }
+
+ @Test
+ fun addToZeroCapacityMap() {
+ val map = MutableIntIntMap(0)
+ map[1] = 1
+
+ assertEquals(1, map.size)
+ assertEquals(1, map[1])
+ }
+
+ @Test
+ fun replaceExistingKey() {
+ val map = MutableIntIntMap()
+ map[1] = 1
+ map[1] = 2
+
+ assertEquals(1, map.size)
+ assertEquals(2, map[1])
+ }
+
+ @Test
+ fun put() {
+ val map = MutableIntIntMap()
+
+ map.put(1, 1)
+ assertEquals(1, map[1])
+ map.put(1, 2)
+ assertEquals(2, map[1])
+ }
+
+ @Test
+ fun putAllArray() {
+ val map = MutableIntIntMap()
+ map[1] = 1
+ map[2] = 2
+
+ map.putAll(arrayOf(3 to 3, 7 to 7))
+
+ assertEquals(4, map.size)
+ assertEquals(3, map[3])
+ assertEquals(7, map[7])
+ }
+
+ @Test
+ fun plus() {
+ val map = MutableIntIntMap()
+ map += 1 to 1
+
+ assertEquals(1, map.size)
+ assertEquals(1, map[1])
+ }
+
+ @Test
+ fun plusArray() {
+ val map = MutableIntIntMap()
+ map += arrayOf(3 to 3, 7 to 7)
+
+ assertEquals(2, map.size)
+ assertEquals(3, map[3])
+ assertEquals(7, map[7])
+ }
+
+ @Test
+ fun findNonExistingKey() {
+ val map = MutableIntIntMap()
+ map[1] = 1
+
+ assertFailsWith<NoSuchElementException> {
+ map[2]
+ }
+ }
+
+ @Test
+ fun getOrDefault() {
+ val map = MutableIntIntMap()
+ map[1] = 1
+
+ assertEquals(2, map.getOrDefault(2, 2))
+ }
+
+ @Test
+ fun getOrElse() {
+ val map = MutableIntIntMap()
+ map[1] = 1
+
+ assertEquals(3, map.getOrElse(3) { 3 })
+ }
+
+ @Test
+ fun getOrPut() {
+ val map = MutableIntIntMap()
+ map[1] = 1
+
+ var counter = 0
+ map.getOrPut(1) {
+ counter++
+ 2
+ }
+ assertEquals(1, map[1])
+ assertEquals(0, counter)
+
+ map.getOrPut(2) {
+ counter++
+ 2
+ }
+ assertEquals(2, map[2])
+ assertEquals(1, counter)
+
+ map.getOrPut(2) {
+ counter++
+ 3
+ }
+ assertEquals(2, map[2])
+ assertEquals(1, counter)
+
+ map.getOrPut(3) {
+ counter++
+ 3
+ }
+ assertEquals(3, map[3])
+ assertEquals(2, counter)
+ }
+
+ @Test
+ fun remove() {
+ val map = MutableIntIntMap()
+ map.remove(1)
+
+ map[1] = 1
+ map.remove(1)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val map = MutableIntIntMap(6)
+ map[1] = 1
+ map[2] = 2
+ map[3] = 3
+ map[4] = 4
+ map[5] = 5
+ map[6] = 6
+
+ // Removing all the entries will mark the medata as deleted
+ map.remove(1)
+ map.remove(2)
+ map.remove(3)
+ map.remove(4)
+ map.remove(5)
+ map.remove(6)
+
+ assertEquals(0, map.size)
+
+ val capacity = map.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ map[7] = 7
+
+ assertEquals(1, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun removeIf() {
+ val map = MutableIntIntMap()
+ map[1] = 1
+ map[2] = 2
+ map[3] = 3
+ map[4] = 4
+ map[5] = 5
+ map[6] = 6
+
+ map.removeIf { key, _ -> key == 1 || key == 3 }
+
+ assertEquals(4, map.size)
+ assertEquals(2, map[2])
+ assertEquals(4, map[4])
+ assertEquals(5, map[5])
+ assertEquals(6, map[6])
+ }
+
+ @Test
+ fun minus() {
+ val map = MutableIntIntMap()
+ map[1] = 1
+ map[2] = 2
+ map[3] = 3
+
+ map -= 1
+
+ assertEquals(2, map.size)
+ assertFalse(1 in map)
+ }
+
+ @Test
+ fun minusArray() {
+ val map = MutableIntIntMap()
+ map[1] = 1
+ map[2] = 2
+ map[3] = 3
+
+ map -= intArrayOf(3, 2)
+
+ assertEquals(1, map.size)
+ assertFalse(3 in map)
+ assertFalse(2 in map)
+ }
+
+ @Test
+ fun minusSet() {
+ val map = MutableIntIntMap()
+ map[1] = 1
+ map[2] = 2
+ map[3] = 3
+
+ map -= intSetOf(3, 2)
+
+ assertEquals(1, map.size)
+ assertFalse(3 in map)
+ assertFalse(2 in map)
+ }
+
+ @Test
+ fun minusList() {
+ val map = MutableIntIntMap()
+ map[1] = 1
+ map[2] = 2
+ map[3] = 3
+
+ map -= intListOf(3, 2)
+
+ assertEquals(1, map.size)
+ assertFalse(3 in map)
+ assertFalse(2 in map)
+ }
+
+ @Test
+ fun conditionalRemove() {
+ val map = MutableIntIntMap()
+ assertFalse(map.remove(1, 1))
+
+ map[1] = 1
+ assertTrue(map.remove(1, 1))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val map = MutableIntIntMap()
+
+ for (i in 0 until 1700) {
+ map[i.toInt()] = i.toInt()
+ }
+
+ assertEquals(1700, map.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val map = MutableIntIntMap()
+
+ for (j in 0 until i) {
+ map[j.toInt()] = j.toInt()
+ }
+
+ var counter = 0
+ map.forEach { key, value ->
+ assertEquals(key, value.toInt())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachKey() {
+ for (i in 0..48) {
+ val map = MutableIntIntMap()
+
+ for (j in 0 until i) {
+ map[j.toInt()] = j.toInt()
+ }
+
+ var counter = 0
+ val keys = BooleanArray(map.size)
+ map.forEachKey { key ->
+ keys[key.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ keys.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun forEachValue() {
+ for (i in 0..48) {
+ val map = MutableIntIntMap()
+
+ for (j in 0 until i) {
+ map[j.toInt()] = j.toInt()
+ }
+
+ var counter = 0
+ val values = BooleanArray(map.size)
+ map.forEachValue { value ->
+ values[value.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ values.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun clear() {
+ val map = MutableIntIntMap()
+
+ for (i in 0 until 32) {
+ map[i.toInt()] = i.toInt()
+ }
+
+ val capacity = map.capacity
+ map.clear()
+
+ assertEquals(0, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun string() {
+ val map = MutableIntIntMap()
+ assertEquals("{}", map.toString())
+
+ map[1] = 1
+ map[2] = 2
+ val oneValueString = 1.toString()
+ val twoValueString = 2.toString()
+ val oneKeyString = 1.toString()
+ val twoKeyString = 2.toString()
+ assertTrue(
+ "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+ "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+ )
+ }
+
+ @Test
+ fun equals() {
+ val map = MutableIntIntMap()
+ map[1] = 1
+
+ assertFalse(map.equals(null))
+ assertEquals(map, map)
+
+ val map2 = MutableIntIntMap()
+ assertNotEquals(map, map2)
+
+ map2[1] = 1
+ assertEquals(map, map2)
+ }
+
+ @Test
+ fun containsKey() {
+ val map = MutableIntIntMap()
+ map[1] = 1
+
+ assertTrue(map.containsKey(1))
+ assertFalse(map.containsKey(2))
+ }
+
+ @Test
+ fun contains() {
+ val map = MutableIntIntMap()
+ map[1] = 1
+
+ assertTrue(1 in map)
+ assertFalse(2 in map)
+ }
+
+ @Test
+ fun containsValue() {
+ val map = MutableIntIntMap()
+ map[1] = 1
+
+ assertTrue(map.containsValue(1))
+ assertFalse(map.containsValue(3))
+ }
+
+ @Test
+ fun empty() {
+ val map = MutableIntIntMap()
+ assertTrue(map.isEmpty())
+ assertFalse(map.isNotEmpty())
+ assertTrue(map.none())
+ assertFalse(map.any())
+
+ map[1] = 1
+
+ assertFalse(map.isEmpty())
+ assertTrue(map.isNotEmpty())
+ assertTrue(map.any())
+ assertFalse(map.none())
+ }
+
+ @Test
+ fun count() {
+ val map = MutableIntIntMap()
+ assertEquals(0, map.count())
+
+ map[1] = 1
+ assertEquals(1, map.count())
+
+ map[2] = 2
+ map[3] = 3
+ map[4] = 4
+ map[5] = 5
+ map[6] = 6
+
+ assertEquals(2, map.count { key, _ -> key <= 2 })
+ assertEquals(0, map.count { key, _ -> key < 0 })
+ }
+
+ @Test
+ fun any() {
+ val map = MutableIntIntMap()
+ map[1] = 1
+ map[2] = 2
+ map[3] = 3
+ map[4] = 4
+ map[5] = 5
+ map[6] = 6
+
+ assertTrue(map.any { key, _ -> key == 4 })
+ assertFalse(map.any { key, _ -> key < 0 })
+ }
+
+ @Test
+ fun all() {
+ val map = MutableIntIntMap()
+ map[1] = 1
+ map[2] = 2
+ map[3] = 3
+ map[4] = 4
+ map[5] = 5
+ map[6] = 6
+
+ assertTrue(map.all { key, value -> key > 0 && value >= 1 })
+ assertFalse(map.all { key, _ -> key < 6 })
+ }
+
+ @Test
+ fun trim() {
+ val map = MutableIntIntMap()
+ assertEquals(7, map.trim())
+
+ map[1] = 1
+ map[3] = 3
+
+ assertEquals(0, map.trim())
+
+ for (i in 0 until 1700) {
+ map[i.toInt()] = i.toInt()
+ }
+
+ assertEquals(2047, map.capacity)
+
+ // After removing these items, our capacity needs should go
+ // from 2047 down to 1023
+ for (i in 0 until 1700) {
+ if (i and 0x1 == 0x0) {
+ val s = i.toInt()
+ map.remove(s)
+ }
+ }
+
+ assertEquals(1024, map.trim())
+ assertEquals(0, map.trim())
+ }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt
index 66d89af..7a613eb 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt
@@ -22,6 +22,14 @@
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
class IntListTest {
private val list: MutableIntList = mutableIntListOf(1, 2, 3, 4, 5)
@@ -80,7 +88,7 @@
@Test
fun string() {
- assertEquals("[1, 2, 3, 4, 5]", list.toString())
+ assertEquals("[${1}, ${2}, ${3}, ${4}, ${5}]", list.toString())
assertEquals("[]", mutableIntListOf().toString())
}
@@ -334,7 +342,7 @@
@Test
fun fold() {
- assertEquals("12345", list.fold("") { acc, i -> acc + i.toString() })
+ assertEquals("12345", list.fold("") { acc, i -> acc + i.toInt().toString() })
}
@Test
@@ -342,14 +350,14 @@
assertEquals(
"01-12-23-34-45-",
list.foldIndexed("") { index, acc, i ->
- "$acc$index$i-"
+ "$acc$index${i.toInt()}-"
}
)
}
@Test
fun foldRight() {
- assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toString() })
+ assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toInt().toString() })
}
@Test
@@ -357,7 +365,7 @@
assertEquals(
"45-34-23-12-01-",
list.foldRightIndexed("") { index, i, acc ->
- "$acc$index$i-"
+ "$acc$index${i.toInt()}-"
}
)
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntLongMapTest.kt
new file mode 100644
index 0000000..734fb05
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntLongMapTest.kt
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class IntLongMapTest {
+ @Test
+ fun intLongMap() {
+ val map = MutableIntLongMap()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun testEmptyIntLongMap() {
+ val map = emptyIntLongMap()
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+
+ assertSame(emptyIntLongMap(), map)
+ }
+
+ @Test
+ fun intLongMapFunction() {
+ val map = mutableIntLongMapOf()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun zeroCapacityHashMap() {
+ val map = MutableIntLongMap(0)
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun intLongMapWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val map = MutableIntLongMap(1800)
+ assertEquals(4095, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun intLongMapPairsFunction() {
+ val map = mutableIntLongMapOf(
+ 1 to 1L,
+ 2 to 2L
+ )
+ assertEquals(2, map.size)
+ assertEquals(1L, map[1])
+ assertEquals(2L, map[2])
+ }
+
+ @Test
+ fun addToMap() {
+ val map = MutableIntLongMap()
+ map[1] = 1L
+
+ assertEquals(1, map.size)
+ assertEquals(1L, map[1])
+ }
+
+ @Test
+ fun addToSizedMap() {
+ val map = MutableIntLongMap(12)
+ map[1] = 1L
+
+ assertEquals(1, map.size)
+ assertEquals(1L, map[1])
+ }
+
+ @Test
+ fun addToSmallMap() {
+ val map = MutableIntLongMap(2)
+ map[1] = 1L
+
+ assertEquals(1, map.size)
+ assertEquals(7, map.capacity)
+ assertEquals(1L, map[1])
+ }
+
+ @Test
+ fun addToZeroCapacityMap() {
+ val map = MutableIntLongMap(0)
+ map[1] = 1L
+
+ assertEquals(1, map.size)
+ assertEquals(1L, map[1])
+ }
+
+ @Test
+ fun replaceExistingKey() {
+ val map = MutableIntLongMap()
+ map[1] = 1L
+ map[1] = 2L
+
+ assertEquals(1, map.size)
+ assertEquals(2L, map[1])
+ }
+
+ @Test
+ fun put() {
+ val map = MutableIntLongMap()
+
+ map.put(1, 1L)
+ assertEquals(1L, map[1])
+ map.put(1, 2L)
+ assertEquals(2L, map[1])
+ }
+
+ @Test
+ fun putAllArray() {
+ val map = MutableIntLongMap()
+ map[1] = 1L
+ map[2] = 2L
+
+ map.putAll(arrayOf(3 to 3L, 7 to 7L))
+
+ assertEquals(4, map.size)
+ assertEquals(3L, map[3])
+ assertEquals(7L, map[7])
+ }
+
+ @Test
+ fun plus() {
+ val map = MutableIntLongMap()
+ map += 1 to 1L
+
+ assertEquals(1, map.size)
+ assertEquals(1L, map[1])
+ }
+
+ @Test
+ fun plusArray() {
+ val map = MutableIntLongMap()
+ map += arrayOf(3 to 3L, 7 to 7L)
+
+ assertEquals(2, map.size)
+ assertEquals(3L, map[3])
+ assertEquals(7L, map[7])
+ }
+
+ @Test
+ fun findNonExistingKey() {
+ val map = MutableIntLongMap()
+ map[1] = 1L
+
+ assertFailsWith<NoSuchElementException> {
+ map[2]
+ }
+ }
+
+ @Test
+ fun getOrDefault() {
+ val map = MutableIntLongMap()
+ map[1] = 1L
+
+ assertEquals(2L, map.getOrDefault(2, 2L))
+ }
+
+ @Test
+ fun getOrElse() {
+ val map = MutableIntLongMap()
+ map[1] = 1L
+
+ assertEquals(3L, map.getOrElse(3) { 3L })
+ }
+
+ @Test
+ fun getOrPut() {
+ val map = MutableIntLongMap()
+ map[1] = 1L
+
+ var counter = 0
+ map.getOrPut(1) {
+ counter++
+ 2L
+ }
+ assertEquals(1L, map[1])
+ assertEquals(0, counter)
+
+ map.getOrPut(2) {
+ counter++
+ 2L
+ }
+ assertEquals(2L, map[2])
+ assertEquals(1, counter)
+
+ map.getOrPut(2) {
+ counter++
+ 3L
+ }
+ assertEquals(2L, map[2])
+ assertEquals(1, counter)
+
+ map.getOrPut(3) {
+ counter++
+ 3L
+ }
+ assertEquals(3L, map[3])
+ assertEquals(2, counter)
+ }
+
+ @Test
+ fun remove() {
+ val map = MutableIntLongMap()
+ map.remove(1)
+
+ map[1] = 1L
+ map.remove(1)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val map = MutableIntLongMap(6)
+ map[1] = 1L
+ map[2] = 2L
+ map[3] = 3L
+ map[4] = 4L
+ map[5] = 5L
+ map[6] = 6L
+
+ // Removing all the entries will mark the medata as deleted
+ map.remove(1)
+ map.remove(2)
+ map.remove(3)
+ map.remove(4)
+ map.remove(5)
+ map.remove(6)
+
+ assertEquals(0, map.size)
+
+ val capacity = map.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ map[7] = 7L
+
+ assertEquals(1, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun removeIf() {
+ val map = MutableIntLongMap()
+ map[1] = 1L
+ map[2] = 2L
+ map[3] = 3L
+ map[4] = 4L
+ map[5] = 5L
+ map[6] = 6L
+
+ map.removeIf { key, _ -> key == 1 || key == 3 }
+
+ assertEquals(4, map.size)
+ assertEquals(2L, map[2])
+ assertEquals(4L, map[4])
+ assertEquals(5L, map[5])
+ assertEquals(6L, map[6])
+ }
+
+ @Test
+ fun minus() {
+ val map = MutableIntLongMap()
+ map[1] = 1L
+ map[2] = 2L
+ map[3] = 3L
+
+ map -= 1
+
+ assertEquals(2, map.size)
+ assertFalse(1 in map)
+ }
+
+ @Test
+ fun minusArray() {
+ val map = MutableIntLongMap()
+ map[1] = 1L
+ map[2] = 2L
+ map[3] = 3L
+
+ map -= intArrayOf(3, 2)
+
+ assertEquals(1, map.size)
+ assertFalse(3 in map)
+ assertFalse(2 in map)
+ }
+
+ @Test
+ fun minusSet() {
+ val map = MutableIntLongMap()
+ map[1] = 1L
+ map[2] = 2L
+ map[3] = 3L
+
+ map -= intSetOf(3, 2)
+
+ assertEquals(1, map.size)
+ assertFalse(3 in map)
+ assertFalse(2 in map)
+ }
+
+ @Test
+ fun minusList() {
+ val map = MutableIntLongMap()
+ map[1] = 1L
+ map[2] = 2L
+ map[3] = 3L
+
+ map -= intListOf(3, 2)
+
+ assertEquals(1, map.size)
+ assertFalse(3 in map)
+ assertFalse(2 in map)
+ }
+
+ @Test
+ fun conditionalRemove() {
+ val map = MutableIntLongMap()
+ assertFalse(map.remove(1, 1L))
+
+ map[1] = 1L
+ assertTrue(map.remove(1, 1L))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val map = MutableIntLongMap()
+
+ for (i in 0 until 1700) {
+ map[i.toInt()] = i.toLong()
+ }
+
+ assertEquals(1700, map.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val map = MutableIntLongMap()
+
+ for (j in 0 until i) {
+ map[j.toInt()] = j.toLong()
+ }
+
+ var counter = 0
+ map.forEach { key, value ->
+ assertEquals(key, value.toInt())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachKey() {
+ for (i in 0..48) {
+ val map = MutableIntLongMap()
+
+ for (j in 0 until i) {
+ map[j.toInt()] = j.toLong()
+ }
+
+ var counter = 0
+ val keys = BooleanArray(map.size)
+ map.forEachKey { key ->
+ keys[key.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ keys.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun forEachValue() {
+ for (i in 0..48) {
+ val map = MutableIntLongMap()
+
+ for (j in 0 until i) {
+ map[j.toInt()] = j.toLong()
+ }
+
+ var counter = 0
+ val values = BooleanArray(map.size)
+ map.forEachValue { value ->
+ values[value.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ values.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun clear() {
+ val map = MutableIntLongMap()
+
+ for (i in 0 until 32) {
+ map[i.toInt()] = i.toLong()
+ }
+
+ val capacity = map.capacity
+ map.clear()
+
+ assertEquals(0, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun string() {
+ val map = MutableIntLongMap()
+ assertEquals("{}", map.toString())
+
+ map[1] = 1L
+ map[2] = 2L
+ val oneValueString = 1L.toString()
+ val twoValueString = 2L.toString()
+ val oneKeyString = 1.toString()
+ val twoKeyString = 2.toString()
+ assertTrue(
+ "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+ "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+ )
+ }
+
+ @Test
+ fun equals() {
+ val map = MutableIntLongMap()
+ map[1] = 1L
+
+ assertFalse(map.equals(null))
+ assertEquals(map, map)
+
+ val map2 = MutableIntLongMap()
+ assertNotEquals(map, map2)
+
+ map2[1] = 1L
+ assertEquals(map, map2)
+ }
+
+ @Test
+ fun containsKey() {
+ val map = MutableIntLongMap()
+ map[1] = 1L
+
+ assertTrue(map.containsKey(1))
+ assertFalse(map.containsKey(2))
+ }
+
+ @Test
+ fun contains() {
+ val map = MutableIntLongMap()
+ map[1] = 1L
+
+ assertTrue(1 in map)
+ assertFalse(2 in map)
+ }
+
+ @Test
+ fun containsValue() {
+ val map = MutableIntLongMap()
+ map[1] = 1L
+
+ assertTrue(map.containsValue(1L))
+ assertFalse(map.containsValue(3L))
+ }
+
+ @Test
+ fun empty() {
+ val map = MutableIntLongMap()
+ assertTrue(map.isEmpty())
+ assertFalse(map.isNotEmpty())
+ assertTrue(map.none())
+ assertFalse(map.any())
+
+ map[1] = 1L
+
+ assertFalse(map.isEmpty())
+ assertTrue(map.isNotEmpty())
+ assertTrue(map.any())
+ assertFalse(map.none())
+ }
+
+ @Test
+ fun count() {
+ val map = MutableIntLongMap()
+ assertEquals(0, map.count())
+
+ map[1] = 1L
+ assertEquals(1, map.count())
+
+ map[2] = 2L
+ map[3] = 3L
+ map[4] = 4L
+ map[5] = 5L
+ map[6] = 6L
+
+ assertEquals(2, map.count { key, _ -> key <= 2 })
+ assertEquals(0, map.count { key, _ -> key < 0 })
+ }
+
+ @Test
+ fun any() {
+ val map = MutableIntLongMap()
+ map[1] = 1L
+ map[2] = 2L
+ map[3] = 3L
+ map[4] = 4L
+ map[5] = 5L
+ map[6] = 6L
+
+ assertTrue(map.any { key, _ -> key == 4 })
+ assertFalse(map.any { key, _ -> key < 0 })
+ }
+
+ @Test
+ fun all() {
+ val map = MutableIntLongMap()
+ map[1] = 1L
+ map[2] = 2L
+ map[3] = 3L
+ map[4] = 4L
+ map[5] = 5L
+ map[6] = 6L
+
+ assertTrue(map.all { key, value -> key > 0 && value >= 1L })
+ assertFalse(map.all { key, _ -> key < 6 })
+ }
+
+ @Test
+ fun trim() {
+ val map = MutableIntLongMap()
+ assertEquals(7, map.trim())
+
+ map[1] = 1L
+ map[3] = 3L
+
+ assertEquals(0, map.trim())
+
+ for (i in 0 until 1700) {
+ map[i.toInt()] = i.toLong()
+ }
+
+ assertEquals(2047, map.capacity)
+
+ // After removing these items, our capacity needs should go
+ // from 2047 down to 1023
+ for (i in 0 until 1700) {
+ if (i and 0x1 == 0x0) {
+ val s = i.toInt()
+ map.remove(s)
+ }
+ }
+
+ assertEquals(1024, map.trim())
+ assertEquals(0, map.trim())
+ }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntObjectMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntObjectMapTest.kt
new file mode 100644
index 0000000..40038d2
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntObjectMapTest.kt
@@ -0,0 +1,632 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class IntObjectMapTest {
+ @Test
+ fun intObjectMap() {
+ val map = MutableIntObjectMap<String>()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun testEmptyIntObjectMap() {
+ val map = emptyIntObjectMap<String>()
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+
+ assertSame(emptyIntObjectMap<String>(), map)
+ }
+
+ @Test
+ fun intObjectMapFunction() {
+ val map = mutableIntObjectMapOf<String>()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun zeroCapacityHashMap() {
+ val map = MutableIntObjectMap<String>(0)
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun intObjectMapWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val map = MutableIntObjectMap<String>(1800)
+ assertEquals(4095, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun intObjectMapPairsFunction() {
+ val map = mutableIntObjectMapOf(
+ 1 to "World",
+ 2 to "Monde"
+ )
+ assertEquals(2, map.size)
+ assertEquals("World", map[1])
+ assertEquals("Monde", map[2])
+ }
+
+ @Test
+ fun addToMap() {
+ val map = MutableIntObjectMap<String>()
+ map[1] = "World"
+
+ assertEquals(1, map.size)
+ assertEquals("World", map[1])
+ }
+
+ @Test
+ fun insertIndex0() {
+ val map = MutableIntObjectMap<String>()
+ map.put(1, "World")
+ assertEquals("World", map[1])
+ }
+
+ @Test
+ fun addToSizedMap() {
+ val map = MutableIntObjectMap<String>(12)
+ map[1] = "World"
+
+ assertEquals(1, map.size)
+ assertEquals("World", map[1])
+ }
+
+ @Test
+ fun addToSmallMap() {
+ val map = MutableIntObjectMap<String>(2)
+ map[1] = "World"
+
+ assertEquals(1, map.size)
+ assertEquals(7, map.capacity)
+ assertEquals("World", map[1])
+ }
+
+ @Test
+ fun addToZeroCapacityMap() {
+ val map = MutableIntObjectMap<String>(0)
+ map[1] = "World"
+
+ assertEquals(1, map.size)
+ assertEquals("World", map[1])
+ }
+
+ @Test
+ fun replaceExistingKey() {
+ val map = MutableIntObjectMap<String>()
+ map[1] = "World"
+ map[1] = "Monde"
+
+ assertEquals(1, map.size)
+ assertEquals("Monde", map[1])
+ }
+
+ @Test
+ fun put() {
+ val map = MutableIntObjectMap<String?>()
+
+ assertNull(map.put(1, "World"))
+ assertEquals("World", map.put(1, "Monde"))
+ assertNull(map.put(2, null))
+ assertNull(map.put(2, "Monde"))
+ }
+
+ @Test
+ fun putAllArray() {
+ val map = MutableIntObjectMap<String?>()
+ map[1] = "World"
+ map[2] = null
+
+ map.putAll(arrayOf(3 to "Welt", 7 to "Mundo"))
+
+ assertEquals(4, map.size)
+ assertEquals("Welt", map[3])
+ assertEquals("Mundo", map[7])
+ }
+
+ @Test
+ fun putAllMap() {
+ val map = MutableIntObjectMap<String?>()
+ map[1] = "World"
+ map[2] = null
+
+ map.putAll(mutableIntObjectMapOf(3 to "Welt", 7 to "Mundo"))
+
+ assertEquals(4, map.size)
+ assertEquals("Welt", map[3])
+ assertEquals("Mundo", map[7])
+ }
+
+ @Test
+ fun plus() {
+ val map = MutableIntObjectMap<String>()
+ map += 1 to "World"
+
+ assertEquals(1, map.size)
+ assertEquals("World", map[1])
+ }
+
+ @Test
+ fun plusMap() {
+ val map = MutableIntObjectMap<String>()
+ map += intObjectMapOf(3 to "Welt", 7 to "Mundo")
+
+ assertEquals(2, map.size)
+ assertEquals("Welt", map[3])
+ assertEquals("Mundo", map[7])
+ }
+
+ @Test
+ fun plusArray() {
+ val map = MutableIntObjectMap<String>()
+ map += arrayOf(3 to "Welt", 7 to "Mundo")
+
+ assertEquals(2, map.size)
+ assertEquals("Welt", map[3])
+ assertEquals("Mundo", map[7])
+ }
+
+ @Test
+ fun nullValue() {
+ val map = MutableIntObjectMap<String?>()
+ map[1] = null
+
+ assertEquals(1, map.size)
+ assertNull(map[1])
+ }
+
+ @Test
+ fun findNonExistingKey() {
+ val map = MutableIntObjectMap<String?>()
+ map[1] = "World"
+
+ assertNull(map[2])
+ }
+
+ @Test
+ fun getOrDefault() {
+ val map = MutableIntObjectMap<String?>()
+ map[1] = "World"
+
+ assertEquals("Monde", map.getOrDefault(2, "Monde"))
+ }
+
+ @Test
+ fun getOrElse() {
+ val map = MutableIntObjectMap<String?>()
+ map[1] = "World"
+ map[2] = null
+
+ assertEquals("Monde", map.getOrElse(2) { "Monde" })
+ assertEquals("Welt", map.getOrElse(3) { "Welt" })
+ }
+
+ @Test
+ fun getOrPut() {
+ val map = MutableIntObjectMap<String?>()
+ map[1] = "World"
+
+ var counter = 0
+ map.getOrPut(1) {
+ counter++
+ "Monde"
+ }
+ assertEquals("World", map[1])
+ assertEquals(0, counter)
+
+ map.getOrPut(2) {
+ counter++
+ "Monde"
+ }
+ assertEquals("Monde", map[2])
+ assertEquals(1, counter)
+
+ map.getOrPut(2) {
+ counter++
+ "Welt"
+ }
+ assertEquals("Monde", map[2])
+ assertEquals(1, counter)
+
+ map.getOrPut(3) {
+ counter++
+ null
+ }
+ assertNull(map[3])
+ assertEquals(2, counter)
+
+ map.getOrPut(3) {
+ counter++
+ "Welt"
+ }
+ assertEquals("Welt", map[3])
+ assertEquals(3, counter)
+ }
+
+ @Test
+ fun remove() {
+ val map = MutableIntObjectMap<String?>()
+ assertNull(map.remove(1))
+
+ map[1] = "World"
+ assertEquals("World", map.remove(1))
+ assertEquals(0, map.size)
+
+ map[1] = null
+ assertNull(map.remove(1))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val map = MutableIntObjectMap<String>(6)
+ map[1] = "World"
+ map[2] = "Monde"
+ map[3] = "Welt"
+ map[4] = "Sekai"
+ map[5] = "Mondo"
+ map[6] = "Sesang"
+
+ // Removing all the entries will mark the medata as deleted
+ map.remove(1)
+ map.remove(2)
+ map.remove(3)
+ map.remove(4)
+ map.remove(5)
+ map.remove(6)
+
+ assertEquals(0, map.size)
+
+ val capacity = map.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ map[7] = "Mundo"
+
+ assertEquals(1, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun removeIf() {
+ val map = MutableIntObjectMap<String>()
+ map[1] = "World"
+ map[2] = "Monde"
+ map[3] = "Welt"
+ map[4] = "Sekai"
+ map[5] = "Mondo"
+ map[6] = "Sesang"
+
+ map.removeIf { key, value ->
+ key == 1 || key == 3 || value.startsWith('S')
+ }
+
+ assertEquals(2, map.size)
+ assertEquals("Monde", map[2])
+ assertEquals("Mondo", map[5])
+ }
+
+ @Test
+ fun minus() {
+ val map = MutableIntObjectMap<String>()
+ map[1] = "World"
+ map[2] = "Monde"
+ map[3] = "Welt"
+
+ map -= 1
+
+ assertEquals(2, map.size)
+ assertNull(map[1])
+ }
+
+ @Test
+ fun minusArray() {
+ val map = MutableIntObjectMap<String>()
+ map[1] = "World"
+ map[2] = "Monde"
+ map[3] = "Welt"
+
+ map -= intArrayOf(3, 2)
+
+ assertEquals(1, map.size)
+ assertNull(map[3])
+ assertNull(map[2])
+ }
+
+ @Test
+ fun minusSet() {
+ val map = MutableIntObjectMap<String>()
+ map[1] = "World"
+ map[2] = "Monde"
+ map[3] = "Welt"
+
+ map -= intSetOf(3, 2)
+
+ assertEquals(1, map.size)
+ assertNull(map[3])
+ assertNull(map[2])
+ }
+
+ @Test
+ fun minusList() {
+ val map = MutableIntObjectMap<String>()
+ map[1] = "World"
+ map[2] = "Monde"
+ map[3] = "Welt"
+
+ map -= intListOf(3, 2)
+
+ assertEquals(1, map.size)
+ assertNull(map[3])
+ assertNull(map[2])
+ }
+
+ @Test
+ fun conditionalRemove() {
+ val map = MutableIntObjectMap<String?>()
+ assertFalse(map.remove(1, "World"))
+
+ map[1] = "World"
+ assertTrue(map.remove(1, "World"))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val map = MutableIntObjectMap<String>()
+
+ for (i in 0 until 1700) {
+ map[i.toInt()] = i.toString()
+ }
+
+ assertEquals(1700, map.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val map = MutableIntObjectMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toInt()] = j.toString()
+ }
+
+ var counter = 0
+ map.forEach { key, value ->
+ assertEquals(key.toInt().toString(), value)
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachKey() {
+ for (i in 0..48) {
+ val map = MutableIntObjectMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toInt()] = j.toString()
+ }
+
+ var counter = 0
+ map.forEachKey { key ->
+ assertEquals(key.toInt().toString(), map[key])
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachValue() {
+ for (i in 0..48) {
+ val map = MutableIntObjectMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toInt()] = j.toString()
+ }
+
+ var counter = 0
+ map.forEachValue { value ->
+ assertNotNull(value.toIntOrNull())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun clear() {
+ val map = MutableIntObjectMap<String>()
+
+ for (i in 0 until 32) {
+ map[i.toInt()] = i.toString()
+ }
+
+ val capacity = map.capacity
+ map.clear()
+
+ assertEquals(0, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun string() {
+ val map = MutableIntObjectMap<String?>()
+ assertEquals("{}", map.toString())
+
+ map[1] = "World"
+ map[2] = "Monde"
+ val oneKey = 1.toString()
+ val twoKey = 2.toString()
+ assertTrue(
+ "{$oneKey=World, $twoKey=Monde}" == map.toString() ||
+ "{$twoKey=Monde, $oneKey=World}" == map.toString()
+ )
+
+ map.clear()
+ map[1] = null
+ assertEquals("{$oneKey=null}", map.toString())
+
+ val selfAsValueMap = MutableIntObjectMap<Any>()
+ selfAsValueMap[1] = selfAsValueMap
+ assertEquals("{$oneKey=(this)}", selfAsValueMap.toString())
+ }
+
+ @Test
+ fun equals() {
+ val map = MutableIntObjectMap<String?>()
+ map[1] = "World"
+ map[2] = null
+
+ assertFalse(map.equals(null))
+ assertEquals(map, map)
+
+ val map2 = MutableIntObjectMap<String?>()
+ map2[2] = null
+
+ assertNotEquals(map, map2)
+
+ map2[1] = "World"
+ assertEquals(map, map2)
+ }
+
+ @Test
+ fun containsKey() {
+ val map = MutableIntObjectMap<String?>()
+ map[1] = "World"
+ map[2] = null
+
+ assertTrue(map.containsKey(1))
+ assertFalse(map.containsKey(3))
+ }
+
+ @Test
+ fun contains() {
+ val map = MutableIntObjectMap<String?>()
+ map[1] = "World"
+ map[2] = null
+
+ assertTrue(1 in map)
+ assertFalse(3 in map)
+ }
+
+ @Test
+ fun containsValue() {
+ val map = MutableIntObjectMap<String?>()
+ map[1] = "World"
+ map[2] = null
+
+ assertTrue(map.containsValue("World"))
+ assertTrue(map.containsValue(null))
+ assertFalse(map.containsValue("Monde"))
+ }
+
+ @Test
+ fun empty() {
+ val map = MutableIntObjectMap<String?>()
+ assertTrue(map.isEmpty())
+ assertFalse(map.isNotEmpty())
+ assertTrue(map.none())
+ assertFalse(map.any())
+
+ map[1] = "World"
+
+ assertFalse(map.isEmpty())
+ assertTrue(map.isNotEmpty())
+ assertTrue(map.any())
+ assertFalse(map.none())
+ }
+
+ @Test
+ fun count() {
+ val map = MutableIntObjectMap<String>()
+ assertEquals(0, map.count())
+
+ map[1] = "World"
+ assertEquals(1, map.count())
+
+ map[2] = "Monde"
+ map[3] = "Welt"
+ map[4] = "Sekai"
+ map[5] = "Mondo"
+ map[6] = "Sesang"
+
+ assertEquals(2, map.count { key, _ -> key < 3 })
+ assertEquals(0, map.count { key, _ -> key < 0 })
+ }
+
+ @Test
+ fun any() {
+ val map = MutableIntObjectMap<String>()
+ map[1] = "World"
+ map[2] = "Monde"
+ map[3] = "Welt"
+ map[4] = "Sekai"
+ map[5] = "Mondo"
+ map[6] = "Sesang"
+
+ assertTrue(map.any { key, _ -> key > 5 })
+ assertFalse(map.any { key, _ -> key < 0 })
+ }
+
+ @Test
+ fun all() {
+ val map = MutableIntObjectMap<String>()
+ map[1] = "World"
+ map[2] = "Monde"
+ map[3] = "Welt"
+ map[4] = "Sekai"
+ map[5] = "Mondo"
+ map[6] = "Sesang"
+
+ assertTrue(map.all { key, value -> key < 7 && value.length > 0 })
+ assertFalse(map.all { key, _ -> key < 6 })
+ }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt
index 9d326e1..9d55a17 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt
@@ -23,6 +23,14 @@
import kotlin.test.assertSame
import kotlin.test.assertTrue
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
class IntSetTest {
@Test
fun emptyIntSetConstructor() {
@@ -147,9 +155,9 @@
val set = MutableIntSet()
set += 1
set += 2
- var element: Int = Int.MIN_VALUE
- var otherElement: Int = Int.MIN_VALUE
- set.forEach { if (element == Int.MIN_VALUE) element = it else otherElement = it }
+ var element: Int = -1
+ var otherElement: Int = -1
+ set.forEach { if (element == -1) element = it else otherElement = it }
assertEquals(element, set.first())
set -= element
assertEquals(otherElement, set.first())
@@ -333,8 +341,8 @@
set += 1
set += 5
assertTrue(
- "[1, 5]" == set.toString() ||
- "[5, 1]" == set.toString()
+ "[${1}, ${5}]" == set.toString() ||
+ "[${5}, ${1}]" == set.toString()
)
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongFloatMapTest.kt
new file mode 100644
index 0000000..b395753
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongFloatMapTest.kt
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class LongFloatMapTest {
+ @Test
+ fun longFloatMap() {
+ val map = MutableLongFloatMap()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun testEmptyLongFloatMap() {
+ val map = emptyLongFloatMap()
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+
+ assertSame(emptyLongFloatMap(), map)
+ }
+
+ @Test
+ fun longFloatMapFunction() {
+ val map = mutableLongFloatMapOf()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun zeroCapacityHashMap() {
+ val map = MutableLongFloatMap(0)
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun longFloatMapWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val map = MutableLongFloatMap(1800)
+ assertEquals(4095, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun longFloatMapPairsFunction() {
+ val map = mutableLongFloatMapOf(
+ 1L to 1f,
+ 2L to 2f
+ )
+ assertEquals(2, map.size)
+ assertEquals(1f, map[1L])
+ assertEquals(2f, map[2L])
+ }
+
+ @Test
+ fun addToMap() {
+ val map = MutableLongFloatMap()
+ map[1L] = 1f
+
+ assertEquals(1, map.size)
+ assertEquals(1f, map[1L])
+ }
+
+ @Test
+ fun addToSizedMap() {
+ val map = MutableLongFloatMap(12)
+ map[1L] = 1f
+
+ assertEquals(1, map.size)
+ assertEquals(1f, map[1L])
+ }
+
+ @Test
+ fun addToSmallMap() {
+ val map = MutableLongFloatMap(2)
+ map[1L] = 1f
+
+ assertEquals(1, map.size)
+ assertEquals(7, map.capacity)
+ assertEquals(1f, map[1L])
+ }
+
+ @Test
+ fun addToZeroCapacityMap() {
+ val map = MutableLongFloatMap(0)
+ map[1L] = 1f
+
+ assertEquals(1, map.size)
+ assertEquals(1f, map[1L])
+ }
+
+ @Test
+ fun replaceExistingKey() {
+ val map = MutableLongFloatMap()
+ map[1L] = 1f
+ map[1L] = 2f
+
+ assertEquals(1, map.size)
+ assertEquals(2f, map[1L])
+ }
+
+ @Test
+ fun put() {
+ val map = MutableLongFloatMap()
+
+ map.put(1L, 1f)
+ assertEquals(1f, map[1L])
+ map.put(1L, 2f)
+ assertEquals(2f, map[1L])
+ }
+
+ @Test
+ fun putAllArray() {
+ val map = MutableLongFloatMap()
+ map[1L] = 1f
+ map[2L] = 2f
+
+ map.putAll(arrayOf(3L to 3f, 7L to 7f))
+
+ assertEquals(4, map.size)
+ assertEquals(3f, map[3L])
+ assertEquals(7f, map[7L])
+ }
+
+ @Test
+ fun plus() {
+ val map = MutableLongFloatMap()
+ map += 1L to 1f
+
+ assertEquals(1, map.size)
+ assertEquals(1f, map[1L])
+ }
+
+ @Test
+ fun plusArray() {
+ val map = MutableLongFloatMap()
+ map += arrayOf(3L to 3f, 7L to 7f)
+
+ assertEquals(2, map.size)
+ assertEquals(3f, map[3L])
+ assertEquals(7f, map[7L])
+ }
+
+ @Test
+ fun findNonExistingKey() {
+ val map = MutableLongFloatMap()
+ map[1L] = 1f
+
+ assertFailsWith<NoSuchElementException> {
+ map[2L]
+ }
+ }
+
+ @Test
+ fun getOrDefault() {
+ val map = MutableLongFloatMap()
+ map[1L] = 1f
+
+ assertEquals(2f, map.getOrDefault(2L, 2f))
+ }
+
+ @Test
+ fun getOrElse() {
+ val map = MutableLongFloatMap()
+ map[1L] = 1f
+
+ assertEquals(3f, map.getOrElse(3L) { 3f })
+ }
+
+ @Test
+ fun getOrPut() {
+ val map = MutableLongFloatMap()
+ map[1L] = 1f
+
+ var counter = 0
+ map.getOrPut(1L) {
+ counter++
+ 2f
+ }
+ assertEquals(1f, map[1L])
+ assertEquals(0, counter)
+
+ map.getOrPut(2L) {
+ counter++
+ 2f
+ }
+ assertEquals(2f, map[2L])
+ assertEquals(1, counter)
+
+ map.getOrPut(2L) {
+ counter++
+ 3f
+ }
+ assertEquals(2f, map[2L])
+ assertEquals(1, counter)
+
+ map.getOrPut(3L) {
+ counter++
+ 3f
+ }
+ assertEquals(3f, map[3L])
+ assertEquals(2, counter)
+ }
+
+ @Test
+ fun remove() {
+ val map = MutableLongFloatMap()
+ map.remove(1L)
+
+ map[1L] = 1f
+ map.remove(1L)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val map = MutableLongFloatMap(6)
+ map[1L] = 1f
+ map[2L] = 2f
+ map[3L] = 3f
+ map[4L] = 4f
+ map[5L] = 5f
+ map[6L] = 6f
+
+ // Removing all the entries will mark the medata as deleted
+ map.remove(1L)
+ map.remove(2L)
+ map.remove(3L)
+ map.remove(4L)
+ map.remove(5L)
+ map.remove(6L)
+
+ assertEquals(0, map.size)
+
+ val capacity = map.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ map[7L] = 7f
+
+ assertEquals(1, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun removeIf() {
+ val map = MutableLongFloatMap()
+ map[1L] = 1f
+ map[2L] = 2f
+ map[3L] = 3f
+ map[4L] = 4f
+ map[5L] = 5f
+ map[6L] = 6f
+
+ map.removeIf { key, _ -> key == 1L || key == 3L }
+
+ assertEquals(4, map.size)
+ assertEquals(2f, map[2L])
+ assertEquals(4f, map[4L])
+ assertEquals(5f, map[5L])
+ assertEquals(6f, map[6L])
+ }
+
+ @Test
+ fun minus() {
+ val map = MutableLongFloatMap()
+ map[1L] = 1f
+ map[2L] = 2f
+ map[3L] = 3f
+
+ map -= 1L
+
+ assertEquals(2, map.size)
+ assertFalse(1L in map)
+ }
+
+ @Test
+ fun minusArray() {
+ val map = MutableLongFloatMap()
+ map[1L] = 1f
+ map[2L] = 2f
+ map[3L] = 3f
+
+ map -= longArrayOf(3L, 2L)
+
+ assertEquals(1, map.size)
+ assertFalse(3L in map)
+ assertFalse(2L in map)
+ }
+
+ @Test
+ fun minusSet() {
+ val map = MutableLongFloatMap()
+ map[1L] = 1f
+ map[2L] = 2f
+ map[3L] = 3f
+
+ map -= longSetOf(3L, 2L)
+
+ assertEquals(1, map.size)
+ assertFalse(3L in map)
+ assertFalse(2L in map)
+ }
+
+ @Test
+ fun minusList() {
+ val map = MutableLongFloatMap()
+ map[1L] = 1f
+ map[2L] = 2f
+ map[3L] = 3f
+
+ map -= longListOf(3L, 2L)
+
+ assertEquals(1, map.size)
+ assertFalse(3L in map)
+ assertFalse(2L in map)
+ }
+
+ @Test
+ fun conditionalRemove() {
+ val map = MutableLongFloatMap()
+ assertFalse(map.remove(1L, 1f))
+
+ map[1L] = 1f
+ assertTrue(map.remove(1L, 1f))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val map = MutableLongFloatMap()
+
+ for (i in 0 until 1700) {
+ map[i.toLong()] = i.toFloat()
+ }
+
+ assertEquals(1700, map.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val map = MutableLongFloatMap()
+
+ for (j in 0 until i) {
+ map[j.toLong()] = j.toFloat()
+ }
+
+ var counter = 0
+ map.forEach { key, value ->
+ assertEquals(key, value.toLong())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachKey() {
+ for (i in 0..48) {
+ val map = MutableLongFloatMap()
+
+ for (j in 0 until i) {
+ map[j.toLong()] = j.toFloat()
+ }
+
+ var counter = 0
+ val keys = BooleanArray(map.size)
+ map.forEachKey { key ->
+ keys[key.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ keys.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun forEachValue() {
+ for (i in 0..48) {
+ val map = MutableLongFloatMap()
+
+ for (j in 0 until i) {
+ map[j.toLong()] = j.toFloat()
+ }
+
+ var counter = 0
+ val values = BooleanArray(map.size)
+ map.forEachValue { value ->
+ values[value.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ values.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun clear() {
+ val map = MutableLongFloatMap()
+
+ for (i in 0 until 32) {
+ map[i.toLong()] = i.toFloat()
+ }
+
+ val capacity = map.capacity
+ map.clear()
+
+ assertEquals(0, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun string() {
+ val map = MutableLongFloatMap()
+ assertEquals("{}", map.toString())
+
+ map[1L] = 1f
+ map[2L] = 2f
+ val oneValueString = 1f.toString()
+ val twoValueString = 2f.toString()
+ val oneKeyString = 1L.toString()
+ val twoKeyString = 2L.toString()
+ assertTrue(
+ "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+ "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+ )
+ }
+
+ @Test
+ fun equals() {
+ val map = MutableLongFloatMap()
+ map[1L] = 1f
+
+ assertFalse(map.equals(null))
+ assertEquals(map, map)
+
+ val map2 = MutableLongFloatMap()
+ assertNotEquals(map, map2)
+
+ map2[1L] = 1f
+ assertEquals(map, map2)
+ }
+
+ @Test
+ fun containsKey() {
+ val map = MutableLongFloatMap()
+ map[1L] = 1f
+
+ assertTrue(map.containsKey(1L))
+ assertFalse(map.containsKey(2L))
+ }
+
+ @Test
+ fun contains() {
+ val map = MutableLongFloatMap()
+ map[1L] = 1f
+
+ assertTrue(1L in map)
+ assertFalse(2L in map)
+ }
+
+ @Test
+ fun containsValue() {
+ val map = MutableLongFloatMap()
+ map[1L] = 1f
+
+ assertTrue(map.containsValue(1f))
+ assertFalse(map.containsValue(3f))
+ }
+
+ @Test
+ fun empty() {
+ val map = MutableLongFloatMap()
+ assertTrue(map.isEmpty())
+ assertFalse(map.isNotEmpty())
+ assertTrue(map.none())
+ assertFalse(map.any())
+
+ map[1L] = 1f
+
+ assertFalse(map.isEmpty())
+ assertTrue(map.isNotEmpty())
+ assertTrue(map.any())
+ assertFalse(map.none())
+ }
+
+ @Test
+ fun count() {
+ val map = MutableLongFloatMap()
+ assertEquals(0, map.count())
+
+ map[1L] = 1f
+ assertEquals(1, map.count())
+
+ map[2L] = 2f
+ map[3L] = 3f
+ map[4L] = 4f
+ map[5L] = 5f
+ map[6L] = 6f
+
+ assertEquals(2, map.count { key, _ -> key <= 2L })
+ assertEquals(0, map.count { key, _ -> key < 0L })
+ }
+
+ @Test
+ fun any() {
+ val map = MutableLongFloatMap()
+ map[1L] = 1f
+ map[2L] = 2f
+ map[3L] = 3f
+ map[4L] = 4f
+ map[5L] = 5f
+ map[6L] = 6f
+
+ assertTrue(map.any { key, _ -> key == 4L })
+ assertFalse(map.any { key, _ -> key < 0L })
+ }
+
+ @Test
+ fun all() {
+ val map = MutableLongFloatMap()
+ map[1L] = 1f
+ map[2L] = 2f
+ map[3L] = 3f
+ map[4L] = 4f
+ map[5L] = 5f
+ map[6L] = 6f
+
+ assertTrue(map.all { key, value -> key > 0L && value >= 1f })
+ assertFalse(map.all { key, _ -> key < 6L })
+ }
+
+ @Test
+ fun trim() {
+ val map = MutableLongFloatMap()
+ assertEquals(7, map.trim())
+
+ map[1L] = 1f
+ map[3L] = 3f
+
+ assertEquals(0, map.trim())
+
+ for (i in 0 until 1700) {
+ map[i.toLong()] = i.toFloat()
+ }
+
+ assertEquals(2047, map.capacity)
+
+ // After removing these items, our capacity needs should go
+ // from 2047 down to 1023
+ for (i in 0 until 1700) {
+ if (i and 0x1 == 0x0) {
+ val s = i.toLong()
+ map.remove(s)
+ }
+ }
+
+ assertEquals(1024, map.trim())
+ assertEquals(0, map.trim())
+ }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongIntMapTest.kt
new file mode 100644
index 0000000..cf64c64
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongIntMapTest.kt
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class LongIntMapTest {
+ @Test
+ fun longIntMap() {
+ val map = MutableLongIntMap()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun testEmptyLongIntMap() {
+ val map = emptyLongIntMap()
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+
+ assertSame(emptyLongIntMap(), map)
+ }
+
+ @Test
+ fun longIntMapFunction() {
+ val map = mutableLongIntMapOf()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun zeroCapacityHashMap() {
+ val map = MutableLongIntMap(0)
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun longIntMapWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val map = MutableLongIntMap(1800)
+ assertEquals(4095, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun longIntMapPairsFunction() {
+ val map = mutableLongIntMapOf(
+ 1L to 1,
+ 2L to 2
+ )
+ assertEquals(2, map.size)
+ assertEquals(1, map[1L])
+ assertEquals(2, map[2L])
+ }
+
+ @Test
+ fun addToMap() {
+ val map = MutableLongIntMap()
+ map[1L] = 1
+
+ assertEquals(1, map.size)
+ assertEquals(1, map[1L])
+ }
+
+ @Test
+ fun addToSizedMap() {
+ val map = MutableLongIntMap(12)
+ map[1L] = 1
+
+ assertEquals(1, map.size)
+ assertEquals(1, map[1L])
+ }
+
+ @Test
+ fun addToSmallMap() {
+ val map = MutableLongIntMap(2)
+ map[1L] = 1
+
+ assertEquals(1, map.size)
+ assertEquals(7, map.capacity)
+ assertEquals(1, map[1L])
+ }
+
+ @Test
+ fun addToZeroCapacityMap() {
+ val map = MutableLongIntMap(0)
+ map[1L] = 1
+
+ assertEquals(1, map.size)
+ assertEquals(1, map[1L])
+ }
+
+ @Test
+ fun replaceExistingKey() {
+ val map = MutableLongIntMap()
+ map[1L] = 1
+ map[1L] = 2
+
+ assertEquals(1, map.size)
+ assertEquals(2, map[1L])
+ }
+
+ @Test
+ fun put() {
+ val map = MutableLongIntMap()
+
+ map.put(1L, 1)
+ assertEquals(1, map[1L])
+ map.put(1L, 2)
+ assertEquals(2, map[1L])
+ }
+
+ @Test
+ fun putAllArray() {
+ val map = MutableLongIntMap()
+ map[1L] = 1
+ map[2L] = 2
+
+ map.putAll(arrayOf(3L to 3, 7L to 7))
+
+ assertEquals(4, map.size)
+ assertEquals(3, map[3L])
+ assertEquals(7, map[7L])
+ }
+
+ @Test
+ fun plus() {
+ val map = MutableLongIntMap()
+ map += 1L to 1
+
+ assertEquals(1, map.size)
+ assertEquals(1, map[1L])
+ }
+
+ @Test
+ fun plusArray() {
+ val map = MutableLongIntMap()
+ map += arrayOf(3L to 3, 7L to 7)
+
+ assertEquals(2, map.size)
+ assertEquals(3, map[3L])
+ assertEquals(7, map[7L])
+ }
+
+ @Test
+ fun findNonExistingKey() {
+ val map = MutableLongIntMap()
+ map[1L] = 1
+
+ assertFailsWith<NoSuchElementException> {
+ map[2L]
+ }
+ }
+
+ @Test
+ fun getOrDefault() {
+ val map = MutableLongIntMap()
+ map[1L] = 1
+
+ assertEquals(2, map.getOrDefault(2L, 2))
+ }
+
+ @Test
+ fun getOrElse() {
+ val map = MutableLongIntMap()
+ map[1L] = 1
+
+ assertEquals(3, map.getOrElse(3L) { 3 })
+ }
+
+ @Test
+ fun getOrPut() {
+ val map = MutableLongIntMap()
+ map[1L] = 1
+
+ var counter = 0
+ map.getOrPut(1L) {
+ counter++
+ 2
+ }
+ assertEquals(1, map[1L])
+ assertEquals(0, counter)
+
+ map.getOrPut(2L) {
+ counter++
+ 2
+ }
+ assertEquals(2, map[2L])
+ assertEquals(1, counter)
+
+ map.getOrPut(2L) {
+ counter++
+ 3
+ }
+ assertEquals(2, map[2L])
+ assertEquals(1, counter)
+
+ map.getOrPut(3L) {
+ counter++
+ 3
+ }
+ assertEquals(3, map[3L])
+ assertEquals(2, counter)
+ }
+
+ @Test
+ fun remove() {
+ val map = MutableLongIntMap()
+ map.remove(1L)
+
+ map[1L] = 1
+ map.remove(1L)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val map = MutableLongIntMap(6)
+ map[1L] = 1
+ map[2L] = 2
+ map[3L] = 3
+ map[4L] = 4
+ map[5L] = 5
+ map[6L] = 6
+
+ // Removing all the entries will mark the medata as deleted
+ map.remove(1L)
+ map.remove(2L)
+ map.remove(3L)
+ map.remove(4L)
+ map.remove(5L)
+ map.remove(6L)
+
+ assertEquals(0, map.size)
+
+ val capacity = map.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ map[7L] = 7
+
+ assertEquals(1, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun removeIf() {
+ val map = MutableLongIntMap()
+ map[1L] = 1
+ map[2L] = 2
+ map[3L] = 3
+ map[4L] = 4
+ map[5L] = 5
+ map[6L] = 6
+
+ map.removeIf { key, _ -> key == 1L || key == 3L }
+
+ assertEquals(4, map.size)
+ assertEquals(2, map[2L])
+ assertEquals(4, map[4L])
+ assertEquals(5, map[5L])
+ assertEquals(6, map[6L])
+ }
+
+ @Test
+ fun minus() {
+ val map = MutableLongIntMap()
+ map[1L] = 1
+ map[2L] = 2
+ map[3L] = 3
+
+ map -= 1L
+
+ assertEquals(2, map.size)
+ assertFalse(1L in map)
+ }
+
+ @Test
+ fun minusArray() {
+ val map = MutableLongIntMap()
+ map[1L] = 1
+ map[2L] = 2
+ map[3L] = 3
+
+ map -= longArrayOf(3L, 2L)
+
+ assertEquals(1, map.size)
+ assertFalse(3L in map)
+ assertFalse(2L in map)
+ }
+
+ @Test
+ fun minusSet() {
+ val map = MutableLongIntMap()
+ map[1L] = 1
+ map[2L] = 2
+ map[3L] = 3
+
+ map -= longSetOf(3L, 2L)
+
+ assertEquals(1, map.size)
+ assertFalse(3L in map)
+ assertFalse(2L in map)
+ }
+
+ @Test
+ fun minusList() {
+ val map = MutableLongIntMap()
+ map[1L] = 1
+ map[2L] = 2
+ map[3L] = 3
+
+ map -= longListOf(3L, 2L)
+
+ assertEquals(1, map.size)
+ assertFalse(3L in map)
+ assertFalse(2L in map)
+ }
+
+ @Test
+ fun conditionalRemove() {
+ val map = MutableLongIntMap()
+ assertFalse(map.remove(1L, 1))
+
+ map[1L] = 1
+ assertTrue(map.remove(1L, 1))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val map = MutableLongIntMap()
+
+ for (i in 0 until 1700) {
+ map[i.toLong()] = i.toInt()
+ }
+
+ assertEquals(1700, map.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val map = MutableLongIntMap()
+
+ for (j in 0 until i) {
+ map[j.toLong()] = j.toInt()
+ }
+
+ var counter = 0
+ map.forEach { key, value ->
+ assertEquals(key, value.toLong())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachKey() {
+ for (i in 0..48) {
+ val map = MutableLongIntMap()
+
+ for (j in 0 until i) {
+ map[j.toLong()] = j.toInt()
+ }
+
+ var counter = 0
+ val keys = BooleanArray(map.size)
+ map.forEachKey { key ->
+ keys[key.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ keys.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun forEachValue() {
+ for (i in 0..48) {
+ val map = MutableLongIntMap()
+
+ for (j in 0 until i) {
+ map[j.toLong()] = j.toInt()
+ }
+
+ var counter = 0
+ val values = BooleanArray(map.size)
+ map.forEachValue { value ->
+ values[value.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ values.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun clear() {
+ val map = MutableLongIntMap()
+
+ for (i in 0 until 32) {
+ map[i.toLong()] = i.toInt()
+ }
+
+ val capacity = map.capacity
+ map.clear()
+
+ assertEquals(0, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun string() {
+ val map = MutableLongIntMap()
+ assertEquals("{}", map.toString())
+
+ map[1L] = 1
+ map[2L] = 2
+ val oneValueString = 1.toString()
+ val twoValueString = 2.toString()
+ val oneKeyString = 1L.toString()
+ val twoKeyString = 2L.toString()
+ assertTrue(
+ "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+ "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+ )
+ }
+
+ @Test
+ fun equals() {
+ val map = MutableLongIntMap()
+ map[1L] = 1
+
+ assertFalse(map.equals(null))
+ assertEquals(map, map)
+
+ val map2 = MutableLongIntMap()
+ assertNotEquals(map, map2)
+
+ map2[1L] = 1
+ assertEquals(map, map2)
+ }
+
+ @Test
+ fun containsKey() {
+ val map = MutableLongIntMap()
+ map[1L] = 1
+
+ assertTrue(map.containsKey(1L))
+ assertFalse(map.containsKey(2L))
+ }
+
+ @Test
+ fun contains() {
+ val map = MutableLongIntMap()
+ map[1L] = 1
+
+ assertTrue(1L in map)
+ assertFalse(2L in map)
+ }
+
+ @Test
+ fun containsValue() {
+ val map = MutableLongIntMap()
+ map[1L] = 1
+
+ assertTrue(map.containsValue(1))
+ assertFalse(map.containsValue(3))
+ }
+
+ @Test
+ fun empty() {
+ val map = MutableLongIntMap()
+ assertTrue(map.isEmpty())
+ assertFalse(map.isNotEmpty())
+ assertTrue(map.none())
+ assertFalse(map.any())
+
+ map[1L] = 1
+
+ assertFalse(map.isEmpty())
+ assertTrue(map.isNotEmpty())
+ assertTrue(map.any())
+ assertFalse(map.none())
+ }
+
+ @Test
+ fun count() {
+ val map = MutableLongIntMap()
+ assertEquals(0, map.count())
+
+ map[1L] = 1
+ assertEquals(1, map.count())
+
+ map[2L] = 2
+ map[3L] = 3
+ map[4L] = 4
+ map[5L] = 5
+ map[6L] = 6
+
+ assertEquals(2, map.count { key, _ -> key <= 2L })
+ assertEquals(0, map.count { key, _ -> key < 0L })
+ }
+
+ @Test
+ fun any() {
+ val map = MutableLongIntMap()
+ map[1L] = 1
+ map[2L] = 2
+ map[3L] = 3
+ map[4L] = 4
+ map[5L] = 5
+ map[6L] = 6
+
+ assertTrue(map.any { key, _ -> key == 4L })
+ assertFalse(map.any { key, _ -> key < 0L })
+ }
+
+ @Test
+ fun all() {
+ val map = MutableLongIntMap()
+ map[1L] = 1
+ map[2L] = 2
+ map[3L] = 3
+ map[4L] = 4
+ map[5L] = 5
+ map[6L] = 6
+
+ assertTrue(map.all { key, value -> key > 0L && value >= 1 })
+ assertFalse(map.all { key, _ -> key < 6L })
+ }
+
+ @Test
+ fun trim() {
+ val map = MutableLongIntMap()
+ assertEquals(7, map.trim())
+
+ map[1L] = 1
+ map[3L] = 3
+
+ assertEquals(0, map.trim())
+
+ for (i in 0 until 1700) {
+ map[i.toLong()] = i.toInt()
+ }
+
+ assertEquals(2047, map.capacity)
+
+ // After removing these items, our capacity needs should go
+ // from 2047 down to 1023
+ for (i in 0 until 1700) {
+ if (i and 0x1 == 0x0) {
+ val s = i.toLong()
+ map.remove(s)
+ }
+ }
+
+ assertEquals(1024, map.trim())
+ assertEquals(0, map.trim())
+ }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt
index 45aa039..39bde8f 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt
@@ -22,6 +22,14 @@
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
class LongListTest {
private val list: MutableLongList = mutableLongListOf(1L, 2L, 3L, 4L, 5L)
@@ -80,7 +88,7 @@
@Test
fun string() {
- assertEquals("[1, 2, 3, 4, 5]", list.toString())
+ assertEquals("[${1L}, ${2L}, ${3L}, ${4L}, ${5L}]", list.toString())
assertEquals("[]", mutableLongListOf().toString())
}
@@ -334,7 +342,7 @@
@Test
fun fold() {
- assertEquals("12345", list.fold("") { acc, i -> acc + i.toString() })
+ assertEquals("12345", list.fold("") { acc, i -> acc + i.toInt().toString() })
}
@Test
@@ -342,14 +350,14 @@
assertEquals(
"01-12-23-34-45-",
list.foldIndexed("") { index, acc, i ->
- "$acc$index$i-"
+ "$acc$index${i.toInt()}-"
}
)
}
@Test
fun foldRight() {
- assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toString() })
+ assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toInt().toString() })
}
@Test
@@ -357,7 +365,7 @@
assertEquals(
"45-34-23-12-01-",
list.foldRightIndexed("") { index, i, acc ->
- "$acc$index$i-"
+ "$acc$index${i.toInt()}-"
}
)
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongLongMapTest.kt
new file mode 100644
index 0000000..b45ae5a
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongLongMapTest.kt
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class LongLongMapTest {
+ @Test
+ fun longLongMap() {
+ val map = MutableLongLongMap()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun testEmptyLongLongMap() {
+ val map = emptyLongLongMap()
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+
+ assertSame(emptyLongLongMap(), map)
+ }
+
+ @Test
+ fun longLongMapFunction() {
+ val map = mutableLongLongMapOf()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun zeroCapacityHashMap() {
+ val map = MutableLongLongMap(0)
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun longLongMapWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val map = MutableLongLongMap(1800)
+ assertEquals(4095, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun longLongMapPairsFunction() {
+ val map = mutableLongLongMapOf(
+ 1L to 1L,
+ 2L to 2L
+ )
+ assertEquals(2, map.size)
+ assertEquals(1L, map[1L])
+ assertEquals(2L, map[2L])
+ }
+
+ @Test
+ fun addToMap() {
+ val map = MutableLongLongMap()
+ map[1L] = 1L
+
+ assertEquals(1, map.size)
+ assertEquals(1L, map[1L])
+ }
+
+ @Test
+ fun addToSizedMap() {
+ val map = MutableLongLongMap(12)
+ map[1L] = 1L
+
+ assertEquals(1, map.size)
+ assertEquals(1L, map[1L])
+ }
+
+ @Test
+ fun addToSmallMap() {
+ val map = MutableLongLongMap(2)
+ map[1L] = 1L
+
+ assertEquals(1, map.size)
+ assertEquals(7, map.capacity)
+ assertEquals(1L, map[1L])
+ }
+
+ @Test
+ fun addToZeroCapacityMap() {
+ val map = MutableLongLongMap(0)
+ map[1L] = 1L
+
+ assertEquals(1, map.size)
+ assertEquals(1L, map[1L])
+ }
+
+ @Test
+ fun replaceExistingKey() {
+ val map = MutableLongLongMap()
+ map[1L] = 1L
+ map[1L] = 2L
+
+ assertEquals(1, map.size)
+ assertEquals(2L, map[1L])
+ }
+
+ @Test
+ fun put() {
+ val map = MutableLongLongMap()
+
+ map.put(1L, 1L)
+ assertEquals(1L, map[1L])
+ map.put(1L, 2L)
+ assertEquals(2L, map[1L])
+ }
+
+ @Test
+ fun putAllArray() {
+ val map = MutableLongLongMap()
+ map[1L] = 1L
+ map[2L] = 2L
+
+ map.putAll(arrayOf(3L to 3L, 7L to 7L))
+
+ assertEquals(4, map.size)
+ assertEquals(3L, map[3L])
+ assertEquals(7L, map[7L])
+ }
+
+ @Test
+ fun plus() {
+ val map = MutableLongLongMap()
+ map += 1L to 1L
+
+ assertEquals(1, map.size)
+ assertEquals(1L, map[1L])
+ }
+
+ @Test
+ fun plusArray() {
+ val map = MutableLongLongMap()
+ map += arrayOf(3L to 3L, 7L to 7L)
+
+ assertEquals(2, map.size)
+ assertEquals(3L, map[3L])
+ assertEquals(7L, map[7L])
+ }
+
+ @Test
+ fun findNonExistingKey() {
+ val map = MutableLongLongMap()
+ map[1L] = 1L
+
+ assertFailsWith<NoSuchElementException> {
+ map[2L]
+ }
+ }
+
+ @Test
+ fun getOrDefault() {
+ val map = MutableLongLongMap()
+ map[1L] = 1L
+
+ assertEquals(2L, map.getOrDefault(2L, 2L))
+ }
+
+ @Test
+ fun getOrElse() {
+ val map = MutableLongLongMap()
+ map[1L] = 1L
+
+ assertEquals(3L, map.getOrElse(3L) { 3L })
+ }
+
+ @Test
+ fun getOrPut() {
+ val map = MutableLongLongMap()
+ map[1L] = 1L
+
+ var counter = 0
+ map.getOrPut(1L) {
+ counter++
+ 2L
+ }
+ assertEquals(1L, map[1L])
+ assertEquals(0, counter)
+
+ map.getOrPut(2L) {
+ counter++
+ 2L
+ }
+ assertEquals(2L, map[2L])
+ assertEquals(1, counter)
+
+ map.getOrPut(2L) {
+ counter++
+ 3L
+ }
+ assertEquals(2L, map[2L])
+ assertEquals(1, counter)
+
+ map.getOrPut(3L) {
+ counter++
+ 3L
+ }
+ assertEquals(3L, map[3L])
+ assertEquals(2, counter)
+ }
+
+ @Test
+ fun remove() {
+ val map = MutableLongLongMap()
+ map.remove(1L)
+
+ map[1L] = 1L
+ map.remove(1L)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val map = MutableLongLongMap(6)
+ map[1L] = 1L
+ map[2L] = 2L
+ map[3L] = 3L
+ map[4L] = 4L
+ map[5L] = 5L
+ map[6L] = 6L
+
+ // Removing all the entries will mark the medata as deleted
+ map.remove(1L)
+ map.remove(2L)
+ map.remove(3L)
+ map.remove(4L)
+ map.remove(5L)
+ map.remove(6L)
+
+ assertEquals(0, map.size)
+
+ val capacity = map.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ map[7L] = 7L
+
+ assertEquals(1, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun removeIf() {
+ val map = MutableLongLongMap()
+ map[1L] = 1L
+ map[2L] = 2L
+ map[3L] = 3L
+ map[4L] = 4L
+ map[5L] = 5L
+ map[6L] = 6L
+
+ map.removeIf { key, _ -> key == 1L || key == 3L }
+
+ assertEquals(4, map.size)
+ assertEquals(2L, map[2L])
+ assertEquals(4L, map[4L])
+ assertEquals(5L, map[5L])
+ assertEquals(6L, map[6L])
+ }
+
+ @Test
+ fun minus() {
+ val map = MutableLongLongMap()
+ map[1L] = 1L
+ map[2L] = 2L
+ map[3L] = 3L
+
+ map -= 1L
+
+ assertEquals(2, map.size)
+ assertFalse(1L in map)
+ }
+
+ @Test
+ fun minusArray() {
+ val map = MutableLongLongMap()
+ map[1L] = 1L
+ map[2L] = 2L
+ map[3L] = 3L
+
+ map -= longArrayOf(3L, 2L)
+
+ assertEquals(1, map.size)
+ assertFalse(3L in map)
+ assertFalse(2L in map)
+ }
+
+ @Test
+ fun minusSet() {
+ val map = MutableLongLongMap()
+ map[1L] = 1L
+ map[2L] = 2L
+ map[3L] = 3L
+
+ map -= longSetOf(3L, 2L)
+
+ assertEquals(1, map.size)
+ assertFalse(3L in map)
+ assertFalse(2L in map)
+ }
+
+ @Test
+ fun minusList() {
+ val map = MutableLongLongMap()
+ map[1L] = 1L
+ map[2L] = 2L
+ map[3L] = 3L
+
+ map -= longListOf(3L, 2L)
+
+ assertEquals(1, map.size)
+ assertFalse(3L in map)
+ assertFalse(2L in map)
+ }
+
+ @Test
+ fun conditionalRemove() {
+ val map = MutableLongLongMap()
+ assertFalse(map.remove(1L, 1L))
+
+ map[1L] = 1L
+ assertTrue(map.remove(1L, 1L))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val map = MutableLongLongMap()
+
+ for (i in 0 until 1700) {
+ map[i.toLong()] = i.toLong()
+ }
+
+ assertEquals(1700, map.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val map = MutableLongLongMap()
+
+ for (j in 0 until i) {
+ map[j.toLong()] = j.toLong()
+ }
+
+ var counter = 0
+ map.forEach { key, value ->
+ assertEquals(key, value.toLong())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachKey() {
+ for (i in 0..48) {
+ val map = MutableLongLongMap()
+
+ for (j in 0 until i) {
+ map[j.toLong()] = j.toLong()
+ }
+
+ var counter = 0
+ val keys = BooleanArray(map.size)
+ map.forEachKey { key ->
+ keys[key.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ keys.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun forEachValue() {
+ for (i in 0..48) {
+ val map = MutableLongLongMap()
+
+ for (j in 0 until i) {
+ map[j.toLong()] = j.toLong()
+ }
+
+ var counter = 0
+ val values = BooleanArray(map.size)
+ map.forEachValue { value ->
+ values[value.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ values.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun clear() {
+ val map = MutableLongLongMap()
+
+ for (i in 0 until 32) {
+ map[i.toLong()] = i.toLong()
+ }
+
+ val capacity = map.capacity
+ map.clear()
+
+ assertEquals(0, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun string() {
+ val map = MutableLongLongMap()
+ assertEquals("{}", map.toString())
+
+ map[1L] = 1L
+ map[2L] = 2L
+ val oneValueString = 1L.toString()
+ val twoValueString = 2L.toString()
+ val oneKeyString = 1L.toString()
+ val twoKeyString = 2L.toString()
+ assertTrue(
+ "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+ "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+ )
+ }
+
+ @Test
+ fun equals() {
+ val map = MutableLongLongMap()
+ map[1L] = 1L
+
+ assertFalse(map.equals(null))
+ assertEquals(map, map)
+
+ val map2 = MutableLongLongMap()
+ assertNotEquals(map, map2)
+
+ map2[1L] = 1L
+ assertEquals(map, map2)
+ }
+
+ @Test
+ fun containsKey() {
+ val map = MutableLongLongMap()
+ map[1L] = 1L
+
+ assertTrue(map.containsKey(1L))
+ assertFalse(map.containsKey(2L))
+ }
+
+ @Test
+ fun contains() {
+ val map = MutableLongLongMap()
+ map[1L] = 1L
+
+ assertTrue(1L in map)
+ assertFalse(2L in map)
+ }
+
+ @Test
+ fun containsValue() {
+ val map = MutableLongLongMap()
+ map[1L] = 1L
+
+ assertTrue(map.containsValue(1L))
+ assertFalse(map.containsValue(3L))
+ }
+
+ @Test
+ fun empty() {
+ val map = MutableLongLongMap()
+ assertTrue(map.isEmpty())
+ assertFalse(map.isNotEmpty())
+ assertTrue(map.none())
+ assertFalse(map.any())
+
+ map[1L] = 1L
+
+ assertFalse(map.isEmpty())
+ assertTrue(map.isNotEmpty())
+ assertTrue(map.any())
+ assertFalse(map.none())
+ }
+
+ @Test
+ fun count() {
+ val map = MutableLongLongMap()
+ assertEquals(0, map.count())
+
+ map[1L] = 1L
+ assertEquals(1, map.count())
+
+ map[2L] = 2L
+ map[3L] = 3L
+ map[4L] = 4L
+ map[5L] = 5L
+ map[6L] = 6L
+
+ assertEquals(2, map.count { key, _ -> key <= 2L })
+ assertEquals(0, map.count { key, _ -> key < 0L })
+ }
+
+ @Test
+ fun any() {
+ val map = MutableLongLongMap()
+ map[1L] = 1L
+ map[2L] = 2L
+ map[3L] = 3L
+ map[4L] = 4L
+ map[5L] = 5L
+ map[6L] = 6L
+
+ assertTrue(map.any { key, _ -> key == 4L })
+ assertFalse(map.any { key, _ -> key < 0L })
+ }
+
+ @Test
+ fun all() {
+ val map = MutableLongLongMap()
+ map[1L] = 1L
+ map[2L] = 2L
+ map[3L] = 3L
+ map[4L] = 4L
+ map[5L] = 5L
+ map[6L] = 6L
+
+ assertTrue(map.all { key, value -> key > 0L && value >= 1L })
+ assertFalse(map.all { key, _ -> key < 6L })
+ }
+
+ @Test
+ fun trim() {
+ val map = MutableLongLongMap()
+ assertEquals(7, map.trim())
+
+ map[1L] = 1L
+ map[3L] = 3L
+
+ assertEquals(0, map.trim())
+
+ for (i in 0 until 1700) {
+ map[i.toLong()] = i.toLong()
+ }
+
+ assertEquals(2047, map.capacity)
+
+ // After removing these items, our capacity needs should go
+ // from 2047 down to 1023
+ for (i in 0 until 1700) {
+ if (i and 0x1 == 0x0) {
+ val s = i.toLong()
+ map.remove(s)
+ }
+ }
+
+ assertEquals(1024, map.trim())
+ assertEquals(0, map.trim())
+ }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongObjectMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongObjectMapTest.kt
new file mode 100644
index 0000000..1a5fd8bf
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongObjectMapTest.kt
@@ -0,0 +1,632 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class LongObjectMapTest {
+ @Test
+ fun longObjectMap() {
+ val map = MutableLongObjectMap<String>()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun testEmptyLongObjectMap() {
+ val map = emptyLongObjectMap<String>()
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+
+ assertSame(emptyLongObjectMap<String>(), map)
+ }
+
+ @Test
+ fun longObjectMapFunction() {
+ val map = mutableLongObjectMapOf<String>()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun zeroCapacityHashMap() {
+ val map = MutableLongObjectMap<String>(0)
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun longObjectMapWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val map = MutableLongObjectMap<String>(1800)
+ assertEquals(4095, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun longObjectMapPairsFunction() {
+ val map = mutableLongObjectMapOf(
+ 1L to "World",
+ 2L to "Monde"
+ )
+ assertEquals(2, map.size)
+ assertEquals("World", map[1L])
+ assertEquals("Monde", map[2L])
+ }
+
+ @Test
+ fun addToMap() {
+ val map = MutableLongObjectMap<String>()
+ map[1L] = "World"
+
+ assertEquals(1, map.size)
+ assertEquals("World", map[1L])
+ }
+
+ @Test
+ fun insertIndex0() {
+ val map = MutableLongObjectMap<String>()
+ map.put(1L, "World")
+ assertEquals("World", map[1L])
+ }
+
+ @Test
+ fun addToSizedMap() {
+ val map = MutableLongObjectMap<String>(12)
+ map[1L] = "World"
+
+ assertEquals(1, map.size)
+ assertEquals("World", map[1L])
+ }
+
+ @Test
+ fun addToSmallMap() {
+ val map = MutableLongObjectMap<String>(2)
+ map[1L] = "World"
+
+ assertEquals(1, map.size)
+ assertEquals(7, map.capacity)
+ assertEquals("World", map[1L])
+ }
+
+ @Test
+ fun addToZeroCapacityMap() {
+ val map = MutableLongObjectMap<String>(0)
+ map[1L] = "World"
+
+ assertEquals(1, map.size)
+ assertEquals("World", map[1L])
+ }
+
+ @Test
+ fun replaceExistingKey() {
+ val map = MutableLongObjectMap<String>()
+ map[1L] = "World"
+ map[1L] = "Monde"
+
+ assertEquals(1, map.size)
+ assertEquals("Monde", map[1L])
+ }
+
+ @Test
+ fun put() {
+ val map = MutableLongObjectMap<String?>()
+
+ assertNull(map.put(1L, "World"))
+ assertEquals("World", map.put(1L, "Monde"))
+ assertNull(map.put(2L, null))
+ assertNull(map.put(2L, "Monde"))
+ }
+
+ @Test
+ fun putAllArray() {
+ val map = MutableLongObjectMap<String?>()
+ map[1L] = "World"
+ map[2L] = null
+
+ map.putAll(arrayOf(3L to "Welt", 7L to "Mundo"))
+
+ assertEquals(4, map.size)
+ assertEquals("Welt", map[3L])
+ assertEquals("Mundo", map[7L])
+ }
+
+ @Test
+ fun putAllMap() {
+ val map = MutableLongObjectMap<String?>()
+ map[1L] = "World"
+ map[2L] = null
+
+ map.putAll(mutableLongObjectMapOf(3L to "Welt", 7L to "Mundo"))
+
+ assertEquals(4, map.size)
+ assertEquals("Welt", map[3L])
+ assertEquals("Mundo", map[7L])
+ }
+
+ @Test
+ fun plus() {
+ val map = MutableLongObjectMap<String>()
+ map += 1L to "World"
+
+ assertEquals(1, map.size)
+ assertEquals("World", map[1L])
+ }
+
+ @Test
+ fun plusMap() {
+ val map = MutableLongObjectMap<String>()
+ map += longObjectMapOf(3L to "Welt", 7L to "Mundo")
+
+ assertEquals(2, map.size)
+ assertEquals("Welt", map[3L])
+ assertEquals("Mundo", map[7L])
+ }
+
+ @Test
+ fun plusArray() {
+ val map = MutableLongObjectMap<String>()
+ map += arrayOf(3L to "Welt", 7L to "Mundo")
+
+ assertEquals(2, map.size)
+ assertEquals("Welt", map[3L])
+ assertEquals("Mundo", map[7L])
+ }
+
+ @Test
+ fun nullValue() {
+ val map = MutableLongObjectMap<String?>()
+ map[1L] = null
+
+ assertEquals(1, map.size)
+ assertNull(map[1L])
+ }
+
+ @Test
+ fun findNonExistingKey() {
+ val map = MutableLongObjectMap<String?>()
+ map[1L] = "World"
+
+ assertNull(map[2L])
+ }
+
+ @Test
+ fun getOrDefault() {
+ val map = MutableLongObjectMap<String?>()
+ map[1L] = "World"
+
+ assertEquals("Monde", map.getOrDefault(2L, "Monde"))
+ }
+
+ @Test
+ fun getOrElse() {
+ val map = MutableLongObjectMap<String?>()
+ map[1L] = "World"
+ map[2L] = null
+
+ assertEquals("Monde", map.getOrElse(2L) { "Monde" })
+ assertEquals("Welt", map.getOrElse(3L) { "Welt" })
+ }
+
+ @Test
+ fun getOrPut() {
+ val map = MutableLongObjectMap<String?>()
+ map[1L] = "World"
+
+ var counter = 0
+ map.getOrPut(1L) {
+ counter++
+ "Monde"
+ }
+ assertEquals("World", map[1L])
+ assertEquals(0, counter)
+
+ map.getOrPut(2L) {
+ counter++
+ "Monde"
+ }
+ assertEquals("Monde", map[2L])
+ assertEquals(1, counter)
+
+ map.getOrPut(2L) {
+ counter++
+ "Welt"
+ }
+ assertEquals("Monde", map[2L])
+ assertEquals(1, counter)
+
+ map.getOrPut(3L) {
+ counter++
+ null
+ }
+ assertNull(map[3L])
+ assertEquals(2, counter)
+
+ map.getOrPut(3L) {
+ counter++
+ "Welt"
+ }
+ assertEquals("Welt", map[3L])
+ assertEquals(3, counter)
+ }
+
+ @Test
+ fun remove() {
+ val map = MutableLongObjectMap<String?>()
+ assertNull(map.remove(1L))
+
+ map[1L] = "World"
+ assertEquals("World", map.remove(1L))
+ assertEquals(0, map.size)
+
+ map[1L] = null
+ assertNull(map.remove(1L))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val map = MutableLongObjectMap<String>(6)
+ map[1L] = "World"
+ map[2L] = "Monde"
+ map[3L] = "Welt"
+ map[4L] = "Sekai"
+ map[5L] = "Mondo"
+ map[6L] = "Sesang"
+
+ // Removing all the entries will mark the medata as deleted
+ map.remove(1L)
+ map.remove(2L)
+ map.remove(3L)
+ map.remove(4L)
+ map.remove(5L)
+ map.remove(6L)
+
+ assertEquals(0, map.size)
+
+ val capacity = map.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ map[7L] = "Mundo"
+
+ assertEquals(1, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun removeIf() {
+ val map = MutableLongObjectMap<String>()
+ map[1L] = "World"
+ map[2L] = "Monde"
+ map[3L] = "Welt"
+ map[4L] = "Sekai"
+ map[5L] = "Mondo"
+ map[6L] = "Sesang"
+
+ map.removeIf { key, value ->
+ key == 1L || key == 3L || value.startsWith('S')
+ }
+
+ assertEquals(2, map.size)
+ assertEquals("Monde", map[2L])
+ assertEquals("Mondo", map[5L])
+ }
+
+ @Test
+ fun minus() {
+ val map = MutableLongObjectMap<String>()
+ map[1L] = "World"
+ map[2L] = "Monde"
+ map[3L] = "Welt"
+
+ map -= 1L
+
+ assertEquals(2, map.size)
+ assertNull(map[1L])
+ }
+
+ @Test
+ fun minusArray() {
+ val map = MutableLongObjectMap<String>()
+ map[1L] = "World"
+ map[2L] = "Monde"
+ map[3L] = "Welt"
+
+ map -= longArrayOf(3L, 2L)
+
+ assertEquals(1, map.size)
+ assertNull(map[3L])
+ assertNull(map[2L])
+ }
+
+ @Test
+ fun minusSet() {
+ val map = MutableLongObjectMap<String>()
+ map[1L] = "World"
+ map[2L] = "Monde"
+ map[3L] = "Welt"
+
+ map -= longSetOf(3L, 2L)
+
+ assertEquals(1, map.size)
+ assertNull(map[3L])
+ assertNull(map[2L])
+ }
+
+ @Test
+ fun minusList() {
+ val map = MutableLongObjectMap<String>()
+ map[1L] = "World"
+ map[2L] = "Monde"
+ map[3L] = "Welt"
+
+ map -= longListOf(3L, 2L)
+
+ assertEquals(1, map.size)
+ assertNull(map[3L])
+ assertNull(map[2L])
+ }
+
+ @Test
+ fun conditionalRemove() {
+ val map = MutableLongObjectMap<String?>()
+ assertFalse(map.remove(1L, "World"))
+
+ map[1L] = "World"
+ assertTrue(map.remove(1L, "World"))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val map = MutableLongObjectMap<String>()
+
+ for (i in 0 until 1700) {
+ map[i.toLong()] = i.toString()
+ }
+
+ assertEquals(1700, map.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val map = MutableLongObjectMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toLong()] = j.toString()
+ }
+
+ var counter = 0
+ map.forEach { key, value ->
+ assertEquals(key.toInt().toString(), value)
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachKey() {
+ for (i in 0..48) {
+ val map = MutableLongObjectMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toLong()] = j.toString()
+ }
+
+ var counter = 0
+ map.forEachKey { key ->
+ assertEquals(key.toInt().toString(), map[key])
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachValue() {
+ for (i in 0..48) {
+ val map = MutableLongObjectMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toLong()] = j.toString()
+ }
+
+ var counter = 0
+ map.forEachValue { value ->
+ assertNotNull(value.toIntOrNull())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun clear() {
+ val map = MutableLongObjectMap<String>()
+
+ for (i in 0 until 32) {
+ map[i.toLong()] = i.toString()
+ }
+
+ val capacity = map.capacity
+ map.clear()
+
+ assertEquals(0, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun string() {
+ val map = MutableLongObjectMap<String?>()
+ assertEquals("{}", map.toString())
+
+ map[1L] = "World"
+ map[2L] = "Monde"
+ val oneKey = 1L.toString()
+ val twoKey = 2L.toString()
+ assertTrue(
+ "{$oneKey=World, $twoKey=Monde}" == map.toString() ||
+ "{$twoKey=Monde, $oneKey=World}" == map.toString()
+ )
+
+ map.clear()
+ map[1L] = null
+ assertEquals("{$oneKey=null}", map.toString())
+
+ val selfAsValueMap = MutableLongObjectMap<Any>()
+ selfAsValueMap[1L] = selfAsValueMap
+ assertEquals("{$oneKey=(this)}", selfAsValueMap.toString())
+ }
+
+ @Test
+ fun equals() {
+ val map = MutableLongObjectMap<String?>()
+ map[1L] = "World"
+ map[2L] = null
+
+ assertFalse(map.equals(null))
+ assertEquals(map, map)
+
+ val map2 = MutableLongObjectMap<String?>()
+ map2[2L] = null
+
+ assertNotEquals(map, map2)
+
+ map2[1L] = "World"
+ assertEquals(map, map2)
+ }
+
+ @Test
+ fun containsKey() {
+ val map = MutableLongObjectMap<String?>()
+ map[1L] = "World"
+ map[2L] = null
+
+ assertTrue(map.containsKey(1L))
+ assertFalse(map.containsKey(3L))
+ }
+
+ @Test
+ fun contains() {
+ val map = MutableLongObjectMap<String?>()
+ map[1L] = "World"
+ map[2L] = null
+
+ assertTrue(1L in map)
+ assertFalse(3L in map)
+ }
+
+ @Test
+ fun containsValue() {
+ val map = MutableLongObjectMap<String?>()
+ map[1L] = "World"
+ map[2L] = null
+
+ assertTrue(map.containsValue("World"))
+ assertTrue(map.containsValue(null))
+ assertFalse(map.containsValue("Monde"))
+ }
+
+ @Test
+ fun empty() {
+ val map = MutableLongObjectMap<String?>()
+ assertTrue(map.isEmpty())
+ assertFalse(map.isNotEmpty())
+ assertTrue(map.none())
+ assertFalse(map.any())
+
+ map[1L] = "World"
+
+ assertFalse(map.isEmpty())
+ assertTrue(map.isNotEmpty())
+ assertTrue(map.any())
+ assertFalse(map.none())
+ }
+
+ @Test
+ fun count() {
+ val map = MutableLongObjectMap<String>()
+ assertEquals(0, map.count())
+
+ map[1L] = "World"
+ assertEquals(1, map.count())
+
+ map[2L] = "Monde"
+ map[3L] = "Welt"
+ map[4L] = "Sekai"
+ map[5L] = "Mondo"
+ map[6L] = "Sesang"
+
+ assertEquals(2, map.count { key, _ -> key < 3L })
+ assertEquals(0, map.count { key, _ -> key < 0L })
+ }
+
+ @Test
+ fun any() {
+ val map = MutableLongObjectMap<String>()
+ map[1L] = "World"
+ map[2L] = "Monde"
+ map[3L] = "Welt"
+ map[4L] = "Sekai"
+ map[5L] = "Mondo"
+ map[6L] = "Sesang"
+
+ assertTrue(map.any { key, _ -> key > 5L })
+ assertFalse(map.any { key, _ -> key < 0L })
+ }
+
+ @Test
+ fun all() {
+ val map = MutableLongObjectMap<String>()
+ map[1L] = "World"
+ map[2L] = "Monde"
+ map[3L] = "Welt"
+ map[4L] = "Sekai"
+ map[5L] = "Mondo"
+ map[6L] = "Sesang"
+
+ assertTrue(map.all { key, value -> key < 7L && value.length > 0 })
+ assertFalse(map.all { key, _ -> key < 6L })
+ }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt
index 1278fcf..2ee0e96 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt
@@ -23,6 +23,14 @@
import kotlin.test.assertSame
import kotlin.test.assertTrue
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
class LongSetTest {
@Test
fun emptyLongSetConstructor() {
@@ -147,9 +155,9 @@
val set = MutableLongSet()
set += 1L
set += 2L
- var element: Long = Long.MIN_VALUE
- var otherElement: Long = Long.MIN_VALUE
- set.forEach { if (element == Long.MIN_VALUE) element = it else otherElement = it }
+ var element: Long = -1L
+ var otherElement: Long = -1L
+ set.forEach { if (element == -1L) element = it else otherElement = it }
assertEquals(element, set.first())
set -= element
assertEquals(otherElement, set.first())
@@ -333,8 +341,8 @@
set += 1L
set += 5L
assertTrue(
- "[1, 5]" == set.toString() ||
- "[5, 1]" == set.toString()
+ "[${1L}, ${5L}]" == set.toString() ||
+ "[${5L}, ${1L}]" == set.toString()
)
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectFloatMapTest.kt
new file mode 100644
index 0000000..3ae3e6e
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectFloatMapTest.kt
@@ -0,0 +1,630 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class ObjectFloatTest {
+ @Test
+ fun objectFloatMap() {
+ val map = MutableObjectFloatMap<String>()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun emptyObjectFloatMap() {
+ val map = emptyObjectFloatMap<String>()
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+
+ assertSame(emptyObjectFloatMap<String>(), map)
+ }
+
+ @Test
+ fun objectFloatMapFunction() {
+ val map = mutableObjectFloatMapOf<String>()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun zeroCapacityHashMap() {
+ val map = MutableObjectFloatMap<String>(0)
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun objectFloatMapWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val map = MutableObjectFloatMap<String>(1800)
+ assertEquals(4095, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun objectFloatMapPairsFunction() {
+ val map = mutableObjectFloatMapOf(
+ "Hello" to 1f,
+ "Bonjour" to 2f
+ )
+ assertEquals(2, map.size)
+ assertEquals(1f, map["Hello"])
+ assertEquals(2f, map["Bonjour"])
+ }
+
+ @Test
+ fun addToMap() {
+ val map = MutableObjectFloatMap<String>()
+ map["Hello"] = 1f
+
+ assertEquals(1, map.size)
+ assertEquals(1f, map["Hello"])
+ }
+
+ @Test
+ fun addToSizedMap() {
+ val map = MutableObjectFloatMap<String>(12)
+ map["Hello"] = 1f
+
+ assertEquals(1, map.size)
+ assertEquals(1f, map["Hello"])
+ }
+
+ @Test
+ fun addToSmallMap() {
+ val map = MutableObjectFloatMap<String>(2)
+ map["Hello"] = 1f
+
+ assertEquals(1, map.size)
+ assertEquals(7, map.capacity)
+ assertEquals(1f, map["Hello"])
+ }
+
+ @Test
+ fun addToZeroCapacityMap() {
+ val map = MutableObjectFloatMap<String>(0)
+ map["Hello"] = 1f
+
+ assertEquals(1, map.size)
+ assertEquals(1f, map["Hello"])
+ }
+
+ @Test
+ fun replaceExistingKey() {
+ val map = MutableObjectFloatMap<String>()
+ map["Hello"] = 1f
+ map["Hello"] = 2f
+
+ assertEquals(1, map.size)
+ assertEquals(2f, map["Hello"])
+ }
+
+ @Test
+ fun put() {
+ val map = MutableObjectFloatMap<String>()
+
+ map.put("Hello", 1f)
+ assertEquals(1f, map["Hello"])
+ map.put("Hello", 2f)
+ assertEquals(2f, map["Hello"])
+ }
+
+ @Test
+ fun putAllArray() {
+ val map = MutableObjectFloatMap<String?>()
+ map["Hello"] = 1f
+ map[null] = 2f
+ map["Bonjour"] = 2f
+
+ map.putAll(arrayOf("Hallo" to 3f, "Hola" to 7f))
+
+ assertEquals(5, map.size)
+ assertEquals(3f, map["Hallo"])
+ assertEquals(7f, map["Hola"])
+ }
+
+ @Test
+ fun plus() {
+ val map = MutableObjectFloatMap<String>()
+ map += "Hello" to 1f
+
+ assertEquals(1, map.size)
+ assertEquals(1f, map["Hello"])
+ }
+
+ @Test
+ fun plusArray() {
+ val map = MutableObjectFloatMap<String>()
+ map += arrayOf("Hallo" to 3f, "Hola" to 7f)
+
+ assertEquals(2, map.size)
+ assertEquals(3f, map["Hallo"])
+ assertEquals(7f, map["Hola"])
+ }
+
+ @Test
+ fun nullKey() {
+ val map = MutableObjectFloatMap<String?>()
+ map[null] = 1f
+
+ assertEquals(1, map.size)
+ assertEquals(1f, map[null])
+ }
+
+ @Test
+ fun findNonExistingKey() {
+ val map = MutableObjectFloatMap<String>()
+ map["Hello"] = 1f
+
+ assertFailsWith<NoSuchElementException> {
+ map["Bonjour"]
+ }
+ }
+
+ @Test
+ fun getOrDefault() {
+ val map = MutableObjectFloatMap<String>()
+ map["Hello"] = 1f
+
+ assertEquals(2f, map.getOrDefault("Bonjour", 2f))
+ }
+
+ @Test
+ fun getOrElse() {
+ val map = MutableObjectFloatMap<String>()
+ map["Hello"] = 1f
+
+ assertEquals(3f, map.getOrElse("Hallo") { 3f })
+ }
+
+ @Test
+ fun getOrPut() {
+ val map = MutableObjectFloatMap<String>()
+ map["Hello"] = 1f
+
+ var counter = 0
+ map.getOrPut("Hello") {
+ counter++
+ 2f
+ }
+ assertEquals(1f, map["Hello"])
+ assertEquals(0, counter)
+
+ map.getOrPut("Bonjour") {
+ counter++
+ 2f
+ }
+ assertEquals(2f, map["Bonjour"])
+ assertEquals(1, counter)
+
+ map.getOrPut("Bonjour") {
+ counter++
+ 3f
+ }
+ assertEquals(2f, map["Bonjour"])
+ assertEquals(1, counter)
+
+ map.getOrPut("Hallo") {
+ counter++
+ 3f
+ }
+ assertEquals(3f, map["Hallo"])
+ assertEquals(2, counter)
+ }
+
+ @Test
+ fun remove() {
+ val map = MutableObjectFloatMap<String?>()
+ map.remove("Hello")
+
+ map["Hello"] = 1f
+ map.remove("Hello")
+ assertEquals(0, map.size)
+
+ map[null] = 1f
+ map.remove(null)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val map = MutableObjectFloatMap<String>(6)
+ map["Hello"] = 1f
+ map["Bonjour"] = 2f
+ map["Hallo"] = 3f
+ map["Konnichiwa"] = 4f
+ map["Ciao"] = 5f
+ map["Annyeong"] = 6f
+
+ // Removing all the entries will mark the medata as deleted
+ map.remove("Hello")
+ map.remove("Bonjour")
+ map.remove("Hallo")
+ map.remove("Konnichiwa")
+ map.remove("Ciao")
+ map.remove("Annyeong")
+
+ assertEquals(0, map.size)
+
+ val capacity = map.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ map["Hola"] = 7f
+
+ assertEquals(1, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun removeIf() {
+ val map = MutableObjectFloatMap<String>()
+ map["Hello"] = 1f
+ map["Bonjour"] = 2f
+ map["Hallo"] = 3f
+ map["Konnichiwa"] = 4f
+ map["Ciao"] = 5f
+ map["Annyeong"] = 6f
+
+ map.removeIf { key, _ -> key.startsWith('H') }
+
+ assertEquals(4, map.size)
+ assertEquals(2f, map["Bonjour"])
+ assertEquals(4f, map["Konnichiwa"])
+ assertEquals(5f, map["Ciao"])
+ assertEquals(6f, map["Annyeong"])
+ }
+
+ @Test
+ fun minus() {
+ val map = MutableObjectFloatMap<String>()
+ map["Hello"] = 1f
+ map["Bonjour"] = 2f
+ map["Hallo"] = 3f
+
+ map -= "Hello"
+
+ assertEquals(2, map.size)
+ assertFalse("Hello" in map)
+ }
+
+ @Test
+ fun minusArray() {
+ val map = MutableObjectFloatMap<String>()
+ map["Hello"] = 1f
+ map["Bonjour"] = 2f
+ map["Hallo"] = 3f
+
+ map -= arrayOf("Hallo", "Bonjour")
+
+ assertEquals(1, map.size)
+ assertFalse("Hallo" in map)
+ assertFalse("Bonjour" in map)
+ }
+
+ @Test
+ fun minusIterable() {
+ val map = MutableObjectFloatMap<String>()
+ map["Hello"] = 1f
+ map["Bonjour"] = 2f
+ map["Hallo"] = 3f
+
+ map -= listOf("Hallo", "Bonjour")
+
+ assertEquals(1, map.size)
+ assertFalse("Hallo" in map)
+ assertFalse("Bonjour" in map)
+ }
+
+ @Test
+ fun minusSequence() {
+ val map = MutableObjectFloatMap<String>()
+ map["Hello"] = 1f
+ map["Bonjour"] = 2f
+ map["Hallo"] = 3f
+
+ map -= listOf("Hallo", "Bonjour").asSequence()
+
+ assertEquals(1, map.size)
+ assertFalse("Hallo" in map)
+ assertFalse("Bonjour" in map)
+ }
+
+ @Test
+ fun conditionalRemove() {
+ val map = MutableObjectFloatMap<String?>()
+ assertFalse(map.remove("Hello", 1f))
+
+ map["Hello"] = 1f
+ assertTrue(map.remove("Hello", 1f))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val map = MutableObjectFloatMap<String>()
+
+ for (i in 0 until 1700) {
+ map[i.toString()] = i.toFloat()
+ }
+
+ assertEquals(1700, map.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val map = MutableObjectFloatMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toString()] = j.toFloat()
+ }
+
+ var counter = 0
+ map.forEach { key, value ->
+ assertEquals(key, value.toInt().toString())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachKey() {
+ for (i in 0..48) {
+ val map = MutableObjectFloatMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toString()] = j.toFloat()
+ }
+
+ var counter = 0
+ map.forEachKey { key ->
+ assertNotNull(key.toIntOrNull())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachValue() {
+ for (i in 0..48) {
+ val map = MutableObjectFloatMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toString()] = j.toFloat()
+ }
+
+ var counter = 0
+ val values = BooleanArray(map.size)
+ map.forEachValue { value ->
+ values[value.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ values.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun clear() {
+ val map = MutableObjectFloatMap<String>()
+
+ for (i in 0 until 32) {
+ map[i.toString()] = i.toFloat()
+ }
+
+ val capacity = map.capacity
+ map.clear()
+
+ assertEquals(0, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun string() {
+ val map = MutableObjectFloatMap<String?>()
+ assertEquals("{}", map.toString())
+
+ map["Hello"] = 1f
+ map["Bonjour"] = 2f
+ val oneString = 1f.toString()
+ val twoString = 2f.toString()
+ assertTrue(
+ "{Hello=$oneString, Bonjour=$twoString}" == map.toString() ||
+ "{Bonjour=$twoString, Hello=$oneString}" == map.toString()
+ )
+
+ map.clear()
+ map[null] = 2f
+ assertEquals("{null=$twoString}", map.toString())
+
+ val selfAsKeyMap = MutableObjectFloatMap<Any>()
+ selfAsKeyMap[selfAsKeyMap] = 1f
+ assertEquals("{(this)=$oneString}", selfAsKeyMap.toString())
+ }
+
+ @Test
+ fun equals() {
+ val map = MutableObjectFloatMap<String?>()
+ map["Hello"] = 1f
+ map[null] = 2f
+
+ assertFalse(map.equals(null))
+ assertEquals(map, map)
+
+ val map2 = MutableObjectFloatMap<String?>()
+ map2[null] = 2f
+
+ assertNotEquals(map, map2)
+
+ map2["Hello"] = 1f
+ assertEquals(map, map2)
+ }
+
+ @Test
+ fun containsKey() {
+ val map = MutableObjectFloatMap<String?>()
+ map["Hello"] = 1f
+ map[null] = 2f
+
+ assertTrue(map.containsKey("Hello"))
+ assertTrue(map.containsKey(null))
+ assertFalse(map.containsKey("Bonjour"))
+ }
+
+ @Test
+ fun contains() {
+ val map = MutableObjectFloatMap<String?>()
+ map["Hello"] = 1f
+ map[null] = 2f
+
+ assertTrue("Hello" in map)
+ assertTrue(null in map)
+ assertFalse("Bonjour" in map)
+ }
+
+ @Test
+ fun containsValue() {
+ val map = MutableObjectFloatMap<String?>()
+ map["Hello"] = 1f
+ map[null] = 2f
+
+ assertTrue(map.containsValue(1f))
+ assertTrue(map.containsValue(2f))
+ assertFalse(map.containsValue(3f))
+ }
+
+ @Test
+ fun empty() {
+ val map = MutableObjectFloatMap<String?>()
+ assertTrue(map.isEmpty())
+ assertFalse(map.isNotEmpty())
+ assertTrue(map.none())
+ assertFalse(map.any())
+
+ map["Hello"] = 1f
+
+ assertFalse(map.isEmpty())
+ assertTrue(map.isNotEmpty())
+ assertTrue(map.any())
+ assertFalse(map.none())
+ }
+
+ @Test
+ fun count() {
+ val map = MutableObjectFloatMap<String>()
+ assertEquals(0, map.count())
+
+ map["Hello"] = 1f
+ assertEquals(1, map.count())
+
+ map["Bonjour"] = 2f
+ map["Hallo"] = 3f
+ map["Konnichiwa"] = 4f
+ map["Ciao"] = 5f
+ map["Annyeong"] = 6f
+
+ assertEquals(2, map.count { key, _ -> key.startsWith("H") })
+ assertEquals(0, map.count { key, _ -> key.startsWith("W") })
+ }
+
+ @Test
+ fun any() {
+ val map = MutableObjectFloatMap<String>()
+ map["Hello"] = 1f
+ map["Bonjour"] = 2f
+ map["Hallo"] = 3f
+ map["Konnichiwa"] = 4f
+ map["Ciao"] = 5f
+ map["Annyeong"] = 6f
+
+ assertTrue(map.any { key, _ -> key.startsWith("K") })
+ assertFalse(map.any { key, _ -> key.startsWith("W") })
+ }
+
+ @Test
+ fun all() {
+ val map = MutableObjectFloatMap<String>()
+ map["Hello"] = 1f
+ map["Bonjour"] = 2f
+ map["Hallo"] = 3f
+ map["Konnichiwa"] = 4f
+ map["Ciao"] = 5f
+ map["Annyeong"] = 6f
+
+ assertTrue(map.all { key, value -> key.length >= 4 && value.toInt() >= 1 })
+ assertFalse(map.all { key, _ -> key.startsWith("W") })
+ }
+
+ @Test
+ fun trim() {
+ val map = MutableObjectFloatMap<String>()
+ assertEquals(7, map.trim())
+
+ map["Hello"] = 1f
+ map["Hallo"] = 3f
+
+ assertEquals(0, map.trim())
+
+ for (i in 0 until 1700) {
+ map[i.toString()] = i.toFloat()
+ }
+
+ assertEquals(2047, map.capacity)
+
+ // After removing these items, our capacity needs should go
+ // from 2047 down to 1023
+ for (i in 0 until 1700) {
+ if (i and 0x1 == 0x0) {
+ val s = i.toString()
+ map.remove(s)
+ }
+ }
+
+ assertEquals(1024, map.trim())
+ assertEquals(0, map.trim())
+ }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectIntMapTest.kt
new file mode 100644
index 0000000..d3bf7c7
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectIntMapTest.kt
@@ -0,0 +1,630 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class ObjectIntTest {
+ @Test
+ fun objectIntMap() {
+ val map = MutableObjectIntMap<String>()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun emptyObjectIntMap() {
+ val map = emptyObjectIntMap<String>()
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+
+ assertSame(emptyObjectIntMap<String>(), map)
+ }
+
+ @Test
+ fun objectIntMapFunction() {
+ val map = mutableObjectIntMapOf<String>()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun zeroCapacityHashMap() {
+ val map = MutableObjectIntMap<String>(0)
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun objectIntMapWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val map = MutableObjectIntMap<String>(1800)
+ assertEquals(4095, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun objectIntMapPairsFunction() {
+ val map = mutableObjectIntMapOf(
+ "Hello" to 1,
+ "Bonjour" to 2
+ )
+ assertEquals(2, map.size)
+ assertEquals(1, map["Hello"])
+ assertEquals(2, map["Bonjour"])
+ }
+
+ @Test
+ fun addToMap() {
+ val map = MutableObjectIntMap<String>()
+ map["Hello"] = 1
+
+ assertEquals(1, map.size)
+ assertEquals(1, map["Hello"])
+ }
+
+ @Test
+ fun addToSizedMap() {
+ val map = MutableObjectIntMap<String>(12)
+ map["Hello"] = 1
+
+ assertEquals(1, map.size)
+ assertEquals(1, map["Hello"])
+ }
+
+ @Test
+ fun addToSmallMap() {
+ val map = MutableObjectIntMap<String>(2)
+ map["Hello"] = 1
+
+ assertEquals(1, map.size)
+ assertEquals(7, map.capacity)
+ assertEquals(1, map["Hello"])
+ }
+
+ @Test
+ fun addToZeroCapacityMap() {
+ val map = MutableObjectIntMap<String>(0)
+ map["Hello"] = 1
+
+ assertEquals(1, map.size)
+ assertEquals(1, map["Hello"])
+ }
+
+ @Test
+ fun replaceExistingKey() {
+ val map = MutableObjectIntMap<String>()
+ map["Hello"] = 1
+ map["Hello"] = 2
+
+ assertEquals(1, map.size)
+ assertEquals(2, map["Hello"])
+ }
+
+ @Test
+ fun put() {
+ val map = MutableObjectIntMap<String>()
+
+ map.put("Hello", 1)
+ assertEquals(1, map["Hello"])
+ map.put("Hello", 2)
+ assertEquals(2, map["Hello"])
+ }
+
+ @Test
+ fun putAllArray() {
+ val map = MutableObjectIntMap<String?>()
+ map["Hello"] = 1
+ map[null] = 2
+ map["Bonjour"] = 2
+
+ map.putAll(arrayOf("Hallo" to 3, "Hola" to 7))
+
+ assertEquals(5, map.size)
+ assertEquals(3, map["Hallo"])
+ assertEquals(7, map["Hola"])
+ }
+
+ @Test
+ fun plus() {
+ val map = MutableObjectIntMap<String>()
+ map += "Hello" to 1
+
+ assertEquals(1, map.size)
+ assertEquals(1, map["Hello"])
+ }
+
+ @Test
+ fun plusArray() {
+ val map = MutableObjectIntMap<String>()
+ map += arrayOf("Hallo" to 3, "Hola" to 7)
+
+ assertEquals(2, map.size)
+ assertEquals(3, map["Hallo"])
+ assertEquals(7, map["Hola"])
+ }
+
+ @Test
+ fun nullKey() {
+ val map = MutableObjectIntMap<String?>()
+ map[null] = 1
+
+ assertEquals(1, map.size)
+ assertEquals(1, map[null])
+ }
+
+ @Test
+ fun findNonExistingKey() {
+ val map = MutableObjectIntMap<String>()
+ map["Hello"] = 1
+
+ assertFailsWith<NoSuchElementException> {
+ map["Bonjour"]
+ }
+ }
+
+ @Test
+ fun getOrDefault() {
+ val map = MutableObjectIntMap<String>()
+ map["Hello"] = 1
+
+ assertEquals(2, map.getOrDefault("Bonjour", 2))
+ }
+
+ @Test
+ fun getOrElse() {
+ val map = MutableObjectIntMap<String>()
+ map["Hello"] = 1
+
+ assertEquals(3, map.getOrElse("Hallo") { 3 })
+ }
+
+ @Test
+ fun getOrPut() {
+ val map = MutableObjectIntMap<String>()
+ map["Hello"] = 1
+
+ var counter = 0
+ map.getOrPut("Hello") {
+ counter++
+ 2
+ }
+ assertEquals(1, map["Hello"])
+ assertEquals(0, counter)
+
+ map.getOrPut("Bonjour") {
+ counter++
+ 2
+ }
+ assertEquals(2, map["Bonjour"])
+ assertEquals(1, counter)
+
+ map.getOrPut("Bonjour") {
+ counter++
+ 3
+ }
+ assertEquals(2, map["Bonjour"])
+ assertEquals(1, counter)
+
+ map.getOrPut("Hallo") {
+ counter++
+ 3
+ }
+ assertEquals(3, map["Hallo"])
+ assertEquals(2, counter)
+ }
+
+ @Test
+ fun remove() {
+ val map = MutableObjectIntMap<String?>()
+ map.remove("Hello")
+
+ map["Hello"] = 1
+ map.remove("Hello")
+ assertEquals(0, map.size)
+
+ map[null] = 1
+ map.remove(null)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val map = MutableObjectIntMap<String>(6)
+ map["Hello"] = 1
+ map["Bonjour"] = 2
+ map["Hallo"] = 3
+ map["Konnichiwa"] = 4
+ map["Ciao"] = 5
+ map["Annyeong"] = 6
+
+ // Removing all the entries will mark the medata as deleted
+ map.remove("Hello")
+ map.remove("Bonjour")
+ map.remove("Hallo")
+ map.remove("Konnichiwa")
+ map.remove("Ciao")
+ map.remove("Annyeong")
+
+ assertEquals(0, map.size)
+
+ val capacity = map.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ map["Hola"] = 7
+
+ assertEquals(1, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun removeIf() {
+ val map = MutableObjectIntMap<String>()
+ map["Hello"] = 1
+ map["Bonjour"] = 2
+ map["Hallo"] = 3
+ map["Konnichiwa"] = 4
+ map["Ciao"] = 5
+ map["Annyeong"] = 6
+
+ map.removeIf { key, _ -> key.startsWith('H') }
+
+ assertEquals(4, map.size)
+ assertEquals(2, map["Bonjour"])
+ assertEquals(4, map["Konnichiwa"])
+ assertEquals(5, map["Ciao"])
+ assertEquals(6, map["Annyeong"])
+ }
+
+ @Test
+ fun minus() {
+ val map = MutableObjectIntMap<String>()
+ map["Hello"] = 1
+ map["Bonjour"] = 2
+ map["Hallo"] = 3
+
+ map -= "Hello"
+
+ assertEquals(2, map.size)
+ assertFalse("Hello" in map)
+ }
+
+ @Test
+ fun minusArray() {
+ val map = MutableObjectIntMap<String>()
+ map["Hello"] = 1
+ map["Bonjour"] = 2
+ map["Hallo"] = 3
+
+ map -= arrayOf("Hallo", "Bonjour")
+
+ assertEquals(1, map.size)
+ assertFalse("Hallo" in map)
+ assertFalse("Bonjour" in map)
+ }
+
+ @Test
+ fun minusIterable() {
+ val map = MutableObjectIntMap<String>()
+ map["Hello"] = 1
+ map["Bonjour"] = 2
+ map["Hallo"] = 3
+
+ map -= listOf("Hallo", "Bonjour")
+
+ assertEquals(1, map.size)
+ assertFalse("Hallo" in map)
+ assertFalse("Bonjour" in map)
+ }
+
+ @Test
+ fun minusSequence() {
+ val map = MutableObjectIntMap<String>()
+ map["Hello"] = 1
+ map["Bonjour"] = 2
+ map["Hallo"] = 3
+
+ map -= listOf("Hallo", "Bonjour").asSequence()
+
+ assertEquals(1, map.size)
+ assertFalse("Hallo" in map)
+ assertFalse("Bonjour" in map)
+ }
+
+ @Test
+ fun conditionalRemove() {
+ val map = MutableObjectIntMap<String?>()
+ assertFalse(map.remove("Hello", 1))
+
+ map["Hello"] = 1
+ assertTrue(map.remove("Hello", 1))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val map = MutableObjectIntMap<String>()
+
+ for (i in 0 until 1700) {
+ map[i.toString()] = i.toInt()
+ }
+
+ assertEquals(1700, map.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val map = MutableObjectIntMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toString()] = j.toInt()
+ }
+
+ var counter = 0
+ map.forEach { key, value ->
+ assertEquals(key, value.toInt().toString())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachKey() {
+ for (i in 0..48) {
+ val map = MutableObjectIntMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toString()] = j.toInt()
+ }
+
+ var counter = 0
+ map.forEachKey { key ->
+ assertNotNull(key.toIntOrNull())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachValue() {
+ for (i in 0..48) {
+ val map = MutableObjectIntMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toString()] = j.toInt()
+ }
+
+ var counter = 0
+ val values = BooleanArray(map.size)
+ map.forEachValue { value ->
+ values[value.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ values.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun clear() {
+ val map = MutableObjectIntMap<String>()
+
+ for (i in 0 until 32) {
+ map[i.toString()] = i.toInt()
+ }
+
+ val capacity = map.capacity
+ map.clear()
+
+ assertEquals(0, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun string() {
+ val map = MutableObjectIntMap<String?>()
+ assertEquals("{}", map.toString())
+
+ map["Hello"] = 1
+ map["Bonjour"] = 2
+ val oneString = 1.toString()
+ val twoString = 2.toString()
+ assertTrue(
+ "{Hello=$oneString, Bonjour=$twoString}" == map.toString() ||
+ "{Bonjour=$twoString, Hello=$oneString}" == map.toString()
+ )
+
+ map.clear()
+ map[null] = 2
+ assertEquals("{null=$twoString}", map.toString())
+
+ val selfAsKeyMap = MutableObjectIntMap<Any>()
+ selfAsKeyMap[selfAsKeyMap] = 1
+ assertEquals("{(this)=$oneString}", selfAsKeyMap.toString())
+ }
+
+ @Test
+ fun equals() {
+ val map = MutableObjectIntMap<String?>()
+ map["Hello"] = 1
+ map[null] = 2
+
+ assertFalse(map.equals(null))
+ assertEquals(map, map)
+
+ val map2 = MutableObjectIntMap<String?>()
+ map2[null] = 2
+
+ assertNotEquals(map, map2)
+
+ map2["Hello"] = 1
+ assertEquals(map, map2)
+ }
+
+ @Test
+ fun containsKey() {
+ val map = MutableObjectIntMap<String?>()
+ map["Hello"] = 1
+ map[null] = 2
+
+ assertTrue(map.containsKey("Hello"))
+ assertTrue(map.containsKey(null))
+ assertFalse(map.containsKey("Bonjour"))
+ }
+
+ @Test
+ fun contains() {
+ val map = MutableObjectIntMap<String?>()
+ map["Hello"] = 1
+ map[null] = 2
+
+ assertTrue("Hello" in map)
+ assertTrue(null in map)
+ assertFalse("Bonjour" in map)
+ }
+
+ @Test
+ fun containsValue() {
+ val map = MutableObjectIntMap<String?>()
+ map["Hello"] = 1
+ map[null] = 2
+
+ assertTrue(map.containsValue(1))
+ assertTrue(map.containsValue(2))
+ assertFalse(map.containsValue(3))
+ }
+
+ @Test
+ fun empty() {
+ val map = MutableObjectIntMap<String?>()
+ assertTrue(map.isEmpty())
+ assertFalse(map.isNotEmpty())
+ assertTrue(map.none())
+ assertFalse(map.any())
+
+ map["Hello"] = 1
+
+ assertFalse(map.isEmpty())
+ assertTrue(map.isNotEmpty())
+ assertTrue(map.any())
+ assertFalse(map.none())
+ }
+
+ @Test
+ fun count() {
+ val map = MutableObjectIntMap<String>()
+ assertEquals(0, map.count())
+
+ map["Hello"] = 1
+ assertEquals(1, map.count())
+
+ map["Bonjour"] = 2
+ map["Hallo"] = 3
+ map["Konnichiwa"] = 4
+ map["Ciao"] = 5
+ map["Annyeong"] = 6
+
+ assertEquals(2, map.count { key, _ -> key.startsWith("H") })
+ assertEquals(0, map.count { key, _ -> key.startsWith("W") })
+ }
+
+ @Test
+ fun any() {
+ val map = MutableObjectIntMap<String>()
+ map["Hello"] = 1
+ map["Bonjour"] = 2
+ map["Hallo"] = 3
+ map["Konnichiwa"] = 4
+ map["Ciao"] = 5
+ map["Annyeong"] = 6
+
+ assertTrue(map.any { key, _ -> key.startsWith("K") })
+ assertFalse(map.any { key, _ -> key.startsWith("W") })
+ }
+
+ @Test
+ fun all() {
+ val map = MutableObjectIntMap<String>()
+ map["Hello"] = 1
+ map["Bonjour"] = 2
+ map["Hallo"] = 3
+ map["Konnichiwa"] = 4
+ map["Ciao"] = 5
+ map["Annyeong"] = 6
+
+ assertTrue(map.all { key, value -> key.length >= 4 && value.toInt() >= 1 })
+ assertFalse(map.all { key, _ -> key.startsWith("W") })
+ }
+
+ @Test
+ fun trim() {
+ val map = MutableObjectIntMap<String>()
+ assertEquals(7, map.trim())
+
+ map["Hello"] = 1
+ map["Hallo"] = 3
+
+ assertEquals(0, map.trim())
+
+ for (i in 0 until 1700) {
+ map[i.toString()] = i.toInt()
+ }
+
+ assertEquals(2047, map.capacity)
+
+ // After removing these items, our capacity needs should go
+ // from 2047 down to 1023
+ for (i in 0 until 1700) {
+ if (i and 0x1 == 0x0) {
+ val s = i.toString()
+ map.remove(s)
+ }
+ }
+
+ assertEquals(1024, map.trim())
+ assertEquals(0, map.trim())
+ }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectListTest.kt
new file mode 100644
index 0000000..d524dcf
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectListTest.kt
@@ -0,0 +1,1312 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class ObjectListTest {
+ private val list: MutableObjectList<Int> = mutableObjectListOf(1, 2, 3, 4, 5)
+
+ @Test
+ fun emptyConstruction() {
+ val l = mutableObjectListOf<Int>()
+ assertEquals(0, l.size)
+ assertEquals(16, l.capacity)
+ }
+
+ @Test
+ fun sizeConstruction() {
+ val l = MutableObjectList<Int>(4)
+ assertEquals(4, l.capacity)
+ }
+
+ @Test
+ fun contentConstruction() {
+ val l = mutableObjectListOf(1, 2, 3)
+ assertEquals(3, l.size)
+ assertEquals(1, l[0])
+ assertEquals(2, l[1])
+ assertEquals(3, l[2])
+ assertEquals(3, l.capacity)
+ repeat(2) {
+ val l2 = mutableObjectListOf(1, 2, 3, 4, 5)
+ assertEquals(list, l2)
+ l2.removeAt(0)
+ }
+ }
+
+ @Test
+ fun hashCodeTest() {
+ val l2 = mutableObjectListOf(1, 2, 3, 4, 5)
+ assertEquals(list.hashCode(), l2.hashCode())
+ l2.removeAt(4)
+ assertNotEquals(list.hashCode(), l2.hashCode())
+ l2.add(5)
+ assertEquals(list.hashCode(), l2.hashCode())
+ l2.clear()
+ assertNotEquals(list.hashCode(), l2.hashCode())
+ }
+
+ @Test
+ fun equalsTest() {
+ val l2 = mutableObjectListOf(1, 2, 3, 4, 5)
+ assertEquals(list, l2)
+ assertNotEquals(list, mutableObjectListOf())
+ l2.removeAt(4)
+ assertNotEquals(list, l2)
+ l2.add(5)
+ assertEquals(list, l2)
+ l2.clear()
+ assertNotEquals(list, l2)
+ }
+
+ @Test
+ fun string() {
+ assertEquals("[1, 2, 3, 4, 5]", list.toString())
+ assertEquals("[]", mutableObjectListOf<Int>().toString())
+ }
+
+ @Test
+ fun size() {
+ assertEquals(5, list.size)
+ assertEquals(5, list.count())
+ val l2 = mutableObjectListOf<Int>()
+ assertEquals(0, l2.size)
+ assertEquals(0, l2.count())
+ l2 += 1
+ assertEquals(1, l2.size)
+ assertEquals(1, l2.count())
+ }
+
+ @Test
+ fun get() {
+ assertEquals(1, list[0])
+ assertEquals(5, list[4])
+ assertEquals(1, list.elementAt(0))
+ assertEquals(5, list.elementAt(4))
+ }
+
+ @Test
+ fun getOutOfBounds() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ list[5]
+ }
+ }
+
+ @Test
+ fun getOutOfBoundsNegative() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ list[-1]
+ }
+ }
+
+ @Test
+ fun elementAtOfBounds() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ list.elementAt(5)
+ }
+ }
+
+ @Test
+ fun elementAtOfBoundsNegative() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ list.elementAt(-1)
+ }
+ }
+
+ @Test
+ fun elementAtOrElse() {
+ assertEquals(1, list.elementAtOrElse(0) {
+ assertEquals(0, it)
+ 0
+ })
+ assertEquals(0, list.elementAtOrElse(-1) {
+ assertEquals(-1, it)
+ 0
+ })
+ assertEquals(0, list.elementAtOrElse(5) {
+ assertEquals(5, it)
+ 0
+ })
+ }
+
+ @Test
+ fun count() {
+ assertEquals(1, list.count { it < 2 })
+ assertEquals(0, list.count { it < 0 })
+ assertEquals(5, list.count { it < 10 })
+ }
+
+ @Test
+ fun isEmpty() {
+ assertFalse(list.isEmpty())
+ assertFalse(list.none())
+ assertTrue(mutableObjectListOf<Int>().isEmpty())
+ assertTrue(mutableObjectListOf<Int>().none())
+ }
+
+ @Test
+ fun isNotEmpty() {
+ assertTrue(list.isNotEmpty())
+ assertTrue(list.any())
+ assertFalse(mutableObjectListOf<Int>().isNotEmpty())
+ }
+
+ @Test
+ fun indices() {
+ assertEquals(IntRange(0, 4), list.indices)
+ assertEquals(IntRange(0, -1), mutableObjectListOf<Int>().indices)
+ }
+
+ @Test
+ fun any() {
+ assertTrue(list.any { it == 5 })
+ assertTrue(list.any { it == 1 })
+ assertFalse(list.any { it == 0 })
+ }
+
+ @Test
+ fun reversedAny() {
+ val reversedList = mutableObjectListOf<Int>()
+ assertFalse(
+ list.reversedAny {
+ reversedList.add(it)
+ false
+ }
+ )
+ val reversedContent = mutableObjectListOf(5, 4, 3, 2, 1)
+ assertEquals(reversedContent, reversedList)
+
+ val reversedSublist = mutableObjectListOf<Int>()
+ assertTrue(
+ list.reversedAny {
+ reversedSublist.add(it)
+ reversedSublist.size == 2
+ }
+ )
+ assertEquals(reversedSublist, mutableObjectListOf(5, 4))
+ }
+
+ @Test
+ fun forEach() {
+ val copy = mutableObjectListOf<Int>()
+ list.forEach { copy += it }
+ assertEquals(list, copy)
+ }
+
+ @Test
+ fun forEachReversed() {
+ val copy = mutableObjectListOf<Int>()
+ list.forEachReversed { copy += it }
+ assertEquals(copy, mutableObjectListOf(5, 4, 3, 2, 1))
+ }
+
+ @Test
+ fun forEachIndexed() {
+ val copy = mutableObjectListOf<Int>()
+ val indices = mutableObjectListOf<Int>()
+ list.forEachIndexed { index, open ->
+ copy += open
+ indices += index
+ }
+ assertEquals(list, copy)
+ assertEquals(indices, mutableObjectListOf(0, 1, 2, 3, 4))
+ }
+
+ @Test
+ fun forEachReversedIndexed() {
+ val copy = mutableObjectListOf<Int>()
+ val indices = mutableObjectListOf<Int>()
+ list.forEachReversedIndexed { index, open ->
+ copy += open
+ indices += index
+ }
+ assertEquals(copy, mutableObjectListOf(5, 4, 3, 2, 1))
+ assertEquals(indices, mutableObjectListOf(4, 3, 2, 1, 0))
+ }
+
+ @Test
+ fun indexOfFirst() {
+ assertEquals(0, list.indexOfFirst { it < 2 })
+ assertEquals(4, list.indexOfFirst { it > 4 })
+ assertEquals(-1, list.indexOfFirst { it < 0 })
+ assertEquals(0, mutableObjectListOf(8, 8).indexOfFirst { it > 7 })
+ }
+
+ @Test
+ fun firstOrNullNoParam() {
+ assertEquals(1, list.firstOrNull())
+ assertNull(emptyObjectList<Int>().firstOrNull())
+ }
+
+ @Test
+ fun firstOrNull() {
+ assertEquals(1, list.firstOrNull { it < 5 })
+ assertEquals(3, list.firstOrNull { it > 2 })
+ assertEquals(5, list.firstOrNull { it > 4 })
+ assertNull(list.firstOrNull { it > 5 })
+ }
+
+ @Test
+ fun lastOrNullNoParam() {
+ assertEquals(5, list.lastOrNull())
+ assertNull(emptyObjectList<Int>().lastOrNull())
+ }
+
+ @Test
+ fun lastOrNull() {
+ assertEquals(4, list.lastOrNull { it < 5 })
+ assertEquals(5, list.lastOrNull { it > 2 })
+ assertEquals(1, list.lastOrNull { it < 2 })
+ assertNull(list.firstOrNull { it > 5 })
+ }
+
+ @Test
+ fun indexOfLast() {
+ assertEquals(0, list.indexOfLast { it < 2 })
+ assertEquals(4, list.indexOfLast { it > 4 })
+ assertEquals(-1, list.indexOfLast { it < 0 })
+ assertEquals(1, objectListOf(8, 8).indexOfLast { it > 7 })
+ }
+
+ @Test
+ fun contains() {
+ assertTrue(list.contains(5))
+ assertTrue(list.contains(1))
+ assertFalse(list.contains(0))
+ }
+
+ @Test
+ fun containsAllList() {
+ assertTrue(list.containsAll(mutableObjectListOf(2, 3, 1)))
+ assertFalse(list.containsAll(mutableObjectListOf(2, 3, 6)))
+ }
+
+ @Test
+ fun lastIndexOf() {
+ assertEquals(4, list.lastIndexOf(5))
+ assertEquals(1, list.lastIndexOf(2))
+ val copy = mutableObjectListOf<Int>()
+ copy.addAll(list)
+ copy.addAll(list)
+ assertEquals(5, copy.lastIndexOf(1))
+ }
+
+ @Test
+ fun first() {
+ assertEquals(1, list.first())
+ }
+
+ @Test
+ fun firstException() {
+ assertFailsWith(NoSuchElementException::class) {
+ mutableObjectListOf<Int>().first()
+ }
+ }
+
+ @Test
+ fun firstWithPredicate() {
+ assertEquals(5, list.first { it > 4 })
+ assertEquals(1, mutableObjectListOf(1, 5).first { it > 0 })
+ }
+
+ @Test
+ fun firstWithPredicateException() {
+ assertFailsWith(NoSuchElementException::class) {
+ mutableObjectListOf<Int>().first { it > 8 }
+ }
+ }
+
+ @Test
+ fun last() {
+ assertEquals(5, list.last())
+ }
+
+ @Test
+ fun lastException() {
+ assertFailsWith(NoSuchElementException::class) {
+ mutableObjectListOf<Int>().last()
+ }
+ }
+
+ @Test
+ fun lastWithPredicate() {
+ assertEquals(1, list.last { it < 2 })
+ assertEquals(5, objectListOf(1, 5).last { it > 0 })
+ }
+
+ @Test
+ fun lastWithPredicateException() {
+ assertFailsWith<NoSuchElementException> {
+ objectListOf(2).last { it > 2 }
+ }
+ }
+
+ @Test
+ fun fold() {
+ assertEquals("12345", list.fold("") { acc, i -> acc + i.toString() })
+ }
+
+ @Test
+ fun foldIndexed() {
+ assertEquals(
+ "01-12-23-34-45-",
+ list.foldIndexed("") { index, acc, i ->
+ "$acc$index$i-"
+ }
+ )
+ }
+
+ @Test
+ fun foldRight() {
+ assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toString() })
+ }
+
+ @Test
+ fun foldRightIndexed() {
+ assertEquals(
+ "45-34-23-12-01-",
+ list.foldRightIndexed("") { index, i, acc ->
+ "$acc$index$i-"
+ }
+ )
+ }
+
+ @Test
+ fun add() {
+ val l = mutableObjectListOf(1, 2, 3)
+ l += 4
+ l.add(5)
+ assertEquals(list, l)
+ }
+
+ @Test
+ fun addAtIndex() {
+ val l = mutableObjectListOf(2, 4)
+ l.add(2, 5)
+ l.add(0, 1)
+ l.add(2, 3)
+ assertEquals(list, l)
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ l.add(-1, 2)
+ }
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ l.add(6, 2)
+ }
+ }
+
+ @Test
+ fun addAllListAtIndex() {
+ val l = mutableObjectListOf(4)
+ val l2 = mutableObjectListOf(1, 2)
+ val l3 = mutableObjectListOf(5)
+ val l4 = mutableObjectListOf(3)
+ assertTrue(l4.addAll(1, l3))
+ assertTrue(l4.addAll(0, l2))
+ assertTrue(l4.addAll(3, l))
+ assertFalse(l4.addAll(0, mutableObjectListOf()))
+ assertEquals(list, l4)
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ l4.addAll(6, mutableObjectListOf())
+ }
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ l4.addAll(-1, mutableObjectListOf())
+ }
+ }
+
+ @Test
+ fun addAllObjectList() {
+ val l = MutableObjectList<Int>()
+ l.add(3)
+ l.add(4)
+ l.add(5)
+ val l2 = mutableObjectListOf(1, 2)
+ assertTrue(l2.addAll(l))
+ assertEquals(list, l2)
+ assertFalse(l2.addAll(mutableObjectListOf()))
+ }
+
+ @Test
+ fun addAllList() {
+ val l = listOf(3, 4, 5)
+ val l2 = mutableObjectListOf(1, 2)
+ assertTrue(l2.addAll(l))
+ assertEquals(list, l2)
+ assertFalse(l2.addAll(mutableObjectListOf()))
+ }
+
+ @Test
+ fun addAllIterable() {
+ val l = listOf(3, 4, 5) as Iterable<Int>
+ val l2 = mutableObjectListOf(1, 2)
+ assertTrue(l2.addAll(l))
+ assertEquals(list, l2)
+ assertFalse(l2.addAll(mutableObjectListOf()))
+ }
+
+ @Test
+ fun addAllSequence() {
+ val l = listOf(3, 4, 5).asSequence()
+ val l2 = mutableObjectListOf(1, 2)
+ assertTrue(l2.addAll(l))
+ assertEquals(list, l2)
+ assertFalse(l2.addAll(mutableObjectListOf()))
+ }
+
+ @Test
+ fun plusAssignObjectList() {
+ val l = objectListOf(3, 4, 5)
+ val l2 = mutableObjectListOf(1, 2)
+ l2 += l
+ assertEquals(list, l2)
+ }
+
+ @Test
+ fun plusAssignIterable() {
+ val l = listOf(3, 4, 5) as Iterable<Int>
+ val l2 = mutableObjectListOf(1, 2)
+ l2 += l
+ assertEquals(list, l2)
+ }
+
+ @Test
+ fun plusAssignSequence() {
+ val l = arrayOf(3, 4, 5).asSequence()
+ val l2 = mutableObjectListOf(1, 2)
+ l2 += l
+ assertEquals(list, l2)
+ }
+
+ @Test
+ fun addAllArrayAtIndex() {
+ val a1 = arrayOf(4)
+ val a2 = arrayOf(1, 2)
+ val a3 = arrayOf(5)
+ val l = mutableObjectListOf(3)
+ assertTrue(l.addAll(1, a3))
+ assertTrue(l.addAll(0, a2))
+ assertTrue(l.addAll(3, a1))
+ assertFalse(l.addAll(0, arrayOf()))
+ assertEquals(list, l)
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ l.addAll(6, arrayOf())
+ }
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ l.addAll(-1, arrayOf())
+ }
+ }
+
+ @Test
+ fun addAllArray() {
+ val a = arrayOf(3, 4, 5)
+ val v = mutableObjectListOf(1, 2)
+ v.addAll(a)
+ assertEquals(5, v.size)
+ assertEquals(3, v[2])
+ assertEquals(4, v[3])
+ assertEquals(5, v[4])
+ }
+
+ @Test
+ fun plusAssignArray() {
+ val a = arrayOf(3, 4, 5)
+ val v = mutableObjectListOf(1, 2)
+ v += a
+ assertEquals(list, v)
+ }
+
+ @Test
+ fun clear() {
+ val l = mutableObjectListOf<Int>()
+ l.addAll(list)
+ assertTrue(l.isNotEmpty())
+ l.clear()
+ assertTrue(l.isEmpty())
+ repeat(5) { index ->
+ assertNull(l.content[index])
+ }
+ }
+
+ @Test
+ fun trim() {
+ val l = mutableObjectListOf(1)
+ l.trim()
+ assertEquals(1, l.capacity)
+ l += arrayOf(1, 2, 3, 4, 5)
+ l.trim()
+ assertEquals(6, l.capacity)
+ assertEquals(6, l.size)
+ l.clear()
+ l.trim()
+ assertEquals(0, l.capacity)
+ l.trim(100)
+ assertEquals(0, l.capacity)
+ l += arrayOf(1, 2, 3, 4, 5)
+ l -= 5
+ l.trim(5)
+ assertEquals(5, l.capacity)
+ l.trim(4)
+ assertEquals(4, l.capacity)
+ l.trim(3)
+ assertEquals(4, l.capacity)
+ }
+
+ @Test
+ fun remove() {
+ val l = mutableObjectListOf(1, 2, 3, 4, 5)
+ l.remove(3)
+ assertEquals(mutableObjectListOf(1, 2, 4, 5), l)
+ }
+
+ @Test
+ fun removeIf() {
+ val l = mutableObjectListOf(1, 2, 3, 4, 5, 6)
+ l.removeIf { it == 100 }
+ assertEquals(objectListOf(1, 2, 3, 4, 5, 6), l)
+ l.removeIf { it % 2 == 0 }
+ assertEquals(objectListOf(1, 3, 5), l)
+ repeat(3) {
+ assertNull(l.content[3 + it])
+ }
+ l.removeIf { it != 3 }
+ assertEquals(objectListOf(3), l)
+ }
+
+ @Test
+ fun removeAt() {
+ val l = mutableObjectListOf(1, 2, 3, 4, 5)
+ l.removeAt(2)
+ assertNull(l.content[4])
+ assertEquals(mutableObjectListOf(1, 2, 4, 5), l)
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ l.removeAt(6)
+ }
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ l.removeAt(-1)
+ }
+ }
+
+ @Test
+ fun set() {
+ val l = mutableObjectListOf(0, 0, 0, 0, 0)
+ l[0] = 1
+ l[4] = 5
+ l[2] = 3
+ l[1] = 2
+ l[3] = 4
+ assertEquals(list, l)
+ assertFailsWith<IndexOutOfBoundsException> {
+ l[-1] = 1
+ }
+ assertFailsWith<IndexOutOfBoundsException> {
+ l[6] = 1
+ }
+ assertEquals(4, l.set(3, 1));
+ }
+
+ @Test
+ fun ensureCapacity() {
+ val l = mutableObjectListOf(1)
+ assertEquals(1, l.capacity)
+ l.ensureCapacity(5)
+ assertEquals(5, l.capacity)
+ }
+
+ @Test
+ fun removeAllObjectList() {
+ assertFalse(list.removeAll(mutableObjectListOf(0, 10, 15)))
+ val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+ assertTrue(l.removeAll(mutableObjectListOf(20, 0, 15, 10, 5)))
+ assertEquals(list, l)
+ }
+
+ @Test
+ fun removeAllScatterSet() {
+ assertFalse(list.removeAll(scatterSetOf(0, 10, 15)))
+ val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+ assertTrue(l.removeAll(scatterSetOf(20, 0, 15, 10, 5)))
+ assertEquals(list, l)
+ }
+
+ @Test
+ fun removeAllArray() {
+ assertFalse(list.removeAll(arrayOf(0, 10, 15)))
+ val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+ assertTrue(l.removeAll(arrayOf(20, 0, 15, 10, 5)))
+ assertEquals(list, l)
+ }
+
+ @Test
+ fun removeAllList() {
+ assertFalse(list.removeAll(listOf(0, 10, 15)))
+ val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+ assertTrue(l.removeAll(listOf(20, 0, 15, 10, 5)))
+ assertEquals(list, l)
+ }
+
+ @Test
+ fun removeAllIterable() {
+ assertFalse(list.removeAll(listOf(0, 10, 15) as Iterable<Int>))
+ val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+ assertTrue(l.removeAll(listOf(20, 0, 15, 10, 5) as Iterable<Int>))
+ assertEquals(list, l)
+ }
+
+ @Test
+ fun removeAllSequence() {
+ assertFalse(list.removeAll(listOf(0, 10, 15).asSequence()))
+ val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+ assertTrue(l.removeAll(listOf(20, 0, 15, 10, 5).asSequence()))
+ assertEquals(list, l)
+ }
+
+ @Test
+ fun minusAssignObjectList() {
+ val l = mutableObjectListOf<Int>().also { it += list }
+ l -= mutableObjectListOf(0, 10, 15)
+ assertEquals(list, l)
+ val l2 = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+ l2 -= mutableObjectListOf(20, 0, 15, 10, 5)
+ assertEquals(list, l2)
+ }
+
+ @Test
+ fun minusAssignScatterSet() {
+ val l = mutableObjectListOf<Int>().also { it += list }
+ l -= scatterSetOf(0, 10, 15)
+ assertEquals(list, l)
+ val l2 = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+ l2 -= scatterSetOf(20, 0, 15, 10, 5)
+ assertEquals(list, l2)
+ }
+
+ @Test
+ fun minusAssignArray() {
+ val l = mutableObjectListOf<Int>().also { it += list }
+ l -= arrayOf(0, 10, 15)
+ assertEquals(list, l)
+ val l2 = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+ l2 -= arrayOf(20, 0, 15, 10, 5)
+ assertEquals(list, l2)
+ }
+
+ @Test
+ fun minusAssignList() {
+ val l = mutableObjectListOf<Int>().also { it += list }
+ l -= listOf(0, 10, 15)
+ assertEquals(list, l)
+ val l2 = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+ l2 -= listOf(20, 0, 15, 10, 5)
+ assertEquals(list, l2)
+ }
+
+ @Test
+ fun minusAssignIterable() {
+ val l = mutableObjectListOf<Int>().also { it += list }
+ l -= listOf(0, 10, 15) as Iterable<Int>
+ assertEquals(list, l)
+ val l2 = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+ l2 -= listOf(20, 0, 15, 10, 5) as Iterable<Int>
+ assertEquals(list, l2)
+ }
+
+ @Test
+ fun minusAssignSequence() {
+ val l = mutableObjectListOf<Int>().also { it += list }
+ l -= listOf(0, 10, 15).asSequence()
+ assertEquals(list, l)
+ val l2 = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+ l2 -= listOf(20, 0, 15, 10, 5).asSequence()
+ assertEquals(list, l2)
+ }
+
+ @Test
+ fun retainAll() {
+ assertFalse(list.retainAll(mutableObjectListOf(1, 2, 3, 4, 5, 6)))
+ val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20)
+ assertTrue(l.retainAll(mutableObjectListOf(1, 2, 3, 4, 5, 6)))
+ assertEquals(list, l)
+ }
+
+ @Test
+ fun retainAllArray() {
+ assertFalse(list.retainAll(arrayOf(1, 2, 3, 4, 5, 6)))
+ val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20)
+ assertTrue(l.retainAll(arrayOf(1, 2, 3, 4, 5, 6)))
+ assertEquals(list, l)
+ }
+
+ @Test
+ fun retainAllCollection() {
+ assertFalse(list.retainAll(listOf(1, 2, 3, 4, 5, 6)))
+ val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20)
+ assertTrue(l.retainAll(listOf(1, 2, 3, 4, 5, 6)))
+ assertEquals(list, l)
+ }
+
+ @Test
+ fun retainAllIterable() {
+ assertFalse(list.retainAll(listOf(1, 2, 3, 4, 5, 6) as Iterable<Int>))
+ val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20)
+ assertTrue(l.retainAll(listOf(1, 2, 3, 4, 5, 6) as Iterable<Int>))
+ assertEquals(list, l)
+ }
+
+ @Test
+ fun retainAllSequence() {
+ assertFalse(list.retainAll(arrayOf(1, 2, 3, 4, 5, 6).asSequence()))
+ val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20)
+ assertTrue(l.retainAll(arrayOf(1, 2, 3, 4, 5, 6).asSequence()))
+ assertEquals(list, l)
+ }
+
+ @Test
+ fun removeRange() {
+ val l = mutableObjectListOf(1, 9, 7, 6, 2, 3, 4, 5)
+ l.removeRange(1, 4)
+ assertNull(l.content[5])
+ assertNull(l.content[6])
+ assertNull(l.content[7])
+ assertEquals(list, l)
+ assertFailsWith<IndexOutOfBoundsException> {
+ l.removeRange(6, 6)
+ }
+ assertFailsWith<IndexOutOfBoundsException> {
+ l.removeRange(100, 200)
+ }
+ assertFailsWith<IndexOutOfBoundsException> {
+ l.removeRange(-1, 0)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ l.removeRange(3, 2)
+ }
+ }
+
+ @Test
+ fun testEmptyObjectList() {
+ val l = emptyObjectList<Int>()
+ assertEquals(0, l.size)
+ }
+
+ @Test
+ fun objectListOfEmpty() {
+ val l = objectListOf<Int>()
+ assertEquals(0, l.size)
+ }
+
+ @Test
+ fun objectListOfOneValue() {
+ val l = objectListOf(2)
+ assertEquals(1, l.size)
+ assertEquals(2, l[0])
+ }
+
+ @Test
+ fun objectListOfTwoValues() {
+ val l = objectListOf(2, 1)
+ assertEquals(2, l.size)
+ assertEquals(2, l[0])
+ assertEquals(1, l[1])
+ }
+
+ @Test
+ fun objectListOfThreeValues() {
+ val l = objectListOf(2, 10, -1)
+ assertEquals(3, l.size)
+ assertEquals(2, l[0])
+ assertEquals(10, l[1])
+ assertEquals(-1, l[2])
+ }
+
+ @Test
+ fun objectListOfFourValues() {
+ val l = objectListOf(2, 10, -1, 10)
+ assertEquals(4, l.size)
+ assertEquals(2, l[0])
+ assertEquals(10, l[1])
+ assertEquals(-1, l[2])
+ assertEquals(10, l[3])
+ }
+
+ @Test
+ fun mutableObjectListOfOneValue() {
+ val l = mutableObjectListOf(2)
+ assertEquals(1, l.size)
+ assertEquals(1, l.capacity)
+ assertEquals(2, l[0])
+ }
+
+ @Test
+ fun mutableObjectListOfTwoValues() {
+ val l = mutableObjectListOf(2, 1)
+ assertEquals(2, l.size)
+ assertEquals(2, l.capacity)
+ assertEquals(2, l[0])
+ assertEquals(1, l[1])
+ }
+
+ @Test
+ fun mutableObjectListOfThreeValues() {
+ val l = mutableObjectListOf(2, 10, -1)
+ assertEquals(3, l.size)
+ assertEquals(3, l.capacity)
+ assertEquals(2, l[0])
+ assertEquals(10, l[1])
+ assertEquals(-1, l[2])
+ }
+
+ @Test
+ fun mutableObjectListOfFourValues() {
+ val l = mutableObjectListOf(2, 10, -1, 10)
+ assertEquals(4, l.size)
+ assertEquals(4, l.capacity)
+ assertEquals(2, l[0])
+ assertEquals(10, l[1])
+ assertEquals(-1, l[2])
+ assertEquals(10, l[3])
+ }
+
+ @Test
+ fun iterator() {
+ val l = mutableObjectListOf(1, 2, 3, 4, 5)
+ val iterator = l.asMutableList().iterator()
+ assertTrue(iterator.hasNext())
+ assertEquals(1, iterator.next())
+ assertTrue(iterator.hasNext())
+ assertEquals(2, iterator.next())
+ assertTrue(iterator.hasNext())
+ assertEquals(3, iterator.next())
+ assertTrue(iterator.hasNext())
+ iterator.remove()
+ assertTrue(iterator.hasNext())
+ assertEquals(l, mutableObjectListOf(1, 2, 4, 5))
+
+ assertEquals(4, iterator.next())
+ assertTrue(iterator.hasNext())
+ assertEquals(5, iterator.next())
+ assertFalse(iterator.hasNext())
+ iterator.remove()
+ assertEquals(l, mutableObjectListOf(1, 2, 4))
+ }
+
+ @Test
+ fun listIterator() {
+ val l = mutableObjectListOf(1, 2, 3, 4, 5)
+ val iterator = l.asMutableList().listIterator()
+ assertEquals(1, iterator.next())
+ assertEquals(1, iterator.previous())
+ assertEquals(0, iterator.nextIndex())
+ iterator.add(6)
+ assertEquals(1, iterator.nextIndex())
+ assertEquals(0, iterator.previousIndex())
+ assertEquals(6, iterator.previous())
+ assertEquals(l, mutableObjectListOf(6, 1, 2, 3, 4, 5))
+ }
+
+ @Test
+ fun listIteratorInitialIndex() {
+ val iterator = list.asMutableList().listIterator(2)
+ assertEquals(2, iterator.nextIndex())
+ }
+
+ @Test
+ fun subList() {
+ val l = list.asMutableList().subList(1, 4)
+ assertEquals(3, l.size)
+ assertEquals(2, l[0])
+ assertEquals(3, l[1])
+ assertEquals(4, l[2])
+ }
+
+ @Test
+ fun subListContains() {
+ val l = list.asMutableList().subList(1, 4)
+ assertTrue(l.contains(2))
+ assertTrue(l.contains(3))
+ assertTrue(l.contains(4))
+ assertFalse(l.contains(5))
+ assertFalse(l.contains(1))
+ }
+
+ @Test
+ fun subListContainsAll() {
+ val l = list.asMutableList().subList(1, 4)
+ val smallList = listOf(2, 3, 4)
+ assertTrue(l.containsAll(smallList))
+ val largeList = listOf(3, 4, 5)
+ assertFalse(l.containsAll(largeList))
+ }
+
+ @Test
+ fun subListIndexOf() {
+ val l = list.asMutableList().subList(1, 4)
+ assertEquals(0, l.indexOf(2))
+ assertEquals(2, l.indexOf(4))
+ assertEquals(-1, l.indexOf(1))
+ val l2 = mutableObjectListOf(2, 1, 1, 3).asMutableList().subList(1, 2)
+ assertEquals(0, l2.indexOf(1))
+ }
+
+ @Test
+ fun subListIsEmpty() {
+ val l = list.asMutableList().subList(1, 4)
+ assertFalse(l.isEmpty())
+ assertTrue(list.asMutableList().subList(4, 4).isEmpty())
+ }
+
+ @Test
+ fun subListIterator() {
+ val l = list.asMutableList().subList(1, 4)
+ val l2 = mutableListOf<Int>()
+ l.forEach { l2 += it }
+ assertEquals(3, l2.size)
+ assertEquals(2, l2[0])
+ assertEquals(3, l2[1])
+ assertEquals(4, l2[2])
+ }
+
+ @Test
+ fun subListLastIndexOf() {
+ val l = list.asMutableList().subList(1, 4)
+ assertEquals(0, l.lastIndexOf(2))
+ assertEquals(2, l.lastIndexOf(4))
+ assertEquals(-1, l.lastIndexOf(1))
+ val l2 = mutableObjectListOf(2, 1, 1, 3).asMutableList().subList(1, 3)
+ assertEquals(1, l2.lastIndexOf(1))
+ }
+
+ @Test
+ fun subListAdd() {
+ val v = mutableObjectListOf(1, 2, 3)
+ val l = v.asMutableList().subList(1, 2)
+ assertTrue(l.add(4))
+ assertEquals(2, l.size)
+ assertEquals(4, v.size)
+ assertEquals(2, l[0])
+ assertEquals(4, l[1])
+ assertEquals(2, v[1])
+ assertEquals(4, v[2])
+ assertEquals(3, v[3])
+ }
+
+ @Test
+ fun subListAddIndex() {
+ val v = mutableObjectListOf(6, 1, 2, 3)
+ val l = v.asMutableList().subList(1, 3)
+ l.add(1, 4)
+ assertEquals(3, l.size)
+ assertEquals(5, v.size)
+ assertEquals(1, l[0])
+ assertEquals(4, l[1])
+ assertEquals(2, l[2])
+ assertEquals(1, v[1])
+ assertEquals(4, v[2])
+ assertEquals(2, v[3])
+ }
+
+ @Test
+ fun subListAddAllAtIndex() {
+ val v = mutableObjectListOf(6, 1, 2, 3)
+ val l = v.asMutableList().subList(1, 3)
+ l.addAll(1, listOf(4, 5))
+ assertEquals(4, l.size)
+ assertEquals(6, v.size)
+ assertEquals(1, l[0])
+ assertEquals(4, l[1])
+ assertEquals(5, l[2])
+ assertEquals(2, l[3])
+ assertEquals(1, v[1])
+ assertEquals(4, v[2])
+ assertEquals(5, v[3])
+ assertEquals(2, v[4])
+ }
+
+ @Test
+ fun subListAddAll() {
+ val v = mutableObjectListOf(6, 1, 2, 3)
+ val l = v.asMutableList().subList(1, 3)
+ l.addAll(listOf(4, 5))
+ assertEquals(4, l.size)
+ assertEquals(6, v.size)
+ assertEquals(1, l[0])
+ assertEquals(2, l[1])
+ assertEquals(4, l[2])
+ assertEquals(5, l[3])
+ assertEquals(1, v[1])
+ assertEquals(2, v[2])
+ assertEquals(4, v[3])
+ assertEquals(5, v[4])
+ assertEquals(3, v[5])
+ }
+
+ @Test
+ fun subListClear() {
+ val v = mutableObjectListOf(1, 2, 3, 4, 5)
+ val l = v.asMutableList().subList(1, 4)
+ l.clear()
+ assertEquals(0, l.size)
+ assertEquals(2, v.size)
+ assertEquals(1, v[0])
+ assertEquals(5, v[1])
+ kotlin.test.assertNull(v.content[2])
+ kotlin.test.assertNull(v.content[3])
+ kotlin.test.assertNull(v.content[4])
+ }
+
+ @Test
+ fun subListListIterator() {
+ val l = list.asMutableList().subList(1, 4)
+ val listIterator = l.listIterator()
+ assertTrue(listIterator.hasNext())
+ assertFalse(listIterator.hasPrevious())
+ assertEquals(0, listIterator.nextIndex())
+ assertEquals(2, listIterator.next())
+ }
+
+ @Test
+ fun subListListIteratorWithIndex() {
+ val l = list.asMutableList().subList(1, 4)
+ val listIterator = l.listIterator(1)
+ assertTrue(listIterator.hasNext())
+ assertTrue(listIterator.hasPrevious())
+ assertEquals(1, listIterator.nextIndex())
+ assertEquals(3, listIterator.next())
+ }
+
+ @Test
+ fun subListRemove() {
+ val l = mutableObjectListOf(1, 2, 3, 4, 5)
+ val l2 = l.asMutableList().subList(1, 4)
+ assertTrue(l2.remove(3))
+ assertEquals(l, mutableObjectListOf(1, 2, 4, 5))
+ assertEquals(2, l2.size)
+ assertEquals(2, l2[0])
+ assertEquals(4, l2[1])
+ assertFalse(l2.remove(3))
+ assertEquals(l, mutableObjectListOf(1, 2, 4, 5))
+ assertEquals(2, l2.size)
+ }
+
+ @Test
+ fun subListRemoveAll() {
+ val l = mutableObjectListOf(1, 2, 3, 4, 5)
+ val l2 = l.asMutableList().subList(1, 4)
+ assertFalse(l2.removeAll(listOf(1, 5, -1)))
+ assertEquals(5, l.size)
+ assertEquals(3, l2.size)
+ assertTrue(l2.removeAll(listOf(3, 4, 5)))
+ assertEquals(3, l.size)
+ assertEquals(1, l2.size)
+ }
+
+ @Test
+ fun subListRemoveAt() {
+ val l = mutableObjectListOf(1, 2, 3, 4, 5)
+ val l2 = l.asMutableList().subList(1, 4)
+ assertEquals(3, l2.removeAt(1))
+ assertEquals(4, l.size)
+ assertEquals(2, l2.size)
+ assertEquals(4, l2.removeAt(1))
+ assertEquals(1, l2.size)
+ }
+
+ @Test
+ fun subListRetainAll() {
+ val l = mutableObjectListOf(1, 2, 3, 4, 5)
+ val l2 = l.asMutableList().subList(1, 4)
+ val l3 = objectListOf(1, 2, 3, 4, 5)
+ assertFalse(l2.retainAll(l3.asList()))
+ assertFalse(l2.retainAll(listOf(2, 3, 4)))
+ assertEquals(3, l2.size)
+ assertEquals(5, l.size)
+ assertTrue(l2.retainAll(setOf(1, 2, 4)))
+ assertEquals(4, l.size)
+ assertEquals(2, l2.size)
+ assertEquals(l, objectListOf(1, 2, 4, 5))
+ }
+
+ @Test
+ fun subListSet() {
+ val l = mutableObjectListOf(1, 2, 3, 4, 5)
+ val l2 = l.asMutableList().subList(1, 4)
+ l2[1] = 10
+ assertEquals(10, l2[1])
+ assertEquals(3, l2.size)
+ assertEquals(10, l[2])
+ }
+
+ @Test
+ fun subListSubList() {
+ val l = objectListOf(1, 2, 3, 4, 5).asList().subList(1, 5)
+ val l2 = l.subList(1, 3)
+ assertEquals(2, l2.size)
+ assertEquals(3, l2[0])
+ }
+
+ @Suppress("KotlinConstantConditions")
+ @Test
+ fun list_outOfBounds_Get_Below() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ val l = mutableObjectListOf(1, 2, 3, 4).asMutableList()
+ l[-1]
+ }
+ }
+
+ @Suppress("KotlinConstantConditions")
+ @Test
+ fun sublist_outOfBounds_Get_Below() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ val l = mutableObjectListOf(1, 2, 3, 4).asMutableList().subList(1, 2)
+ l[-1]
+ }
+ }
+
+ @Test
+ fun list_outOfBounds_Get_Above() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ val l = mutableObjectListOf(1, 2, 3, 4).asMutableList()
+ l[4]
+ }
+ }
+
+ @Test
+ fun sublist_outOfBounds_Get_Above() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ val l = mutableObjectListOf(1, 2, 3, 4).asMutableList().subList(1, 2)
+ l[1]
+ }
+ }
+
+ @Test
+ fun list_outOfBounds_RemoveAt_Below() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+ l.removeAt(-1)
+ }
+ }
+
+ @Test
+ fun sublist_outOfBounds_RemoveAt_Below() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+ l.removeAt(-1)
+ }
+ }
+
+ @Test
+ fun list_outOfBounds_RemoveAt_Above() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+ l.removeAt(4)
+ }
+ }
+
+ @Test
+ fun sublist_outOfBounds_RemoveAt_Above() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+ l.removeAt(1)
+ }
+ }
+
+ @Suppress("KotlinConstantConditions")
+ @Test
+ fun list_outOfBounds_Set_Below() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+ l[-1] = 1
+ }
+ }
+
+ @Suppress("KotlinConstantConditions")
+ @Test
+ fun sublist_outOfBounds_Set_Below() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+ l[-1] = 1
+ }
+ }
+
+ @Test
+ fun list_outOfBounds_Set_Above() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+ l[4] = 1
+ }
+ }
+
+ @Test
+ fun sublist_outOfBounds_Set_Above() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+ l[1] = 1
+ }
+ }
+
+ @Test
+ fun list_outOfBounds_SubList_Below() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+ l.subList(-1, 1)
+ }
+ }
+
+ @Test
+ fun sublist_outOfBounds_SubList_Below() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+ l.subList(-1, 1)
+ }
+ }
+
+ @Test
+ fun list_outOfBounds_SubList_Above() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+ l.subList(5, 5)
+ }
+ }
+
+ @Test
+ fun sublist_outOfBounds_SubList_Above() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+ l.subList(1, 2)
+ }
+ }
+
+ @Test
+ fun list_outOfBounds_SubList_Order() {
+ assertFailsWith(IllegalArgumentException::class) {
+ val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+ l.subList(3, 2)
+ }
+ }
+
+ @Test
+ fun sublist_outOfBounds_SubList_Order() {
+ assertFailsWith(IllegalArgumentException::class) {
+ val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+ l.subList(1, 0)
+ }
+ }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectLongMapTest.kt
new file mode 100644
index 0000000..3cf8c73
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectLongMapTest.kt
@@ -0,0 +1,630 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class ObjectLongTest {
+ @Test
+ fun objectLongMap() {
+ val map = MutableObjectLongMap<String>()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun emptyObjectLongMap() {
+ val map = emptyObjectLongMap<String>()
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+
+ assertSame(emptyObjectLongMap<String>(), map)
+ }
+
+ @Test
+ fun objectLongMapFunction() {
+ val map = mutableObjectLongMapOf<String>()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun zeroCapacityHashMap() {
+ val map = MutableObjectLongMap<String>(0)
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun objectLongMapWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val map = MutableObjectLongMap<String>(1800)
+ assertEquals(4095, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun objectLongMapPairsFunction() {
+ val map = mutableObjectLongMapOf(
+ "Hello" to 1L,
+ "Bonjour" to 2L
+ )
+ assertEquals(2, map.size)
+ assertEquals(1L, map["Hello"])
+ assertEquals(2L, map["Bonjour"])
+ }
+
+ @Test
+ fun addToMap() {
+ val map = MutableObjectLongMap<String>()
+ map["Hello"] = 1L
+
+ assertEquals(1, map.size)
+ assertEquals(1L, map["Hello"])
+ }
+
+ @Test
+ fun addToSizedMap() {
+ val map = MutableObjectLongMap<String>(12)
+ map["Hello"] = 1L
+
+ assertEquals(1, map.size)
+ assertEquals(1L, map["Hello"])
+ }
+
+ @Test
+ fun addToSmallMap() {
+ val map = MutableObjectLongMap<String>(2)
+ map["Hello"] = 1L
+
+ assertEquals(1, map.size)
+ assertEquals(7, map.capacity)
+ assertEquals(1L, map["Hello"])
+ }
+
+ @Test
+ fun addToZeroCapacityMap() {
+ val map = MutableObjectLongMap<String>(0)
+ map["Hello"] = 1L
+
+ assertEquals(1, map.size)
+ assertEquals(1L, map["Hello"])
+ }
+
+ @Test
+ fun replaceExistingKey() {
+ val map = MutableObjectLongMap<String>()
+ map["Hello"] = 1L
+ map["Hello"] = 2L
+
+ assertEquals(1, map.size)
+ assertEquals(2L, map["Hello"])
+ }
+
+ @Test
+ fun put() {
+ val map = MutableObjectLongMap<String>()
+
+ map.put("Hello", 1L)
+ assertEquals(1L, map["Hello"])
+ map.put("Hello", 2L)
+ assertEquals(2L, map["Hello"])
+ }
+
+ @Test
+ fun putAllArray() {
+ val map = MutableObjectLongMap<String?>()
+ map["Hello"] = 1L
+ map[null] = 2L
+ map["Bonjour"] = 2L
+
+ map.putAll(arrayOf("Hallo" to 3L, "Hola" to 7L))
+
+ assertEquals(5, map.size)
+ assertEquals(3L, map["Hallo"])
+ assertEquals(7L, map["Hola"])
+ }
+
+ @Test
+ fun plus() {
+ val map = MutableObjectLongMap<String>()
+ map += "Hello" to 1L
+
+ assertEquals(1, map.size)
+ assertEquals(1L, map["Hello"])
+ }
+
+ @Test
+ fun plusArray() {
+ val map = MutableObjectLongMap<String>()
+ map += arrayOf("Hallo" to 3L, "Hola" to 7L)
+
+ assertEquals(2, map.size)
+ assertEquals(3L, map["Hallo"])
+ assertEquals(7L, map["Hola"])
+ }
+
+ @Test
+ fun nullKey() {
+ val map = MutableObjectLongMap<String?>()
+ map[null] = 1L
+
+ assertEquals(1, map.size)
+ assertEquals(1L, map[null])
+ }
+
+ @Test
+ fun findNonExistingKey() {
+ val map = MutableObjectLongMap<String>()
+ map["Hello"] = 1L
+
+ assertFailsWith<NoSuchElementException> {
+ map["Bonjour"]
+ }
+ }
+
+ @Test
+ fun getOrDefault() {
+ val map = MutableObjectLongMap<String>()
+ map["Hello"] = 1L
+
+ assertEquals(2L, map.getOrDefault("Bonjour", 2L))
+ }
+
+ @Test
+ fun getOrElse() {
+ val map = MutableObjectLongMap<String>()
+ map["Hello"] = 1L
+
+ assertEquals(3L, map.getOrElse("Hallo") { 3L })
+ }
+
+ @Test
+ fun getOrPut() {
+ val map = MutableObjectLongMap<String>()
+ map["Hello"] = 1L
+
+ var counter = 0
+ map.getOrPut("Hello") {
+ counter++
+ 2L
+ }
+ assertEquals(1L, map["Hello"])
+ assertEquals(0, counter)
+
+ map.getOrPut("Bonjour") {
+ counter++
+ 2L
+ }
+ assertEquals(2L, map["Bonjour"])
+ assertEquals(1, counter)
+
+ map.getOrPut("Bonjour") {
+ counter++
+ 3L
+ }
+ assertEquals(2L, map["Bonjour"])
+ assertEquals(1, counter)
+
+ map.getOrPut("Hallo") {
+ counter++
+ 3L
+ }
+ assertEquals(3L, map["Hallo"])
+ assertEquals(2, counter)
+ }
+
+ @Test
+ fun remove() {
+ val map = MutableObjectLongMap<String?>()
+ map.remove("Hello")
+
+ map["Hello"] = 1L
+ map.remove("Hello")
+ assertEquals(0, map.size)
+
+ map[null] = 1L
+ map.remove(null)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val map = MutableObjectLongMap<String>(6)
+ map["Hello"] = 1L
+ map["Bonjour"] = 2L
+ map["Hallo"] = 3L
+ map["Konnichiwa"] = 4L
+ map["Ciao"] = 5L
+ map["Annyeong"] = 6L
+
+ // Removing all the entries will mark the medata as deleted
+ map.remove("Hello")
+ map.remove("Bonjour")
+ map.remove("Hallo")
+ map.remove("Konnichiwa")
+ map.remove("Ciao")
+ map.remove("Annyeong")
+
+ assertEquals(0, map.size)
+
+ val capacity = map.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ map["Hola"] = 7L
+
+ assertEquals(1, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun removeIf() {
+ val map = MutableObjectLongMap<String>()
+ map["Hello"] = 1L
+ map["Bonjour"] = 2L
+ map["Hallo"] = 3L
+ map["Konnichiwa"] = 4L
+ map["Ciao"] = 5L
+ map["Annyeong"] = 6L
+
+ map.removeIf { key, _ -> key.startsWith('H') }
+
+ assertEquals(4, map.size)
+ assertEquals(2L, map["Bonjour"])
+ assertEquals(4L, map["Konnichiwa"])
+ assertEquals(5L, map["Ciao"])
+ assertEquals(6L, map["Annyeong"])
+ }
+
+ @Test
+ fun minus() {
+ val map = MutableObjectLongMap<String>()
+ map["Hello"] = 1L
+ map["Bonjour"] = 2L
+ map["Hallo"] = 3L
+
+ map -= "Hello"
+
+ assertEquals(2, map.size)
+ assertFalse("Hello" in map)
+ }
+
+ @Test
+ fun minusArray() {
+ val map = MutableObjectLongMap<String>()
+ map["Hello"] = 1L
+ map["Bonjour"] = 2L
+ map["Hallo"] = 3L
+
+ map -= arrayOf("Hallo", "Bonjour")
+
+ assertEquals(1, map.size)
+ assertFalse("Hallo" in map)
+ assertFalse("Bonjour" in map)
+ }
+
+ @Test
+ fun minusIterable() {
+ val map = MutableObjectLongMap<String>()
+ map["Hello"] = 1L
+ map["Bonjour"] = 2L
+ map["Hallo"] = 3L
+
+ map -= listOf("Hallo", "Bonjour")
+
+ assertEquals(1, map.size)
+ assertFalse("Hallo" in map)
+ assertFalse("Bonjour" in map)
+ }
+
+ @Test
+ fun minusSequence() {
+ val map = MutableObjectLongMap<String>()
+ map["Hello"] = 1L
+ map["Bonjour"] = 2L
+ map["Hallo"] = 3L
+
+ map -= listOf("Hallo", "Bonjour").asSequence()
+
+ assertEquals(1, map.size)
+ assertFalse("Hallo" in map)
+ assertFalse("Bonjour" in map)
+ }
+
+ @Test
+ fun conditionalRemove() {
+ val map = MutableObjectLongMap<String?>()
+ assertFalse(map.remove("Hello", 1L))
+
+ map["Hello"] = 1L
+ assertTrue(map.remove("Hello", 1L))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val map = MutableObjectLongMap<String>()
+
+ for (i in 0 until 1700) {
+ map[i.toString()] = i.toLong()
+ }
+
+ assertEquals(1700, map.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val map = MutableObjectLongMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toString()] = j.toLong()
+ }
+
+ var counter = 0
+ map.forEach { key, value ->
+ assertEquals(key, value.toInt().toString())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachKey() {
+ for (i in 0..48) {
+ val map = MutableObjectLongMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toString()] = j.toLong()
+ }
+
+ var counter = 0
+ map.forEachKey { key ->
+ assertNotNull(key.toIntOrNull())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachValue() {
+ for (i in 0..48) {
+ val map = MutableObjectLongMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toString()] = j.toLong()
+ }
+
+ var counter = 0
+ val values = BooleanArray(map.size)
+ map.forEachValue { value ->
+ values[value.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ values.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun clear() {
+ val map = MutableObjectLongMap<String>()
+
+ for (i in 0 until 32) {
+ map[i.toString()] = i.toLong()
+ }
+
+ val capacity = map.capacity
+ map.clear()
+
+ assertEquals(0, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun string() {
+ val map = MutableObjectLongMap<String?>()
+ assertEquals("{}", map.toString())
+
+ map["Hello"] = 1L
+ map["Bonjour"] = 2L
+ val oneString = 1L.toString()
+ val twoString = 2L.toString()
+ assertTrue(
+ "{Hello=$oneString, Bonjour=$twoString}" == map.toString() ||
+ "{Bonjour=$twoString, Hello=$oneString}" == map.toString()
+ )
+
+ map.clear()
+ map[null] = 2L
+ assertEquals("{null=$twoString}", map.toString())
+
+ val selfAsKeyMap = MutableObjectLongMap<Any>()
+ selfAsKeyMap[selfAsKeyMap] = 1L
+ assertEquals("{(this)=$oneString}", selfAsKeyMap.toString())
+ }
+
+ @Test
+ fun equals() {
+ val map = MutableObjectLongMap<String?>()
+ map["Hello"] = 1L
+ map[null] = 2L
+
+ assertFalse(map.equals(null))
+ assertEquals(map, map)
+
+ val map2 = MutableObjectLongMap<String?>()
+ map2[null] = 2L
+
+ assertNotEquals(map, map2)
+
+ map2["Hello"] = 1L
+ assertEquals(map, map2)
+ }
+
+ @Test
+ fun containsKey() {
+ val map = MutableObjectLongMap<String?>()
+ map["Hello"] = 1L
+ map[null] = 2L
+
+ assertTrue(map.containsKey("Hello"))
+ assertTrue(map.containsKey(null))
+ assertFalse(map.containsKey("Bonjour"))
+ }
+
+ @Test
+ fun contains() {
+ val map = MutableObjectLongMap<String?>()
+ map["Hello"] = 1L
+ map[null] = 2L
+
+ assertTrue("Hello" in map)
+ assertTrue(null in map)
+ assertFalse("Bonjour" in map)
+ }
+
+ @Test
+ fun containsValue() {
+ val map = MutableObjectLongMap<String?>()
+ map["Hello"] = 1L
+ map[null] = 2L
+
+ assertTrue(map.containsValue(1L))
+ assertTrue(map.containsValue(2L))
+ assertFalse(map.containsValue(3L))
+ }
+
+ @Test
+ fun empty() {
+ val map = MutableObjectLongMap<String?>()
+ assertTrue(map.isEmpty())
+ assertFalse(map.isNotEmpty())
+ assertTrue(map.none())
+ assertFalse(map.any())
+
+ map["Hello"] = 1L
+
+ assertFalse(map.isEmpty())
+ assertTrue(map.isNotEmpty())
+ assertTrue(map.any())
+ assertFalse(map.none())
+ }
+
+ @Test
+ fun count() {
+ val map = MutableObjectLongMap<String>()
+ assertEquals(0, map.count())
+
+ map["Hello"] = 1L
+ assertEquals(1, map.count())
+
+ map["Bonjour"] = 2L
+ map["Hallo"] = 3L
+ map["Konnichiwa"] = 4L
+ map["Ciao"] = 5L
+ map["Annyeong"] = 6L
+
+ assertEquals(2, map.count { key, _ -> key.startsWith("H") })
+ assertEquals(0, map.count { key, _ -> key.startsWith("W") })
+ }
+
+ @Test
+ fun any() {
+ val map = MutableObjectLongMap<String>()
+ map["Hello"] = 1L
+ map["Bonjour"] = 2L
+ map["Hallo"] = 3L
+ map["Konnichiwa"] = 4L
+ map["Ciao"] = 5L
+ map["Annyeong"] = 6L
+
+ assertTrue(map.any { key, _ -> key.startsWith("K") })
+ assertFalse(map.any { key, _ -> key.startsWith("W") })
+ }
+
+ @Test
+ fun all() {
+ val map = MutableObjectLongMap<String>()
+ map["Hello"] = 1L
+ map["Bonjour"] = 2L
+ map["Hallo"] = 3L
+ map["Konnichiwa"] = 4L
+ map["Ciao"] = 5L
+ map["Annyeong"] = 6L
+
+ assertTrue(map.all { key, value -> key.length >= 4 && value.toInt() >= 1 })
+ assertFalse(map.all { key, _ -> key.startsWith("W") })
+ }
+
+ @Test
+ fun trim() {
+ val map = MutableObjectLongMap<String>()
+ assertEquals(7, map.trim())
+
+ map["Hello"] = 1L
+ map["Hallo"] = 3L
+
+ assertEquals(0, map.trim())
+
+ for (i in 0 until 1700) {
+ map[i.toString()] = i.toLong()
+ }
+
+ assertEquals(2047, map.capacity)
+
+ // After removing these items, our capacity needs should go
+ // from 2047 down to 1023
+ for (i in 0 until 1700) {
+ if (i and 0x1 == 0x0) {
+ val s = i.toString()
+ map.remove(s)
+ }
+ }
+
+ assertEquals(1024, map.trim())
+ assertEquals(0, map.trim())
+ }
+}
diff --git a/collection/collection/template/ObjectPValueMap.kt.template b/collection/collection/template/ObjectPValueMap.kt.template
new file mode 100644
index 0000000..ba8284e
--- /dev/null
+++ b/collection/collection/template/ObjectPValueMap.kt.template
@@ -0,0 +1,855 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyObjectPValueMap = MutableObjectPValueMap<Any?>(0)
+
+/**
+ * Returns an empty, read-only [ObjectPValueMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> emptyObjectPValueMap(): ObjectPValueMap<K> =
+ EmptyObjectPValueMap as ObjectPValueMap<K>
+
+/**
+ * Returns an empty, read-only [ObjectPValueMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> objectPValueMap(): ObjectPValueMap<K> =
+ EmptyObjectPValueMap as ObjectPValueMap<K>
+
+/**
+ * Returns a new [MutableObjectPValueMap].
+ */
+public fun <K> mutableObjectPValueMapOf(): MutableObjectPValueMap<K> = MutableObjectPValueMap()
+
+/**
+ * Returns a new [MutableObjectPValueMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [PValue] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <K> objectPValueMapOf(vararg pairs: Pair<K, PValue>): ObjectPValueMap<K> =
+ MutableObjectPValueMap<K>(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * Returns a new [MutableObjectPValueMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [PValue] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <K> mutableObjectPValueMapOf(vararg pairs: Pair<K, PValue>): MutableObjectPValueMap<K> =
+ MutableObjectPValueMap<K>(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * [ObjectPValueMap] is a container with a [Map]-like interface for keys with
+ * reference types and [PValue] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableObjectPValueMap].
+ *
+ * @see [MutableObjectPValueMap]
+ * @see ScatterMap
+ */
+public sealed class ObjectPValueMap<K> {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` entries, including when
+ // the table is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var keys: Array<Any?> = EMPTY_OBJECTS
+
+ @PublishedApi
+ @JvmField
+ internal var values: PValueArray = EmptyPValueArray
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of key-value pairs that can be stored in this map
+ * without requiring internal storage reallocation.
+ */
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of key-value pairs in this map.
+ */
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this map has at least one entry.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this map has no entries.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this map is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this map is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the value corresponding to the given [key], or `null` if such
+ * a key is not present in the map.
+ * @throws NoSuchElementException when [key] is not found
+ */
+ public operator fun get(key: K): PValue {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ throw NoSuchElementException("There is no key $key in the map")
+ }
+ return values[index]
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * or [defaultValue] if this map contains no mapping for the key.
+ */
+ public fun getOrDefault(key: K, defaultValue: PValue): PValue {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ return defaultValue
+ }
+
+ /**
+ * Returns the value for the given [key] if the value is present
+ * and not null. Otherwise, returns the result of the [defaultValue]
+ * function.
+ */
+ public inline fun getOrElse(key: K, defaultValue: () -> PValue): PValue {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ return defaultValue()
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ public inline fun forEach(block: (key: K, value: PValue) -> Unit) {
+ val k = keys
+ val v = values
+
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ block(k[index] as K, v[index])
+ }
+ }
+
+ /**
+ * Iterates over every key stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachKey(block: (key: K) -> Unit) {
+ val k = keys
+
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ block(k[index] as K)
+ }
+ }
+
+ /**
+ * Iterates over every value stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachValue(block: (value: PValue) -> Unit) {
+ val v = values
+
+ forEachIndexed { index ->
+ block(v[index])
+ }
+ }
+
+ /**
+ * Returns true if all entries match the given [predicate].
+ */
+ public inline fun all(predicate: (K, PValue) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (!predicate(key, value)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one entry matches the given [predicate].
+ */
+ public inline fun any(predicate: (K, PValue) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (predicate(key, value)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of entries in this map.
+ */
+ public fun count(): Int = size
+
+ /**
+ * Returns the number of entries matching the given [predicate].
+ */
+ public inline fun count(predicate: (K, PValue) -> Boolean): Int {
+ var count = 0
+ forEach { key, value ->
+ if (predicate(key, value)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public operator fun contains(key: K): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsKey(key: K): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [value] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsValue(value: PValue): Boolean {
+ forEachValue { v ->
+ if (value == v) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the hash code value for this map. The hash code the sum of the hash
+ * codes of each key/value pair.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { key, value ->
+ hash += key.hashCode() xor value.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this hash map for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [ObjectPValueMap]
+ * - Has the same [size] as this map
+ * - Contains key/value pairs equal to this map's pair
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is ObjectPValueMap<*>) {
+ return false
+ }
+ if (other.size != size) {
+ return false
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ val o = other as ObjectPValueMap<Any?>
+
+ forEach { key, value ->
+ if (value != o[key]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this map. The map is denoted in the
+ * string by the `{}`. Each key/value pair present in the map is represented
+ * inside '{}` by a substring of the form `key=value`, and pairs are
+ * separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ val s = StringBuilder().append('{')
+ var i = 0
+ forEach { key, value ->
+ s.append(if (key === this) "(this)" else key)
+ s.append("=")
+ s.append(value)
+ i++
+ if (i < _size) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append('}').toString()
+ }
+
+ /**
+ * Scans the hash table to find the index in the backing arrays of the
+ * specified [key]. Returns -1 if the key is not present.
+ */
+ @PublishedApi
+ internal fun findKeyIndex(key: K): Int {
+ val hash = hash(key)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutableObjectPValueMap] is a container with a [MutableMap]-like interface for keys with
+ * reference types and [PValue] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableObjectPValueMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableObjectPValueMap<K>(
+ initialCapacity: Int = DefaultScatterCapacity
+) : ObjectPValueMap<K>() {
+ // Number of entries we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ keys = arrayOfNulls(newCapacity)
+ values = PValueArray(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * if the value is present in the map and not `null`. Otherwise,
+ * calls `defaultValue()` and puts the result in the map associated
+ * with [key].
+ */
+ public inline fun getOrPut(key: K, defaultValue: () -> PValue): PValue {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ val value = defaultValue()
+ set(key, value)
+ return value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public operator fun set(key: K, value: PValue) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public fun put(key: K, value: PValue) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [PValue] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+ public fun putAll(@Suppress("ArrayReturn") pairs: Array<out Pair<K, PValue>>) {
+ for ((key, value) in pairs) {
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public fun putAll(from: ObjectPValueMap<K>) {
+ from.forEach { key, value ->
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts the key/value mapping from the [pair] in this map, using the first
+ * element as the key, and the second element as the value.
+ *
+ * Note that the [Pair] is allocated and the [PValue] value is boxed.
+ * Use [set] instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(pair: Pair<K, PValue>) {
+ this[pair.first] = pair.second
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [PValue] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+ public inline operator fun plusAssign(
+ @Suppress("ArrayReturn") pairs: Array<out Pair<K, PValue>>
+ ): Unit = putAll(pairs)
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public inline operator fun plusAssign(from: ObjectPValueMap<K>): Unit = putAll(from)
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public fun remove(key: K) {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ removeValueAt(index)
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map if the
+ * associated value equals [value]. Returns whether the removal happened.
+ */
+ public fun remove(key: K, value: PValue): Boolean {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ if (values[index] == value) {
+ removeValueAt(index)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes any mapping for which the specified [predicate] returns true.
+ */
+ public fun removeIf(predicate: (K, PValue) -> Boolean) {
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ if (predicate(keys[index] as K, values[index])) {
+ removeValueAt(index)
+ }
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public inline operator fun minusAssign(key: K) {
+ remove(key)
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: Array<out K>) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: Iterable<K>) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: Sequence<K>) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: ScatterSet<K>) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ private fun removeValueAt(index: Int) {
+ _size -= 1
+
+ // TODO: We could just mark the entry as empty if there's a group
+ // window around this entry that was already empty
+ writeMetadata(index, Deleted)
+ keys[index] = null
+ }
+
+ /**
+ * Removes all mappings from this map.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ keys.fill(null, 0, _capacity)
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the hash table to find the index at which we can store a value
+ * for the give [key]. If the key already exists in the table, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the table is full.
+ */
+ private fun findAbsoluteInsertIndex(key: K): Int {
+ val hash = hash(key)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the table in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutableObjectPValueMap]'s storage so it is sized appropriately
+ * to hold the current mappings.
+ *
+ * Returns the number of empty entries removed from this map's storage.
+ * Returns be 0 if no trimming is necessary or possible.
+ */
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted entries from the table to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_set`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousKeys = keys
+ val previousValues = values
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newKeys = keys
+ val newValues = values
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousKey = previousKeys[i]
+ val hash = hash(previousKey)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newKeys[index] = previousKey
+ newValues[index] = previousValues[i]
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
diff --git a/collection/collection/template/ObjectPValueMapTest.kt.template b/collection/collection/template/ObjectPValueMapTest.kt.template
new file mode 100644
index 0000000..d2c6f43
--- /dev/null
+++ b/collection/collection/template/ObjectPValueMapTest.kt.template
@@ -0,0 +1,630 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class ObjectPValueTest {
+ @Test
+ fun objectPValueMap() {
+ val map = MutableObjectPValueMap<String>()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun emptyObjectPValueMap() {
+ val map = emptyObjectPValueMap<String>()
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+
+ assertSame(emptyObjectPValueMap<String>(), map)
+ }
+
+ @Test
+ fun objectPValueMapFunction() {
+ val map = mutableObjectPValueMapOf<String>()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun zeroCapacityHashMap() {
+ val map = MutableObjectPValueMap<String>(0)
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun objectPValueMapWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val map = MutableObjectPValueMap<String>(1800)
+ assertEquals(4095, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun objectPValueMapPairsFunction() {
+ val map = mutableObjectPValueMapOf(
+ "Hello" to 1ValueSuffix,
+ "Bonjour" to 2ValueSuffix
+ )
+ assertEquals(2, map.size)
+ assertEquals(1ValueSuffix, map["Hello"])
+ assertEquals(2ValueSuffix, map["Bonjour"])
+ }
+
+ @Test
+ fun addToMap() {
+ val map = MutableObjectPValueMap<String>()
+ map["Hello"] = 1ValueSuffix
+
+ assertEquals(1, map.size)
+ assertEquals(1ValueSuffix, map["Hello"])
+ }
+
+ @Test
+ fun addToSizedMap() {
+ val map = MutableObjectPValueMap<String>(12)
+ map["Hello"] = 1ValueSuffix
+
+ assertEquals(1, map.size)
+ assertEquals(1ValueSuffix, map["Hello"])
+ }
+
+ @Test
+ fun addToSmallMap() {
+ val map = MutableObjectPValueMap<String>(2)
+ map["Hello"] = 1ValueSuffix
+
+ assertEquals(1, map.size)
+ assertEquals(7, map.capacity)
+ assertEquals(1ValueSuffix, map["Hello"])
+ }
+
+ @Test
+ fun addToZeroCapacityMap() {
+ val map = MutableObjectPValueMap<String>(0)
+ map["Hello"] = 1ValueSuffix
+
+ assertEquals(1, map.size)
+ assertEquals(1ValueSuffix, map["Hello"])
+ }
+
+ @Test
+ fun replaceExistingKey() {
+ val map = MutableObjectPValueMap<String>()
+ map["Hello"] = 1ValueSuffix
+ map["Hello"] = 2ValueSuffix
+
+ assertEquals(1, map.size)
+ assertEquals(2ValueSuffix, map["Hello"])
+ }
+
+ @Test
+ fun put() {
+ val map = MutableObjectPValueMap<String>()
+
+ map.put("Hello", 1ValueSuffix)
+ assertEquals(1ValueSuffix, map["Hello"])
+ map.put("Hello", 2ValueSuffix)
+ assertEquals(2ValueSuffix, map["Hello"])
+ }
+
+ @Test
+ fun putAllArray() {
+ val map = MutableObjectPValueMap<String?>()
+ map["Hello"] = 1ValueSuffix
+ map[null] = 2ValueSuffix
+ map["Bonjour"] = 2ValueSuffix
+
+ map.putAll(arrayOf("Hallo" to 3ValueSuffix, "Hola" to 7ValueSuffix))
+
+ assertEquals(5, map.size)
+ assertEquals(3ValueSuffix, map["Hallo"])
+ assertEquals(7ValueSuffix, map["Hola"])
+ }
+
+ @Test
+ fun plus() {
+ val map = MutableObjectPValueMap<String>()
+ map += "Hello" to 1ValueSuffix
+
+ assertEquals(1, map.size)
+ assertEquals(1ValueSuffix, map["Hello"])
+ }
+
+ @Test
+ fun plusArray() {
+ val map = MutableObjectPValueMap<String>()
+ map += arrayOf("Hallo" to 3ValueSuffix, "Hola" to 7ValueSuffix)
+
+ assertEquals(2, map.size)
+ assertEquals(3ValueSuffix, map["Hallo"])
+ assertEquals(7ValueSuffix, map["Hola"])
+ }
+
+ @Test
+ fun nullKey() {
+ val map = MutableObjectPValueMap<String?>()
+ map[null] = 1ValueSuffix
+
+ assertEquals(1, map.size)
+ assertEquals(1ValueSuffix, map[null])
+ }
+
+ @Test
+ fun findNonExistingKey() {
+ val map = MutableObjectPValueMap<String>()
+ map["Hello"] = 1ValueSuffix
+
+ assertFailsWith<NoSuchElementException> {
+ map["Bonjour"]
+ }
+ }
+
+ @Test
+ fun getOrDefault() {
+ val map = MutableObjectPValueMap<String>()
+ map["Hello"] = 1ValueSuffix
+
+ assertEquals(2ValueSuffix, map.getOrDefault("Bonjour", 2ValueSuffix))
+ }
+
+ @Test
+ fun getOrElse() {
+ val map = MutableObjectPValueMap<String>()
+ map["Hello"] = 1ValueSuffix
+
+ assertEquals(3ValueSuffix, map.getOrElse("Hallo") { 3ValueSuffix })
+ }
+
+ @Test
+ fun getOrPut() {
+ val map = MutableObjectPValueMap<String>()
+ map["Hello"] = 1ValueSuffix
+
+ var counter = 0
+ map.getOrPut("Hello") {
+ counter++
+ 2ValueSuffix
+ }
+ assertEquals(1ValueSuffix, map["Hello"])
+ assertEquals(0, counter)
+
+ map.getOrPut("Bonjour") {
+ counter++
+ 2ValueSuffix
+ }
+ assertEquals(2ValueSuffix, map["Bonjour"])
+ assertEquals(1, counter)
+
+ map.getOrPut("Bonjour") {
+ counter++
+ 3ValueSuffix
+ }
+ assertEquals(2ValueSuffix, map["Bonjour"])
+ assertEquals(1, counter)
+
+ map.getOrPut("Hallo") {
+ counter++
+ 3ValueSuffix
+ }
+ assertEquals(3ValueSuffix, map["Hallo"])
+ assertEquals(2, counter)
+ }
+
+ @Test
+ fun remove() {
+ val map = MutableObjectPValueMap<String?>()
+ map.remove("Hello")
+
+ map["Hello"] = 1ValueSuffix
+ map.remove("Hello")
+ assertEquals(0, map.size)
+
+ map[null] = 1ValueSuffix
+ map.remove(null)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val map = MutableObjectPValueMap<String>(6)
+ map["Hello"] = 1ValueSuffix
+ map["Bonjour"] = 2ValueSuffix
+ map["Hallo"] = 3ValueSuffix
+ map["Konnichiwa"] = 4ValueSuffix
+ map["Ciao"] = 5ValueSuffix
+ map["Annyeong"] = 6ValueSuffix
+
+ // Removing all the entries will mark the medata as deleted
+ map.remove("Hello")
+ map.remove("Bonjour")
+ map.remove("Hallo")
+ map.remove("Konnichiwa")
+ map.remove("Ciao")
+ map.remove("Annyeong")
+
+ assertEquals(0, map.size)
+
+ val capacity = map.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ map["Hola"] = 7ValueSuffix
+
+ assertEquals(1, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun removeIf() {
+ val map = MutableObjectPValueMap<String>()
+ map["Hello"] = 1ValueSuffix
+ map["Bonjour"] = 2ValueSuffix
+ map["Hallo"] = 3ValueSuffix
+ map["Konnichiwa"] = 4ValueSuffix
+ map["Ciao"] = 5ValueSuffix
+ map["Annyeong"] = 6ValueSuffix
+
+ map.removeIf { key, _ -> key.startsWith('H') }
+
+ assertEquals(4, map.size)
+ assertEquals(2ValueSuffix, map["Bonjour"])
+ assertEquals(4ValueSuffix, map["Konnichiwa"])
+ assertEquals(5ValueSuffix, map["Ciao"])
+ assertEquals(6ValueSuffix, map["Annyeong"])
+ }
+
+ @Test
+ fun minus() {
+ val map = MutableObjectPValueMap<String>()
+ map["Hello"] = 1ValueSuffix
+ map["Bonjour"] = 2ValueSuffix
+ map["Hallo"] = 3ValueSuffix
+
+ map -= "Hello"
+
+ assertEquals(2, map.size)
+ assertFalse("Hello" in map)
+ }
+
+ @Test
+ fun minusArray() {
+ val map = MutableObjectPValueMap<String>()
+ map["Hello"] = 1ValueSuffix
+ map["Bonjour"] = 2ValueSuffix
+ map["Hallo"] = 3ValueSuffix
+
+ map -= arrayOf("Hallo", "Bonjour")
+
+ assertEquals(1, map.size)
+ assertFalse("Hallo" in map)
+ assertFalse("Bonjour" in map)
+ }
+
+ @Test
+ fun minusIterable() {
+ val map = MutableObjectPValueMap<String>()
+ map["Hello"] = 1ValueSuffix
+ map["Bonjour"] = 2ValueSuffix
+ map["Hallo"] = 3ValueSuffix
+
+ map -= listOf("Hallo", "Bonjour")
+
+ assertEquals(1, map.size)
+ assertFalse("Hallo" in map)
+ assertFalse("Bonjour" in map)
+ }
+
+ @Test
+ fun minusSequence() {
+ val map = MutableObjectPValueMap<String>()
+ map["Hello"] = 1ValueSuffix
+ map["Bonjour"] = 2ValueSuffix
+ map["Hallo"] = 3ValueSuffix
+
+ map -= listOf("Hallo", "Bonjour").asSequence()
+
+ assertEquals(1, map.size)
+ assertFalse("Hallo" in map)
+ assertFalse("Bonjour" in map)
+ }
+
+ @Test
+ fun conditionalRemove() {
+ val map = MutableObjectPValueMap<String?>()
+ assertFalse(map.remove("Hello", 1ValueSuffix))
+
+ map["Hello"] = 1ValueSuffix
+ assertTrue(map.remove("Hello", 1ValueSuffix))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val map = MutableObjectPValueMap<String>()
+
+ for (i in 0 until 1700) {
+ map[i.toString()] = i.toPValue()
+ }
+
+ assertEquals(1700, map.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val map = MutableObjectPValueMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toString()] = j.toPValue()
+ }
+
+ var counter = 0
+ map.forEach { key, value ->
+ assertEquals(key, value.toInt().toString())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachKey() {
+ for (i in 0..48) {
+ val map = MutableObjectPValueMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toString()] = j.toPValue()
+ }
+
+ var counter = 0
+ map.forEachKey { key ->
+ assertNotNull(key.toIntOrNull())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachValue() {
+ for (i in 0..48) {
+ val map = MutableObjectPValueMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toString()] = j.toPValue()
+ }
+
+ var counter = 0
+ val values = BooleanArray(map.size)
+ map.forEachValue { value ->
+ values[value.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ values.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun clear() {
+ val map = MutableObjectPValueMap<String>()
+
+ for (i in 0 until 32) {
+ map[i.toString()] = i.toPValue()
+ }
+
+ val capacity = map.capacity
+ map.clear()
+
+ assertEquals(0, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun string() {
+ val map = MutableObjectPValueMap<String?>()
+ assertEquals("{}", map.toString())
+
+ map["Hello"] = 1ValueSuffix
+ map["Bonjour"] = 2ValueSuffix
+ val oneString = 1ValueSuffix.toString()
+ val twoString = 2ValueSuffix.toString()
+ assertTrue(
+ "{Hello=$oneString, Bonjour=$twoString}" == map.toString() ||
+ "{Bonjour=$twoString, Hello=$oneString}" == map.toString()
+ )
+
+ map.clear()
+ map[null] = 2ValueSuffix
+ assertEquals("{null=$twoString}", map.toString())
+
+ val selfAsKeyMap = MutableObjectPValueMap<Any>()
+ selfAsKeyMap[selfAsKeyMap] = 1ValueSuffix
+ assertEquals("{(this)=$oneString}", selfAsKeyMap.toString())
+ }
+
+ @Test
+ fun equals() {
+ val map = MutableObjectPValueMap<String?>()
+ map["Hello"] = 1ValueSuffix
+ map[null] = 2ValueSuffix
+
+ assertFalse(map.equals(null))
+ assertEquals(map, map)
+
+ val map2 = MutableObjectPValueMap<String?>()
+ map2[null] = 2ValueSuffix
+
+ assertNotEquals(map, map2)
+
+ map2["Hello"] = 1ValueSuffix
+ assertEquals(map, map2)
+ }
+
+ @Test
+ fun containsKey() {
+ val map = MutableObjectPValueMap<String?>()
+ map["Hello"] = 1ValueSuffix
+ map[null] = 2ValueSuffix
+
+ assertTrue(map.containsKey("Hello"))
+ assertTrue(map.containsKey(null))
+ assertFalse(map.containsKey("Bonjour"))
+ }
+
+ @Test
+ fun contains() {
+ val map = MutableObjectPValueMap<String?>()
+ map["Hello"] = 1ValueSuffix
+ map[null] = 2ValueSuffix
+
+ assertTrue("Hello" in map)
+ assertTrue(null in map)
+ assertFalse("Bonjour" in map)
+ }
+
+ @Test
+ fun containsValue() {
+ val map = MutableObjectPValueMap<String?>()
+ map["Hello"] = 1ValueSuffix
+ map[null] = 2ValueSuffix
+
+ assertTrue(map.containsValue(1ValueSuffix))
+ assertTrue(map.containsValue(2ValueSuffix))
+ assertFalse(map.containsValue(3ValueSuffix))
+ }
+
+ @Test
+ fun empty() {
+ val map = MutableObjectPValueMap<String?>()
+ assertTrue(map.isEmpty())
+ assertFalse(map.isNotEmpty())
+ assertTrue(map.none())
+ assertFalse(map.any())
+
+ map["Hello"] = 1ValueSuffix
+
+ assertFalse(map.isEmpty())
+ assertTrue(map.isNotEmpty())
+ assertTrue(map.any())
+ assertFalse(map.none())
+ }
+
+ @Test
+ fun count() {
+ val map = MutableObjectPValueMap<String>()
+ assertEquals(0, map.count())
+
+ map["Hello"] = 1ValueSuffix
+ assertEquals(1, map.count())
+
+ map["Bonjour"] = 2ValueSuffix
+ map["Hallo"] = 3ValueSuffix
+ map["Konnichiwa"] = 4ValueSuffix
+ map["Ciao"] = 5ValueSuffix
+ map["Annyeong"] = 6ValueSuffix
+
+ assertEquals(2, map.count { key, _ -> key.startsWith("H") })
+ assertEquals(0, map.count { key, _ -> key.startsWith("W") })
+ }
+
+ @Test
+ fun any() {
+ val map = MutableObjectPValueMap<String>()
+ map["Hello"] = 1ValueSuffix
+ map["Bonjour"] = 2ValueSuffix
+ map["Hallo"] = 3ValueSuffix
+ map["Konnichiwa"] = 4ValueSuffix
+ map["Ciao"] = 5ValueSuffix
+ map["Annyeong"] = 6ValueSuffix
+
+ assertTrue(map.any { key, _ -> key.startsWith("K") })
+ assertFalse(map.any { key, _ -> key.startsWith("W") })
+ }
+
+ @Test
+ fun all() {
+ val map = MutableObjectPValueMap<String>()
+ map["Hello"] = 1ValueSuffix
+ map["Bonjour"] = 2ValueSuffix
+ map["Hallo"] = 3ValueSuffix
+ map["Konnichiwa"] = 4ValueSuffix
+ map["Ciao"] = 5ValueSuffix
+ map["Annyeong"] = 6ValueSuffix
+
+ assertTrue(map.all { key, value -> key.length >= 4 && value.toInt() >= 1 })
+ assertFalse(map.all { key, _ -> key.startsWith("W") })
+ }
+
+ @Test
+ fun trim() {
+ val map = MutableObjectPValueMap<String>()
+ assertEquals(7, map.trim())
+
+ map["Hello"] = 1ValueSuffix
+ map["Hallo"] = 3ValueSuffix
+
+ assertEquals(0, map.trim())
+
+ for (i in 0 until 1700) {
+ map[i.toString()] = i.toPValue()
+ }
+
+ assertEquals(2047, map.capacity)
+
+ // After removing these items, our capacity needs should go
+ // from 2047 down to 1023
+ for (i in 0 until 1700) {
+ if (i and 0x1 == 0x0) {
+ val s = i.toString()
+ map.remove(s)
+ }
+ }
+
+ assertEquals(1024, map.trim())
+ assertEquals(0, map.trim())
+ }
+}
diff --git a/collection/collection/template/PKeyList.kt.template b/collection/collection/template/PKeyList.kt.template
new file mode 100644
index 0000000..620d2e4
--- /dev/null
+++ b/collection/collection/template/PKeyList.kt.template
@@ -0,0 +1,922 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:Suppress("NOTHING_TO_INLINE", "RedundantVisibilityModifier")
+@file:OptIn(ExperimentalContracts::class)
+
+package androidx.collection
+
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+/**
+ * [PKeyList] is a [List]-like collection for [PKey] values. It allows retrieving
+ * the elements without boxing. [PKeyList] is always backed by a [MutablePKeyList],
+ * its [MutableList]-like subclass.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the list (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. It is also not safe to mutate during reentrancy --
+ * in the middle of a [forEach], for example. However, concurrent reads are safe.
+ */
+public sealed class PKeyList(initialCapacity: Int) {
+ @JvmField
+ @PublishedApi
+ internal var content: PKeyArray = if (initialCapacity == 0) {
+ EmptyPKeyArray
+ } else {
+ PKeyArray(initialCapacity)
+ }
+
+ @Suppress("PropertyName")
+ @JvmField
+ @PublishedApi
+ internal var _size: Int = 0
+
+ /**
+ * The number of elements in the [PKeyList].
+ */
+ @get:androidx.annotation.IntRange(from = 0)
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns the last valid index in the [PKeyList]. This can be `-1` when the list is empty.
+ */
+ @get:androidx.annotation.IntRange(from = -1)
+ public inline val lastIndex: Int get() = _size - 1
+
+ /**
+ * Returns an [IntRange] of the valid indices for this [PKeyList].
+ */
+ public inline val indices: IntRange get() = 0 until _size
+
+ /**
+ * Returns `true` if the collection has no elements in it.
+ */
+ public fun none(): Boolean {
+ return isEmpty()
+ }
+
+ /**
+ * Returns `true` if there's at least one element in the collection.
+ */
+ public fun any(): Boolean {
+ return isNotEmpty()
+ }
+
+ /**
+ * Returns `true` if any of the elements give a `true` return value for [predicate].
+ */
+ public inline fun any(predicate: (element: PKey) -> Boolean): Boolean {
+ contract { callsInPlace(predicate) }
+ forEach {
+ if (predicate(it)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Returns `true` if any of the elements give a `true` return value for [predicate] while
+ * iterating in the reverse order.
+ */
+ public inline fun reversedAny(predicate: (element: PKey) -> Boolean): Boolean {
+ contract { callsInPlace(predicate) }
+ forEachReversed {
+ if (predicate(it)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Returns `true` if the [PKeyList] contains [element] or `false` otherwise.
+ */
+ public operator fun contains(element: PKey): Boolean {
+ forEach {
+ if (it == element) {
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Returns `true` if the [PKeyList] contains all elements in [elements] or `false` if
+ * one or more are missing.
+ */
+ public fun containsAll(elements: PKeyList): Boolean {
+ for (i in elements.indices) {
+ if (!contains(elements[i])) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns the number of elements in this list.
+ */
+ public fun count(): Int = _size
+
+ /**
+ * Counts the number of elements matching [predicate].
+ * @return The number of elements in this list for which [predicate] returns true.
+ */
+ public inline fun count(predicate: (element: PKey) -> Boolean): Int {
+ contract { callsInPlace(predicate) }
+ var count = 0
+ forEach { if (predicate(it)) count++ }
+ return count
+ }
+
+ /**
+ * Returns the first element in the [PKeyList] or throws a [NoSuchElementException] if
+ * it [isEmpty].
+ */
+ public fun first(): PKey {
+ if (isEmpty()) {
+ throw NoSuchElementException("PKeyList is empty.")
+ }
+ return content[0]
+ }
+
+ /**
+ * Returns the first element in the [PKeyList] for which [predicate] returns `true` or
+ * throws [NoSuchElementException] if nothing matches.
+ * @see indexOfFirst
+ */
+ public inline fun first(predicate: (element: PKey) -> Boolean): PKey {
+ contract { callsInPlace(predicate) }
+ forEach { item ->
+ if (predicate(item)) return item
+ }
+ throw NoSuchElementException("PKeyList contains no element matching the predicate.")
+ }
+
+ /**
+ * Accumulates values, starting with [initial], and applying [operation] to each element
+ * in the [PKeyList] in order.
+ * @param initial The value of `acc` for the first call to [operation] or return value if
+ * there are no elements in this list.
+ * @param operation function that takes current accumulator value and an element, and
+ * calculates the next accumulator value.
+ */
+ public inline fun <R> fold(initial: R, operation: (acc: R, element: PKey) -> R): R {
+ contract { callsInPlace(operation) }
+ var acc = initial
+ forEach { item ->
+ acc = operation(acc, item)
+ }
+ return acc
+ }
+
+ /**
+ * Accumulates values, starting with [initial], and applying [operation] to each element
+ * in the [PKeyList] in order.
+ */
+ public inline fun <R> foldIndexed(
+ initial: R,
+ operation: (index: Int, acc: R, element: PKey) -> R
+ ): R {
+ contract { callsInPlace(operation) }
+ var acc = initial
+ forEachIndexed { i, item ->
+ acc = operation(i, acc, item)
+ }
+ return acc
+ }
+
+ /**
+ * Accumulates values, starting with [initial], and applying [operation] to each element
+ * in the [PKeyList] in reverse order.
+ * @param initial The value of `acc` for the first call to [operation] or return value if
+ * there are no elements in this list.
+ * @param operation function that takes an element and the current accumulator value, and
+ * calculates the next accumulator value.
+ */
+ public inline fun <R> foldRight(initial: R, operation: (element: PKey, acc: R) -> R): R {
+ contract { callsInPlace(operation) }
+ var acc = initial
+ forEachReversed { item ->
+ acc = operation(item, acc)
+ }
+ return acc
+ }
+
+ /**
+ * Accumulates values, starting with [initial], and applying [operation] to each element
+ * in the [PKeyList] in reverse order.
+ */
+ public inline fun <R> foldRightIndexed(
+ initial: R,
+ operation: (index: Int, element: PKey, acc: R) -> R
+ ): R {
+ contract { callsInPlace(operation) }
+ var acc = initial
+ forEachReversedIndexed { i, item ->
+ acc = operation(i, item, acc)
+ }
+ return acc
+ }
+
+ /**
+ * Calls [block] for each element in the [PKeyList], in order.
+ * @param block will be executed for every element in the list, accepting an element from
+ * the list
+ */
+ public inline fun forEach(block: (element: PKey) -> Unit) {
+ contract { callsInPlace(block) }
+ val content = content
+ for (i in 0 until _size) {
+ block(content[i])
+ }
+ }
+
+ /**
+ * Calls [block] for each element in the [PKeyList] along with its index, in order.
+ * @param block will be executed for every element in the list, accepting the index and
+ * the element at that index.
+ */
+ public inline fun forEachIndexed(block: (index: Int, element: PKey) -> Unit) {
+ contract { callsInPlace(block) }
+ val content = content
+ for (i in 0 until _size) {
+ block(i, content[i])
+ }
+ }
+
+ /**
+ * Calls [block] for each element in the [PKeyList] in reverse order.
+ * @param block will be executed for every element in the list, accepting an element from
+ * the list
+ */
+ public inline fun forEachReversed(block: (element: PKey) -> Unit) {
+ contract { callsInPlace(block) }
+ val content = content
+ for (i in _size - 1 downTo 0) {
+ block(content[i])
+ }
+ }
+
+ /**
+ * Calls [block] for each element in the [PKeyList] along with its index, in reverse
+ * order.
+ * @param block will be executed for every element in the list, accepting the index and
+ * the element at that index.
+ */
+ public inline fun forEachReversedIndexed(block: (index: Int, element: PKey) -> Unit) {
+ contract { callsInPlace(block) }
+ val content = content
+ for (i in _size - 1 downTo 0) {
+ block(i, content[i])
+ }
+ }
+
+ /**
+ * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+ * the [index] is out of bounds of this collection.
+ */
+ public operator fun get(@androidx.annotation.IntRange(from = 0) index: Int): PKey {
+ if (index !in 0 until _size) {
+ throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ }
+ return content[index]
+ }
+
+ /**
+ * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+ * the [index] is out of bounds of this collection.
+ */
+ public fun elementAt(@androidx.annotation.IntRange(from = 0) index: Int): PKey {
+ if (index !in 0 until _size) {
+ throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ }
+ return content[index]
+ }
+
+ /**
+ * Returns the element at the given [index] or [defaultValue] if [index] is out of bounds
+ * of the collection.
+ * @param index The index of the element whose value should be returned
+ * @param defaultValue A lambda to call with [index] as a parameter to return a value at
+ * an index not in the list.
+ */
+ public inline fun elementAtOrElse(
+ @androidx.annotation.IntRange(from = 0) index: Int,
+ defaultValue: (index: Int) -> PKey
+ ): PKey {
+ if (index !in 0 until _size) {
+ return defaultValue(index)
+ }
+ return content[index]
+ }
+
+ /**
+ * Returns the index of [element] in the [PKeyList] or `-1` if [element] is not there.
+ */
+ public fun indexOf(element: PKey): Int {
+ forEachIndexed { i, item ->
+ if (element == item) {
+ return i
+ }
+ }
+ return -1
+ }
+
+ /**
+ * Returns the index if the first element in the [PKeyList] for which [predicate]
+ * returns `true`.
+ */
+ public inline fun indexOfFirst(predicate: (element: PKey) -> Boolean): Int {
+ contract { callsInPlace(predicate) }
+ forEachIndexed { i, item ->
+ if (predicate(item)) {
+ return i
+ }
+ }
+ return -1
+ }
+
+ /**
+ * Returns the index if the last element in the [PKeyList] for which [predicate]
+ * returns `true`.
+ */
+ public inline fun indexOfLast(predicate: (element: PKey) -> Boolean): Int {
+ contract { callsInPlace(predicate) }
+ forEachReversedIndexed { i, item ->
+ if (predicate(item)) {
+ return i
+ }
+ }
+ return -1
+ }
+
+ /**
+ * Returns `true` if the [PKeyList] has no elements in it or `false` otherwise.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if there are elements in the [PKeyList] or `false` if it is empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the last element in the [PKeyList] or throws a [NoSuchElementException] if
+ * it [isEmpty].
+ */
+ public fun last(): PKey {
+ if (isEmpty()) {
+ throw NoSuchElementException("PKeyList is empty.")
+ }
+ return content[lastIndex]
+ }
+
+ /**
+ * Returns the last element in the [PKeyList] for which [predicate] returns `true` or
+ * throws [NoSuchElementException] if nothing matches.
+ * @see indexOfLast
+ */
+ public inline fun last(predicate: (element: PKey) -> Boolean): PKey {
+ contract { callsInPlace(predicate) }
+ forEachReversed { item ->
+ if (predicate(item)) {
+ return item
+ }
+ }
+ throw NoSuchElementException("PKeyList contains no element matching the predicate.")
+ }
+
+ /**
+ * Returns the index of the last element in the [PKeyList] that is the same as
+ * [element] or `-1` if no elements match.
+ */
+ public fun lastIndexOf(element: PKey): Int {
+ forEachReversedIndexed { i, item ->
+ if (item == element) {
+ return i
+ }
+ }
+ return -1
+ }
+
+ /**
+ * Returns a hash code based on the contents of the [PKeyList].
+ */
+ override fun hashCode(): Int {
+ var hashCode = 0
+ forEach { element ->
+ hashCode += 31 * element.hashCode()
+ }
+ return hashCode
+ }
+
+ /**
+ * Returns `true` if [other] is a [PKeyList] and the contents of this and [other] are the
+ * same.
+ */
+ override fun equals(other: Any?): Boolean {
+ if (other !is PKeyList || other._size != _size) {
+ return false
+ }
+ val content = content
+ val otherContent = other.content
+ for (i in indices) {
+ if (content[i] != otherContent[i]) {
+ return false
+ }
+ }
+ return true
+ }
+
+ /**
+ * Returns a String representation of the list, surrounded by "[]" and each element
+ * separated by ", ".
+ */
+ override fun toString(): String {
+ if (isEmpty()) {
+ return "[]"
+ }
+ val last = lastIndex
+ return buildString {
+ append('[')
+ val content = content
+ for (i in 0 until last) {
+ append(content[i])
+ append(',')
+ append(' ')
+ }
+ append(content[last])
+ append(']')
+ }
+ }
+}
+
+/**
+ * [MutablePKeyList] is a [MutableList]-like collection for [PKey] values.
+ * It allows storing and retrieving the elements without boxing. Immutable
+ * access is available through its base class [PKeyList], which has a [List]-like
+ * interface.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the list (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. It is also not safe to mutate during reentrancy --
+ * in the middle of a [forEach], for example. However, concurrent reads are safe.
+ *
+ * @constructor Creates a [MutablePKeyList] with a [capacity] of `initialCapacity`.
+ */
+public class MutablePKeyList(
+ initialCapacity: Int = 16
+) : PKeyList(initialCapacity) {
+ /**
+ * Returns the total number of elements that can be held before the [MutablePKeyList] must
+ * grow.
+ *
+ * @see ensureCapacity
+ */
+ public inline val capacity: Int
+ get() = content.size
+
+ /**
+ * Adds [element] to the [MutablePKeyList] and returns `true`.
+ */
+ public fun add(element: PKey): Boolean {
+ ensureCapacity(_size + 1)
+ content[_size] = element
+ _size++
+ return true
+ }
+
+ /**
+ * Adds [element] to the [MutablePKeyList] at the given [index], shifting over any
+ * elements at [index] and after, if any.
+ * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+ */
+ public fun add(@androidx.annotation.IntRange(from = 0) index: Int, element: PKey) {
+ if (index !in 0.._size) {
+ throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+ }
+ ensureCapacity(_size + 1)
+ val content = content
+ if (index != _size) {
+ content.copyInto(
+ destination = content,
+ destinationOffset = index + 1,
+ startIndex = index,
+ endIndex = _size
+ )
+ }
+ content[index] = element
+ _size++
+ }
+
+ /**
+ * Adds all [elements] to the [MutablePKeyList] at the given [index], shifting over any
+ * elements at [index] and after, if any.
+ * @return `true` if the [MutablePKeyList] was changed or `false` if [elements] was empty
+ * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive.
+ */
+ public fun addAll(
+ @androidx.annotation.IntRange(from = 0) index: Int,
+ elements: PKeyArray
+ ): Boolean {
+ if (index !in 0.._size) {
+ throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+ }
+ if (elements.isEmpty()) return false
+ ensureCapacity(_size + elements.size)
+ val content = content
+ if (index != _size) {
+ content.copyInto(
+ destination = content,
+ destinationOffset = index + elements.size,
+ startIndex = index,
+ endIndex = _size
+ )
+ }
+ elements.copyInto(content, index)
+ _size += elements.size
+ return true
+ }
+
+ /**
+ * Adds all [elements] to the [MutablePKeyList] at the given [index], shifting over any
+ * elements at [index] and after, if any.
+ * @return `true` if the [MutablePKeyList] was changed or `false` if [elements] was empty
+ * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+ */
+ public fun addAll(
+ @androidx.annotation.IntRange(from = 0) index: Int,
+ elements: PKeyList
+ ): Boolean {
+ if (index !in 0.._size) {
+ throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+ }
+ if (elements.isEmpty()) return false
+ ensureCapacity(_size + elements._size)
+ val content = content
+ if (index != _size) {
+ content.copyInto(
+ destination = content,
+ destinationOffset = index + elements._size,
+ startIndex = index,
+ endIndex = _size
+ )
+ }
+ elements.content.copyInto(
+ destination = content,
+ destinationOffset = index,
+ startIndex = 0,
+ endIndex = elements._size
+ )
+ _size += elements._size
+ return true
+ }
+
+ /**
+ * Adds all [elements] to the end of the [MutablePKeyList] and returns `true` if the
+ * [MutablePKeyList] was changed or `false` if [elements] was empty.
+ */
+ public fun addAll(elements: PKeyList): Boolean {
+ return addAll(_size, elements)
+ }
+
+ /**
+ * Adds all [elements] to the end of the [MutablePKeyList] and returns `true` if the
+ * [MutablePKeyList] was changed or `false` if [elements] was empty.
+ */
+ public fun addAll(elements: PKeyArray): Boolean {
+ return addAll(_size, elements)
+ }
+
+ /**
+ * Adds all [elements] to the end of the [MutablePKeyList].
+ */
+ public operator fun plusAssign(elements: PKeyList) {
+ addAll(_size, elements)
+ }
+
+ /**
+ * Adds all [elements] to the end of the [MutablePKeyList].
+ */
+ public operator fun plusAssign(elements: PKeyArray) {
+ addAll(_size, elements)
+ }
+
+ /**
+ * Removes all elements in the [MutablePKeyList]. The storage isn't released.
+ * @see trim
+ */
+ public fun clear() {
+ _size = 0
+ }
+
+ /**
+ * Reduces the internal storage. If [capacity] is greater than [minCapacity] and [size], the
+ * internal storage is reduced to the maximum of [size] and [minCapacity].
+ * @see ensureCapacity
+ */
+ public fun trim(minCapacity: Int = _size) {
+ val minSize = maxOf(minCapacity, _size)
+ if (capacity > minSize) {
+ content = content.copyOf(minSize)
+ }
+ }
+
+ /**
+ * Ensures that there is enough space to store [capacity] elements in the [MutablePKeyList].
+ * @see trim
+ */
+ public fun ensureCapacity(capacity: Int) {
+ val oldContent = content
+ if (oldContent.size < capacity) {
+ val newSize = maxOf(capacity, oldContent.size * 3 / 2)
+ content = oldContent.copyOf(newSize)
+ }
+ }
+
+ /**
+ * [add] [element] to the [MutablePKeyList].
+ */
+ public inline operator fun plusAssign(element: PKey) {
+ add(element)
+ }
+
+ /**
+ * [remove] [element] from the [MutablePKeyList]
+ */
+ public inline operator fun minusAssign(element: PKey) {
+ remove(element)
+ }
+
+ /**
+ * Removes [element] from the [MutablePKeyList]. If [element] was in the [MutablePKeyList]
+ * and was removed, `true` will be returned, or `false` will be returned if the element
+ * was not found.
+ */
+ public fun remove(element: PKey): Boolean {
+ val index = indexOf(element)
+ if (index >= 0) {
+ removeAt(index)
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Removes all [elements] from the [MutablePKeyList] and returns `true` if anything was removed.
+ */
+ public fun removeAll(elements: PKeyArray): Boolean {
+ val initialSize = _size
+ for (i in elements.indices) {
+ remove(elements[i])
+ }
+ return initialSize != _size
+ }
+
+ /**
+ * Removes all [elements] from the [MutablePKeyList] and returns `true` if anything was removed.
+ */
+ public fun removeAll(elements: PKeyList): Boolean {
+ val initialSize = _size
+ for (i in 0..elements.lastIndex) {
+ remove(elements[i])
+ }
+ return initialSize != _size
+ }
+
+ /**
+ * Removes all [elements] from the [MutablePKeyList].
+ */
+ public operator fun minusAssign(elements: PKeyArray) {
+ elements.forEach { element ->
+ remove(element)
+ }
+ }
+
+ /**
+ * Removes all [elements] from the [MutablePKeyList].
+ */
+ public operator fun minusAssign(elements: PKeyList) {
+ elements.forEach { element ->
+ remove(element)
+ }
+ }
+
+ /**
+ * Removes the element at the given [index] and returns it.
+ * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
+ */
+ public fun removeAt(@androidx.annotation.IntRange(from = 0) index: Int): PKey {
+ if (index !in 0 until _size) {
+ throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ }
+ val content = content
+ val item = content[index]
+ if (index != lastIndex) {
+ content.copyInto(
+ destination = content,
+ destinationOffset = index,
+ startIndex = index + 1,
+ endIndex = _size
+ )
+ }
+ _size--
+ return item
+ }
+
+ /**
+ * Removes items from index [start] (inclusive) to [end] (exclusive).
+ * @throws IndexOutOfBoundsException if [start] or [end] isn't between 0 and [size], inclusive
+ * @throws IllegalArgumentException if [start] is greater than [end]
+ */
+ public fun removeRange(
+ @androidx.annotation.IntRange(from = 0) start: Int,
+ @androidx.annotation.IntRange(from = 0) end: Int
+ ) {
+ if (start !in 0.._size || end !in 0.._size) {
+ throw IndexOutOfBoundsException("Start ($start) and end ($end) must be in 0..$_size")
+ }
+ if (end < start) {
+ throw IllegalArgumentException("Start ($start) is more than end ($end)")
+ }
+ if (end != start) {
+ if (end < _size) {
+ content.copyInto(
+ destination = content,
+ destinationOffset = start,
+ startIndex = end,
+ endIndex = _size
+ )
+ }
+ _size -= (end - start)
+ }
+ }
+
+ /**
+ * Keeps only [elements] in the [MutablePKeyList] and removes all other values.
+ * @return `true` if the [MutablePKeyList] has changed.
+ */
+ public fun retainAll(elements: PKeyArray): Boolean {
+ val initialSize = _size
+ val content = content
+ for (i in lastIndex downTo 0) {
+ val item = content[i]
+ if (elements.indexOfFirst { it == item } < 0) {
+ removeAt(i)
+ }
+ }
+ return initialSize != _size
+ }
+
+ /**
+ * Keeps only [elements] in the [MutablePKeyList] and removes all other values.
+ * @return `true` if the [MutablePKeyList] has changed.
+ */
+ public fun retainAll(elements: PKeyList): Boolean {
+ val initialSize = _size
+ val content = content
+ for (i in lastIndex downTo 0) {
+ val item = content[i]
+ if (item !in elements) {
+ removeAt(i)
+ }
+ }
+ return initialSize != _size
+ }
+
+ /**
+ * Sets the value at [index] to [element].
+ * @return the previous value set at [index]
+ * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
+ */
+ public operator fun set(
+ @androidx.annotation.IntRange(from = 0) index: Int,
+ element: PKey
+ ): PKey {
+ if (index !in 0 until _size) {
+ throw IndexOutOfBoundsException("set index $index must be between 0 .. $lastIndex")
+ }
+ val content = content
+ val old = content[index]
+ content[index] = element
+ return old
+ }
+
+ /**
+ * Sorts the [MutablePKeyList] elements in ascending order.
+ */
+ public fun sort() {
+ content.sort(fromIndex = 0, toIndex = _size)
+ }
+
+ /**
+ * Sorts the [MutablePKeyList] elements in descending order.
+ */
+ public fun sortDescending() {
+ content.sortDescending(fromIndex = 0, toIndex = _size)
+ }
+}
+
+private val EmptyPKeyList: PKeyList = MutablePKeyList(0)
+
+/**
+ * @return a read-only [PKeyList] with nothing in it.
+ */
+public fun emptyPKeyList(): PKeyList = EmptyPKeyList
+
+/**
+ * @return a read-only [PKeyList] with nothing in it.
+ */
+public fun pKeyListOf(): PKeyList = EmptyPKeyList
+
+/**
+ * @return a new read-only [PKeyList] with [element1] as the only item in the list.
+ */
+public fun pKeyListOf(element1: PKey): PKeyList = mutablePKeyListOf(element1)
+
+/**
+ * @return a new read-only [PKeyList] with 2 elements, [element1] and [element2], in order.
+ */
+public fun pKeyListOf(element1: PKey, element2: PKey): PKeyList =
+ mutablePKeyListOf(element1, element2)
+
+/**
+ * @return a new read-only [PKeyList] with 3 elements, [element1], [element2], and [element3],
+ * in order.
+ */
+public fun pKeyListOf(element1: PKey, element2: PKey, element3: PKey): PKeyList =
+ mutablePKeyListOf(element1, element2, element3)
+
+/**
+ * @return a new read-only [PKeyList] with [elements] in order.
+ */
+public fun pKeyListOf(vararg elements: PKey): PKeyList =
+ MutablePKeyList(elements.size).apply { plusAssign(elements) }
+
+/**
+ * @return a new empty [MutablePKeyList] with the default capacity.
+ */
+public inline fun mutablePKeyListOf(): MutablePKeyList = MutablePKeyList()
+
+/**
+ * @return a new [MutablePKeyList] with [element1] as the only item in the list.
+ */
+public fun mutablePKeyListOf(element1: PKey): MutablePKeyList {
+ val list = MutablePKeyList(1)
+ list += element1
+ return list
+}
+
+/**
+ * @return a new [MutablePKeyList] with 2 elements, [element1] and [element2], in order.
+ */
+public fun mutablePKeyListOf(element1: PKey, element2: PKey): MutablePKeyList {
+ val list = MutablePKeyList(2)
+ list += element1
+ list += element2
+ return list
+}
+
+/**
+ * @return a new [MutablePKeyList] with 3 elements, [element1], [element2], and [element3],
+ * in order.
+ */
+public fun mutablePKeyListOf(element1: PKey, element2: PKey, element3: PKey): MutablePKeyList {
+ val list = MutablePKeyList(3)
+ list += element1
+ list += element2
+ list += element3
+ return list
+}
+
+/**
+ * @return a new [MutablePKeyList] with the given elements, in order.
+ */
+public inline fun mutablePKeyListOf(vararg elements: PKey): MutablePKeyList =
+ MutablePKeyList(elements.size).apply { plusAssign(elements) }
diff --git a/collection/collection/template/PKeyListTest.kt.template b/collection/collection/template/PKeyListTest.kt.template
new file mode 100644
index 0000000..34915e5
--- /dev/null
+++ b/collection/collection/template/PKeyListTest.kt.template
@@ -0,0 +1,723 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class PKeyListTest {
+ private val list: MutablePKeyList = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+
+ @Test
+ fun emptyConstruction() {
+ val l = mutablePKeyListOf()
+ assertEquals(0, l.size)
+ assertEquals(16, l.capacity)
+ }
+
+ @Test
+ fun sizeConstruction() {
+ val l = MutablePKeyList(4)
+ assertEquals(4, l.capacity)
+ }
+
+ @Test
+ fun contentConstruction() {
+ val l = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix)
+ assertEquals(3, l.size)
+ assertEquals(1KeySuffix, l[0])
+ assertEquals(2KeySuffix, l[1])
+ assertEquals(3KeySuffix, l[2])
+ assertEquals(3, l.capacity)
+ repeat(2) {
+ val l2 = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+ assertEquals(list, l2)
+ l2.removeAt(0)
+ }
+ }
+
+ @Test
+ fun hashCodeTest() {
+ val l2 = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+ assertEquals(list.hashCode(), l2.hashCode())
+ l2.removeAt(4)
+ assertNotEquals(list.hashCode(), l2.hashCode())
+ l2.add(5KeySuffix)
+ assertEquals(list.hashCode(), l2.hashCode())
+ l2.clear()
+ assertNotEquals(list.hashCode(), l2.hashCode())
+ }
+
+ @Test
+ fun equalsTest() {
+ val l2 = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+ assertEquals(list, l2)
+ assertNotEquals(list, mutablePKeyListOf())
+ l2.removeAt(4)
+ assertNotEquals(list, l2)
+ l2.add(5KeySuffix)
+ assertEquals(list, l2)
+ l2.clear()
+ assertNotEquals(list, l2)
+ }
+
+ @Test
+ fun string() {
+ assertEquals("[${1KeySuffix}, ${2KeySuffix}, ${3KeySuffix}, ${4KeySuffix}, ${5KeySuffix}]", list.toString())
+ assertEquals("[]", mutablePKeyListOf().toString())
+ }
+
+ @Test
+ fun size() {
+ assertEquals(5, list.size)
+ assertEquals(5, list.count())
+ val l2 = mutablePKeyListOf()
+ assertEquals(0, l2.size)
+ assertEquals(0, l2.count())
+ l2 += 1KeySuffix
+ assertEquals(1, l2.size)
+ assertEquals(1, l2.count())
+ }
+
+ @Test
+ fun get() {
+ assertEquals(1KeySuffix, list[0])
+ assertEquals(5KeySuffix, list[4])
+ assertEquals(1KeySuffix, list.elementAt(0))
+ assertEquals(5KeySuffix, list.elementAt(4))
+ }
+
+ @Test
+ fun getOutOfBounds() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ list[5]
+ }
+ }
+
+ @Test
+ fun getOutOfBoundsNegative() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ list[-1]
+ }
+ }
+
+ @Test
+ fun elementAtOfBounds() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ list.elementAt(5)
+ }
+ }
+
+ @Test
+ fun elementAtOfBoundsNegative() {
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ list.elementAt(-1)
+ }
+ }
+
+ @Test
+ fun elementAtOrElse() {
+ assertEquals(1KeySuffix, list.elementAtOrElse(0) {
+ assertEquals(0, it)
+ 0KeySuffix
+ })
+ assertEquals(0KeySuffix, list.elementAtOrElse(-1) {
+ assertEquals(-1, it)
+ 0KeySuffix
+ })
+ assertEquals(0KeySuffix, list.elementAtOrElse(5) {
+ assertEquals(5, it)
+ 0KeySuffix
+ })
+ }
+
+ @Test
+ fun count() {
+ assertEquals(1, list.count { it < 2KeySuffix })
+ assertEquals(0, list.count { it < 0KeySuffix })
+ assertEquals(5, list.count { it < 10KeySuffix })
+ }
+
+ @Test
+ fun isEmpty() {
+ assertFalse(list.isEmpty())
+ assertFalse(list.none())
+ assertTrue(mutablePKeyListOf().isEmpty())
+ assertTrue(mutablePKeyListOf().none())
+ }
+
+ @Test
+ fun isNotEmpty() {
+ assertTrue(list.isNotEmpty())
+ assertTrue(list.any())
+ assertFalse(mutablePKeyListOf().isNotEmpty())
+ }
+
+ @Test
+ fun indices() {
+ assertEquals(IntRange(0, 4), list.indices)
+ assertEquals(IntRange(0, -1), mutablePKeyListOf().indices)
+ }
+
+ @Test
+ fun any() {
+ assertTrue(list.any { it == 5KeySuffix })
+ assertTrue(list.any { it == 1KeySuffix })
+ assertFalse(list.any { it == 0KeySuffix })
+ }
+
+ @Test
+ fun reversedAny() {
+ val reversedList = mutablePKeyListOf()
+ assertFalse(
+ list.reversedAny {
+ reversedList.add(it)
+ false
+ }
+ )
+ val reversedContent = mutablePKeyListOf(5KeySuffix, 4KeySuffix, 3KeySuffix, 2KeySuffix, 1KeySuffix)
+ assertEquals(reversedContent, reversedList)
+
+ val reversedSublist = mutablePKeyListOf()
+ assertTrue(
+ list.reversedAny {
+ reversedSublist.add(it)
+ reversedSublist.size == 2
+ }
+ )
+ assertEquals(reversedSublist, mutablePKeyListOf(5KeySuffix, 4KeySuffix))
+ }
+
+ @Test
+ fun forEach() {
+ val copy = mutablePKeyListOf()
+ list.forEach { copy += it }
+ assertEquals(list, copy)
+ }
+
+ @Test
+ fun forEachReversed() {
+ val copy = mutablePKeyListOf()
+ list.forEachReversed { copy += it }
+ assertEquals(copy, mutablePKeyListOf(5KeySuffix, 4KeySuffix, 3KeySuffix, 2KeySuffix, 1KeySuffix))
+ }
+
+ @Test
+ fun forEachIndexed() {
+ val copy = mutablePKeyListOf()
+ val indices = mutablePKeyListOf()
+ list.forEachIndexed { index, item ->
+ copy += item
+ indices += index.toPKey()
+ }
+ assertEquals(list, copy)
+ assertEquals(indices, mutablePKeyListOf(0KeySuffix, 1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix))
+ }
+
+ @Test
+ fun forEachReversedIndexed() {
+ val copy = mutablePKeyListOf()
+ val indices = mutablePKeyListOf()
+ list.forEachReversedIndexed { index, item ->
+ copy += item
+ indices += index.toPKey()
+ }
+ assertEquals(copy, mutablePKeyListOf(5KeySuffix, 4KeySuffix, 3KeySuffix, 2KeySuffix, 1KeySuffix))
+ assertEquals(indices, mutablePKeyListOf(4KeySuffix, 3KeySuffix, 2KeySuffix, 1KeySuffix, 0KeySuffix))
+ }
+
+ @Test
+ fun indexOfFirst() {
+ assertEquals(0, list.indexOfFirst { it == 1KeySuffix })
+ assertEquals(4, list.indexOfFirst { it == 5KeySuffix })
+ assertEquals(-1, list.indexOfFirst { it == 0KeySuffix })
+ assertEquals(0, mutablePKeyListOf(8KeySuffix, 8KeySuffix).indexOfFirst { it == 8KeySuffix })
+ }
+
+ @Test
+ fun indexOfLast() {
+ assertEquals(0, list.indexOfLast { it == 1KeySuffix })
+ assertEquals(4, list.indexOfLast { it == 5KeySuffix })
+ assertEquals(-1, list.indexOfLast { it == 0KeySuffix })
+ assertEquals(1, mutablePKeyListOf(8KeySuffix, 8KeySuffix).indexOfLast { it == 8KeySuffix })
+ }
+
+ @Test
+ fun contains() {
+ assertTrue(list.contains(5KeySuffix))
+ assertTrue(list.contains(1KeySuffix))
+ assertFalse(list.contains(0KeySuffix))
+ }
+
+ @Test
+ fun containsAllList() {
+ assertTrue(list.containsAll(mutablePKeyListOf(2KeySuffix, 3KeySuffix, 1KeySuffix)))
+ assertFalse(list.containsAll(mutablePKeyListOf(2KeySuffix, 3KeySuffix, 6KeySuffix)))
+ }
+
+ @Test
+ fun lastIndexOf() {
+ assertEquals(4, list.lastIndexOf(5KeySuffix))
+ assertEquals(1, list.lastIndexOf(2KeySuffix))
+ val copy = mutablePKeyListOf()
+ copy.addAll(list)
+ copy.addAll(list)
+ assertEquals(5, copy.lastIndexOf(1KeySuffix))
+ }
+
+ @Test
+ fun first() {
+ assertEquals(1KeySuffix, list.first())
+ }
+
+ @Test
+ fun firstException() {
+ assertFailsWith(NoSuchElementException::class) {
+ mutablePKeyListOf().first()
+ }
+ }
+
+ @Test
+ fun firstWithPredicate() {
+ assertEquals(5KeySuffix, list.first { it == 5KeySuffix })
+ assertEquals(1KeySuffix, mutablePKeyListOf(1KeySuffix, 5KeySuffix).first { it != 0KeySuffix })
+ }
+
+ @Test
+ fun firstWithPredicateException() {
+ assertFailsWith(NoSuchElementException::class) {
+ mutablePKeyListOf().first { it == 8KeySuffix }
+ }
+ }
+
+ @Test
+ fun last() {
+ assertEquals(5KeySuffix, list.last())
+ }
+
+ @Test
+ fun lastException() {
+ assertFailsWith(NoSuchElementException::class) {
+ mutablePKeyListOf().last()
+ }
+ }
+
+ @Test
+ fun lastWithPredicate() {
+ assertEquals(1KeySuffix, list.last { it == 1KeySuffix })
+ assertEquals(5KeySuffix, mutablePKeyListOf(1KeySuffix, 5KeySuffix).last { it != 0KeySuffix })
+ }
+
+ @Test
+ fun lastWithPredicateException() {
+ assertFailsWith(NoSuchElementException::class) {
+ mutablePKeyListOf().last { it == 8KeySuffix }
+ }
+ }
+
+ @Test
+ fun fold() {
+ assertEquals("12345", list.fold("") { acc, i -> acc + i.toInt().toString() })
+ }
+
+ @Test
+ fun foldIndexed() {
+ assertEquals(
+ "01-12-23-34-45-",
+ list.foldIndexed("") { index, acc, i ->
+ "$acc$index${i.toInt()}-"
+ }
+ )
+ }
+
+ @Test
+ fun foldRight() {
+ assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toInt().toString() })
+ }
+
+ @Test
+ fun foldRightIndexed() {
+ assertEquals(
+ "45-34-23-12-01-",
+ list.foldRightIndexed("") { index, i, acc ->
+ "$acc$index${i.toInt()}-"
+ }
+ )
+ }
+
+ @Test
+ fun add() {
+ val l = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix)
+ l += 4KeySuffix
+ l.add(5KeySuffix)
+ assertEquals(list, l)
+ }
+
+ @Test
+ fun addAtIndex() {
+ val l = mutablePKeyListOf(2KeySuffix, 4KeySuffix)
+ l.add(2, 5KeySuffix)
+ l.add(0, 1KeySuffix)
+ l.add(2, 3KeySuffix)
+ assertEquals(list, l)
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ l.add(-1, 2KeySuffix)
+ }
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ l.add(6, 2KeySuffix)
+ }
+ }
+
+ @Test
+ fun addAllListAtIndex() {
+ val l = mutablePKeyListOf(4KeySuffix)
+ val l2 = mutablePKeyListOf(1KeySuffix, 2KeySuffix)
+ val l3 = mutablePKeyListOf(5KeySuffix)
+ val l4 = mutablePKeyListOf(3KeySuffix)
+ assertTrue(l4.addAll(1, l3))
+ assertTrue(l4.addAll(0, l2))
+ assertTrue(l4.addAll(3, l))
+ assertFalse(l4.addAll(0, mutablePKeyListOf()))
+ assertEquals(list, l4)
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ l4.addAll(6, mutablePKeyListOf())
+ }
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ l4.addAll(-1, mutablePKeyListOf())
+ }
+ }
+
+ @Test
+ fun addAllList() {
+ val l = MutablePKeyList()
+ l.add(3KeySuffix)
+ l.add(4KeySuffix)
+ l.add(5KeySuffix)
+ val l2 = mutablePKeyListOf(1KeySuffix, 2KeySuffix)
+ assertTrue(l2.addAll(l))
+ assertEquals(list, l2)
+ assertFalse(l2.addAll(mutablePKeyListOf()))
+ }
+
+ @Test
+ fun plusAssignList() {
+ val l = MutablePKeyList()
+ l.add(3KeySuffix)
+ l.add(4KeySuffix)
+ l.add(5KeySuffix)
+ val l2 = mutablePKeyListOf(1KeySuffix, 2KeySuffix)
+ l2 += l
+ assertEquals(list, l2)
+ }
+
+ @Test
+ fun addAllArrayAtIndex() {
+ val a1 = pKeyArrayOf(4KeySuffix)
+ val a2 = pKeyArrayOf(1KeySuffix, 2KeySuffix)
+ val a3 = pKeyArrayOf(5KeySuffix)
+ val l = mutablePKeyListOf(3KeySuffix)
+ assertTrue(l.addAll(1, a3))
+ assertTrue(l.addAll(0, a2))
+ assertTrue(l.addAll(3, a1))
+ assertFalse(l.addAll(0, pKeyArrayOf()))
+ assertEquals(list, l)
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ l.addAll(6, pKeyArrayOf())
+ }
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ l.addAll(-1, pKeyArrayOf())
+ }
+ }
+
+ @Test
+ fun addAllArray() {
+ val a = pKeyArrayOf(3KeySuffix, 4KeySuffix, 5KeySuffix)
+ val v = mutablePKeyListOf(1KeySuffix, 2KeySuffix)
+ v.addAll(a)
+ assertEquals(5, v.size)
+ assertEquals(3KeySuffix, v[2])
+ assertEquals(4KeySuffix, v[3])
+ assertEquals(5KeySuffix, v[4])
+ }
+
+ @Test
+ fun plusAssignArray() {
+ val a = pKeyArrayOf(3KeySuffix, 4KeySuffix, 5KeySuffix)
+ val v = mutablePKeyListOf(1KeySuffix, 2KeySuffix)
+ v += a
+ assertEquals(list, v)
+ }
+
+ @Test
+ fun clear() {
+ val l = mutablePKeyListOf()
+ l.addAll(list)
+ assertTrue(l.isNotEmpty())
+ l.clear()
+ assertTrue(l.isEmpty())
+ }
+
+ @Test
+ fun trim() {
+ val l = mutablePKeyListOf(1KeySuffix)
+ l.trim()
+ assertEquals(1, l.capacity)
+ l += pKeyArrayOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+ l.trim()
+ assertEquals(6, l.capacity)
+ assertEquals(6, l.size)
+ l.clear()
+ l.trim()
+ assertEquals(0, l.capacity)
+ l.trim(100)
+ assertEquals(0, l.capacity)
+ l += pKeyArrayOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+ l -= 5KeySuffix
+ l.trim(5)
+ assertEquals(5, l.capacity)
+ l.trim(4)
+ assertEquals(4, l.capacity)
+ l.trim(3)
+ assertEquals(4, l.capacity)
+ }
+
+ @Test
+ fun remove() {
+ val l = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+ l.remove(3KeySuffix)
+ assertEquals(mutablePKeyListOf(1KeySuffix, 2KeySuffix, 4KeySuffix, 5KeySuffix), l)
+ }
+
+ @Test
+ fun removeAt() {
+ val l = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+ l.removeAt(2)
+ assertEquals(mutablePKeyListOf(1KeySuffix, 2KeySuffix, 4KeySuffix, 5KeySuffix), l)
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ l.removeAt(6)
+ }
+ assertFailsWith(IndexOutOfBoundsException::class) {
+ l.removeAt(-1)
+ }
+ }
+
+ @Test
+ fun set() {
+ val l = mutablePKeyListOf(0KeySuffix, 0KeySuffix, 0KeySuffix, 0KeySuffix, 0KeySuffix)
+ l[0] = 1KeySuffix
+ l[4] = 5KeySuffix
+ l[2] = 3KeySuffix
+ l[1] = 2KeySuffix
+ l[3] = 4KeySuffix
+ assertEquals(list, l)
+ assertFailsWith<IndexOutOfBoundsException> {
+ l.set(-1, 1KeySuffix)
+ }
+ assertFailsWith<IndexOutOfBoundsException> {
+ l.set(6, 1KeySuffix)
+ }
+ assertEquals(4KeySuffix, l.set(3, 1KeySuffix));
+ }
+
+ @Test
+ fun ensureCapacity() {
+ val l = mutablePKeyListOf(1KeySuffix)
+ assertEquals(1, l.capacity)
+ l.ensureCapacity(5)
+ assertEquals(5, l.capacity)
+ }
+
+ @Test
+ fun removeAllList() {
+ assertFalse(list.removeAll(mutablePKeyListOf(0KeySuffix, 10KeySuffix, 15KeySuffix)))
+ val l = mutablePKeyListOf(0KeySuffix, 1KeySuffix, 15KeySuffix, 10KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 20KeySuffix, 5KeySuffix)
+ assertTrue(l.removeAll(mutablePKeyListOf(20KeySuffix, 0KeySuffix, 15KeySuffix, 10KeySuffix, 5KeySuffix)))
+ assertEquals(list, l)
+ }
+
+ @Test
+ fun removeAllPKeyArray() {
+ assertFalse(list.removeAll(pKeyArrayOf(0KeySuffix, 10KeySuffix, 15KeySuffix)))
+ val l = mutablePKeyListOf(0KeySuffix, 1KeySuffix, 15KeySuffix, 10KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 20KeySuffix, 5KeySuffix)
+ assertTrue(l.removeAll(pKeyArrayOf(20KeySuffix, 0KeySuffix, 15KeySuffix, 10KeySuffix, 5KeySuffix)))
+ assertEquals(list, l)
+ }
+
+ @Test
+ fun minusAssignList() {
+ val l = mutablePKeyListOf().also { it += list }
+ l -= mutablePKeyListOf(0KeySuffix, 10KeySuffix, 15KeySuffix)
+ assertEquals(list, l)
+ val l2 = mutablePKeyListOf(0KeySuffix, 1KeySuffix, 15KeySuffix, 10KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 20KeySuffix, 5KeySuffix)
+ l2 -= mutablePKeyListOf(20KeySuffix, 0KeySuffix, 15KeySuffix, 10KeySuffix, 5KeySuffix)
+ assertEquals(list, l2)
+ }
+
+ @Test
+ fun minusAssignPKeyArray() {
+ val l = mutablePKeyListOf().also { it += list }
+ l -= pKeyArrayOf(0KeySuffix, 10KeySuffix, 15KeySuffix)
+ assertEquals(list, l)
+ val l2 = mutablePKeyListOf(0KeySuffix, 1KeySuffix, 15KeySuffix, 10KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 20KeySuffix, 5KeySuffix)
+ l2 -= pKeyArrayOf(20KeySuffix, 0KeySuffix, 15KeySuffix, 10KeySuffix, 5KeySuffix)
+ assertEquals(list, l2)
+ }
+
+ @Test
+ fun retainAll() {
+ assertFalse(list.retainAll(mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 6KeySuffix)))
+ val l = mutablePKeyListOf(0KeySuffix, 1KeySuffix, 15KeySuffix, 10KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 20KeySuffix)
+ assertTrue(l.retainAll(mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 6KeySuffix)))
+ assertEquals(list, l)
+ }
+
+ @Test
+ fun retainAllPKeyArray() {
+ assertFalse(list.retainAll(pKeyArrayOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 6KeySuffix)))
+ val l = mutablePKeyListOf(0KeySuffix, 1KeySuffix, 15KeySuffix, 10KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 20KeySuffix)
+ assertTrue(l.retainAll(pKeyArrayOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 6KeySuffix)))
+ assertEquals(list, l)
+ }
+
+ @Test
+ fun removeRange() {
+ val l = mutablePKeyListOf(1KeySuffix, 9KeySuffix, 7KeySuffix, 6KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+ l.removeRange(1, 4)
+ assertEquals(list, l)
+ assertFailsWith<IndexOutOfBoundsException> {
+ l.removeRange(6, 6)
+ }
+ assertFailsWith<IndexOutOfBoundsException> {
+ l.removeRange(100, 200)
+ }
+ assertFailsWith<IndexOutOfBoundsException> {
+ l.removeRange(-1, 0)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ l.removeRange(3, 2)
+ }
+ }
+
+ @Test
+ fun sort() {
+ val l = mutablePKeyListOf(1KeySuffix, 4KeySuffix, 2KeySuffix, 5KeySuffix, 3KeySuffix)
+ l.sort()
+ assertEquals(list, l)
+ }
+
+ @Test
+ fun sortDescending() {
+ val l = mutablePKeyListOf(1KeySuffix, 4KeySuffix, 2KeySuffix, 5KeySuffix, 3KeySuffix)
+ l.sortDescending()
+ assertEquals(mutablePKeyListOf(5KeySuffix, 4KeySuffix, 3KeySuffix, 2KeySuffix, 1KeySuffix), l)
+ }
+
+ @Test
+ fun testEmptyPKeyList() {
+ val l = emptyPKeyList()
+ assertEquals(0, l.size)
+ }
+
+ @Test
+ fun pKeyListOfEmpty() {
+ val l = pKeyListOf()
+ assertEquals(0, l.size)
+ }
+
+ @Test
+ fun pKeyListOfOneValue() {
+ val l = pKeyListOf(2KeySuffix)
+ assertEquals(1, l.size)
+ assertEquals(2KeySuffix, l[0])
+ }
+
+ @Test
+ fun pKeyListOfTwoValues() {
+ val l = pKeyListOf(2KeySuffix, 1KeySuffix)
+ assertEquals(2, l.size)
+ assertEquals(2KeySuffix, l[0])
+ assertEquals(1KeySuffix, l[1])
+ }
+
+ @Test
+ fun pKeyListOfThreeValues() {
+ val l = pKeyListOf(2KeySuffix, 10KeySuffix, -1KeySuffix)
+ assertEquals(3, l.size)
+ assertEquals(2KeySuffix, l[0])
+ assertEquals(10KeySuffix, l[1])
+ assertEquals(-1KeySuffix, l[2])
+ }
+
+ @Test
+ fun pKeyListOfFourValues() {
+ val l = pKeyListOf(2KeySuffix, 10KeySuffix, -1KeySuffix, 10KeySuffix)
+ assertEquals(4, l.size)
+ assertEquals(2KeySuffix, l[0])
+ assertEquals(10KeySuffix, l[1])
+ assertEquals(-1KeySuffix, l[2])
+ assertEquals(10KeySuffix, l[3])
+ }
+
+ @Test
+ fun mutablePKeyListOfOneValue() {
+ val l = mutablePKeyListOf(2KeySuffix)
+ assertEquals(1, l.size)
+ assertEquals(1, l.capacity)
+ assertEquals(2KeySuffix, l[0])
+ }
+
+ @Test
+ fun mutablePKeyListOfTwoValues() {
+ val l = mutablePKeyListOf(2KeySuffix, 1KeySuffix)
+ assertEquals(2, l.size)
+ assertEquals(2, l.capacity)
+ assertEquals(2KeySuffix, l[0])
+ assertEquals(1KeySuffix, l[1])
+ }
+
+ @Test
+ fun mutablePKeyListOfThreeValues() {
+ val l = mutablePKeyListOf(2KeySuffix, 10KeySuffix, -1KeySuffix)
+ assertEquals(3, l.size)
+ assertEquals(3, l.capacity)
+ assertEquals(2KeySuffix, l[0])
+ assertEquals(10KeySuffix, l[1])
+ assertEquals(-1KeySuffix, l[2])
+ }
+
+ @Test
+ fun mutablePKeyListOfFourValues() {
+ val l = mutablePKeyListOf(2KeySuffix, 10KeySuffix, -1KeySuffix, 10KeySuffix)
+ assertEquals(4, l.size)
+ assertEquals(4, l.capacity)
+ assertEquals(2KeySuffix, l[0])
+ assertEquals(10KeySuffix, l[1])
+ assertEquals(-1KeySuffix, l[2])
+ assertEquals(10KeySuffix, l[3])
+ }
+}
diff --git a/collection/collection/template/PKeyObjectMap.kt.template b/collection/collection/template/PKeyObjectMap.kt.template
new file mode 100644
index 0000000..2bb53a2b
--- /dev/null
+++ b/collection/collection/template/PKeyObjectMap.kt.template
@@ -0,0 +1,847 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyPKeyObjectMap = MutablePKeyObjectMap<Nothing>(0)
+
+/**
+ * Returns an empty, read-only [PKeyObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> emptyPKeyObjectMap(): PKeyObjectMap<V> = EmptyPKeyObjectMap as PKeyObjectMap<V>
+
+/**
+ * Returns an empty, read-only [PKeyObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> pKeyObjectMapOf(): PKeyObjectMap<V> = EmptyPKeyObjectMap as PKeyObjectMap<V>
+
+/**
+ * Returns a new [PKeyObjectMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [PKey] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <V> pKeyObjectMapOf(vararg pairs: Pair<PKey, V>): PKeyObjectMap<V> =
+ MutablePKeyObjectMap<V>(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * Returns a new [MutablePKeyObjectMap].
+ */
+public fun <V> mutablePKeyObjectMapOf(): MutablePKeyObjectMap<V> = MutablePKeyObjectMap()
+
+/**
+ * Returns a new [MutablePKeyObjectMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [PKey] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <V> mutablePKeyObjectMapOf(vararg pairs: Pair<PKey, V>): MutablePKeyObjectMap<V> =
+ MutablePKeyObjectMap<V>(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * [PKeyObjectMap] is a container with a [Map]-like interface for keys with
+ * [PKey] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutablePKeyObjectMap].
+ *
+ * @see [MutablePKeyObjectMap]
+ */
+public sealed class PKeyObjectMap<V> {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` entries, including when
+ // the table is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var keys: PKeyArray = EmptyPKeyArray
+
+ @PublishedApi
+ @JvmField
+ internal var values: Array<Any?> = EMPTY_OBJECTS
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of key-value pairs that can be stored in this map
+ * without requiring internal storage reallocation.
+ */
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of key-value pairs in this map.
+ */
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this map has at least one entry.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this map has no entries.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this map is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this map is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the value corresponding to the given [key], or `null` if such
+ * a key is not present in the map.
+ */
+ public operator fun get(key: PKey): V? {
+ val index = findKeyIndex(key)
+ @Suppress("UNCHECKED_CAST")
+ return if (index >= 0) values[index] as V? else null
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * or [defaultValue] if this map contains no mapping for the key.
+ */
+ public fun getOrDefault(key: PKey, defaultValue: V): V {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ @Suppress("UNCHECKED_CAST")
+ return values[index] as V
+ }
+ return defaultValue
+ }
+
+ /**
+ * Returns the value for the given [key] if the value is present
+ * and not null. Otherwise, returns the result of the [defaultValue]
+ * function.
+ */
+ public inline fun getOrElse(key: PKey, defaultValue: () -> V): V {
+ return get(key) ?: defaultValue()
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ public inline fun forEach(block: (key: PKey, value: V) -> Unit) {
+ val k = keys
+ val v = values
+
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ block(k[index], v[index] as V)
+ }
+ }
+
+ /**
+ * Iterates over every key stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachKey(block: (key: PKey) -> Unit) {
+ val k = keys
+
+ forEachIndexed { index ->
+ block(k[index])
+ }
+ }
+
+ /**
+ * Iterates over every value stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachValue(block: (value: V) -> Unit) {
+ val v = values
+
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ block(v[index] as V)
+ }
+ }
+
+ /**
+ * Returns true if all entries match the given [predicate].
+ */
+ public inline fun all(predicate: (PKey, V) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (!predicate(key, value)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one entry matches the given [predicate].
+ */
+ public inline fun any(predicate: (PKey, V) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (predicate(key, value)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of entries in this map.
+ */
+ public fun count(): Int = size
+
+ /**
+ * Returns the number of entries matching the given [predicate].
+ */
+ public inline fun count(predicate: (PKey, V) -> Boolean): Int {
+ var count = 0
+ forEach { key, value ->
+ if (predicate(key, value)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public operator fun contains(key: PKey): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsKey(key: PKey): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [value] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsValue(value: V): Boolean {
+ forEachValue { v ->
+ if (value == v) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the hash code value for this map. The hash code the sum of the hash
+ * codes of each key/value pair.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { key, value ->
+ hash += key.hashCode() xor value.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this hash map for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [PKeyObjectMap]
+ * - Has the same [size] as this map
+ * - Contains key/value pairs equal to this map's pair
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is PKeyObjectMap<*>) {
+ return false
+ }
+ if (other.size != size) {
+ return false
+ }
+
+ forEach { key, value ->
+ if (value == null) {
+ if (other[key] != null || !other.containsKey(key)) {
+ return false
+ }
+ } else if (value != other[key]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this map. The map is denoted in the
+ * string by the `{}`. Each key/value pair present in the map is represented
+ * inside '{}` by a substring of the form `key=value`, and pairs are
+ * separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ val s = StringBuilder().append('{')
+ var i = 0
+ forEach { key, value ->
+ s.append(key)
+ s.append("=")
+ s.append(if (value === this) "(this)" else value)
+ i++
+ if (i < _size) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append('}').toString()
+ }
+
+ /**
+ * Scans the hash table to find the index in the backing arrays of the
+ * specified [key]. Returns -1 if the key is not present.
+ */
+ internal inline fun findKeyIndex(key: PKey): Int {
+ val hash = hash(key)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutablePKeyObjectMap] is a container with a [MutableMap]-like interface for keys with
+ * [PKey] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutablePKeyObjectMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see ScatterMap
+ */
+public class MutablePKeyObjectMap<V>(
+ initialCapacity: Int = DefaultScatterCapacity
+) : PKeyObjectMap<V>() {
+ // Number of entries we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ keys = PKeyArray(newCapacity)
+ values = arrayOfNulls(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * if the value is present in the map and not `null`. Otherwise,
+ * calls `defaultValue()` and puts the result in the map associated
+ * with [key].
+ */
+ public inline fun getOrPut(key: PKey, defaultValue: () -> V): V {
+ return get(key) ?: defaultValue().also { set(key, it) }
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public operator fun set(key: PKey, value: V) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations. Return the previous value associated with the [key],
+ * or `null` if the key was not present in the map.
+ */
+ public fun put(key: PKey, value: V): V? {
+ val index = findAbsoluteInsertIndex(key)
+ val oldValue = values[index]
+ keys[index] = key
+ values[index] = value
+
+ @Suppress("UNCHECKED_CAST")
+ return oldValue as V?
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [PKey] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+ public fun putAll(@Suppress("ArrayReturn") pairs: Array<out Pair<PKey, V>>) {
+ for ((key, value) in pairs) {
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public fun putAll(from: PKeyObjectMap<V>) {
+ from.forEach { key, value ->
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts the key/value mapping from the [pair] in this map, using the first
+ * element as the key, and the second element as the value.
+ *
+ * Note that the [Pair] is allocated and the [PKey] key is boxed.
+ * Use [set] instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(pair: Pair<PKey, V>) {
+ this[pair.first] = pair.second
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [PKey] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+ public inline operator fun plusAssign(
+ @Suppress("ArrayReturn") pairs: Array<out Pair<PKey, V>>
+ ): Unit = putAll(pairs)
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public inline operator fun plusAssign(from: PKeyObjectMap<V>): Unit = putAll(from)
+
+ /**
+ * Removes the specified [key] and its associated value from the map. If the
+ * [key] was present in the map, this function returns the value that was
+ * present before removal.
+ */
+ public fun remove(key: PKey): V? {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return removeValueAt(index)
+ }
+ return null
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map if the
+ * associated value equals [value]. Returns whether the removal happened.
+ */
+ public fun remove(key: PKey, value: V): Boolean {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ if (values[index] == value) {
+ removeValueAt(index)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes any mapping for which the specified [predicate] returns true.
+ */
+ public fun removeIf(predicate: (PKey, V) -> Boolean) {
+ forEachIndexed { index ->
+ @Suppress("UNCHECKED_CAST")
+ if (predicate(keys[index], values[index] as V)) {
+ removeValueAt(index)
+ }
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public inline operator fun minusAssign(key: PKey) {
+ remove(key)
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: PKeyArray) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: PKeySet) {
+ keys.forEach { key ->
+ minusAssign(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: PKeyList) {
+ keys.forEach { key ->
+ minusAssign(key)
+ }
+ }
+
+ private fun removeValueAt(index: Int): V? {
+ _size -= 1
+
+ // TODO: We could just mark the entry as empty if there's a group
+ // window around this entry that was already empty
+ writeMetadata(index, Deleted)
+ val oldValue = values[index]
+ values[index] = null
+
+ @Suppress("UNCHECKED_CAST")
+ return oldValue as V?
+ }
+
+ /**
+ * Removes all mappings from this map.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ values.fill(null, 0, _capacity)
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the hash table to find the index at which we can store a value
+ * for the give [key]. If the key already exists in the table, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the table is full.
+ */
+ private fun findAbsoluteInsertIndex(key: PKey): Int {
+ val hash = hash(key)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the table in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutablePKeyObjectMap]'s storage so it is sized appropriately
+ * to hold the current mappings.
+ *
+ * Returns the number of empty entries removed from this map's storage.
+ * Returns be 0 if no trimming is necessary or possible.
+ */
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted entries from the table to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_set`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousKeys = keys
+ val previousValues = values
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newKeys = keys
+ val newValues = values
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousKey = previousKeys[i]
+ val hash = hash(previousKey)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newKeys[index] = previousKey
+ newValues[index] = previousValues[i]
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
diff --git a/collection/collection/template/PKeyObjectMapTest.kt.template b/collection/collection/template/PKeyObjectMapTest.kt.template
new file mode 100644
index 0000000..d04a330
--- /dev/null
+++ b/collection/collection/template/PKeyObjectMapTest.kt.template
@@ -0,0 +1,632 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class PKeyObjectMapTest {
+ @Test
+ fun pKeyObjectMap() {
+ val map = MutablePKeyObjectMap<String>()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun testEmptyPKeyObjectMap() {
+ val map = emptyPKeyObjectMap<String>()
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+
+ assertSame(emptyPKeyObjectMap<String>(), map)
+ }
+
+ @Test
+ fun pKeyObjectMapFunction() {
+ val map = mutablePKeyObjectMapOf<String>()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun zeroCapacityHashMap() {
+ val map = MutablePKeyObjectMap<String>(0)
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun pKeyObjectMapWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val map = MutablePKeyObjectMap<String>(1800)
+ assertEquals(4095, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun pKeyObjectMapPairsFunction() {
+ val map = mutablePKeyObjectMapOf(
+ 1KeySuffix to "World",
+ 2KeySuffix to "Monde"
+ )
+ assertEquals(2, map.size)
+ assertEquals("World", map[1KeySuffix])
+ assertEquals("Monde", map[2KeySuffix])
+ }
+
+ @Test
+ fun addToMap() {
+ val map = MutablePKeyObjectMap<String>()
+ map[1KeySuffix] = "World"
+
+ assertEquals(1, map.size)
+ assertEquals("World", map[1KeySuffix])
+ }
+
+ @Test
+ fun insertIndex0() {
+ val map = MutablePKeyObjectMap<String>()
+ map.put(1KeySuffix, "World")
+ assertEquals("World", map[1KeySuffix])
+ }
+
+ @Test
+ fun addToSizedMap() {
+ val map = MutablePKeyObjectMap<String>(12)
+ map[1KeySuffix] = "World"
+
+ assertEquals(1, map.size)
+ assertEquals("World", map[1KeySuffix])
+ }
+
+ @Test
+ fun addToSmallMap() {
+ val map = MutablePKeyObjectMap<String>(2)
+ map[1KeySuffix] = "World"
+
+ assertEquals(1, map.size)
+ assertEquals(7, map.capacity)
+ assertEquals("World", map[1KeySuffix])
+ }
+
+ @Test
+ fun addToZeroCapacityMap() {
+ val map = MutablePKeyObjectMap<String>(0)
+ map[1KeySuffix] = "World"
+
+ assertEquals(1, map.size)
+ assertEquals("World", map[1KeySuffix])
+ }
+
+ @Test
+ fun replaceExistingKey() {
+ val map = MutablePKeyObjectMap<String>()
+ map[1KeySuffix] = "World"
+ map[1KeySuffix] = "Monde"
+
+ assertEquals(1, map.size)
+ assertEquals("Monde", map[1KeySuffix])
+ }
+
+ @Test
+ fun put() {
+ val map = MutablePKeyObjectMap<String?>()
+
+ assertNull(map.put(1KeySuffix, "World"))
+ assertEquals("World", map.put(1KeySuffix, "Monde"))
+ assertNull(map.put(2KeySuffix, null))
+ assertNull(map.put(2KeySuffix, "Monde"))
+ }
+
+ @Test
+ fun putAllArray() {
+ val map = MutablePKeyObjectMap<String?>()
+ map[1KeySuffix] = "World"
+ map[2KeySuffix] = null
+
+ map.putAll(arrayOf(3KeySuffix to "Welt", 7KeySuffix to "Mundo"))
+
+ assertEquals(4, map.size)
+ assertEquals("Welt", map[3KeySuffix])
+ assertEquals("Mundo", map[7KeySuffix])
+ }
+
+ @Test
+ fun putAllMap() {
+ val map = MutablePKeyObjectMap<String?>()
+ map[1KeySuffix] = "World"
+ map[2KeySuffix] = null
+
+ map.putAll(mutablePKeyObjectMapOf(3KeySuffix to "Welt", 7KeySuffix to "Mundo"))
+
+ assertEquals(4, map.size)
+ assertEquals("Welt", map[3KeySuffix])
+ assertEquals("Mundo", map[7KeySuffix])
+ }
+
+ @Test
+ fun plus() {
+ val map = MutablePKeyObjectMap<String>()
+ map += 1KeySuffix to "World"
+
+ assertEquals(1, map.size)
+ assertEquals("World", map[1KeySuffix])
+ }
+
+ @Test
+ fun plusMap() {
+ val map = MutablePKeyObjectMap<String>()
+ map += pKeyObjectMapOf(3KeySuffix to "Welt", 7KeySuffix to "Mundo")
+
+ assertEquals(2, map.size)
+ assertEquals("Welt", map[3KeySuffix])
+ assertEquals("Mundo", map[7KeySuffix])
+ }
+
+ @Test
+ fun plusArray() {
+ val map = MutablePKeyObjectMap<String>()
+ map += arrayOf(3KeySuffix to "Welt", 7KeySuffix to "Mundo")
+
+ assertEquals(2, map.size)
+ assertEquals("Welt", map[3KeySuffix])
+ assertEquals("Mundo", map[7KeySuffix])
+ }
+
+ @Test
+ fun nullValue() {
+ val map = MutablePKeyObjectMap<String?>()
+ map[1KeySuffix] = null
+
+ assertEquals(1, map.size)
+ assertNull(map[1KeySuffix])
+ }
+
+ @Test
+ fun findNonExistingKey() {
+ val map = MutablePKeyObjectMap<String?>()
+ map[1KeySuffix] = "World"
+
+ assertNull(map[2KeySuffix])
+ }
+
+ @Test
+ fun getOrDefault() {
+ val map = MutablePKeyObjectMap<String?>()
+ map[1KeySuffix] = "World"
+
+ assertEquals("Monde", map.getOrDefault(2KeySuffix, "Monde"))
+ }
+
+ @Test
+ fun getOrElse() {
+ val map = MutablePKeyObjectMap<String?>()
+ map[1KeySuffix] = "World"
+ map[2KeySuffix] = null
+
+ assertEquals("Monde", map.getOrElse(2KeySuffix) { "Monde" })
+ assertEquals("Welt", map.getOrElse(3KeySuffix) { "Welt" })
+ }
+
+ @Test
+ fun getOrPut() {
+ val map = MutablePKeyObjectMap<String?>()
+ map[1KeySuffix] = "World"
+
+ var counter = 0
+ map.getOrPut(1KeySuffix) {
+ counter++
+ "Monde"
+ }
+ assertEquals("World", map[1KeySuffix])
+ assertEquals(0, counter)
+
+ map.getOrPut(2KeySuffix) {
+ counter++
+ "Monde"
+ }
+ assertEquals("Monde", map[2KeySuffix])
+ assertEquals(1, counter)
+
+ map.getOrPut(2KeySuffix) {
+ counter++
+ "Welt"
+ }
+ assertEquals("Monde", map[2KeySuffix])
+ assertEquals(1, counter)
+
+ map.getOrPut(3KeySuffix) {
+ counter++
+ null
+ }
+ assertNull(map[3KeySuffix])
+ assertEquals(2, counter)
+
+ map.getOrPut(3KeySuffix) {
+ counter++
+ "Welt"
+ }
+ assertEquals("Welt", map[3KeySuffix])
+ assertEquals(3, counter)
+ }
+
+ @Test
+ fun remove() {
+ val map = MutablePKeyObjectMap<String?>()
+ assertNull(map.remove(1KeySuffix))
+
+ map[1KeySuffix] = "World"
+ assertEquals("World", map.remove(1KeySuffix))
+ assertEquals(0, map.size)
+
+ map[1KeySuffix] = null
+ assertNull(map.remove(1KeySuffix))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val map = MutablePKeyObjectMap<String>(6)
+ map[1KeySuffix] = "World"
+ map[2KeySuffix] = "Monde"
+ map[3KeySuffix] = "Welt"
+ map[4KeySuffix] = "Sekai"
+ map[5KeySuffix] = "Mondo"
+ map[6KeySuffix] = "Sesang"
+
+ // Removing all the entries will mark the medata as deleted
+ map.remove(1KeySuffix)
+ map.remove(2KeySuffix)
+ map.remove(3KeySuffix)
+ map.remove(4KeySuffix)
+ map.remove(5KeySuffix)
+ map.remove(6KeySuffix)
+
+ assertEquals(0, map.size)
+
+ val capacity = map.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ map[7KeySuffix] = "Mundo"
+
+ assertEquals(1, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun removeIf() {
+ val map = MutablePKeyObjectMap<String>()
+ map[1KeySuffix] = "World"
+ map[2KeySuffix] = "Monde"
+ map[3KeySuffix] = "Welt"
+ map[4KeySuffix] = "Sekai"
+ map[5KeySuffix] = "Mondo"
+ map[6KeySuffix] = "Sesang"
+
+ map.removeIf { key, value ->
+ key == 1KeySuffix || key == 3KeySuffix || value.startsWith('S')
+ }
+
+ assertEquals(2, map.size)
+ assertEquals("Monde", map[2KeySuffix])
+ assertEquals("Mondo", map[5KeySuffix])
+ }
+
+ @Test
+ fun minus() {
+ val map = MutablePKeyObjectMap<String>()
+ map[1KeySuffix] = "World"
+ map[2KeySuffix] = "Monde"
+ map[3KeySuffix] = "Welt"
+
+ map -= 1KeySuffix
+
+ assertEquals(2, map.size)
+ assertNull(map[1KeySuffix])
+ }
+
+ @Test
+ fun minusArray() {
+ val map = MutablePKeyObjectMap<String>()
+ map[1KeySuffix] = "World"
+ map[2KeySuffix] = "Monde"
+ map[3KeySuffix] = "Welt"
+
+ map -= pKeyArrayOf(3KeySuffix, 2KeySuffix)
+
+ assertEquals(1, map.size)
+ assertNull(map[3KeySuffix])
+ assertNull(map[2KeySuffix])
+ }
+
+ @Test
+ fun minusSet() {
+ val map = MutablePKeyObjectMap<String>()
+ map[1KeySuffix] = "World"
+ map[2KeySuffix] = "Monde"
+ map[3KeySuffix] = "Welt"
+
+ map -= pKeySetOf(3KeySuffix, 2KeySuffix)
+
+ assertEquals(1, map.size)
+ assertNull(map[3KeySuffix])
+ assertNull(map[2KeySuffix])
+ }
+
+ @Test
+ fun minusList() {
+ val map = MutablePKeyObjectMap<String>()
+ map[1KeySuffix] = "World"
+ map[2KeySuffix] = "Monde"
+ map[3KeySuffix] = "Welt"
+
+ map -= pKeyListOf(3KeySuffix, 2KeySuffix)
+
+ assertEquals(1, map.size)
+ assertNull(map[3KeySuffix])
+ assertNull(map[2KeySuffix])
+ }
+
+ @Test
+ fun conditionalRemove() {
+ val map = MutablePKeyObjectMap<String?>()
+ assertFalse(map.remove(1KeySuffix, "World"))
+
+ map[1KeySuffix] = "World"
+ assertTrue(map.remove(1KeySuffix, "World"))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val map = MutablePKeyObjectMap<String>()
+
+ for (i in 0 until 1700) {
+ map[i.toPKey()] = i.toString()
+ }
+
+ assertEquals(1700, map.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val map = MutablePKeyObjectMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toPKey()] = j.toString()
+ }
+
+ var counter = 0
+ map.forEach { key, value ->
+ assertEquals(key.toInt().toString(), value)
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachKey() {
+ for (i in 0..48) {
+ val map = MutablePKeyObjectMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toPKey()] = j.toString()
+ }
+
+ var counter = 0
+ map.forEachKey { key ->
+ assertEquals(key.toInt().toString(), map[key])
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachValue() {
+ for (i in 0..48) {
+ val map = MutablePKeyObjectMap<String>()
+
+ for (j in 0 until i) {
+ map[j.toPKey()] = j.toString()
+ }
+
+ var counter = 0
+ map.forEachValue { value ->
+ assertNotNull(value.toIntOrNull())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun clear() {
+ val map = MutablePKeyObjectMap<String>()
+
+ for (i in 0 until 32) {
+ map[i.toPKey()] = i.toString()
+ }
+
+ val capacity = map.capacity
+ map.clear()
+
+ assertEquals(0, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun string() {
+ val map = MutablePKeyObjectMap<String?>()
+ assertEquals("{}", map.toString())
+
+ map[1KeySuffix] = "World"
+ map[2KeySuffix] = "Monde"
+ val oneKey = 1KeySuffix.toString()
+ val twoKey = 2KeySuffix.toString()
+ assertTrue(
+ "{$oneKey=World, $twoKey=Monde}" == map.toString() ||
+ "{$twoKey=Monde, $oneKey=World}" == map.toString()
+ )
+
+ map.clear()
+ map[1KeySuffix] = null
+ assertEquals("{$oneKey=null}", map.toString())
+
+ val selfAsValueMap = MutablePKeyObjectMap<Any>()
+ selfAsValueMap[1KeySuffix] = selfAsValueMap
+ assertEquals("{$oneKey=(this)}", selfAsValueMap.toString())
+ }
+
+ @Test
+ fun equals() {
+ val map = MutablePKeyObjectMap<String?>()
+ map[1KeySuffix] = "World"
+ map[2KeySuffix] = null
+
+ assertFalse(map.equals(null))
+ assertEquals(map, map)
+
+ val map2 = MutablePKeyObjectMap<String?>()
+ map2[2KeySuffix] = null
+
+ assertNotEquals(map, map2)
+
+ map2[1KeySuffix] = "World"
+ assertEquals(map, map2)
+ }
+
+ @Test
+ fun containsKey() {
+ val map = MutablePKeyObjectMap<String?>()
+ map[1KeySuffix] = "World"
+ map[2KeySuffix] = null
+
+ assertTrue(map.containsKey(1KeySuffix))
+ assertFalse(map.containsKey(3KeySuffix))
+ }
+
+ @Test
+ fun contains() {
+ val map = MutablePKeyObjectMap<String?>()
+ map[1KeySuffix] = "World"
+ map[2KeySuffix] = null
+
+ assertTrue(1KeySuffix in map)
+ assertFalse(3KeySuffix in map)
+ }
+
+ @Test
+ fun containsValue() {
+ val map = MutablePKeyObjectMap<String?>()
+ map[1KeySuffix] = "World"
+ map[2KeySuffix] = null
+
+ assertTrue(map.containsValue("World"))
+ assertTrue(map.containsValue(null))
+ assertFalse(map.containsValue("Monde"))
+ }
+
+ @Test
+ fun empty() {
+ val map = MutablePKeyObjectMap<String?>()
+ assertTrue(map.isEmpty())
+ assertFalse(map.isNotEmpty())
+ assertTrue(map.none())
+ assertFalse(map.any())
+
+ map[1KeySuffix] = "World"
+
+ assertFalse(map.isEmpty())
+ assertTrue(map.isNotEmpty())
+ assertTrue(map.any())
+ assertFalse(map.none())
+ }
+
+ @Test
+ fun count() {
+ val map = MutablePKeyObjectMap<String>()
+ assertEquals(0, map.count())
+
+ map[1KeySuffix] = "World"
+ assertEquals(1, map.count())
+
+ map[2KeySuffix] = "Monde"
+ map[3KeySuffix] = "Welt"
+ map[4KeySuffix] = "Sekai"
+ map[5KeySuffix] = "Mondo"
+ map[6KeySuffix] = "Sesang"
+
+ assertEquals(2, map.count { key, _ -> key < 3KeySuffix })
+ assertEquals(0, map.count { key, _ -> key < 0KeySuffix })
+ }
+
+ @Test
+ fun any() {
+ val map = MutablePKeyObjectMap<String>()
+ map[1KeySuffix] = "World"
+ map[2KeySuffix] = "Monde"
+ map[3KeySuffix] = "Welt"
+ map[4KeySuffix] = "Sekai"
+ map[5KeySuffix] = "Mondo"
+ map[6KeySuffix] = "Sesang"
+
+ assertTrue(map.any { key, _ -> key > 5KeySuffix })
+ assertFalse(map.any { key, _ -> key < 0KeySuffix })
+ }
+
+ @Test
+ fun all() {
+ val map = MutablePKeyObjectMap<String>()
+ map[1KeySuffix] = "World"
+ map[2KeySuffix] = "Monde"
+ map[3KeySuffix] = "Welt"
+ map[4KeySuffix] = "Sekai"
+ map[5KeySuffix] = "Mondo"
+ map[6KeySuffix] = "Sesang"
+
+ assertTrue(map.all { key, value -> key < 7KeySuffix && value.length > 0 })
+ assertFalse(map.all { key, _ -> key < 6KeySuffix })
+ }
+}
diff --git a/collection/collection/template/PKeyPValueMap.kt.template b/collection/collection/template/PKeyPValueMap.kt.template
new file mode 100644
index 0000000..0d0a6fe
--- /dev/null
+++ b/collection/collection/template/PKeyPValueMap.kt.template
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyPKeyPValueMap = MutablePKeyPValueMap(0)
+
+/**
+ * Returns an empty, read-only [PKeyPValueMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyPKeyPValueMap(): PKeyPValueMap = EmptyPKeyPValueMap
+
+/**
+ * Returns a new [MutablePKeyPValueMap].
+ */
+public fun pKeyPValueMapOf(): PKeyPValueMap = EmptyPKeyPValueMap
+
+/**
+ * Returns a new [PKeyPValueMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [PKey] key and [PValue] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun pKeyPValueMapOf(vararg pairs: Pair<PKey, PValue>): PKeyPValueMap =
+ MutablePKeyPValueMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * Returns a new [MutablePKeyPValueMap].
+ */
+public fun mutablePKeyPValueMapOf(): MutablePKeyPValueMap = MutablePKeyPValueMap()
+
+/**
+ * Returns a new [MutablePKeyPValueMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [PKey] key and [PValue] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutablePKeyPValueMapOf(vararg pairs: Pair<PKey, PValue>): MutablePKeyPValueMap =
+ MutablePKeyPValueMap(pairs.size).also { map ->
+ pairs.forEach { (key, value) -> map[key] = value }
+ }
+
+/**
+ * [PKeyPValueMap] is a container with a [Map]-like interface for
+ * [PKey] primitive keys and [PValue] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutablePKeyPValueMap].
+ *
+ * @see [MutablePKeyPValueMap]
+ * @see [ScatterMap]
+ */
+public sealed class PKeyPValueMap {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` entries, including when
+ // the table is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var keys: PKeyArray = EmptyPKeyArray
+
+ @PublishedApi
+ @JvmField
+ internal var values: PValueArray = EmptyPValueArray
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of key-value pairs that can be stored in this map
+ * without requiring internal storage reallocation.
+ */
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @Suppress("PropertyName")
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of key-value pairs in this map.
+ */
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this map has at least one entry.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this map has no entries.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this map is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this map is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the value corresponding to the given [key].
+ * @throws NoSuchElementException if [key] is not in the map
+ */
+ public operator fun get(key: PKey): PValue {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ throw NoSuchElementException("Cannot find value for key $key")
+ }
+ return values[index]
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * or [defaultValue] if this map contains no mapping for the key.
+ */
+ public fun getOrDefault(key: PKey, defaultValue: PValue): PValue {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ return values[index]
+ }
+ return defaultValue
+ }
+
+ /**
+ * Returns the value for the given [key] if the value is present
+ * and not null. Otherwise, returns the result of the [defaultValue]
+ * function.
+ */
+ public inline fun getOrElse(key: PKey, defaultValue: () -> PValue): PValue {
+ val index = findKeyIndex(key)
+ if (index < 0) {
+ return defaultValue()
+ }
+ return values[index]
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every key/value pair stored in this map by invoking
+ * the specified [block] lambda.
+ */
+ public inline fun forEach(block: (key: PKey, value: PValue) -> Unit) {
+ val k = keys
+ val v = values
+
+ forEachIndexed { index ->
+ block(k[index], v[index])
+ }
+ }
+
+ /**
+ * Iterates over every key stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachKey(block: (key: PKey) -> Unit) {
+ val k = keys
+
+ forEachIndexed { index ->
+ block(k[index])
+ }
+ }
+
+ /**
+ * Iterates over every value stored in this map by invoking the specified
+ * [block] lambda.
+ */
+ public inline fun forEachValue(block: (value: PValue) -> Unit) {
+ val v = values
+
+ forEachIndexed { index ->
+ block(v[index])
+ }
+ }
+
+ /**
+ * Returns true if all entries match the given [predicate].
+ */
+ public inline fun all(predicate: (PKey, PValue) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (!predicate(key, value)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one entry matches the given [predicate].
+ */
+ public inline fun any(predicate: (PKey, PValue) -> Boolean): Boolean {
+ forEach { key, value ->
+ if (predicate(key, value)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of entries in this map.
+ */
+ public fun count(): Int = size
+
+ /**
+ * Returns the number of entries matching the given [predicate].
+ */
+ public inline fun count(predicate: (PKey, PValue) -> Boolean): Int {
+ var count = 0
+ forEach { key, value ->
+ if (predicate(key, value)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public operator fun contains(key: PKey): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [key] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsKey(key: PKey): Boolean = findKeyIndex(key) >= 0
+
+ /**
+ * Returns true if the specified [value] is present in this hash map, false
+ * otherwise.
+ */
+ public fun containsValue(value: PValue): Boolean {
+ forEachValue { v ->
+ if (value == v) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the hash code value for this map. The hash code the sum of the hash
+ * codes of each key/value pair.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { key, value ->
+ hash += key.hashCode() xor value.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this hash map for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [PKeyPValueMap]
+ * - Has the same [size] as this map
+ * - Contains key/value pairs equal to this map's pair
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is PKeyPValueMap) {
+ return false
+ }
+ if (other.size != size) {
+ return false
+ }
+
+ forEach { key, value ->
+ if (value != other[key]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this map. The map is denoted in the
+ * string by the `{}`. Each key/value pair present in the map is represented
+ * inside '{}` by a substring of the form `key=value`, and pairs are
+ * separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ val s = StringBuilder().append('{')
+ var i = 0
+ forEach { key, value ->
+ s.append(key)
+ s.append("=")
+ s.append(value)
+ i++
+ if (i < _size) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append('}').toString()
+ }
+
+ /**
+ * Scans the hash table to find the index in the backing arrays of the
+ * specified [key]. Returns -1 if the key is not present.
+ */
+ @PublishedApi
+ internal fun findKeyIndex(key: PKey): Int {
+ val hash = hash(key)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutablePKeyPValueMap] is a container with a [MutableMap]-like interface for
+ * [PKey] primitive keys and [PValue] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutablePKeyPValueMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutablePKeyPValueMap(
+ initialCapacity: Int = DefaultScatterCapacity
+) : PKeyPValueMap() {
+ // Number of entries we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ keys = PKeyArray(newCapacity)
+ values = PValueArray(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Returns the value to which the specified [key] is mapped,
+ * if the value is present in the map and not `null`. Otherwise,
+ * calls `defaultValue()` and puts the result in the map associated
+ * with [key].
+ */
+ public inline fun getOrPut(key: PKey, defaultValue: () -> PValue): PValue {
+ val index = findKeyIndex(key)
+ return if (index < 0) {
+ val defValue = defaultValue()
+ put(key, defValue)
+ defValue
+ } else {
+ values[index]
+ }
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations.
+ */
+ public operator fun set(key: PKey, value: PValue) {
+ val index = findAbsoluteInsertIndex(key)
+ keys[index] = key
+ values[index] = value
+ }
+
+ /**
+ * Creates a new mapping from [key] to [value] in this map. If [key] is
+ * already present in the map, the association is modified and the previously
+ * associated value is replaced with [value]. If [key] is not present, a new
+ * entry is added to the map, which may require to grow the underlying storage
+ * and cause allocations. Return the previous value associated with the [key],
+ * or `null` if the key was not present in the map.
+ */
+ public fun put(key: PKey, value: PValue) {
+ set(key, value)
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [PKey] key and [PValue] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<PKey, PValue>>) {
+ for ((key, value) in pairs) {
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public fun putAll(from: PKeyPValueMap) {
+ from.forEach { key, value ->
+ this[key] = value
+ }
+ }
+
+ /**
+ * Puts the key/value mapping from the [pair] in this map, using the first
+ * element as the key, and the second element as the value.
+ *
+ * Note that [pair] allocated and both the [PKey] key and [PValue] value are
+ * boxed. Use [set] instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(pair: Pair<PKey, PValue>) {
+ this[pair.first] = pair.second
+ }
+
+ /**
+ * Puts all the [pairs] into this map, using the first component of the pair
+ * as the key, and the second component as the value.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [PKey] key and [PValue] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+ public inline operator fun plusAssign(
+ @Suppress("ArrayReturn") pairs: Array<Pair<PKey, PValue>>
+ ): Unit = putAll(pairs)
+
+ /**
+ * Puts all the key/value mappings in the [from] map into this map.
+ */
+ public inline operator fun plusAssign(from: PKeyPValueMap): Unit = putAll(from)
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public fun remove(key: PKey) {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ removeValueAt(index)
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map if the
+ * associated value equals [value]. Returns whether the removal happened.
+ */
+ public fun remove(key: PKey, value: PValue): Boolean {
+ val index = findKeyIndex(key)
+ if (index >= 0) {
+ if (values[index] == value) {
+ removeValueAt(index)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes any mapping for which the specified [predicate] returns true.
+ */
+ public fun removeIf(predicate: (PKey, PValue) -> Boolean) {
+ forEachIndexed { index ->
+ if (predicate(keys[index], values[index])) {
+ removeValueAt(index)
+ }
+ }
+ }
+
+ /**
+ * Removes the specified [key] and its associated value from the map.
+ */
+ public inline operator fun minusAssign(key: PKey) {
+ remove(key)
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: PKeyArray) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: PKeySet) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ /**
+ * Removes the specified [keys] and their associated value from the map.
+ */
+ public inline operator fun minusAssign(keys: PKeyList) {
+ keys.forEach { key ->
+ remove(key)
+ }
+ }
+
+ private fun removeValueAt(index: Int) {
+ _size -= 1
+
+ // TODO: We could just mark the entry as empty if there's a group
+ // window around this entry that was already empty
+ writeMetadata(index, Deleted)
+ }
+
+ /**
+ * Removes all mappings from this map.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the hash table to find the index at which we can store a value
+ * for the give [key]. If the key already exists in the table, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the table is full.
+ */
+ private fun findAbsoluteInsertIndex(key: PKey): Int {
+ val hash = hash(key)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (keys[index] == key) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the table in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutablePKeyPValueMap]'s storage so it is sized appropriately
+ * to hold the current mappings.
+ *
+ * Returns the number of empty entries removed from this map's storage.
+ * Returns be 0 if no trimming is necessary or possible.
+ */
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted entries from the table to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_set`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousKeys = keys
+ val previousValues = values
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newKeys = keys
+ val newValues = values
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousKey = previousKeys[i]
+ val hash = hash(previousKey)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newKeys[index] = previousKey
+ newValues[index] = previousValues[i]
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
diff --git a/collection/collection/template/PKeyPValueMapTest.kt.template b/collection/collection/template/PKeyPValueMapTest.kt.template
new file mode 100644
index 0000000..c8f3cd5
--- /dev/null
+++ b/collection/collection/template/PKeyPValueMapTest.kt.template
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class PKeyPValueMapTest {
+ @Test
+ fun pKeyPValueMap() {
+ val map = MutablePKeyPValueMap()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun testEmptyPKeyPValueMap() {
+ val map = emptyPKeyPValueMap()
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+
+ assertSame(emptyPKeyPValueMap(), map)
+ }
+
+ @Test
+ fun pKeyPValueMapFunction() {
+ val map = mutablePKeyPValueMapOf()
+ assertEquals(7, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun zeroCapacityHashMap() {
+ val map = MutablePKeyPValueMap(0)
+ assertEquals(0, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun pKeyPValueMapWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val map = MutablePKeyPValueMap(1800)
+ assertEquals(4095, map.capacity)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun pKeyPValueMapPairsFunction() {
+ val map = mutablePKeyPValueMapOf(
+ 1KeySuffix to 1ValueSuffix,
+ 2KeySuffix to 2ValueSuffix
+ )
+ assertEquals(2, map.size)
+ assertEquals(1ValueSuffix, map[1KeySuffix])
+ assertEquals(2ValueSuffix, map[2KeySuffix])
+ }
+
+ @Test
+ fun addToMap() {
+ val map = MutablePKeyPValueMap()
+ map[1KeySuffix] = 1ValueSuffix
+
+ assertEquals(1, map.size)
+ assertEquals(1ValueSuffix, map[1KeySuffix])
+ }
+
+ @Test
+ fun addToSizedMap() {
+ val map = MutablePKeyPValueMap(12)
+ map[1KeySuffix] = 1ValueSuffix
+
+ assertEquals(1, map.size)
+ assertEquals(1ValueSuffix, map[1KeySuffix])
+ }
+
+ @Test
+ fun addToSmallMap() {
+ val map = MutablePKeyPValueMap(2)
+ map[1KeySuffix] = 1ValueSuffix
+
+ assertEquals(1, map.size)
+ assertEquals(7, map.capacity)
+ assertEquals(1ValueSuffix, map[1KeySuffix])
+ }
+
+ @Test
+ fun addToZeroCapacityMap() {
+ val map = MutablePKeyPValueMap(0)
+ map[1KeySuffix] = 1ValueSuffix
+
+ assertEquals(1, map.size)
+ assertEquals(1ValueSuffix, map[1KeySuffix])
+ }
+
+ @Test
+ fun replaceExistingKey() {
+ val map = MutablePKeyPValueMap()
+ map[1KeySuffix] = 1ValueSuffix
+ map[1KeySuffix] = 2ValueSuffix
+
+ assertEquals(1, map.size)
+ assertEquals(2ValueSuffix, map[1KeySuffix])
+ }
+
+ @Test
+ fun put() {
+ val map = MutablePKeyPValueMap()
+
+ map.put(1KeySuffix, 1ValueSuffix)
+ assertEquals(1ValueSuffix, map[1KeySuffix])
+ map.put(1KeySuffix, 2ValueSuffix)
+ assertEquals(2ValueSuffix, map[1KeySuffix])
+ }
+
+ @Test
+ fun putAllArray() {
+ val map = MutablePKeyPValueMap()
+ map[1KeySuffix] = 1ValueSuffix
+ map[2KeySuffix] = 2ValueSuffix
+
+ map.putAll(arrayOf(3KeySuffix to 3ValueSuffix, 7KeySuffix to 7ValueSuffix))
+
+ assertEquals(4, map.size)
+ assertEquals(3ValueSuffix, map[3KeySuffix])
+ assertEquals(7ValueSuffix, map[7KeySuffix])
+ }
+
+ @Test
+ fun plus() {
+ val map = MutablePKeyPValueMap()
+ map += 1KeySuffix to 1ValueSuffix
+
+ assertEquals(1, map.size)
+ assertEquals(1ValueSuffix, map[1KeySuffix])
+ }
+
+ @Test
+ fun plusArray() {
+ val map = MutablePKeyPValueMap()
+ map += arrayOf(3KeySuffix to 3ValueSuffix, 7KeySuffix to 7ValueSuffix)
+
+ assertEquals(2, map.size)
+ assertEquals(3ValueSuffix, map[3KeySuffix])
+ assertEquals(7ValueSuffix, map[7KeySuffix])
+ }
+
+ @Test
+ fun findNonExistingKey() {
+ val map = MutablePKeyPValueMap()
+ map[1KeySuffix] = 1ValueSuffix
+
+ assertFailsWith<NoSuchElementException> {
+ map[2KeySuffix]
+ }
+ }
+
+ @Test
+ fun getOrDefault() {
+ val map = MutablePKeyPValueMap()
+ map[1KeySuffix] = 1ValueSuffix
+
+ assertEquals(2ValueSuffix, map.getOrDefault(2KeySuffix, 2ValueSuffix))
+ }
+
+ @Test
+ fun getOrElse() {
+ val map = MutablePKeyPValueMap()
+ map[1KeySuffix] = 1ValueSuffix
+
+ assertEquals(3ValueSuffix, map.getOrElse(3KeySuffix) { 3ValueSuffix })
+ }
+
+ @Test
+ fun getOrPut() {
+ val map = MutablePKeyPValueMap()
+ map[1KeySuffix] = 1ValueSuffix
+
+ var counter = 0
+ map.getOrPut(1KeySuffix) {
+ counter++
+ 2ValueSuffix
+ }
+ assertEquals(1ValueSuffix, map[1KeySuffix])
+ assertEquals(0, counter)
+
+ map.getOrPut(2KeySuffix) {
+ counter++
+ 2ValueSuffix
+ }
+ assertEquals(2ValueSuffix, map[2KeySuffix])
+ assertEquals(1, counter)
+
+ map.getOrPut(2KeySuffix) {
+ counter++
+ 3ValueSuffix
+ }
+ assertEquals(2ValueSuffix, map[2KeySuffix])
+ assertEquals(1, counter)
+
+ map.getOrPut(3KeySuffix) {
+ counter++
+ 3ValueSuffix
+ }
+ assertEquals(3ValueSuffix, map[3KeySuffix])
+ assertEquals(2, counter)
+ }
+
+ @Test
+ fun remove() {
+ val map = MutablePKeyPValueMap()
+ map.remove(1KeySuffix)
+
+ map[1KeySuffix] = 1ValueSuffix
+ map.remove(1KeySuffix)
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val map = MutablePKeyPValueMap(6)
+ map[1KeySuffix] = 1ValueSuffix
+ map[2KeySuffix] = 2ValueSuffix
+ map[3KeySuffix] = 3ValueSuffix
+ map[4KeySuffix] = 4ValueSuffix
+ map[5KeySuffix] = 5ValueSuffix
+ map[6KeySuffix] = 6ValueSuffix
+
+ // Removing all the entries will mark the medata as deleted
+ map.remove(1KeySuffix)
+ map.remove(2KeySuffix)
+ map.remove(3KeySuffix)
+ map.remove(4KeySuffix)
+ map.remove(5KeySuffix)
+ map.remove(6KeySuffix)
+
+ assertEquals(0, map.size)
+
+ val capacity = map.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ map[7KeySuffix] = 7ValueSuffix
+
+ assertEquals(1, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun removeIf() {
+ val map = MutablePKeyPValueMap()
+ map[1KeySuffix] = 1ValueSuffix
+ map[2KeySuffix] = 2ValueSuffix
+ map[3KeySuffix] = 3ValueSuffix
+ map[4KeySuffix] = 4ValueSuffix
+ map[5KeySuffix] = 5ValueSuffix
+ map[6KeySuffix] = 6ValueSuffix
+
+ map.removeIf { key, _ -> key == 1KeySuffix || key == 3KeySuffix }
+
+ assertEquals(4, map.size)
+ assertEquals(2ValueSuffix, map[2KeySuffix])
+ assertEquals(4ValueSuffix, map[4KeySuffix])
+ assertEquals(5ValueSuffix, map[5KeySuffix])
+ assertEquals(6ValueSuffix, map[6KeySuffix])
+ }
+
+ @Test
+ fun minus() {
+ val map = MutablePKeyPValueMap()
+ map[1KeySuffix] = 1ValueSuffix
+ map[2KeySuffix] = 2ValueSuffix
+ map[3KeySuffix] = 3ValueSuffix
+
+ map -= 1KeySuffix
+
+ assertEquals(2, map.size)
+ assertFalse(1KeySuffix in map)
+ }
+
+ @Test
+ fun minusArray() {
+ val map = MutablePKeyPValueMap()
+ map[1KeySuffix] = 1ValueSuffix
+ map[2KeySuffix] = 2ValueSuffix
+ map[3KeySuffix] = 3ValueSuffix
+
+ map -= pKeyArrayOf(3KeySuffix, 2KeySuffix)
+
+ assertEquals(1, map.size)
+ assertFalse(3KeySuffix in map)
+ assertFalse(2KeySuffix in map)
+ }
+
+ @Test
+ fun minusSet() {
+ val map = MutablePKeyPValueMap()
+ map[1KeySuffix] = 1ValueSuffix
+ map[2KeySuffix] = 2ValueSuffix
+ map[3KeySuffix] = 3ValueSuffix
+
+ map -= pKeySetOf(3KeySuffix, 2KeySuffix)
+
+ assertEquals(1, map.size)
+ assertFalse(3KeySuffix in map)
+ assertFalse(2KeySuffix in map)
+ }
+
+ @Test
+ fun minusList() {
+ val map = MutablePKeyPValueMap()
+ map[1KeySuffix] = 1ValueSuffix
+ map[2KeySuffix] = 2ValueSuffix
+ map[3KeySuffix] = 3ValueSuffix
+
+ map -= pKeyListOf(3KeySuffix, 2KeySuffix)
+
+ assertEquals(1, map.size)
+ assertFalse(3KeySuffix in map)
+ assertFalse(2KeySuffix in map)
+ }
+
+ @Test
+ fun conditionalRemove() {
+ val map = MutablePKeyPValueMap()
+ assertFalse(map.remove(1KeySuffix, 1ValueSuffix))
+
+ map[1KeySuffix] = 1ValueSuffix
+ assertTrue(map.remove(1KeySuffix, 1ValueSuffix))
+ assertEquals(0, map.size)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val map = MutablePKeyPValueMap()
+
+ for (i in 0 until 1700) {
+ map[i.toPKey()] = i.toPValue()
+ }
+
+ assertEquals(1700, map.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val map = MutablePKeyPValueMap()
+
+ for (j in 0 until i) {
+ map[j.toPKey()] = j.toPValue()
+ }
+
+ var counter = 0
+ map.forEach { key, value ->
+ assertEquals(key, value.toPKey())
+ counter++
+ }
+
+ assertEquals(i, counter)
+ }
+ }
+
+ @Test
+ fun forEachKey() {
+ for (i in 0..48) {
+ val map = MutablePKeyPValueMap()
+
+ for (j in 0 until i) {
+ map[j.toPKey()] = j.toPValue()
+ }
+
+ var counter = 0
+ val keys = BooleanArray(map.size)
+ map.forEachKey { key ->
+ keys[key.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ keys.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun forEachValue() {
+ for (i in 0..48) {
+ val map = MutablePKeyPValueMap()
+
+ for (j in 0 until i) {
+ map[j.toPKey()] = j.toPValue()
+ }
+
+ var counter = 0
+ val values = BooleanArray(map.size)
+ map.forEachValue { value ->
+ values[value.toInt()] = true
+ counter++
+ }
+
+ assertEquals(i, counter)
+ values.forEach { assertTrue(it) }
+ }
+ }
+
+ @Test
+ fun clear() {
+ val map = MutablePKeyPValueMap()
+
+ for (i in 0 until 32) {
+ map[i.toPKey()] = i.toPValue()
+ }
+
+ val capacity = map.capacity
+ map.clear()
+
+ assertEquals(0, map.size)
+ assertEquals(capacity, map.capacity)
+ }
+
+ @Test
+ fun string() {
+ val map = MutablePKeyPValueMap()
+ assertEquals("{}", map.toString())
+
+ map[1KeySuffix] = 1ValueSuffix
+ map[2KeySuffix] = 2ValueSuffix
+ val oneValueString = 1ValueSuffix.toString()
+ val twoValueString = 2ValueSuffix.toString()
+ val oneKeyString = 1KeySuffix.toString()
+ val twoKeyString = 2KeySuffix.toString()
+ assertTrue(
+ "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+ "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+ )
+ }
+
+ @Test
+ fun equals() {
+ val map = MutablePKeyPValueMap()
+ map[1KeySuffix] = 1ValueSuffix
+
+ assertFalse(map.equals(null))
+ assertEquals(map, map)
+
+ val map2 = MutablePKeyPValueMap()
+ assertNotEquals(map, map2)
+
+ map2[1KeySuffix] = 1ValueSuffix
+ assertEquals(map, map2)
+ }
+
+ @Test
+ fun containsKey() {
+ val map = MutablePKeyPValueMap()
+ map[1KeySuffix] = 1ValueSuffix
+
+ assertTrue(map.containsKey(1KeySuffix))
+ assertFalse(map.containsKey(2KeySuffix))
+ }
+
+ @Test
+ fun contains() {
+ val map = MutablePKeyPValueMap()
+ map[1KeySuffix] = 1ValueSuffix
+
+ assertTrue(1KeySuffix in map)
+ assertFalse(2KeySuffix in map)
+ }
+
+ @Test
+ fun containsValue() {
+ val map = MutablePKeyPValueMap()
+ map[1KeySuffix] = 1ValueSuffix
+
+ assertTrue(map.containsValue(1ValueSuffix))
+ assertFalse(map.containsValue(3ValueSuffix))
+ }
+
+ @Test
+ fun empty() {
+ val map = MutablePKeyPValueMap()
+ assertTrue(map.isEmpty())
+ assertFalse(map.isNotEmpty())
+ assertTrue(map.none())
+ assertFalse(map.any())
+
+ map[1KeySuffix] = 1ValueSuffix
+
+ assertFalse(map.isEmpty())
+ assertTrue(map.isNotEmpty())
+ assertTrue(map.any())
+ assertFalse(map.none())
+ }
+
+ @Test
+ fun count() {
+ val map = MutablePKeyPValueMap()
+ assertEquals(0, map.count())
+
+ map[1KeySuffix] = 1ValueSuffix
+ assertEquals(1, map.count())
+
+ map[2KeySuffix] = 2ValueSuffix
+ map[3KeySuffix] = 3ValueSuffix
+ map[4KeySuffix] = 4ValueSuffix
+ map[5KeySuffix] = 5ValueSuffix
+ map[6KeySuffix] = 6ValueSuffix
+
+ assertEquals(2, map.count { key, _ -> key <= 2KeySuffix })
+ assertEquals(0, map.count { key, _ -> key < 0KeySuffix })
+ }
+
+ @Test
+ fun any() {
+ val map = MutablePKeyPValueMap()
+ map[1KeySuffix] = 1ValueSuffix
+ map[2KeySuffix] = 2ValueSuffix
+ map[3KeySuffix] = 3ValueSuffix
+ map[4KeySuffix] = 4ValueSuffix
+ map[5KeySuffix] = 5ValueSuffix
+ map[6KeySuffix] = 6ValueSuffix
+
+ assertTrue(map.any { key, _ -> key == 4KeySuffix })
+ assertFalse(map.any { key, _ -> key < 0KeySuffix })
+ }
+
+ @Test
+ fun all() {
+ val map = MutablePKeyPValueMap()
+ map[1KeySuffix] = 1ValueSuffix
+ map[2KeySuffix] = 2ValueSuffix
+ map[3KeySuffix] = 3ValueSuffix
+ map[4KeySuffix] = 4ValueSuffix
+ map[5KeySuffix] = 5ValueSuffix
+ map[6KeySuffix] = 6ValueSuffix
+
+ assertTrue(map.all { key, value -> key > 0KeySuffix && value >= 1ValueSuffix })
+ assertFalse(map.all { key, _ -> key < 6KeySuffix })
+ }
+
+ @Test
+ fun trim() {
+ val map = MutablePKeyPValueMap()
+ assertEquals(7, map.trim())
+
+ map[1KeySuffix] = 1ValueSuffix
+ map[3KeySuffix] = 3ValueSuffix
+
+ assertEquals(0, map.trim())
+
+ for (i in 0 until 1700) {
+ map[i.toPKey()] = i.toPValue()
+ }
+
+ assertEquals(2047, map.capacity)
+
+ // After removing these items, our capacity needs should go
+ // from 2047 down to 1023
+ for (i in 0 until 1700) {
+ if (i and 0x1 == 0x0) {
+ val s = i.toPKey()
+ map.remove(s)
+ }
+ }
+
+ assertEquals(1024, map.trim())
+ assertEquals(0, map.trim())
+ }
+}
diff --git a/collection/collection/template/PKeySet.kt.template b/collection/collection/template/PKeySet.kt.template
new file mode 100644
index 0000000..f950f03
--- /dev/null
+++ b/collection/collection/template/PKeySet.kt.template
@@ -0,0 +1,784 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "RedundantVisibilityModifier",
+ "KotlinRedundantDiagnosticSuppress",
+ "KotlinConstantConditions",
+ "PropertyName",
+ "ConstPropertyName",
+ "PrivatePropertyName",
+ "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.contracts.contract
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// This is a copy of ScatterSet, but with primitive elements
+
+// Default empty set to avoid allocations
+private val EmptyPKeySet = MutablePKeySet(0)
+
+// An empty array of pKeys
+internal val EmptyPKeyArray = PKeyArray(0)
+
+/**
+ * Returns an empty, read-only [PKeySet].
+ */
+public fun emptyPKeySet(): PKeySet = EmptyPKeySet
+
+/**
+ * Returns an empty, read-only [ScatterSet].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun pKeySetOf(): PKeySet = EmptyPKeySet
+
+/**
+ * Returns a new read-only [PKeySet] with only [element1] in it.
+ */
+@Suppress("UNCHECKED_CAST")
+public fun pKeySetOf(element1: PKey): PKeySet = mutablePKeySetOf(element1)
+
+/**
+ * Returns a new read-only [PKeySet] with only [element1] and [element2] in it.
+ */
+@Suppress("UNCHECKED_CAST")
+public fun pKeySetOf(element1: PKey, element2: PKey): PKeySet =
+ mutablePKeySetOf(element1, element2)
+
+/**
+ * Returns a new read-only [PKeySet] with only [element1], [element2], and [element3] in it.
+ */
+@Suppress("UNCHECKED_CAST")
+public fun pKeySetOf(element1: PKey, element2: PKey, element3: PKey): PKeySet =
+ mutablePKeySetOf(element1, element2, element3)
+
+/**
+ * Returns a new read-only [PKeySet] with only [elements] in it.
+ */
+@Suppress("UNCHECKED_CAST")
+public fun pKeySetOf(vararg elements: PKey): PKeySet =
+ MutablePKeySet(elements.size).apply { plusAssign(elements) }
+
+/**
+ * Returns a new [MutablePKeySet].
+ */
+public fun mutablePKeySetOf(): MutablePKeySet = MutablePKeySet()
+
+/**
+ * Returns a new [MutablePKeySet] with only [element1] in it.
+ */
+public fun mutablePKeySetOf(element1: PKey): MutablePKeySet =
+ MutablePKeySet(1).apply {
+ plusAssign(element1)
+ }
+
+/**
+ * Returns a new [MutablePKeySet] with only [element1] and [element2] in it.
+ */
+public fun mutablePKeySetOf(element1: PKey, element2: PKey): MutablePKeySet =
+ MutablePKeySet(2).apply {
+ plusAssign(element1)
+ plusAssign(element2)
+ }
+
+/**
+ * Returns a new [MutablePKeySet] with only [element1], [element2], and [element3] in it.
+ */
+public fun mutablePKeySetOf(element1: PKey, element2: PKey, element3: PKey): MutablePKeySet =
+ MutablePKeySet(3).apply {
+ plusAssign(element1)
+ plusAssign(element2)
+ plusAssign(element3)
+ }
+
+/**
+ * Returns a new [MutablePKeySet] with the specified elements.
+ */
+public fun mutablePKeySetOf(vararg elements: PKey): MutablePKeySet =
+ MutablePKeySet(elements.size).apply { plusAssign(elements) }
+
+/**
+ * [PKeySet] is a container with a [Set]-like interface designed to avoid
+ * allocations, including boxing.
+ *
+ * This implementation makes no guarantee as to the order of the elements,
+ * nor does it make guarantees that the order remains constant over time.
+ *
+ * Though [PKeySet] offers a read-only interface, it is always backed
+ * by a [MutablePKeySet]. Read operations alone are thread-safe. However,
+ * any mutations done through the backing [MutablePKeySet] while reading
+ * on another thread are not safe and the developer must protect the set
+ * from such changes during read operations.
+ *
+ * @see [MutablePKeySet]
+ */
+public sealed class PKeySet {
+ // NOTE: Our arrays are marked internal to implement inlined forEach{}
+ // The backing array for the metadata bytes contains
+ // `capacity + 1 + ClonedMetadataCount` elements, including when
+ // the set is empty (see [EmptyGroup]).
+ @PublishedApi
+ @JvmField
+ internal var metadata: LongArray = EmptyGroup
+
+ @PublishedApi
+ @JvmField
+ internal var elements: PKeyArray = EmptyPKeyArray
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the capacity
+ @JvmField
+ internal var _capacity: Int = 0
+
+ /**
+ * Returns the number of elements that can be stored in this set
+ * without requiring internal storage reallocation.
+ */
+ @get:androidx.annotation.IntRange(from = 0)
+ public val capacity: Int
+ get() = _capacity
+
+ // We use a backing field for capacity to avoid invokevirtual calls
+ // every time we need to look at the size
+ @JvmField
+ internal var _size: Int = 0
+
+ /**
+ * Returns the number of elements in this set.
+ */
+ @get:androidx.annotation.IntRange(from = 0)
+ public val size: Int
+ get() = _size
+
+ /**
+ * Returns `true` if this set has at least one element.
+ */
+ public fun any(): Boolean = _size != 0
+
+ /**
+ * Returns `true` if this set has no elements.
+ */
+ public fun none(): Boolean = _size == 0
+
+ /**
+ * Indicates whether this set is empty.
+ */
+ public fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * Returns `true` if this set is not empty.
+ */
+ public fun isNotEmpty(): Boolean = _size != 0
+
+ /**
+ * Returns the first element in the collection.
+ * @throws NoSuchElementException if the collection is empty
+ */
+ public inline fun first(): PKey {
+ forEach { return it }
+ throw NoSuchElementException("The PKeySet is empty")
+ }
+
+ /**
+ * Returns the first element in the collection for which [predicate] returns `true`.
+ *
+ * **Note** There is no mechanism for both determining if there is an element that matches
+ * [predicate] _and_ returning it if it exists. Developers should use [forEach] to achieve
+ * this behavior.
+ *
+ * @param predicate Called on elements of the set, returning `true` for an element that matches
+ * or `false` if it doesn't
+ * @return An element in the set for which [predicate] returns `true`.
+ * @throws NoSuchElementException if [predicate] returns `false` for all elements or the
+ * collection is empty.
+ */
+ public inline fun first(predicate: (element: PKey) -> Boolean): PKey {
+ contract { callsInPlace(predicate) }
+ forEach { if (predicate(it)) return it }
+ throw NoSuchElementException("Could not find a match")
+ }
+
+ /**
+ * Iterates over every element stored in this set by invoking
+ * the specified [block] lambda.
+ */
+ @PublishedApi
+ internal inline fun forEachIndex(block: (index: Int) -> Unit) {
+ contract { callsInPlace(block) }
+ val m = metadata
+ val lastIndex = m.size - 2 // We always have 0 or at least 2 elements
+
+ for (i in 0..lastIndex) {
+ var slot = m[i]
+ if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+ // Branch-less if (i == lastIndex) 7 else 8
+ // i - lastIndex returns a negative value when i < lastIndex,
+ // so 1 is set as the MSB. By inverting and shifting we get
+ // 0 when i < lastIndex, 1 otherwise.
+ val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+ for (j in 0 until bitCount) {
+ if (isFull(slot and 0xFFL)) {
+ val index = (i shl 3) + j
+ block(index)
+ }
+ slot = slot shr 8
+ }
+ if (bitCount != 8) return
+ }
+ }
+ }
+
+ /**
+ * Iterates over every element stored in this set by invoking
+ * the specified [block] lambda.
+ * @param block called with each element in the set
+ */
+ public inline fun forEach(block: (element: PKey) -> Unit) {
+ contract { callsInPlace(block) }
+ val k = elements
+
+ forEachIndex { index ->
+ block(k[index])
+ }
+ }
+
+ /**
+ * Returns true if all elements match the given [predicate].
+ * @param predicate called for elements in the set to determine if it returns return `true` for
+ * all elements.
+ */
+ public inline fun all(predicate: (element: PKey) -> Boolean): Boolean {
+ contract { callsInPlace(predicate) }
+ forEach { element ->
+ if (!predicate(element)) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns true if at least one element matches the given [predicate].
+ * @param predicate called for elements in the set to determine if it returns `true` for any
+ * elements.
+ */
+ public inline fun any(predicate: (element: PKey) -> Boolean): Boolean {
+ contract { callsInPlace(predicate) }
+ forEach { element ->
+ if (predicate(element)) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the number of elements in this set.
+ */
+ @androidx.annotation.IntRange(from = 0)
+ public fun count(): Int = _size
+
+ /**
+ * Returns the number of elements matching the given [predicate].
+ * @param predicate Called for all elements in the set to count the number for which it returns
+ * `true`.
+ */
+ @androidx.annotation.IntRange(from = 0)
+ public inline fun count(predicate: (element: PKey) -> Boolean): Int {
+ contract { callsInPlace(predicate) }
+ var count = 0
+ forEach { element ->
+ if (predicate(element)) count++
+ }
+ return count
+ }
+
+ /**
+ * Returns `true` if the specified [element] is present in this set, `false`
+ * otherwise.
+ * @param element The element to look for in this set
+ */
+ public operator fun contains(element: PKey): Boolean = findElementIndex(element) >= 0
+
+ /**
+ * Returns the hash code value for this set. The hash code of a set is defined to be the
+ * sum of the hash codes of the elements in the set.
+ */
+ public override fun hashCode(): Int {
+ var hash = 0
+
+ forEach { element ->
+ hash += element.hashCode()
+ }
+
+ return hash
+ }
+
+ /**
+ * Compares the specified object [other] with this set for equality.
+ * The two objects are considered equal if [other]:
+ * - Is a [PKeySet]
+ * - Has the same [size] as this set
+ * - Contains elements equal to this set's elements
+ */
+ public override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is PKeySet) {
+ return false
+ }
+ if (other._size != _size) {
+ return false
+ }
+
+ forEach { element ->
+ if (element !in other) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns a string representation of this set. The set is denoted in the
+ * string by the `{}`. Each element is separated by `, `.
+ */
+ public override fun toString(): String {
+ if (isEmpty()) {
+ return "[]"
+ }
+
+ val s = StringBuilder().append('[')
+ val last = _size - 1
+ var index = 0
+ forEach { element ->
+ s.append(element)
+ if (index++ < last) {
+ s.append(',').append(' ')
+ }
+ }
+
+ return s.append(']').toString()
+ }
+
+ /**
+ * Scans the set to find the index in the backing arrays of the
+ * specified [element]. Returns -1 if the element is not present.
+ */
+ internal inline fun findElementIndex(element: PKey): Int {
+ val hash = hash(element)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = h1(hash) and probeMask
+ var probeIndex = 0
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (elements[index] == element) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ return -1
+ }
+}
+
+/**
+ * [MutablePKeySet] is a container with a [MutableSet]-like interface based on a flat
+ * hash table implementation. The underlying implementation is designed to avoid
+ * all allocations on insertion, removal, retrieval, and iteration. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added elements to the set.
+ *
+ * This implementation makes no guarantee as to the order of the elements stored,
+ * nor does it make guarantees that the order remains constant over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the set (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Concurrent reads are however safe.
+ *
+ * @constructor Creates a new [MutablePKeySet]
+ * @param initialCapacity The initial desired capacity for this container.
+ * The container will honor this value by guaranteeing its internal structures
+ * can hold that many elements without requiring any allocations. The initial
+ * capacity can be set to 0.
+ */
+public class MutablePKeySet(
+ initialCapacity: Int = DefaultScatterCapacity
+) : PKeySet() {
+ // Number of elements we can add before we need to grow
+ private var growthLimit = 0
+
+ init {
+ require(initialCapacity >= 0) { "Capacity must be a positive value." }
+ initializeStorage(unloadedCapacity(initialCapacity))
+ }
+
+ private fun initializeStorage(initialCapacity: Int) {
+ val newCapacity = if (initialCapacity > 0) {
+ // Since we use longs for storage, our capacity is never < 7, enforce
+ // it here. We do have a special case for 0 to create small empty maps
+ maxOf(7, normalizeCapacity(initialCapacity))
+ } else {
+ 0
+ }
+ _capacity = newCapacity
+ initializeMetadata(newCapacity)
+ elements = PKeyArray(newCapacity)
+ }
+
+ private fun initializeMetadata(capacity: Int) {
+ metadata = if (capacity == 0) {
+ EmptyGroup
+ } else {
+ // Round up to the next multiple of 8 and find how many longs we need
+ val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+ LongArray(size).apply {
+ fill(AllEmpty)
+ }
+ }
+ writeRawMetadata(metadata, capacity, Sentinel)
+ initializeGrowth()
+ }
+
+ private fun initializeGrowth() {
+ growthLimit = loadedCapacity(capacity) - _size
+ }
+
+ /**
+ * Adds the specified element to the set.
+ * @param element The element to add to the set.
+ * @return `true` if the element has been added or `false` if the element is already
+ * contained within the set.
+ */
+ public fun add(element: PKey): Boolean {
+ val oldSize = _size
+ val index = findAbsoluteInsertIndex(element)
+ elements[index] = element
+ return _size != oldSize
+ }
+
+ /**
+ * Adds the specified element to the set.
+ * @param element The element to add to the set.
+ */
+ public operator fun plusAssign(element: PKey) {
+ val index = findAbsoluteInsertIndex(element)
+ elements[index] = element
+ }
+
+ /**
+ * Adds all the [elements] into this set.
+ * @param elements An array of elements to add to the set.
+ * @return `true` if any of the specified elements were added to the collection,
+ * `false` if the collection was not modified.
+ */
+ public fun addAll(@Suppress("ArrayReturn") elements: PKeyArray): Boolean {
+ val oldSize = _size
+ plusAssign(elements)
+ return oldSize != _size
+ }
+
+ /**
+ * Adds all the [elements] into this set.
+ * @param elements An array of elements to add to the set.
+ */
+ public operator fun plusAssign(@Suppress("ArrayReturn") elements: PKeyArray) {
+ elements.forEach { element ->
+ plusAssign(element)
+ }
+ }
+
+ /**
+ * Adds all the elements in the [elements] set into this set.
+ * @param elements A [PKeySet] of elements to add to this set.
+ * @return `true` if any of the specified elements were added to the collection,
+ * `false` if the collection was not modified.
+ */
+ public fun addAll(elements: PKeySet): Boolean {
+ val oldSize = _size
+ plusAssign(elements)
+ return oldSize != _size
+ }
+
+ /**
+ * Adds all the elements in the [elements] set into this set.
+ * @param elements A [PKeySet] of elements to add to this set.
+ */
+ public operator fun plusAssign(elements: PKeySet) {
+ elements.forEach { element ->
+ plusAssign(element)
+ }
+ }
+
+ /**
+ * Removes the specified [element] from the set.
+ * @param element The element to remove from the set.
+ * @return `true` if the [element] was present in the set, or `false` if it wasn't
+ * present before removal.
+ */
+ public fun remove(element: PKey): Boolean {
+ val index = findElementIndex(element)
+ val exists = index >= 0
+ if (exists) {
+ removeElementAt(index)
+ }
+ return exists
+ }
+
+ /**
+ * Removes the specified [element] from the set if it is present.
+ * @param element The element to remove from the set.
+ */
+ public operator fun minusAssign(element: PKey) {
+ val index = findElementIndex(element)
+ if (index >= 0) {
+ removeElementAt(index)
+ }
+ }
+
+ /**
+ * Removes the specified [elements] from the set, if present.
+ * @param elements An array of elements to be removed from the set.
+ * @return `true` if the set was changed or `false` if none of the elements were present.
+ */
+ public fun removeAll(@Suppress("ArrayReturn") elements: PKeyArray): Boolean {
+ val oldSize = _size
+ minusAssign(elements)
+ return oldSize != _size
+ }
+
+ /**
+ * Removes the specified [elements] from the set, if present.
+ * @param elements An array of elements to be removed from the set.
+ */
+ public operator fun minusAssign(@Suppress("ArrayReturn") elements: PKeyArray) {
+ elements.forEach { element ->
+ minusAssign(element)
+ }
+ }
+
+ /**
+ * Removes the specified [elements] from the set, if present.
+ * @param elements An [PKeySet] of elements to be removed from the set.
+ * @return `true` if the set was changed or `false` if none of the elements were present.
+ */
+ public fun removeAll(elements: PKeySet): Boolean {
+ val oldSize = _size
+ minusAssign(elements)
+ return oldSize != _size
+ }
+
+ /**
+ * Removes the specified [elements] from the set, if present.
+ * @param elements An [PKeySet] of elements to be removed from the set.
+ */
+ public operator fun minusAssign(elements: PKeySet) {
+ elements.forEach { element ->
+ minusAssign(element)
+ }
+ }
+
+ private fun removeElementAt(index: Int) {
+ _size -= 1
+
+ // TODO: We could just mark the element as empty if there's a group
+ // window around this element that was already empty
+ writeMetadata(index, Deleted)
+ }
+
+ /**
+ * Removes all elements from this set.
+ */
+ public fun clear() {
+ _size = 0
+ if (metadata !== EmptyGroup) {
+ metadata.fill(AllEmpty)
+ writeRawMetadata(metadata, _capacity, Sentinel)
+ }
+ initializeGrowth()
+ }
+
+ /**
+ * Scans the set to find the index at which we can store a given [element].
+ * If the element already exists in the set, its index
+ * will be returned, otherwise the index of an empty slot will be returned.
+ * Calling this function may cause the internal storage to be reallocated
+ * if the set is full.
+ */
+ private fun findAbsoluteInsertIndex(element: PKey): Int {
+ val hash = hash(element)
+ val hash1 = h1(hash)
+ val hash2 = h2(hash)
+
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+
+ while (true) {
+ val g = group(metadata, probeOffset)
+ var m = g.match(hash2)
+ while (m.hasNext()) {
+ val index = (probeOffset + m.get()) and probeMask
+ if (elements[index] == element) {
+ return index
+ }
+ m = m.next()
+ }
+
+ if (g.maskEmpty() != 0L) {
+ break
+ }
+
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+
+ var index = findFirstAvailableSlot(hash1)
+ if (growthLimit == 0 && !isDeleted(metadata, index)) {
+ adjustStorage()
+ index = findFirstAvailableSlot(hash1)
+ }
+
+ _size += 1
+ growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+ writeMetadata(index, hash2.toLong())
+
+ return index
+ }
+
+ /**
+ * Finds the first empty or deleted slot in the set in which we can
+ * store a value without resizing the internal storage.
+ */
+ private fun findFirstAvailableSlot(hash1: Int): Int {
+ val probeMask = _capacity
+ var probeOffset = hash1 and probeMask
+ var probeIndex = 0
+ while (true) {
+ val g = group(metadata, probeOffset)
+ val m = g.maskEmptyOrDeleted()
+ if (m != 0L) {
+ return (probeOffset + m.lowestBitSet()) and probeMask
+ }
+ probeIndex += GroupWidth
+ probeOffset = (probeOffset + probeIndex) and probeMask
+ }
+ }
+
+ /**
+ * Trims this [MutablePKeySet]'s storage so it is sized appropriately
+ * to hold the current elements.
+ *
+ * Returns the number of empty elements removed from this set's storage.
+ * Returns 0 if no trimming is necessary or possible.
+ */
+ @androidx.annotation.IntRange(from = 0)
+ public fun trim(): Int {
+ val previousCapacity = _capacity
+ val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+ if (newCapacity < previousCapacity) {
+ resizeStorage(newCapacity)
+ return previousCapacity - _capacity
+ }
+ return 0
+ }
+
+ /**
+ * Grow internal storage if necessary. This function can instead opt to
+ * remove deleted elements from the set to avoid an expensive reallocation
+ * of the underlying storage. This "rehash in place" occurs when the
+ * current size is <= 25/32 of the set capacity. The choice of 25/32 is
+ * detailed in the implementation of abseil's `raw_hash_map`.
+ */
+ private fun adjustStorage() {
+ if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+ // TODO: Avoid resize and drop deletes instead
+ resizeStorage(nextCapacity(_capacity))
+ } else {
+ resizeStorage(nextCapacity(_capacity))
+ }
+ }
+
+ private fun resizeStorage(newCapacity: Int) {
+ val previousMetadata = metadata
+ val previousElements = elements
+ val previousCapacity = _capacity
+
+ initializeStorage(newCapacity)
+
+ val newElements = elements
+
+ for (i in 0 until previousCapacity) {
+ if (isFull(previousMetadata, i)) {
+ val previousElement = previousElements[i]
+ val hash = hash(previousElement)
+ val index = findFirstAvailableSlot(h1(hash))
+
+ writeMetadata(index, h2(hash).toLong())
+ newElements[index] = previousElement
+ }
+ }
+ }
+
+ /**
+ * Writes the "H2" part of an entry into the metadata array at the specified
+ * [index]. The index must be a valid index. This function ensures the
+ * metadata is also written in the clone area at the end.
+ */
+ private inline fun writeMetadata(index: Int, value: Long) {
+ val m = metadata
+ writeRawMetadata(m, index, value)
+
+ // Mirroring
+ val c = _capacity
+ val cloneIndex = ((index - ClonedMetadataCount) and c) +
+ (ClonedMetadataCount and c)
+ writeRawMetadata(m, cloneIndex, value)
+ }
+}
+
+/**
+ * Returns the hash code of [k]. This follows the [HashSet] default behavior on Android
+ * of returning [Object.hashcode()] with the higher bits of hash spread to the lower bits.
+ */
+internal inline fun hash(k: PKey): Int {
+ val hash = k.hashCode()
+ return hash xor (hash ushr 16)
+}
diff --git a/collection/collection/template/PKeySetTest.kt.template b/collection/collection/template/PKeySetTest.kt.template
new file mode 100644
index 0000000..cd5a422
--- /dev/null
+++ b/collection/collection/template/PKeySetTest.kt.template
@@ -0,0 +1,534 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class PKeySetTest {
+ @Test
+ fun emptyPKeySetConstructor() {
+ val set = MutablePKeySet()
+ assertEquals(7, set.capacity)
+ assertEquals(0, set.size)
+ }
+
+ @Test
+ fun immutableEmptyPKeySet() {
+ val set: PKeySet = emptyPKeySet()
+ assertEquals(0, set.capacity)
+ assertEquals(0, set.size)
+ }
+
+ @Test
+ fun zeroCapacityPKeySet() {
+ val set = MutablePKeySet(0)
+ assertEquals(0, set.capacity)
+ assertEquals(0, set.size)
+ }
+
+ @Test
+ fun emptyPKeySetWithCapacity() {
+ // When unloading the suggested capacity, we'll fall outside of the
+ // expected bucket of 2047 entries, and we'll get 4095 instead
+ val set = MutablePKeySet(1800)
+ assertEquals(4095, set.capacity)
+ assertEquals(0, set.size)
+ }
+
+ @Test
+ fun mutablePKeySetBuilder() {
+ val empty = mutablePKeySetOf()
+ assertEquals(0, empty.size)
+
+ val withElements = mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+ assertEquals(2, withElements.size)
+ assertTrue(1KeySuffix in withElements)
+ assertTrue(2KeySuffix in withElements)
+ }
+
+ @Test
+ fun addToPKeySet() {
+ val set = MutablePKeySet()
+ set += 1KeySuffix
+ assertTrue(set.add(2KeySuffix))
+
+ assertEquals(2, set.size)
+ val elements = PKeyArray(2)
+ var index = 0
+ set.forEach { element ->
+ elements[index++] = element
+ }
+ elements.sort()
+ assertEquals(1KeySuffix, elements[0])
+ assertEquals(2KeySuffix, elements[1])
+ }
+
+ @Test
+ fun addToSizedPKeySet() {
+ val set = MutablePKeySet(12)
+ set += 1KeySuffix
+
+ assertEquals(1, set.size)
+ assertEquals(1KeySuffix, set.first())
+ }
+
+ @Test
+ fun addExistingElement() {
+ val set = MutablePKeySet(12)
+ set += 1KeySuffix
+ assertFalse(set.add(1KeySuffix))
+ set += 1KeySuffix
+
+ assertEquals(1, set.size)
+ assertEquals(1KeySuffix, set.first())
+ }
+
+ @Test
+ fun addAllArray() {
+ val set = mutablePKeySetOf(1KeySuffix)
+ assertFalse(set.addAll(pKeyArrayOf(1KeySuffix)))
+ assertEquals(1, set.size)
+ assertTrue(set.addAll(pKeyArrayOf(1KeySuffix, 2KeySuffix)))
+ assertEquals(2, set.size)
+ assertTrue(2KeySuffix in set)
+ }
+
+ @Test
+ fun addAllPKeySet() {
+ val set = mutablePKeySetOf(1KeySuffix)
+ assertFalse(set.addAll(mutablePKeySetOf(1KeySuffix)))
+ assertEquals(1, set.size)
+ assertTrue(set.addAll(mutablePKeySetOf(1KeySuffix, 2KeySuffix)))
+ assertEquals(2, set.size)
+ assertTrue(2KeySuffix in set)
+ }
+
+ @Test
+ fun plusAssignArray() {
+ val set = mutablePKeySetOf(1KeySuffix)
+ set += pKeyArrayOf(1KeySuffix)
+ assertEquals(1, set.size)
+ set += pKeyArrayOf(1KeySuffix, 2KeySuffix)
+ assertEquals(2, set.size)
+ assertTrue(2KeySuffix in set)
+ }
+
+ @Test
+ fun plusAssignPKeySet() {
+ val set = mutablePKeySetOf(1KeySuffix)
+ set += mutablePKeySetOf(1KeySuffix)
+ assertEquals(1, set.size)
+ set += mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+ assertEquals(2, set.size)
+ assertTrue(2KeySuffix in set)
+ }
+
+ @Test
+ fun firstWithValue() {
+ val set = MutablePKeySet()
+ set += 1KeySuffix
+ set += 2KeySuffix
+ var element: PKey = -1KeySuffix
+ var otherElement: PKey = -1KeySuffix
+ set.forEach { if (element == -1KeySuffix) element = it else otherElement = it }
+ assertEquals(element, set.first())
+ set -= element
+ assertEquals(otherElement, set.first())
+ }
+
+ @Test
+ fun firstEmpty() {
+ assertFailsWith(NoSuchElementException::class) {
+ val set = MutablePKeySet()
+ set.first()
+ }
+ }
+
+ @Test
+ fun firstMatching() {
+ val set = MutablePKeySet()
+ set += 1KeySuffix
+ set += 2KeySuffix
+ assertEquals(1KeySuffix, set.first { it < 2KeySuffix })
+ assertEquals(2KeySuffix, set.first { it > 1KeySuffix })
+ }
+
+ @Test
+ fun firstMatchingEmpty() {
+ assertFailsWith(NoSuchElementException::class) {
+ val set = MutablePKeySet()
+ set.first { it > 0KeySuffix }
+ }
+ }
+
+ @Test
+ fun firstMatchingNoMatch() {
+ assertFailsWith(NoSuchElementException::class) {
+ val set = MutablePKeySet()
+ set += 1KeySuffix
+ set += 2KeySuffix
+ set.first { it < 0KeySuffix }
+ }
+ }
+
+ @Test
+ fun remove() {
+ val set = MutablePKeySet()
+ assertFalse(set.remove(1KeySuffix))
+
+ set += 1KeySuffix
+ assertTrue(set.remove(1KeySuffix))
+ assertEquals(0, set.size)
+
+ set += 1KeySuffix
+ set -= 1KeySuffix
+ assertEquals(0, set.size)
+ }
+
+ @Test
+ fun removeThenAdd() {
+ // Use a size of 6 to fit in a single entry in the metadata table
+ val set = MutablePKeySet(6)
+ set += 1KeySuffix
+ set += 5KeySuffix
+ set += 6KeySuffix
+ set += 9KeySuffix
+ set += 11KeySuffix
+ set += 13KeySuffix
+
+ // Removing all the entries will mark the medata as deleted
+ set.remove(1KeySuffix)
+ set.remove(5KeySuffix)
+ set.remove(6KeySuffix)
+ set.remove(9KeySuffix)
+ set.remove(11KeySuffix)
+ set.remove(13KeySuffix)
+
+ assertEquals(0, set.size)
+
+ val capacity = set.capacity
+
+ // Make sure reinserting an entry after filling the table
+ // with "Deleted" markers works
+ set += 3KeySuffix
+
+ assertEquals(1, set.size)
+ assertEquals(capacity, set.capacity)
+ }
+
+ @Test
+ fun removeAllArray() {
+ val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+ assertFalse(set.removeAll(pKeyArrayOf(3KeySuffix, 5KeySuffix)))
+ assertEquals(2, set.size)
+ assertTrue(set.removeAll(pKeyArrayOf(3KeySuffix, 1KeySuffix, 5KeySuffix)))
+ assertEquals(1, set.size)
+ assertFalse(1KeySuffix in set)
+ }
+
+ @Test
+ fun removeAllPKeySet() {
+ val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+ assertFalse(set.removeAll(mutablePKeySetOf(3KeySuffix, 5KeySuffix)))
+ assertEquals(2, set.size)
+ assertTrue(set.removeAll(mutablePKeySetOf(3KeySuffix, 1KeySuffix, 5KeySuffix)))
+ assertEquals(1, set.size)
+ assertFalse(1KeySuffix in set)
+ }
+
+ @Test
+ fun minusAssignArray() {
+ val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+ set -= pKeyArrayOf(3KeySuffix, 5KeySuffix)
+ assertEquals(2, set.size)
+ set -= pKeyArrayOf(3KeySuffix, 1KeySuffix, 5KeySuffix)
+ assertEquals(1, set.size)
+ assertFalse(1KeySuffix in set)
+ }
+
+ @Test
+ fun minusAssignPKeySet() {
+ val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+ set -= mutablePKeySetOf(3KeySuffix, 5KeySuffix)
+ assertEquals(2, set.size)
+ set -= mutablePKeySetOf(3KeySuffix, 1KeySuffix, 5KeySuffix)
+ assertEquals(1, set.size)
+ assertFalse(1KeySuffix in set)
+ }
+
+ @Test
+ fun insertManyEntries() {
+ val set = MutablePKeySet()
+
+ for (i in 0 until 1700) {
+ set += i.toPKey()
+ }
+
+ assertEquals(1700, set.size)
+ }
+
+ @Test
+ fun forEach() {
+ for (i in 0..48) {
+ val set = MutablePKeySet()
+
+ for (j in 0 until i) {
+ set += j.toPKey()
+ }
+
+ val elements = PKeyArray(i)
+ var index = 0
+ set.forEach { element ->
+ elements[index++] = element
+ }
+ elements.sort()
+
+ index = 0
+ elements.forEach { element ->
+ assertEquals(element, index.toPKey())
+ index++
+ }
+ }
+ }
+
+ @Test
+ fun clear() {
+ val set = MutablePKeySet()
+
+ for (i in 0 until 32) {
+ set += i.toPKey()
+ }
+
+ val capacity = set.capacity
+ set.clear()
+
+ assertEquals(0, set.size)
+ assertEquals(capacity, set.capacity)
+ }
+
+ @Test
+ fun string() {
+ val set = MutablePKeySet()
+ assertEquals("[]", set.toString())
+
+ set += 1KeySuffix
+ set += 5KeySuffix
+ assertTrue(
+ "[${1KeySuffix}, ${5KeySuffix}]" == set.toString() ||
+ "[${5KeySuffix}, ${1KeySuffix}]" == set.toString()
+ )
+ }
+
+ @Test
+ fun equals() {
+ val set = MutablePKeySet()
+ set += 1KeySuffix
+ set += 5KeySuffix
+
+ assertFalse(set.equals(null))
+ assertEquals(set, set)
+
+ val set2 = MutablePKeySet()
+ set2 += 5KeySuffix
+
+ assertNotEquals(set, set2)
+
+ set2 += 1KeySuffix
+ assertEquals(set, set2)
+ }
+
+ @Test
+ fun contains() {
+ val set = MutablePKeySet()
+ set += 1KeySuffix
+ set += 5KeySuffix
+
+ assertTrue(set.contains(1KeySuffix))
+ assertTrue(set.contains(5KeySuffix))
+ assertFalse(set.contains(2KeySuffix))
+ }
+
+ @Test
+ fun empty() {
+ val set = MutablePKeySet()
+ assertTrue(set.isEmpty())
+ assertFalse(set.isNotEmpty())
+ assertTrue(set.none())
+ assertFalse(set.any())
+
+ set += 1KeySuffix
+
+ assertFalse(set.isEmpty())
+ assertTrue(set.isNotEmpty())
+ assertTrue(set.any())
+ assertFalse(set.none())
+ }
+
+ @Test
+ fun count() {
+ val set = MutablePKeySet()
+ assertEquals(0, set.count())
+
+ set += 1KeySuffix
+ assertEquals(1, set.count())
+
+ set += 5KeySuffix
+ set += 6KeySuffix
+ set += 9KeySuffix
+ set += 11KeySuffix
+ set += 13KeySuffix
+
+ assertEquals(2, set.count { it < 6KeySuffix })
+ assertEquals(0, set.count { it < 0KeySuffix })
+ }
+
+ @Test
+ fun any() {
+ val set = MutablePKeySet()
+ set += 1KeySuffix
+ set += 5KeySuffix
+ set += 6KeySuffix
+ set += 9KeySuffix
+ set += 11KeySuffix
+ set += 13KeySuffix
+
+ assertTrue(set.any { it >= 11KeySuffix })
+ assertFalse(set.any { it < 0KeySuffix })
+ }
+
+ @Test
+ fun all() {
+ val set = MutablePKeySet()
+ set += 1KeySuffix
+ set += 5KeySuffix
+ set += 6KeySuffix
+ set += 9KeySuffix
+ set += 11KeySuffix
+ set += 13KeySuffix
+
+ assertTrue(set.all { it > 0KeySuffix })
+ assertFalse(set.all { it < 0KeySuffix })
+ }
+
+ @Test
+ fun trim() {
+ val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 7KeySuffix)
+ val capacity = set.capacity
+ assertEquals(0, set.trim())
+ set.clear()
+ assertEquals(capacity, set.trim())
+ assertEquals(0, set.capacity)
+ set.addAll(pKeyArrayOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 7KeySuffix, 6KeySuffix, 8KeySuffix,
+ 9KeySuffix, 10KeySuffix, 11KeySuffix, 12KeySuffix, 13KeySuffix, 14KeySuffix))
+ set.removeAll(pKeyArrayOf(6KeySuffix, 8KeySuffix, 9KeySuffix, 10KeySuffix, 11KeySuffix, 12KeySuffix, 13KeySuffix, 14KeySuffix))
+ assertTrue(set.trim() > 0)
+ assertEquals(capacity, set.capacity)
+ }
+
+ @Test
+ fun pKeySetOfEmpty() {
+ assertSame(emptyPKeySet(), pKeySetOf())
+ assertEquals(0, pKeySetOf().size)
+ }
+
+ @Test
+ fun pKeySetOfOne() {
+ val set = pKeySetOf(1KeySuffix)
+ assertEquals(1, set.size)
+ assertEquals(1KeySuffix, set.first())
+ }
+
+ @Test
+ fun pKeySetOfTwo() {
+ val set = pKeySetOf(1KeySuffix, 2KeySuffix)
+ assertEquals(2, set.size)
+ assertTrue(1KeySuffix in set)
+ assertTrue(2KeySuffix in set)
+ assertFalse(5KeySuffix in set)
+ }
+
+ @Test
+ fun pKeySetOfThree() {
+ val set = pKeySetOf(1KeySuffix, 2KeySuffix, 3KeySuffix)
+ assertEquals(3, set.size)
+ assertTrue(1KeySuffix in set)
+ assertTrue(2KeySuffix in set)
+ assertTrue(3KeySuffix in set)
+ assertFalse(5KeySuffix in set)
+ }
+
+ @Test
+ fun pKeySetOfFour() {
+ val set = pKeySetOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix)
+ assertEquals(4, set.size)
+ assertTrue(1KeySuffix in set)
+ assertTrue(2KeySuffix in set)
+ assertTrue(3KeySuffix in set)
+ assertTrue(4KeySuffix in set)
+ assertFalse(5KeySuffix in set)
+ }
+
+ @Test
+ fun mutablePKeySetOfOne() {
+ val set = mutablePKeySetOf(1KeySuffix)
+ assertEquals(1, set.size)
+ assertEquals(1KeySuffix, set.first())
+ }
+
+ @Test
+ fun mutablePKeySetOfTwo() {
+ val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+ assertEquals(2, set.size)
+ assertTrue(1KeySuffix in set)
+ assertTrue(2KeySuffix in set)
+ assertFalse(5KeySuffix in set)
+ }
+
+ @Test
+ fun mutablePKeySetOfThree() {
+ val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix, 3KeySuffix)
+ assertEquals(3, set.size)
+ assertTrue(1KeySuffix in set)
+ assertTrue(2KeySuffix in set)
+ assertTrue(3KeySuffix in set)
+ assertFalse(5KeySuffix in set)
+ }
+
+ @Test
+ fun mutablePKeySetOfFour() {
+ val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix)
+ assertEquals(4, set.size)
+ assertTrue(1KeySuffix in set)
+ assertTrue(2KeySuffix in set)
+ assertTrue(3KeySuffix in set)
+ assertTrue(4KeySuffix in set)
+ assertFalse(5KeySuffix in set)
+ }
+}
diff --git a/collection/collection/template/generateCollections.sh b/collection/collection/template/generateCollections.sh
new file mode 100755
index 0000000..39db416d
--- /dev/null
+++ b/collection/collection/template/generateCollections.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+
+primitives=("Float" "Long" "Int")
+suffixes=("f" "L" "")
+
+scriptDir=`dirname ${PWD}/${0}`
+
+for index in ${!primitives[@]}
+do
+ primitive=${primitives[$index]}
+ firstLower=`echo ${primitive:0:1} | tr '[:upper:]' '[:lower:]'`
+ lower="${firstLower}${primitive:1}"
+ echo "generating ${primitive}ObjectMap.kt"
+ sed -e "s/PKey/${primitive}/g" -e "s/pKey/${lower}/g" ${scriptDir}/PKeyObjectMap.kt.template > ${scriptDir}/../src/commonMain/kotlin/androidx/collection/${primitive}ObjectMap.kt
+ echo "generating ${primitive}ObjectMapTest.kt"
+ sed -e "s/PValue/${primitive}/g" ${scriptDir}/ObjectPValueMap.kt.template > ${scriptDir}/../src/commonMain/kotlin/androidx/collection/Object${primitive}Map.kt
+
+ suffix=${suffixes[$index]}
+ echo "generating Object${primitive}Map.kt"
+ sed -e "s/PValue/${primitive}/g" -e "s/ValueSuffix/${suffix}/g" ${scriptDir}/ObjectPValueMapTest.kt.template > ${scriptDir}/../src/commonTest/kotlin/androidx/collection/Object${primitive}MapTest.kt
+ echo "generating Object${primitive}MapTest.kt"
+ sed -e "s/PKey/${primitive}/g" -e"s/pKey/${lower}/g" -e "s/KeySuffix/${suffix}/g" ${scriptDir}/PKeyObjectMapTest.kt.template > ${scriptDir}/../src/commonTest/kotlin/androidx/collection/${primitive}ObjectMapTest.kt
+
+ echo "generating ${primitive}Set.kt"
+ sed -e "s/PKey/${primitive}/g" -e"s/pKey/${lower}/g" ${scriptDir}/PKeySet.kt.template > ${scriptDir}/../src/commonMain/kotlin/androidx/collection/${primitive}Set.kt
+ echo "generating ${primitive}SetTest.kt"
+ sed -e "s/PKey/${primitive}/g" -e"s/pKey/${lower}/g" -e "s/KeySuffix/${suffix}/g" ${scriptDir}/PKeySetTest.kt.template > ${scriptDir}/../src/commonTest/kotlin/androidx/collection/${primitive}SetTest.kt
+
+ echo "generating ${primitive}List.kt"
+ sed -e "s/PKey/${primitive}/g" -e"s/pKey/${lower}/g" ${scriptDir}/PKeyList.kt.template > ${scriptDir}/../src/commonMain/kotlin/androidx/collection/${primitive}List.kt
+ echo "generating ${primitive}ListTest.kt"
+ sed -e "s/PKey/${primitive}/g" -e"s/pKey/${lower}/g" -e "s/KeySuffix/${suffix}/g" ${scriptDir}/PKeyListTest.kt.template > ${scriptDir}/../src/commonTest/kotlin/androidx/collection/${primitive}ListTest.kt
+done
+
+for keyIndex in ${!primitives[@]}
+do
+ key=${primitives[$keyIndex]}
+ firstLower=`echo ${key:0:1} | tr '[:upper:]' '[:lower:]'`
+ lowerKey="${firstLower}${key:1}"
+ keySuffix=${suffixes[$keyIndex]}
+ for valueIndex in ${!primitives[@]}
+ do
+ value=${primitives[$valueIndex]}
+ valueSuffix=${suffixes[$valueIndex]}
+ echo "generating ${key}${value}Map.kt"
+ sed -e "s/PKey/${key}/g" -e "s/pKey/${lowerKey}/g" -e "s/PValue/${value}/g" ${scriptDir}/PKeyPValueMap.kt.template > ${scriptDir}/../src/commonMain/kotlin/androidx/collection/${key}${value}Map.kt
+ echo "generating ${key}${value}MapTest.kt"
+ sed -e "s/PKey/${key}/g" -e "s/pKey/${lowerKey}/g" -e "s/PValue/${value}/g" -e "s/ValueSuffix/${valueSuffix}/g" -e "s/KeySuffix/${keySuffix}/g" ${scriptDir}/PKeyPValueMapTest.kt.template > ${scriptDir}/../src/commonTest/kotlin/androidx/collection/${key}${value}MapTest.kt
+ done
+done
diff --git a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
index 2767e5aa..67921dc 100644
--- a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
+++ b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
@@ -28,11 +28,13 @@
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.TabRow
@@ -1115,6 +1117,77 @@
}
@Test
+ fun testRightEnterExitTransitionIsChosenDuringInterruption() {
+ var flag by mutableStateOf(false)
+ var fixedPosition: Offset? = null
+ var slidePosition: Offset? = null
+ rule.setContent {
+ AnimatedContent(
+ targetState = flag,
+ label = "",
+ transitionSpec = {
+ if (false isTransitioningTo true) {
+ ContentTransform(
+ targetContentEnter = EnterTransition.None,
+ initialContentExit = slideOutOfContainer(
+ AnimatedContentTransitionScope.SlideDirection.Start,
+ animationSpec = tween(durationMillis = 500)
+ ),
+ targetContentZIndex = -1.0f,
+ sizeTransform = SizeTransform(clip = false)
+ )
+ } else {
+ ContentTransform(
+ targetContentEnter = slideIntoContainer(
+ AnimatedContentTransitionScope.SlideDirection.End
+ ),
+ initialContentExit = ExitTransition.Hold,
+ targetContentZIndex = 0.0f,
+ sizeTransform = SizeTransform(clip = false)
+ )
+ }
+ },
+ modifier = Modifier.fillMaxSize()
+ ) { flag ->
+ Spacer(
+ modifier = Modifier
+ .wrapContentSize(Alignment.Center)
+ .size(256.dp)
+ .onGloballyPositioned {
+ if (flag) {
+ fixedPosition = it.positionInRoot()
+ } else {
+ slidePosition = it.positionInRoot()
+ }
+ }
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ flag = true
+ }
+ rule.waitUntil { fixedPosition != null }
+ val initialFixedPosition = fixedPosition
+ // Advance 10 frames
+ repeat(10) {
+ val lastSlidePos = slidePosition
+ rule.waitUntil { slidePosition != lastSlidePos }
+ assertEquals(initialFixedPosition, fixedPosition)
+ }
+
+ // Change the target state amid transition, creating an interruption
+ flag = false
+ // Advance 10 frames
+ repeat(10) {
+ val lastSlidePos = slidePosition
+ rule.waitUntil { slidePosition != lastSlidePos }
+ assertEquals(initialFixedPosition, fixedPosition)
+ }
+ rule.waitForIdle()
+ }
+
+ @Test
fun testScaleToFitWithFitHeight() {
var target by mutableStateOf(true)
var box1Coords: LayoutCoordinates? = null
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt
index b82c044..e557851 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt
@@ -820,13 +820,14 @@
}
if (!contentMap.containsKey(targetState) || !contentMap.containsKey(currentState)) {
contentMap.clear()
- val enter = transitionSpec(rootScope).targetContentEnter
- val exit = rootScope.transitionSpec().initialContentExit
- val zIndex = transitionSpec(rootScope).targetContentZIndex
currentlyVisible.fastForEach { stateForContent ->
contentMap[stateForContent] = {
+ // Only update content transform when enter/exit _direction_ changes.
+ val contentTransform = remember(stateForContent == targetState) {
+ rootScope.transitionSpec()
+ }
PopulateContentFor(
- stateForContent, rootScope, enter, exit, zIndex, currentlyVisible, content
+ stateForContent, rootScope, contentTransform, currentlyVisible, content
)
}
}
@@ -871,33 +872,32 @@
private inline fun <S> Transition<S>.PopulateContentFor(
stateForContent: S,
rootScope: AnimatedContentRootScope<S>,
- enter: EnterTransition,
- exit: ExitTransition,
- zIndex: Float,
+ contentTransform: ContentTransform,
currentlyVisible: SnapshotStateList<S>,
crossinline content: @Composable() AnimatedContentScope.(targetState: S) -> Unit
) {
- var activeEnter by remember { mutableStateOf(enter) }
+ var activeEnter by remember { mutableStateOf(contentTransform.targetContentEnter) }
var activeExit by remember { mutableStateOf(ExitTransition.None) }
- val targetZIndex = remember { zIndex }
+ val targetZIndex = remember { contentTransform.targetContentZIndex }
val isEntering = targetState == stateForContent
if (targetState == currentState) {
// Transition finished, reset active enter & exit.
- activeEnter = androidx.compose.animation.EnterTransition.None
- activeExit = androidx.compose.animation.ExitTransition.None
+ activeEnter = EnterTransition.None
+ activeExit = ExitTransition.None
} else if (isEntering) {
// If the previous enter transition never finishes when multiple
// interruptions happen, avoid adding new enter transitions for simplicity.
- if (activeEnter == androidx.compose.animation.EnterTransition.None)
- activeEnter += enter
+ if (activeEnter == EnterTransition.None)
+ activeEnter += contentTransform.targetContentEnter
} else {
// If the previous exit transition never finishes when multiple
// interruptions happen, avoid adding new enter transitions for simplicity.
- if (activeExit == androidx.compose.animation.ExitTransition.None) {
- activeExit += exit
+ if (activeExit == ExitTransition.None) {
+ activeExit += contentTransform.initialContentExit
}
}
+
val childData = remember { AnimatedContentRootScope.ChildData(stateForContent) }
AnimatedEnterExitImpl(
this,
@@ -915,16 +915,15 @@
.then(
if (isEntering) {
activeEnter[ScaleToFitTransitionKey]
- ?: activeExit[ScaleToFitTransitionKey] ?: androidx.compose.ui.Modifier
+ ?: activeExit[ScaleToFitTransitionKey] ?: Modifier
} else {
activeExit[ScaleToFitTransitionKey]
- ?: activeEnter[ScaleToFitTransitionKey] ?: androidx.compose.ui.Modifier
+ ?: activeEnter[ScaleToFitTransitionKey] ?: Modifier
}
),
shouldDisposeBlock = { currentState, targetState ->
- currentState == androidx.compose.animation.EnterExitState.PostExit &&
- targetState == androidx.compose.animation.EnterExitState.PostExit &&
- !activeExit.data.hold
+ currentState == EnterExitState.PostExit &&
+ targetState == EnterExitState.PostExit && !activeExit.data.hold
},
onLookaheadMeasured = {
if (isEntering) rootScope.targetSizeMap.getOrPut(targetState) {
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 66ef5ba..166ecd1 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -111,6 +111,30 @@
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier onFocusedBoundsChanged(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,kotlin.Unit> onPositioned);
}
+ public final class GraphicsSurfaceKt {
+ method @androidx.compose.runtime.Composable public static void EmbeddedGraphicsSurface(optional androidx.compose.ui.Modifier modifier, optional boolean isOpaque, optional long surfaceSize, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.GraphicsSurfaceScope,kotlin.Unit> onInit);
+ method @androidx.compose.runtime.Composable public static void GraphicsSurface(optional androidx.compose.ui.Modifier modifier, optional boolean isOpaque, optional int zOrder, optional long surfaceSize, optional boolean isSecure, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.GraphicsSurfaceScope,kotlin.Unit> onInit);
+ }
+
+ public interface GraphicsSurfaceScope {
+ method public void onSurface(kotlin.jvm.functions.Function5<? super androidx.compose.foundation.SurfaceCoroutineScope,? super android.view.Surface,? super java.lang.Integer,? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onSurface);
+ }
+
+ @kotlin.jvm.JvmInline public final value class GraphicsSurfaceZOrder {
+ method public int getZOrder();
+ property public final int zOrder;
+ field public static final androidx.compose.foundation.GraphicsSurfaceZOrder.Companion Companion;
+ }
+
+ public static final class GraphicsSurfaceZOrder.Companion {
+ method public int getBehind();
+ method public int getMediaOverlay();
+ method public int getOnTop();
+ property public final int Behind;
+ property public final int MediaOverlay;
+ property public final int OnTop;
+ }
+
public final class HoverableKt {
method public static androidx.compose.ui.Modifier hoverable(androidx.compose.ui.Modifier, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional boolean enabled);
}
@@ -246,6 +270,14 @@
property public final androidx.compose.runtime.saveable.Saver<androidx.compose.foundation.ScrollState,?> Saver;
}
+ public interface SurfaceCoroutineScope extends androidx.compose.foundation.SurfaceScope kotlinx.coroutines.CoroutineScope {
+ }
+
+ public interface SurfaceScope {
+ method public void onChanged(android.view.Surface, kotlin.jvm.functions.Function3<? super android.view.Surface,? super java.lang.Integer,? super java.lang.Integer,kotlin.Unit> onChanged);
+ method public void onDestroyed(android.view.Surface, kotlin.jvm.functions.Function1<? super android.view.Surface,kotlin.Unit> onDestroyed);
+ }
+
public final class SystemGestureExclusionKt {
method public static androidx.compose.ui.Modifier systemGestureExclusion(androidx.compose.ui.Modifier);
method public static androidx.compose.ui.Modifier systemGestureExclusion(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,androidx.compose.ui.geometry.Rect> exclusion);
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 3118943..5c2e81b 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -111,6 +111,30 @@
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier onFocusedBoundsChanged(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,kotlin.Unit> onPositioned);
}
+ public final class GraphicsSurfaceKt {
+ method @androidx.compose.runtime.Composable public static void EmbeddedGraphicsSurface(optional androidx.compose.ui.Modifier modifier, optional boolean isOpaque, optional long surfaceSize, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.GraphicsSurfaceScope,kotlin.Unit> onInit);
+ method @androidx.compose.runtime.Composable public static void GraphicsSurface(optional androidx.compose.ui.Modifier modifier, optional boolean isOpaque, optional int zOrder, optional long surfaceSize, optional boolean isSecure, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.GraphicsSurfaceScope,kotlin.Unit> onInit);
+ }
+
+ public interface GraphicsSurfaceScope {
+ method public void onSurface(kotlin.jvm.functions.Function5<? super androidx.compose.foundation.SurfaceCoroutineScope,? super android.view.Surface,? super java.lang.Integer,? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onSurface);
+ }
+
+ @kotlin.jvm.JvmInline public final value class GraphicsSurfaceZOrder {
+ method public int getZOrder();
+ property public final int zOrder;
+ field public static final androidx.compose.foundation.GraphicsSurfaceZOrder.Companion Companion;
+ }
+
+ public static final class GraphicsSurfaceZOrder.Companion {
+ method public int getBehind();
+ method public int getMediaOverlay();
+ method public int getOnTop();
+ property public final int Behind;
+ property public final int MediaOverlay;
+ property public final int OnTop;
+ }
+
public final class HoverableKt {
method public static androidx.compose.ui.Modifier hoverable(androidx.compose.ui.Modifier, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional boolean enabled);
}
@@ -248,6 +272,14 @@
property public final androidx.compose.runtime.saveable.Saver<androidx.compose.foundation.ScrollState,?> Saver;
}
+ public interface SurfaceCoroutineScope extends androidx.compose.foundation.SurfaceScope kotlinx.coroutines.CoroutineScope {
+ }
+
+ public interface SurfaceScope {
+ method public void onChanged(android.view.Surface, kotlin.jvm.functions.Function3<? super android.view.Surface,? super java.lang.Integer,? super java.lang.Integer,kotlin.Unit> onChanged);
+ method public void onDestroyed(android.view.Surface, kotlin.jvm.functions.Function1<? super android.view.Surface,kotlin.Unit> onDestroyed);
+ }
+
public final class SystemGestureExclusionKt {
method public static androidx.compose.ui.Modifier systemGestureExclusion(androidx.compose.ui.Modifier);
method public static androidx.compose.ui.Modifier systemGestureExclusion(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,androidx.compose.ui.geometry.Rect> exclusion);
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
index b50584f..4a07721 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
@@ -66,6 +66,7 @@
ComposableDemo("Vertical scroll") { VerticalScrollExample() },
ComposableDemo("Controlled Scrollable Row") { ControlledScrollableRowSample() },
ComposableDemo("Draw Modifiers") { DrawModifiersDemo() },
+ ComposableDemo("Graphics Surfaces") { GraphicsSurfaceDemo() },
DemoCategory("Lazy lists", LazyListDemos),
DemoCategory("Snapping", SnappingDemos),
DemoCategory("Pagers", PagerDemos),
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/GraphicsSurfaceDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/GraphicsSurfaceDemo.kt
new file mode 100644
index 0000000..4cc8c77
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/GraphicsSurfaceDemo.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.demos
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.samples.EmbeddedGraphicsSurfaceColors
+import androidx.compose.foundation.samples.GraphicsSurfaceColors
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun GraphicsSurfaceDemo() {
+ Column(Modifier.verticalScroll(rememberScrollState())) {
+ Text("GraphicsSurface:")
+ GraphicsSurfaceColors()
+ Spacer(Modifier.height(50.dp))
+ Text("EmbeddedGraphicsSurface:")
+ EmbeddedGraphicsSurfaceColors()
+ }
+}
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/GraphicsSurfaceSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/GraphicsSurfaceSamples.kt
new file mode 100644
index 0000000..dc63c5f
--- /dev/null
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/GraphicsSurfaceSamples.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.samples
+
+import android.graphics.Rect
+import androidx.annotation.Sampled
+import androidx.compose.foundation.EmbeddedGraphicsSurface
+import androidx.compose.foundation.GraphicsSurface
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.withFrameNanos
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.lerp
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.unit.dp
+import kotlin.math.sin
+
+@Sampled
+@Composable
+fun GraphicsSurfaceColors() {
+ GraphicsSurface(
+ modifier = Modifier.fillMaxWidth().height(400.dp)
+ ) {
+ // Resources can be initialized/cached here
+
+ // A surface is available, we can start rendering
+ onSurface { surface, width, height ->
+ var w = width
+ var h = height
+
+ // Initial draw to avoid a black frame
+ surface.lockCanvas(Rect(0, 0, w, h)).apply {
+ drawColor(Color.Blue.toArgb())
+ surface.unlockCanvasAndPost(this)
+ }
+
+ // React to surface dimension changes
+ surface.onChanged { newWidth, newHeight ->
+ w = newWidth
+ h = newHeight
+ }
+
+ // Cleanup if needed
+ surface.onDestroyed {
+ }
+
+ // Render loop, automatically cancelled by GraphicsSurface
+ // on surface destruction
+ while (true) {
+ withFrameNanos { time ->
+ surface.lockCanvas(Rect(0, 0, w, h)).apply {
+ val timeMs = time / 1_000_000L
+ val t = 0.5f + 0.5f * sin(timeMs / 1_000.0f)
+ drawColor(lerp(Color.Blue, Color.Green, t).toArgb())
+ surface.unlockCanvasAndPost(this)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun EmbeddedGraphicsSurfaceColors() {
+ EmbeddedGraphicsSurface(
+ modifier = Modifier.fillMaxWidth().height(400.dp)
+ ) {
+ // Resources can be initialized/cached here
+
+ // A surface is available, we can start rendering
+ onSurface { surface, width, height ->
+ var w = width
+ var h = height
+
+ // Initial draw to avoid a black frame
+ surface.lockCanvas(Rect(0, 0, w, h)).apply {
+ drawColor(Color.Yellow.toArgb())
+ surface.unlockCanvasAndPost(this)
+ }
+
+ // React to surface dimension changes
+ surface.onChanged { newWidth, newHeight ->
+ w = newWidth
+ h = newHeight
+ }
+
+ // Cleanup if needed
+ surface.onDestroyed {
+ }
+
+ // Render loop, automatically cancelled by EmbeddedGraphicsSurface
+ // on surface destruction
+ while (true) {
+ withFrameNanos { time ->
+ surface.lockCanvas(Rect(0, 0, w, h)).apply {
+ val timeMs = time / 1_000_000L
+ val t = 0.5f + 0.5f * sin(timeMs / 1_000.0f)
+ drawColor(lerp(Color.Yellow, Color.Red, t).toArgb())
+ surface.unlockCanvasAndPost(this)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/EmbeddedGraphicsSurfaceTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/EmbeddedGraphicsSurfaceTest.kt
new file mode 100644
index 0000000..6a10cd1
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/EmbeddedGraphicsSurfaceTest.kt
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation
+
+import android.graphics.PorterDuff
+import android.os.Build
+import android.view.Surface
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+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
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.unit.dp
+import androidx.core.graphics.ColorUtils
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import kotlin.math.roundToInt
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@RunWith(AndroidJUnit4::class)
+class EmbeddedGraphicsSurfaceTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ val size = 48.dp
+
+ @Test
+ fun testOnSurface() {
+ var surfaceRef: Surface? = null
+ var surfaceWidth = 0
+ var surfaceHeight = 0
+ var expectedSize = 0
+
+ rule.setContent {
+ expectedSize = with(LocalDensity.current) {
+ size.toPx().roundToInt()
+ }
+
+ EmbeddedGraphicsSurface(modifier = Modifier.size(size)) {
+ onSurface { surface, width, height ->
+ surfaceRef = surface
+ surfaceWidth = width
+ surfaceHeight = height
+ }
+ }
+ }
+
+ rule.onRoot()
+ .assertWidthIsEqualTo(size)
+ .assertHeightIsEqualTo(size)
+ .assertIsDisplayed()
+
+ rule.runOnIdle {
+ assertNotNull(surfaceRef)
+ assertEquals(expectedSize, surfaceWidth)
+ assertEquals(expectedSize, surfaceHeight)
+ }
+ }
+
+ @Test
+ fun testOnSurfaceChanged() {
+ var surfaceWidth = 0
+ var surfaceHeight = 0
+ var expectedSize = 0
+
+ var desiredSize by mutableStateOf(size)
+
+ rule.setContent {
+ expectedSize = with(LocalDensity.current) {
+ desiredSize.toPx().roundToInt()
+ }
+
+ EmbeddedGraphicsSurface(modifier = Modifier.size(desiredSize)) {
+ onSurface { surface, initWidth, initHeight ->
+ surfaceWidth = initWidth
+ surfaceHeight = initHeight
+
+ surface.onChanged { newWidth, newHeight ->
+ surfaceWidth = newWidth
+ surfaceHeight = newHeight
+ }
+ }
+ }
+ }
+
+ rule.onRoot()
+ .assertWidthIsEqualTo(desiredSize)
+ .assertHeightIsEqualTo(desiredSize)
+
+ rule.runOnIdle {
+ assertEquals(expectedSize, surfaceWidth)
+ assertEquals(expectedSize, surfaceHeight)
+ }
+
+ desiredSize = size * 2
+ val prevSurfaceWidth = surfaceWidth
+ val prevSurfaceHeight = surfaceHeight
+
+ rule.onRoot()
+ .assertWidthIsEqualTo(desiredSize)
+ .assertHeightIsEqualTo(desiredSize)
+
+ rule.runOnIdle {
+ assertNotEquals(prevSurfaceWidth, surfaceWidth)
+ assertNotEquals(prevSurfaceHeight, surfaceHeight)
+ assertEquals(expectedSize, surfaceWidth)
+ assertEquals(expectedSize, surfaceHeight)
+ }
+ }
+
+ @Test
+ fun testOnSurfaceDestroyed() {
+ var surfaceRef: Surface? = null
+ var visible by mutableStateOf(true)
+
+ rule.setContent {
+ if (visible) {
+ EmbeddedGraphicsSurface(modifier = Modifier.size(size)) {
+ onSurface { surface, _, _ ->
+ surfaceRef = surface
+
+ surface.onDestroyed {
+ surfaceRef = null
+ }
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertNotNull(surfaceRef)
+ }
+
+ visible = false
+
+ rule.runOnIdle {
+ assertNull(surfaceRef)
+ }
+ }
+
+ @Test
+ fun testOnSurfaceRecreated() {
+ var surfaceCreatedCount = 0
+ var surfaceDestroyedCount = 0
+ var visible by mutableStateOf(true)
+
+ // NOTE: TextureView only destroys the surface when TextureView is detached from
+ // the window, and only creates when it gets attached to the window
+ rule.setContent {
+ if (visible) {
+ EmbeddedGraphicsSurface(modifier = Modifier.size(size)) {
+ onSurface { surface, _, _ ->
+ surfaceCreatedCount++
+ surface.onDestroyed {
+ surfaceDestroyedCount++
+ }
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, surfaceCreatedCount)
+ assertEquals(0, surfaceDestroyedCount)
+ visible = false
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, surfaceCreatedCount)
+ assertEquals(1, surfaceDestroyedCount)
+ visible = true
+ }
+
+ rule.runOnIdle {
+ assertEquals(2, surfaceCreatedCount)
+ assertEquals(1, surfaceDestroyedCount)
+ }
+ }
+
+ @Test
+ fun testRender() {
+ var surfaceRef: Surface? = null
+ var expectedSize = 0
+
+ rule.setContent {
+ expectedSize = with(LocalDensity.current) {
+ size.toPx().roundToInt()
+ }
+ EmbeddedGraphicsSurface(modifier = Modifier.size(size)) {
+ onSurface { surface, _, _ ->
+ surfaceRef = surface
+ surface.lockHardwareCanvas().apply {
+ drawColor(Color.Blue.toArgb())
+ surface.unlockCanvasAndPost(this)
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertNotNull(surfaceRef)
+ }
+
+ surfaceRef!!
+ .captureToImage(expectedSize, expectedSize)
+ .assertPixels { Color.Blue }
+ }
+
+ @Test
+ fun testNotOpaque() {
+ val translucentRed = Color(1.0f, 0.0f, 0.0f, 0.5f).toArgb()
+
+ rule.setContent {
+ Box(modifier = Modifier.size(size)) {
+ Canvas(modifier = Modifier.size(size)) {
+ drawRect(Color.White)
+ }
+ EmbeddedGraphicsSurface(
+ modifier = Modifier
+ .size(size)
+ .testTag("EmbeddedGraphicSurface"),
+ isOpaque = false
+ ) {
+ onSurface { surface, _, _ ->
+ surface.lockHardwareCanvas().apply {
+ drawColor(0x00000000, PorterDuff.Mode.CLEAR)
+ drawColor(translucentRed)
+ surface.unlockCanvasAndPost(this)
+ }
+ }
+ }
+ }
+ }
+
+ val expectedColor = Color(ColorUtils.compositeColors(translucentRed, Color.White.toArgb()))
+
+ rule
+ .onNodeWithTag("EmbeddedGraphicSurface")
+ .captureToImage()
+ .assertPixels { expectedColor }
+ }
+
+ @Test
+ fun testOpaque() {
+ rule.setContent {
+ Box(modifier = Modifier.size(size)) {
+ Canvas(modifier = Modifier.size(size)) {
+ drawRect(Color.Green)
+ }
+ EmbeddedGraphicsSurface(
+ modifier = Modifier
+ .size(size)
+ .testTag("EmbeddedGraphicSurface")
+ ) {
+ onSurface { surface, _, _ ->
+ surface.lockHardwareCanvas().apply {
+ drawColor(Color.Red.toArgb())
+ surface.unlockCanvasAndPost(this)
+ }
+ }
+ }
+ }
+ }
+
+ rule
+ .onNodeWithTag("EmbeddedGraphicSurface")
+ .captureToImage()
+ .assertPixels { Color.Red }
+ }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/GraphicsSurfaceTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/GraphicsSurfaceTest.kt
new file mode 100644
index 0000000..46d036b
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/GraphicsSurfaceTest.kt
@@ -0,0 +1,589 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation
+
+import android.graphics.Bitmap
+import android.graphics.PorterDuff
+import android.graphics.Rect
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.view.Choreographer
+import android.view.PixelCopy
+import android.view.Surface
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.withFrameNanos
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.ViewRootForTest
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.unit.dp
+import androidx.concurrent.futures.ResolvableFuture
+import androidx.core.graphics.ColorUtils
+import androidx.core.graphics.createBitmap
+import androidx.test.core.internal.os.HandlerExecutor
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@RunWith(AndroidJUnit4::class)
+class GraphicsSurfaceTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ val size = 48.dp
+
+ @Test
+ fun testOnSurface() {
+ var surfaceRef: Surface? = null
+ var surfaceWidth = 0
+ var surfaceHeight = 0
+ var expectedSize = 0
+
+ rule.setContent {
+ expectedSize = with(LocalDensity.current) {
+ size.toPx().roundToInt()
+ }
+
+ GraphicsSurface(modifier = Modifier.size(size)) {
+ onSurface { surface, width, height ->
+ surfaceRef = surface
+ surfaceWidth = width
+ surfaceHeight = height
+ }
+ }
+ }
+
+ rule.onRoot()
+ .assertWidthIsEqualTo(size)
+ .assertHeightIsEqualTo(size)
+ .assertIsDisplayed()
+
+ rule.runOnIdle {
+ assertNotNull(surfaceRef)
+ assertEquals(expectedSize, surfaceWidth)
+ assertEquals(expectedSize, surfaceHeight)
+ }
+ }
+
+ @Test
+ fun testOnSurfaceChanged() {
+ var surfaceWidth = 0
+ var surfaceHeight = 0
+ var expectedSize = 0
+
+ var desiredSize by mutableStateOf(size)
+
+ rule.setContent {
+ expectedSize = with(LocalDensity.current) {
+ desiredSize.toPx().roundToInt()
+ }
+
+ GraphicsSurface(modifier = Modifier.size(desiredSize)) {
+ onSurface { surface, _, _ ->
+ surface.onChanged { newWidth, newHeight ->
+ surfaceWidth = newWidth
+ surfaceHeight = newHeight
+ }
+ }
+ }
+ }
+
+ rule.onRoot()
+ .assertWidthIsEqualTo(desiredSize)
+ .assertHeightIsEqualTo(desiredSize)
+
+ // onChanged() hasn't been called yet
+ rule.runOnIdle {
+ assertEquals(0, surfaceWidth)
+ assertEquals(0, surfaceHeight)
+ }
+
+ desiredSize = size * 2
+ val prevSurfaceWidth = surfaceWidth
+ val prevSurfaceHeight = surfaceHeight
+
+ rule.onRoot()
+ .assertWidthIsEqualTo(desiredSize)
+ .assertHeightIsEqualTo(desiredSize)
+
+ rule.runOnIdle {
+ assertNotEquals(prevSurfaceWidth, surfaceWidth)
+ assertNotEquals(prevSurfaceHeight, surfaceHeight)
+ assertEquals(expectedSize, surfaceWidth)
+ assertEquals(expectedSize, surfaceHeight)
+ }
+ }
+
+ @Test
+ fun testOnSurfaceDestroyed() {
+ var surfaceRef: Surface? = null
+ var visible by mutableStateOf(true)
+
+ rule.setContent {
+ if (visible) {
+ GraphicsSurface(modifier = Modifier.size(size)) {
+ onSurface { surface, _, _ ->
+ surfaceRef = surface
+
+ surface.onDestroyed {
+ surfaceRef = null
+ }
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertNotNull(surfaceRef)
+ }
+
+ visible = false
+
+ rule.runOnIdle {
+ assertNull(surfaceRef)
+ }
+ }
+
+ @Test
+ fun testOnSurfaceRecreated() {
+ var surfaceCreatedCount = 0
+ var surfaceDestroyedCount = 0
+
+ var view: View? = null
+
+ rule.setContent {
+ view = LocalView.current
+ GraphicsSurface(modifier = Modifier.size(size)) {
+ onSurface { surface, _, _ ->
+ surfaceCreatedCount++
+ surface.onDestroyed {
+ surfaceDestroyedCount++
+ }
+ }
+ }
+ }
+
+ // NOTE: SurfaceView only triggers a Surface destroy/create cycle on visibility
+ // change if its *own* visibility or the visibility of the window changes. Here
+ // we change the visibility of the window by setting the visibility of the root
+ // view (the host view in ViewRootImpl).
+ rule.runOnIdle {
+ assertEquals(1, surfaceCreatedCount)
+ assertEquals(0, surfaceDestroyedCount)
+ view?.rootView?.visibility = View.INVISIBLE
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, surfaceCreatedCount)
+ assertEquals(1, surfaceDestroyedCount)
+ view?.rootView?.visibility = View.VISIBLE
+ }
+
+ rule.runOnIdle {
+ assertEquals(2, surfaceCreatedCount)
+ assertEquals(1, surfaceDestroyedCount)
+ }
+ }
+
+ @Test
+ fun testRender() {
+ var surfaceRef: Surface? = null
+ var expectedSize = 0
+
+ rule.setContent {
+ expectedSize = with(LocalDensity.current) {
+ size.toPx().roundToInt()
+ }
+ GraphicsSurface(modifier = Modifier.size(size)) {
+ onSurface { surface, _, _ ->
+ surfaceRef = surface
+ surface.lockHardwareCanvas().apply {
+ drawColor(Color.Blue.toArgb())
+ surface.unlockCanvasAndPost(this)
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertNotNull(surfaceRef)
+ }
+
+ surfaceRef!!
+ .captureToImage(expectedSize, expectedSize)
+ .assertPixels { Color.Blue }
+ }
+
+ @Test
+ fun testZOrderDefault() {
+ val frameCount = 4
+ val latch = CountDownLatch(frameCount)
+
+ rule.setContent {
+ Box(modifier = Modifier.size(size)) {
+ GraphicsSurface(
+ modifier = Modifier
+ .size(size)
+ .testTag("GraphicSurface")
+ ) {
+ onSurface { surface, _, _ ->
+ // Draw > 3 frames to make sure the screenshot copy will pick up
+ // a SurfaceFlinger composition that includes our Surface
+ repeat(frameCount) {
+ withFrameNanos {
+ surface.lockHardwareCanvas().apply {
+ drawColor(Color.Blue.toArgb())
+ surface.unlockCanvasAndPost(this)
+ }
+ latch.countDown()
+ }
+ }
+ }
+ }
+ Canvas(modifier = Modifier.size(size)) {
+ drawRect(Color.Green)
+ }
+ }
+ }
+
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ throw AssertionError("Failed waiting for render")
+ }
+
+ rule
+ .onNodeWithTag("GraphicSurface")
+ .screenshotToImage()!!
+ .assertPixels { Color.Green }
+ }
+
+ @Test
+ fun testZOrderMediaOverlay() {
+ val frameCount = 4
+ val latch = CountDownLatch(frameCount)
+
+ rule.setContent {
+ Box(modifier = Modifier.size(size)) {
+ GraphicsSurface(
+ modifier = Modifier.size(size),
+ zOrder = GraphicsSurfaceZOrder.Behind
+ ) {
+ onSurface { surface, _, _ ->
+ // Draw > 3 frames to make sure the screenshot copy will pick up
+ // a SurfaceFlinger composition that includes our Surface
+ repeat(frameCount) {
+ withFrameNanos {
+ surface.lockHardwareCanvas().apply {
+ drawColor(Color.Blue.toArgb())
+ surface.unlockCanvasAndPost(this)
+ }
+ latch.countDown()
+ }
+ }
+ }
+ }
+ GraphicsSurface(
+ modifier = Modifier
+ .size(size)
+ .testTag("GraphicSurface"),
+ zOrder = GraphicsSurfaceZOrder.MediaOverlay
+ ) {
+ onSurface { surface, _, _ ->
+ surface.lockHardwareCanvas().apply {
+ drawColor(Color.Red.toArgb())
+ surface.unlockCanvasAndPost(this)
+ }
+ latch.countDown()
+ }
+ }
+ }
+ }
+
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ throw AssertionError("Failed waiting for render")
+ }
+
+ rule
+ .onNodeWithTag("GraphicSurface")
+ .screenshotToImage()!!
+ .assertPixels { Color.Red }
+ }
+
+ @Test
+ fun testZOrderOnTop() {
+ val frameCount = 4
+ val latch = CountDownLatch(frameCount)
+
+ rule.setContent {
+ Box(modifier = Modifier.size(size)) {
+ GraphicsSurface(
+ modifier = Modifier
+ .size(size)
+ .testTag("GraphicSurface"),
+ zOrder = GraphicsSurfaceZOrder.OnTop
+ ) {
+ onSurface { surface, _, _ ->
+ // Draw > 3 frames to make sure the screenshot copy will pick up
+ // a SurfaceFlinger composition that includes our Surface
+ repeat(frameCount) {
+ withFrameNanos {
+ surface.lockHardwareCanvas().apply {
+ drawColor(Color.Blue.toArgb())
+ surface.unlockCanvasAndPost(this)
+ }
+ latch.countDown()
+ }
+ }
+ }
+ }
+ Canvas(modifier = Modifier.size(size)) {
+ drawRect(Color.Green)
+ }
+ }
+ }
+
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ throw AssertionError("Failed waiting for render")
+ }
+
+ rule
+ .onNodeWithTag("GraphicSurface")
+ .screenshotToImage()!!
+ .assertPixels { Color.Blue }
+ }
+
+ @Test
+ fun testNotOpaque() {
+ val frameCount = 4
+ val latch = CountDownLatch(frameCount)
+ val translucentRed = Color(1.0f, 0.0f, 0.0f, 0.5f).toArgb()
+
+ rule.setContent {
+ Box(modifier = Modifier.size(size)) {
+ GraphicsSurface(
+ modifier = Modifier
+ .size(size)
+ .testTag("GraphicSurface"),
+ isOpaque = false,
+ zOrder = GraphicsSurfaceZOrder.OnTop
+ ) {
+ onSurface { surface, _, _ ->
+ // Draw > 3 frames to make sure the screenshot copy will pick up
+ // a SurfaceFlinger composition that includes our Surface
+ repeat(frameCount) {
+ withFrameNanos {
+ surface.lockHardwareCanvas().apply {
+ // Since we are drawing a translucent color we need to
+ // clear first
+ drawColor(0x00000000, PorterDuff.Mode.CLEAR)
+ drawColor(translucentRed)
+ surface.unlockCanvasAndPost(this)
+ }
+ latch.countDown()
+ }
+ }
+ }
+ }
+ Canvas(modifier = Modifier.size(size)) {
+ drawRect(Color.White)
+ }
+ }
+ }
+
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ throw AssertionError("Failed waiting for render")
+ }
+
+ val expectedColor = Color(ColorUtils.compositeColors(translucentRed, Color.White.toArgb()))
+
+ rule
+ .onNodeWithTag("GraphicSurface")
+ .screenshotToImage()!!
+ .assertPixels { expectedColor }
+ }
+
+ @Test
+ fun testSecure() {
+ val frameCount = 4
+ val latch = CountDownLatch(frameCount)
+
+ rule.setContent {
+ GraphicsSurface(
+ modifier = Modifier
+ .size(size)
+ .testTag("GraphicSurface"),
+ isSecure = true
+ ) {
+ onSurface { surface, _, _ ->
+ // Draw > 3 frames to make sure the screenshot copy will pick up
+ // a SurfaceFlinger composition that includes our Surface
+ repeat(frameCount) {
+ withFrameNanos {
+ surface.lockHardwareCanvas().apply {
+ drawColor(Color.Blue.toArgb())
+ surface.unlockCanvasAndPost(this)
+ }
+ latch.countDown()
+ }
+ }
+ }
+ }
+ }
+
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ throw AssertionError("Failed waiting for render")
+ }
+
+ val screen = rule
+ .onNodeWithTag("GraphicSurface")
+ .screenshotToImage(true)
+
+ // Before API 33 taking a screenshot with a secure surface returns null
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ assertNull(screen)
+ } else {
+ screen!!.assertPixels { Color.Black }
+ }
+ }
+}
+
+/**
+ * Returns an ImageBitmap containing a screenshot of the device. On API < 33,
+ * a secure surface present on screen can cause this function to return null.
+ */
+private fun SemanticsNodeInteraction.screenshotToImage(
+ hasSecureSurfaces: Boolean = false
+): ImageBitmap? {
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ instrumentation.waitForIdleSync()
+
+ val uiAutomation = instrumentation.uiAutomation
+
+ val node = fetchSemanticsNode()
+ val view = (node.root as ViewRootForTest).view
+
+ val bitmapFuture: ResolvableFuture<Bitmap> = ResolvableFuture.create()
+
+ val mainExecutor = HandlerExecutor(Handler(Looper.getMainLooper()))
+ mainExecutor.execute {
+ Choreographer.getInstance().postFrameCallback {
+ val location = IntArray(2)
+ view.getLocationOnScreen(location)
+
+ val bounds = node.boundsInRoot.translate(
+ location[0].toFloat(),
+ location[1].toFloat()
+ )
+
+ // do multiple retries of uiAutomation.takeScreenshot because it is known to return null
+ // on API 31+ b/257274080
+ var bitmap: Bitmap? = null
+ var i = 0
+ while (i < 3 && bitmap == null) {
+ bitmap = uiAutomation.takeScreenshot()
+ i++
+ }
+
+ if (bitmap != null) {
+ bitmap = Bitmap.createBitmap(
+ bitmap,
+ bounds.left.toInt(),
+ bounds.top.toInt(),
+ bounds.width.toInt(),
+ bounds.height.toInt()
+ )
+ bitmapFuture.set(bitmap)
+ } else {
+ if (hasSecureSurfaces) {
+ // may be null on older API levels when a secure surface is showing
+ bitmapFuture.set(null)
+ }
+ // if we don't show secure surfaces, let the future timeout on get()
+ }
+ }
+ }
+
+ return try {
+ bitmapFuture.get(5, TimeUnit.SECONDS)?.asImageBitmap()
+ } catch (e: ExecutionException) {
+ null
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+internal fun Surface.captureToImage(width: Int, height: Int): ImageBitmap {
+ val bitmap = createBitmap(width, height)
+
+ val latch = CountDownLatch(1)
+ var copyResult = 0
+ val onCopyFinished = PixelCopy.OnPixelCopyFinishedListener { result ->
+ copyResult = result
+ latch.countDown()
+ android.util.Log.d("Test", Thread.currentThread().toString())
+ }
+
+ PixelCopy.request(
+ this,
+ Rect(0, 0, width, height),
+ bitmap,
+ onCopyFinished,
+ Handler(Looper.getMainLooper())
+ )
+
+ if (!latch.await(1, TimeUnit.SECONDS)) {
+ throw AssertionError("Failed waiting for PixelCopy!")
+ }
+
+ if (copyResult != PixelCopy.SUCCESS) {
+ throw AssertionError("PixelCopy failed!")
+ }
+
+ return bitmap.asImageBitmap()
+}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/GraphicsSurface.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/GraphicsSurface.kt
new file mode 100644
index 0000000..17a6823
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/GraphicsSurface.kt
@@ -0,0 +1,438 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation
+
+import android.graphics.PixelFormat
+import android.graphics.SurfaceTexture
+import android.view.Surface
+import android.view.SurfaceHolder
+import android.view.SurfaceView
+import android.view.TextureView
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.viewinterop.AndroidView
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.launch
+
+/**
+ * [SurfaceScope] is a scoped environment provided by [GraphicsSurface] and
+ * [EmbeddedGraphicsSurface] to handle [Surface] lifecycle events.
+ *
+ * @sample androidx.compose.foundation.samples.GraphicsSurfaceColors
+ */
+interface SurfaceScope {
+ /**
+ * Invokes [onChanged] when the surface's geometry (width and height) changes.
+ * Always invoked on the main thread.
+ */
+ @Suppress("PrimitiveInLambda")
+ fun Surface.onChanged(onChanged: Surface.(width: Int, height: Int) -> Unit)
+
+ /**
+ * Invokes [onDestroyed] when the surface is destroyed. All rendering into
+ * the surface should stop immediately after [onDestroyed] is invoked.
+ * Always invoked on the main thread.
+ */
+ fun Surface.onDestroyed(onDestroyed: Surface.() -> Unit)
+}
+
+/**
+ * [SurfaceCoroutineScope] is a scoped environment provided by
+ * [GraphicsSurface] and [EmbeddedGraphicsSurface] when a new [Surface] is
+ * created. This environment is a coroutine scope that also provides access to
+ * a [SurfaceScope] environment which can itself be used to handle other [Surface]
+ * lifecycle events.
+ *
+ * @see SurfaceScope
+ * @see GraphicsSurfaceScope
+ *
+ * @sample androidx.compose.foundation.samples.GraphicsSurfaceColors
+ */
+interface SurfaceCoroutineScope : SurfaceScope, CoroutineScope
+
+/**
+ * [GraphicsSurfaceScope] is a scoped environment provided when a [GraphicsSurface]
+ * or [EmbeddedGraphicsSurface] is first initialized. This environment can be
+ * used to register a lambda to invoke when a new [Surface] associated with the
+ * [GraphicsSurface]/[EmbeddedGraphicsSurface] is created.
+ */
+interface GraphicsSurfaceScope {
+ /**
+ * Invokes [onSurface] when a new [Surface] is created. The [onSurface] lambda
+ * is invoked on the main thread as part of a [SurfaceCoroutineScope] to provide
+ * a coroutine context.
+ *
+ * @param onSurface Callback invoked when a new [Surface] is created. The initial
+ * dimensions of the surface are provided.
+ */
+ @Suppress("PrimitiveInLambda")
+ fun onSurface(
+ onSurface: suspend SurfaceCoroutineScope.(surface: Surface, width: Int, height: Int) -> Unit
+ )
+}
+
+/**
+ * Base class for [GraphicsSurface] and [EmbeddedGraphicsSurface] state. This class
+ * provides methods to properly dispatch lifecycle events on [Surface] creation,
+ * change, and destruction. Surface creation is treated as a coroutine launch,
+ * using the specified [scope] as the parent. This scope must be the main thread scope.
+ */
+private abstract class BaseGraphicsSurfaceState(val scope: CoroutineScope) :
+ GraphicsSurfaceScope, SurfaceScope {
+
+ private var onSurface:
+ (suspend SurfaceCoroutineScope.(surface: Surface, width: Int, height: Int) -> Unit)? = null
+ private var onSurfaceChanged: (Surface.(width: Int, height: Int) -> Unit)? = null
+ private var onSurfaceDestroyed: (Surface.() -> Unit)? = null
+
+ private var job: Job? = null
+
+ override fun onSurface(
+ onSurface: suspend SurfaceCoroutineScope.(surface: Surface, width: Int, height: Int) -> Unit
+ ) {
+ this.onSurface = onSurface
+ }
+
+ override fun Surface.onChanged(onChanged: Surface.(width: Int, height: Int) -> Unit) {
+ onSurfaceChanged = onChanged
+ }
+
+ override fun Surface.onDestroyed(onDestroyed: Surface.() -> Unit) {
+ onSurfaceDestroyed = onDestroyed
+ }
+
+ /**
+ * Dispatch a surface creation event by launching a new coroutine in [scope].
+ * Any previous job from a previous surface creation dispatch is cancelled.
+ */
+ fun dispatchSurfaceCreated(surface: Surface, width: Int, height: Int) {
+ if (onSurface != null) {
+ job = scope.launch(start = CoroutineStart.UNDISPATCHED) {
+ job?.cancelAndJoin()
+ val receiver =
+ object : SurfaceCoroutineScope, SurfaceScope by this@BaseGraphicsSurfaceState,
+ CoroutineScope by this {}
+ onSurface?.invoke(receiver, surface, width, height)
+ }
+ }
+ }
+
+ /**
+ * Dispatch a surface change event, providing the surface's new width and height.
+ * Must be invoked from the main thread.
+ */
+ fun dispatchSurfaceChanged(surface: Surface, width: Int, height: Int) {
+ onSurfaceChanged?.invoke(surface, width, height)
+ }
+
+ /**
+ * Dispatch a surface destruction event. Any pending job from [dispatchSurfaceCreated]
+ * is cancelled before dispatching the event. Must be invoked from the main thread.
+ */
+ fun dispatchSurfaceDestroyed(surface: Surface) {
+ onSurfaceDestroyed?.invoke(surface)
+ job?.cancel()
+ job = null
+ }
+}
+
+private class GraphicsSurfaceState(scope: CoroutineScope) : BaseGraphicsSurfaceState(scope),
+ SurfaceHolder.Callback {
+
+ var lastWidth = -1
+ var lastHeight = -1
+
+ override fun surfaceCreated(holder: SurfaceHolder) {
+ val frame = holder.surfaceFrame
+ lastWidth = frame.width()
+ lastHeight = frame.height()
+
+ dispatchSurfaceCreated(holder.surface, lastWidth, lastHeight)
+ }
+
+ override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
+ if (lastWidth != width || lastHeight != height) {
+ lastWidth = width
+ lastHeight = height
+
+ dispatchSurfaceChanged(holder.surface, width, height)
+ }
+ }
+
+ override fun surfaceDestroyed(holder: SurfaceHolder) {
+ dispatchSurfaceDestroyed(holder.surface)
+ }
+}
+
+@Composable
+private fun rememberGraphicsSurfaceState(): GraphicsSurfaceState {
+ val scope = rememberCoroutineScope()
+ return remember { GraphicsSurfaceState(scope) }
+}
+
+@JvmInline
+value class GraphicsSurfaceZOrder private constructor(val zOrder: Int) {
+ companion object {
+ val Behind = GraphicsSurfaceZOrder(0)
+ val MediaOverlay = GraphicsSurfaceZOrder(1)
+ val OnTop = GraphicsSurfaceZOrder(2)
+ }
+}
+
+/**
+ * Provides a dedicated drawing [Surface] as a separate layer positioned by default behind
+ * the window holding the [GraphicsSurface] composable. Because [GraphicsSurface] uses
+ * a separate window layer, graphics composition is handled by the system compositor which
+ * can bypass the GPU and provide better performance and power usage characteristics compared
+ * to [EmbeddedGraphicsSurface]. It is therefore recommended to use [GraphicsSurface]
+ * whenever possible.
+ *
+ * The z-ordering of the surface can be controlled using the [zOrder] parameter:
+ *
+ * - [GraphicsSurfaceZOrder.Behind]: positions the surface behind the window
+ * - [GraphicsSurfaceZOrder.MediaOverlay]: positions the surface behind the window but
+ * above other [GraphicsSurfaceZOrder.Behind] surfaces
+ * - [GraphicsSurfaceZOrder.OnTop]: positions the surface above the window
+ *
+ * The drawing surface is opaque by default, which can be controlled with the [isOpaque]
+ * parameter. When the surface is transparent, you may need to change the z-order to
+ * see something behind the surface.
+ *
+ * To start rendering, the caller must first acquire the [Surface] when it's created.
+ * This is achieved by providing the [onInit] lambda, which allows the caller to
+ * register an appropriate [GraphicsSurfaceScope.onSurface] callback. The [onInit]
+ * lambda can also be used to initialize/cache resources needed once a surface is
+ * available.
+ *
+ * After acquiring a surface, the caller can start rendering into it. Rendering into a
+ * surface can be done from any thread.
+ *
+ * It is recommended to register the [SurfaceScope.onChanged] and [SurfaceScope.onDestroyed]
+ * callbacks to properly handle the lifecycle of the surface and react to dimension
+ * changes. You must ensure that the rendering thread stops interacting with the surface
+ * when the [SurfaceScope.onDestroyed] callback is invoked.
+ *
+ * If a [surfaceSize] is specified (set to non-[IntSize.Zero]), the surface will use
+ * the specified size instead of the layout size of this composable. The surface will
+ * be stretched at render time to fit the layout size. This can be used for instance to
+ * render at a lower resolution for performance reasons.
+ *
+ * @param modifier Modifier to be applied to the [GraphicsSurface]
+ * @param isOpaque Whether the managed surface should be opaque or transparent.
+ * @param zOrder Sets the z-order of the surface relative to its parent window.
+ * @param surfaceSize Sets the surface size independently of the layout size of
+ * this [GraphicsSurface]. If set to [IntSize.Zero], the surface
+ * size will be equal to the [GraphicsSurface] layout size.
+ * @param isSecure Control whether the surface view's content should be treated as
+ * secure, preventing it from appearing in screenshots or from being
+ * viewed on non-secure displays.
+ * @param onInit Lambda invoked on first composition. This lambda can be used to
+ * declare a [GraphicsSurfaceScope.onSurface] callback that will be
+ * invoked when a surface is available.
+ *
+ * @sample androidx.compose.foundation.samples.GraphicsSurfaceColors
+ */
+@Composable
+fun GraphicsSurface(
+ modifier: Modifier = Modifier,
+ isOpaque: Boolean = true,
+ zOrder: GraphicsSurfaceZOrder = GraphicsSurfaceZOrder.Behind,
+ surfaceSize: IntSize = IntSize.Zero,
+ isSecure: Boolean = false,
+ onInit: GraphicsSurfaceScope.() -> Unit
+) {
+ val state = rememberGraphicsSurfaceState()
+
+ AndroidView(
+ factory = { context ->
+ SurfaceView(context).apply {
+ state.onInit()
+ holder.addCallback(state)
+ }
+ },
+ modifier = modifier,
+ onReset = { },
+ update = { view ->
+ if (surfaceSize != IntSize.Zero) {
+ view.holder.setFixedSize(surfaceSize.width, surfaceSize.height)
+ } else {
+ view.holder.setSizeFromLayout()
+ }
+
+ view.holder.setFormat(
+ if (isOpaque) {
+ PixelFormat.OPAQUE
+ } else {
+ PixelFormat.TRANSLUCENT
+ }
+ )
+
+ when (zOrder) {
+ GraphicsSurfaceZOrder.Behind -> view.setZOrderOnTop(false)
+ GraphicsSurfaceZOrder.MediaOverlay -> view.setZOrderMediaOverlay(true)
+ GraphicsSurfaceZOrder.OnTop -> view.setZOrderOnTop(true)
+ }
+
+ view.setSecure(isSecure)
+ }
+ )
+}
+
+private class EmbeddedGraphicsSurfaceState(scope: CoroutineScope) : BaseGraphicsSurfaceState(scope),
+ TextureView.SurfaceTextureListener {
+
+ var surfaceSize = IntSize.Zero
+
+ private var surfaceTextureSurface: Surface? = null
+
+ override fun onSurfaceTextureAvailable(
+ surfaceTexture: SurfaceTexture,
+ width: Int,
+ height: Int
+ ) {
+ var w = width
+ var h = height
+
+ if (surfaceSize != IntSize.Zero) {
+ w = surfaceSize.width
+ h = surfaceSize.height
+ surfaceTexture.setDefaultBufferSize(w, h)
+ }
+
+ val surface = Surface(surfaceTexture)
+ surfaceTextureSurface = surface
+
+ dispatchSurfaceCreated(surface, w, h)
+ }
+
+ override fun onSurfaceTextureSizeChanged(
+ surfaceTexture: SurfaceTexture,
+ width: Int,
+ height: Int
+ ) {
+ var w = width
+ var h = height
+
+ if (surfaceSize != IntSize.Zero) {
+ w = surfaceSize.width
+ h = surfaceSize.height
+ surfaceTexture.setDefaultBufferSize(w, h)
+ }
+
+ dispatchSurfaceChanged(surfaceTextureSurface!!, w, h)
+ }
+
+ override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
+ dispatchSurfaceDestroyed(surfaceTextureSurface!!)
+ surfaceTextureSurface = null
+ return true
+ }
+
+ override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
+ // onSurfaceTextureUpdated is called when the content of the SurfaceTexture
+ // has changed, which is not relevant to us since we are the producer here
+ }
+}
+
+@Composable
+private fun rememberEmbeddedGraphicsSurfaceState(): EmbeddedGraphicsSurfaceState {
+ val scope = rememberCoroutineScope()
+ return remember { EmbeddedGraphicsSurfaceState(scope) }
+}
+
+/**
+ * Provides a dedicated drawing [Surface] embedded directly in the UI hierarchy.
+ * Unlike [GraphicsSurface], [EmbeddedGraphicsSurface] positions its surface as a
+ * regular element inside the composable hierarchy. This means that graphics
+ * composition is handled like any other UI widget, using the GPU. This can lead
+ * to increased power and memory bandwidth usage compared to [GraphicsSurface]. It
+ * is therefore recommended to use [GraphicsSurface] when possible.
+ *
+ * [EmbeddedGraphicsSurface] can however be useful when interactions with other widgets
+ * is necessary, for instance if the surface needs to be "sandwiched" between two
+ * other widgets, or if it must participate in visual effects driven by
+ * a `Modifier.graphicsLayer{}`.
+ *
+ * The drawing surface is opaque by default, which can be controlled with the [isOpaque]
+ * parameter.
+ *
+ * To start rendering, the caller must first acquire the [Surface] when it's created.
+ * This is achieved by providing the [onInit] lambda, which allows the caller to
+ * register an appropriate [GraphicsSurfaceScope.onSurface] callback. The [onInit]
+ * lambda can also be used to initialize/cache resources needed once a surface is
+ * available.
+ *
+ * After acquiring a surface, the caller can start rendering into it. Rendering into a
+ * surface can be done from any thread.
+ *
+ * It is recommended to register the [SurfaceScope.onChanged] and [SurfaceScope.onDestroyed]
+ * callbacks to properly handle the lifecycle of the surface and react to dimension
+ * changes. You must ensure that the rendering thread stops interacting with the surface
+ * when the [SurfaceScope.onDestroyed] callback is invoked.
+ *
+ * If a [surfaceSize] is specified (set to non-[IntSize.Zero]), the surface will use
+ * the specified size instead of the layout size of this composable. The surface will
+ * be stretched at render time to fit the layout size. This can be used for instance to
+ * render at a lower resolution for performance reasons.
+ *
+ * @param modifier Modifier to be applied to the [GraphicsSurface]
+ * @param isOpaque Whether the managed surface should be opaque or transparent. If
+ * transparent and [isMediaOverlay] is `false`, the surface will
+ * be positioned above the parent window.
+ * @param surfaceSize Sets the surface size independently of the layout size of
+ * this [GraphicsSurface]. If set to [IntSize.Zero], the surface
+ * size will be equal to the [GraphicsSurface] layout size.
+ * @param onInit Lambda invoked on first composition. This lambda can be used to
+ * declare a [GraphicsSurfaceScope.onSurface] callback that will be
+ * invoked when a surface is available.
+ *
+ * @sample androidx.compose.foundation.samples.EmbeddedGraphicsSurfaceColors
+ */
+@Composable
+fun EmbeddedGraphicsSurface(
+ modifier: Modifier = Modifier,
+ isOpaque: Boolean = true,
+ surfaceSize: IntSize = IntSize.Zero,
+ onInit: GraphicsSurfaceScope.() -> Unit
+) {
+ val state = rememberEmbeddedGraphicsSurfaceState()
+
+ AndroidView(
+ factory = { context ->
+ TextureView(context).apply {
+ state.surfaceSize = surfaceSize
+ state.onInit()
+ surfaceTextureListener = state
+ }
+ },
+ modifier = modifier,
+ onReset = { },
+ update = { view ->
+ if (surfaceSize != IntSize.Zero) {
+ view.surfaceTexture?.setDefaultBufferSize(surfaceSize.width, surfaceSize.height)
+ }
+ state.surfaceSize = surfaceSize
+ view.isOpaque = isOpaque
+ }
+ )
+}
diff --git a/compose/lint/internal-lint-checks/build.gradle b/compose/lint/internal-lint-checks/build.gradle
index 3039214..f2132be 100644
--- a/compose/lint/internal-lint-checks/build.gradle
+++ b/compose/lint/internal-lint-checks/build.gradle
@@ -26,6 +26,7 @@
compileOnly(libs.androidLintApi)
compileOnly(libs.kotlinStdlib)
implementation(project(":compose:lint:common"))
+ implementation(project(":collection:collection"))
testImplementation(project(":compose:lint:common-test"))
testImplementation(libs.kotlinStdlib)
diff --git a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/AsCollectionDetector.kt b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/AsCollectionDetector.kt
new file mode 100644
index 0000000..2d470e0
--- /dev/null
+++ b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/AsCollectionDetector.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.lint
+
+import androidx.collection.MutableObjectList
+import androidx.collection.MutableScatterMap
+import androidx.collection.MutableScatterSet
+import androidx.collection.ObjectList
+import androidx.collection.ScatterMap
+import androidx.collection.ScatterSet
+import androidx.collection.scatterSetOf
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.impl.source.PsiClassReferenceType
+import java.util.EnumSet
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+
+/**
+ * Using [ScatterMap.asMap], [ScatterSet.asSet], [ObjectList.asList], or their mutable
+ * counterparts indicates that the developer may be using the collection incorrectly.
+ * Using the interfaces is slower access. It is best to use those only for when it touches
+ * public API.
+ */
+class AsCollectionDetector : Detector(), SourceCodeScanner {
+ override fun getApplicableUastTypes() = listOf<Class<out UElement>>(
+ UCallExpression::class.java
+ )
+
+ override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
+ override fun visitCallExpression(node: UCallExpression) {
+ val methodName = node.methodName ?: return
+ if (methodName in MethodNames) {
+ val receiverType = node.receiverType as? PsiClassReferenceType ?: return
+ val qualifiedName = receiverType.reference.qualifiedName ?: return
+ val indexOfAngleBracket = qualifiedName.indexOf('<')
+ if (indexOfAngleBracket > 0 &&
+ qualifiedName.substring(0, indexOfAngleBracket) in CollectionClasses
+ ) {
+ context.report(
+ ISSUE,
+ node,
+ context.getLocation(node),
+ "Use method $methodName() only for public API usage"
+ )
+ }
+ }
+ }
+ }
+
+ companion object {
+ private val MethodNames = scatterSetOf(
+ "asMap",
+ "asMutableMap",
+ "asSet",
+ "asMutableSet",
+ "asList",
+ "asMutableList"
+ )
+ private val CollectionClasses = scatterSetOf(
+ ScatterMap::class.qualifiedName,
+ MutableScatterMap::class.qualifiedName,
+ ScatterSet::class.qualifiedName,
+ MutableScatterSet::class.qualifiedName,
+ ObjectList::class.qualifiedName,
+ MutableObjectList::class.qualifiedName,
+ )
+
+ private val AsCollectionDetectorId = "AsCollectionCall"
+
+ val ISSUE = Issue.create(
+ id = AsCollectionDetectorId,
+ briefDescription = "High performance collections don't implement standard collection " +
+ "interfaces so that they can remain high performance. Converting to standard " +
+ "collections wraps the classes with another object. Use these interface " +
+ "wrappers only for exposing to public API.",
+ explanation = "ScatterMap, ScatterSet, and AnyList are written for high " +
+ "performance access. Using the standard collection interfaces for these classes " +
+ "forces slower performance access to these collections. The methods returning " +
+ "these interfaces should be limited to public API, where standard collection " +
+ "interfaces are expected.",
+ category = Category.PERFORMANCE, priority = 3, severity = Severity.ERROR,
+ implementation = Implementation(
+ AsCollectionDetector::class.java,
+ EnumSet.of(Scope.JAVA_FILE)
+ )
+ )
+ }
+}
diff --git a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
index 7e43f93..f13628f 100644
--- a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
+++ b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
@@ -28,6 +28,7 @@
override val api = 14
override val issues get(): List<Issue> {
return listOf(
+ AsCollectionDetector.ISSUE,
ExceptionMessageDetector.ISSUE,
ListIteratorDetector.ISSUE,
SteppedForLoopDetector.ISSUE,
diff --git a/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/AsCollectionDetectorTest.kt b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/AsCollectionDetectorTest.kt
new file mode 100644
index 0000000..39ce2a1
--- /dev/null
+++ b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/AsCollectionDetectorTest.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/* ktlint-disable max-line-length */
+@RunWith(Parameterized::class)
+class AsCollectionDetectorTest(
+ val types: CollectionType
+) : LintDetectorTest() {
+
+ override fun getDetector(): Detector = AsCollectionDetector()
+
+ override fun getIssues(): MutableList<Issue> =
+ mutableListOf(AsCollectionDetector.ISSUE)
+
+ private val collectionTilde = "~".repeat(types.collection.length)
+
+ @Test
+ fun immutableAsImmutable() {
+ lint().files(
+ ScatterMapClass,
+ ScatterSetClass,
+ ObjectListClass,
+ kotlin(
+ """
+ package androidx.compose.lint
+
+ import androidx.collection.${types.immutable}
+
+ fun foo(collection: ${types.immutable}${types.params}): ${types.collection}${types.params} =
+ collection.as${types.collection}()
+ """
+ )
+ ).run().expect(
+ """
+src/androidx/compose/lint/test.kt:7: Error: Use method as${types.collection}() only for public API usage [AsCollectionCall]
+ collection.as${types.collection}()
+ ~~~~~~~~~~~~~$collectionTilde~~
+1 errors, 0 warnings
+ """
+ )
+ }
+
+ @Test
+ fun mutableAsImmutable() {
+ lint().files(
+ ScatterMapClass,
+ ScatterSetClass,
+ ObjectListClass,
+ kotlin(
+ """
+ package androidx.compose.lint
+
+ import androidx.collection.Mutable${types.immutable}
+
+ fun foo(collection: Mutable${types.immutable}${types.params}): ${types.collection}${types.params} =
+ collection.as${types.collection}()
+ """
+ )
+ ).run().expect(
+ """
+src/androidx/compose/lint/test.kt:7: Error: Use method as${types.collection}() only for public API usage [AsCollectionCall]
+ collection.as${types.collection}()
+ ~~~~~~~~~~~~~$collectionTilde~~
+1 errors, 0 warnings
+ """
+ )
+ }
+
+ @Test
+ fun mutableAsMutable() {
+ lint().files(
+ ScatterMapClass,
+ ScatterSetClass,
+ ObjectListClass,
+ kotlin(
+ """
+ package androidx.compose.lint
+
+ import androidx.collection.Mutable${types.immutable}
+
+ fun foo(collection: Mutable${types.immutable}${types.params}): Mutable${types.collection}${types.params} =
+ collection.asMutable${types.collection}()
+ """
+ )
+ ).run().expect(
+ """
+src/androidx/compose/lint/test.kt:7: Error: Use method asMutable${types.collection}() only for public API usage [AsCollectionCall]
+ collection.asMutable${types.collection}()
+ ~~~~~~~~~~~~~~~~~~~~$collectionTilde~~
+1 errors, 0 warnings
+ """
+ )
+ }
+
+ @Test
+ fun nonCollectionAs() {
+ lint().files(
+ kotlin(
+ """
+ package androidx.compose.lint
+
+ fun foo(): ${types.collection}${types.params} =
+ WeirdCollection().as${types.collection}()
+
+ class WeirdCollection {
+ fun asList(): List<String>? = null
+ fun asSet(): Set<String>? = null
+ fun asMap(): Map<String, String>? = null
+ }
+ """
+ )
+ ).run().expectClean()
+ }
+
+ class CollectionType(
+ val immutable: String,
+ val collection: String,
+ val params: String
+ )
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun initParameters() = listOf(
+ CollectionType("ScatterMap", "Map", "<String, String>"),
+ CollectionType("ScatterSet", "Set", "<String>"),
+ CollectionType("ObjectList", "List", "<String>")
+ )
+
+ val ScatterMapClass = kotlin(
+ """
+ package androidx.collection
+ sealed class ScatterMap<K, V> {
+ fun asMap(): Map<K, V> = mapOf()
+ }
+
+ class MutableScatterMap<K, V> : ScatterMap<K, V>() {
+ fun asMutableMap(): MutableMap<K, V> = mutableMapOf()
+ }
+ """.trimIndent()
+ )
+
+ val ScatterSetClass = kotlin(
+ """
+ package androidx.collection
+ sealed class ScatterSet<E> {
+ fun asSet(): Set<E> = setOf()
+ }
+
+ class MutableScatterSet<E> : ScatterSet<E>() {
+ fun asMutableSet(): MutableSet<E> = mutableSetOf()
+ }
+ """.trimIndent()
+ )
+
+ val ObjectListClass = kotlin(
+ """
+ package androidx.collection
+ sealed class ObjectList<E> {
+ fun asList(): List<E> = listOf()
+ }
+
+ class MutableObjectList<E> : ObjectList<E>() {
+ fun asMutableList(): MutableList<E> = mutableListOf()
+ }
+ """.trimIndent()
+ )
+ }
+}
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index d665b1b..2e69826 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -29,13 +29,17 @@
}
public final class AppBarKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void BottomAppBar(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BottomAppBar(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.BottomAppBarState BottomAppBarState(float initialHeightOffsetLimit, float initialHeightOffset, float initialContentOffset);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void MediumTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SmallTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.BottomAppBarState rememberBottomAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.TopAppBarState rememberTopAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
}
@@ -66,6 +70,7 @@
}
public final class BottomAppBarDefaults {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.BottomAppBarScrollBehavior exitAlwaysScrollBehavior(optional androidx.compose.material3.BottomAppBarState state, optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec);
method @androidx.compose.runtime.Composable public long getBottomAppBarFabColor();
method @androidx.compose.runtime.Composable public long getContainerColor();
method public float getContainerElevation();
@@ -79,6 +84,39 @@
field public static final androidx.compose.material3.BottomAppBarDefaults INSTANCE;
}
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public interface BottomAppBarScrollBehavior {
+ method public androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? getFlingAnimationSpec();
+ method public androidx.compose.ui.input.nestedscroll.NestedScrollConnection getNestedScrollConnection();
+ method public androidx.compose.animation.core.AnimationSpec<java.lang.Float>? getSnapAnimationSpec();
+ method public androidx.compose.material3.BottomAppBarState getState();
+ method public boolean isPinned();
+ property public abstract androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec;
+ property public abstract boolean isPinned;
+ property public abstract androidx.compose.ui.input.nestedscroll.NestedScrollConnection nestedScrollConnection;
+ property public abstract androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec;
+ property public abstract androidx.compose.material3.BottomAppBarState state;
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public interface BottomAppBarState {
+ method public float getCollapsedFraction();
+ method public float getContentOffset();
+ method public float getHeightOffset();
+ method public float getHeightOffsetLimit();
+ method public void setContentOffset(float);
+ method public void setHeightOffset(float);
+ method public void setHeightOffsetLimit(float);
+ property public abstract float collapsedFraction;
+ property public abstract float contentOffset;
+ property public abstract float heightOffset;
+ property public abstract float heightOffsetLimit;
+ field public static final androidx.compose.material3.BottomAppBarState.Companion Companion;
+ }
+
+ public static final class BottomAppBarState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.BottomAppBarState,?> getSaver();
+ property public final androidx.compose.runtime.saveable.Saver<androidx.compose.material3.BottomAppBarState,?> Saver;
+ }
+
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class BottomSheetDefaults {
method @androidx.compose.runtime.Composable public void DragHandle(optional androidx.compose.ui.Modifier modifier, optional float width, optional float height, optional androidx.compose.ui.graphics.Shape shape, optional long color);
method @androidx.compose.runtime.Composable public long getContainerColor();
@@ -677,9 +715,11 @@
public static final class FabPosition.Companion {
method public int getCenter();
method public int getEnd();
+ method public int getEndOverlay();
method public int getStart();
property public final int Center;
property public final int End;
+ property public final int EndOverlay;
property public final int Start;
}
@@ -829,6 +869,10 @@
property @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumTouchTargetEnforcement;
}
+ public final class LabelKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Label(kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional boolean isPersistent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
@androidx.compose.runtime.Immutable public final class ListItemColors {
ctor public ListItemColors(long containerColor, long headlineColor, long leadingIconColor, long overlineColor, long supportingTextColor, long trailingIconColor, long disabledHeadlineColor, long disabledLeadingIconColor, long disabledTrailingIconColor);
method public long getContainerColor();
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index d665b1b..2e69826 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -29,13 +29,17 @@
}
public final class AppBarKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void BottomAppBar(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BottomAppBar(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.BottomAppBarState BottomAppBarState(float initialHeightOffsetLimit, float initialHeightOffset, float initialContentOffset);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void MediumTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SmallTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.BottomAppBarState rememberBottomAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.TopAppBarState rememberTopAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
}
@@ -66,6 +70,7 @@
}
public final class BottomAppBarDefaults {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.BottomAppBarScrollBehavior exitAlwaysScrollBehavior(optional androidx.compose.material3.BottomAppBarState state, optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec);
method @androidx.compose.runtime.Composable public long getBottomAppBarFabColor();
method @androidx.compose.runtime.Composable public long getContainerColor();
method public float getContainerElevation();
@@ -79,6 +84,39 @@
field public static final androidx.compose.material3.BottomAppBarDefaults INSTANCE;
}
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public interface BottomAppBarScrollBehavior {
+ method public androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? getFlingAnimationSpec();
+ method public androidx.compose.ui.input.nestedscroll.NestedScrollConnection getNestedScrollConnection();
+ method public androidx.compose.animation.core.AnimationSpec<java.lang.Float>? getSnapAnimationSpec();
+ method public androidx.compose.material3.BottomAppBarState getState();
+ method public boolean isPinned();
+ property public abstract androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec;
+ property public abstract boolean isPinned;
+ property public abstract androidx.compose.ui.input.nestedscroll.NestedScrollConnection nestedScrollConnection;
+ property public abstract androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec;
+ property public abstract androidx.compose.material3.BottomAppBarState state;
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public interface BottomAppBarState {
+ method public float getCollapsedFraction();
+ method public float getContentOffset();
+ method public float getHeightOffset();
+ method public float getHeightOffsetLimit();
+ method public void setContentOffset(float);
+ method public void setHeightOffset(float);
+ method public void setHeightOffsetLimit(float);
+ property public abstract float collapsedFraction;
+ property public abstract float contentOffset;
+ property public abstract float heightOffset;
+ property public abstract float heightOffsetLimit;
+ field public static final androidx.compose.material3.BottomAppBarState.Companion Companion;
+ }
+
+ public static final class BottomAppBarState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.BottomAppBarState,?> getSaver();
+ property public final androidx.compose.runtime.saveable.Saver<androidx.compose.material3.BottomAppBarState,?> Saver;
+ }
+
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class BottomSheetDefaults {
method @androidx.compose.runtime.Composable public void DragHandle(optional androidx.compose.ui.Modifier modifier, optional float width, optional float height, optional androidx.compose.ui.graphics.Shape shape, optional long color);
method @androidx.compose.runtime.Composable public long getContainerColor();
@@ -677,9 +715,11 @@
public static final class FabPosition.Companion {
method public int getCenter();
method public int getEnd();
+ method public int getEndOverlay();
method public int getStart();
property public final int Center;
property public final int End;
+ property public final int EndOverlay;
property public final int Start;
}
@@ -829,6 +869,10 @@
property @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumTouchTargetEnforcement;
}
+ public final class LabelKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Label(kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional boolean isPersistent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
@androidx.compose.runtime.Immutable public final class ListItemColors {
ctor public ListItemColors(long containerColor, long headlineColor, long leadingIconColor, long overlineColor, long supportingTextColor, long trailingIconColor, long disabledHeadlineColor, long disabledLeadingIconColor, long disabledTrailingIconColor);
method public long getContainerColor();
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index c6d720e..2e8c6a2 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -57,6 +57,7 @@
import androidx.compose.material3.samples.ElevatedFilterChipSample
import androidx.compose.material3.samples.ElevatedSuggestionChipSample
import androidx.compose.material3.samples.EnterAlwaysTopAppBar
+import androidx.compose.material3.samples.ExitAlwaysBottomAppBar
import androidx.compose.material3.samples.ExitUntilCollapsedLargeTopAppBar
import androidx.compose.material3.samples.ExitUntilCollapsedMediumTopAppBar
import androidx.compose.material3.samples.ExposedDropdownMenuSample
@@ -465,7 +466,12 @@
name = ::BottomAppBarWithFAB.name,
description = BottomAppBarsExampleDescription,
sourceUrl = BottomAppBarsExampleSourceUrl,
- ) { BottomAppBarWithFAB() }
+ ) { BottomAppBarWithFAB() },
+ Example(
+ name = ::ExitAlwaysBottomAppBar.name,
+ description = BottomAppBarsExampleDescription,
+ sourceUrl = BottomAppBarsExampleSourceUrl,
+ ) { ExitAlwaysBottomAppBar() }
)
private const val TopAppBarExampleDescription = "Top app bar examples"
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
index 6cbc0aa..a6538f0 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
@@ -19,6 +19,7 @@
import androidx.annotation.Sampled
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
@@ -31,6 +32,7 @@
import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon
@@ -418,11 +420,13 @@
@Sampled
@Composable
fun SimpleBottomAppBar() {
- BottomAppBar {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(Icons.Filled.Menu, contentDescription = "Localized description")
+ BottomAppBar(
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Menu, contentDescription = "Localized description")
+ }
}
- }
+ )
}
@Preview
@@ -452,3 +456,59 @@
}
)
}
+
+/**
+ * A sample for a [BottomAppBar] that collapses when the content is scrolled up, and
+ * appears when the content scrolled down.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Sampled
+@Composable
+fun ExitAlwaysBottomAppBar() {
+ val scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
+ Scaffold(
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ bottomBar = {
+ BottomAppBar(
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Check, contentDescription = "Localized description")
+ }
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Edit, contentDescription = "Localized description")
+ }
+ },
+ scrollBehavior = scrollBehavior
+ )
+ },
+ floatingActionButton = {
+ FloatingActionButton(
+ modifier = Modifier.offset(y = 4.dp),
+ onClick = { /* do something */ },
+ containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+ elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
+ ) {
+ Icon(Icons.Filled.Add, "Localized description")
+ }
+ },
+ floatingActionButtonPosition = FabPosition.EndOverlay,
+ content = { innerPadding ->
+ LazyColumn(
+ contentPadding = innerPadding,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ val list = (0..75).map { it.toString() }
+ items(count = list.size) {
+ Text(
+ text = list[it],
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ )
+ }
+ }
+ }
+ )
+}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt
index 77929ef..2528da4 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt
@@ -19,12 +19,16 @@
import androidx.annotation.Sampled
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
+import androidx.compose.material3.Label
+import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.RangeSlider
import androidx.compose.material3.RangeSliderState
import androidx.compose.material3.Slider
@@ -41,6 +45,8 @@
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
@Preview
@Sampled
@@ -56,7 +62,6 @@
}
}
-@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Sampled
@Composable
@@ -84,25 +89,40 @@
@Composable
fun SliderWithCustomThumbSample() {
var sliderPosition by remember { mutableStateOf(0f) }
+ val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
Column {
- Text(text = sliderPosition.toString())
Slider(
modifier = Modifier.semantics { contentDescription = "Localized Description" },
value = sliderPosition,
onValueChange = { sliderPosition = it },
- valueRange = 0f..5f,
- steps = 10,
+ valueRange = 0f..100f,
+ interactionSource = interactionSource,
onValueChangeFinished = {
// launch some business logic update with the state you hold
// viewModel.updateSelectedSliderValue(sliderPosition)
},
thumb = {
- Icon(
- imageVector = Icons.Filled.Favorite,
- contentDescription = null,
- modifier = Modifier.size(ButtonDefaults.IconSize),
- tint = Color.Red
- )
+ Label(
+ label = {
+ PlainTooltip(
+ modifier = Modifier
+ .requiredSize(45.dp, 25.dp)
+ .wrapContentWidth()
+ ) {
+ val roundedEnd =
+ (sliderPosition * 100.0).roundToInt() / 100.0
+ Text(roundedEnd.toString())
+ }
+ },
+ interactionSource = interactionSource
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = null,
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ tint = Color.Red
+ )
+ }
}
)
}
@@ -221,23 +241,52 @@
)
val endThumbColors = SliderDefaults.colors(thumbColor = Color.Green)
Column {
- Text(text = (rangeSliderState.activeRangeStart..rangeSliderState.activeRangeEnd).toString())
RangeSlider(
state = rangeSliderState,
modifier = Modifier.semantics { contentDescription = "Localized Description" },
startInteractionSource = startInteractionSource,
endInteractionSource = endInteractionSource,
startThumb = {
- SliderDefaults.Thumb(
- interactionSource = startInteractionSource,
- colors = startThumbAndTrackColors
- )
+ Label(
+ label = {
+ PlainTooltip(
+ modifier = Modifier
+ .requiredSize(45.dp, 25.dp)
+ .wrapContentWidth()
+ ) {
+ val roundedStart =
+ (rangeSliderState.activeRangeStart * 100.0).roundToInt() / 100.0
+ Text(roundedStart.toString())
+ }
+ },
+ interactionSource = startInteractionSource
+ ) {
+ SliderDefaults.Thumb(
+ interactionSource = startInteractionSource,
+ colors = startThumbAndTrackColors
+ )
+ }
},
endThumb = {
- SliderDefaults.Thumb(
- interactionSource = endInteractionSource,
- colors = endThumbColors
- )
+ Label(
+ label = {
+ PlainTooltip(
+ modifier = Modifier
+ .requiredSize(45.dp, 25.dp)
+ .wrapContentWidth()
+ ) {
+ val roundedEnd =
+ (rangeSliderState.activeRangeEnd * 100.0).roundToInt() / 100.0
+ Text(roundedEnd.toString())
+ }
+ },
+ interactionSource = endInteractionSource
+ ) {
+ SliderDefaults.Thumb(
+ interactionSource = endInteractionSource,
+ colors = endThumbColors
+ )
+ }
},
track = { rangeSliderState ->
SliderDefaults.Track(
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
index ddbb438..7b8d1a5 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
@@ -24,6 +24,7 @@
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material3.BottomAppBarDefaults.bottomAppBarFabColor
import androidx.compose.material3.TopAppBarDefaults.enterAlwaysScrollBehavior
import androidx.compose.material3.tokens.TopAppBarSmallTokens
import androidx.compose.testutils.assertAgainstGolden
@@ -361,7 +362,7 @@
floatingActionButton = {
FloatingActionButton(
onClick = { /* do something */ },
- containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+ containerColor = bottomAppBarFabColor,
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
) {
Icon(Icons.Filled.Add, "Localized description")
@@ -393,7 +394,7 @@
floatingActionButton = {
FloatingActionButton(
onClick = { /* do something */ },
- containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+ containerColor = bottomAppBarFabColor,
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
) {
Icon(Icons.Filled.Add, "Localized description")
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
index ef6ffe4..a913ceb 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
@@ -23,6 +23,7 @@
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
@@ -1183,6 +1184,105 @@
.assertTopPositionInRootIsEqualTo(12.dp)
}
+ @Test
+ fun bottomAppBar_exitAlways_scaffoldWithFAB_default_positioning() {
+ rule.setMaterialContent(lightColorScheme()) {
+ val scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
+ Scaffold(
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ bottomBar = {
+ BottomAppBar(
+ modifier = Modifier.testTag(BottomAppBarTestTag),
+ scrollBehavior = scrollBehavior
+ ) {}
+ },
+ floatingActionButton = {
+ FloatingActionButton(
+ modifier = Modifier
+ .testTag("FAB")
+ .offset(y = 4.dp),
+ onClick = { /* do something */ },
+ ) {}
+ },
+ floatingActionButtonPosition = FabPosition.EndOverlay
+ ) {}
+ }
+
+ val appBarBounds = rule.onNodeWithTag(BottomAppBarTestTag).getUnclippedBoundsInRoot()
+ val fabBounds = rule.onNodeWithTag("FAB").getUnclippedBoundsInRoot()
+ rule.onNodeWithTag("FAB")
+ // FAB should be 16.dp from the end
+ .assertLeftPositionInRootIsEqualTo(appBarBounds.width - 16.dp - fabBounds.width)
+ // FAB should be 12.dp from the bottom
+ .assertTopPositionInRootIsEqualTo(rule.rootHeight() - 12.dp - fabBounds.height)
+ }
+
+ @Test
+ fun bottomAppBar_exitAlways_scaffoldWithFAB_scrolled_positioning() {
+ lateinit var scrollBehavior: BottomAppBarScrollBehavior
+ val scrollHeightOffsetDp = 20.dp
+ var scrollHeightOffsetPx = 0f
+
+ rule.setMaterialContent(lightColorScheme()) {
+ scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
+ scrollHeightOffsetPx = with(LocalDensity.current) { scrollHeightOffsetDp.toPx() }
+ Scaffold(
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ bottomBar = {
+ BottomAppBar(
+ modifier = Modifier.testTag(BottomAppBarTestTag),
+ scrollBehavior = scrollBehavior
+ ) {}
+ },
+ floatingActionButton = {
+ FloatingActionButton(
+ modifier = Modifier
+ .testTag("FAB")
+ .offset(y = 4.dp),
+ onClick = { /* do something */ },
+ ) {}
+ },
+ floatingActionButtonPosition = FabPosition.EndOverlay
+ ) {}
+ }
+
+ // Simulate scrolled content.
+ rule.runOnIdle {
+ scrollBehavior.state.heightOffset = -scrollHeightOffsetPx
+ scrollBehavior.state.contentOffset = -scrollHeightOffsetPx
+ }
+ rule.waitForIdle()
+ rule.onNodeWithTag(BottomAppBarTestTag)
+ .assertHeightIsEqualTo(BottomAppBarTokens.ContainerHeight - scrollHeightOffsetDp)
+
+ val appBarBounds = rule.onNodeWithTag(BottomAppBarTestTag).getUnclippedBoundsInRoot()
+ val fabBounds = rule.onNodeWithTag("FAB").getUnclippedBoundsInRoot()
+ rule.onNodeWithTag("FAB")
+ // FAB should be 16.dp from the end
+ .assertLeftPositionInRootIsEqualTo(appBarBounds.width - 16.dp - fabBounds.width)
+ // FAB should be 12.dp from the bottom
+ .assertTopPositionInRootIsEqualTo(rule.rootHeight() - 12.dp - fabBounds.height)
+ }
+
+ @Test
+ fun bottomAppBar_exitAlways_allowHorizontalScroll() {
+ lateinit var state: LazyListState
+ rule.setMaterialContent(lightColorScheme()) {
+ state = rememberLazyListState()
+ MultiPageContent(BottomAppBarDefaults.exitAlwaysScrollBehavior(), state)
+ }
+
+ rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+ }
+
+ rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+ }
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MultiPageContent(scrollBehavior: TopAppBarScrollBehavior, state: LazyListState) {
@@ -1218,6 +1318,39 @@
}
}
+ @Composable
+ private fun MultiPageContent(scrollBehavior: BottomAppBarScrollBehavior, state: LazyListState) {
+ Scaffold(
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ bottomBar = {
+ BottomAppBar(
+ modifier = Modifier.testTag(BottomAppBarTestTag),
+ scrollBehavior = scrollBehavior
+ ) {}
+ }
+ ) { contentPadding ->
+ LazyRow(
+ Modifier
+ .fillMaxSize()
+ .testTag(LazyListTag), state
+ ) {
+ items(2) { page ->
+ LazyColumn(
+ modifier = Modifier.fillParentMaxSize(),
+ contentPadding = contentPadding
+ ) {
+ items(50) {
+ Text(
+ modifier = Modifier.fillParentMaxWidth(),
+ text = "Item #$page x $it"
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
/**
* Checks the app bar's components positioning when it's a [TopAppBar], a
* [CenterAlignedTopAppBar], or a larger app bar that is scrolled up and collapsed into a small
@@ -1583,7 +1716,8 @@
(TopAppBarSmallTokens.ContainerHeight - FakeIconSize) / 2
private val LazyListTag = "lazyList"
- private val TopAppBarTestTag = "bar"
+ private val TopAppBarTestTag = "topAppBar"
+ private val BottomAppBarTestTag = "bottomAppBar"
private val NavigationIconTestTag = "navigationIcon"
private val TitleTestTag = "title"
private val ActionsTestTag = "actions"
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
index 9dfb619..925e620 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
@@ -552,7 +552,7 @@
val pressedElevation = 2.dp
val hoveredElevation = 3.dp
val focusedElevation = 4.dp
- lateinit var tonalElevation: State<Dp>
+ var tonalElevation: Dp = Dp.Unspecified
lateinit var shadowElevation: State<Dp>
rule.setMaterialContent(lightColorScheme()) {
@@ -563,12 +563,12 @@
focusedElevation = focusedElevation
)
- tonalElevation = fabElevation.tonalElevation(interactionSource)
+ tonalElevation = fabElevation.tonalElevation()
shadowElevation = fabElevation.shadowElevation(interactionSource)
}
rule.runOnIdle {
- assertThat(tonalElevation.value).isEqualTo(defaultElevation)
+ assertThat(tonalElevation).isEqualTo(defaultElevation)
assertThat(shadowElevation.value).isEqualTo(defaultElevation)
}
@@ -577,7 +577,7 @@
}
rule.runOnIdle {
- assertThat(tonalElevation.value).isEqualTo(pressedElevation)
+ assertThat(tonalElevation).isEqualTo(defaultElevation)
assertThat(shadowElevation.value).isEqualTo(pressedElevation)
}
}
@@ -589,7 +589,7 @@
val pressedElevation = 2.dp
val hoveredElevation = 3.dp
val focusedElevation = 4.dp
- lateinit var tonalElevation: State<Dp>
+ var tonalElevation: Dp = Dp.Unspecified
lateinit var shadowElevation: State<Dp>
rule.setMaterialContent(lightColorScheme()) {
@@ -600,12 +600,12 @@
focusedElevation = focusedElevation
)
- tonalElevation = fabElevation.tonalElevation(interactionSource)
+ tonalElevation = fabElevation.tonalElevation()
shadowElevation = fabElevation.shadowElevation(interactionSource)
}
rule.runOnIdle {
- assertThat(tonalElevation.value).isEqualTo(defaultElevation)
+ assertThat(tonalElevation).isEqualTo(defaultElevation)
assertThat(shadowElevation.value).isEqualTo(defaultElevation)
}
@@ -614,7 +614,7 @@
}
rule.runOnIdle {
- assertThat(tonalElevation.value).isEqualTo(5.dp)
+ assertThat(tonalElevation).isEqualTo(5.dp)
assertThat(shadowElevation.value).isEqualTo(5.dp)
}
}
@@ -626,7 +626,7 @@
var pressedElevation by mutableStateOf(2.dp)
val hoveredElevation = 3.dp
val focusedElevation = 4.dp
- lateinit var tonalElevation: State<Dp>
+ var tonalElevation: Dp = Dp.Unspecified
lateinit var shadowElevation: State<Dp>
rule.setMaterialContent(lightColorScheme()) {
@@ -637,12 +637,12 @@
focusedElevation = focusedElevation
)
- tonalElevation = fabElevation.tonalElevation(interactionSource)
+ tonalElevation = fabElevation.tonalElevation()
shadowElevation = fabElevation.shadowElevation(interactionSource)
}
rule.runOnIdle {
- assertThat(tonalElevation.value).isEqualTo(defaultElevation)
+ assertThat(tonalElevation).isEqualTo(defaultElevation)
assertThat(shadowElevation.value).isEqualTo(defaultElevation)
}
@@ -651,7 +651,7 @@
}
rule.runOnIdle {
- assertThat(tonalElevation.value).isEqualTo(pressedElevation)
+ assertThat(tonalElevation).isEqualTo(defaultElevation)
assertThat(shadowElevation.value).isEqualTo(pressedElevation)
}
@@ -661,7 +661,7 @@
// We are still pressed, so we should now show the updated value for the pressed state
rule.runOnIdle {
- assertThat(tonalElevation.value).isEqualTo(5.dp)
+ assertThat(tonalElevation).isEqualTo(defaultElevation)
assertThat(shadowElevation.value).isEqualTo(5.dp)
}
}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
index e2380c1..859ae81 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
@@ -143,6 +143,42 @@
}
@Test
+ fun modalBottomSheet_isDismissedOnSwipeDown() {
+ var showBottomSheet by mutableStateOf(true)
+ val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
+
+ rule.setContent {
+ val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+ WindowInsets(0) else BottomSheetDefaults.windowInsets
+
+ if (showBottomSheet) {
+ ModalBottomSheet(
+ sheetState = sheetState,
+ onDismissRequest = { showBottomSheet = false },
+ windowInsets = windowInsets
+ ) {
+ Box(
+ Modifier
+ .size(sheetHeight)
+ .testTag(sheetTag)
+ )
+ }
+ }
+ }
+
+ assertThat(sheetState.isVisible).isTrue()
+
+ // Swipe Down
+ rule.onNodeWithTag(sheetTag).performTouchInput {
+ swipeDown()
+ }
+ rule.waitForIdle()
+
+ // Bottom sheet should not exist
+ rule.onNodeWithTag(sheetTag).assertDoesNotExist()
+ }
+
+ @Test
fun modalBottomSheet_fillsScreenWidth() {
var boxWidth = 0
var screenWidth by mutableStateOf(0)
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt
index 4204af2..a053929a 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt
@@ -28,6 +28,7 @@
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
@@ -197,10 +198,12 @@
)
}
)
- .anchoredDraggable(
- state = sheetState.anchoredDraggableState,
+ .draggable(
+ state = sheetState.anchoredDraggableState.draggableState,
orientation = Orientation.Vertical,
- enabled = sheetState.isVisible
+ enabled = sheetState.isVisible,
+ startDragImmediately = sheetState.anchoredDraggableState.isAnimationRunning,
+ onDragStopped = { settleToDismiss(it) }
)
.modalBottomSheetAnchors(
sheetState = sheetState,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
index cd79a42..7d8c9f6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
@@ -73,6 +73,7 @@
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.LastBaseline
import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.clearAndSetSemantics
@@ -391,6 +392,7 @@
* @param contentPadding the padding applied to the content of this BottomAppBar
* @param windowInsets a window insets that app bar will respect.
*/
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomAppBar(
actions: @Composable RowScope.() -> Unit,
@@ -402,12 +404,76 @@
contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets,
) = BottomAppBar(
+ actions = actions,
+ modifier = modifier,
+ floatingActionButton = floatingActionButton,
+ containerColor = containerColor,
+ contentColor = contentColor,
+ tonalElevation = tonalElevation,
+ contentPadding = contentPadding,
+ windowInsets = windowInsets,
+ scrollBehavior = null
+)
+
+/**
+ * <a href="https://m3.material.io/components/bottom-app-bar/overview" class="external" target="_blank">Material Design bottom app bar</a>.
+ *
+ * A bottom app bar displays navigation and key actions at the bottom of mobile screens.
+ *
+ * ![Bottom app bar image](https://developer.android.com/images/reference/androidx/compose/material3/bottom-app-bar.png)
+ *
+ * @sample androidx.compose.material3.samples.SimpleBottomAppBar
+ *
+ * It can optionally display a [FloatingActionButton] embedded at the end of the BottomAppBar.
+ *
+ * @sample androidx.compose.material3.samples.BottomAppBarWithFAB
+ *
+ * A bottom app bar that uses a [scrollBehavior] to customize its nested scrolling behavior when
+ * working in conjunction with a scrolling content looks like:
+ *
+ * @sample androidx.compose.material3.samples.ExitAlwaysBottomAppBar
+ *
+ * Also see [NavigationBar].
+ *
+ * @param actions the icon content of this BottomAppBar. The default layout here is a [Row],
+ * so content inside will be placed horizontally.
+ * @param modifier the [Modifier] to be applied to this BottomAppBar
+ * @param floatingActionButton optional floating action button at the end of this BottomAppBar
+ * @param containerColor the color used for the background of this BottomAppBar. Use
+ * [Color.Transparent] to have no color.
+ * @param contentColor the preferred color for content inside this BottomAppBar. Defaults to either
+ * the matching content color for [containerColor], or to the current [LocalContentColor] if
+ * [containerColor] is not a color from the theme.
+ * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
+ * overlay is applied on top of the container. A higher tonal elevation value will result in a
+ * darker color in light theme and lighter color in dark theme. See also: [Surface].
+ * @param contentPadding the padding applied to the content of this BottomAppBar
+ * @param windowInsets a window insets that app bar will respect.
+ * @param scrollBehavior a [BottomAppBarScrollBehavior] which holds various offset values that will
+ * be applied by this bottom app bar to set up its height. A scroll behavior is designed to
+ * work in conjunction with a scrolled content to change the bottom app bar appearance as the
+ * content scrolls. See [BottomAppBarScrollBehavior.nestedScrollConnection].
+ */
+@ExperimentalMaterial3Api
+@Composable
+fun BottomAppBar(
+ actions: @Composable RowScope.() -> Unit,
+ modifier: Modifier = Modifier,
+ floatingActionButton: @Composable (() -> Unit)? = null,
+ containerColor: Color = BottomAppBarDefaults.containerColor,
+ contentColor: Color = contentColorFor(containerColor),
+ tonalElevation: Dp = BottomAppBarDefaults.ContainerElevation,
+ contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
+ windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets,
+ scrollBehavior: BottomAppBarScrollBehavior? = null,
+) = BottomAppBar(
modifier = modifier,
containerColor = containerColor,
contentColor = contentColor,
tonalElevation = tonalElevation,
windowInsets = windowInsets,
- contentPadding = contentPadding
+ contentPadding = contentPadding,
+ scrollBehavior = scrollBehavior
) {
Row(
modifier = Modifier.weight(1f),
@@ -455,6 +521,7 @@
* @param content the content of this BottomAppBar. The default layout here is a [Row],
* so content inside will be placed horizontally.
*/
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomAppBar(
modifier: Modifier = Modifier,
@@ -464,7 +531,81 @@
contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets,
content: @Composable RowScope.() -> Unit
+) = BottomAppBar(
+ modifier = modifier,
+ containerColor = containerColor,
+ contentColor = contentColor,
+ tonalElevation = tonalElevation,
+ contentPadding = contentPadding,
+ windowInsets = windowInsets,
+ scrollBehavior = null,
+ content = content
+)
+
+/**
+ * <a href="https://m3.material.io/components/bottom-app-bar/overview" class="external" target="_blank">Material Design bottom app bar</a>.
+ *
+ * A bottom app bar displays navigation and key actions at the bottom of mobile screens.
+ *
+ * ![Bottom app bar image](https://developer.android.com/images/reference/androidx/compose/material3/bottom-app-bar.png)
+ *
+ * If you are interested in displaying a [FloatingActionButton], consider using another overload.
+ *
+ * Also see [NavigationBar].
+ *
+ * @param modifier the [Modifier] to be applied to this BottomAppBar
+ * @param containerColor the color used for the background of this BottomAppBar. Use
+ * [Color.Transparent] to have no color.
+ * @param contentColor the preferred color for content inside this BottomAppBar. Defaults to either
+ * the matching content color for [containerColor], or to the current [LocalContentColor] if
+ * [containerColor] is not a color from the theme.
+ * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
+ * overlay is applied on top of the container. A higher tonal elevation value will result in a
+ * darker color in light theme and lighter color in dark theme. See also: [Surface].
+ * @param contentPadding the padding applied to the content of this BottomAppBar
+ * @param windowInsets a window insets that app bar will respect.
+ * @param scrollBehavior a [BottomAppBarScrollBehavior] which holds various offset values that will
+ * be applied by this bottom app bar to set up its height. A scroll behavior is designed to
+ * work in conjunction with a scrolled content to change the bottom app bar appearance as the
+ * content scrolls. See [BottomAppBarScrollBehavior.nestedScrollConnection].
+ * @param content the content of this BottomAppBar. The default layout here is a [Row],
+ * so content inside will be placed horizontally.
+ */
+@ExperimentalMaterial3Api
+@Composable
+fun BottomAppBar(
+ modifier: Modifier = Modifier,
+ containerColor: Color = BottomAppBarDefaults.containerColor,
+ contentColor: Color = contentColorFor(containerColor),
+ tonalElevation: Dp = BottomAppBarDefaults.ContainerElevation,
+ contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
+ windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets,
+ scrollBehavior: BottomAppBarScrollBehavior? = null,
+ content: @Composable RowScope.() -> Unit
) {
+ // Set up support for resizing the bottom app bar when vertically dragging the bar itself.
+ val appBarDragModifier = if (scrollBehavior != null && !scrollBehavior.isPinned) {
+ Modifier.draggable(
+ orientation = Orientation.Vertical,
+ state = rememberDraggableState { delta ->
+ scrollBehavior.state.heightOffset -= delta
+ },
+ onDragStopped = { velocity ->
+ settleAppBarBottom(
+ scrollBehavior.state,
+ velocity,
+ scrollBehavior.flingAnimationSpec,
+ scrollBehavior.snapAnimationSpec
+ )
+ }
+ )
+ } else {
+ Modifier
+ }
+
+ // Compose a Surface with a Row content.
+ // The height of the app bar is determined by subtracting the bar's height offset from the
+ // app bar's defined constant height value (i.e. the ContainerHeight token).
Surface(
color = containerColor,
contentColor = contentColor,
@@ -472,6 +613,19 @@
// TODO(b/209583788): Consider adding a shape parameter if updated design guidance allows
shape = BottomAppBarTokens.ContainerShape.value,
modifier = modifier
+ .layout { measurable, constraints ->
+ // Sets the app bar's height offset to collapse the entire bar's height when content
+ // is scrolled.
+ scrollBehavior?.state?.heightOffsetLimit =
+ -BottomAppBarTokens.ContainerHeight.toPx()
+
+ val placeable = measurable.measure(constraints)
+ val height = placeable.height + (scrollBehavior?.state?.heightOffset ?: 0f)
+ layout(placeable.width, height.roundToInt()) {
+ placeable.place(0, 0)
+ }
+ }
+ .then(appBarDragModifier)
) {
Row(
Modifier
@@ -979,6 +1133,50 @@
}
}
+/**
+ * A BottomAppBarScrollBehavior defines how a bottom app bar should behave when the content under
+ * it is scrolled.
+ *
+ * @see [BottomAppBarDefaults.exitAlwaysScrollBehavior]
+ */
+@ExperimentalMaterial3Api
+@Stable
+interface BottomAppBarScrollBehavior {
+
+ /**
+ * A [BottomAppBarState] that is attached to this behavior and is read and updated when
+ * scrolling happens.
+ */
+ val state: BottomAppBarState
+
+ /**
+ * Indicates whether the bottom app bar is pinned.
+ *
+ * A pinned app bar will stay fixed in place when content is scrolled and will not react to any
+ * drag gestures.
+ */
+ val isPinned: Boolean
+
+ /**
+ * An optional [AnimationSpec] that defines how the bottom app bar snaps to either fully
+ * collapsed or fully extended state when a fling or a drag scrolled it into an intermediate
+ * position.
+ */
+ val snapAnimationSpec: AnimationSpec<Float>?
+
+ /**
+ * An optional [DecayAnimationSpec] that defined how to fling the bottom app bar when the user
+ * flings the app bar itself, or the content below it.
+ */
+ val flingAnimationSpec: DecayAnimationSpec<Float>?
+
+ /**
+ * A [NestedScrollConnection] that should be attached to a [Modifier.nestedScroll] in order to
+ * keep track of the scroll events.
+ */
+ val nestedScrollConnection: NestedScrollConnection
+}
+
/** Contains default values used for the bottom app bar implementations. */
object BottomAppBarDefaults {
@@ -1012,6 +1210,287 @@
val bottomAppBarFabColor: Color
@Composable get() =
FabSecondaryTokens.ContainerColor.value
+
+ /**
+ * Returns a [BottomAppBarScrollBehavior]. A bottom app bar that is set up with this
+ * [BottomAppBarScrollBehavior] will immediately collapse when the content is pulled up, and
+ * will immediately appear when the content is pulled down.
+ *
+ * @param state the state object to be used to control or observe the bottom app bar's scroll
+ * state. See [rememberBottomAppBarState] for a state that is remembered across compositions.
+ * @param canScroll a callback used to determine whether scroll events are to be
+ * handled by this [ExitAlwaysScrollBehavior]
+ * @param snapAnimationSpec an optional [AnimationSpec] that defines how the bottom app bar
+ * snaps to either fully collapsed or fully extended state when a fling or a drag scrolled it
+ * into an intermediate position
+ * @param flingAnimationSpec an optional [DecayAnimationSpec] that defined how to fling the
+ * bottom app bar when the user flings the app bar itself, or the content below it
+ */
+ @ExperimentalMaterial3Api
+ @Composable
+ fun exitAlwaysScrollBehavior(
+ state: BottomAppBarState = rememberBottomAppBarState(),
+ canScroll: () -> Boolean = { true },
+ snapAnimationSpec: AnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow),
+ flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay()
+ ): BottomAppBarScrollBehavior =
+ ExitAlwaysScrollBehavior(
+ state = state,
+ snapAnimationSpec = snapAnimationSpec,
+ flingAnimationSpec = flingAnimationSpec,
+ canScroll = canScroll
+ )
+}
+
+/**
+ * Creates a [BottomAppBarState] that is remembered across compositions.
+ *
+ * @param initialHeightOffsetLimit the initial value for [BottomAppBarState.heightOffsetLimit],
+ * which represents the pixel limit that a bottom app bar is allowed to collapse when the scrollable
+ * content is scrolled
+ * @param initialHeightOffset the initial value for [BottomAppBarState.heightOffset]. The initial
+ * offset height offset should be between zero and [initialHeightOffsetLimit].
+ * @param initialContentOffset the initial value for [BottomAppBarState.contentOffset]
+ */
+@ExperimentalMaterial3Api
+@Composable
+fun rememberBottomAppBarState(
+ initialHeightOffsetLimit: Float = -Float.MAX_VALUE,
+ initialHeightOffset: Float = 0f,
+ initialContentOffset: Float = 0f
+): BottomAppBarState {
+ return rememberSaveable(saver = BottomAppBarState.Saver) {
+ BottomAppBarState(
+ initialHeightOffsetLimit,
+ initialHeightOffset,
+ initialContentOffset
+ )
+ }
+}
+
+/**
+ * A state object that can be hoisted to control and observe the bottom app bar state. The state is
+ * read and updated by a [BottomAppBarScrollBehavior] implementation.
+ *
+ * In most cases, this state will be created via [rememberBottomAppBarState].
+ */
+@ExperimentalMaterial3Api
+interface BottomAppBarState {
+
+ /**
+ * The bottom app bar's height offset limit in pixels, which represents the limit that a bottom
+ * app bar is allowed to collapse to.
+ *
+ * Use this limit to coerce the [heightOffset] value when it's updated.
+ */
+ var heightOffsetLimit: Float
+
+ /**
+ * The bottom app bar's current height offset in pixels. This height offset is applied to the
+ * fixed height of the app bar to control the displayed height when content is being scrolled.
+ *
+ * Updates to the [heightOffset] value are coerced between zero and [heightOffsetLimit].
+ */
+ var heightOffset: Float
+
+ /**
+ * The total offset of the content scrolled under the bottom app bar.
+ *
+ * This value is updated by a [BottomAppBarScrollBehavior] whenever a nested scroll connection
+ * consumes scroll events. A common implementation would update the value to be the sum of all
+ * [NestedScrollConnection.onPostScroll] `consumed.y` values.
+ */
+ var contentOffset: Float
+
+ /**
+ * A value that represents the collapsed height percentage of the app bar.
+ *
+ * A `0.0` represents a fully expanded bar, and `1.0` represents a fully collapsed bar (computed
+ * as [heightOffset] / [heightOffsetLimit]).
+ */
+ val collapsedFraction: Float
+
+ companion object {
+ /**
+ * The default [Saver] implementation for [BottomAppBarState].
+ */
+ val Saver: Saver<BottomAppBarState, *> = listSaver(
+ save = { listOf(it.heightOffsetLimit, it.heightOffset, it.contentOffset) },
+ restore = {
+ BottomAppBarState(
+ initialHeightOffsetLimit = it[0],
+ initialHeightOffset = it[1],
+ initialContentOffset = it[2]
+ )
+ }
+ )
+ }
+}
+
+/**
+ * Creates a [BottomAppBarState].
+ *
+ * @param initialHeightOffsetLimit the initial value for [BottomAppBarState.heightOffsetLimit],
+ * which represents the pixel limit that a bottom app bar is allowed to collapse when the scrollable
+ * content is scrolled
+ * @param initialHeightOffset the initial value for [BottomAppBarState.heightOffset]. The initial
+ * offset height offset should be between zero and [initialHeightOffsetLimit].
+ * @param initialContentOffset the initial value for [BottomAppBarState.contentOffset]
+ */
+@ExperimentalMaterial3Api
+fun BottomAppBarState(
+ initialHeightOffsetLimit: Float,
+ initialHeightOffset: Float,
+ initialContentOffset: Float
+): BottomAppBarState = BottomAppBarStateImpl(
+ initialHeightOffsetLimit,
+ initialHeightOffset,
+ initialContentOffset
+)
+
+@ExperimentalMaterial3Api
+@Stable
+private class BottomAppBarStateImpl(
+ initialHeightOffsetLimit: Float,
+ initialHeightOffset: Float,
+ initialContentOffset: Float
+) : BottomAppBarState {
+
+ override var heightOffsetLimit by mutableFloatStateOf(initialHeightOffsetLimit)
+
+ override var heightOffset: Float
+ get() = _heightOffset.floatValue
+ set(newOffset) {
+ _heightOffset.floatValue = newOffset.coerceIn(
+ minimumValue = heightOffsetLimit,
+ maximumValue = 0f
+ )
+ }
+
+ override var contentOffset by mutableFloatStateOf(initialContentOffset)
+
+ override val collapsedFraction: Float
+ get() = if (heightOffsetLimit != 0f) {
+ heightOffset / heightOffsetLimit
+ } else {
+ 0f
+ }
+
+ private var _heightOffset = mutableFloatStateOf(initialHeightOffset)
+}
+
+/**
+ * A [BottomAppBarScrollBehavior] that adjusts its properties to affect the colors and height of a
+ * bottom app bar.
+ *
+ * A bottom app bar that is set up with this [BottomAppBarScrollBehavior] will immediately collapse
+ * when the nested content is pulled up, and will immediately appear when the content is pulled
+ * down.
+ *
+ * @param state a [BottomAppBarState]
+ * @param snapAnimationSpec an optional [AnimationSpec] that defines how the bottom app bar snaps to
+ * either fully collapsed or fully extended state when a fling or a drag scrolled it into an
+ * intermediate position
+ * @param flingAnimationSpec an optional [DecayAnimationSpec] that defined how to fling the bottom
+ * app bar when the user flings the app bar itself, or the content below it
+ * @param canScroll a callback used to determine whether scroll events are to be
+ * handled by this [ExitAlwaysScrollBehavior]
+ */
+@ExperimentalMaterial3Api
+private class ExitAlwaysScrollBehavior(
+ override val state: BottomAppBarState,
+ override val snapAnimationSpec: AnimationSpec<Float>?,
+ override val flingAnimationSpec: DecayAnimationSpec<Float>?,
+ val canScroll: () -> Boolean = { true }
+) : BottomAppBarScrollBehavior {
+ override val isPinned: Boolean = false
+ override var nestedScrollConnection =
+ object : NestedScrollConnection {
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ if (!canScroll()) return Offset.Zero
+ state.contentOffset += consumed.y
+ if (state.heightOffset == 0f || state.heightOffset == state.heightOffsetLimit) {
+ if (consumed.y == 0f && available.y > 0f) {
+ // Reset the total content offset to zero when scrolling all the way down.
+ // This will eliminate some float precision inaccuracies.
+ state.contentOffset = 0f
+ }
+ }
+ state.heightOffset = state.heightOffset + consumed.y
+ return Offset.Zero
+ }
+
+ override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+ val superConsumed = super.onPostFling(consumed, available)
+ return superConsumed + settleAppBarBottom(
+ state,
+ available.y,
+ flingAnimationSpec,
+ snapAnimationSpec
+ )
+ }
+ }
+}
+
+/**
+ * Settles the app bar by flinging, in case the given velocity is greater than zero, and snapping
+ * after the fling settles.
+ */
+@ExperimentalMaterial3Api
+private suspend fun settleAppBarBottom(
+ state: BottomAppBarState,
+ velocity: Float,
+ flingAnimationSpec: DecayAnimationSpec<Float>?,
+ snapAnimationSpec: AnimationSpec<Float>?
+): Velocity {
+ // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
+ // and just return Zero Velocity.
+ // Note that we don't check for 0f due to float precision with the collapsedFraction
+ // calculation.
+ if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) {
+ return Velocity.Zero
+ }
+ var remainingVelocity = velocity
+ // In case there is an initial velocity that was left after a previous user fling, animate to
+ // continue the motion to expand or collapse the app bar.
+ if (flingAnimationSpec != null && abs(velocity) > 1f) {
+ var lastValue = 0f
+ AnimationState(
+ initialValue = 0f,
+ initialVelocity = velocity,
+ )
+ .animateDecay(flingAnimationSpec) {
+ val delta = value - lastValue
+ val initialHeightOffset = state.heightOffset
+ state.heightOffset = initialHeightOffset + delta
+ val consumed = abs(initialHeightOffset - state.heightOffset)
+ lastValue = value
+ remainingVelocity = this.velocity
+ // avoid rounding errors and stop if anything is unconsumed
+ if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
+ }
+ }
+ // Snap if animation specs were provided.
+ if (snapAnimationSpec != null) {
+ if (state.heightOffset < 0 &&
+ state.heightOffset > state.heightOffsetLimit
+ ) {
+ AnimationState(initialValue = state.heightOffset).animateTo(
+ if (state.collapsedFraction < 0.5f) {
+ 0f
+ } else {
+ state.heightOffsetLimit
+ },
+ animationSpec = snapAnimationSpec
+ ) { state.heightOffset = value }
+ }
+ }
+
+ return Velocity(0f, remainingVelocity)
}
// Padding minus IconButton's min touch target expansion
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
index 88b46ce..259114e 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
@@ -117,7 +117,7 @@
val containerColor = colors.containerColor(enabled).value
val contentColor = colors.contentColor(enabled).value
val shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp
- val tonalElevation = elevation?.tonalElevation(enabled, interactionSource)?.value ?: 0.dp
+ val tonalElevation = elevation?.tonalElevation(enabled) ?: 0.dp
Surface(
onClick = onClick,
modifier = modifier.semantics { role = Role.Button },
@@ -769,8 +769,7 @@
private val disabledElevation: Dp,
) {
/**
- * Represents the tonal elevation used in a button, depending on its [enabled] state and
- * [interactionSource]. This should typically be the same value as the [shadowElevation].
+ * Represents the tonal elevation used in a button, depending on its [enabled] state.
*
* Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis.
* When surface's color is [ColorScheme.surface], a higher elevation will result in a darker
@@ -779,16 +778,14 @@
* See [shadowElevation] which controls the elevation of the shadow drawn around the button.
*
* @param enabled whether the button is enabled
- * @param interactionSource the [InteractionSource] for this button
*/
- @Composable
- internal fun tonalElevation(enabled: Boolean, interactionSource: InteractionSource): State<Dp> {
- return animateElevation(enabled = enabled, interactionSource = interactionSource)
+ internal fun tonalElevation(enabled: Boolean): Dp {
+ return if (enabled) defaultElevation else disabledElevation
}
/**
* Represents the shadow elevation used in a button, depending on its [enabled] state and
- * [interactionSource]. This should typically be the same value as the [tonalElevation].
+ * [interactionSource].
*
* Shadow elevation is used to apply a shadow around the button to give it higher emphasis.
*
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt
index 8a55fb0..bbec175 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt
@@ -86,7 +86,7 @@
shape = shape,
color = colors.containerColor(enabled = true).value,
contentColor = colors.contentColor(enabled = true).value,
- tonalElevation = elevation.tonalElevation(enabled = true, interactionSource = null).value,
+ tonalElevation = elevation.tonalElevation(enabled = true),
shadowElevation = elevation.shadowElevation(enabled = true, interactionSource = null).value,
border = border,
) {
@@ -147,7 +147,7 @@
shape = shape,
color = colors.containerColor(enabled).value,
contentColor = colors.contentColor(enabled).value,
- tonalElevation = elevation.tonalElevation(enabled, interactionSource).value,
+ tonalElevation = elevation.tonalElevation(enabled),
shadowElevation = elevation.shadowElevation(enabled, interactionSource).value,
border = border,
interactionSource = interactionSource,
@@ -564,8 +564,7 @@
private val disabledElevation: Dp
) {
/**
- * Represents the tonal elevation used in a card, depending on its [enabled] state and
- * [interactionSource]. This should typically be the same value as the [shadowElevation].
+ * Represents the tonal elevation used in a card, depending on its [enabled].
*
* Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis.
* When surface's color is [ColorScheme.surface], a higher elevation will result in a darker
@@ -574,22 +573,13 @@
* See [shadowElevation] which controls the elevation of the shadow drawn around the card.
*
* @param enabled whether the card is enabled
- * @param interactionSource the [InteractionSource] for this card
*/
- @Composable
- internal fun tonalElevation(
- enabled: Boolean,
- interactionSource: InteractionSource?
- ): State<Dp> {
- if (interactionSource == null) {
- return remember { mutableStateOf(defaultElevation) }
- }
- return animateElevation(enabled = enabled, interactionSource = interactionSource)
- }
+ internal fun tonalElevation(enabled: Boolean): Dp =
+ if (enabled) defaultElevation else disabledElevation
/**
* Represents the shadow elevation used in a card, depending on its [enabled] state and
- * [interactionSource]. This should typically be the same value as the [tonalElevation].
+ * [interactionSource].
*
* Shadow elevation is used to apply a shadow around the card to give it higher emphasis.
*
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
index 4f00d2c..09096d6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
@@ -1325,7 +1325,7 @@
enabled = enabled,
shape = shape,
color = colors.containerColor(enabled).value,
- tonalElevation = elevation?.tonalElevation(enabled, interactionSource)?.value ?: 0.dp,
+ tonalElevation = elevation?.tonalElevation(enabled) ?: 0.dp,
shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp,
border = border,
interactionSource = interactionSource,
@@ -1373,7 +1373,7 @@
enabled = enabled,
shape = shape,
color = colors.containerColor(enabled, selected).value,
- tonalElevation = elevation?.tonalElevation(enabled, interactionSource)?.value ?: 0.dp,
+ tonalElevation = elevation?.tonalElevation(enabled) ?: 0.dp,
shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp,
border = border,
interactionSource = interactionSource,
@@ -1454,8 +1454,7 @@
private val disabledElevation: Dp
) {
/**
- * Represents the tonal elevation used in a chip, depending on its [enabled] state and
- * [interactionSource]. This should typically be the same value as the [shadowElevation].
+ * Represents the tonal elevation used in a chip, depending on its [enabled] state.
*
* Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis.
* When surface's color is [ColorScheme.surface], a higher elevation will result in a darker
@@ -1464,19 +1463,14 @@
* See [shadowElevation] which controls the elevation of the shadow drawn around the chip.
*
* @param enabled whether the chip is enabled
- * @param interactionSource the [InteractionSource] for this chip
*/
- @Composable
- internal fun tonalElevation(
- enabled: Boolean,
- interactionSource: InteractionSource
- ): State<Dp> {
- return animateElevation(enabled = enabled, interactionSource = interactionSource)
+ internal fun tonalElevation(enabled: Boolean): Dp {
+ return if (enabled) elevation else disabledElevation
}
/**
* Represents the shadow elevation used in a chip, depending on its [enabled] state and
- * [interactionSource]. This should typically be the same value as the [tonalElevation].
+ * [interactionSource].
*
* Shadow elevation is used to apply a shadow around the chip to give it higher emphasis.
*
@@ -1617,8 +1611,7 @@
val disabledElevation: Dp
) {
/**
- * Represents the tonal elevation used in a chip, depending on [enabled] and
- * [interactionSource]. This should typically be the same value as the [shadowElevation].
+ * Represents the tonal elevation used in a chip, depending on [enabled].
*
* Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis.
* When surface's color is [ColorScheme.surface], a higher elevation will result in a darker
@@ -1627,19 +1620,14 @@
* See [shadowElevation] which controls the elevation of the shadow drawn around the Chip.
*
* @param enabled whether the chip is enabled
- * @param interactionSource the [InteractionSource] for this chip
*/
- @Composable
- internal fun tonalElevation(
- enabled: Boolean,
- interactionSource: InteractionSource
- ): State<Dp> {
- return animateElevation(enabled = enabled, interactionSource = interactionSource)
+ internal fun tonalElevation(enabled: Boolean): Dp {
+ return if (enabled) elevation else disabledElevation
}
/**
* Represents the shadow elevation used in a chip, depending on [enabled] and
- * [interactionSource]. This should typically be the same value as the [tonalElevation].
+ * [interactionSource].
*
* Shadow elevation is used to apply a shadow around the surface to give it higher emphasis.
*
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
index ae586c8..dd8ce60 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
@@ -108,7 +108,7 @@
shape = shape,
color = containerColor,
contentColor = contentColor,
- tonalElevation = elevation.tonalElevation(interactionSource = interactionSource).value,
+ tonalElevation = elevation.tonalElevation(),
shadowElevation = elevation.shadowElevation(interactionSource = interactionSource).value,
interactionSource = interactionSource,
) {
@@ -496,9 +496,8 @@
return animateElevation(interactionSource = interactionSource)
}
- @Composable
- internal fun tonalElevation(interactionSource: InteractionSource): State<Dp> {
- return animateElevation(interactionSource = interactionSource)
+ internal fun tonalElevation(): Dp {
+ return defaultElevation
}
@Composable
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Label.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Label.kt
new file mode 100644
index 0000000..1aedef2
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Label.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.foundation.BasicTooltipBox
+import androidx.compose.foundation.BasicTooltipState
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.MutatorMutex
+import androidx.compose.foundation.interaction.DragInteraction
+import androidx.compose.foundation.interaction.HoverInteraction
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.foundation.rememberBasicTooltipState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import kotlinx.coroutines.flow.collectLatest
+
+/**
+ * Label component that will append a [label] to [content].
+ * The positioning logic uses [TooltipDefaults.rememberPlainTooltipPositionProvider].
+ *
+ * Label appended to thumbs of Slider:
+ *
+ * @sample androidx.compose.material3.samples.SliderWithCustomThumbSample
+ *
+ * Label appended to thumbs of RangeSlider:
+ *
+ * @sample androidx.compose.material3.samples.RangeSliderWithCustomComponents
+ *
+ * @param label composable that will be appended to [content]
+ * @param modifier [Modifier] that will be applied to [content]
+ * @param interactionSource the [MutableInteractionSource] representing the
+ * stream of [Interaction]s for the [content].
+ * @param isPersistent boolean to determine if the label should be persistent.
+ * If true, then the label will always show and be anchored to [content].
+ * if false, then the label will only show when pressing down or hovering over the [content].
+ * @param content the composable that [label] will anchor to.
+ */
+@ExperimentalMaterial3Api
+@Composable
+fun Label(
+ label: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ isPersistent: Boolean = false,
+ content: @Composable () -> Unit
+) {
+ // Has the same positioning logic as PlainTooltips
+ val positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider()
+ val state = if (isPersistent)
+ remember { LabelStateImpl() }
+ else
+ rememberBasicTooltipState(mutatorMutex = MutatorMutex())
+ BasicTooltipBox(
+ positionProvider = positionProvider,
+ tooltip = label,
+ state = state,
+ modifier = modifier,
+ focusable = false,
+ enableUserInput = false,
+ content = content
+ )
+ HandleInteractions(
+ enabled = !isPersistent,
+ state = state,
+ interactionSource = interactionSource
+ )
+}
+
+@Composable
+private fun HandleInteractions(
+ enabled: Boolean,
+ state: BasicTooltipState,
+ interactionSource: MutableInteractionSource
+) {
+ if (enabled) {
+ LaunchedEffect(interactionSource) {
+ interactionSource.interactions.collectLatest { interaction ->
+ when (interaction) {
+ is PressInteraction.Press,
+ is DragInteraction.Start,
+ is HoverInteraction.Enter -> { state.show(MutatePriority.UserInput) }
+ is PressInteraction.Release,
+ is DragInteraction.Stop,
+ is HoverInteraction.Exit -> { state.dismiss() }
+ }
+ }
+ }
+ }
+}
+
+private class LabelStateImpl(
+ override val isVisible: Boolean = true,
+ override val isPersistent: Boolean = true
+) : BasicTooltipState {
+ override suspend fun show(mutatePriority: MutatePriority) {}
+
+ override fun dismiss() {}
+
+ override fun onDispose() {}
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
index b346d44..fea69f4 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
@@ -197,7 +197,7 @@
layoutWidth - FabSpacing.roundToPx() - fabWidth
}
}
- FabPosition.End -> {
+ FabPosition.End, FabPosition.EndOverlay -> {
if (layoutDirection == LayoutDirection.Ltr) {
layoutWidth - FabSpacing.roundToPx() - fabWidth
} else {
@@ -225,7 +225,7 @@
val bottomBarHeight = bottomBarPlaceables.fastMaxBy { it.height }?.height
val fabOffsetFromBottom = fabPlacement?.let {
- if (bottomBarHeight == null) {
+ if (bottomBarHeight == null || fabPosition == FabPosition.EndOverlay) {
it.height + FabSpacing.roundToPx() +
contentWindowInsets.getBottom(this@SubcomposeLayout)
} else {
@@ -329,13 +329,20 @@
* exists)
*/
val End = FabPosition(2)
+
+ /**
+ * Position FAB at the bottom of the screen at the end, overlaying the [NavigationBar] (if
+ * it exists)
+ */
+ val EndOverlay = FabPosition(3)
}
override fun toString(): String {
return when (this) {
Start -> "FabPosition.Start"
Center -> "FabPosition.Center"
- else -> "FabPosition.End"
+ End -> "FabPosition.End"
+ else -> "FabPosition.EndOverlay"
}
}
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
index 67bdfb5..21e04e7 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
@@ -89,7 +89,7 @@
* relative to the anchor content.
* @param tooltip the composable that will be used to populate the tooltip's content.
* @param state handles the state of the tooltip's visibility.
- * @param modifier the [Modifier] to be applied to the tooltip.
+ * @param modifier the [Modifier] to be applied to the TooltipBox.
* @param focusable [Boolean] that determines if the tooltip is focusable. When true,
* the tooltip will consume touch events while it's shown and will have accessibility
* focus move to the first element of the component. When false, the tooltip
@@ -142,16 +142,16 @@
content: @Composable () -> Unit
) {
Surface(
- modifier = modifier
+ shape = shape,
+ color = containerColor
+ ) {
+ Box(modifier = modifier
.sizeIn(
minWidth = TooltipMinWidth,
maxWidth = PlainTooltipMaxWidth,
minHeight = TooltipMinHeight
- ),
- shape = shape,
- color = containerColor
- ) {
- Box(modifier = Modifier.padding(PlainTooltipContentPadding)) {
+ ).padding(PlainTooltipContentPadding)
+ ) {
val textStyle =
MaterialTheme.typography.fromToken(PlainTooltipTokens.SupportingTextFont)
CompositionLocalProvider(
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
index 8ba40fa..a36e3d6 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
@@ -502,23 +502,26 @@
val box = context.bounds
val size = box.size.toSize()
val coordinates = layoutInfo.coordinates
- val topLeft = toIntOffset(coordinates.localToWindow(Offset.Zero))
- val topRight = toIntOffset(coordinates.localToWindow(Offset(size.width, 0f)))
- val bottomRight = toIntOffset(coordinates.localToWindow(Offset(size.width, size.height)))
- val bottomLeft = toIntOffset(coordinates.localToWindow(Offset(0f, size.height)))
var bounds: QuadBounds? = null
+ if (layoutInfo.isAttached) {
+ val topLeft = toIntOffset(coordinates.localToWindow(Offset.Zero))
+ val topRight = toIntOffset(coordinates.localToWindow(Offset(size.width, 0f)))
+ val bottomRight =
+ toIntOffset(coordinates.localToWindow(Offset(size.width, size.height)))
+ val bottomLeft = toIntOffset(coordinates.localToWindow(Offset(0f, size.height)))
- if (topLeft.x != box.left || topLeft.y != box.top ||
- topRight.x != box.right || topRight.y != box.top ||
- bottomRight.x != box.right || bottomRight.y != box.bottom ||
- bottomLeft.x != box.left || bottomLeft.y != box.bottom
- ) {
- bounds = QuadBounds(
- topLeft.x, topLeft.y,
- topRight.x, topRight.y,
- bottomRight.x, bottomRight.y,
- bottomLeft.x, bottomLeft.y,
- )
+ if (topLeft.x != box.left || topLeft.y != box.top ||
+ topRight.x != box.right || topRight.y != box.top ||
+ bottomRight.x != box.right || bottomRight.y != box.bottom ||
+ bottomLeft.x != box.left || bottomLeft.y != box.bottom
+ ) {
+ bounds = QuadBounds(
+ topLeft.x, topLeft.y,
+ topRight.x, topRight.y,
+ bottomRight.x, bottomRight.y,
+ bottomLeft.x, bottomLeft.y,
+ )
+ }
}
if (!includeNodesOutsizeOfWindow) {
// Ignore this node if the bounds are completely outside the window
diff --git a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/HyphensLineBreakBenchmark.kt b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/HyphensLineBreakBenchmark.kt
deleted file mode 100644
index 72f0dbc..0000000
--- a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/HyphensLineBreakBenchmark.kt
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * 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.compose.ui.text.benchmark
-
-import android.graphics.Bitmap
-import android.graphics.Canvas
-import android.graphics.text.LineBreakConfig
-import android.graphics.text.LineBreaker
-import android.os.Build
-import android.text.Layout
-import android.text.TextPaint
-import androidx.benchmark.junit4.BenchmarkRule
-import androidx.benchmark.junit4.measureRepeated
-import androidx.compose.ui.text.android.InternalPlatformTextApi
-import androidx.compose.ui.text.android.StaticLayoutFactory
-import androidx.compose.ui.text.style.Hyphens
-import androidx.compose.ui.text.style.LineBreak
-import androidx.test.filters.LargeTest
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@LargeTest
-@RunWith(Parameterized::class)
-@OptIn(InternalPlatformTextApi::class)
-class HyphensLineBreakBenchmark(
- private val textLength: Int,
- private val hyphensString: String,
- private val lineBreakString: String
-) {
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "length={0} hyphens={1} lineBreak={2}")
- fun initParameters(): List<Array<Any?>> {
- return cartesian(
- arrayOf(32, 128, 512),
- arrayOf(
- Hyphens.None.toTestString(),
- Hyphens.Auto.toTestString()
- ),
- arrayOf(
- LineBreak.Paragraph.toTestString(),
- LineBreak.Simple.toTestString(),
- LineBreak.Heading.toTestString()
- )
- )
- }
- }
-
- @get:Rule
- val benchmarkRule = BenchmarkRule()
-
- @get:Rule
- val textBenchmarkRule = TextBenchmarkTestRule()
-
- private val width = 100
- private val textSize: Float = 10F
- private val hyphenationFrequency = toLayoutHyphenationFrequency(hyphensString.toHyphens())
- private val lineBreakStyle = toLayoutLineBreakStyle(lineBreakString.toLineBreak().strictness)
- private val breakStrategy = toLayoutBreakStrategy(lineBreakString.toLineBreak().strategy)
- private val lineBreakWordStyle =
- toLayoutLineBreakWordStyle(lineBreakString.toLineBreak().wordBreak)
-
- @Test
- fun constructLayout() {
- textBenchmarkRule.generator { textGenerator ->
- val text = textGenerator.nextParagraph(textLength)
- val textPaint = TextPaint()
- textPaint.textSize = textSize
- benchmarkRule.measureRepeated {
- StaticLayoutFactory.create(
- text = text,
- width = width,
- paint = textPaint,
- hyphenationFrequency = hyphenationFrequency,
- lineBreakStyle = lineBreakStyle,
- breakStrategy = breakStrategy,
- lineBreakWordStyle = lineBreakWordStyle
- )
- }
- }
- }
-
- @Test
- fun constructLayoutDraw() {
- textBenchmarkRule.generator { textGenerator ->
- val text = textGenerator.nextParagraph(textLength)
- val textPaint = TextPaint()
- textPaint.textSize = textSize
- val canvas = Canvas(Bitmap.createBitmap(width, 1000, Bitmap.Config.ARGB_8888))
- benchmarkRule.measureRepeated {
- val layout = StaticLayoutFactory.create(
- text = text,
- width = width,
- paint = textPaint,
- hyphenationFrequency = hyphenationFrequency,
- lineBreakStyle = lineBreakStyle,
- breakStrategy = breakStrategy,
- lineBreakWordStyle = lineBreakWordStyle
- )
- layout.draw(canvas)
- }
- }
- }
-
- private fun toLayoutHyphenationFrequency(hyphens: Hyphens?): Int = when (hyphens) {
- Hyphens.Auto -> if (Build.VERSION.SDK_INT <= 32) {
- Layout.HYPHENATION_FREQUENCY_NORMAL
- } else {
- Layout.HYPHENATION_FREQUENCY_NORMAL_FAST
- }
- Hyphens.None -> Layout.HYPHENATION_FREQUENCY_NONE
- else -> Layout.HYPHENATION_FREQUENCY_NONE
- }
-
- private fun toLayoutBreakStrategy(breakStrategy: LineBreak.Strategy?): Int =
- when (breakStrategy) {
- LineBreak.Strategy.Simple -> LineBreaker.BREAK_STRATEGY_SIMPLE
- LineBreak.Strategy.HighQuality -> LineBreaker.BREAK_STRATEGY_HIGH_QUALITY
- LineBreak.Strategy.Balanced -> LineBreaker.BREAK_STRATEGY_BALANCED
- else -> LineBreaker.BREAK_STRATEGY_SIMPLE
- }
-
- private fun toLayoutLineBreakStyle(lineBreakStrictness: LineBreak.Strictness?): Int =
- when (lineBreakStrictness) {
- LineBreak.Strictness.Default -> LineBreakConfig.LINE_BREAK_STYLE_NONE
- LineBreak.Strictness.Loose -> LineBreakConfig.LINE_BREAK_STYLE_LOOSE
- LineBreak.Strictness.Normal -> LineBreakConfig.LINE_BREAK_STYLE_NORMAL
- LineBreak.Strictness.Strict -> LineBreakConfig.LINE_BREAK_STYLE_STRICT
- else -> LineBreakConfig.LINE_BREAK_STYLE_NONE
- }
-
- private fun toLayoutLineBreakWordStyle(lineBreakWordStyle: LineBreak.WordBreak?): Int =
- when (lineBreakWordStyle) {
- LineBreak.WordBreak.Default -> LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE
- LineBreak.WordBreak.Phrase -> LineBreakConfig.LINE_BREAK_WORD_STYLE_PHRASE
- else -> LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE
- }
-}
-
-/**
- * Required to make this test work due to a bug with value classes and Parameterized JUnit tests.
- * https://youtrack.jetbrains.com/issue/KT-35523
- *
- * However, it's not enough to use a wrapper because wrapper makes the test name unnecessarily
- * long which causes Perfetto to be unable to create output files with a very long name in some
- * file systems.
- *
- * Using a String instead of an Integer gives us a better test naming.
- */
-private fun String.toLineBreak(): LineBreak = when (this) {
- "Simple" -> LineBreak.Simple
- "Heading" -> LineBreak.Heading
- "Paragraph" -> LineBreak.Paragraph
- else -> throw IllegalArgumentException("Unrecognized LineBreak value for this test")
-}
-
-private fun LineBreak.toTestString(): String = when (this) {
- LineBreak.Simple -> "Simple"
- LineBreak.Heading -> "Heading"
- LineBreak.Paragraph -> "Paragraph"
- else -> throw IllegalArgumentException("Unrecognized LineBreak value for this test")
-}
-
-private fun String.toHyphens(): Hyphens = when (this) {
- "None" -> Hyphens.None
- "Auto" -> Hyphens.Auto
- else -> throw IllegalArgumentException("Unrecognized Hyphens value for this test")
-}
-
-private fun Hyphens.toTestString(): String = when (this) {
- Hyphens.None -> "None"
- Hyphens.Auto -> "Auto"
- else -> throw IllegalArgumentException("Unrecognized Hyphens value for this test")
-}
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 59a93d9..e5c14c2 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -609,7 +609,7 @@
}
public final class FocusRestorerKt {
- method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier focusRestorer(androidx.compose.ui.Modifier);
+ method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier focusRestorer(androidx.compose.ui.Modifier, optional kotlin.jvm.functions.Function0<androidx.compose.ui.focus.FocusRequester>? onRestoreFailed);
}
public interface FocusState {
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 7608ff3..71da5eb 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -609,7 +609,7 @@
}
public final class FocusRestorerKt {
- method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier focusRestorer(androidx.compose.ui.Modifier);
+ method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier focusRestorer(androidx.compose.ui.Modifier, optional kotlin.jvm.functions.Function0<androidx.compose.ui.focus.FocusRequester>? onRestoreFailed);
}
public interface FocusState {
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/FocusSamples.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/FocusSamples.kt
index 69035e5..4041e63 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/FocusSamples.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/FocusSamples.kt
@@ -134,6 +134,27 @@
}
}
+@OptIn(ExperimentalComposeUiApi::class)
+@Sampled
+@Composable
+fun FocusRestorerCustomFallbackSample() {
+ val focusRequester = remember { FocusRequester() }
+ LazyRow(
+ // If restoration fails, focus would fallback to the item associated with focusRequester.
+ Modifier.focusRestorer { focusRequester }
+ ) {
+ item {
+ Button(
+ modifier = Modifier.focusRequester(focusRequester),
+ onClick = {}
+ ) { Text("1") }
+ }
+ item { Button(onClick = {}) { Text("2") } }
+ item { Button(onClick = {}) { Text("3") } }
+ item { Button(onClick = {}) { Text("4") } }
+ }
+}
+
@Sampled
@Composable
fun RequestFocusSample() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRestorerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRestorerTest.kt
index dbe8b98..469567e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRestorerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRestorerTest.kt
@@ -177,4 +177,48 @@
assertThat(grandChildState.isFocused).isFalse()
}
}
+
+ @Test
+ fun restorationFailed_fallbackToOnRestoreFailedDestination() {
+ // Arrange.
+ val (parent, child2) = FocusRequester.createRefs()
+ lateinit var child1State: FocusState
+ lateinit var child2State: FocusState
+ rule.setFocusableContent {
+ Box(
+ Modifier
+ .size(10.dp)
+ .focusRequester(parent)
+ .focusRestorer { child2 }
+ .focusGroup()
+ ) {
+ key(1) {
+ Box(
+ Modifier
+ .size(10.dp)
+ .onFocusChanged { child1State = it }
+ .focusTarget()
+ )
+ }
+ key(2) {
+ Box(
+ Modifier
+ .size(10.dp)
+ .focusRequester(child2)
+ .onFocusChanged { child2State = it }
+ .focusTarget()
+ )
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle { parent.requestFocus() }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(child1State.isFocused).isFalse()
+ assertThat(child2State.isFocused).isTrue()
+ }
+ }
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRestorer.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRestorer.kt
index c490f79..bfa2812 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRestorer.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRestorer.kt
@@ -19,7 +19,6 @@
import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRestorerNode.Companion.FocusRestorerElement
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.Nodes
import androidx.compose.ui.node.currentValueOf
@@ -63,17 +62,25 @@
// TODO: Move focusRestorer to foundation after saveFocusedChild and restoreFocusedChild are stable.
/**
- * This modifier can be uses to save and restore focus to a focus group.
+ * This modifier can be used to save and restore focus to a focus group.
* When focus leaves the focus group, it stores a reference to the item that was previously focused.
* Then when focus re-enters this focus group, it restores focus to the previously focused item.
*
+ * @param onRestoreFailed callback provides a lambda that is invoked if focus restoration fails.
+ * This lambda can be used to return a custom fallback item by providing a [FocusRequester]
+ * attached to that item. This can be used to customize the initially focused item.
+ *
* @sample androidx.compose.ui.samples.FocusRestorerSample
+ * @sample androidx.compose.ui.samples.FocusRestorerCustomFallbackSample
*/
@ExperimentalComposeUiApi
-fun Modifier.focusRestorer(): Modifier = this then FocusRestorerElement
+fun Modifier.focusRestorer(
+ onRestoreFailed: (() -> FocusRequester)? = null
+): Modifier = this then FocusRestorerElement(onRestoreFailed)
-internal class FocusRestorerNode :
- FocusPropertiesModifierNode, FocusRequesterModifierNode, Modifier.Node() {
+internal class FocusRestorerNode(
+ var onRestoreFailed: (() -> FocusRequester)?
+) : FocusPropertiesModifierNode, FocusRequesterModifierNode, Modifier.Node() {
private val onExit: (FocusDirection) -> FocusRequester = {
@OptIn(ExperimentalComposeUiApi::class)
saveFocusedChild()
@@ -83,22 +90,32 @@
@OptIn(ExperimentalComposeUiApi::class)
private val onEnter: (FocusDirection) -> FocusRequester = {
@OptIn(ExperimentalComposeUiApi::class)
- if (restoreFocusedChild()) FocusRequester.Cancel else FocusRequester.Default
+ if (restoreFocusedChild()) {
+ FocusRequester.Cancel
+ } else {
+ onRestoreFailed?.invoke() ?: FocusRequester.Default
+ }
}
+
override fun applyFocusProperties(focusProperties: FocusProperties) {
@OptIn(ExperimentalComposeUiApi::class)
focusProperties.enter = onEnter
@OptIn(ExperimentalComposeUiApi::class)
focusProperties.exit = onExit
}
+}
- companion object {
- val FocusRestorerElement = object : ModifierNodeElement<FocusRestorerNode>() {
- override fun create() = FocusRestorerNode()
- override fun update(node: FocusRestorerNode) {}
- override fun InspectorInfo.inspectableProperties() { name = "focusRestorer" }
- override fun hashCode(): Int = "focusRestorer".hashCode()
- override fun equals(other: Any?) = other === this
- }
+private data class FocusRestorerElement(
+ val onRestoreFailed: (() -> FocusRequester)?
+) : ModifierNodeElement<FocusRestorerNode>() {
+ override fun create() = FocusRestorerNode(onRestoreFailed)
+
+ override fun update(node: FocusRestorerNode) {
+ node.onRestoreFailed = onRestoreFailed
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "focusRestorer"
+ properties["onRestoreFailed"] = onRestoreFailed
}
}
diff --git a/core/core-performance-play-services/build.gradle b/core/core-performance-play-services/build.gradle
index c7a44dd..5ae6276 100644
--- a/core/core-performance-play-services/build.gradle
+++ b/core/core-performance-play-services/build.gradle
@@ -29,23 +29,13 @@
implementation(libs.kotlinCoroutinesCore)
implementation(project(":core:core-performance"))
implementation("androidx.datastore:datastore-preferences:1.0.0")
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
- testImplementation(libs.robolectric)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.truth)
- androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoAndroid)
- testImplementation(libs.testCore)
- testImplementation(libs.kotlinStdlib)
- testImplementation(libs.kotlinCoroutinesTest)
- testImplementation(libs.junit)
- testImplementation(libs.truth)
-
}
android {
diff --git a/credentials/credentials-fido/credentials-fido/build.gradle b/credentials/credentials-fido/credentials-fido/build.gradle
index 400e738..0fa93f9 100644
--- a/credentials/credentials-fido/credentials-fido/build.gradle
+++ b/credentials/credentials-fido/credentials-fido/build.gradle
@@ -15,6 +15,7 @@
*/
import androidx.build.LibraryType
+import androidx.build.Publish
plugins {
id("AndroidXPlugin")
@@ -53,4 +54,5 @@
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2023"
description = "Util library for apps using FIDO"
+ publish = Publish.SNAPSHOT_AND_RELEASE
}
diff --git a/development/update-verification-metadata.sh b/development/update-verification-metadata.sh
index d0a6593e..a0487f3 100755
--- a/development/update-verification-metadata.sh
+++ b/development/update-verification-metadata.sh
@@ -57,7 +57,7 @@
fi
# next, remove 'version=' lines https://github.com/gradle/gradle/issues/20192
- sed -i 's/ \(trusted-key.*\)version="[^"]*"/\1/' gradle/verification-metadata.xml
+ sed -i 's/\(trusted-key.*\)version="[^"]*"/\1/' gradle/verification-metadata.xml
# rename keyring
mv gradle/verification-keyring-dryrun.keys gradle/verification-keyring.keys 2>/dev/null || true
diff --git a/docs/api_guidelines/dependencies.md b/docs/api_guidelines/dependencies.md
index 011fab9..5a43db3 100644
--- a/docs/api_guidelines/dependencies.md
+++ b/docs/api_guidelines/dependencies.md
@@ -107,9 +107,16 @@
image and are made available to developers through the `<uses-library>` manifest
tag.
-Examples include Wear OS extensions (`com.google.android.wearable`), Camera OEM
-extensions (`androidx.camera.extensions.impl`), and Window OEM extensions
-(`androix.window.extensions`).
+Examples include Camera OEM extensions (`androidx.camera.extensions.impl`) and
+Window OEM extensions (`androidx.window.extensions`).
+
+Extension libraries may be defined in AndroidX library projects (see
+`androidx.window.extensions`) or externally, ex. in AOSP alongside the platform.
+In either case, we recommend that libraries use extensions as pinned, rather
+than project-type, dependencies to facilitate versioning across repositories.
+
+*Do not* ship extension interfaces to Google Maven. Teams may choose to ship
+stub JARs publicly, but that is not covered by AndroidX workflows.
Project dependencies on extension libraries **must** use `compileOnly`:
diff --git a/docs/api_guidelines/deprecation.md b/docs/api_guidelines/deprecation.md
index f5dc6d8..2d7069e 100644
--- a/docs/api_guidelines/deprecation.md
+++ b/docs/api_guidelines/deprecation.md
@@ -91,8 +91,9 @@
artifact as `@Deprecated` and update the API files
([example CL](https://android-review.googlesource.com/c/platform/frameworks/support/+/1938773))
1. Schedule a release of the artifact as a new minor version. When you populate
- the release notes, explain that the entire artifact has been deprecated.
- Include the reason for deprecation and the migration strategy.
+ the release notes, explain that the entire artifact has been deprecated and
+ will no longer receive new features or bug fixes. Include the reason for
+ deprecation and the migration strategy.
1. After the artifact has been released, remove the artifact from the source
tree, versions file, and tip-of-tree docs configuration
([example CL](https://android-review.googlesource.com/c/platform/frameworks/support/+/2061731/))
@@ -107,3 +108,58 @@
After an artifact has been released as fully-deprecated, it can be removed from
the source tree.
+
+#### Long-term support
+
+Artifacts which have been fully deprecated and removed are not required to fix
+any bugs -- including security issues -- which are reported after the library
+has been removed from source control; however, library owners *may* utilize
+release branches to provide long-term support.
+
+When working on long-term support in a release branch, you may encounter the
+following issues:
+
+- Release metadata produced by the build system is not compatible with the
+ release scheduling tool
+- Build targets associated with the release branch do not match targets used
+ by the snapped build ID
+- Delta between last snapped build ID and proposed snap build ID is too large
+ and cannot be processed by the release branch management tool
+
+### Discouraging usage in Play Store
+
+[Google Play SDK Console](https://play.google.com/sdk-console/) allows library
+owners to annotate specific library versions with notes, which are shown to app
+developers in the Play Store Console, or permanently mark them as outdated,
+which shows a warning in Play Store Console asking app developers to upgrade.
+
+In both cases, library owners have the option to prevent app developers from
+releasing apps to Play Store that have been built against specific library
+versions.
+
+Generally, Jetpack discourages the use of either notes or marking versions as
+outdated. There are few cases that warrant pushing notifications to app
+developers, and it is easy to abuse notes as advertising to drive adoption. As a
+rule, upgrades to Jetpack libraries should be driven by the needs of app
+developers.
+
+Cases where notes may be used include:
+
+1. The library is used directly, rather than transitively, and contains `P0` or
+ `P1` (ship-blocking, from the app's perspective) issues
+ - Transitively-included libraries should instead urge their dependent
+ libraries to bump their pinned dependency versions
+1. The library contains ship-blocking security issues. In this case, we
+ recommend preventing app releases since developers may be less aware of
+ security issues.
+1. The library was built against a pre-release SDK which has been superseded by
+ a finalized SDK. In this case, we recommend preventing app releases since
+ the library may crash or show unexpected behavior.
+
+Cases where marking a version as outdated maybe used:
+
+1. The library has security implications and the version is no longer receiving
+ security updates, e.g. the release branch has moved to the next version.
+
+In all cases, there must be a newer stable or bugfix release of the library that
+app developers can use to replace the outdated version.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ea083dd..bffdc0b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -57,7 +57,7 @@
paparazzi = "1.0.0"
paparazziNative = "2022.1.1-canary-f5f9f71"
skiko = "0.7.7"
-spdxGradlePlugin = "0.1.0"
+spdxGradlePlugin = "0.2.0"
sqldelight = "1.3.0"
retrofit = "2.7.2"
wire = "4.7.0"
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 0b3b82b..a869424 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -27,7 +27,7 @@
<trusted-keys>
<trusted-key id="00089ee8c3afa95a854d0f1df800dd0933ecf7f7" group="com.google.guava"/>
<trusted-key id="019082bc00e0324e2aef4cf00d3b328562a119a7" group="org.openjdk.jmh"/>
- <trusted-key id="03c123038c20aae9e286c857479d601f3a7b5c1a" group="com.github.ajalt.clikt" name="clikt-jvm" version="3.5.3"/>
+ <trusted-key id="03c123038c20aae9e286c857479d601f3a7b5c1a" group="com.github.ajalt.clikt" name="clikt-jvm" />
<trusted-key id="042b29e928995b9db963c636c7ca19b7b620d787" group="org.apache.maven"/>
<trusted-key id="04543577d6a9cc626239c50c7ecbd740ff06aeb5">
<trusting group="com.sun.activation"/>
@@ -221,7 +221,7 @@
<trusting group="com.sun.activation"/>
<trusting group="jakarta.activation"/>
</trusted-key>
- <trusted-key id="6ead752b3e2b38e8e2236d7ba9321edaa5cb3202" group="ch.randelshofer" name="fastdoubleparser" version="0.8.0"/>
+ <trusted-key id="6ead752b3e2b38e8e2236d7ba9321edaa5cb3202" group="ch.randelshofer" name="fastdoubleparser" />
<trusted-key id="6f538074ccebf35f28af9b066a0975f8b1127b83">
<trusting group="org.jetbrains.kotlin"/>
<trusting group="org.jetbrains.kotlin.jvm"/>
@@ -245,9 +245,9 @@
<trusting group="org.jvnet.staxex"/>
<trusting group="^com[.]sun($|([.].*))" regex="true"/>
</trusted-key>
- <trusted-key id="713da88be50911535fe716f5208b0ab1d63011c7" group="org.apache.tomcat" name="annotations-api" version="6.0.53"/>
+ <trusted-key id="713da88be50911535fe716f5208b0ab1d63011c7" group="org.apache.tomcat" name="annotations-api" />
<trusted-key id="720746177725a89207a7075bfd5dea07fcb690a8" group="org.codehaus.mojo"/>
- <trusted-key id="73976c9c39c1479b84e2641a5a68a2249128e2c6" group="com.google.crypto.tink" name="tink-android" version="1.8.0"/>
+ <trusted-key id="73976c9c39c1479b84e2641a5a68a2249128e2c6" group="com.google.crypto.tink" name="tink-android" />
<trusted-key id="748f15b2cf9ba8f024155e6ed7c92b70fa1c814d" group="org.apache.logging.log4j"/>
<trusted-key id="7615ad56144df2376f49d98b1669c4bb543e0445" group="com.google.errorprone"/>
<trusted-key id="7616eb882daf57a11477aaf559a252fb1199d873" group="com.google.code.findbugs"/>
@@ -261,11 +261,11 @@
<trusted-key id="7e22d50a7ebd9d2cd269b2d4056aca74d46000bf" group="io.netty"/>
<trusted-key id="7f36e793ae3252e5d9e9b98fee9e7dc9d92fc896" group="com.google.errorprone"/>
<trusted-key id="7faa0f2206de228f0db01ad741321490758aad6f" group="org.codehaus.groovy"/>
- <trusted-key id="7fe5e98df3a5c0dc34663ab7c1add37ca0069309" group="org.spdx" name="spdx-gradle-plugin" version="0.1.0"/>
+ <trusted-key id="7fe5e98df3a5c0dc34663ab7c1add37ca0069309" group="org.spdx" name="spdx-gradle-plugin"/>
<trusted-key id="808d78b17a5a2d7c3668e31fbffc9b54721244ad" group="org.apache.commons"/>
<trusted-key id="80f6d6b0d90c6747753344cab5a9e81b565e89e0" group="org.tomlj"/>
<trusted-key id="8254180bfc943b816e0b5e2e5e2f2b3d474efe6b" group="it.unimi.dsi"/>
- <trusted-key id="82c9ec0e52c47a936a849e0113d979595e6d01e1" group="org.apache.maven.shared" name="maven-shared-utils" version="3.3.4"/>
+ <trusted-key id="82c9ec0e52c47a936a849e0113d979595e6d01e1" group="org.apache.maven.shared" name="maven-shared-utils" />
<trusted-key id="82f833963889d7ed06f1e4dc6525fd70cc303655" group="org.codehaus.mojo"/>
<trusted-key id="835a685c8c6f49c54980e5caf406f31bc1468eba" group="org.jcodec"/>
<trusted-key id="842afb86375d805422835bfd82b5574242c20d6f" group="org.antlr"/>
@@ -274,7 +274,7 @@
<trusting group="org.apache.maven" name="maven-parent"/>
</trusted-key>
<trusted-key id="8569c95cadc508b09fe90f3002216ed811210daa" group="io.github.detekt.sarif4k"/>
- <trusted-key id="86616cd3c4f0803e73374a434dbf5995d492505d" group="org.json" name="json" version="20230227"/>
+ <trusted-key id="86616cd3c4f0803e73374a434dbf5995d492505d" group="org.json" name="json" />
<trusted-key id="8756c4f765c9ac3cb6b85d62379ce192d401ab61">
<trusting group="com.github.ajalt"/>
<trusting group="com.github.javaparser"/>
@@ -342,7 +342,7 @@
<trusted-key id="ae9e53fc28ff2ab1012273d0bf1518e0160788a2" group="org.apache" name="apache"/>
<trusted-key id="afa2b1823fc021bfd08c211fd5f4c07a434ab3da" group="com.squareup"/>
<trusted-key id="afcc4c7594d09e2182c60e0f7a01b0f236e5430f" group="com.google.code.gson"/>
- <trusted-key id="b02137d875d833d9b23392ecae5a7fb608a0221c" group="org.codehaus.plexus" name="plexus-classworlds" version="2.6.0"/>
+ <trusted-key id="b02137d875d833d9b23392ecae5a7fb608a0221c" group="org.codehaus.plexus" name="plexus-classworlds" />
<trusted-key id="b02335aa54ccf21e52bbf9abd9c565aa72ba2fdd">
<trusting group="com.google.protobuf"/>
<trusting group="io.grpc"/>
@@ -448,7 +448,7 @@
<trusting group="org.codehaus.plexus"/>
</trusted-key>
<trusted-key id="f3184bcd55f4d016e30d4c9bf42e87f9665015c9" group="org.jsoup"/>
- <trusted-key id="f3d15b8ff9902805de4be6b18dc6f3d0abdbd017" group="org.codehaus.plexus" name="plexus-sec-dispatcher" version="2.0"/>
+ <trusted-key id="f3d15b8ff9902805de4be6b18dc6f3d0abdbd017" group="org.codehaus.plexus" name="plexus-sec-dispatcher" />
<trusted-key id="f42b96b8648b5c4a1c43a62fbb2914c1fa0811c3" group="net.bytebuddy"/>
<trusted-key id="fa1703b1d287caea3a60f931e0130a3ed5a2079e" group="org.webjars"/>
<trusted-key id="fa77dcfef2ee6eb2debedd2c012579464d01c06a">
diff --git a/libraryversions.toml b/libraryversions.toml
index 55a1d9a..1f1cb09 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -21,7 +21,7 @@
COLLECTION = "1.4.0-alpha01"
COMPOSE = "1.6.0-alpha05"
COMPOSE_COMPILER = "1.5.3"
-COMPOSE_MATERIAL3 = "1.2.0-alpha07"
+COMPOSE_MATERIAL3 = "1.2.0-alpha08"
COMPOSE_MATERIAL3_ADAPTIVE = "1.0.0-alpha01"
COMPOSE_RUNTIME_TRACING = "1.0.0-alpha05"
CONSTRAINTLAYOUT = "2.2.0-alpha13"
@@ -111,7 +111,7 @@
RECYCLERVIEW_SELECTION = "1.2.0-alpha02"
REMOTECALLBACK = "1.0.0-alpha02"
RESOURCEINSPECTION = "1.1.0-alpha01"
-ROOM = "2.6.0-rc01"
+ROOM = "2.7.0-alpha01"
SAFEPARCEL = "1.0.0-alpha01"
SAVEDSTATE = "1.3.0-alpha01"
SECURITY = "1.1.0-alpha07"
@@ -125,7 +125,7 @@
SLICE_BUILDERS_KTX = "1.0.0-alpha08"
SLICE_REMOTECALLBACK = "1.0.0-alpha01"
SLIDINGPANELAYOUT = "1.3.0-alpha01"
-SQLITE = "2.4.0-rc01"
+SQLITE = "2.5.0-alpha01"
SQLITE_INSPECTOR = "2.1.0-alpha01"
STABLE_AIDL = "1.0.0-alpha01"
STARTUP = "1.2.0-alpha03"
diff --git a/metrics/integration-tests/janktest/build.gradle b/metrics/integration-tests/janktest/build.gradle
index 476a3ae..3aa5881 100644
--- a/metrics/integration-tests/janktest/build.gradle
+++ b/metrics/integration-tests/janktest/build.gradle
@@ -25,6 +25,9 @@
viewBinding true
}
namespace "androidx.metrics.performance.janktest"
+ defaultConfig {
+ multiDexEnabled true
+ }
}
dependencies {
diff --git a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
index dc60ee0..5ae8346 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
@@ -171,6 +171,7 @@
assertWithMessage("Replacement Fragment should be the primary navigation Fragment")
.that(fragmentManager.primaryNavigationFragment)
.isSameInstanceAs(replacementFragment)
+ assertThat(navigatorState.transitionsInProgress.value).isEmpty()
}
@UiThreadTest
@@ -359,6 +360,7 @@
assertWithMessage("Fragment should be the primary navigation Fragment after pop")
.that(fragmentManager.primaryNavigationFragment)
.isSameInstanceAs(fragment)
+ assertThat(navigatorState.transitionsInProgress.value).isEmpty()
}
@UiThreadTest
@@ -528,6 +530,26 @@
assertWithMessage("PrimaryFragment should be the correct type")
.that(fragmentManager.primaryNavigationFragment)
.isNotInstanceOf(EmptyFragment::class.java)
+ assertThat(navigatorState.transitionsInProgress.value).isEmpty()
+ }
+
+ @UiThreadTest
+ @Test
+ fun testMultipleNavigateFragmentTransactionsThenPopMultiple() {
+ val entry = createBackStackEntry()
+ val secondEntry = createBackStackEntry(SECOND_FRAGMENT, clazz = Fragment::class)
+ val thirdEntry = createBackStackEntry(THIRD_FRAGMENT)
+
+ // Push 3 fragments
+ fragmentNavigator.navigate(listOf(entry, secondEntry, thirdEntry), null, null)
+ fragmentManager.executePendingTransactions()
+
+ // Now pop multiple fragments with savedState so that the secondEntry does not get
+ // marked complete by clear viewModel
+ fragmentNavigator.popBackStack(secondEntry, true)
+ fragmentManager.executePendingTransactions()
+ assertThat(navigatorState.backStack.value).containsExactly(entry)
+ assertThat(navigatorState.transitionsInProgress.value).isEmpty()
}
@UiThreadTest
diff --git a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
index 52c924c..b7e0009 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
@@ -77,6 +77,9 @@
assertWithMessage("New Entry should be RESUMED")
.that(navController.currentBackStackEntry!!.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
+ assertThat(navController.visibleEntries.value).containsExactly(
+ navController.currentBackStackEntry!!
+ )
}
@Test
@@ -217,6 +220,7 @@
// ensure original Fragment is dismissed and backStacks are in sync
assertThat(navigator.backStack.value.size).isEqualTo(1)
assertThat(fm.fragments.size).isEqualTo(2) // start + dialog fragment
+ assertThat(navController.visibleEntries.value.size).isEqualTo(2)
}
@Test
@@ -310,6 +314,9 @@
fm?.executePendingTransactions()
assertThat(navController.currentBackStackEntry?.destination?.route).isEqualTo("first")
+ assertThat(navController.visibleEntries.value).containsExactly(
+ navController.currentBackStackEntry
+ )
}
@LargeTest
@@ -335,6 +342,9 @@
onBackPressedDispatcher.onBackPressed()
assertThat(navController.currentBackStackEntry?.destination?.route).isEqualTo("third")
+ assertThat(navController.visibleEntries.value).containsExactly(
+ navController.currentBackStackEntry!!
+ )
}
@LargeTest
@@ -364,6 +374,9 @@
onBackPressedDispatcher.onBackPressed()
assertThat(navController.currentBackStackEntry?.destination?.route).isEqualTo("fourth")
+ assertThat(navController.visibleEntries.value).containsExactly(
+ navController.currentBackStackEntry!!
+ )
}
private fun withNavigationActivity(
diff --git a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
index 3fb3248..0c8b113 100644
--- a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
+++ b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
@@ -85,16 +85,14 @@
entry.id == fragment.tag
}
if (entry != null) {
- if (!state.backStack.value.contains(entry)) {
- if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
- Log.v(
- TAG,
- "Marking transition complete for entry $entry " +
- "due to fragment $source lifecycle reaching DESTROYED"
- )
- }
- state.markTransitionComplete(entry)
+ if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+ Log.v(
+ TAG,
+ "Marking transition complete for entry $entry " +
+ "due to fragment $source lifecycle reaching DESTROYED"
+ )
}
+ state.markTransitionComplete(entry)
}
}
}
@@ -113,19 +111,16 @@
}
state.markTransitionComplete(entry)
}
- // Once the lifecycle reaches DESTROYED, if the entry is not in the back stack, we can
- // mark the transition complete
+ // Once the lifecycle reaches DESTROYED, we can mark the transition complete
if (event == Lifecycle.Event.ON_DESTROY) {
- if (!state.backStack.value.contains(entry)) {
- if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
- Log.v(
- TAG,
- "Marking transition complete for entry $entry due " +
- "to fragment $owner view lifecycle reaching DESTROYED"
- )
- }
- state.markTransitionComplete(entry)
+ if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+ Log.v(
+ TAG,
+ "Marking transition complete for entry $entry due " +
+ "to fragment $owner view lifecycle reaching DESTROYED"
+ )
}
+ state.markTransitionComplete(entry)
}
}
}
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
index 3b662ee..2ddc413 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
@@ -186,6 +186,9 @@
assertThat(navigator.backStack.size)
.isEqualTo(1)
assertThat(originalViewModel.isCleared).isTrue()
+ assertThat(navController.visibleEntries.value).containsExactly(
+ navController.currentBackStackEntry
+ )
}
@UiThreadTest
@@ -215,6 +218,7 @@
val newViewModel = ViewModelProvider(newBackStackEntry).get<TestAndroidViewModel>()
assertThat(newBackStackEntry.id).isSameInstanceAs(originalBackStackEntry.id)
assertThat(newViewModel).isSameInstanceAs(originalViewModel)
+ assertThat(navController.visibleEntries.value).containsExactly(newBackStackEntry)
}
@UiThreadTest
@@ -606,6 +610,9 @@
navController.navigate(R.id.second_test)
assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.second_test)
assertThat(navigator.backStack.size).isEqualTo(2)
+ assertThat(navController.visibleEntries.value).containsExactly(
+ navController.currentBackStackEntry
+ )
}
@UiThreadTest
@@ -2028,6 +2035,7 @@
.isFalse()
assertThat(navController.currentDestination).isNull()
assertThat(navigator.backStack.size).isEqualTo(0)
+ assertThat(navController.visibleEntries.value).isEmpty()
}
@UiThreadTest
@@ -2074,6 +2082,9 @@
.isTrue()
assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.start_test)
assertThat(navigator.backStack.size).isEqualTo(1)
+ assertThat(navController.visibleEntries.value).containsExactly(
+ navController.currentBackStackEntry
+ )
}
@UiThreadTest
@@ -2092,6 +2103,9 @@
navigator.popCurrent()
assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.start_test)
assertThat(navigator.backStack.size).isEqualTo(1)
+ assertThat(navController.visibleEntries.value).containsExactly(
+ navController.currentBackStackEntry
+ )
}
@UiThreadTest
@@ -2132,6 +2146,9 @@
)
assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.second_test)
assertThat(navigator.backStack.size).isEqualTo(1)
+ assertThat(navController.visibleEntries.value).containsExactly(
+ navController.currentBackStackEntry
+ )
}
@UiThreadTest
@@ -2172,6 +2189,9 @@
.isTrue()
assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.start_test)
assertThat(navigator.backStack.size).isEqualTo(1)
+ assertThat(navController.visibleEntries.value).containsExactly(
+ navController.currentBackStackEntry
+ )
}
@UiThreadTest
@@ -2229,6 +2249,9 @@
navController.navigate(R.id.self)
assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.second_test)
assertThat(navigator.backStack.size).isEqualTo(2)
+ assertThat(navController.visibleEntries.value).containsExactly(
+ navController.currentBackStackEntry
+ )
}
@UiThreadTest
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index 51d0426..87d7074 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -126,8 +126,9 @@
* whenever they change. If there is no visible [NavBackStackEntry], this will be set to an
* empty list.
*
- * - `CREATED` entries are listed first and include all entries that have been popped from
- * the back stack and are in the process of completing their exit transition
+ * - `CREATED` entries are listed first and include all entries that are in the process of
+ * completing their exit transition. Note that this can include entries that have been
+ * popped off the Navigation back stack.
* - `STARTED` entries on the back stack are next and include all entries that are running
* their enter transition and entries whose destination is partially covered by a
* `FloatingWindow` destination
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/build.gradle b/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
index 5de128a..e8985b4 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
+++ b/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
@@ -92,7 +92,6 @@
dependencies {
api(libs.kotlinStdlib)
api(libs.kotlinCoroutinesCore)
- implementation(libs.multidex)
implementation("androidx.core:core-ktx:1.12.0-alpha05")
api project(path: ':privacysandbox:sdkruntime:sdkruntime-core')
diff --git a/privacysandbox/ui/ui-tests/build.gradle b/privacysandbox/ui/ui-tests/build.gradle
index cf66642..77e6e30 100644
--- a/privacysandbox/ui/ui-tests/build.gradle
+++ b/privacysandbox/ui/ui-tests/build.gradle
@@ -44,6 +44,9 @@
android {
namespace "androidx.privacysandbox.ui.tests"
+ defaultConfig {
+ multiDexEnabled true
+ }
}
androidx {
diff --git a/tv/tv-material/api/api_lint.ignore b/tv/tv-material/api/api_lint.ignore
new file mode 100644
index 0000000..5954166
--- /dev/null
+++ b/tv/tv-material/api/api_lint.ignore
@@ -0,0 +1,4 @@
+// Baseline format: 1.0
+
+GetterSetterNames: field NavigationDrawerScope.doesNavigationDrawerHaveFocus:
+ Invalid name for boolean property `doesNavigationDrawerHaveFocus`. Should start with one of `has`, `can`, `should`, `is`.
\ No newline at end of file
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index 1ea43bf..dcdfda26 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -690,6 +690,10 @@
property public final androidx.tv.material3.Glow selectedGlow;
}
+ public final class NavigationDrawerItemKt {
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawerItem(androidx.tv.material3.NavigationDrawerScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> leadingContent, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional float tonalElevation, optional androidx.tv.material3.NavigationDrawerItemShape shape, optional androidx.tv.material3.NavigationDrawerItemColors colors, optional androidx.tv.material3.NavigationDrawerItemScale scale, optional androidx.tv.material3.NavigationDrawerItemBorder border, optional androidx.tv.material3.NavigationDrawerItemGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
@SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemScale {
ctor public NavigationDrawerItemScale(@FloatRange(from=0.0) float scale, @FloatRange(from=0.0) float focusedScale, @FloatRange(from=0.0) float pressedScale, @FloatRange(from=0.0) float selectedScale, @FloatRange(from=0.0) float disabledScale, @FloatRange(from=0.0) float focusedSelectedScale, @FloatRange(from=0.0) float focusedDisabledScale, @FloatRange(from=0.0) float pressedSelectedScale);
method public float getDisabledScale();
@@ -737,14 +741,14 @@
}
public final class NavigationDrawerKt {
- method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void ModalNavigationDrawer(kotlin.jvm.functions.Function1<? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, optional androidx.compose.ui.graphics.Brush scrimBrush, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawer(kotlin.jvm.functions.Function1<? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void ModalNavigationDrawer(kotlin.jvm.functions.Function2<? super androidx.tv.material3.NavigationDrawerScope,? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, optional androidx.compose.ui.graphics.Brush scrimBrush, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawer(kotlin.jvm.functions.Function2<? super androidx.tv.material3.NavigationDrawerScope,? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static androidx.tv.material3.DrawerState rememberDrawerState(androidx.tv.material3.DrawerValue initialValue);
}
@SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public interface NavigationDrawerScope {
- method public boolean isActivated();
- property public abstract boolean isActivated;
+ method public boolean getDoesNavigationDrawerHaveFocus();
+ property public abstract boolean doesNavigationDrawerHaveFocus;
}
@SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NonInteractiveSurfaceColors {
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index 1ea43bf..dcdfda26 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -690,6 +690,10 @@
property public final androidx.tv.material3.Glow selectedGlow;
}
+ public final class NavigationDrawerItemKt {
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawerItem(androidx.tv.material3.NavigationDrawerScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> leadingContent, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional float tonalElevation, optional androidx.tv.material3.NavigationDrawerItemShape shape, optional androidx.tv.material3.NavigationDrawerItemColors colors, optional androidx.tv.material3.NavigationDrawerItemScale scale, optional androidx.tv.material3.NavigationDrawerItemBorder border, optional androidx.tv.material3.NavigationDrawerItemGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
@SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemScale {
ctor public NavigationDrawerItemScale(@FloatRange(from=0.0) float scale, @FloatRange(from=0.0) float focusedScale, @FloatRange(from=0.0) float pressedScale, @FloatRange(from=0.0) float selectedScale, @FloatRange(from=0.0) float disabledScale, @FloatRange(from=0.0) float focusedSelectedScale, @FloatRange(from=0.0) float focusedDisabledScale, @FloatRange(from=0.0) float pressedSelectedScale);
method public float getDisabledScale();
@@ -737,14 +741,14 @@
}
public final class NavigationDrawerKt {
- method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void ModalNavigationDrawer(kotlin.jvm.functions.Function1<? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, optional androidx.compose.ui.graphics.Brush scrimBrush, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawer(kotlin.jvm.functions.Function1<? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void ModalNavigationDrawer(kotlin.jvm.functions.Function2<? super androidx.tv.material3.NavigationDrawerScope,? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, optional androidx.compose.ui.graphics.Brush scrimBrush, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawer(kotlin.jvm.functions.Function2<? super androidx.tv.material3.NavigationDrawerScope,? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static androidx.tv.material3.DrawerState rememberDrawerState(androidx.tv.material3.DrawerValue initialValue);
}
@SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public interface NavigationDrawerScope {
- method public boolean isActivated();
- property public abstract boolean isActivated;
+ method public boolean getDoesNavigationDrawerHaveFocus();
+ property public abstract boolean doesNavigationDrawerHaveFocus;
}
@SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NonInteractiveSurfaceColors {
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerItemScreenshotTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerItemScreenshotTest.kt
new file mode 100644
index 0000000..e98dd07
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerItemScreenshotTest.kt
@@ -0,0 +1,379 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChild
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@OptIn(ExperimentalTvMaterial3Api::class)
+class NavigationDrawerItemScreenshotTest(private val scheme: ColorSchemeWrapper) {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @get:Rule
+ val screenshotRule = AndroidXScreenshotTestRule(TV_GOLDEN_MATERIAL3)
+
+ val wrapperModifier = Modifier
+ .testTag(NavigationDrawerItemWrapperTag)
+ .background(scheme.colorScheme.surface)
+ .padding(20.dp)
+
+ @Test
+ fun navigationDrawerItem_customColor() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = false,
+ onClick = {},
+ leadingContent = {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = null,
+ modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+ )
+ },
+ colors = NavigationDrawerItemDefaults.colors(containerColor = Color.Red)
+ ) {
+ Text("Favourite")
+ }
+ }
+ }
+
+ assertAgainstGolden("navigationDrawerItem_${scheme.name}_customColor")
+ }
+
+ @Test
+ fun navigationDrawerItem_oneLine() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = false,
+ onClick = {},
+ leadingContent = {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = null,
+ modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+ )
+ }
+ ) {
+ Text("Favourite")
+ }
+ }
+ }
+
+ assertAgainstGolden("navigationDrawerItem_${scheme.name}_oneLine")
+ }
+
+ @Test
+ fun navigationDrawerItem_twoLine() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = false,
+ onClick = {},
+ leadingContent = {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = null,
+ modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+ )
+ },
+ supportingContent = { Text("You like this") }
+ ) {
+ Text("Favourite")
+ }
+ }
+ }
+
+ assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine")
+ }
+
+ @Test
+ fun navigationDrawerItem_twoLine_focused() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = false,
+ onClick = {},
+ leadingContent = {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = null,
+ modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+ )
+ },
+ supportingContent = { Text("You like this") }
+ ) {
+ Text("Favourite")
+ }
+ }
+ }
+
+ rule.onNodeWithTag(NavigationDrawerItemWrapperTag)
+ .onChild()
+ .requestFocus()
+ rule.waitForIdle()
+
+ assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine_focused")
+ }
+
+ @Test
+ fun navigationDrawerItem_twoLine_disabled() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = false,
+ onClick = {},
+ enabled = false,
+ leadingContent = {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = null,
+ modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+ )
+ },
+ supportingContent = { Text("You like this") }
+ ) {
+ Text("Favourite")
+ }
+ }
+ }
+
+ assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine_disabled")
+ }
+
+ @Test
+ fun navigationDrawerItem_twoLine_focusedDisabled() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = false,
+ onClick = {},
+ enabled = false,
+ leadingContent = {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = null,
+ modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+ )
+ },
+ supportingContent = { Text("You like this") }
+ ) {
+ Text("Favourite")
+ }
+ }
+ }
+
+ rule.onNodeWithTag(NavigationDrawerItemWrapperTag)
+ .onChild()
+ .requestFocus()
+ rule.waitForIdle()
+
+ assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine_focusedDisabled")
+ }
+
+ @Test
+ fun navigationDrawerItem_twoLine_selected() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = true,
+ onClick = {},
+ leadingContent = {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = null,
+ modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+ )
+ },
+ supportingContent = { Text("You like this") }
+ ) {
+ Text("Favourite")
+ }
+ }
+ }
+
+ assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine_selected")
+ }
+
+ @Test
+ fun navigationDrawerItem_twoLine_focusedSelected() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = true,
+ onClick = {},
+ leadingContent = {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = null,
+ modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+ )
+ },
+ supportingContent = { Text("You like this") }
+ ) {
+ Text("Favourite")
+ }
+ }
+ }
+
+ rule.onNodeWithTag(NavigationDrawerItemWrapperTag)
+ .onChild()
+ .requestFocus()
+ rule.waitForIdle()
+
+ assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine_focusedSelected")
+ }
+
+ @Test
+ fun navigationDrawerItem_inactive() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ DrawerScope(false) {
+ NavigationDrawerItem(
+ selected = false,
+ onClick = {},
+ leadingContent = {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = null,
+ modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+ )
+ },
+ ) {
+ Text("Favourite")
+ }
+ }
+ }
+
+ assertAgainstGolden("navigationDrawerItem_${scheme.name}_inactive")
+ }
+
+ @Test
+ fun navigationDrawerItem_inactive_selected() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ DrawerScope(false) {
+ NavigationDrawerItem(
+ selected = true,
+ onClick = {},
+ leadingContent = {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = null,
+ modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+ )
+ },
+ ) {
+ Text("Favourite")
+ }
+ }
+ }
+
+ assertAgainstGolden("navigationDrawerItem_${scheme.name}_inactive_selected")
+ }
+
+ @Test
+ fun navigationDrawerItem_twoLine_withTrailingContent() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = false,
+ onClick = {},
+ leadingContent = {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = null,
+ modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+ )
+ },
+ supportingContent = { Text("You like this") },
+ trailingContent = {
+ NavigationDrawerItemDefaults.TrailingBadge("NEW")
+ }
+ ) {
+ Text("Favourite")
+ }
+ }
+ }
+
+ assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine_withTrailingContent")
+ }
+
+ private fun assertAgainstGolden(goldenName: String) {
+ rule.onNodeWithTag(NavigationDrawerItemWrapperTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, goldenName)
+ }
+
+ // Provide the ColorScheme and their name parameter in a ColorSchemeWrapper.
+ // This makes sure that the default method name and the initial Scuba image generated
+ // name is as expected.
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun parameters() = arrayOf(
+ ColorSchemeWrapper("lightTheme", lightColorScheme()),
+ ColorSchemeWrapper("darkTheme", darkColorScheme()),
+ )
+ }
+
+ class ColorSchemeWrapper constructor(val name: String, val colorScheme: ColorScheme) {
+ override fun toString(): String {
+ return name
+ }
+ }
+
+ @Composable
+ private fun DrawerScope(
+ doesNavigationDrawerHaveFocus: Boolean = true,
+ content: @Composable NavigationDrawerScope.() -> Unit
+ ) {
+ Box(wrapperModifier) {
+ NavigationDrawerScopeImpl(doesNavigationDrawerHaveFocus).apply {
+ content()
+ }
+ }
+ }
+}
+
+private const val NavigationDrawerItemWrapperTag = "navigationDrawerItem_wrapper"
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerItemTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerItemTest.kt
new file mode 100644
index 0000000..74d9ee4
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerItemTest.kt
@@ -0,0 +1,443 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsEqualTo
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performKeyInput
+import androidx.compose.ui.test.pressKey
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.width
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(
+ ExperimentalTestApi::class,
+ ExperimentalTvMaterial3Api::class
+)
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class NavigationDrawerItemTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun navigationDrawerItem_findByTagAndClick() {
+ var counter = 0
+ val onClick: () -> Unit = { ++counter }
+
+ rule.setContent {
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = false,
+ onClick = onClick,
+ leadingContent = { },
+ modifier = Modifier.testTag(NavigationDrawerItemTag),
+ ) {
+ Text(text = "Test Text")
+ }
+ }
+ }
+
+ rule.onNodeWithTag(NavigationDrawerItemTag)
+ .requestFocus()
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ rule.runOnIdle {
+ Truth.assertThat(counter).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun navigationDrawerItem_clickIsIndependentBetweenItems() {
+ var openItemClickCounter = 0
+ val openItemOnClick: () -> Unit = { ++openItemClickCounter }
+ val openItemTag = "OpenItem"
+
+ var closeItemClickCounter = 0
+ val closeItemOnClick: () -> Unit = { ++closeItemClickCounter }
+ val closeItemTag = "CloseItem"
+
+ rule.setContent {
+ DrawerScope {
+ Column {
+ NavigationDrawerItem(
+ selected = false,
+ onClick = openItemOnClick,
+ leadingContent = { },
+ modifier = Modifier.testTag(openItemTag),
+ ) {
+ Text(text = "Test Text")
+ }
+ NavigationDrawerItem(
+ selected = false,
+ onClick = closeItemOnClick,
+ leadingContent = { },
+ modifier = Modifier.testTag(closeItemTag),
+ ) {
+ Text(text = "Test Text")
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(openItemTag)
+ .requestFocus()
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+
+ rule.runOnIdle {
+ Truth.assertThat(openItemClickCounter).isEqualTo(1)
+ Truth.assertThat(closeItemClickCounter).isEqualTo(0)
+ }
+
+ rule.onNodeWithTag(closeItemTag)
+ .requestFocus()
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+
+ rule.runOnIdle {
+ Truth.assertThat(openItemClickCounter).isEqualTo(1)
+ Truth.assertThat(closeItemClickCounter).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun navigationDrawerItem_longClickAction() {
+ var counter = 0
+ val onLongClick: () -> Unit = { ++counter }
+
+ rule.setContent {
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = false,
+ onClick = { },
+ onLongClick = onLongClick,
+ leadingContent = { },
+ modifier = Modifier.testTag(NavigationDrawerItemTag),
+ ) {
+ Text(text = "Test Text")
+ }
+ }
+ }
+
+ rule.onNodeWithTag(NavigationDrawerItemTag)
+ .requestFocus()
+ .performLongKeyPress(rule, Key.DirectionCenter)
+ rule.runOnIdle {
+ Truth.assertThat(counter).isEqualTo(1)
+ }
+
+ rule.onNodeWithTag(NavigationDrawerItemTag)
+ .requestFocus()
+ .performLongKeyPress(rule, Key.DirectionCenter, count = 2)
+ rule.runOnIdle {
+ Truth.assertThat(counter).isEqualTo(3)
+ }
+ }
+
+ @Test
+ fun navigationDrawerItem_findByTagAndStateChangeCheck() {
+ var checkedState by mutableStateOf(true)
+ val onClick: () -> Unit = { checkedState = !checkedState }
+
+ rule.setContent {
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = checkedState,
+ onClick = onClick,
+ leadingContent = { },
+ modifier = Modifier.testTag(NavigationDrawerItemTag),
+ ) {
+ Text(text = "Test Text")
+ }
+ }
+ }
+
+ rule.onNodeWithTag(NavigationDrawerItemTag)
+ .requestFocus()
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ rule.runOnIdle {
+ Truth.assertThat(!checkedState)
+ }
+ }
+
+ @Test
+ fun navigationDrawerItem_trailingContentPadding() {
+ val testTrailingContentTag = "TrailingIconTag"
+
+ rule.setContent {
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = false,
+ onClick = {},
+ trailingContent = {
+ Box(
+ modifier = Modifier
+ .size(NavigationDrawerItemDefaults.IconSize)
+ .background(Color.Red)
+ .testTag(testTrailingContentTag)
+ )
+ },
+ leadingContent = { },
+ modifier = Modifier
+ .testTag(NavigationDrawerItemTag)
+ .border(1.dp, Color.Blue),
+ ) {
+ Text(
+ text = "Test Text",
+ modifier = Modifier
+ .testTag(NavigationDrawerItemTextTag)
+ .fillMaxWidth()
+ )
+ }
+ }
+ }
+
+ rule.waitForIdle()
+
+ val itemBounds = rule.onNodeWithTag(NavigationDrawerItemTag).getUnclippedBoundsInRoot()
+ val textBounds = rule.onNodeWithTag(
+ NavigationDrawerItemTextTag,
+ useUnmergedTree = true
+ ).getUnclippedBoundsInRoot()
+ val trailingContentBounds = rule
+ .onNodeWithTag(testTrailingContentTag, useUnmergedTree = true)
+ .getUnclippedBoundsInRoot()
+
+ (itemBounds.bottom - trailingContentBounds.bottom).assertIsEqualTo(
+ 16.dp,
+ "padding between the bottom of the trailing content and the bottom of the nav " +
+ "drawer item"
+ )
+
+ (itemBounds.right - trailingContentBounds.right).assertIsEqualTo(
+ 16.dp,
+ "padding between the end of the trailing content and the end of the nav drawer item"
+ )
+
+ (trailingContentBounds.left - textBounds.right).assertIsEqualTo(
+ 8.dp,
+ "padding between the start of the trailing content and the end of the text."
+ )
+ }
+
+ @Test
+ fun navigationDrawerItem_semantics() {
+ var selected by mutableStateOf(false)
+ rule.setContent {
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = selected,
+ onClick = { selected = !selected },
+ leadingContent = { },
+ modifier = Modifier.testTag(NavigationDrawerItemTag),
+ ) {
+ Text(text = "Test Text")
+ }
+ }
+ }
+
+ rule.onNodeWithTag(NavigationDrawerItemTag)
+ .assertHasClickAction()
+ .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Selected))
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.Selected, false))
+ .requestFocus()
+ .assertIsEnabled()
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.Selected, true))
+ Truth.assertThat(selected).isEqualTo(true)
+ }
+
+ @Test
+ fun navigationDrawerItem_longClickSemantics() {
+ var selected by mutableStateOf(false)
+ rule.setContent {
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = selected,
+ onClick = {},
+ onLongClick = { selected = !selected },
+ leadingContent = { },
+ modifier = Modifier.testTag(NavigationDrawerItemTag),
+ ) {
+ Text(text = "Test Text")
+ }
+ }
+ }
+
+ rule.onNodeWithTag(NavigationDrawerItemTag)
+ .assertHasClickAction()
+ .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.OnLongClick))
+ .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Selected))
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.Selected, false))
+ .requestFocus()
+ .assertIsEnabled()
+ .performLongKeyPress(rule, Key.DirectionCenter)
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.Selected, true))
+ Truth.assertThat(selected).isEqualTo(true)
+ }
+
+ @Test
+ fun navigationDrawerItem_disabledSemantics() {
+ rule.setContent {
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = false,
+ onClick = {},
+ enabled = false,
+ leadingContent = { },
+ modifier = Modifier.testTag(NavigationDrawerItemTag),
+ ) {
+ Text(text = "Test Text")
+ }
+ }
+ }
+
+ rule.onNodeWithTag(NavigationDrawerItemTag)
+ .assertIsNotEnabled()
+ }
+
+ @Test
+ fun navigationDrawerItem_canBeDisabled() {
+ rule.setContent {
+ var enabled by remember { mutableStateOf(true) }
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = false,
+ onClick = { enabled = false },
+ leadingContent = { },
+ enabled = enabled,
+ modifier = Modifier.testTag(NavigationDrawerItemTag),
+ ) {
+ Text(text = "Test Text")
+ }
+ }
+ }
+ rule.onNodeWithTag(NavigationDrawerItemTag)
+ // Confirm the button starts off enabled, with a click action
+ .assertHasClickAction()
+ .assertIsEnabled()
+ .requestFocus()
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ // Then confirm it's disabled with click action after clicking it
+ .assertHasClickAction()
+ .assertIsNotEnabled()
+ }
+
+ @Test
+ fun navigationDrawerItem_oneLineHeight() {
+ val expectedHeightNoIcon = 56.dp
+
+ rule.setContent {
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = false,
+ onClick = {},
+ leadingContent = { },
+ modifier = Modifier.testTag(NavigationDrawerItemTag),
+ ) {
+ Text(text = "Test Text")
+ }
+ }
+ }
+
+ rule.onNodeWithTag(NavigationDrawerItemTag).assertHeightIsEqualTo(expectedHeightNoIcon)
+ }
+
+ @Test
+ fun navigationDrawerItem_width() {
+ rule.setContent {
+ DrawerScope {
+ NavigationDrawerItem(
+ selected = false,
+ onClick = { },
+ leadingContent = { },
+ modifier = Modifier.testTag(NavigationDrawerItemTag),
+ ) {
+ Text(text = "Test Text")
+ }
+ }
+ }
+ rule.onNodeWithTag(NavigationDrawerItemTag)
+ .assertWidthIsEqualTo(rule.onRoot().getUnclippedBoundsInRoot().width)
+ }
+
+ @Test
+ fun navigationDrawerItem_widthInInactiveState() {
+ rule.setContent {
+ DrawerScope(false) {
+ NavigationDrawerItem(
+ selected = false,
+ onClick = { },
+ leadingContent = { },
+ modifier = Modifier.testTag(NavigationDrawerItemTag),
+ ) {
+ Text(text = "Test Text")
+ }
+ }
+ }
+ rule.onNodeWithTag(NavigationDrawerItemTag)
+ .assertWidthIsEqualTo(56.dp)
+ }
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun DrawerScope(
+ isActivated: Boolean = true,
+ content: @Composable NavigationDrawerScope.() -> Unit
+) {
+ Box {
+ NavigationDrawerScopeImpl(isActivated).apply {
+ content()
+ }
+ }
+}
+
+private const val NavigationDrawerItemTag = "NavigationDrawerItem"
+private const val NavigationDrawerItemTextTag = "NavigationDrawerItemText"
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawer.kt b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawer.kt
index 049ddf1..320fe9e 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawer.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawer.kt
@@ -79,7 +79,7 @@
@ExperimentalTvMaterial3Api
@Composable
fun ModalNavigationDrawer(
- drawerContent: @Composable (DrawerValue) -> Unit,
+ drawerContent: @Composable NavigationDrawerScope.(DrawerValue) -> Unit,
modifier: Modifier = Modifier,
drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
scrimBrush: Brush = SolidColor(LocalColorScheme.current.scrim.copy(alpha = 0.5f)),
@@ -155,7 +155,7 @@
@ExperimentalTvMaterial3Api
@Composable
fun NavigationDrawer(
- drawerContent: @Composable (DrawerValue) -> Unit,
+ drawerContent: @Composable NavigationDrawerScope.(DrawerValue) -> Unit,
modifier: Modifier = Modifier,
drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
content: @Composable () -> Unit
@@ -237,7 +237,7 @@
modifier: Modifier = Modifier,
drawerState: DrawerState = remember { DrawerState() },
sizeAnimationFinishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null,
- content: @Composable (DrawerValue) -> Unit
+ content: @Composable NavigationDrawerScope.(DrawerValue) -> Unit
) {
// indicates that the drawer has been set to its initial state and has grabbed focus if
// necessary. Controls whether focus is used to decide the state of the drawer going forward.
@@ -269,7 +269,11 @@
}
.focusGroup()
- Box(modifier = internalModifier) { content.invoke(drawerState.currentValue) }
+ Box(modifier = internalModifier) {
+ NavigationDrawerScopeImpl(drawerState.currentValue == DrawerValue.Open).apply {
+ content(drawerState.currentValue)
+ }
+ }
}
private const val ClosedDrawerWidth = 80
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItem.kt b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItem.kt
new file mode 100644
index 0000000..967d1c2f
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItem.kt
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.unit.Dp
+
+/**
+ * TV Material Design navigation drawer item.
+ *
+ * A [NavigationDrawerItem] represents a destination within drawers, either [NavigationDrawer] or
+ * [ModalNavigationDrawer]
+ *
+ * @sample androidx.tv.samples.SampleNavigationDrawer
+ * @sample androidx.tv.samples.SampleModalNavigationDrawerWithSolidScrim
+ * @sample androidx.tv.samples.SampleModalNavigationDrawerWithGradientScrim
+ *
+ * @param selected defines whether this composable is selected or not
+ * @param onClick called when this composable is clicked
+ * @param leadingContent the leading content of the list item
+ * @param modifier to be applied to the list item
+ * @param enabled controls the enabled state of this composable. When `false`, this component will
+ * not respond to user input, and it will appear visually disabled and disabled to accessibility
+ * services
+ * @param onLongClick called when this composable is long clicked (long-pressed)
+ * @param supportingContent the content displayed below the headline content
+ * @param trailingContent the trailing meta badge or icon
+ * @param tonalElevation the tonal elevation of this composable
+ * @param shape defines the shape of Composable's container in different interaction states
+ * @param colors defines the background and content colors used in the composable
+ * for different interaction states
+ * @param scale defines the size of the composable relative to its original size in
+ * different interaction states
+ * @param border defines a border around the composable in different interaction states
+ * @param glow defines a shadow to be shown behind the composable for different interaction states
+ * @param interactionSource the [MutableInteractionSource] representing the stream of
+ * [Interaction]s for this component. You can create and pass in your own [remember]ed instance
+ * to observe [Interaction]s and customize the appearance / behavior of this composable in different
+ * states
+ * @param content main content of this composable
+ */
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
+@Composable
+fun NavigationDrawerScope.NavigationDrawerItem(
+ selected: Boolean,
+ onClick: () -> Unit,
+ leadingContent: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ onLongClick: (() -> Unit)? = null,
+ supportingContent: (@Composable () -> Unit)? = null,
+ trailingContent: (@Composable () -> Unit)? = null,
+ tonalElevation: Dp = NavigationDrawerItemDefaults.NavigationDrawerItemElevation,
+ shape: NavigationDrawerItemShape = NavigationDrawerItemDefaults.shape(),
+ colors: NavigationDrawerItemColors = NavigationDrawerItemDefaults.colors(),
+ scale: NavigationDrawerItemScale = NavigationDrawerItemScale.None,
+ border: NavigationDrawerItemBorder = NavigationDrawerItemDefaults.border(),
+ glow: NavigationDrawerItemGlow = NavigationDrawerItemDefaults.glow(),
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ content: @Composable () -> Unit,
+) {
+ val animatedWidth by animateDpAsState(
+ targetValue = if (doesNavigationDrawerHaveFocus) {
+ NavigationDrawerItemDefaults.ExpandedDrawerItemWidth
+ } else {
+ NavigationDrawerItemDefaults.CollapsedDrawerItemWidth
+ },
+ label = "NavigationDrawerItem width open/closed state of the drawer item"
+ )
+ val navDrawerItemHeight = if (supportingContent == null) {
+ NavigationDrawerItemDefaults.ContainerHeightOneLine
+ } else {
+ NavigationDrawerItemDefaults.ContainerHeightTwoLine
+ }
+ ListItem(
+ selected = selected,
+ onClick = onClick,
+ headlineContent = {
+ AnimatedVisibility(
+ visible = doesNavigationDrawerHaveFocus,
+ enter = NavigationDrawerItemDefaults.ContentAnimationEnter,
+ exit = NavigationDrawerItemDefaults.ContentAnimationExit,
+ ) {
+ content()
+ }
+ },
+ leadingContent = {
+ Box(Modifier.size(NavigationDrawerItemDefaults.IconSize)) {
+ leadingContent()
+ }
+ },
+ trailingContent = if (trailingContent == null) null else {
+ {
+ AnimatedVisibility(
+ visible = doesNavigationDrawerHaveFocus,
+ enter = NavigationDrawerItemDefaults.ContentAnimationEnter,
+ exit = NavigationDrawerItemDefaults.ContentAnimationExit,
+ ) {
+ trailingContent()
+ }
+ }
+ },
+ supportingContent = if (supportingContent == null) null else {
+ {
+ AnimatedVisibility(
+ visible = doesNavigationDrawerHaveFocus,
+ enter = NavigationDrawerItemDefaults.ContentAnimationEnter,
+ exit = NavigationDrawerItemDefaults.ContentAnimationExit,
+ ) {
+ supportingContent()
+ }
+ }
+ },
+ modifier = modifier
+ .layout { measurable, constraints ->
+ val width = animatedWidth.roundToPx()
+ val height = navDrawerItemHeight.roundToPx()
+ val placeable = measurable.measure(
+ constraints.copy(
+ minWidth = width,
+ maxWidth = width,
+ minHeight = height,
+ maxHeight = height,
+ )
+ )
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ },
+ enabled = enabled,
+ onLongClick = onLongClick,
+ tonalElevation = tonalElevation,
+ shape = shape.toToggleableListItemShape(),
+ colors = colors.toToggleableListItemColors(doesNavigationDrawerHaveFocus),
+ scale = scale.toToggleableListItemScale(),
+ border = border.toToggleableListItemBorder(),
+ glow = glow.toToggleableListItemGlow(),
+ interactionSource = interactionSource,
+ )
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun NavigationDrawerItemShape.toToggleableListItemShape() =
+ ListItemDefaults.shape(
+ shape = shape,
+ focusedShape = focusedShape,
+ pressedShape = pressedShape,
+ selectedShape = selectedShape,
+ disabledShape = disabledShape,
+ focusedSelectedShape = focusedSelectedShape,
+ focusedDisabledShape = focusedDisabledShape,
+ pressedSelectedShape = pressedSelectedShape,
+ )
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun NavigationDrawerItemColors.toToggleableListItemColors(
+ doesNavigationDrawerHaveFocus: Boolean
+) =
+ ListItemDefaults.colors(
+ containerColor = containerColor,
+ contentColor = if (doesNavigationDrawerHaveFocus) contentColor else inactiveContentColor,
+ focusedContainerColor = focusedContainerColor,
+ focusedContentColor = focusedContentColor,
+ pressedContainerColor = pressedContainerColor,
+ pressedContentColor = pressedContentColor,
+ selectedContainerColor = selectedContainerColor,
+ selectedContentColor = selectedContentColor,
+ disabledContainerColor = disabledContainerColor,
+ disabledContentColor =
+ if (doesNavigationDrawerHaveFocus) disabledContentColor else disabledInactiveContentColor,
+ focusedSelectedContainerColor = focusedSelectedContainerColor,
+ focusedSelectedContentColor = focusedSelectedContentColor,
+ pressedSelectedContainerColor = pressedSelectedContainerColor,
+ pressedSelectedContentColor = pressedSelectedContentColor,
+ )
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun NavigationDrawerItemScale.toToggleableListItemScale() =
+ ListItemDefaults.scale(
+ scale = scale,
+ focusedScale = focusedScale,
+ pressedScale = pressedScale,
+ selectedScale = selectedScale,
+ disabledScale = disabledScale,
+ focusedSelectedScale = focusedSelectedScale,
+ focusedDisabledScale = focusedDisabledScale,
+ pressedSelectedScale = pressedSelectedScale,
+ )
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun NavigationDrawerItemBorder.toToggleableListItemBorder() =
+ ListItemDefaults.border(
+ border = border,
+ focusedBorder = focusedBorder,
+ pressedBorder = pressedBorder,
+ selectedBorder = selectedBorder,
+ disabledBorder = disabledBorder,
+ focusedSelectedBorder = focusedSelectedBorder,
+ focusedDisabledBorder = focusedDisabledBorder,
+ pressedSelectedBorder = pressedSelectedBorder,
+ )
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun NavigationDrawerItemGlow.toToggleableListItemGlow() =
+ ListItemDefaults.glow(
+ glow = glow,
+ focusedGlow = focusedGlow,
+ pressedGlow = pressedGlow,
+ selectedGlow = selectedGlow,
+ focusedSelectedGlow = focusedSelectedGlow,
+ pressedSelectedGlow = pressedSelectedGlow,
+ )
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerScope.kt b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerScope.kt
index ccdcec7..27dccf0 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerScope.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerScope.kt
@@ -25,10 +25,10 @@
/**
* Whether any item within the [NavigationDrawer] or [ModalNavigationDrawer] is focused
*/
- val isActivated: Boolean
+ val doesNavigationDrawerHaveFocus: Boolean
}
@OptIn(ExperimentalTvMaterial3Api::class)
-internal class NavigationDrawerScopeImpl constructor(
- override val isActivated: Boolean
+internal class NavigationDrawerScopeImpl(
+ override val doesNavigationDrawerHaveFocus: Boolean
) : NavigationDrawerScope
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt
index 3140de6..9b8dbb5 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt
@@ -21,9 +21,13 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.TouchInjectionScope
import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
@@ -301,6 +305,64 @@
undoActionModifier = Modifier.testTag(TEST_TAG)
)
+ @Test
+ fun onRightSwipe_dispatchEventsToParent() {
+ var onPreScrollDispatch = 0f
+ rule.setContent {
+ val nestedScrollConnection = remember {
+ object : NestedScrollConnection {
+ override fun onPreScroll(
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ onPreScrollDispatch = available.x
+ return available
+ }
+ }
+ }
+ Box(
+ modifier = Modifier.nestedScroll(nestedScrollConnection)
+ ) {
+ swipeToRevealWithDefaults(
+ modifier = Modifier.testTag(TEST_TAG)
+ )
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeRight() }
+
+ assert(onPreScrollDispatch > 0)
+ }
+
+ @Test
+ fun onLeftSwipe_dispatchEventsToParent() {
+ var onPreScrollDispatch = 0f
+ rule.setContent {
+ val nestedScrollConnection = remember {
+ object : NestedScrollConnection {
+ override fun onPreScroll(
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ onPreScrollDispatch = available.x
+ return available
+ }
+ }
+ }
+ Box(
+ modifier = Modifier.nestedScroll(nestedScrollConnection)
+ ) {
+ swipeToRevealWithDefaults(
+ modifier = Modifier.testTag(TEST_TAG)
+ )
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeLeft() }
+
+ assert(onPreScrollDispatch < 0) // Swiping left means the dispatch will be negative
+ }
+
private fun verifyLastClickAction(
expectedClickType: RevealActionType,
initialRevealValue: RevealValue,
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeableV2Test.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeableV2Test.kt
index 9f96a9d..5c8e950 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeableV2Test.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeableV2Test.kt
@@ -17,13 +17,22 @@
package androidx.wear.compose.foundation
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.rememberScrollableState
+import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration
@@ -32,15 +41,23 @@
import androidx.compose.ui.semantics.SemanticsProperties.VerticalScrollAxisRange
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.TouchInjectionScope
import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeDown
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeRight
+import androidx.compose.ui.test.swipeUp
import androidx.compose.ui.unit.dp
import kotlin.math.absoluteValue
import org.junit.Rule
import org.junit.Test
+internal const val CHILD_TEST_TAG = "childTestTag"
+
// TODO(b/201009199) Some of these tests may need specific values adjusted when swipeableV2
// supports property nested scrolling, but the tests should all still be valid.
@OptIn(ExperimentalWearFoundationApi::class)
@@ -221,6 +238,260 @@
.assert(SemanticsMatcher.keyNotDefined(VerticalScrollAxisRange))
}
+ @Test
+ fun onSwipeLeft_sendsPreScrollEventToParent() {
+ var delta = 0f
+ rule.testSwipe(
+ touchInput = { swipeLeft() },
+ consumePreScrollDelta = { offset ->
+ delta = offset.x
+ }
+ )
+
+ assert(delta < 0) {
+ "Expected delta to be negative, was $delta"
+ }
+ }
+
+ @Test
+ fun onSwipeRight_sendsPreScrollEventToParent() {
+ var delta = 0f
+ rule.testSwipe(
+ touchInput = { swipeRight() },
+ consumePreScrollDelta = { offset ->
+ delta = offset.x
+ }
+ )
+
+ assert(delta > 0) {
+ "Expected delta to be positive, was $delta"
+ }
+ }
+
+ @Test
+ fun onSwipeUp_sendsPreScrollEventToParent() {
+ var delta = 0f
+ rule.testSwipe(
+ touchInput = { swipeUp() },
+ consumePreScrollDelta = { offset ->
+ delta = offset.y
+ },
+ orientation = Orientation.Vertical
+ )
+
+ assert(delta < 0) {
+ "Expected delta to be negative, was $delta"
+ }
+ }
+
+ @Test
+ fun onSwipeDown_sendsPreScrollEventToParent() {
+ var delta = 0f
+ rule.testSwipe(
+ touchInput = { swipeDown() },
+ consumePreScrollDelta = { offset ->
+ delta = offset.y
+ },
+ orientation = Orientation.Vertical
+ )
+
+ assert(delta > 0) {
+ "Expected delta to be positive, was $delta"
+ }
+ }
+
+ @Test
+ fun onSwipeLeft_sendsPostScrollEventToParent() {
+ var delta = 0f
+ rule.testSwipe(
+ touchInput = { swipeLeft() },
+ consumePostScrollDelta = { offset ->
+ delta = offset.x
+ }
+ )
+
+ assert(delta < 0) {
+ "Expected delta to be negative, was $delta"
+ }
+ }
+
+ @Test
+ fun onSwipeRight_sendsPostScrollEventToParent() {
+ var delta = 0f
+ rule.testSwipe(
+ touchInput = { swipeRight() },
+ consumePostScrollDelta = { offset ->
+ delta = offset.x
+ },
+ reverseAnchors = true // reverse anchors or else swipeable consumes whole delta
+ )
+
+ assert(delta > 0) {
+ "Expected delta to be positive, was $delta"
+ }
+ }
+
+ @Test
+ fun onSwipeUp_sendsPostScrollEventToParent() {
+ var delta = 0f
+ rule.testSwipe(
+ touchInput = { swipeUp() },
+ consumePostScrollDelta = { offset ->
+ delta = offset.y
+ },
+ orientation = Orientation.Vertical
+ )
+
+ assert(delta < 0) {
+ "Expected delta to be negative, was $delta"
+ }
+ }
+
+ @Test
+ fun onSwipeDown_sendsPostScrollEventToParent() {
+ var delta = 0f
+ rule.testSwipe(
+ touchInput = { swipeDown() },
+ consumePostScrollDelta = { offset ->
+ delta = offset.y
+ },
+ orientation = Orientation.Vertical,
+ reverseAnchors = true // reverse anchors or else swipeable consumes whole delta
+ )
+
+ assert(delta > 0) {
+ "Expected delta to be positive, was $delta"
+ }
+ }
+
+ @Test
+ fun onSwipeLeftToChild_sendsPreScrollEventToParent() {
+ var delta = 0f
+ rule.testSwipe(
+ touchInput = { swipeLeft() },
+ consumePreScrollDelta = { offset ->
+ delta = offset.x
+ },
+ testTag = CHILD_TEST_TAG
+ )
+
+ assert(delta < 0) {
+ "Expected delta to be negative, was $delta"
+ }
+ }
+
+ @Test
+ fun onSwipeRightToChild_sendsPreScrollEventToParent() {
+ var delta = 0f
+ rule.testSwipe(
+ touchInput = { swipeRight() },
+ consumePreScrollDelta = { offset ->
+ delta = offset.x
+ },
+ testTag = CHILD_TEST_TAG
+ )
+
+ assert(delta > 0) {
+ "Expected delta to be positive, was $delta"
+ }
+ }
+
+ private fun ComposeContentTestRule.testSwipe(
+ touchInput: TouchInjectionScope.() -> Unit,
+ consumePreScrollDelta: (Offset) -> Unit = {},
+ consumePostScrollDelta: (Offset) -> Unit = {},
+ orientation: Orientation = Orientation.Horizontal,
+ reverseAnchors: Boolean = false,
+ testTag: String = TEST_TAG
+ ) {
+ setContent {
+ val nestedScrollConnection = remember {
+ object : NestedScrollConnection {
+ override fun onPreScroll(
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ consumePreScrollDelta(available)
+ return super.onPreScroll(available, source)
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ consumePostScrollDelta(available)
+ return super.onPostScroll(consumed, available, source)
+ }
+ }
+ }
+ Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) {
+ SwipeableContent(
+ orientation = orientation,
+ reverseAnchors = reverseAnchors,
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .testTag(CHILD_TEST_TAG)
+ .nestedScroll(remember { object : NestedScrollConnection {} })
+ .scrollable(
+ state = rememberScrollableState { _ ->
+ 0f // Do not consume any delta, just return it
+ },
+ orientation = orientation
+ )
+ )
+ }
+ }
+ }
+
+ onNodeWithTag(testTag).performTouchInput { touchInput() }
+ }
+
+ @Composable
+ private fun SwipeableContent(
+ modifier: Modifier = Modifier,
+ orientation: Orientation = Orientation.Horizontal,
+ reverseAnchors: Boolean = false,
+ content: @Composable BoxScope.() -> Unit = {}
+ ) {
+ // To participate as a producer of scroll events
+ val nestedScrollDispatcher = remember { NestedScrollDispatcher() }
+ // To participate as a consumer of scroll events
+ val nestedScrollConnection = remember { object : NestedScrollConnection {} }
+ val swipeableV2State = remember {
+ SwipeableV2State(
+ initialValue = false,
+ nestedScrollDispatcher = nestedScrollDispatcher
+ )
+ }
+ val factor = if (reverseAnchors) -1 else 1
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .nestedScroll(nestedScrollConnection)
+ .swipeableV2(
+ swipeableV2State,
+ orientation
+ )
+ .swipeAnchors(
+ state = swipeableV2State,
+ possibleValues = setOf(false, true)
+ ) { value, layoutSize ->
+ when (value) {
+ false -> 0f
+ true -> factor * (
+ if (orientation == Orientation.Horizontal) layoutSize.width.toFloat()
+ else layoutSize.height.toFloat()
+ )
+ }
+ }
+ .nestedScroll(nestedScrollConnection, nestedScrollDispatcher),
+ content = content
+ )
+ }
+
/**
* A square [Box] has the [TEST_TAG] test tag. Touch slop is disabled to make swipe calculations
* more exact.
diff --git a/wear/compose/compose-foundation/src/main/baseline-prof.txt b/wear/compose/compose-foundation/src/main/baseline-prof.txt
index 00329b5..edfde2d 100644
--- a/wear/compose/compose-foundation/src/main/baseline-prof.txt
+++ b/wear/compose/compose-foundation/src/main/baseline-prof.txt
@@ -30,21 +30,28 @@
SPLandroidx/wear/compose/foundation/ExpandableItemsDefaults;->**(**)**
HSPLandroidx/wear/compose/foundation/ExpandableKt**->**(**)**
HSPLandroidx/wear/compose/foundation/ExpandableState;->**(**)**
-PLandroidx/wear/compose/foundation/FocusNode;->**(**)**
-HPLandroidx/wear/compose/foundation/HierarchicalFocusCoordinatorKt**->**(**)**
-PLandroidx/wear/compose/foundation/InternalMutatorMutex;->**(**)**
+SPLandroidx/wear/compose/foundation/FocusNode;->**(**)**
+HSPLandroidx/wear/compose/foundation/HierarchicalFocusCoordinatorKt**->**(**)**
+SPLandroidx/wear/compose/foundation/InternalMutatorMutex;->**(**)**
+HSPLandroidx/wear/compose/foundation/Modifiers;->**(**)**
HSPLandroidx/wear/compose/foundation/PaddingWrapper;->**(**)**
HSPLandroidx/wear/compose/foundation/PartialLayoutInfo;->**(**)**
Landroidx/wear/compose/foundation/ReduceMotion;
+HSPLandroidx/wear/compose/foundation/ResourcesKt**->**(**)**
+PLandroidx/wear/compose/foundation/RevealActionType;->**(**)**
HPLandroidx/wear/compose/foundation/RevealScopeImpl;->**(**)**
HPLandroidx/wear/compose/foundation/RevealState;->**(**)**
HPLandroidx/wear/compose/foundation/RevealValue;->**(**)**
-HPLandroidx/wear/compose/foundation/SwipeAnchorsModifier;->**(**)**
+HSPLandroidx/wear/compose/foundation/SwipeAnchorsModifier;->**(**)**
+HSPLandroidx/wear/compose/foundation/SwipeToDismissBoxKt**->**(**)**
+HSPLandroidx/wear/compose/foundation/SwipeToDismissBoxState;->**(**)**
+SPLandroidx/wear/compose/foundation/SwipeToDismissKeys;->**(**)**
+SPLandroidx/wear/compose/foundation/SwipeToDismissValue;->**(**)**
PLandroidx/wear/compose/foundation/SwipeToRevealDefaults;->**(**)**
HPLandroidx/wear/compose/foundation/SwipeToRevealKt**->**(**)**
-PLandroidx/wear/compose/foundation/SwipeableV2Defaults;->**(**)**
-HPLandroidx/wear/compose/foundation/SwipeableV2Kt**->**(**)**
-HPLandroidx/wear/compose/foundation/SwipeableV2State;->**(**)**
+SPLandroidx/wear/compose/foundation/SwipeableV2Defaults;->**(**)**
+HSPLandroidx/wear/compose/foundation/SwipeableV2Kt**->**(**)**
+HSPLandroidx/wear/compose/foundation/SwipeableV2State;->**(**)**
SPLandroidx/wear/compose/foundation/lazy/AutoCenteringParams;->**(**)**
HSPLandroidx/wear/compose/foundation/lazy/CombinedPaddingValues;->**(**)**
HSPLandroidx/wear/compose/foundation/lazy/DefaultScalingLazyListItemInfo;->**(**)**
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt
index b2a3c52..50f3c3b 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt
@@ -51,6 +51,9 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
@@ -183,6 +186,7 @@
positionalThreshold: Density.(totalDistance: Float) -> Float,
internal val anchors: Map<RevealValue, Float>,
internal val coroutineScope: CoroutineScope,
+ internal val nestedScrollDispatcher: NestedScrollDispatcher
) {
/**
* [SwipeableV2State] internal instance for the state.
@@ -197,6 +201,7 @@
)
},
positionalThreshold = positionalThreshold,
+ nestedScrollDispatcher = nestedScrollDispatcher
)
public var lastActionType by mutableStateOf(RevealActionType.None)
@@ -340,9 +345,10 @@
confirmValueChange: (RevealValue) -> Boolean = { true },
positionalThreshold: Density.(totalDistance: Float) -> Float =
SwipeToRevealDefaults.defaultThreshold(),
- anchors: Map<RevealValue, Float> = createAnchors()
+ anchors: Map<RevealValue, Float> = createAnchors(),
): RevealState {
val coroutineScope = rememberCoroutineScope()
+ val nestedScrollDispatcher = remember { NestedScrollDispatcher() }
return remember(initialValue, animationSpec) {
RevealState(
initialValue = initialValue,
@@ -350,7 +356,8 @@
confirmValueChange = confirmValueChange,
positionalThreshold = positionalThreshold,
anchors = anchors,
- coroutineScope = coroutineScope
+ coroutineScope = coroutineScope,
+ nestedScrollDispatcher = nestedScrollDispatcher
)
}
}
@@ -404,6 +411,8 @@
content: @Composable () -> Unit
) {
val revealScope = remember(state) { RevealScopeImpl(state) }
+ // A no-op NestedScrollConnection which does not consume scroll/fling events
+ val noOpNestedScrollConnection = remember { object : NestedScrollConnection {} }
Box(
modifier = modifier
.swipeableV2(
@@ -421,6 +430,10 @@
// Multiply the anchor with -1f to get the actual swipeable anchor
-state.swipeAnchors[value]!! * swipeableWidth
}
+ // NestedScrollDispatcher sends the scroll/fling events from the node to its parent
+ // and onwards including the modifier chain. Apply it in the end to let nested scroll
+ // connection applied before this modifier consume the scroll/fling events.
+ .nestedScroll(noOpNestedScrollConnection, state.nestedScrollDispatcher)
) {
val swipeCompleted by remember {
derivedStateOf { state.currentValue == RevealValue.Revealed }
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
index 784c92f..94200e9 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
@@ -39,6 +39,9 @@
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
@@ -55,6 +58,7 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import kotlin.math.abs
import kotlinx.coroutines.CancellationException
@@ -126,6 +130,9 @@
}
}
+ // Update the orientation in the swipeable state
+ state.orientation = orientation
+
return this.then(semantics).draggable(
state = state.swipeDraggableState,
orientation = orientation,
@@ -218,6 +225,7 @@
internal val positionalThreshold: Density.(totalDistance: Float) -> Float =
SwipeableV2Defaults.PositionalThreshold,
internal val velocityThreshold: Dp = SwipeableV2Defaults.VelocityThreshold,
+ private val nestedScrollDispatcher: NestedScrollDispatcher? = null
) {
private val swipeMutex = InternalMutatorMutex()
@@ -242,6 +250,11 @@
}
/**
+ * The orientation in which the swipeable can be swiped.
+ */
+ internal var orientation = Orientation.Horizontal
+
+ /**
* The current value of the [SwipeableV2State].
*/
var currentValue: T by mutableStateOf(initialValue)
@@ -427,17 +440,29 @@
* Find the closest anchor taking into account the velocity and settle at it with an animation.
*/
suspend fun settle(velocity: Float) {
+ var availableVelocity = velocity
+ // Dispatch the velocity to parent nodes for consuming
+ nestedScrollDispatcher?.let {
+ val consumedVelocity = nestedScrollDispatcher.dispatchPreFling(
+ if (orientation == Orientation.Horizontal) {
+ Velocity(x = velocity, y = 0f)
+ } else {
+ Velocity(x = 0f, y = velocity)
+ }
+ )
+ availableVelocity -= (consumedVelocity.x + consumedVelocity.y)
+ }
val previousValue = this.currentValue
val targetValue = computeTarget(
offset = requireOffset(),
currentValue = previousValue,
- velocity = velocity
+ velocity = availableVelocity
)
if (confirmValueChange(targetValue)) {
- animateTo(targetValue, velocity)
+ animateTo(targetValue, availableVelocity)
} else {
// If the user vetoed the state change, rollback to the previous state.
- animateTo(previousValue, velocity)
+ animateTo(previousValue, availableVelocity)
}
}
@@ -447,14 +472,41 @@
* @return The delta the consumed by the [SwipeableV2State]
*/
fun dispatchRawDelta(delta: Float): Float {
+ var remainingDelta = delta
+
+ // Dispatch the delta as a scroll event to parent node for consuming it
+ nestedScrollDispatcher?.let {
+ val consumedByParent = nestedScrollDispatcher.dispatchPreScroll(
+ available = offsetWithOrientation(remainingDelta),
+ source = NestedScrollSource.Drag
+ )
+ remainingDelta -= (consumedByParent.x + consumedByParent.y)
+ }
val currentDragPosition = offset ?: 0f
- val potentiallyConsumed = currentDragPosition + delta
+ val potentiallyConsumed = currentDragPosition + remainingDelta
val clamped = potentiallyConsumed.coerceIn(minOffset, maxOffset)
val deltaToConsume = clamped - currentDragPosition
if (abs(deltaToConsume) >= 0) {
offset = ((offset ?: 0f) + deltaToConsume).coerceIn(minOffset, maxOffset)
}
- return deltaToConsume
+
+ nestedScrollDispatcher?.let {
+ val consumedDelta = nestedScrollDispatcher.dispatchPostScroll(
+ consumed = offsetWithOrientation(deltaToConsume),
+ available = offsetWithOrientation(delta - deltaToConsume),
+ source = NestedScrollSource.Drag
+ )
+ remainingDelta -= (deltaToConsume + consumedDelta.x + consumedDelta.y)
+ }
+ return remainingDelta
+ }
+
+ private fun offsetWithOrientation(delta: Float): Offset {
+ return if (orientation == Orientation.Horizontal) {
+ Offset(x = delta, y = 0f)
+ } else {
+ Offset(x = 0f, y = delta)
+ }
}
private fun computeTarget(
diff --git a/wear/compose/compose-material/src/main/baseline-prof.txt b/wear/compose/compose-material/src/main/baseline-prof.txt
index de41148..8c7b94c 100644
--- a/wear/compose/compose-material/src/main/baseline-prof.txt
+++ b/wear/compose/compose-material/src/main/baseline-prof.txt
@@ -67,7 +67,7 @@
HPLandroidx/wear/compose/material/PlaceholderKt**->**(**)**
PLandroidx/wear/compose/material/PlaceholderModifier;->**(**)**
HPLandroidx/wear/compose/material/PlaceholderShimmerModifier;->**(**)**
-PLandroidx/wear/compose/material/PlaceholderStage;->**(**)**
+HPLandroidx/wear/compose/material/PlaceholderStage;->**(**)**
HPLandroidx/wear/compose/material/PlaceholderState;->**(**)**
HSPLandroidx/wear/compose/material/PositionIndicatorAlignment;->**(**)**
HSPLandroidx/wear/compose/material/PositionIndicatorKt**->**(**)**
@@ -146,7 +146,12 @@
HSPLandroidx/wear/compose/materialcore/IconKt**->**(**)**
HPLandroidx/wear/compose/materialcore/RangeDefaults;->**(**)**
PLandroidx/wear/compose/materialcore/RangeIcons;->**(**)**
+HPLandroidx/wear/compose/materialcore/RepeatableClickableKt**->**(**)**
+HSPLandroidx/wear/compose/materialcore/ResourcesKt**->**(**)**
+HPLandroidx/wear/compose/materialcore/SelectionControlsKt**->**(**)**
+PLandroidx/wear/compose/materialcore/SelectionStage;->**(**)**
HPLandroidx/wear/compose/materialcore/SliderKt**->**(**)**
PLandroidx/wear/compose/materialcore/StepperDefaults;->**(**)**
HPLandroidx/wear/compose/materialcore/StepperKt**->**(**)**
HSPLandroidx/wear/compose/materialcore/TextKt**->**(**)**
+HSPLandroidx/wear/compose/materialcore/ToggleButtonKt**->**(**)**
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
index ee7dddf..a13d6d5 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
@@ -170,6 +170,9 @@
ComposableDemo("Swipe To Reveal - Undo") {
SwipeToRevealWithDifferentUndo()
},
+ ComposableDemo("S2R + EdgeSwipeToDismiss") { params ->
+ SwipeToRevealWithEdgeSwipeToDismiss(params.navigateBack)
+ },
ComposableDemo("Material S2R Chip") {
SwipeToRevealChipSample()
},
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToRevealDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToRevealDemo.kt
index a8029d3..e37c1fc 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToRevealDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToRevealDemo.kt
@@ -16,6 +16,7 @@
package androidx.wear.compose.integration.demos
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -43,12 +44,15 @@
import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
import androidx.wear.compose.foundation.RevealActionType
import androidx.wear.compose.foundation.RevealValue
+import androidx.wear.compose.foundation.SwipeToDismissBox
import androidx.wear.compose.foundation.createAnchors
+import androidx.wear.compose.foundation.edgeSwipeToDismiss
import androidx.wear.compose.foundation.expandableItem
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.rememberExpandableState
import androidx.wear.compose.foundation.rememberExpandableStateMapping
import androidx.wear.compose.foundation.rememberRevealState
+import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
import androidx.wear.compose.material.AppCard
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
@@ -240,6 +244,45 @@
}
}
+@OptIn(ExperimentalWearMaterialApi::class, ExperimentalWearFoundationApi::class)
+@Composable
+fun SwipeToRevealWithEdgeSwipeToDismiss(
+ navigateBack: () -> Unit
+) {
+ val swipeToDismissBoxState = rememberSwipeToDismissBoxState()
+ SwipeToDismissBox(
+ state = swipeToDismissBoxState,
+ onDismissed = navigateBack
+ ) {
+ ScalingLazyColumn(
+ contentPadding = PaddingValues(0.dp)
+ ) {
+ repeat(5) {
+ item {
+ SwipeToRevealChip(
+ modifier = Modifier
+ .fillMaxWidth()
+ .edgeSwipeToDismiss(swipeToDismissBoxState),
+ primaryAction = SwipeToRevealDefaults.primaryAction(
+ icon = { Icon(SwipeToRevealDefaults.Delete, "Delete") },
+ label = { Text("Delete") }),
+ revealState = rememberRevealState()
+ ) {
+ Chip(
+ onClick = { /*TODO*/ },
+ colors = ChipDefaults.secondaryChipColors(),
+ modifier = Modifier.fillMaxWidth(),
+ label = {
+ Text("S2R Chip with defaults")
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
@OptIn(ExperimentalWearFoundationApi::class, ExperimentalWearMaterialApi::class)
@Composable
private fun SwipeToRevealChipExpandable(
diff --git a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt
index c9871fe..25d3741 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt
@@ -40,8 +40,6 @@
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
- private lateinit var device: UiDevice
-
@Before
fun setUp() {
val instrumentation = InstrumentationRegistry.getInstrumentation()
@@ -64,8 +62,8 @@
val swipeToDismissBox = device.findObject(By.desc(CONTENT_DESCRIPTION))
// Setting a gesture margin is important otherwise gesture nav is triggered.
swipeToDismissBox.setGestureMargin(device.displayWidth / 5)
- repeat(10) {
- swipeToDismissBox.swipe(Direction.RIGHT, 0.75f)
+ repeat(3) {
+ swipeToDismissBox.swipe(Direction.RIGHT, 0.75f, SWIPE_SPEED)
device.waitForIdle()
}
}
@@ -80,4 +78,7 @@
@JvmStatic
fun parameters() = createCompilationParams()
}
+
+ private lateinit var device: UiDevice
+ private val SWIPE_SPEED = 500
}
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
index c36b80e..e8f57d7 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
@@ -28,6 +28,7 @@
import androidx.annotation.Px
import androidx.annotation.RestrictTo
import androidx.annotation.UiThread
+import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import androidx.wear.watchface.RenderParameters.HighlightedElement
import androidx.wear.watchface.complications.ComplicationSlotBounds
@@ -243,8 +244,8 @@
public const val ROUND_RECT: Int = 0
/**
- * For a full screen image complication slot drawn behind the watch face. Note you can only
- * have a single background complication slot.
+ * For a full screen image complication slot drawn behind the watch face. Note you can only have
+ * a single background complication slot.
*/
public const val BACKGROUND: Int = 1
@@ -342,21 +343,6 @@
* expanded by [ComplicationSlotBounds.perComplicationTypeMargins]. Expanded bounds can overlap so
* the [ComplicationSlot] with the lowest id that intersects the coordinates, if any, is selected.
*
- * @param accessibilityTraversalIndex Used to sort Complications when generating accessibility
- * content description labels.
- * @param bounds The complication slot's [ComplicationSlotBounds].
- * @param supportedTypes The list of [ComplicationType]s accepted by this complication slot, must be
- * non-empty. During complication data source selection, each item in this list is compared in
- * turn with entries from a data source's data source's supported types. The first matching entry
- * from `supportedTypes` is chosen. If there are no matches then that data source is not eligible
- * to be selected in this slot.
- * @param defaultPolicy The [DefaultComplicationDataSourcePolicy] which controls the initial
- * complication data source when the watch face is first installed.
- * @param defaultDataSourceType The default [ComplicationType] for the default complication data
- * source.
- * @param configExtras Extras to be merged into the Intent sent when invoking the complication data
- * source chooser activity. This features is intended for OEM watch faces where they have elements
- * that behave like a complication but are in fact entirely watch face specific.
* @property id The Watch Face's ID for the complication slot.
* @property boundsType The [ComplicationSlotBoundsTypeIntDef] of the complication slot.
* @property canvasComplicationFactory The [CanvasComplicationFactory] used to generate a
@@ -373,6 +359,25 @@
* complication slot.
*/
public class ComplicationSlot
+/**
+ * Constructs a [ComplicationSlot].
+ *
+ * @param accessibilityTraversalIndex Used to sort Complications when generating accessibility
+ * content description labels.
+ * @param bounds The complication slot's [ComplicationSlotBounds].
+ * @param supportedTypes The list of [ComplicationType]s accepted by this complication slot, must be
+ * non-empty. During complication data source selection, each item in this list is compared in
+ * turn with entries from a data source's data source's supported types. The first matching entry
+ * from `supportedTypes` is chosen. If there are no matches then that data source is not eligible
+ * to be selected in this slot.
+ * @param defaultPolicy The [DefaultComplicationDataSourcePolicy] which controls the initial
+ * complication data source when the watch face is first installed.
+ * @param defaultDataSourceType The default [ComplicationType] for the default complication data
+ * source.
+ * @param configExtras Extras to be merged into the Intent sent when invoking the complication data
+ * source chooser activity. This features is intended for OEM watch faces where they have elements
+ * that behave like a complication but are in fact entirely watch face specific.
+ */
@ComplicationExperimental
internal constructor(
public val id: Int,
@@ -391,8 +396,7 @@
screenReaderNameResourceId: Int?,
// TODO(b/230364881): This should really be public but some metalava bug is preventing
// @ComplicationExperimental from working on the getter so it's currently hidden.
- @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public val boundingArc: BoundingArc?
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public val boundingArc: BoundingArc?
) {
/**
* The [ComplicationSlotsManager] this is attached to. Only set after the
@@ -430,7 +434,8 @@
private var lastComplicationUpdate = Instant.EPOCH
- private class ComplicationDataHistoryEntry(
+ @VisibleForTesting
+ internal class ComplicationDataHistoryEntry(
val complicationData: ComplicationData,
val time: Instant
)
@@ -439,7 +444,8 @@
* There doesn't seem to be a convenient ring buffer in the standard library so implement our
* own one.
*/
- private class RingBuffer(val size: Int) : Iterable<ComplicationDataHistoryEntry> {
+ @VisibleForTesting
+ internal class RingBuffer(val size: Int) : Iterable<ComplicationDataHistoryEntry> {
private val entries = arrayOfNulls<ComplicationDataHistoryEntry>(size)
private var readIndex = 0
private var writeIndex = 0
@@ -467,9 +473,11 @@
/**
* In userdebug builds maintain a history of the last [MAX_COMPLICATION_HISTORY_ENTRIES]-1
- * complications, which is logged in dumpsys to help debug complication issues.
+ * complications sent by the system, which is logged in dumpsys to help debug complication
+ * issues.
*/
- private val complicationHistory =
+ @VisibleForTesting
+ internal val complicationHistory =
if (Build.TYPE.equals("userdebug")) {
RingBuffer(MAX_COMPLICATION_HISTORY_ENTRIES)
} else {
@@ -666,8 +674,11 @@
)
}
+ /** Builder for constructing [ComplicationSlot]s. */
+ @OptIn(ComplicationExperimental::class)
+ public class Builder
/**
- * Builder for constructing [ComplicationSlot]s.
+ * Constructs a [Builder].
*
* @param id The watch face's ID for this complication. Can be any integer but should be unique
* within the watch face.
@@ -683,8 +694,6 @@
* @param complicationTapFilter The [ComplicationTapFilter] used to perform hit testing for this
* complication.
*/
- @OptIn(ComplicationExperimental::class)
- public class Builder
internal constructor(
private val id: Int,
private val canvasComplicationFactory: CanvasComplicationFactory,
@@ -1010,18 +1019,38 @@
/**
* Sets the current [ComplicationData] and if it's a timeline, the correct override for
- * [instant] is chosen.
+ * [instant] is chosen. Any images associated with the complication are loaded asynchronously
+ * and the complication history is updated.
*/
- internal fun setComplicationData(
- complicationData: ComplicationData,
- loadDrawablesAsynchronous: Boolean,
- instant: Instant
- ) {
+ internal fun setComplicationData(complicationData: ComplicationData, instant: Instant) {
lastComplicationUpdate = instant
complicationHistory?.push(ComplicationDataHistoryEntry(complicationData, instant))
timelineComplicationData = complicationData
timelineEntries = complicationData.asWireComplicationData().timelineEntries?.toList()
- selectComplicationDataForInstant(instant, loadDrawablesAsynchronous, true)
+ selectComplicationDataForInstant(
+ instant,
+ loadDrawablesAsynchronous = true,
+ forceUpdate = true
+ )
+ }
+
+ /**
+ * Sets the current [ComplicationData] and if it's a timeline, the correct override for
+ * [instant] is chosen. Any images are loaded synchronously. The complication history is not
+ * updated.
+ */
+ internal fun setComplicationDataForScreenshot(
+ complicationData: ComplicationData,
+ instant: Instant
+ ) {
+ lastComplicationUpdate = instant
+ timelineComplicationData = complicationData
+ timelineEntries = complicationData.asWireComplicationData().timelineEntries?.toList()
+ selectComplicationDataForInstant(
+ instant,
+ loadDrawablesAsynchronous = false,
+ forceUpdate = true
+ )
}
/**
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
index d9cdbd9..22d34a0 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
@@ -327,14 +327,14 @@
return
}
complication.dataDirty = complication.dataDirty || (complication.renderer.getData() != data)
- complication.setComplicationData(data, true, instant)
+ complication.setComplicationData(data, instant)
}
/**
* For use by screen shot code which will reset the data afterwards, hence dirty bit not set.
*/
@UiThread
- internal fun setComplicationDataUpdateSync(
+ internal fun setComplicationDataUpdateForScreenshot(
complicationSlotId: Int,
data: ComplicationData,
instant: Instant
@@ -348,7 +348,7 @@
)
return
}
- complication.setComplicationData(data, false, instant)
+ complication.setComplicationDataForScreenshot(data, instant)
}
/**
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index b2e238e..568894e 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -252,8 +252,9 @@
}
.apply { setContext(context) }
- val engine = watchFaceService.createHeadlessEngine(componentName)
- as WatchFaceService.EngineWrapper
+ val engine =
+ watchFaceService.createHeadlessEngine(componentName)
+ as WatchFaceService.EngineWrapper
val headlessWatchFaceImpl = engine.createHeadlessInstance(params)
return engine.deferredWatchFaceImpl.await().WFEditorDelegate(headlessWatchFaceImpl)
}
@@ -472,10 +473,7 @@
}
}
- /**
- * The [OverlayStyle]. This feature is unimplemented on any platform, and will be
- * removed.
- */
+ /** The [OverlayStyle]. This feature is unimplemented on any platform, and will be removed. */
@Deprecated("OverlayStyle will be removed in a future release.")
@Suppress("Deprecation")
public var overlayStyle: OverlayStyle = OverlayStyle()
@@ -655,8 +653,7 @@
private val tapListener = watchface.tapListener
internal var complicationDeniedDialogIntent = watchface.complicationDeniedDialogIntent
internal var complicationRationaleDialogIntent = watchface.complicationRationaleDialogIntent
- @Suppress("Deprecation")
- internal var overlayStyle = watchface.overlayStyle
+ @Suppress("Deprecation") internal var overlayStyle = watchface.overlayStyle
private var mockTime = MockTime(1.0, 0, Long.MAX_VALUE)
@@ -671,27 +668,25 @@
init {
val context = watchFaceHostApi.getContext()
- val displayManager =
- context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+ val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
displayManager.registerDisplayListener(
- object : DisplayManager.DisplayListener {
- override fun onDisplayAdded(displayId: Int) {}
+ object : DisplayManager.DisplayListener {
+ override fun onDisplayAdded(displayId: Int) {}
- override fun onDisplayChanged(displayId: Int) {
- val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)!!
- if (display.state == Display.STATE_OFF &&
- watchState.isVisible.value == false) {
- // We want to avoid a glimpse of a stale time when transitioning from
- // hidden to visible, so we render two black frames to clear the buffers
- // when the display has been turned off and the watch is not visible.
- renderer.renderBlackFrame()
- renderer.renderBlackFrame()
- }
+ override fun onDisplayChanged(displayId: Int) {
+ val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)!!
+ if (display.state == Display.STATE_OFF && watchState.isVisible.value == false) {
+ // We want to avoid a glimpse of a stale time when transitioning from
+ // hidden to visible, so we render two black frames to clear the buffers
+ // when the display has been turned off and the watch is not visible.
+ renderer.renderBlackFrame()
+ renderer.renderBlackFrame()
}
+ }
- override fun onDisplayRemoved(displayId: Int) {}
- },
- watchFaceHostApi.getUiThreadHandler()
+ override fun onDisplayRemoved(displayId: Int) {}
+ },
+ watchFaceHostApi.getUiThreadHandler()
)
}
@@ -893,8 +888,11 @@
get() = watchFaceHostApi.getComplicationRationaleIntent()
override var editorObscuresWatchFace: Boolean
- get() = InteractiveInstanceManager
- .getCurrentInteractiveInstance()?.engine?.editorObscuresWatchFace ?: false
+ get() =
+ InteractiveInstanceManager.getCurrentInteractiveInstance()
+ ?.engine
+ ?.editorObscuresWatchFace
+ ?: false
set(value) {
InteractiveInstanceManager.getCurrentInteractiveInstance()?.engine?.let {
it.editorObscuresWatchFace = value
@@ -915,7 +913,7 @@
slotIdToComplicationData?.let {
for ((id, complicationData) in it) {
- complicationSlotsManager.setComplicationDataUpdateSync(
+ complicationSlotsManager.setComplicationDataUpdateForScreenshot(
id,
complicationData,
instant
@@ -930,7 +928,7 @@
slotIdToComplicationData?.let {
val now = getNow()
for ((id, complicationData) in oldComplicationData) {
- complicationSlotsManager.setComplicationDataUpdateSync(
+ complicationSlotsManager.setComplicationDataUpdateForScreenshot(
id,
complicationData,
now
@@ -994,8 +992,11 @@
// Separate calls are issued to deliver the state of isAmbient and isVisible, so during init
// we might not yet know the state of both (which is required by the shouldAnimate logic).
// If the editor is obscuring the watch face, there's no need to schedule a frame.
- if (!watchState.isAmbient.hasValue() || !watchState.isVisible.hasValue() ||
- editorObscuresWatchFace) {
+ if (
+ !watchState.isAmbient.hasValue() ||
+ !watchState.isVisible.hasValue() ||
+ editorObscuresWatchFace
+ ) {
return
}
@@ -1194,7 +1195,7 @@
params.idAndComplicationDatumWireFormats?.let {
for (idAndData in it) {
- complicationSlotsManager.setComplicationDataUpdateSync(
+ complicationSlotsManager.setComplicationDataUpdateForScreenshot(
idAndData.id,
idAndData.complicationData.toApiComplicationData(),
instant
@@ -1218,7 +1219,7 @@
if (params.idAndComplicationDatumWireFormats != null) {
val now = getNow()
for ((id, complicationData) in oldComplicationData) {
- complicationSlotsManager.setComplicationDataUpdateSync(
+ complicationSlotsManager.setComplicationDataUpdateForScreenshot(
id,
complicationData,
now
@@ -1272,20 +1273,21 @@
// Compute the bounds of the complication based on the display rather than
// the headless renderer (which may be smaller).
- val bounds = it.computeBounds(
- Rect(
- 0,
- 0,
- Resources.getSystem().displayMetrics.widthPixels,
- Resources.getSystem().displayMetrics.heightPixels
+ val bounds =
+ it.computeBounds(
+ Rect(
+ 0,
+ 0,
+ Resources.getSystem().displayMetrics.widthPixels,
+ Resources.getSystem().displayMetrics.heightPixels
+ )
)
- )
var prevData: ComplicationData? = null
val screenshotComplicationData = params.complicationData
if (screenshotComplicationData != null) {
prevData = it.renderer.getData()
- complicationSlotsManager.setComplicationDataUpdateSync(
+ complicationSlotsManager.setComplicationDataUpdateForScreenshot(
params.complicationSlotId,
screenshotComplicationData.toApiComplicationData(),
instant
@@ -1303,12 +1305,13 @@
params.complicationSlotId
)
picture.endRecording()
- complicationBitmap = Api28CreateBitmapHelper.createBitmap(
- picture,
- bounds.width(),
- bounds.height(),
- Bitmap.Config.ARGB_8888
- )
+ complicationBitmap =
+ Api28CreateBitmapHelper.createBitmap(
+ picture,
+ bounds.width(),
+ bounds.height(),
+ Bitmap.Config.ARGB_8888
+ )
} else {
complicationBitmap =
Bitmap.createBitmap(
@@ -1330,7 +1333,7 @@
// Restore previous ComplicationData & style if required.
if (prevData != null) {
val now = getNow()
- complicationSlotsManager.setComplicationDataUpdateSync(
+ complicationSlotsManager.setComplicationDataUpdateForScreenshot(
params.complicationSlotId,
prevData,
now
@@ -1421,7 +1424,7 @@
params.idAndComplicationDatumWireFormats?.let {
for (idAndData in it) {
- watchFaceImpl.complicationSlotsManager.setComplicationDataUpdateSync(
+ watchFaceImpl.complicationSlotsManager.setComplicationDataUpdateForScreenshot(
idAndData.id,
idAndData.complicationData.toApiComplicationData(),
instant
@@ -1443,7 +1446,7 @@
if (params.idAndComplicationDatumWireFormats != null) {
val now = watchFaceImpl.getNow()
for ((id, complicationData) in oldComplicationData) {
- watchFaceImpl.complicationSlotsManager.setComplicationDataUpdateSync(
+ watchFaceImpl.complicationSlotsManager.setComplicationDataUpdateForScreenshot(
id,
complicationData,
now
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 3a6c4ec..f131d12 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -70,6 +70,7 @@
import androidx.wear.watchface.complications.data.ShortTextComplicationData
import androidx.wear.watchface.complications.data.TimeDifferenceComplicationText
import androidx.wear.watchface.complications.data.TimeDifferenceStyle
+import androidx.wear.watchface.complications.data.toApiComplicationData
import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
import androidx.wear.watchface.complications.rendering.ComplicationDrawable
import androidx.wear.watchface.control.HeadlessWatchFaceImpl
@@ -142,6 +143,7 @@
import org.mockito.kotlin.verifyNoMoreInteractions
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowBuild
private const val INTERACTIVE_UPDATE_RATE_MS = 16L
private const val LEFT_COMPLICATION_ID = 1000
@@ -728,6 +730,7 @@
@Before
public fun setUp() {
+ ShadowBuild.setType("userdebug")
Assume.assumeTrue("This test suite assumes API 26", Build.VERSION.SDK_INT >= 26)
`when`(handler.getLooper()).thenReturn(Looper.myLooper())
@@ -758,9 +761,11 @@
(TODO: b/264994539) - Explicitly releasing the mSurfaceControl field,
accessed via reflection. Remove when a proper fix is found
*/
- val mSurfaceControlObject: Field = WatchFaceService.EngineWrapper::class
- .java.superclass // android.service.wallpaper.WallpaperService$Engine
- .getDeclaredField("mSurfaceControl")
+ val mSurfaceControlObject: Field =
+ WatchFaceService.EngineWrapper::class
+ .java
+ .superclass // android.service.wallpaper.WallpaperService$Engine
+ .getDeclaredField("mSurfaceControl")
mSurfaceControlObject.isAccessible = true
(mSurfaceControlObject.get(engineWrapper) as SurfaceControl).release()
}
@@ -1317,11 +1322,7 @@
null
)
verify(tapListener)
- .onTapEvent(
- TapType.UP,
- TapEvent(10, 200, Instant.ofEpochMilli(looperTimeMillis)),
- null
- )
+ .onTapEvent(TapType.UP, TapEvent(10, 200, Instant.ofEpochMilli(looperTimeMillis)), null)
}
@Test
@@ -2858,6 +2859,48 @@
@Test
@Config(sdk = [Build.VERSION_CODES.O_MR1])
+ public fun updateComplicationData_appendsToHistory() {
+ initWallpaperInteractiveWatchFaceInstance(
+ complicationSlots = listOf(leftComplication)
+ )
+ // Validate that the history is initially empty.
+ assertThat(leftComplication.complicationHistory!!.iterator().asSequence().toList())
+ .isEmpty()
+ val longTextComplication =
+ WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+ .setLongText(WireComplicationText.plainText("TYPE_LONG_TEXT"))
+ .build()
+
+ interactiveWatchFaceInstance.updateComplicationData(
+ listOf(IdAndComplicationDataWireFormat(LEFT_COMPLICATION_ID, longTextComplication))
+ )
+
+ assertThat(leftComplication.complicationHistory.toList().map { it.complicationData })
+ .containsExactly(longTextComplication.toApiComplicationData())
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.O_MR1])
+ public fun setComplicationDataUpdateForScreenshot_doesNotAppendToHistory() {
+ initWallpaperInteractiveWatchFaceInstance(
+ complicationSlots = listOf(leftComplication)
+ )
+
+ complicationSlotsManager.setComplicationDataUpdateForScreenshot(
+ LEFT_COMPLICATION_ID,
+ WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+ .setLongText(WireComplicationText.plainText("TYPE_LONG_TEXT"))
+ .build()
+ .toApiComplicationData(),
+ Instant.now()
+ )
+
+ assertThat(leftComplication.complicationHistory!!.iterator().asSequence().toList())
+ .isEmpty()
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.O_MR1])
public fun complicationCache() {
val complicationCache = HashMap<String, ByteArray>()
val instanceParams =