Merge "Fix ImageCapture NPE crash in 1.3.0 alpha" into androidx-main
diff --git a/activity/activity-compose/build.gradle b/activity/activity-compose/build.gradle
index d51c200..a0a7d61 100644
--- a/activity/activity-compose/build.gradle
+++ b/activity/activity-compose/build.gradle
@@ -36,7 +36,7 @@
// Outside of androidx this is resolved via constraint added to lifecycle-common,
// but it doesn't work in androidx.
// See aosp/1804059
- implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
+ implementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
androidTestImplementation projectOrArtifact(":compose:material:material")
diff --git a/activity/activity-ktx/build.gradle b/activity/activity-ktx/build.gradle
index b28eac7..435ee81 100644
--- a/activity/activity-ktx/build.gradle
+++ b/activity/activity-ktx/build.gradle
@@ -33,10 +33,10 @@
api("androidx.core:core-ktx:1.1.0") {
because "Mirror activity dependency graph for -ktx artifacts"
}
- api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1") {
+ api(project(":lifecycle:lifecycle-runtime-ktx")) {
because 'Mirror activity dependency graph for -ktx artifacts'
}
- api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
+ api(project(":lifecycle:lifecycle-viewmodel-ktx"))
api("androidx.savedstate:savedstate-ktx:1.2.0") {
because 'Mirror activity dependency graph for -ktx artifacts'
}
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index 27fd54e..bbadf0f 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -22,10 +22,10 @@
api("androidx.annotation:annotation:1.1.0")
implementation("androidx.collection:collection:1.0.0")
api("androidx.core:core:1.8.0")
- api("androidx.lifecycle:lifecycle-runtime:2.5.1")
- api("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
+ api(project(":lifecycle:lifecycle-runtime"))
+ api(project(":lifecycle:lifecycle-viewmodel"))
api("androidx.savedstate:savedstate:1.2.0")
- api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
+ api(project(":lifecycle:lifecycle-viewmodel-savedstate"))
implementation("androidx.tracing:tracing:1.0.0")
api(libs.kotlinStdlib)
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextTest.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextTest.java
index b2c2950..88b1592 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextTest.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextTest.java
@@ -53,6 +53,7 @@
import androidx.test.rule.ActivityTestRule;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -303,6 +304,7 @@
TextViewCompat.getCompoundDrawableTintList(textView));
}
+ @Ignore // b/259134876
@Test
public void testCompoundDrawablesTintList() {
// Given an ACTV with a white drawableLeftCompat and a ColorStateList drawableTint set
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
index c7bc830..375f3519 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
@@ -46,6 +46,9 @@
if (Features.ADD_PERMISSIONS_AND_GET_VISIBILITY.equals(feature)) {
return true;
}
+ if (Features.TOKENIZER_TYPE_RFC822.equals(feature)) {
+ return true;
+ }
return false;
}
}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
index 336b213..47fad2c 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
@@ -44,6 +44,11 @@
if (Features.ADD_PERMISSIONS_AND_GET_VISIBILITY.equals(feature)) {
return BuildCompat.isAtLeastT();
}
+ // TODO: Update to reflect support in Android U+ once this feature is synced over into
+ // service-appsearch
+ if (Features.TOKENIZER_TYPE_RFC822.equals(feature)) {
+ return false;
+ }
return false;
}
}
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index 8580d6f..935fa28 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -165,6 +165,7 @@
field public static final int INDEXING_TYPE_PREFIXES = 2; // 0x2
field public static final int TOKENIZER_TYPE_NONE = 0; // 0x0
field public static final int TOKENIZER_TYPE_PLAIN = 1; // 0x1
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.TOKENIZER_TYPE_RFC822) public static final int TOKENIZER_TYPE_RFC822 = 3; // 0x3
}
public static final class AppSearchSchema.StringPropertyConfig.Builder {
@@ -205,6 +206,7 @@
field public static final String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA";
field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+ field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
}
public class GenericDocument {
diff --git a/appsearch/appsearch/api/public_plus_experimental_current.txt b/appsearch/appsearch/api/public_plus_experimental_current.txt
index 8580d6f..935fa28 100644
--- a/appsearch/appsearch/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch/api/public_plus_experimental_current.txt
@@ -165,6 +165,7 @@
field public static final int INDEXING_TYPE_PREFIXES = 2; // 0x2
field public static final int TOKENIZER_TYPE_NONE = 0; // 0x0
field public static final int TOKENIZER_TYPE_PLAIN = 1; // 0x1
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.TOKENIZER_TYPE_RFC822) public static final int TOKENIZER_TYPE_RFC822 = 3; // 0x3
}
public static final class AppSearchSchema.StringPropertyConfig.Builder {
@@ -205,6 +206,7 @@
field public static final String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA";
field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+ field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
}
public class GenericDocument {
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index 8580d6f..935fa28 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -165,6 +165,7 @@
field public static final int INDEXING_TYPE_PREFIXES = 2; // 0x2
field public static final int TOKENIZER_TYPE_NONE = 0; // 0x0
field public static final int TOKENIZER_TYPE_PLAIN = 1; // 0x1
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.TOKENIZER_TYPE_RFC822) public static final int TOKENIZER_TYPE_RFC822 = 3; // 0x3
}
public static final class AppSearchSchema.StringPropertyConfig.Builder {
@@ -205,6 +206,7 @@
field public static final String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA";
field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+ field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
}
public class GenericDocument {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
index 1417929..d08a89d 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
@@ -368,4 +368,14 @@
assertThat(schemaString).isEqualTo(expectedString);
}
+
+ @Test
+ public void testStringPropertyConfig_setTokenizerType() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new StringPropertyConfig.Builder("subject").setTokenizerType(5).build());
+ assertThrows(IllegalArgumentException.class, () ->
+ new StringPropertyConfig.Builder("subject").setTokenizerType(2).build());
+ assertThrows(IllegalArgumentException.class, () ->
+ new StringPropertyConfig.Builder("subject").setTokenizerType(-1).build());
+ }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
index bc1e02c..751ee86 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
@@ -3406,4 +3406,54 @@
assertThat(matches.get(0).getSubmatch()).isEqualTo("🐟");
}
}
+
+ @Test
+ public void testRfc822() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.TOKENIZER_TYPE_RFC822));
+ AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+ .addProperty(new StringPropertyConfig.Builder("address")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_RFC822)
+ .build()
+ ).build();
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+ .setForceOverride(true).addSchemas(emailSchema).build()).get();
+
+ GenericDocument email = new GenericDocument.Builder<>("NS", "alex1", "Email")
+ .setPropertyString("address", "Alex Saveliev <alex.sav@google.com>")
+ .build();
+ mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
+
+ SearchResults sr = mDb1.search("com", new SearchSpec.Builder().build());
+ List<SearchResult> page = sr.getNextPageAsync().get();
+
+ // RFC tokenization will produce the following tokens for
+ // "Alex Saveliev <alex.sav@google.com>" : ["Alex Saveliev <alex.sav@google.com>", "Alex",
+ // "Saveliev", "alex.sav", "alex.sav@google.com", "alex.sav", "google", "com"]. Therefore,
+ // a query for "com" should match the document.
+ assertThat(page).hasSize(1);
+ assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("alex1");
+
+ // Plain tokenizer will not match this
+ AppSearchSchema plainEmailSchema = new AppSearchSchema.Builder("Email")
+ .addProperty(new StringPropertyConfig.Builder("address")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+
+ // Flipping the tokenizer type is a backwards compatible change. The index will be
+ // rebuilt with the email doc being tokenized in the new way.
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder().addSchemas(plainEmailSchema).build()).get();
+
+ sr = mDb1.search("com", new SearchSpec.Builder().build());
+
+ // Plain tokenization will produce the following tokens for
+ // "Alex Saveliev <alex.sav@google.com>" : ["Alex", "Saveliev", "<", "alex.sav",
+ // "google.com", ">"]. So "com" will not match any of the tokens produced.
+ assertThat(sr.getNextPageAsync().get()).hasSize(0);
+ }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
index dd47914..2086796 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
@@ -41,6 +41,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -780,4 +781,30 @@
ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),
ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)));
}
+
+ @Test
+ public void testRfc822TokenizerType() {
+ AppSearchSchema.StringPropertyConfig prop1 =
+ new AppSearchSchema.StringPropertyConfig.Builder("prop1")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(
+ AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_RFC822)
+ .build();
+ AppSearchSchema schema1 =
+ new AppSearchSchema.Builder("type1").addProperty(prop1).build();
+
+ SetSchemaRequest request = new SetSchemaRequest.Builder()
+ .addSchemas(schema1)
+ .setForceOverride(true)
+ .setVersion(142857)
+ .build();
+ AppSearchSchema[] schemas = request.getSchemas().toArray(new AppSearchSchema[0]);
+ assertThat(schemas).hasLength(1);
+ List<AppSearchSchema.PropertyConfig> properties = schemas[0].getProperties();
+ assertThat(properties).hasSize(1);
+ assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(0)).getTokenizerType())
+ .isEqualTo(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_RFC822);
+ }
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
index 1a19c65..cce0237 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
@@ -21,6 +21,7 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RequiresFeature;
import androidx.annotation.RestrictTo;
import androidx.appsearch.exceptions.IllegalSchemaException;
import androidx.appsearch.util.BundleUtil;
@@ -482,6 +483,7 @@
@IntDef(value = {
TOKENIZER_TYPE_NONE,
TOKENIZER_TYPE_PLAIN,
+ TOKENIZER_TYPE_RFC822
})
@Retention(RetentionPolicy.SOURCE)
public @interface TokenizerType {}
@@ -507,6 +509,25 @@
*/
public static final int TOKENIZER_TYPE_PLAIN = 1;
+ // TODO(b/204333391): In icing, the "2" tokenizer is the verbatim tokenizer.
+
+ /**
+ * Tokenization for emails. This value indicates that tokens should be extracted from
+ * this property based on email structure.
+ *
+ * <p>Ex. A property with "alex.sav@google.com" will produce tokens for "alex", "sav",
+ * "alex.sav", "google", "com", and "alexsav@google.com"
+ *
+ * <p>It is only valid for tokenizer_type to be 'RFC822' if {@link #getIndexingType} is
+ * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}.
+ */
+// @exportToFramework:startStrip()
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.TOKENIZER_TYPE_RFC822)
+// @exportToFramework:endStrip()
+ public static final int TOKENIZER_TYPE_RFC822 = 3;
+
StringPropertyConfig(@NonNull Bundle bundle) {
super(bundle);
}
@@ -576,8 +597,13 @@
*/
@NonNull
public StringPropertyConfig.Builder setTokenizerType(@TokenizerType int tokenizerType) {
- Preconditions.checkArgumentInRange(
- tokenizerType, TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_PLAIN, "tokenizerType");
+ // TODO(b/204333391): Change to checkArgumentInRange once verbatim is supported
+ if (tokenizerType != TOKENIZER_TYPE_NONE && tokenizerType != TOKENIZER_TYPE_PLAIN
+ && tokenizerType != TOKENIZER_TYPE_RFC822) {
+ throw new IllegalArgumentException("Tokenizer value " + tokenizerType + " is "
+ + "out of range. Valid values are TOKENIZER_TYPE_NONE, "
+ + "TOKENIZER_TYPE_PLAIN, and TOKENIZER_TYPE_RFC822");
+ }
mTokenizerType = tokenizerType;
return this;
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
index dc530ee..5edc780 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
@@ -69,6 +69,12 @@
String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";
/**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers
+ * {@link AppSearchSchema.StringPropertyConfig#TOKENIZER_TYPE_RFC822}.
+ */
+ String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
+
+ /**
* Returns whether a feature is supported at run-time. Feature support depends on the
* feature in question, the AppSearch backend being used and the Android version of the
* device.
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
index 07e709a..7dae734 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
@@ -293,6 +293,8 @@
*
* <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned
* by this function, rather than calling it multiple times.
+ *
+ * @return A mapping of schema types to lists of projection strings.
*/
@NonNull
public Map<String, List<String>> getProjections() {
@@ -314,6 +316,8 @@
*
* <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned
* by this function, rather than calling it multiple times.
+ *
+ * @return A mapping of schema types to lists of projection {@link PropertyPath} objects.
*/
@NonNull
public Map<String, List<PropertyPath>> getProjectionPaths() {
@@ -624,6 +628,9 @@
* it will be ignored for that result. Property paths cannot be null.
*
* @see #addProjectionPaths
+ *
+ * @param schema a string corresponding to the schema to add projections to.
+ * @param propertyPaths the projections to add.
*/
@NonNull
public SearchSpec.Builder addProjection(
@@ -699,6 +706,9 @@
* subject: "IMPORTANT"
* }
* }</pre>
+ *
+ * @param schema a string corresponding to the schema to add projections to.
+ * @param propertyPaths the projections to add.
*/
@NonNull
public SearchSpec.Builder addProjectionPaths(
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
index 96b0f88..c23a469 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
@@ -197,6 +197,9 @@
} else if (tokenizerType == 1) { // TOKENIZER_TYPE_PLAIN
tokenizerEnum = mHelper.getAppSearchClass(
"AppSearchSchema", "StringPropertyConfig", "TOKENIZER_TYPE_PLAIN");
+ } else if (tokenizerType == 3) { // TOKENIZER_TYPE_RFC822
+ tokenizerEnum = mHelper.getAppSearchClass(
+ "AppSearchSchema", "StringPropertyConfig", "TOKENIZER_TYPE_RFC822");
} else {
throw new ProcessingException("Unknown tokenizer type " + tokenizerType, property);
}
diff --git a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
index 1339501..c461fc3 100644
--- a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
+++ b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
@@ -937,8 +937,30 @@
+ "public class Gift {\n"
+ " @Document.Namespace String namespace;\n"
+ " @Document.Id String id;\n"
- + " @Document.StringProperty(tokenizerType=0) String tokNone;\n"
- + " @Document.StringProperty(tokenizerType=1) String tokPlain;\n"
+ + "\n"
+ // NONE index type will generate a NONE tokenizerType type.
+ + " @Document.StringProperty(tokenizerType=0, indexingType=0) "
+ + " String tokNoneInvalid;\n"
+ + " @Document.StringProperty(tokenizerType=1, indexingType=0) "
+ + " String tokPlainInvalid;\n"
+ + " @Document.StringProperty(tokenizerType=3, indexingType=0) "
+ + " String tokRfc822Invalid;\n"
+ + "\n"
+ // Indexing type exact.
+ + " @Document.StringProperty(tokenizerType=0, indexingType=1) "
+ + " String tokNone;\n"
+ + " @Document.StringProperty(tokenizerType=1, indexingType=1) "
+ + " String tokPlain;\n"
+ + " @Document.StringProperty(tokenizerType=3, indexingType=1) "
+ + " String tokRfc822;\n"
+ + "\n"
+ // Indexing type prefix.
+ + " @Document.StringProperty(tokenizerType=0, indexingType=2) "
+ + " String tokNonePrefix;\n"
+ + " @Document.StringProperty(tokenizerType=1, indexingType=2) "
+ + " String tokPlainPrefix;\n"
+ + " @Document.StringProperty(tokenizerType=3, indexingType=2) "
+ + " String tokRfc822Prefix;\n"
+ "}\n");
assertThat(compilation).succeededWithoutWarnings();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
index b6ccd81..0fa23d4 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
@@ -20,16 +20,51 @@
@Override
public AppSearchSchema getSchema() throws AppSearchException {
return new AppSearchSchema.Builder(SCHEMA_NAME)
- .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokNone")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokNoneInvalid")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
.build())
- .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokPlain")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokPlainInvalid")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
.build())
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokRfc822Invalid")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+ .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
+ .build())
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokNone")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+ .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .build())
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokPlain")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .build())
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokRfc822")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_RFC822)
+ .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .build())
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokNonePrefix")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+ .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokPlainPrefix")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokRfc822Prefix")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_RFC822)
+ .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
.build();
}
@@ -37,6 +72,18 @@
public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
GenericDocument.Builder<?> builder =
new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+ String tokNoneInvalidCopy = document.tokNoneInvalid;
+ if (tokNoneInvalidCopy != null) {
+ builder.setPropertyString("tokNoneInvalid", tokNoneInvalidCopy);
+ }
+ String tokPlainInvalidCopy = document.tokPlainInvalid;
+ if (tokPlainInvalidCopy != null) {
+ builder.setPropertyString("tokPlainInvalid", tokPlainInvalidCopy);
+ }
+ String tokRfc822InvalidCopy = document.tokRfc822Invalid;
+ if (tokRfc822InvalidCopy != null) {
+ builder.setPropertyString("tokRfc822Invalid", tokRfc822InvalidCopy);
+ }
String tokNoneCopy = document.tokNone;
if (tokNoneCopy != null) {
builder.setPropertyString("tokNone", tokNoneCopy);
@@ -45,6 +92,22 @@
if (tokPlainCopy != null) {
builder.setPropertyString("tokPlain", tokPlainCopy);
}
+ String tokRfc822Copy = document.tokRfc822;
+ if (tokRfc822Copy != null) {
+ builder.setPropertyString("tokRfc822", tokRfc822Copy);
+ }
+ String tokNonePrefixCopy = document.tokNonePrefix;
+ if (tokNonePrefixCopy != null) {
+ builder.setPropertyString("tokNonePrefix", tokNonePrefixCopy);
+ }
+ String tokPlainPrefixCopy = document.tokPlainPrefix;
+ if (tokPlainPrefixCopy != null) {
+ builder.setPropertyString("tokPlainPrefix", tokPlainPrefixCopy);
+ }
+ String tokRfc822PrefixCopy = document.tokRfc822Prefix;
+ if (tokRfc822PrefixCopy != null) {
+ builder.setPropertyString("tokRfc822Prefix", tokRfc822PrefixCopy);
+ }
return builder.build();
}
@@ -52,6 +115,21 @@
public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
String idConv = genericDoc.getId();
String namespaceConv = genericDoc.getNamespace();
+ String[] tokNoneInvalidCopy = genericDoc.getPropertyStringArray("tokNoneInvalid");
+ String tokNoneInvalidConv = null;
+ if (tokNoneInvalidCopy != null && tokNoneInvalidCopy.length != 0) {
+ tokNoneInvalidConv = tokNoneInvalidCopy[0];
+ }
+ String[] tokPlainInvalidCopy = genericDoc.getPropertyStringArray("tokPlainInvalid");
+ String tokPlainInvalidConv = null;
+ if (tokPlainInvalidCopy != null && tokPlainInvalidCopy.length != 0) {
+ tokPlainInvalidConv = tokPlainInvalidCopy[0];
+ }
+ String[] tokRfc822InvalidCopy = genericDoc.getPropertyStringArray("tokRfc822Invalid");
+ String tokRfc822InvalidConv = null;
+ if (tokRfc822InvalidCopy != null && tokRfc822InvalidCopy.length != 0) {
+ tokRfc822InvalidConv = tokRfc822InvalidCopy[0];
+ }
String[] tokNoneCopy = genericDoc.getPropertyStringArray("tokNone");
String tokNoneConv = null;
if (tokNoneCopy != null && tokNoneCopy.length != 0) {
@@ -62,11 +140,38 @@
if (tokPlainCopy != null && tokPlainCopy.length != 0) {
tokPlainConv = tokPlainCopy[0];
}
+ String[] tokRfc822Copy = genericDoc.getPropertyStringArray("tokRfc822");
+ String tokRfc822Conv = null;
+ if (tokRfc822Copy != null && tokRfc822Copy.length != 0) {
+ tokRfc822Conv = tokRfc822Copy[0];
+ }
+ String[] tokNonePrefixCopy = genericDoc.getPropertyStringArray("tokNonePrefix");
+ String tokNonePrefixConv = null;
+ if (tokNonePrefixCopy != null && tokNonePrefixCopy.length != 0) {
+ tokNonePrefixConv = tokNonePrefixCopy[0];
+ }
+ String[] tokPlainPrefixCopy = genericDoc.getPropertyStringArray("tokPlainPrefix");
+ String tokPlainPrefixConv = null;
+ if (tokPlainPrefixCopy != null && tokPlainPrefixCopy.length != 0) {
+ tokPlainPrefixConv = tokPlainPrefixCopy[0];
+ }
+ String[] tokRfc822PrefixCopy = genericDoc.getPropertyStringArray("tokRfc822Prefix");
+ String tokRfc822PrefixConv = null;
+ if (tokRfc822PrefixCopy != null && tokRfc822PrefixCopy.length != 0) {
+ tokRfc822PrefixConv = tokRfc822PrefixCopy[0];
+ }
Gift document = new Gift();
document.namespace = namespaceConv;
document.id = idConv;
+ document.tokNoneInvalid = tokNoneInvalidConv;
+ document.tokPlainInvalid = tokPlainInvalidConv;
+ document.tokRfc822Invalid = tokRfc822InvalidConv;
document.tokNone = tokNoneConv;
document.tokPlain = tokPlainConv;
+ document.tokRfc822 = tokRfc822Conv;
+ document.tokNonePrefix = tokNonePrefixConv;
+ document.tokPlainPrefix = tokPlainPrefixConv;
+ document.tokRfc822Prefix = tokRfc822PrefixConv;
return document;
}
}
diff --git a/benchmark/benchmark-common/api/public_plus_experimental_current.txt b/benchmark/benchmark-common/api/public_plus_experimental_current.txt
index 54c1187..07ca9f6 100644
--- a/benchmark/benchmark-common/api/public_plus_experimental_current.txt
+++ b/benchmark/benchmark-common/api/public_plus_experimental_current.txt
@@ -36,7 +36,7 @@
public final class ConfigurationErrorKt {
}
- @kotlin.RequiresOptIn public @interface ExperimentalBenchmarkStateApi {
+ @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalBenchmarkStateApi {
}
public final class MetricNameUtilsKt {
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellBehaviorTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellBehaviorTest.kt
index bd01556..493611c 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellBehaviorTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellBehaviorTest.kt
@@ -62,12 +62,16 @@
@Test
fun pidof() {
// Should only be one process - this one!
- val pidofString = Shell.executeScriptCaptureStdout("pidof ${Packages.TEST}").trim()
+ val output = Shell.executeScriptCaptureStdoutStderr("pidof ${Packages.TEST}")
+ val pidofString = output.stdout.trim()
when {
Build.VERSION.SDK_INT < 23 -> {
- // command doesn't exist (and we don't try and read stderr here)
- assertEquals("", pidofString)
+ // command doesn't exist
+ assertTrue(
+ output.stdout.isBlank() && output.stderr.isNotBlank(),
+ "saw output $output"
+ )
}
Build.VERSION.SDK_INT == 23 -> {
// on API 23 specifically, pidof prints... all processes, ignoring the arg...
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellTest.kt
index c23a119..576a03b 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellTest.kt
@@ -68,10 +68,7 @@
val output = Shell.optionalCommand("echo foo")
val expected = when {
- Build.VERSION.SDK_INT >= 23 -> "foo\n"
- // known bug in the shell on L (21,22). `echo` doesn't work with shell
- // programmatically, only works in interactive shell :|
- Build.VERSION.SDK_INT in 21..22 -> ""
+ Build.VERSION.SDK_INT >= 21 -> "foo\n"
else -> null
}
@@ -153,7 +150,7 @@
)
}
- @SdkSuppress(minSdkVersion = 26) // xargs only available 26+
+ @SdkSuppress(minSdkVersion = 23) // xargs added api 23
@Test
fun executeScriptCaptureStdout_pipe_xargs() {
// validate piping works with xargs
@@ -170,7 +167,7 @@
)
}
- @SdkSuppress(minSdkVersion = 26) // xargs only available 26+
+ @SdkSuppress(minSdkVersion = 23) // xargs added api 23
@Test
fun executeScriptCaptureStdout_stdinArg_xargs() {
// validate stdin to first command in script
@@ -204,7 +201,7 @@
)
}
- @SdkSuppress(minSdkVersion = 26) // xargs only available 26+
+ @SdkSuppress(minSdkVersion = 23) // xargs added api 23
@Test
fun executeScriptCaptureStdout_multilineRedirectStdin_xargs() {
Assert.assertEquals(
@@ -383,7 +380,7 @@
}
}
- @SdkSuppress(minSdkVersion = 21)
+ @SdkSuppress(minSdkVersion = 23) // xargs added api 23
@Test
fun shellReuse() {
val script = Shell.createShellScript("xargs echo $1", stdin = "foo")
@@ -397,6 +394,32 @@
script.cleanUp()
}
+ @SdkSuppress(minSdkVersion = 21)
+ @Test
+ fun getChecksum() {
+ val emptyPaths = listOf("/data/local/tmp/emptyfile1", "/data/local/tmp/emptyfile2")
+ try {
+ val checksums = emptyPaths.map {
+ Shell.executeScriptSilent("rm -f $it")
+ Shell.executeScriptSilent("touch $it")
+ Shell.getChecksum(it)
+ }
+
+ assertEquals(checksums.first(), checksums.last())
+ if (Build.VERSION.SDK_INT < 23) {
+ checksums.forEach { checksum ->
+ // getChecksum uses ls -l to check size pre API 23,
+ // this validates that behavior + result parsing
+ assertEquals("0", checksum)
+ }
+ }
+ } finally {
+ emptyPaths.forEach {
+ Shell.executeScriptSilent("rm -f $it")
+ }
+ }
+ }
+
@RequiresApi(21)
private fun pidof(packageName: String): Int? {
return Shell.getPidsForProcess(packageName).firstOrNull()
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/ExperimentalBenchmarkStateApi.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/ExperimentalBenchmarkStateApi.kt
index 283a472..33eca57 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/ExperimentalBenchmarkStateApi.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/ExperimentalBenchmarkStateApi.kt
@@ -21,4 +21,5 @@
* of the BenchmarkRule JUnit4 API.
*/
@RequiresOptIn
+@Retention(AnnotationRetention.BINARY)
public annotation class ExperimentalBenchmarkStateApi
\ No newline at end of file
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
index 86d304e..680dd62 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
@@ -107,27 +107,63 @@
}
}
+ /**
+ * Get a checksum for a given path
+ *
+ * Note: Does not check for stderr, as this method is used during ShellImpl init, so stderr not
+ * yet available
+ */
@RequiresApi(21)
- fun chmodExecutable(absoluteFilePath: String) {
- // use unsafe commands, as this is used in Shell.executeScript
+ internal fun getChecksum(path: String): String {
+ val sum = if (Build.VERSION.SDK_INT >= 23) {
+ ShellImpl.executeCommandUnsafe("md5sum $path").substringBefore(" ")
+ } else {
+ // this isn't good, but it's good enough for API 22
+ val out = ShellImpl.executeCommandUnsafe("ls -l $path").split(Regex("\\s+"))[3]
+ println("value is $out")
+ out
+ }
+ check(sum.isNotBlank()) {
+ "Checksum for $path was blank"
+ }
+ return sum
+ }
+
+ /**
+ * Copy file and make executable
+ *
+ * Note: this operation does checksum validation of dst, since it's used during setup of the
+ * shell script used to capture stderr, so stderr isn't available.
+ */
+ @RequiresApi(21)
+ private fun moveToTmpAndMakeExecutable(src: String, dst: String) {
+ ShellImpl.executeCommandUnsafe("cp $src $dst")
if (Build.VERSION.SDK_INT >= 23) {
- ShellImpl.executeCommandUnsafe("chmod +x $absoluteFilePath")
+ ShellImpl.executeCommandUnsafe("chmod +x $dst")
} else {
// chmod with support for +x only added in API 23
// While 777 is technically more permissive, this is only used for scripts and temporary
// files in tests, so we don't worry about permissions / access here
- ShellImpl.executeCommandUnsafe("chmod 777 $absoluteFilePath")
+ ShellImpl.executeCommandUnsafe("chmod 777 $dst")
}
- }
- @RequiresApi(21)
- fun moveToTmpAndMakeExecutable(src: String, dst: String) {
- ShellImpl.executeCommandUnsafe("cp $src $dst")
- chmodExecutable(dst)
+ // validate checksums instead of checking stderr, since it's not yet safe to
+ // read from stderr. This detects the problem where root left a stale executable
+ // that can't be modified by shell at the dst path
+ val srcSum = getChecksum(src)
+ val dstSum = getChecksum(dst)
+ if (srcSum != dstSum) {
+ throw IllegalStateException("Failed to verify copied executable $dst, " +
+ "md5 sums $srcSum, $dstSum don't match. Check if root owns" +
+ " $dst and if so, delete it with `adb root`-ed shell session.")
+ }
}
/**
* Writes the inputStream to an executable file with the given name in `/data/local/tmp`
+ *
+ * Note: this operation does not validate command success, since it's used during setup of shell
+ * scripting code used to parse stderr. This means callers should validate.
*/
@RequiresApi(21)
fun createRunnableExecutable(name: String, inputStream: InputStream): String {
@@ -467,7 +503,8 @@
// These variables are used in executeCommand and executeScript, so we keep them as var
// instead of val and use a separate initializer
isSessionRooted = executeCommandUnsafe("id").contains("uid=0(root)")
- // use a script below, since `su` command failure is unrecoverable on some API levels
+ // use a script below, since direct `su` command failure brings down this process
+ // on some API levels (and can fail even on userdebug builds)
isSuAvailable = createShellScript(
script = "su root id",
stdin = null
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 1b8fa29..01381a9 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
@@ -26,9 +26,9 @@
import androidx.benchmark.userspaceTrace
import androidx.test.platform.app.InstrumentationRegistry
import androidx.tracing.trace
-import org.jetbrains.annotations.TestOnly
import java.io.File
import java.io.IOException
+import org.jetbrains.annotations.TestOnly
/**
* PerfettoHelper is used to start and stop the perfetto tracing and move the
@@ -122,8 +122,10 @@
// Perfetto
val perfettoCmd = perfettoCommand(actualConfigPath, isTextProtoConfig)
Log.i(LOG_TAG, "Starting perfetto tracing with cmd: $perfettoCmd")
- val perfettoCmdOutput =
- Shell.executeScriptCaptureStdout("$perfettoCmd; echo EXITCODE=$?").trim()
+ // Note: we intentionally don't check stderr, as benign warnings are printed
+ val perfettoCmdOutput = Shell.executeScriptCaptureStdoutStderr(
+ "$perfettoCmd; echo EXITCODE=$?"
+ ).stdout.trim()
val expectedSuffix = "\nEXITCODE=0"
if (!perfettoCmdOutput.endsWith(expectedSuffix)) {
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/UiState.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/UiState.kt
index bcd94d2..c9949cc 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/UiState.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/UiState.kt
@@ -16,16 +16,19 @@
package androidx.benchmark.perfetto
+import android.os.Build
+import android.util.Log
import androidx.annotation.RestrictTo
+import androidx.benchmark.BenchmarkState.Companion.TAG
+import java.io.File
+import java.io.FileNotFoundException
import perfetto.protos.Trace
import perfetto.protos.TracePacket
import perfetto.protos.UiState
-import java.io.File
/**
* Convenience for UiState construction with specified package
*/
-@Suppress("FunctionName") // constructor convenience
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun UiState(
timelineStart: Long?,
@@ -42,5 +45,19 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun File.appendUiState(state: UiState) {
val traceToAppend = Trace(packet = listOf(TracePacket(ui_state = state)))
- appendBytes(traceToAppend.encode())
+ appendBytesSafely(traceToAppend.encode())
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun File.appendBytesSafely(bytes: ByteArray) {
+ try {
+ appendBytes(bytes)
+ } catch (e: FileNotFoundException) {
+ if (Build.VERSION.SDK_INT in 21..22) {
+ // Failure is common on API 21/22 due to b/227510293
+ Log.d(TAG, "Unable to append additional bytes to ${this.absolutePath}")
+ } else {
+ throw e
+ }
+ }
}
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
index 676ce86..1671a9b 100644
--- a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
+++ b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
@@ -24,13 +24,13 @@
import androidx.benchmark.UserspaceTracing
import androidx.benchmark.perfetto.PerfettoCaptureWrapper
import androidx.benchmark.perfetto.UiState
+import androidx.benchmark.perfetto.appendBytesSafely
import androidx.benchmark.perfetto.appendUiState
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import androidx.tracing.Trace
import androidx.tracing.trace
import java.io.File
-import java.io.FileNotFoundException
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.rules.RuleChain
@@ -225,23 +225,16 @@
userspaceTrace = UserspaceTracing.commitToTrace()
}?.apply {
// trace completed, and copied into app writeable dir
-
- try {
- val file = File(this)
-
- file.appendBytes(userspaceTrace!!.encode())
- file.appendUiState(
- UiState(
- timelineStart = null,
- timelineEnd = null,
- highlightPackage = InstrumentationRegistry.getInstrumentation()
- .context.packageName
- )
+ val file = File(this)
+ file.appendBytesSafely(userspaceTrace!!.encode())
+ file.appendUiState(
+ UiState(
+ timelineStart = null,
+ timelineEnd = null,
+ highlightPackage = InstrumentationRegistry.getInstrumentation()
+ .context.packageName
)
- } catch (exception: FileNotFoundException) {
- // TODO(b/227510293): fix record to return a null in this case
- Log.d(TAG, "Unable to add additional detail to captured trace $this")
- }
+ )
}
if (enableReport) {
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
index cf9a3e3..0da5134 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
@@ -27,6 +27,7 @@
@RunWith(AndroidJUnit4::class)
@MediumTest
class ProfileInstallBroadcastTest {
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
@Test
fun installProfile() {
assertNull(ProfileInstallBroadcast.installProfile(Packages.TARGET))
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt
index 7023a69..7583d02 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt
@@ -21,11 +21,11 @@
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import java.util.Locale
+import kotlin.test.assertEquals
import org.junit.Assume.assumeTrue
import org.junit.Test
import org.junit.runner.RunWith
-import java.util.Locale
-import kotlin.test.assertEquals
@MediumTest
@RunWith(AndroidJUnit4::class)
diff --git a/buildSrc/OWNERS b/buildSrc/OWNERS
index d675db7..93edef1 100644
--- a/buildSrc/OWNERS
+++ b/buildSrc/OWNERS
@@ -1,7 +1,6 @@
set noparent
jeffrygaston@google.com
-sjgilbert@google.com
aurimas@google.com
alanv@google.com
nickanthony@google.com
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
index e6780db..f7110aa 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
@@ -99,18 +99,10 @@
@Input
lateinit var excludedPackagesForKotlin: Set<String>
- /**
- * These two variables control displaying of additional metadata in the refdocs.
- *
- * LIBRARY_METADATA_FILE: file containing artifactID and other metadata
- * SHOW_LIBRARY_METADATA: set to "true" to display the data
- */
+ // Maps to the system variable LIBRARY_METADATA_FILE containing artifactID and other metadata
@get:[InputFile PathSensitive(PathSensitivity.NONE)]
abstract val libraryMetadataFile: RegularFileProperty
- @Input
- var showLibraryMetadata: Boolean = false
-
// The base URL to create source links for classes, as a format string with placeholders for the
// file path and qualified class name.
@Input
@@ -212,7 +204,6 @@
excludedPackagesForJava = excludedPackagesForJava,
excludedPackagesForKotlin = excludedPackagesForKotlin,
libraryMetadataFile = libraryMetadataFile,
- showLibraryMetadata = showLibraryMetadata,
)
}
@@ -236,7 +227,6 @@
val excludedPackagesForJava: ListProperty<String>
val excludedPackagesForKotlin: ListProperty<String>
var libraryMetadataFile: Provider<RegularFile>
- var showLibraryMetadata: Boolean
}
fun runDackkaWithArgs(
@@ -247,7 +237,6 @@
excludedPackagesForJava: Set<String>,
excludedPackagesForKotlin: Set<String>,
libraryMetadataFile: Provider<RegularFile>,
- showLibraryMetadata: Boolean,
) {
val workQueue = workerExecutor.noIsolation()
workQueue.submit(DackkaWorkAction::class.java) { parameters ->
@@ -257,7 +246,6 @@
parameters.excludedPackagesForJava.set(excludedPackagesForJava)
parameters.excludedPackagesForKotlin.set(excludedPackagesForKotlin)
parameters.libraryMetadataFile = libraryMetadataFile
- parameters.showLibraryMetadata = showLibraryMetadata
}
}
@@ -273,7 +261,6 @@
// b/183989795 tracks moving these away from an environment variables
it.environment("DEVSITE_TENANT", "androidx")
it.environment("LIBRARY_METADATA_FILE", parameters.libraryMetadataFile.get().toString())
- it.environment("SHOW_LIBRARY_METADATA", parameters.showLibraryMetadata)
if (parameters.excludedPackages.get().isNotEmpty())
it.environment(
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dackka/OWNERS b/buildSrc/private/src/main/kotlin/androidx/build/dackka/OWNERS
index 76c5a36..a823e8b 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dackka/OWNERS
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dackka/OWNERS
@@ -1,3 +1,5 @@
asfalcone@google.com
+fsladkey@google.com
+juliamcclellan@google.com
owengray@google.com
tiem@google.com
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
index 3a6a8be..8784f69 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -458,7 +458,6 @@
excludedPackagesForJava = hiddenPackagesJava
excludedPackagesForKotlin = emptySet()
libraryMetadataFile.set(getMetadataRegularFile(project))
- showLibraryMetadata = true
projectStructureMetadataFile = mergedProjectMetadata
// See go/dackka-source-link for details on this link.
baseSourceLink = "https://cs.android.com/search?" +
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/docs/OWNERS b/buildSrc/private/src/main/kotlin/androidx/build/docs/OWNERS
index b26dde4..a823e8b 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/OWNERS
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/OWNERS
@@ -1,3 +1,5 @@
asfalcone@google.com
fsladkey@google.com
-owengray@google.com
\ No newline at end of file
+juliamcclellan@google.com
+owengray@google.com
+tiem@google.com
diff --git a/busytown/impl/build-metalava-and-androidx.sh b/busytown/impl/build-metalava-and-androidx.sh
index d24f86c..b609c1a 100755
--- a/busytown/impl/build-metalava-and-androidx.sh
+++ b/busytown/impl/build-metalava-and-androidx.sh
@@ -46,7 +46,7 @@
# Mac grep doesn't support -P, so use perl version of `grep -oP "(?<=metalavaVersion=).*"`
export METALAVA_VERSION=`perl -nle'print $& while m{(?<=metalavaVersion=).*}g' $METALAVA_DIR/src/main/resources/version.properties`
-export METALAVA_REPO="$CHECKOUT_ROOT/out/dist/repo/m2repository"
+export METALAVA_REPO="$DIST_DIR/repo/m2repository"
function buildAndroidx() {
./frameworks/support/busytown/impl/build.sh $androidxArguments \
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index f266b2d..585948a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -323,7 +323,7 @@
/*hasEmbeddedTransform=*/true,
requireNonNull(getCropRect(resolution)),
getRelativeRotation(camera),
- /*mirroring=*/true,
+ /*mirroring=*/isFrontCamera(camera),
this::notifyReset);
SurfaceEdge inputEdge = SurfaceEdge.create(singletonList(cameraSurface));
SurfaceEdge outputEdge = mNode.transform(inputEdge);
@@ -343,6 +343,11 @@
return sessionConfigBuilder;
}
+ private static boolean isFrontCamera(@NonNull CameraInternal camera) {
+ Integer lensFacing = camera.getCameraInfoInternal().getLensFacing();
+ return lensFacing != null && lensFacing == CameraSelector.LENS_FACING_FRONT;
+ }
+
/**
* Sets a {@link SurfaceProcessorInternal}.
*
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java
index 3848aa5..5008b5c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java
@@ -16,8 +16,13 @@
package androidx.camera.core;
+import static androidx.camera.core.CameraEffect.IMAGE_CAPTURE;
+import static androidx.camera.core.CameraEffect.PREVIEW;
+import static androidx.camera.core.CameraEffect.VIDEO_CAPTURE;
import static androidx.core.util.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@@ -25,7 +30,11 @@
import androidx.lifecycle.Lifecycle;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
+import java.util.Map;
/**
* Represents a collection of {@link UseCase}.
@@ -82,10 +91,17 @@
* A builder for generating {@link UseCaseGroup}.
*/
public static final class Builder {
+
+ // Allow-list effect targets supported by CameraX.
+ private static final List<Integer> SUPPORTED_TARGETS = Arrays.asList(
+ PREVIEW,
+ IMAGE_CAPTURE);
+
private ViewPort mViewPort;
private final List<UseCase> mUseCases;
private final List<CameraEffect> mEffects;
+
public Builder() {
mUseCases = new ArrayList<>();
mEffects = new ArrayList<>();
@@ -101,7 +117,13 @@
}
/**
- * Adds a {@link CameraEffect} to the collection
+ * Adds a {@link CameraEffect} to the collection.
+ *
+ * <p>The value of {@link CameraEffect#getTargets()} must be unique and must be one of
+ * the supported values below:
+ * <ul>
+ * <li>{@link CameraEffect#PREVIEW}
+ * </ul>
*
* <p>Once added, CameraX will use the {@link CameraEffect}s to process the outputs of
* the {@link UseCase}s.
@@ -116,6 +138,57 @@
}
/**
+ * Checks effect targets and throw {@link IllegalArgumentException}.
+ *
+ * <p>Throws exception if the effects 1) contains duplicate targets or 2) contains
+ * effects that is not in the allowlist.
+ */
+ private void checkEffectTargets() {
+ Map<Integer, CameraEffect> targetEffectMap = new HashMap<>();
+ for (CameraEffect effect : mEffects) {
+ int targets = effect.getTargets();
+ if (!SUPPORTED_TARGETS.contains(targets)) {
+ throw new IllegalArgumentException(String.format(Locale.US,
+ "Target %s is not in the supported list %s.",
+ getHumanReadableTargets(targets),
+ getHumanReadableSupportedTargets()));
+ }
+ if (targetEffectMap.containsKey(effect.getTargets())) {
+ throw new IllegalArgumentException(String.format(Locale.US,
+ "%s and %s contain duplicate targets %s.",
+ requireNonNull(
+ targetEffectMap.get(effect.getTargets())).getClass().getName(),
+ effect.getClass().getName(),
+ getHumanReadableTargets(targets)));
+ }
+ targetEffectMap.put(effect.getTargets(), effect);
+ }
+ }
+
+ static String getHumanReadableSupportedTargets() {
+ List<String> targetNameList = new ArrayList<>();
+ for (Integer targets : SUPPORTED_TARGETS) {
+ targetNameList.add(getHumanReadableTargets(targets));
+ }
+ return "[" + String.join(", ", targetNameList) + "]";
+ }
+
+ static String getHumanReadableTargets(int targets) {
+ List<String> names = new ArrayList<>();
+ if ((targets & IMAGE_CAPTURE) != 0) {
+ names.add("IMAGE_CAPTURE");
+ }
+ if ((targets & PREVIEW) != 0) {
+ names.add("PREVIEW");
+ }
+
+ if ((targets & VIDEO_CAPTURE) != 0) {
+ names.add("VIDEO_CAPTURE");
+ }
+ return String.join("|", names);
+ }
+
+ /**
* Adds {@link UseCase} to the collection.
*/
@NonNull
@@ -130,6 +203,7 @@
@NonNull
public UseCaseGroup build() {
checkArgument(!mUseCases.isEmpty(), "UseCase must not be empty.");
+ checkEffectTargets();
return new UseCaseGroup(mViewPort, mUseCases, mEffects);
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RgbaImageProxy.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RgbaImageProxy.java
index 388a07b..a087c88 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RgbaImageProxy.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RgbaImageProxy.java
@@ -35,7 +35,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
-import androidx.annotation.VisibleForTesting;
import androidx.camera.core.ExperimentalGetImage;
import androidx.camera.core.ImageInfo;
import androidx.camera.core.ImageProxy;
@@ -91,7 +90,6 @@
*
* <p>The {@link Bitmap} must be {@link Bitmap.Config#ARGB_8888}.
*/
- @VisibleForTesting
public RgbaImageProxy(@NonNull Bitmap bitmap, @NonNull Rect cropRect, int rotationDegrees,
@NonNull Matrix sensorToBuffer, long timestamp) {
this(createDirectByteBuffer(bitmap),
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
index 04956c5..2918de6 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
@@ -235,7 +235,7 @@
@AnyThread
@Override
public void updateTransformMatrix(@NonNull float[] output, @NonNull float[] input) {
- System.arraycopy(input, 0, output, 0, 16);
+ System.arraycopy(mGlTransform, 0, output, 0, 16);
}
/**
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 9920500..8a1c98f 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -24,6 +24,7 @@
import android.util.Rational
import android.util.Size
import android.view.Surface
+import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
import androidx.camera.core.SurfaceRequest.TransformationInfo
import androidx.camera.core.impl.CameraFactory
import androidx.camera.core.impl.CameraThreadConfig
@@ -44,6 +45,7 @@
import androidx.camera.testing.fakes.FakeCamera
import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager
import androidx.camera.testing.fakes.FakeCameraFactory
+import androidx.camera.testing.fakes.FakeCameraInfoInternal
import androidx.camera.testing.fakes.FakeSurfaceProcessorInternal
import androidx.camera.testing.fakes.FakeUseCase
import androidx.test.core.app.ApplicationProvider
@@ -51,6 +53,7 @@
import java.util.Collections
import java.util.concurrent.ExecutionException
import org.junit.After
+import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -58,8 +61,6 @@
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
-import kotlin.jvm.Throws
-import org.junit.Assert
private val TEST_CAMERA_SELECTOR = CameraSelector.DEFAULT_BACK_CAMERA
@@ -72,27 +73,34 @@
minSdk = Build.VERSION_CODES.LOLLIPOP
)
class PreviewTest {
+
var cameraUseCaseAdapter: CameraUseCaseAdapter? = null
private lateinit var appSurface: Surface
private lateinit var appSurfaceTexture: SurfaceTexture
- private lateinit var camera: FakeCamera
+ private lateinit var backCamera: FakeCamera
+ private lateinit var frontCamera: FakeCamera
private lateinit var cameraXConfig: CameraXConfig
private lateinit var context: Context
+ private lateinit var previewToDetach: Preview
@Before
@Throws(ExecutionException::class, InterruptedException::class)
fun setUp() {
appSurfaceTexture = SurfaceTexture(0)
appSurface = Surface(appSurfaceTexture)
- camera = FakeCamera()
+ backCamera = FakeCamera("back")
+ frontCamera = FakeCamera("front", null, FakeCameraInfoInternal(0, LENS_FACING_FRONT))
val cameraFactoryProvider =
CameraFactory.Provider { _: Context?, _: CameraThreadConfig?, _: CameraSelector? ->
val cameraFactory = FakeCameraFactory()
cameraFactory.insertDefaultBackCamera(
- camera.cameraInfoInternal.cameraId
- ) { camera }
+ backCamera.cameraInfoInternal.cameraId
+ ) { backCamera }
+ cameraFactory.insertDefaultFrontCamera(
+ frontCamera.cameraInfoInternal.cameraId
+ ) { frontCamera }
cameraFactory
}
cameraXConfig = CameraXConfig.Builder.fromConfig(
@@ -111,6 +119,9 @@
this?.removeUseCases(useCases)
}
cameraUseCaseAdapter = null
+ if (::previewToDetach.isInitialized) {
+ previewToDetach.onDetached()
+ }
CameraXUtil.shutdown().get()
}
@@ -252,14 +263,22 @@
}
@Test
- fun createPreviewWithProcessor_mirroringIsTrue() {
+ fun backCameraWithProcessor_notMirrored() {
// Arrange.
val processor = FakeSurfaceProcessorInternal(mainThreadExecutor())
-
// Act: create pipeline
- val preview = createPreview(processor)
+ val preview = createPreview(processor, backCamera)
+ // Assert
+ assertThat(preview.getCameraSurface().mirroring).isFalse()
+ }
- // Assert: preview is mirrored by default.
+ @Test
+ fun frontCameraWithProcessor_mirrored() {
+ // Arrange.
+ val processor = FakeSurfaceProcessorInternal(mainThreadExecutor())
+ // Act: create pipeline
+ val preview = createPreview(processor, frontCamera)
+ // Assert
assertThat(preview.getCameraSurface().mirroring).isTrue()
}
@@ -281,9 +300,6 @@
shadowOf(getMainLooper()).idle()
// Assert: the rotation of the SettableFuture is updated based on ROTATION_90.
assertThat(preview.getCameraSurface().rotationDegrees).isEqualTo(180)
-
- // Clean up
- preview.onDetached()
}
private fun Preview.getCameraSurface(): SettableSurface {
@@ -540,21 +556,24 @@
return Pair(surfaceRequest!!, transformationInfo!!)
}
- private fun createPreview(surfaceProcessor: SurfaceProcessorInternal? = null): Preview {
- val preview = Preview.Builder()
+ private fun createPreview(
+ surfaceProcessor: SurfaceProcessorInternal? = null,
+ camera: FakeCamera = backCamera
+ ): Preview {
+ previewToDetach = Preview.Builder()
.setTargetRotation(Surface.ROTATION_0)
.build()
- preview.processor = surfaceProcessor
- preview.setSurfaceProvider(CameraXExecutors.directExecutor()) {}
+ previewToDetach.processor = surfaceProcessor
+ previewToDetach.setSurfaceProvider(CameraXExecutors.directExecutor()) {}
val previewConfig = PreviewConfig(
cameraXConfig.getUseCaseConfigFactoryProvider(null)!!.newInstance(context).getConfig(
UseCaseConfigFactory.CaptureType.PREVIEW,
ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY
)!! as OptionsBundle
)
- preview.onAttach(camera, null, previewConfig)
+ previewToDetach.onAttach(camera, null, previewConfig)
- preview.onSuggestedResolutionUpdated(Size(640, 480))
- return preview
+ previewToDetach.onSuggestedResolutionUpdated(Size(640, 480))
+ return previewToDetach
}
}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/UseCaseGroupTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/UseCaseGroupTest.kt
new file mode 100644
index 0000000..a4eae79
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/UseCaseGroupTest.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.camera.core
+
+import android.os.Build
+import androidx.camera.core.CameraEffect.IMAGE_CAPTURE
+import androidx.camera.core.CameraEffect.PREVIEW
+import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
+import androidx.camera.core.UseCaseGroup.Builder.getHumanReadableTargets
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.testing.fakes.FakePreviewEffect
+import androidx.camera.testing.fakes.FakeSurfaceProcessor
+import androidx.camera.testing.fakes.FakeUseCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+/**
+ * Unit tests for [UseCaseGroup].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class UseCaseGroupTest {
+
+ @Test
+ fun duplicateTargets_throwsException() {
+ // Arrange.
+ val previewEffect = FakePreviewEffect(
+ CameraXExecutors.mainThreadExecutor(),
+ FakeSurfaceProcessor(CameraXExecutors.mainThreadExecutor())
+ )
+ val builder = UseCaseGroup.Builder().addUseCase(FakeUseCase())
+ .addEffect(previewEffect)
+ .addEffect(previewEffect)
+
+ // Act.
+ var message: String? = null
+ try {
+ builder.build()
+ } catch (e: IllegalArgumentException) {
+ message = e.message
+ }
+
+ // Assert.
+ assertThat(message).isEqualTo(
+ "androidx.camera.testing.fakes.FakePreviewEffect " +
+ "and androidx.camera.testing.fakes.FakePreviewEffect " +
+ "contain duplicate targets PREVIEW."
+ )
+ }
+
+ @Test
+ fun verifyHumanReadableTargetsNames() {
+ assertThat(getHumanReadableTargets(PREVIEW)).isEqualTo("PREVIEW")
+ assertThat(getHumanReadableTargets(PREVIEW or VIDEO_CAPTURE))
+ .isEqualTo("PREVIEW|VIDEO_CAPTURE")
+ assertThat(getHumanReadableTargets(PREVIEW or VIDEO_CAPTURE or IMAGE_CAPTURE))
+ .isEqualTo("IMAGE_CAPTURE|PREVIEW|VIDEO_CAPTURE")
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
index 6f44748..2ea4195 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
@@ -100,6 +100,27 @@
}
@Test
+ fun updateMatrix_containsOpenGlFlipping() {
+ // Arrange.
+ val surfaceOut = createFakeSurfaceOutputImpl()
+ val input = FloatArray(16).also {
+ android.opengl.Matrix.setIdentityM(it, 0)
+ }
+
+ // Act.
+ val result = FloatArray(16)
+ surfaceOut.updateTransformMatrix(result, input)
+
+ // Assert: the result contains the flipping for OpenGL.
+ val expected = FloatArray(16).also {
+ android.opengl.Matrix.setIdentityM(it, 0)
+ android.opengl.Matrix.translateM(it, 0, 0f, 1f, 0f)
+ android.opengl.Matrix.scaleM(it, 0, 1f, -1f, 1f)
+ }
+ assertThat(result).usingTolerance(1E-4).containsExactly(expected)
+ }
+
+ @Test
fun closedSurface_noLongerReceivesCloseRequest() {
// Arrange.
val surfaceOutImpl = createFakeSurfaceOutputImpl()
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
index d037ab4..db01315 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
@@ -47,6 +47,7 @@
import androidx.core.util.Consumer
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.FlakyTest
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
@@ -341,6 +342,7 @@
file.delete()
}
+ @FlakyTest(bugId = 259294631)
@Test
fun canRecordToFile_rightAfterPreviousRecordingStopped() {
// Arrange.
diff --git a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
index b3c9695..28e1fde 100644
--- a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
+++ b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
@@ -133,7 +133,7 @@
}
@Test
- fun enableEffect_effectIsEnabled() {
+ fun enableEffect_previewEffectIsEnabled() {
// Arrange: launch app and verify effect is inactive.
fragment.assertPreviewIsStreaming()
val processor =
@@ -150,6 +150,23 @@
}
@Test
+ fun enableEffect_imageCaptureEffectIsEnabled() {
+ // Arrange: launch app and verify effect is inactive.
+ fragment.assertPreviewIsStreaming()
+ val effect = fragment.mToneMappingImageEffect as ToneMappingImageEffect
+ assertThat(effect.isInvoked()).isFalse()
+
+ // Act: turn on effect.
+ val effectToggleId = "androidx.camera.integration.view:id/effect_toggle"
+ uiDevice.findObject(UiSelector().resourceId(effectToggleId)).click()
+ instrumentation.waitForIdleSync()
+ fragment.assertCanTakePicture()
+
+ // Assert: verify that effect is active.
+ assertThat(effect.isInvoked()).isTrue()
+ }
+
+ @Test
fun controllerBound_canGetCameraControl() {
fragment.assertPreviewIsStreaming()
instrumentation.runOnMainSync {
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
index 3881ff3..79c1b1c 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
@@ -20,8 +20,8 @@
import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE;
+import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
-import static java.util.Collections.singletonList;
import android.Manifest;
import android.annotation.SuppressLint;
@@ -150,6 +150,7 @@
@VisibleForTesting
ToneMappingPreviewEffect mToneMappingPreviewEffect;
+ ToneMappingImageEffect mToneMappingImageEffect;
private final ImageAnalysis.Analyzer mAnalyzer = image -> {
byte[] bytes = new byte[image.getPlanes()[0].getBuffer().remaining()];
@@ -221,6 +222,7 @@
// Set up post-processing effects.
mToneMappingPreviewEffect = new ToneMappingPreviewEffect();
+ mToneMappingImageEffect = new ToneMappingImageEffect();
mEffectToggle = view.findViewById(R.id.effect_toggle);
mEffectToggle.setOnCheckedChangeListener((compoundButton, isChecked) -> onEffectsToggled());
onEffectsToggled();
@@ -372,7 +374,8 @@
private void onEffectsToggled() {
if (mEffectToggle.isChecked()) {
- mCameraController.setEffects(singletonList(mToneMappingPreviewEffect));
+ mCameraController.setEffects(
+ asList(mToneMappingPreviewEffect, mToneMappingImageEffect));
} else {
mCameraController.setEffects(emptyList());
}
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingImageEffect.kt b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingImageEffect.kt
new file mode 100644
index 0000000..846fa0c
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingImageEffect.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.camera.integration.view
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.Paint
+import androidx.camera.core.CameraEffect
+import androidx.camera.core.ImageProcessor
+import androidx.camera.core.ImageProcessor.Response
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.imagecapture.RgbaImageProxy
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
+
+/**
+ * A image effect that applies the same tone mapping as [ToneMappingSurfaceProcessor].
+ */
+class ToneMappingImageEffect : CameraEffect(
+ IMAGE_CAPTURE, mainThreadExecutor(), ToneMappingImageProcessor()
+) {
+
+ fun isInvoked(): Boolean {
+ return (imageProcessor as ToneMappingImageProcessor).processoed
+ }
+
+ private class ToneMappingImageProcessor : ImageProcessor {
+
+ var processoed = false
+
+ override fun process(request: ImageProcessor.Request): Response {
+ processoed = true
+ val inputImage = request.inputImages.single() as RgbaImageProxy
+ val bitmap = inputImage.createBitmap()
+ applyToneMapping(bitmap)
+ val outputImage = createOutputImage(bitmap, inputImage)
+ inputImage.close()
+ return Response { outputImage }
+ }
+
+ /**
+ * Creates output image
+ */
+ private fun createOutputImage(newBitmap: Bitmap, imageIn: ImageProxy): ImageProxy {
+ return RgbaImageProxy(
+ newBitmap,
+ imageIn.cropRect,
+ imageIn.imageInfo.rotationDegrees,
+ imageIn.imageInfo.sensorToBufferTransformMatrix,
+ imageIn.imageInfo.timestamp
+ )
+ }
+
+ /**
+ * Applies the same color matrix as [ToneMappingSurfaceProcessor].
+ */
+ private fun applyToneMapping(bitmap: Bitmap) {
+ val paint = Paint()
+ paint.colorFilter = ColorMatrixColorFilter(
+ floatArrayOf(
+ 0.5F, 0.8F, 0.3F, 0F, 0F,
+ 0.4F, 0.7F, 0.2F, 0F, 0F,
+ 0.3F, 0.5F, 0.1F, 0F, 0F,
+ 0F, 0F, 0F, 1F, 0F,
+ )
+ )
+ val canvas = Canvas(bitmap)
+ canvas.drawBitmap(bitmap, 0F, 0F, paint)
+ }
+ }
+}
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 8157779..5bc3d2c 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -870,7 +870,18 @@
method public androidx.car.app.messaging.model.CarMessage.Builder setSender(androidx.core.app.Person);
}
+ @androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.ExperimentalCarApi public interface ConversationCallback {
+ method public void onMarkAsRead();
+ method public void onTextReply(String);
+ }
+
+ @androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(6) public interface ConversationCallbackDelegate {
+ method public void sendMarkAsRead(androidx.car.app.OnDoneCallback);
+ method public void sendTextReply(String, androidx.car.app.OnDoneCallback);
+ }
+
@androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(6) public class ConversationItem implements androidx.car.app.model.Item {
+ method public androidx.car.app.messaging.model.ConversationCallbackDelegate getConversationCallbackDelegate();
method public androidx.car.app.model.CarIcon? getIcon();
method public String getId();
method public java.util.List<androidx.car.app.messaging.model.CarMessage!> getMessages();
@@ -881,6 +892,7 @@
public static final class ConversationItem.Builder {
ctor public ConversationItem.Builder();
method public androidx.car.app.messaging.model.ConversationItem build();
+ method public androidx.car.app.messaging.model.ConversationItem.Builder setConversationCallback(androidx.car.app.messaging.model.ConversationCallback);
method public androidx.car.app.messaging.model.ConversationItem.Builder setGroupConversation(boolean);
method public androidx.car.app.messaging.model.ConversationItem.Builder setIcon(androidx.car.app.model.CarIcon);
method public androidx.car.app.messaging.model.ConversationItem.Builder setId(String);
diff --git a/car/app/app/src/main/aidl/androidx/car/app/messaging/model/IConversationCallback.aidl b/car/app/app/src/main/aidl/androidx/car/app/messaging/model/IConversationCallback.aidl
new file mode 100644
index 0000000..44042e2
--- /dev/null
+++ b/car/app/app/src/main/aidl/androidx/car/app/messaging/model/IConversationCallback.aidl
@@ -0,0 +1,34 @@
+/*
+ * 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.car.app.messaging.model;
+
+import androidx.car.app.IOnDoneCallback;
+
+/**
+ * Handles Host -> Client IPC calls for a conversation.
+ *
+ * @hide
+ */
+oneway interface IConversationCallback {
+ /**
+ * Notifies the app that it should mark all messages in the current conversation as read
+ */
+ void onMarkAsRead(IOnDoneCallback callback) = 1;
+ /**
+ * Notifies the app that it should send a reply to a given conversation
+ */
+ void onTextReply(IOnDoneCallback callback, String replyText) = 2;
+}
\ No newline at end of file
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationCallback.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationCallback.java
new file mode 100644
index 0000000..e369a72
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationCallback.java
@@ -0,0 +1,36 @@
+/*
+ * 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.car.app.messaging.model;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.annotations.CarProtocol;
+import androidx.car.app.annotations.ExperimentalCarApi;
+
+/** Host -> Client callbacks for a {@link ConversationItem} */
+@ExperimentalCarApi
+@CarProtocol
+public interface ConversationCallback {
+ /**
+ * Notifies the app that it should mark all messages in the current conversation as read
+ */
+ void onMarkAsRead();
+
+ /**
+ * Notifies the app that it should send a reply to a given conversation
+ */
+ void onTextReply(@NonNull String replyText);
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationCallbackDelegate.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationCallbackDelegate.java
new file mode 100644
index 0000000..924e566
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationCallbackDelegate.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.messaging.model;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.OnDoneCallback;
+import androidx.car.app.annotations.CarProtocol;
+import androidx.car.app.annotations.ExperimentalCarApi;
+import androidx.car.app.annotations.RequiresCarApi;
+
+/** Used by the host to invoke {@link ConversationCallback} methods on the client */
+@ExperimentalCarApi
+@CarProtocol
+@RequiresCarApi(6)
+public interface ConversationCallbackDelegate {
+
+ /** Called from the host to invoke {@link ConversationCallback#onMarkAsRead()} on the client. */
+ // This mirrors the AIDL class and is not supported to support an executor as an input.
+ @SuppressLint("ExecutorRegistration")
+ void sendMarkAsRead(@NonNull OnDoneCallback onDoneCallback);
+
+ /**
+ * Called from the host to invoke {@link ConversationCallback#onTextReply(String)} on the
+ * client.
+ */
+ // This mirrors the AIDL class and is not supported to support an executor as an input.
+ @SuppressLint("ExecutorRegistration")
+ void sendTextReply(@NonNull String replyText, @NonNull OnDoneCallback onDoneCallback);
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationCallbackDelegateImpl.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationCallbackDelegateImpl.java
new file mode 100644
index 0000000..a31fd66
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationCallbackDelegateImpl.java
@@ -0,0 +1,118 @@
+/*
+ * 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.car.app.messaging.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import static java.util.Objects.requireNonNull;
+
+import android.os.RemoteException;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.OnDoneCallback;
+import androidx.car.app.annotations.CarProtocol;
+import androidx.car.app.annotations.ExperimentalCarApi;
+import androidx.car.app.annotations.RequiresCarApi;
+import androidx.car.app.utils.RemoteUtils;
+
+/**
+ * Handles binder transactions related to {@link ConversationCallback}
+ *
+ * <p> This class exists because we don't want to expose {@link IConversationCallback} to the A4C
+ * client.
+ *
+ * @hide
+ */
+@ExperimentalCarApi
+@RestrictTo(LIBRARY)
+@CarProtocol
+@RequiresCarApi(6)
+class ConversationCallbackDelegateImpl implements ConversationCallbackDelegate {
+ @Keep
+ @Nullable
+ private final IConversationCallback mConversationCallbackBinder;
+
+ ConversationCallbackDelegateImpl(@NonNull ConversationCallback conversationCallback) {
+ mConversationCallbackBinder = new ConversationCallbackStub(conversationCallback);
+ }
+
+ /** Default constructor for serialization. */
+ private ConversationCallbackDelegateImpl() {
+ mConversationCallbackBinder = null;
+ }
+
+ @Override
+ public void sendMarkAsRead(@NonNull OnDoneCallback onDoneCallback) {
+ try {
+ requireNonNull(mConversationCallbackBinder)
+ .onMarkAsRead(RemoteUtils.createOnDoneCallbackStub(onDoneCallback));
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void sendTextReply(@NonNull String replyText, @NonNull OnDoneCallback onDoneCallback) {
+ try {
+ requireNonNull(mConversationCallbackBinder).onTextReply(
+ RemoteUtils.createOnDoneCallbackStub(onDoneCallback),
+ replyText
+ );
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static class ConversationCallbackStub extends IConversationCallback.Stub {
+ @Keep
+ @NonNull
+ private final ConversationCallback mConversationCallback;
+
+ ConversationCallbackStub(@NonNull ConversationCallback conversationCallback) {
+ mConversationCallback = conversationCallback;
+ }
+
+ @Override
+ public void onMarkAsRead(@NonNull IOnDoneCallback onDoneCallback) {
+ RemoteUtils.dispatchCallFromHost(
+ onDoneCallback,
+ "onMarkAsRead", () -> {
+ mConversationCallback.onMarkAsRead();
+ return null;
+ }
+ );
+ }
+
+ @Override
+ public void onTextReply(
+ @NonNull IOnDoneCallback onDoneCallback,
+ @NonNull String replyText
+ ) {
+ RemoteUtils.dispatchCallFromHost(
+ onDoneCallback,
+ "onReply", () -> {
+ mConversationCallback.onTextReply(replyText);
+ return null;
+ }
+ );
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
index 42eeeba..2b70b2c 100644
--- a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
@@ -18,6 +18,8 @@
import static java.util.Objects.requireNonNull;
+import android.annotation.SuppressLint;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.annotations.CarProtocol;
@@ -35,12 +37,17 @@
@CarProtocol
@RequiresCarApi(6)
public class ConversationItem implements Item {
- @NonNull private final String mId;
- @NonNull private final CarText mTitle;
+ @NonNull
+ private final String mId;
+ @NonNull
+ private final CarText mTitle;
@Nullable
private final CarIcon mIcon;
private final boolean mIsGroupConversation;
- @NonNull private final List<CarMessage> mMessages;
+ @NonNull
+ private final List<CarMessage> mMessages;
+ @NonNull
+ private final ConversationCallbackDelegate mConversationCallbackDelegate;
ConversationItem(@NonNull Builder builder) {
this.mId = requireNonNull(builder.mId);
@@ -48,6 +55,8 @@
this.mIcon = builder.mIcon;
this.mIsGroupConversation = builder.mIsGroupConversation;
this.mMessages = requireNonNull(builder.mMessages);
+ this.mConversationCallbackDelegate = new ConversationCallbackDelegateImpl(
+ requireNonNull(builder.mConversationCallback));
}
/** Default constructor for serialization. */
@@ -57,6 +66,18 @@
mIcon = null;
mIsGroupConversation = false;
mMessages = new ArrayList<>();
+ mConversationCallbackDelegate = new ConversationCallbackDelegateImpl(
+ new ConversationCallback() {
+ @Override
+ public void onMarkAsRead() {
+ // Do nothing
+ }
+
+ @Override
+ public void onTextReply(@NonNull String replyText) {
+ // Do nothing
+ }
+ });
}
/**
@@ -64,17 +85,20 @@
*
* @see Builder#setId
*/
- public @NonNull String getId() {
+ @NonNull
+ public String getId() {
return mId;
}
/** Returns the title of the conversation */
- public @NonNull CarText getTitle() {
+ @NonNull
+ public CarText getTitle() {
return mTitle;
}
/** Returns a {@link CarIcon} for the conversation, or {@code null} if not set */
- public @Nullable CarIcon getIcon() {
+ @Nullable
+ public CarIcon getIcon() {
return mIcon;
}
@@ -88,10 +112,17 @@
}
/** Returns a list of messages for this {@link ConversationItem} */
- public @NonNull List<CarMessage> getMessages() {
+ @NonNull
+ public List<CarMessage> getMessages() {
return mMessages;
}
+ /** Returns host->client callbacks for this conversation */
+ @NonNull
+ public ConversationCallbackDelegate getConversationCallbackDelegate() {
+ return mConversationCallbackDelegate;
+ }
+
/** A builder for {@link ConversationItem} */
public static final class Builder {
@Nullable
@@ -103,6 +134,8 @@
boolean mIsGroupConversation;
@Nullable
List<CarMessage> mMessages;
+ @Nullable
+ ConversationCallback mConversationCallback;
/**
* Specifies a unique identifier for the conversation
@@ -114,19 +147,22 @@
* <li> Identifying {@link ConversationItem}s in "mark as read" / "reply" callbacks
* </ul>
*/
- public @NonNull Builder setId(@NonNull String id) {
+ @NonNull
+ public Builder setId(@NonNull String id) {
mId = id;
return this;
}
/** Sets the title of the conversation */
- public @NonNull Builder setTitle(@NonNull CarText title) {
+ @NonNull
+ public Builder setTitle(@NonNull CarText title) {
mTitle = title;
return this;
}
/** Sets a {@link CarIcon} for the conversation */
- public @NonNull Builder setIcon(@NonNull CarIcon icon) {
+ @NonNull
+ public Builder setIcon(@NonNull CarIcon icon) {
mIcon = icon;
return this;
}
@@ -141,19 +177,31 @@
* historical example, message readout may include sender names for group conversations, but
* omit them for 1:1 conversations.
*/
- public @NonNull Builder setGroupConversation(boolean isGroupConversation) {
+ @NonNull
+ public Builder setGroupConversation(boolean isGroupConversation) {
mIsGroupConversation = isGroupConversation;
return this;
}
/** Specifies a list of messages for the conversation */
- public @NonNull Builder setMessages(@NonNull List<CarMessage> messages) {
+ @NonNull
+ public Builder setMessages(@NonNull List<CarMessage> messages) {
mMessages = messages;
return this;
}
+ /** Sets a {@link ConversationCallback} for the conversation */
+ @SuppressLint({"MissingGetterMatchingBuilder", "ExecutorRegistration"})
+ @NonNull
+ public Builder setConversationCallback(
+ @NonNull ConversationCallback conversationCallback) {
+ mConversationCallback = conversationCallback;
+ return this;
+ }
+
/** Returns a new {@link ConversationItem} instance defined by this builder */
- public @NonNull ConversationItem build() {
+ @NonNull
+ public ConversationItem build() {
return new ConversationItem(this);
}
}
diff --git a/car/app/app/src/main/java/androidx/car/app/suggestion/model/Suggestion.java b/car/app/app/src/main/java/androidx/car/app/suggestion/model/Suggestion.java
index cfa432d..3791896 100644
--- a/car/app/app/src/main/java/androidx/car/app/suggestion/model/Suggestion.java
+++ b/car/app/app/src/main/java/androidx/car/app/suggestion/model/Suggestion.java
@@ -88,7 +88,7 @@
}
/**
- * Returns an image to display with the suggestion or {@code null} if not set.
+ * Returns a {@code CarIcon} to display with the suggestion or {@code null} if not set.
*
* @see Builder#setIcon(CarIcon)
*/
@@ -226,7 +226,7 @@
}
/**
- * Sets su suggestion image to display.
+ * Sets a suggestion image to display.
*
* <h4>Image Sizing Guidance</h4>
*
@@ -235,6 +235,8 @@
* either one of the dimensions, it will be scaled down to be centered inside the
* bounding box while preserving the aspect ratio.
*
+ * Icon images are expected to be tintable.
+ *
* <p>See {@link CarIcon} for more details related to providing icon and image resources
* that work with different car screen pixel densities.
*
diff --git a/car/app/app/src/test/java/androidx/car/app/model/constraints/RowListConstraintsTest.java b/car/app/app/src/test/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
index fa94459..8bb5fca 100644
--- a/car/app/app/src/test/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
@@ -18,7 +18,9 @@
import static org.junit.Assert.assertThrows;
+import androidx.annotation.NonNull;
import androidx.car.app.TestUtils;
+import androidx.car.app.messaging.model.ConversationCallback;
import androidx.car.app.messaging.model.ConversationItem;
import androidx.car.app.model.CarText;
import androidx.car.app.model.ItemList;
@@ -97,6 +99,17 @@
.setId("id")
.setTitle(CarText.create("title"))
.setMessages(new ArrayList<>())
+ .setConversationCallback(new ConversationCallback() {
+ @Override
+ public void onMarkAsRead() {
+ // do nothing
+ }
+
+ @Override
+ public void onTextReply(@NonNull String replyText) {
+ // do nothing
+ }
+ })
.build()
)
.build();
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ArraySet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ArraySet.kt
index 6d7a336..0c7ac8e 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ArraySet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ArraySet.kt
@@ -59,132 +59,37 @@
* @constructor Creates a new empty ArraySet. The default capacity of an array map is 0, and
* will grow once items are added to it.
*/
-public class ArraySet<E> @JvmOverloads constructor(capacity: Int = 0) :
- MutableCollection<E>, MutableSet<E> {
+public expect class ArraySet<E> @JvmOverloads constructor(
+ capacity: Int = 0
+) : MutableCollection<E>, MutableSet<E> {
- private var hashes: IntArray = EMPTY_INTS
- private var array: Array<Any?> = EMPTY_OBJECTS
+ internal var hashes: IntArray
+ internal var array: Array<Any?>
- private var _size = 0
-
+ internal var _size: Int
override val size: Int
- get() = _size
/**
* Create a new ArraySet with the mappings from the given ArraySet.
*/
- public constructor(set: ArraySet<out E>?) : this(capacity = 0) {
- if (set != null) {
- addAll(set)
- }
- }
+ public constructor(set: ArraySet<out E>?)
/**
* Create a new ArraySet with the mappings from the given [Collection].
*/
- public constructor(set: Collection<E>?) : this(capacity = 0) {
- if (set != null) {
- addAll(set)
- }
- }
+ public constructor(set: Collection<E>?)
/**
* Create a new ArraySet with items from the given array.
*/
- public constructor(array: Array<out E>?) : this(capacity = 0) {
- if (array != null) {
- for (value in array) {
- add(value)
- }
- }
- }
-
- init {
- if (capacity > 0) {
- allocArrays(capacity)
- }
- }
-
- private fun binarySearchInternal(hash: Int): Int =
- try {
- binarySearch(hashes, _size, hash)
- } catch (e: IndexOutOfBoundsException) {
- throw ConcurrentModificationException()
- }
-
- private fun indexOf(key: Any?, hash: Int): Int {
- val n = _size
-
- // Important fast case: if nothing is in here, nothing to look for.
- if (n == 0) {
- return -1
- }
- val index = binarySearchInternal(hash)
-
- // If the hash code wasn't found, then we have no entry for this key.
- if (index < 0) {
- return index
- }
-
- // If the key at the returned index matches, that's what we want.
- if (key == array[index]) {
- return index
- }
-
- // Search for a matching key after the index.
- var end = index + 1
- while (end < n && hashes[end] == hash) {
- if (key == array[end]) {
- return end
- }
- end++
- }
-
- // Search for a matching key before the index.
- var i = index - 1
- while (i >= 0 && hashes[i] == hash) {
- if (key == array[i]) {
- return i
- }
- i--
- }
-
- // Key not found -- return negative value indicating where a
- // new entry for this key should go. We use the end of the
- // hash chain to reduce the number of array entries that will
- // need to be copied when inserting.
- return end.inv()
- }
-
- private fun indexOfNull(): Int = indexOf(key = null, hash = 0)
-
- private fun allocArrays(size: Int) {
- hashes = IntArray(size)
- array = arrayOfNulls(size)
- }
-
- private inline fun printlnIfDebug(message: () -> String) {
- if (DEBUG) {
- println(message())
- }
- }
+ public constructor(array: Array<out E>?)
/**
* Make the array map empty. All storage is released.
*
* @throws ConcurrentModificationException if concurrent modifications detected.
*/
- override fun clear() {
- if (_size != 0) {
- hashes = EMPTY_INTS
- array = EMPTY_OBJECTS
- _size = 0
- }
- @Suppress("KotlinConstantConditions")
- if (_size != 0) {
- throw ConcurrentModificationException()
- }
- }
+ override fun clear()
/**
* Ensure the array map can hold at least [minimumCapacity]
@@ -192,21 +97,7 @@
*
* @throws ConcurrentModificationException if concurrent modifications detected.
*/
- public fun ensureCapacity(minimumCapacity: Int) {
- val oSize: Int = _size
- if (hashes.size < minimumCapacity) {
- val ohashes = hashes
- val oarray = array
- allocArrays(minimumCapacity)
- if (_size > 0) {
- ohashes.copyInto(destination = hashes, endIndex = _size)
- oarray.copyInto(destination = array, endIndex = _size)
- }
- }
- if (_size != oSize) {
- throw ConcurrentModificationException()
- }
- }
+ public fun ensureCapacity(minimumCapacity: Int)
/**
* Check whether a value exists in the set.
@@ -214,8 +105,7 @@
* @param element The value to search for.
* @return Returns true if the value exists, else false.
*/
- override operator fun contains(element: E): Boolean =
- indexOf(element) >= 0
+ override operator fun contains(element: E): Boolean
/**
* Returns the index of a value in the set.
@@ -223,23 +113,20 @@
* @param key The value to search for.
* @return Returns the index of the value if it exists, else a negative integer.
*/
- public fun indexOf(key: Any?): Int =
- if (key == null) indexOfNull() else indexOf(key = key, hash = key.hashCode())
+ public fun indexOf(key: Any?): Int
/**
* Return the value at the given index in the array.
+ *
* @param index The desired index, must be between 0 and [size]-1.
* @return Returns the value stored at the given index.
*/
- @Suppress("UNCHECKED_CAST")
- public fun valueAt(index: Int): E =
- array[index] as E
+ public fun valueAt(index: Int): E
/**
- * Return true if the array map contains no items.
+ * Return `true` if the array map contains no items.
*/
- override fun isEmpty(): Boolean =
- _size <= 0
+ override fun isEmpty(): Boolean
/**
* Adds the specified object to this set. The set is not modified if it
@@ -249,98 +136,15 @@
* @return `true` if this set is modified, `false` otherwise.
* @throws ConcurrentModificationException if concurrent modifications detected.
*/
- override fun add(element: E): Boolean {
- val oSize = _size
- val hash: Int
- var index: Int
- if (element == null) {
- hash = 0
- index = indexOfNull()
- } else {
- hash = element.hashCode()
- index = indexOf(element, hash)
- }
-
- if (index >= 0) {
- return false
- }
-
- index = index.inv()
- if (oSize >= hashes.size) {
- val n =
- when {
- oSize >= BASE_SIZE * 2 -> oSize + (oSize shr 1)
- oSize >= BASE_SIZE -> BASE_SIZE * 2
- else -> BASE_SIZE
- }
-
- printlnIfDebug { "$TAG add: grow from ${hashes.size} to $n" }
-
- val ohashes = hashes
- val oarray = array
- allocArrays(n)
-
- if (oSize != _size) {
- throw ConcurrentModificationException()
- }
-
- if (hashes.isNotEmpty()) {
- printlnIfDebug { "$TAG add: copy 0-$oSize to 0" }
- ohashes.copyInto(destination = hashes, endIndex = ohashes.size)
- oarray.copyInto(destination = array, endIndex = oarray.size)
- }
- }
-
- if (index < oSize) {
- printlnIfDebug { "$TAG add: move $index-${oSize - index} to ${index + 1}" }
-
- hashes.copyInto(
- destination = hashes,
- destinationOffset = index + 1,
- startIndex = index,
- endIndex = oSize
- )
- array.copyInto(
- destination = array,
- destinationOffset = index + 1,
- startIndex = index,
- endIndex = oSize
- )
- }
-
- if (oSize != _size || index >= hashes.size) {
- throw ConcurrentModificationException()
- }
-
- hashes[index] = hash
- array[index] = element
- _size++
- return true
- }
+ override fun add(element: E): Boolean
/**
* Perform a [add] of all values in [array]
+ *
* @param array The array whose contents are to be retrieved.
* @throws ConcurrentModificationException if concurrent modifications detected.
*/
- public fun addAll(array: ArraySet<out E>) {
- val n = array._size
- ensureCapacity(_size + n)
- if (_size == 0) {
- if (n > 0) {
- array.hashes.copyInto(destination = hashes, endIndex = n)
- array.array.copyInto(destination = this.array, endIndex = n)
- if (0 != _size) {
- throw ConcurrentModificationException()
- }
- _size = n
- }
- } else {
- for (i in 0 until n) {
- add(array.valueAt(i))
- }
- }
- }
+ public fun addAll(array: ArraySet<out E>)
/**
* Removes the specified object from this set.
@@ -348,125 +152,23 @@
* @param element the object to remove.
* @return `true` if this set was modified, `false` otherwise.
*/
- override fun remove(element: E): Boolean {
- val index = indexOf(element)
- if (index >= 0) {
- removeAt(index)
- return true
- }
- return false
- }
+ override fun remove(element: E): Boolean
/**
* Remove the key/value mapping at the given index.
+ *
* @param index The desired index, must be between 0 and [size]-1.
* @return Returns the value that was stored at this index.
* @throws ConcurrentModificationException if concurrent modifications detected.
*/
- public fun removeAt(index: Int): E {
- val oSize = _size
- val old = array[index]
- if (oSize <= 1) {
- // Now empty.
- printlnIfDebug { "$TAG remove: shrink from ${hashes.size} to 0" }
- clear()
- } else {
- val nSize = oSize - 1
- if (hashes.size > (BASE_SIZE * 2) && (_size < hashes.size / 3)) {
- // Shrunk enough to reduce size of arrays. We don't allow it to
- // shrink smaller than (BASE_SIZE*2) to avoid flapping between
- // that and BASE_SIZE.
- val n = if (_size > BASE_SIZE * 2) _size + (_size shr 1) else BASE_SIZE * 2
- printlnIfDebug { "$TAG remove: shrink from ${hashes.size} to $n" }
- val ohashes = hashes
- val oarray = array
- allocArrays(n)
- if (index > 0) {
- printlnIfDebug { "$TAG remove: copy from 0-$index to 0" }
- ohashes.copyInto(destination = hashes, endIndex = index)
- oarray.copyInto(destination = array, endIndex = index)
- }
- if (index < nSize) {
- printlnIfDebug { "$TAG remove: copy from ${index + 1}-$nSize to $index" }
- ohashes.copyInto(
- destination = hashes,
- destinationOffset = index,
- startIndex = index + 1,
- endIndex = nSize + 1
- )
- oarray.copyInto(
- destination = array,
- destinationOffset = index,
- startIndex = index + 1,
- endIndex = nSize + 1
- )
- }
- } else {
- if (index < nSize) {
- printlnIfDebug { "$TAG remove: move ${index + 1}-$nSize to $index" }
- hashes.copyInto(
- destination = hashes,
- destinationOffset = index,
- startIndex = index + 1,
- endIndex = nSize + 1
- )
- array.copyInto(
- destination = array,
- destinationOffset = index,
- startIndex = index + 1,
- endIndex = nSize + 1
- )
- }
- array[nSize] = null
- }
- if (oSize != _size) {
- throw ConcurrentModificationException()
- }
- _size = nSize
- }
- @Suppress("UNCHECKED_CAST")
- return old as E
- }
+ public fun removeAt(index: Int): E
/**
* Perform a [remove] of all values in [array]
+ *
* @param array The array whose contents are to be removed.
*/
- public fun removeAll(array: ArraySet<out E>): Boolean {
- // TODO: If array is sufficiently large, a marking approach might be beneficial. In a first
- // pass, use the property that the sets are sorted by hash to make this linear passes
- // (except for hash collisions, which means worst case still n*m), then do one
- // collection pass into a new array. This avoids binary searches and excessive memcpy.
- val n = array._size
-
- // Note: ArraySet does not make thread-safety guarantees. So instead of OR-ing together all
- // the single results, compare size before and after.
- val originalSize = _size
- for (i in 0 until n) {
- remove(array.valueAt(i))
- }
- return originalSize != _size
- }
-
- @Suppress("ArrayReturn")
- public fun toArray(): Array<Any?> {
- return array.copyOfRange(fromIndex = 0, toIndex = _size)
- }
-
- @Suppress("ArrayReturn")
- public fun <T> toArray(array: Array<T>): Array<T> {
- return if (array.size < _size) {
- @Suppress("UNCHECKED_CAST")
- this.array.copyOfRange(fromIndex = 0, toIndex = _size) as Array<T>
- } else {
- @Suppress("UNCHECKED_CAST")
- this.array.copyInto(array as Array<Any?>, 0, 0, _size)
- if (array.size > _size) {
- array[_size] = null
- }
- array
- }
- }
+ public fun removeAll(array: ArraySet<out E>): Boolean
/**
* This implementation returns false if the object is not a set, or
@@ -477,70 +179,19 @@
*
* @see Any.equals
*/
- override fun equals(other: Any?): Boolean {
- if (this === other) {
- return true
- }
- if (other is Set<*>) {
- if (size != other.size) {
- return false
- }
- try {
- for (i in 0 until _size) {
- val mine = valueAt(i)
- if (!other.contains(mine)) {
- return false
- }
- }
- } catch (ignored: NullPointerException) {
- return false
- } catch (ignored: ClassCastException) {
- return false
- }
- return true
- }
- return false
- }
+ override fun equals(other: Any?): Boolean
/**
* @see Any.hashCode
*/
- override fun hashCode(): Int {
- val hashes = hashes
- val s = _size
- var result = 0
- for (i in 0 until s) {
- result += hashes[i]
- }
- return result
- }
+ override fun hashCode(): Int
/**
* This implementation composes a string by iterating over its values. If
* this set contains itself as a value, the string "(this Set)"
* will appear in its place.
*/
- override fun toString(): String {
- if (isEmpty()) {
- return "{}"
- }
-
- return buildString(capacity = _size * 14) {
- append('{')
- for (i in 0 until _size) {
- if (i > 0) {
- append(", ")
- }
- val value = valueAt(i)
- if (value !== this@ArraySet) {
- append(value)
- } else {
- append("(this Set)")
- }
- }
- append('}')
- }
- }
+ override fun toString(): String
/**
* Return a [MutableIterator] over all values in the set.
@@ -548,84 +199,430 @@
* **Note:** this is a less efficient way to access the array contents compared to
* looping from 0 until [size] and calling [valueAt].
*/
- override fun iterator(): MutableIterator<E> =
- ElementIterator()
-
- private inner class ElementIterator : IndexBasedArrayIterator<E>(_size) {
- override fun elementAt(index: Int): E =
- valueAt(index)
-
- override fun removeAt(index: Int) {
- this@ArraySet.removeAt(index)
- }
- }
+ override fun iterator(): MutableIterator<E>
/**
* Determine if the array set contains all of the values in the given collection.
+ *
* @param elements The collection whose contents are to be checked against.
* @return Returns true if this array set contains a value for every entry
* in [elements] else returns false.
*/
- override fun containsAll(elements: Collection<E>): Boolean {
- for (item in elements) {
- if (!contains(item)) {
- return false
- }
- }
- return true
- }
+ override fun containsAll(elements: Collection<E>): Boolean
/**
* Perform an [add] of all values in [elements]
+ *
* @param elements The collection whose contents are to be retrieved.
*/
- override fun addAll(elements: Collection<E>): Boolean {
- ensureCapacity(_size + elements.size)
- var added = false
- for (value in elements) {
- added = add(value) or added
- }
- return added
- }
+ override fun addAll(elements: Collection<E>): Boolean
/**
* Remove all values in the array set that exist in the given collection.
+ *
* @param elements The collection whose contents are to be used to remove values.
* @return Returns true if any values were removed from the array set, else false.
*/
- override fun removeAll(elements: Collection<E>): Boolean {
- var removed = false
- for (value in elements) {
- removed = removed or remove(value)
- }
- return removed
- }
+ override fun removeAll(elements: Collection<E>): Boolean
/**
* Remove all values in the array set that do **not** exist in the given collection.
+ *
* @param elements The collection whose contents are to be used to determine which
* values to keep.
* @return Returns true if any values were removed from the array set, else false.
*/
- override fun retainAll(elements: Collection<E>): Boolean {
- var removed = false
- for (i in _size - 1 downTo 0) {
- if (array[i] !in elements) {
- removeAt(i)
- removed = true
+ override fun retainAll(elements: Collection<E>): Boolean
+}
+
+/**
+ * The minimum amount by which the capacity of a ArraySet will increase.
+ * This is tuned to be relatively space-efficient.
+ */
+internal const val ARRAY_SET_BASE_SIZE = 4
+
+internal fun <E> ArraySet<E>.binarySearchInternal(hash: Int): Int =
+ try {
+ binarySearch(hashes, _size, hash)
+ } catch (e: IndexOutOfBoundsException) {
+ throw ConcurrentModificationException()
+ }
+
+internal fun <E> ArraySet<E>.indexOf(key: Any?, hash: Int): Int {
+ val n = _size
+
+ // Important fast case: if nothing is in here, nothing to look for.
+ if (n == 0) {
+ return -1
+ }
+ val index = binarySearchInternal(hash)
+
+ // If the hash code wasn't found, then we have no entry for this key.
+ if (index < 0) {
+ return index
+ }
+
+ // If the key at the returned index matches, that's what we want.
+ if (key == array[index]) {
+ return index
+ }
+
+ // Search for a matching key after the index.
+ var end = index + 1
+ while (end < n && hashes[end] == hash) {
+ if (key == array[end]) {
+ return end
+ }
+ end++
+ }
+
+ // Search for a matching key before the index.
+ var i = index - 1
+ while (i >= 0 && hashes[i] == hash) {
+ if (key == array[i]) {
+ return i
+ }
+ i--
+ }
+
+ // Key not found -- return negative value indicating where a
+ // new entry for this key should go. We use the end of the
+ // hash chain to reduce the number of array entries that will
+ // need to be copied when inserting.
+ return end.inv()
+}
+
+internal fun <E> ArraySet<E>.indexOfNull(): Int = indexOf(key = null, hash = 0)
+
+internal fun <E> ArraySet<E>.allocArrays(size: Int) {
+ hashes = IntArray(size)
+ array = arrayOfNulls(size)
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.clearInternal() {
+ if (_size != 0) {
+ hashes = EMPTY_INTS
+ array = EMPTY_OBJECTS
+ _size = 0
+ }
+ @Suppress("KotlinConstantConditions")
+ if (_size != 0) {
+ throw ConcurrentModificationException()
+ }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.ensureCapacityInternal(minimumCapacity: Int) {
+ val oSize: Int = _size
+ if (hashes.size < minimumCapacity) {
+ val ohashes = hashes
+ val oarray = array
+ allocArrays(minimumCapacity)
+ if (_size > 0) {
+ ohashes.copyInto(destination = hashes, endIndex = _size)
+ oarray.copyInto(destination = array, endIndex = _size)
+ }
+ }
+ if (_size != oSize) {
+ throw ConcurrentModificationException()
+ }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.containsInternal(element: E): Boolean {
+ return indexOf(element) >= 0
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.indexOfInternal(key: Any?): Int {
+ return if (key == null) indexOfNull() else indexOf(key = key, hash = key.hashCode())
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.valueAtInternal(index: Int): E {
+ @Suppress("UNCHECKED_CAST")
+ return array[index] as E
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.isEmptyInternal(): Boolean {
+ return _size <= 0
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.addInternal(element: E): Boolean {
+ val oSize = _size
+ val hash: Int
+ var index: Int
+ if (element == null) {
+ hash = 0
+ index = indexOfNull()
+ } else {
+ hash = element.hashCode()
+ index = indexOf(element, hash)
+ }
+
+ if (index >= 0) {
+ return false
+ }
+
+ index = index.inv()
+ if (oSize >= hashes.size) {
+ val n =
+ when {
+ oSize >= ARRAY_SET_BASE_SIZE * 2 -> oSize + (oSize shr 1)
+ oSize >= ARRAY_SET_BASE_SIZE -> ARRAY_SET_BASE_SIZE * 2
+ else -> ARRAY_SET_BASE_SIZE
+ }
+
+ val ohashes = hashes
+ val oarray = array
+ allocArrays(n)
+
+ if (oSize != _size) {
+ throw ConcurrentModificationException()
+ }
+
+ if (hashes.isNotEmpty()) {
+ ohashes.copyInto(destination = hashes, endIndex = ohashes.size)
+ oarray.copyInto(destination = array, endIndex = oarray.size)
+ }
+ }
+
+ if (index < oSize) {
+ hashes.copyInto(
+ destination = hashes,
+ destinationOffset = index + 1,
+ startIndex = index,
+ endIndex = oSize
+ )
+ array.copyInto(
+ destination = array,
+ destinationOffset = index + 1,
+ startIndex = index,
+ endIndex = oSize
+ )
+ }
+
+ if (oSize != _size || index >= hashes.size) {
+ throw ConcurrentModificationException()
+ }
+
+ hashes[index] = hash
+ array[index] = element
+ _size++
+ return true
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.addAllInternal(array: ArraySet<out E>) {
+ val n = array._size
+ ensureCapacity(_size + n)
+ if (_size == 0) {
+ if (n > 0) {
+ array.hashes.copyInto(destination = hashes, endIndex = n)
+ array.array.copyInto(destination = this.array, endIndex = n)
+ if (0 != _size) {
+ throw ConcurrentModificationException()
+ }
+ _size = n
+ }
+ } else {
+ for (i in 0 until n) {
+ add(array.valueAt(i))
+ }
+ }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.removeInternal(element: E): Boolean {
+ val index = indexOf(element)
+ if (index >= 0) {
+ removeAt(index)
+ return true
+ }
+ return false
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.removeAtInternal(index: Int): E {
+ val oSize = _size
+ val old = array[index]
+ if (oSize <= 1) {
+ // Now empty.
+ clear()
+ } else {
+ val nSize = oSize - 1
+ if (hashes.size > (ARRAY_SET_BASE_SIZE * 2) && (_size < hashes.size / 3)) {
+ // Shrunk enough to reduce size of arrays. We don't allow it to
+ // shrink smaller than (ARRAY_SET_BASE_SIZE*2) to avoid flapping between
+ // that and ARRAY_SET_BASE_SIZE.
+ val n = when {
+ _size > ARRAY_SET_BASE_SIZE * 2 -> _size + (_size shr 1)
+ else -> ARRAY_SET_BASE_SIZE * 2
+ }
+ val ohashes = hashes
+ val oarray = array
+ allocArrays(n)
+ if (index > 0) {
+ ohashes.copyInto(destination = hashes, endIndex = index)
+ oarray.copyInto(destination = array, endIndex = index)
+ }
+ if (index < nSize) {
+ ohashes.copyInto(
+ destination = hashes,
+ destinationOffset = index,
+ startIndex = index + 1,
+ endIndex = nSize + 1
+ )
+ oarray.copyInto(
+ destination = array,
+ destinationOffset = index,
+ startIndex = index + 1,
+ endIndex = nSize + 1
+ )
+ }
+ } else {
+ if (index < nSize) {
+ hashes.copyInto(
+ destination = hashes,
+ destinationOffset = index,
+ startIndex = index + 1,
+ endIndex = nSize + 1
+ )
+ array.copyInto(
+ destination = array,
+ destinationOffset = index,
+ startIndex = index + 1,
+ endIndex = nSize + 1
+ )
+ }
+ array[nSize] = null
+ }
+ if (oSize != _size) {
+ throw ConcurrentModificationException()
+ }
+ _size = nSize
+ }
+ @Suppress("UNCHECKED_CAST")
+ return old as E
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.removeAllInternal(array: ArraySet<out E>): Boolean {
+ // TODO: If array is sufficiently large, a marking approach might be beneficial. In a first
+ // pass, use the property that the sets are sorted by hash to make this linear passes
+ // (except for hash collisions, which means worst case still n*m), then do one
+ // collection pass into a new array. This avoids binary searches and excessive memcpy.
+ val n = array._size
+
+ // Note: ArraySet does not make thread-safety guarantees. So instead of OR-ing together all
+ // the single results, compare size before and after.
+ val originalSize = _size
+ for (i in 0 until n) {
+ remove(array.valueAt(i))
+ }
+ return originalSize != _size
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.equalsInternal(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (other is Set<*>) {
+ if (size != other.size) {
+ return false
+ }
+ try {
+ for (i in 0 until _size) {
+ val mine = valueAt(i)
+ if (!other.contains(mine)) {
+ return false
+ }
+ }
+ } catch (ignored: NullPointerException) {
+ return false
+ } catch (ignored: ClassCastException) {
+ return false
+ }
+ return true
+ }
+ return false
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.hashCodeInternal(): Int {
+ val hashes = hashes
+ val s = _size
+ var result = 0
+ for (i in 0 until s) {
+ result += hashes[i]
+ }
+ return result
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.toStringInternal(): String {
+ if (isEmpty()) {
+ return "{}"
+ }
+
+ return buildString(capacity = _size * 14) {
+ append('{')
+ for (i in 0 until _size) {
+ if (i > 0) {
+ append(", ")
+ }
+ val value = valueAt(i)
+ if (value !== this@toStringInternal) {
+ append(value)
+ } else {
+ append("(this Set)")
}
}
- return removed
+ append('}')
}
+}
- private companion object {
- private const val DEBUG = false
- private const val TAG = "ArraySet"
-
- /**
- * The minimum amount by which the capacity of a ArraySet will increase.
- * This is tuned to be relatively space-efficient.
- */
- private const val BASE_SIZE = 4
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.containsAllInternal(elements: Collection<E>): Boolean {
+ for (item in elements) {
+ if (!contains(item)) {
+ return false
+ }
}
+ return true
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.addAllInternal(elements: Collection<E>): Boolean {
+ ensureCapacity(_size + elements.size)
+ var added = false
+ for (value in elements) {
+ added = add(value) or added
+ }
+ return added
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.removeAllInternal(elements: Collection<E>): Boolean {
+ var removed = false
+ for (value in elements) {
+ removed = removed or remove(value)
+ }
+ return removed
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.retainAllInternal(elements: Collection<E>): Boolean {
+ var removed = false
+ for (i in _size - 1 downTo 0) {
+ if (array[i] !in elements) {
+ removeAt(i)
+ removed = true
+ }
+ }
+ return removed
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ArraySetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ArraySetTest.kt
index 365694f..257a876 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/ArraySetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ArraySetTest.kt
@@ -35,7 +35,6 @@
import kotlinx.coroutines.runBlocking
internal class ArraySetTest {
-
private val set = ArraySet<String>()
/**
diff --git a/collection/collection/src/jvmMain/java/androidx/collection/ArraySetJvmUtil.java b/collection/collection/src/jvmMain/java/androidx/collection/ArraySetJvmUtil.java
new file mode 100644
index 0000000..13642b6
--- /dev/null
+++ b/collection/collection/src/jvmMain/java/androidx/collection/ArraySetJvmUtil.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection;
+
+import java.lang.reflect.Array;
+
+class ArraySetJvmUtil {
+ private ArraySetJvmUtil() {
+ }
+
+ // Necessary to implement in Java to allow allocating a typed array without a callback for
+ // initialization. We also need to ignore the nullity of the type in order to null out the
+ // (n+1)'th item for behavior compatibility.
+ @SuppressWarnings("unchecked")
+ static <T> T[] resizeForToArray(T[] destination, int size) {
+ if (destination.length < size) {
+ return (T[]) Array.newInstance(destination.getClass().getComponentType(), size);
+ } else {
+ if (destination.length > size) {
+ destination[size] = null;
+ }
+ return destination;
+ }
+ }
+}
diff --git a/collection/collection/src/jvmMain/kotlin/androidx/collection/ArraySet.jvm.kt b/collection/collection/src/jvmMain/kotlin/androidx/collection/ArraySet.jvm.kt
new file mode 100644
index 0000000..e804ff1
--- /dev/null
+++ b/collection/collection/src/jvmMain/kotlin/androidx/collection/ArraySet.jvm.kt
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_INTS
+import androidx.collection.internal.EMPTY_OBJECTS
+
+/**
+ * ArraySet is a generic set data structure that is designed to be more memory efficient than a
+ * traditional [HashSet]. The design is very similar to
+ * [ArrayMap], with all of the caveats described there. This implementation is
+ * separate from ArrayMap, however, so the Object array contains only one item for each
+ * entry in the set (instead of a pair for a mapping).
+ *
+ * Note that this implementation is not intended to be appropriate for data structures
+ * that may contain large numbers of items. It is generally slower than a traditional
+ * HashSet, since lookups require a binary search and adds and removes require inserting
+ * and deleting entries in the array. For containers holding up to hundreds of items,
+ * the performance difference is not significant, less than 50%.
+ *
+ * Because this container is intended to better balance memory use, unlike most other
+ * standard Java containers it will shrink its array as items are removed from it. Currently
+ * you have no control over this shrinking -- if you set a capacity and then remove an
+ * item, it may reduce the capacity to better match the current size. In the future an
+ * explicit call to set the capacity should turn off this aggressive shrinking behavior.
+ *
+ * This structure is **NOT** thread-safe.
+ *
+ * @constructor Creates a new empty ArraySet. The default capacity of an array map is 0, and
+ * will grow once items are added to it.
+ */
+public actual class ArraySet<E>
+// TODO(b/237405792): Default value for optional argument is required here to workaround Metalava's
+// lack of support for expect / actual.
+@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
+// TODO(b/237405286): @JvmOverloads is redundant in this actual, but is necessary here to workaround
+// Metalava's lack of support for expect / actual.
+@JvmOverloads actual constructor(capacity: Int = 0) : MutableCollection<E>, MutableSet<E> {
+ internal actual var hashes: IntArray = EMPTY_INTS
+ internal actual var array: Array<Any?> = EMPTY_OBJECTS
+
+ internal actual var _size = 0
+ actual override val size: Int
+ get() = _size
+
+ /**
+ * Create a new ArraySet with the mappings from the given ArraySet.
+ */
+ public actual constructor(set: ArraySet<out E>?) : this(capacity = 0) {
+ if (set != null) {
+ addAll(set)
+ }
+ }
+
+ /**
+ * Create a new ArraySet with the mappings from the given [Collection].
+ */
+ public actual constructor(set: Collection<E>?) : this(capacity = 0) {
+ if (set != null) {
+ addAll(set)
+ }
+ }
+
+ /**
+ * Create a new ArraySet with items from the given array.
+ */
+ public actual constructor(array: Array<out E>?) : this(capacity = 0) {
+ if (array != null) {
+ for (value in array) {
+ add(value)
+ }
+ }
+ }
+
+ init {
+ if (capacity > 0) {
+ allocArrays(capacity)
+ }
+ }
+
+ /**
+ * Make the array map empty. All storage is released.
+ *
+ * @throws ConcurrentModificationException if concurrent modifications detected.
+ */
+ actual override fun clear() {
+ clearInternal()
+ }
+
+ /**
+ * Ensure the array map can hold at least [minimumCapacity]
+ * items.
+ *
+ * @throws ConcurrentModificationException if concurrent modifications detected.
+ */
+ public actual fun ensureCapacity(minimumCapacity: Int) {
+ ensureCapacityInternal(minimumCapacity)
+ }
+
+ /**
+ * Check whether a value exists in the set.
+ *
+ * @param element The value to search for.
+ * @return Returns true if the value exists, else false.
+ */
+ actual override operator fun contains(element: E): Boolean {
+ return containsInternal(element)
+ }
+
+ /**
+ * Returns the index of a value in the set.
+ *
+ * @param key The value to search for.
+ * @return Returns the index of the value if it exists, else a negative integer.
+ */
+ public actual fun indexOf(key: Any?): Int {
+ return indexOfInternal(key)
+ }
+
+ /**
+ * Return the value at the given index in the array.
+ *
+ * @param index The desired index, must be between 0 and [size]-1.
+ * @return Returns the value stored at the given index.
+ */
+ public actual fun valueAt(index: Int): E {
+ return valueAtInternal(index)
+ }
+
+ /**
+ * Return `true` if the array map contains no items.
+ */
+ actual override fun isEmpty(): Boolean {
+ return isEmptyInternal()
+ }
+
+ /**
+ * Adds the specified object to this set. The set is not modified if it
+ * already contains the object.
+ *
+ * @param element the object to add.
+ * @return `true` if this set is modified, `false` otherwise.
+ * @throws ConcurrentModificationException if concurrent modifications detected.
+ */
+ actual override fun add(element: E): Boolean {
+ return addInternal(element)
+ }
+
+ /**
+ * Perform a [add] of all values in [array]
+ *
+ * @param array The array whose contents are to be retrieved.
+ * @throws ConcurrentModificationException if concurrent modifications detected.
+ */
+ public actual fun addAll(array: ArraySet<out E>) {
+ addAllInternal(array)
+ }
+
+ /**
+ * Removes the specified object from this set.
+ *
+ * @param element the object to remove.
+ * @return `true` if this set was modified, `false` otherwise.
+ */
+ actual override fun remove(element: E): Boolean {
+ return removeInternal(element)
+ }
+
+ /**
+ * Remove the key/value mapping at the given index.
+ *
+ * @param index The desired index, must be between 0 and [size]-1.
+ * @return Returns the value that was stored at this index.
+ * @throws ConcurrentModificationException if concurrent modifications detected.
+ */
+ public actual fun removeAt(index: Int): E {
+ return removeAtInternal(index)
+ }
+
+ /**
+ * Perform a [remove] of all values in [array]
+ *
+ * @param array The array whose contents are to be removed.
+ */
+ public actual fun removeAll(array: ArraySet<out E>): Boolean {
+ return removeAllInternal(array)
+ }
+
+ @Suppress("ArrayReturn")
+ public fun toArray(): Array<Any?> {
+ return array.copyOfRange(fromIndex = 0, toIndex = _size)
+ }
+
+ @Suppress("ArrayReturn")
+ public fun <T> toArray(array: Array<T>): Array<T> {
+ val result = ArraySetJvmUtil.resizeForToArray(array, _size)
+
+ @Suppress("UNCHECKED_CAST")
+ this.array.copyInto(result as Array<Any?>, 0, 0, _size)
+ return result
+ }
+
+ /**
+ * This implementation returns false if the object is not a set, or
+ * if the sets have different sizes. Otherwise, for each value in this
+ * set, it checks to make sure the value also exists in the other set.
+ * If any value doesn't exist, the method returns false; otherwise, it
+ * returns true.
+ *
+ * @see Any.equals
+ */
+ actual override fun equals(other: Any?): Boolean {
+ return equalsInternal(other)
+ }
+
+ /**
+ * @see Any.hashCode
+ */
+ actual override fun hashCode(): Int {
+ return hashCodeInternal()
+ }
+
+ /**
+ * This implementation composes a string by iterating over its values. If
+ * this set contains itself as a value, the string "(this Set)"
+ * will appear in its place.
+ */
+ actual override fun toString(): String {
+ return toStringInternal()
+ }
+
+ /**
+ * Return a [MutableIterator] over all values in the set.
+ *
+ * **Note:** this is a less efficient way to access the array contents compared to
+ * looping from 0 until [size] and calling [valueAt].
+ */
+ actual override fun iterator(): MutableIterator<E> = ElementIterator()
+
+ private inner class ElementIterator : IndexBasedArrayIterator<E>(_size) {
+ override fun elementAt(index: Int): E = valueAt(index)
+
+ override fun removeAt(index: Int) {
+ this@ArraySet.removeAt(index)
+ }
+ }
+
+ /**
+ * Determine if the array set contains all of the values in the given collection.
+ *
+ * @param elements The collection whose contents are to be checked against.
+ * @return Returns true if this array set contains a value for every entry
+ * in [elements] else returns false.
+ */
+ actual override fun containsAll(elements: Collection<E>): Boolean {
+ return containsAllInternal(elements)
+ }
+
+ /**
+ * Perform an [add] of all values in [elements]
+ *
+ * @param elements The collection whose contents are to be retrieved.
+ */
+ actual override fun addAll(elements: Collection<E>): Boolean {
+ return addAllInternal(elements)
+ }
+
+ /**
+ * Remove all values in the array set that exist in the given collection.
+ *
+ * @param elements The collection whose contents are to be used to remove values.
+ * @return Returns true if any values were removed from the array set, else false.
+ */
+ actual override fun removeAll(elements: Collection<E>): Boolean {
+ return removeAllInternal(elements)
+ }
+
+ /**
+ * Remove all values in the array set that do **not** exist in the given collection.
+ *
+ * @param elements The collection whose contents are to be used to determine which
+ * values to keep.
+ * @return Returns true if any values were removed from the array set, else false.
+ */
+ actual override fun retainAll(elements: Collection<E>): Boolean {
+ return retainAllInternal(elements)
+ }
+}
diff --git a/collection/collection/src/jvmTest/kotlin/androidx/collection/ArraySetJvmTest.kt b/collection/collection/src/jvmTest/kotlin/androidx/collection/ArraySetJvmTest.kt
new file mode 100644
index 0000000..df77ccc
--- /dev/null
+++ b/collection/collection/src/jvmTest/kotlin/androidx/collection/ArraySetJvmTest.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.assertNull
+import org.junit.Test
+
+class ArraySetJvmTest {
+
+ @Test
+ fun toArray_emptyTypedDestination() {
+ val set = ArraySet<Int>()
+ for (i in 0..5) {
+ set.add(i)
+ }
+
+ // Forces casting, otherwise may not pick up certain failures. Typing just the destination
+ // array or return type is not sufficient to test on JVM.
+ @Suppress("UNUSED_VARIABLE")
+ val result: Array<Int> = set.toArray(emptyArray())
+ }
+
+ @Test
+ fun toArray_nullsLastElement() {
+ val set = ArraySet<Int>()
+ for (i in 0..4) {
+ set.add(i)
+ }
+
+ val result: Array<Int> = set.toArray(Array(10) { -1 })
+ assertNull(result[5])
+ }
+}
\ No newline at end of file
diff --git a/collection/collection/src/nativeMain/kotlin/androidx/collection/ArraySet.native.kt b/collection/collection/src/nativeMain/kotlin/androidx/collection/ArraySet.native.kt
new file mode 100644
index 0000000..9c7be56
--- /dev/null
+++ b/collection/collection/src/nativeMain/kotlin/androidx/collection/ArraySet.native.kt
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_INTS
+import androidx.collection.internal.EMPTY_OBJECTS
+
+/**
+ * ArraySet is a generic set data structure that is designed to be more memory efficient than a
+ * traditional [HashSet]. The design is very similar to
+ * [ArrayMap], with all of the caveats described there. This implementation is
+ * separate from ArrayMap, however, so the Object array contains only one item for each
+ * entry in the set (instead of a pair for a mapping).
+ *
+ * Note that this implementation is not intended to be appropriate for data structures
+ * that may contain large numbers of items. It is generally slower than a traditional
+ * HashSet, since lookups require a binary search and adds and removes require inserting
+ * and deleting entries in the array. For containers holding up to hundreds of items,
+ * the performance difference is not significant, less than 50%.
+ *
+ * Because this container is intended to better balance memory use, unlike most other
+ * standard Java containers it will shrink its array as items are removed from it. Currently
+ * you have no control over this shrinking -- if you set a capacity and then remove an
+ * item, it may reduce the capacity to better match the current size. In the future an
+ * explicit call to set the capacity should turn off this aggressive shrinking behavior.
+ *
+ * This structure is **NOT** thread-safe.
+ *
+ * @constructor Creates a new empty ArraySet. The default capacity of an array map is 0, and
+ * will grow once items are added to it.
+ */
+public actual class ArraySet<E> actual constructor(
+ capacity: Int
+) : MutableCollection<E>, MutableSet<E> {
+
+ internal actual var hashes: IntArray = EMPTY_INTS
+ internal actual var array: Array<Any?> = EMPTY_OBJECTS
+
+ internal actual var _size = 0
+ actual override val size: Int
+ get() = _size
+
+ /**
+ * Create a new ArraySet with the mappings from the given ArraySet.
+ */
+ public actual constructor(set: ArraySet<out E>?) : this(capacity = 0) {
+ if (set != null) {
+ addAll(set)
+ }
+ }
+
+ /**
+ * Create a new ArraySet with the mappings from the given [Collection].
+ */
+ public actual constructor(set: Collection<E>?) : this(capacity = 0) {
+ if (set != null) {
+ addAll(set)
+ }
+ }
+
+ /**
+ * Create a new ArraySet with items from the given array.
+ */
+ public actual constructor(array: Array<out E>?) : this(capacity = 0) {
+ if (array != null) {
+ for (value in array) {
+ add(value)
+ }
+ }
+ }
+
+ init {
+ if (capacity > 0) {
+ allocArrays(capacity)
+ }
+ }
+
+ /**
+ * Make the array map empty. All storage is released.
+ *
+ * @throws ConcurrentModificationException if concurrent modifications detected.
+ */
+ actual override fun clear() {
+ clearInternal()
+ }
+
+ /**
+ * Ensure the array map can hold at least [minimumCapacity]
+ * items.
+ *
+ * @throws ConcurrentModificationException if concurrent modifications detected.
+ */
+ public actual fun ensureCapacity(minimumCapacity: Int) {
+ ensureCapacityInternal(minimumCapacity)
+ }
+
+ /**
+ * Check whether a value exists in the set.
+ *
+ * @param element The value to search for.
+ * @return Returns true if the value exists, else false.
+ */
+ actual override operator fun contains(element: E): Boolean {
+ return containsInternal(element)
+ }
+
+ /**
+ * Returns the index of a value in the set.
+ *
+ * @param key The value to search for.
+ * @return Returns the index of the value if it exists, else a negative integer.
+ */
+ public actual fun indexOf(key: Any?): Int {
+ return indexOfInternal(key)
+ }
+
+ /**
+ * Return the value at the given index in the array.
+ *
+ * @param index The desired index, must be between 0 and [size]-1.
+ * @return Returns the value stored at the given index.
+ */
+ public actual fun valueAt(index: Int): E {
+ return valueAtInternal(index)
+ }
+
+ /**
+ * Return `true` if the array map contains no items.
+ */
+ actual override fun isEmpty(): Boolean {
+ return isEmptyInternal()
+ }
+
+ /**
+ * Adds the specified object to this set. The set is not modified if it
+ * already contains the object.
+ *
+ * @param element the object to add.
+ * @return `true` if this set is modified, `false` otherwise.
+ * @throws ConcurrentModificationException if concurrent modifications detected.
+ */
+ actual override fun add(element: E): Boolean {
+ return addInternal(element)
+ }
+
+ /**
+ * Perform a [add] of all values in [array]
+ *
+ * @param array The array whose contents are to be retrieved.
+ * @throws ConcurrentModificationException if concurrent modifications detected.
+ */
+ public actual fun addAll(array: ArraySet<out E>) {
+ addAllInternal(array)
+ }
+
+ /**
+ * Removes the specified object from this set.
+ *
+ * @param element the object to remove.
+ * @return `true` if this set was modified, `false` otherwise.
+ */
+ actual override fun remove(element: E): Boolean {
+ return removeInternal(element)
+ }
+
+ /**
+ * Remove the key/value mapping at the given index.
+ *
+ * @param index The desired index, must be between 0 and [size]-1.
+ * @return Returns the value that was stored at this index.
+ * @throws ConcurrentModificationException if concurrent modifications detected.
+ */
+ public actual fun removeAt(index: Int): E {
+ return removeAtInternal(index)
+ }
+
+ /**
+ * Perform a [remove] of all values in [array]
+ *
+ * @param array The array whose contents are to be removed.
+ */
+ public actual fun removeAll(array: ArraySet<out E>): Boolean {
+ return removeAllInternal(array)
+ }
+
+ /**
+ * This implementation returns false if the object is not a set, or
+ * if the sets have different sizes. Otherwise, for each value in this
+ * set, it checks to make sure the value also exists in the other set.
+ * If any value doesn't exist, the method returns false; otherwise, it
+ * returns true.
+ *
+ * @see Any.equals
+ */
+ actual override fun equals(other: Any?): Boolean {
+ return equalsInternal(other)
+ }
+
+ /**
+ * @see Any.hashCode
+ */
+ actual override fun hashCode(): Int {
+ return hashCodeInternal()
+ }
+
+ /**
+ * This implementation composes a string by iterating over its values. If
+ * this set contains itself as a value, the string "(this Set)"
+ * will appear in its place.
+ */
+ actual override fun toString(): String {
+ return toStringInternal()
+ }
+
+ /**
+ * Return a [MutableIterator] over all values in the set.
+ *
+ * **Note:** this is a less efficient way to access the array contents compared to
+ * looping from 0 until [size] and calling [valueAt].
+ */
+ actual override fun iterator(): MutableIterator<E> = ElementIterator()
+
+ private inner class ElementIterator : IndexBasedArrayIterator<E>(_size) {
+ override fun elementAt(index: Int): E = valueAt(index)
+
+ override fun removeAt(index: Int) {
+ this@ArraySet.removeAt(index)
+ }
+ }
+
+ /**
+ * Determine if the array set contains all of the values in the given collection.
+ *
+ * @param elements The collection whose contents are to be checked against.
+ * @return Returns true if this array set contains a value for every entry
+ * in [elements] else returns false.
+ */
+ actual override fun containsAll(elements: Collection<E>): Boolean {
+ return containsAllInternal(elements)
+ }
+
+ /**
+ * Perform an [add] of all values in [elements]
+ *
+ * @param elements The collection whose contents are to be retrieved.
+ */
+ actual override fun addAll(elements: Collection<E>): Boolean {
+ return addAllInternal(elements)
+ }
+
+ /**
+ * Remove all values in the array set that exist in the given collection.
+ *
+ * @param elements The collection whose contents are to be used to remove values.
+ * @return Returns true if any values were removed from the array set, else false.
+ */
+ actual override fun removeAll(elements: Collection<E>): Boolean {
+ return removeAll(elements)
+ }
+
+ /**
+ * Remove all values in the array set that do **not** exist in the given collection.
+ *
+ * @param elements The collection whose contents are to be used to determine which
+ * values to keep.
+ * @return Returns true if any values were removed from the array set, else false.
+ */
+ actual override fun retainAll(elements: Collection<E>): Boolean {
+ return retainAllInternal(elements)
+ }
+}
diff --git a/compose/animation/animation-graphics/api/public_plus_experimental_current.txt b/compose/animation/animation-graphics/api/public_plus_experimental_current.txt
index f650e30..08a1623 100644
--- a/compose/animation/animation-graphics/api/public_plus_experimental_current.txt
+++ b/compose/animation/animation-graphics/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.compose.animation.graphics {
- @kotlin.RequiresOptIn(message="This is an experimental animation graphics API.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface ExperimentalAnimationGraphicsApi {
+ @kotlin.RequiresOptIn(message="This is an experimental animation graphics API.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface ExperimentalAnimationGraphicsApi {
}
}
diff --git a/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/ExperimentalAnimationGraphicsApi.kt b/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/ExperimentalAnimationGraphicsApi.kt
index 0dc6ac5..79068b1 100644
--- a/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/ExperimentalAnimationGraphicsApi.kt
+++ b/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/ExperimentalAnimationGraphicsApi.kt
@@ -18,4 +18,5 @@
@RequiresOptIn(message = "This is an experimental animation graphics API.")
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalAnimationGraphicsApi
diff --git a/compose/animation/animation/api/public_plus_experimental_current.txt b/compose/animation/animation/api/public_plus_experimental_current.txt
index 90ce764..a5aedf1 100644
--- a/compose/animation/animation/api/public_plus_experimental_current.txt
+++ b/compose/animation/animation/api/public_plus_experimental_current.txt
@@ -132,7 +132,7 @@
property public final androidx.compose.animation.ExitTransition None;
}
- @kotlin.RequiresOptIn(message="This is an experimental animation API.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface ExperimentalAnimationApi {
+ @kotlin.RequiresOptIn(message="This is an experimental animation API.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface ExperimentalAnimationApi {
}
public final class FlingCalculatorKt {
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
index 33ed868..742fcbe 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
@@ -62,6 +62,7 @@
AnnotationTarget.FIELD,
AnnotationTarget.PROPERTY_GETTER,
)
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalAnimationApi
/**
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
index db43f57..6843730 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
@@ -262,7 +262,12 @@
}
}
}
- """.trimIndent()
+
+ inline fun <T> Identity(block: () -> T): T = block()
+
+ @Composable
+ fun Stack(content: @Composable () -> Unit) = content()
+ """
)
@Test
@@ -287,6 +292,7 @@
fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(Test)<A()>,<M3>,<A()>:Test.kt")
+ val tmp0_marker = %composer.currentMarker
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
@@ -302,6 +308,7 @@
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
if (condition) {
+ %composer.endToMarker(tmp0_marker)
if (isTraceInProgress()) {
traceEventEnd()
}
@@ -359,6 +366,7 @@
fun Test(a: Boolean, b: Boolean, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(Test)<A()>,<M3>,<M3>,<A()>:Test.kt")
+ val tmp0_marker = %composer.currentMarker
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(a)) 0b0100 else 0b0010
@@ -377,6 +385,7 @@
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
if (a) {
+ %composer.endToMarker(tmp0_marker)
if (isTraceInProgress()) {
traceEventEnd()
}
@@ -397,6 +406,7 @@
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
if (b) {
+ %composer.endToMarker(tmp0_marker)
if (isTraceInProgress()) {
traceEventEnd()
}
@@ -456,12 +466,14 @@
traceEventStart(<>, %changed, -1, <>)
}
A(%composer, 0)
+ val tmp0_marker = %composer.currentMarker
M3({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
if (condition) {
+ %composer.endToMarker(tmp0_marker)
}
A(%composer, 0)
} else {
@@ -501,6 +513,7 @@
T {
%this%T.compose(composableLambdaInstance(<>, true) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C<M1>:Test.kt")
+ val tmp0_marker = %composer.currentMarker
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
if (isTraceInProgress()) {
traceEventStart(<>, %changed, -1, <>)
@@ -510,6 +523,7 @@
sourceInformation(%composer, "C:Test.kt")
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
if (condition) {
+ %composer.endToMarker(tmp0_marker)
if (isTraceInProgress()) {
traceEventEnd()
}
@@ -584,11 +598,13 @@
sourceInformation(%composer, "C<A()>,<M1>,<A()>:Test.kt")
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
+ val tmp0_marker = %composer.currentMarker
M1({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C:Test.kt")
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
if (condition) {
+ %composer.endToMarker(tmp0_marker)
}
} else {
%composer.skipToGroupEnd()
@@ -647,6 +663,7 @@
traceEventStart(<>, %changed, -1, <>)
}
A(%composer, 0)
+ val tmp0_marker = %composer.currentMarker
M3({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C<A()>,<M1>,<A()>:Test.kt")
@@ -657,6 +674,7 @@
sourceInformation(%composer, "C:Test.kt")
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
if (condition) {
+ %composer.endToMarker(tmp0_marker)
}
} else {
%composer.skipToGroupEnd()
@@ -709,6 +727,7 @@
fun testInline_M1_W_Return_Func(condition: Boolean, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(testInline_M1_W_Return_Func)<A()>,<M1>,<A()>:Test.kt")
+ val tmp0_marker = %composer.currentMarker
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
@@ -728,8 +747,7 @@
while (true) {
A(%composer, 0)
if (condition) {
- %composer.endReplaceableGroup()
- %composer.endReplaceableGroup()
+ %composer.endToMarker(tmp0_marker)
if (isTraceInProgress()) {
traceEventEnd()
}
@@ -799,12 +817,14 @@
traceEventStart(<>, %changed, -1, <>)
}
A(%composer, 0)
+ val tmp0_marker = %composer.currentMarker
M3({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
if (condition) {
+ %composer.endToMarker(tmp0_marker)
}
A(%composer, 0)
} else {
@@ -812,12 +832,14 @@
}
%composer.endReplaceableGroup()
}, %composer, 0)
+ val tmp1_marker = %composer.currentMarker
M3({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
if (condition) {
+ %composer.endToMarker(tmp1_marker)
}
A(%composer, 0)
} else {
@@ -865,6 +887,7 @@
fun test_CM1_CCM1_RetFun(condition: Boolean, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(test_CM1_CCM1_RetFun)<Text("...>,<M1>,<Text("...>:Test.kt")
+ val tmp0_marker = %composer.currentMarker
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
@@ -888,8 +911,7 @@
sourceInformation(%composer, "C<Text("...>:Test.kt")
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Text("In CCM1", %composer, 0b0110)
- %composer.endReplaceableGroup()
- %composer.endReplaceableGroup()
+ %composer.endToMarker(tmp0_marker)
if (isTraceInProgress()) {
traceEventEnd()
}
@@ -951,11 +973,13 @@
if (isTraceInProgress()) {
traceEventStart(<>, %changed, -1, <>)
}
+ val tmp0_marker = %composer.currentMarker
FakeBox({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C<A()>:Test.kt")
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
if (condition) {
+ %composer.endToMarker(tmp0_marker)
}
A(%composer, 0)
} else {
@@ -1127,11 +1151,13 @@
if (isTraceInProgress()) {
traceEventStart(<>, %changed, -1, <>)
}
+ val tmp0_marker = %composer.currentMarker
IW({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C<A()>:Test.kt")
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
if (condition) {
+ %composer.endToMarker(tmp0_marker)
}
A(%composer, 0)
} else {
@@ -1153,6 +1179,178 @@
)
@Test
+ fun testVerifyEarlyExitFromNonComposable() = verifyInlineReturn(
+ source = """
+ @Composable
+ fun Test(condition: Boolean) {
+ Text("Some text")
+ Identity {
+ if (condition) return@Test
+ }
+ Text("Some more text")
+ }
+ """,
+ expectedTransformed = """
+ @Composable
+ fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Test)<Text("...>,<Text("...>:Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ Text("Some text", %composer, 0b0110)
+ Identity {
+ if (condition) {
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ return
+ }
+ }
+ Text("Some more text", %composer, 0b0110)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ """
+ )
+
+ @Test
+ fun testVerifyEarlyExitFromNonComposable_M1() = verifyInlineReturn(
+ source = """
+ @Composable
+ fun Test(condition: Boolean) {
+ Text("Some text")
+ M1 {
+ Identity {
+ if (condition) return@Test
+ }
+ }
+ Text("Some more text")
+ }
+ """,
+ expectedTransformed = """
+ @Composable
+ fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Test)<Text("...>,<M1>,<Text("...>:Test.kt")
+ val tmp0_marker = %composer.currentMarker
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ Text("Some text", %composer, 0b0110)
+ M1({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ Identity {
+ if (condition) {
+ %composer.endToMarker(tmp0_marker)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ return
+ }
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ Text("Some more text", %composer, 0b0110)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ """
+ )
+
+ @Test
+ fun testVerifyEarlyExitFromNonComposable_M1_RM1() = verifyInlineReturn(
+ source = """
+ @Composable
+ fun Test(condition: Boolean) {
+ Text("Some text")
+ M1 {
+ Identity {
+ if (condition) return@M1
+ }
+ }
+ Text("Some more text")
+ }
+ """,
+ expectedTransformed = """
+ @Composable
+ fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Test)<Text("...>,<M1>,<Text("...>:Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ Text("Some text", %composer, 0b0110)
+ val tmp0_marker = %composer.currentMarker
+ M1({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ Identity {
+ if (condition) {
+ %composer.endToMarker(tmp0_marker)
+ }
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ Text("Some more text", %composer, 0b0110)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ """
+ )
+
+ @Test
fun testEnsureRuntimeTestWillCompile_CL() = ensureSetup {
classLoader(
"""
@@ -1203,6 +1401,7 @@
fun test_CM1_RetFun(condition: Boolean, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(test_CM1_RetFun)<Text("...>,<M1>,<Text("...>:Test.kt")
+ val tmp0_marker = %composer.currentMarker
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
@@ -1218,6 +1417,7 @@
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Text("M1 - before", %composer, 0b0110)
if (condition) {
+ %composer.endToMarker(tmp0_marker)
if (isTraceInProgress()) {
traceEventEnd()
}
@@ -5828,4 +6028,4 @@
}
"""
)
-}
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt
index 1fe4e9a..1f5756b 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt
@@ -155,11 +155,13 @@
traceEventStart(<>, %changed, -1, <>)
}
IW({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
} else {
%composer.skipToGroupEnd()
}
+ %composer.endReplaceableGroup()
}, %composer, 0)
if (isTraceInProgress()) {
traceEventEnd()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
index 656fb00..cbaf15d 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
@@ -3882,6 +3882,77 @@
}
"""
)
+
+ @Test // regression test for 204897513
+ fun test_InlineForLoop() = verifyComposeIrTransform(
+ source = """
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ Bug(listOf(1, 2, 3)) {
+ Text(it.toString())
+ }
+ }
+
+ @Composable
+ inline fun <T> Bug(items: List<T>, content: @Composable (item: T) -> Unit) {
+ for (item in items) content(item)
+ }
+ """,
+ expectedTransformed = """
+ @Composable
+ fun Test(%composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Test)<Bug(li...>:Test.kt")
+ if (%changed !== 0 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ Bug(listOf(1, 2, 3), { it: Int, %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C<Text(i...>:Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(it)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
+ Text(it.toString(), %composer, 0)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(%composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ @Composable
+ @ComposableInferredTarget(scheme = "[0[0]]")
+ fun <T> Bug(items: List<T>, content: Function3<@[ParameterName(name = 'item')] T, Composer, Int, Unit>, %composer: Composer?, %changed: Int) {
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C(Bug)P(1)*<conten...>:Test.kt")
+ val tmp0_iterator = items.iterator()
+ while (tmp0_iterator.hasNext()) {
+ val item = tmp0_iterator.next()
+ content(item, %composer, 0b01110000 and %changed)
+ }
+ %composer.endReplaceableGroup()
+ }
+ """,
+ extra = """
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Text(value: String) {}
+ """
+ )
}
class FunctionBodySkippingTransformTestsNoSource : FunctionBodySkippingTransformTestsBase() {
@@ -3945,4 +4016,72 @@
}
"""
)
+
+ @Test // regression test for 204897513
+ fun test_InlineForLoop_no_source_info() = verifyComposeIrTransform(
+ source = """
+ import androidx.compose.runtime.*
+
+ @Composable
+ private fun Test() {
+ Bug(listOf(1, 2, 3)) {
+ Text(it.toString())
+ }
+ }
+
+ @Composable
+ private inline fun <T> Bug(items: List<T>, content: @Composable (item: T) -> Unit) {
+ for (item in items) content(item)
+ }
+ """,
+ expectedTransformed = """
+ @Composable
+ private fun Test(%composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ if (%changed !== 0 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ Bug(listOf(1, 2, 3), { it: Int, %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(it)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
+ Text(it.toString(), %composer, 0)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(%composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ @Composable
+ @ComposableInferredTarget(scheme = "[0[0]]")
+ private fun <T> Bug(items: List<T>, content: Function3<@[ParameterName(name = 'item')] T, Composer, Int, Unit>, %composer: Composer?, %changed: Int) {
+ %composer.startReplaceableGroup(<>)
+ val tmp0_iterator = items.iterator()
+ while (tmp0_iterator.hasNext()) {
+ val item = tmp0_iterator.next()
+ content(item, %composer, 0b01110000 and %changed)
+ }
+ %composer.endReplaceableGroup()
+ }
+ """,
+ extra = """
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Text(value: String) {}
+ """
+ )
}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt
index e8ffd3f..d085612 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt
@@ -118,6 +118,7 @@
fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(Test)<A()>,<Wrappe...>,<A()>:Test.kt")
+ val tmp0_marker = %composer.currentMarker
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
@@ -133,6 +134,7 @@
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
if (!condition) {
+ %composer.endToMarker(tmp0_marker)
if (isTraceInProgress()) {
traceEventEnd()
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index 79b1f93c..3541e35 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -616,9 +616,8 @@
}
}
- private val rollbackGroupMarkerEnabled get() = false
- // Temporarily disabled for b/255722247
- // currentMarkerProperty != null && endToMarkerFunction != null
+ private val rollbackGroupMarkerEnabled get() =
+ currentMarkerProperty != null && endToMarkerFunction != null
private val endRestartGroupFunction by guardedLazy {
composerIrClass
@@ -1092,7 +1091,7 @@
body.startOffset,
body.endOffset,
listOfNotNull(
- if (collectSourceInformation && scope.isInlinedLambda)
+ if (scope.isInlinedLambda)
irStartReplaceableGroup(body, scope, irFunctionSourceKey())
else null,
*sourceInformationPreamble.statements.toTypedArray(),
@@ -1100,9 +1099,7 @@
*skipPreamble.statements.toTypedArray(),
*bodyPreamble.statements.toTypedArray(),
transformedBody,
- if (collectSourceInformation && scope.isInlinedLambda)
- irEndReplaceableGroup()
- else null,
+ if (scope.isInlinedLambda) irEndReplaceableGroup() else null,
returnVar?.let { irReturn(declaration.symbol, irGet(it)) }
)
)
@@ -2633,7 +2630,8 @@
break@loop
}
- if (scope.isInlinedLambda) leavingInlinedLambda = true
+ if (scope.isInlinedLambda && scope.inComposableCall)
+ leavingInlinedLambda = true
}
is Scope.BlockScope -> {
blockScopeMarks.add(scope)
diff --git a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
index f788920..f586629 100644
--- a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
@@ -111,7 +111,7 @@
method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, float weight, optional boolean fill);
}
- @kotlin.RequiresOptIn(message="The API of this layout is experimental and is likely to change in the future.") public @interface ExperimentalLayoutApi {
+ @kotlin.RequiresOptIn(message="The API of this layout is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalLayoutApi {
}
public final class IntrinsicKt {
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/ExperimentalLayoutApi.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/ExperimentalLayoutApi.kt
index 4b2c4f5..c9ae79f 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/ExperimentalLayoutApi.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/ExperimentalLayoutApi.kt
@@ -17,4 +17,5 @@
package androidx.compose.foundation.layout
@RequiresOptIn("The API of this layout is experimental and is likely to change in the future.")
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalLayoutApi
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index bda6e92..5320f47 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -66,7 +66,7 @@
method @Deprecated public static androidx.compose.ui.Modifier excludeFromSystemGesture(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,androidx.compose.ui.geometry.Rect> exclusion);
}
- @kotlin.RequiresOptIn(message="This foundation API is experimental and is likely to change or be removed in the " + "future.") public @interface ExperimentalFoundationApi {
+ @kotlin.RequiresOptIn(message="This foundation API is experimental and is likely to change or be removed in the " + "future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalFoundationApi {
}
public final class FocusableKt {
@@ -103,7 +103,7 @@
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.foundation.Indication> LocalIndication;
}
- @kotlin.RequiresOptIn(message="This API is internal to library.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InternalFoundationApi {
+ @kotlin.RequiresOptIn(message="This API is internal to library.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InternalFoundationApi {
}
public final class MagnifierKt {
@@ -371,8 +371,8 @@
@androidx.compose.foundation.ExperimentalFoundationApi public interface SnapLayoutInfoProvider {
method public float calculateApproachOffset(androidx.compose.ui.unit.Density, float initialVelocity);
+ method public float calculateSnapStepSize(androidx.compose.ui.unit.Density);
method public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> calculateSnappingOffsetBounds(androidx.compose.ui.unit.Density);
- method public float snapStepSize(androidx.compose.ui.unit.Density);
}
}
@@ -1185,7 +1185,7 @@
method public static void appendInlineContent(androidx.compose.ui.text.AnnotatedString.Builder, String id, optional String alternateText);
}
- @kotlin.RequiresOptIn(message="Internal/Unstable API for use only between foundation modules sharing " + "the same exact version, subject to change without notice.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InternalFoundationTextApi {
+ @kotlin.RequiresOptIn(message="Internal/Unstable API for use only between foundation modules sharing " + "the same exact version, subject to change without notice.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InternalFoundationTextApi {
}
public final class KeyEventHelpers_androidKt {
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index 78d781e..dd28bf30 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -64,6 +64,7 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
androidTestImplementation "androidx.activity:activity-compose:1.3.1"
+ androidTestImplementation("androidx.core:core:1.9.0")
androidTestImplementation(libs.espressoCore)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinTest)
@@ -133,6 +134,7 @@
implementation(project(":test:screenshot:screenshot"))
implementation(project(":internal-testutils-runtime"))
implementation("androidx.activity:activity-compose:1.3.1")
+ implementation("androidx.core:core:1.9.0")
implementation(libs.testUiautomator)
implementation(libs.testRules)
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyGridSnapLayoutInfoProvider.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyGridSnapLayoutInfoProvider.kt
index 9971ff9..da578d4 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyGridSnapLayoutInfoProvider.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyGridSnapLayoutInfoProvider.kt
@@ -68,7 +68,7 @@
return distanceFromItemBeforeTarget.rangeTo(distanceFromItemAfterTarget)
}
- override fun Density.snapStepSize(): Float {
+ override fun Density.calculateSnapStepSize(): Float {
return if (singleAxisItems.isNotEmpty()) {
val size = if (layoutInfo.orientation == Orientation.Vertical) {
singleAxisItems.sumOf { it.size.height }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyListSnappingDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyListSnappingDemos.kt
index 197006b..58c29e2 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyListSnappingDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyListSnappingDemos.kt
@@ -27,6 +27,7 @@
import androidx.compose.integration.demos.common.ComposableDemo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
val LazyListSnappingDemos = listOf(
@@ -42,9 +43,7 @@
@Composable
private fun SamePageSizeDemo() {
val lazyListState = rememberLazyListState()
- val layoutInfoProvider = remember(lazyListState) {
- SnapLayoutInfoProvider(lazyListState)
- }
+ val layoutInfoProvider = rememberNextPageSnappingLayoutInfoProvider(lazyListState)
val flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
SnappingDemoMainLayout(
@@ -59,9 +58,8 @@
@Composable
private fun MultiSizePageDemo() {
val lazyListState = rememberLazyListState()
- val snapLayoutInfoProvider =
- remember(lazyListState) { SnapLayoutInfoProvider(lazyListState) }
- val flingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider)
+ val layoutInfoProvider = rememberNextPageSnappingLayoutInfoProvider(lazyListState)
+ val flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
SnappingDemoMainLayout(lazyListState = lazyListState, flingBehavior = flingBehavior) {
ResizableSnapDemoItem(
@@ -76,8 +74,8 @@
@Composable
private fun LargePageSizeDemo() {
val lazyListState = rememberLazyListState()
- val snappingLayout = remember(lazyListState) { SnapLayoutInfoProvider(lazyListState) }
- val flingBehavior = rememberSnapFlingBehavior(snappingLayout)
+ val layoutInfoProvider = rememberNextPageSnappingLayoutInfoProvider(lazyListState)
+ val flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
SnappingDemoMainLayout(lazyListState = lazyListState, flingBehavior = flingBehavior) {
ResizableSnapDemoItem(
@@ -109,8 +107,7 @@
@Composable
private fun MultiPageSnappingDemo() {
val lazyListState = rememberLazyListState()
- val layoutInfoProvider = rememberMultiPageSnappingLayoutInfoProvider(lazyListState)
- val flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
+ val flingBehavior = rememberSnapFlingBehavior(lazyListState)
SnappingDemoMainLayout(lazyListState = lazyListState, flingBehavior = flingBehavior) {
DefaultSnapDemoItem(position = it)
}
@@ -130,15 +127,16 @@
@OptIn(ExperimentalFoundationApi::class)
@Composable
-private fun rememberMultiPageSnappingLayoutInfoProvider(
+private fun rememberNextPageSnappingLayoutInfoProvider(
state: LazyListState
): SnapLayoutInfoProvider {
- val animation: DecayAnimationSpec<Float> = rememberSplineBasedDecay()
return remember(state) {
- MultiPageSnappingLayoutInfoProvider(
- SnapLayoutInfoProvider(lazyListState = state),
- animation
- )
+ val basedSnappingLayoutInfoProvider = SnapLayoutInfoProvider(lazyListState = state)
+ object : SnapLayoutInfoProvider by basedSnappingLayoutInfoProvider {
+ override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
+ return 0f
+ }
+ }
}
}
@@ -147,10 +145,12 @@
private fun rememberViewPortSnappingLayoutInfoProvider(
state: LazyListState
): SnapLayoutInfoProvider {
+ val decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay()
return remember(state) {
ViewPortBasedSnappingLayoutInfoProvider(
- SnapLayoutInfoProvider(lazyListState = state)
- ) { state.layoutInfo.visibleItemsInfo.sumOf { it.size }.toFloat() }
+ SnapLayoutInfoProvider(lazyListState = state),
+ decayAnimationSpec,
+ ) { state.layoutInfo.viewportSize.width.toFloat() }
}
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnapLayoutInfoProvider.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnapLayoutInfoProvider.kt
index 37802cd..81bce0f 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnapLayoutInfoProvider.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnapLayoutInfoProvider.kt
@@ -33,22 +33,24 @@
fun Density.nextFullItemCenter(layoutCenter: Float): Float {
val intItemSize = itemSize().roundToInt()
- return floor((layoutCenter + snapStepSize()) / itemSize().roundToInt()) * intItemSize
+ return floor((layoutCenter + calculateSnapStepSize()) / itemSize().roundToInt()) *
+ intItemSize
}
fun Density.previousFullItemCenter(layoutCenter: Float): Float {
val intItemSize = itemSize().roundToInt()
- return ceil((layoutCenter - snapStepSize()) / itemSize().roundToInt()) * intItemSize
+ return ceil((layoutCenter - calculateSnapStepSize()) / itemSize().roundToInt()) *
+ intItemSize
}
override fun Density.calculateSnappingOffsetBounds(): ClosedFloatingPointRange<Float> {
- val layoutCenter = layoutSize() / 2f + scrollState.value + snapStepSize() / 2f
+ val layoutCenter = layoutSize() / 2f + scrollState.value + calculateSnapStepSize() / 2f
val lowerBound = nextFullItemCenter(layoutCenter) - layoutCenter
val upperBound = previousFullItemCenter(layoutCenter) - layoutCenter
return upperBound.rangeTo(lowerBound)
}
- override fun Density.snapStepSize(): Float {
+ override fun Density.calculateSnapStepSize(): Float {
return itemSize()
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnappingDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnappingDemos.kt
index 9806c16..ef0f3387 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnappingDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnappingDemos.kt
@@ -178,13 +178,15 @@
layoutSize: Density.() -> Float
): SnapLayoutInfoProvider {
val density = LocalDensity.current
+ val decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay()
return remember(scrollState, density, layoutSize) {
ViewPortBasedSnappingLayoutInfoProvider(
SnapLayoutInfoProvider(
scrollState = scrollState,
itemSize = { RowItemSize.toPx() },
layoutSize = layoutSize
- )
+ ),
+ decayAnimationSpec
) { with(density, layoutSize) }
}
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/SnappingDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/SnappingDemos.kt
index b10ec35..f4e5e58 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/SnappingDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/SnappingDemos.kt
@@ -42,11 +42,13 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
-import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import kotlin.math.absoluteValue
+import kotlin.math.sign
val SnappingDemos = listOf(
DemoCategory("Lazy List Snapping", LazyListSnappingDemos),
@@ -60,17 +62,22 @@
private val decayAnimationSpec: DecayAnimationSpec<Float>
) : SnapLayoutInfoProvider by baseSnapLayoutInfoProvider {
override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
- return decayAnimationSpec.calculateTargetValue(0f, initialVelocity) / 2f
+ val offset = decayAnimationSpec.calculateTargetValue(0f, initialVelocity)
+ val finalDecayedOffset = (offset.absoluteValue - calculateSnapStepSize()).coerceAtLeast(0f)
+ return finalDecayedOffset * initialVelocity.sign
}
}
@OptIn(ExperimentalFoundationApi::class)
internal class ViewPortBasedSnappingLayoutInfoProvider(
private val baseSnapLayoutInfoProvider: SnapLayoutInfoProvider,
+ private val decayAnimationSpec: DecayAnimationSpec<Float>,
private val viewPortStep: () -> Float
) : SnapLayoutInfoProvider by baseSnapLayoutInfoProvider {
override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
- return viewPortStep()
+ val offset = decayAnimationSpec.calculateTargetValue(0f, initialVelocity)
+ val viewPortOffset = viewPortStep()
+ return offset.coerceIn(-viewPortOffset, viewPortOffset)
}
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt
index e51f4fe..6186f91 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt
@@ -40,7 +40,6 @@
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.Dialog
@Preview
@Composable
@@ -77,13 +76,6 @@
}
}
-@Composable
-fun DialogInputFieldDemo(onNavigateUp: () -> Unit) {
- Dialog(onDismissRequest = onNavigateUp) {
- InputFieldDemo()
- }
-}
-
@OptIn(ExperimentalComposeUiApi::class)
@Composable
internal fun EditLine(
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/FocusTextFieldImmediatelyDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/FocusTextFieldImmediatelyDemo.kt
new file mode 100644
index 0000000..c750f11
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/FocusTextFieldImmediatelyDemo.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.foundation.demos.text
+
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+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.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+
+@Composable
+fun FocusTextFieldImmediatelyDemo() {
+ val focusRequester = remember { FocusRequester() }
+ var value by remember { mutableStateOf("") }
+
+ DisposableEffect(focusRequester) {
+ focusRequester.requestFocus()
+ onDispose {}
+ }
+
+ TextField(
+ value,
+ onValueChange = { value = it },
+ modifier = Modifier
+ .wrapContentSize()
+ .focusRequester(focusRequester)
+ )
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index 2b5a89f..eb423ca 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -95,9 +95,7 @@
ComposableDemo("Full-screen field") { FullScreenTextFieldDemo() },
ComposableDemo("Ime Action") { ImeActionDemo() },
ComposableDemo("Ime SingleLine") { ImeSingleLineDemo() },
- ComposableDemo("Inside Dialog") { onNavigateUp ->
- DialogInputFieldDemo(onNavigateUp)
- },
+ ComposableDemo("Inside Dialog") { TextFieldsInDialogDemo() },
ComposableDemo("Inside scrollable") { TextFieldsInScrollableDemo() },
ComposableDemo("Keyboard Types") { KeyboardTypeDemo() },
ComposableDemo("Min/Max Lines") { BasicTextFieldMinMaxDemo() },
@@ -106,6 +104,7 @@
ComposableDemo("Visual Transformation") { VisualTransformationDemo() },
ComposableDemo("TextFieldValue") { TextFieldValueDemo() },
ComposableDemo("Tail Following Text Field") { TailFollowingTextFieldDemo() },
+ ComposableDemo("Focus immediately") { FocusTextFieldImmediatelyDemo() },
)
),
DemoCategory(
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldsInDialogDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldsInDialogDemo.kt
new file mode 100644
index 0000000..bce2a5f
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldsInDialogDemo.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalComposeUiApi::class)
+
+package androidx.compose.foundation.demos.text
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.integration.demos.common.ComposableDemo
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ListItem
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+
+private val dialogDemos = listOf(
+ ComposableDemo("Full screen dialog, multiple fields") { onNavigateUp ->
+ Dialog(onDismissRequest = onNavigateUp) {
+ InputFieldDemo()
+ }
+ },
+ ComposableDemo(
+ "Small dialog, single field (platform default width, decor fits system windows)"
+ ) { onNavigateUp ->
+ Dialog(
+ onDismissRequest = onNavigateUp,
+ properties = DialogProperties(
+ usePlatformDefaultWidth = true,
+ decorFitsSystemWindows = true
+ )
+ ) { SingleTextFieldDialog() }
+ },
+ ComposableDemo(
+ "Small dialog, single field (decor fits system windows)"
+ ) { onNavigateUp ->
+ Dialog(
+ onDismissRequest = onNavigateUp,
+ properties = DialogProperties(
+ usePlatformDefaultWidth = false,
+ decorFitsSystemWindows = true
+ )
+ ) { SingleTextFieldDialog() }
+ },
+ ComposableDemo(
+ "Small dialog, single field (platform default width)"
+ ) { onNavigateUp ->
+ Dialog(
+ onDismissRequest = onNavigateUp,
+ properties = DialogProperties(
+ usePlatformDefaultWidth = true,
+ decorFitsSystemWindows = false
+ )
+ ) { SingleTextFieldDialog() }
+ },
+ ComposableDemo(
+ "Small dialog, single field"
+ ) { onNavigateUp ->
+ Dialog(
+ onDismissRequest = onNavigateUp,
+ properties = DialogProperties(
+ usePlatformDefaultWidth = false,
+ decorFitsSystemWindows = false
+ )
+ ) { SingleTextFieldDialog() }
+ },
+ ComposableDemo("Show keyboard automatically") { onNavigateUp ->
+ Dialog(onDismissRequest = onNavigateUp) {
+ AutoFocusTextFieldDialog()
+ }
+ }
+)
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun TextFieldsInDialogDemo() {
+ val listState = rememberLazyListState()
+ val (currentDemoIndex, setDemoIndex) = rememberSaveable { mutableStateOf(-1) }
+
+ if (currentDemoIndex == -1) {
+ LazyColumn(state = listState) {
+ itemsIndexed(dialogDemos) { index, demo ->
+ ListItem(Modifier.clickable { setDemoIndex(index) }) {
+ Text(demo.title)
+ }
+ }
+ }
+ } else {
+ val currentDemo = dialogDemos[currentDemoIndex]
+ Text(
+ currentDemo.title,
+ modifier = Modifier
+ .fillMaxSize()
+ .wrapContentSize(),
+ textAlign = TextAlign.Center
+ )
+ Layout(
+ content = { currentDemo.content(onNavigateUp = { setDemoIndex(-1) }) }
+ ) { measurables, _ ->
+ check(measurables.isEmpty()) { "Dialog demo must only emit a Dialog composable." }
+ layout(0, 0) {}
+ }
+ }
+}
+
+@Composable
+private fun SingleTextFieldDialog() {
+ var text by remember { mutableStateOf("") }
+ TextField(text, onValueChange = { text = it })
+}
+
+@Composable
+private fun AutoFocusTextFieldDialog() {
+ var text by remember { mutableStateOf("") }
+ val focusRequester = remember { FocusRequester() }
+
+ LaunchedEffect(focusRequester) {
+ focusRequester.requestFocus()
+ }
+
+ TextField(
+ text,
+ onValueChange = { text = it },
+ modifier = Modifier.focusRequester(focusRequester)
+ )
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableInScrollableViewGroupTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableInScrollableViewGroupTest.kt
index eb18a04..0ceccea 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableInScrollableViewGroupTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableInScrollableViewGroupTest.kt
@@ -37,9 +37,7 @@
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -205,10 +203,6 @@
/**
* Test case for a [clickable] inside an [AndroidView] inside a non-scrollable Compose container
*/
- @Ignore(
- "b/203141462 - currently this is not implemented so AndroidView()s will always " +
- "appear scrollable"
- )
@Test
fun clickable_androidViewInNotScrollableContainer() {
val interactionSource = MutableInteractionSource()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
index 2b284d1..69973c1 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
@@ -22,7 +22,6 @@
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.gestures.DefaultFlingBehavior
import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.gestures.ModifierLocalScrollableContainer
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.foundation.gestures.ScrollableDefaults
@@ -60,6 +59,7 @@
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.consumeScrollContainerInfo
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
@@ -69,8 +69,6 @@
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.materialize
-import androidx.compose.ui.modifier.ModifierLocalConsumer
-import androidx.compose.ui.modifier.ModifierLocalReadScope
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.LocalFocusManager
@@ -1689,49 +1687,73 @@
fun scrollable_setsModifierLocalScrollableContainer() {
val controller = ScrollableState { it }
- var isOuterInScrollableContainer: Boolean? = null
- var isInnerInScrollableContainer: Boolean? = null
+ var isOuterInVerticalScrollableContainer: Boolean? = null
+ var isInnerInVerticalScrollableContainer: Boolean? = null
+ var isOuterInHorizontalScrollableContainer: Boolean? = null
+ var isInnerInHorizontalScrollableContainer: Boolean? = null
rule.setContent {
Box {
Box(
modifier = Modifier
.testTag(scrollableBoxTag)
.size(100.dp)
- .then(
- object : ModifierLocalConsumer {
- override fun onModifierLocalsUpdated(
- scope: ModifierLocalReadScope
- ) {
- with(scope) {
- isOuterInScrollableContainer =
- ModifierLocalScrollableContainer.current
- }
- }
- }
- )
+ .consumeScrollContainerInfo {
+ isOuterInVerticalScrollableContainer = it?.canScrollVertically()
+ isOuterInHorizontalScrollableContainer = it?.canScrollHorizontally()
+ }
.scrollable(
state = controller,
orientation = Orientation.Horizontal
)
- .then(
- object : ModifierLocalConsumer {
- override fun onModifierLocalsUpdated(
- scope: ModifierLocalReadScope
- ) {
- with(scope) {
- isInnerInScrollableContainer =
- ModifierLocalScrollableContainer.current
- }
- }
- }
- )
+ .consumeScrollContainerInfo {
+ isInnerInHorizontalScrollableContainer = it?.canScrollHorizontally()
+ isInnerInVerticalScrollableContainer = it?.canScrollVertically()
+ }
)
}
}
rule.runOnIdle {
- assertThat(isOuterInScrollableContainer).isFalse()
- assertThat(isInnerInScrollableContainer).isTrue()
+ assertThat(isInnerInHorizontalScrollableContainer).isTrue()
+ assertThat(isInnerInVerticalScrollableContainer).isFalse()
+ assertThat(isOuterInVerticalScrollableContainer).isFalse()
+ assertThat(isOuterInHorizontalScrollableContainer).isFalse()
+ }
+ }
+
+ @Test
+ fun scrollable_nested_setsModifierLocalScrollableContainer() {
+ val horizontalController = ScrollableState { it }
+ val verticalController = ScrollableState { it }
+
+ var horizontalDrag: Boolean? = null
+ var verticalDrag: Boolean? = null
+ rule.setContent {
+ Box(
+ modifier = Modifier
+ .size(100.dp)
+ .scrollable(
+ state = horizontalController,
+ orientation = Orientation.Horizontal
+ )
+ ) {
+ Box(
+ modifier = Modifier
+ .size(100.dp)
+ .scrollable(
+ state = verticalController,
+ orientation = Orientation.Vertical
+ )
+ .consumeScrollContainerInfo {
+ horizontalDrag = it?.canScrollHorizontally()
+ verticalDrag = it?.canScrollVertically()
+ })
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(horizontalDrag).isTrue()
+ assertThat(verticalDrag).isTrue()
}
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapLayoutInfoProviderTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapLayoutInfoProviderTest.kt
index d889aac..42dd8ee 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapLayoutInfoProviderTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapLayoutInfoProviderTest.kt
@@ -16,6 +16,8 @@
package androidx.compose.foundation.gesture.snapping
+import androidx.compose.animation.core.calculateTargetValue
+import androidx.compose.animation.splineBasedDecay
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
@@ -34,6 +36,7 @@
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.test.filters.LargeTest
+import kotlin.math.absoluteValue
import kotlin.math.round
import kotlin.math.sign
import kotlin.test.assertEquals
@@ -59,7 +62,7 @@
val density = LocalDensity.current
val state = rememberLazyListState()
val layoutInfoProvider = remember(state) { createLayoutInfo(state) }.also {
- actualItemSize = with(it) { density.snapStepSize() }
+ actualItemSize = with(it) { density.calculateSnapStepSize() }
}
expectedItemSize = with(density) { FixedItemSize.toPx() }
MainLayout(
@@ -84,7 +87,7 @@
val density = LocalDensity.current
val state = rememberLazyListState()
val layoutInfoProvider = remember(state) { createLayoutInfo(state) }.also {
- actualItemSize = with(it) { density.snapStepSize() }
+ actualItemSize = with(it) { density.calculateSnapStepSize() }
}
expectedItemSize = state.layoutInfo.visibleItemsInfo.map { it.size }.average().toFloat()
@@ -104,7 +107,7 @@
val density = LocalDensity.current
val state = rememberLazyListState()
val layoutInfoProvider = remember(state) { createLayoutInfo(state) }.also {
- snapStepSize = with(it) { density.snapStepSize() }
+ snapStepSize = with(it) { density.calculateSnapStepSize() }
}
actualItemSize = with(density) { (FixedItemSize + FixedItemSize / 2).toPx() }
@@ -150,7 +153,40 @@
}
@Test
- fun calculateApproachOffset_approachOffsetIsAlwaysZero() {
+ fun calculateApproachOffset_highVelocity_approachOffsetIsEqualToDecayMinusItemSize() {
+ lateinit var layoutInfoProvider: SnapLayoutInfoProvider
+ val decay = splineBasedDecay<Float>(rule.density)
+ fun calculateTargetOffset(velocity: Float): Float {
+ val offset = decay.calculateTargetValue(0f, velocity).absoluteValue
+ return (offset - with(density) { 200.dp.toPx() }).coerceAtLeast(0f) * velocity.sign
+ }
+ rule.setContent {
+ val state = rememberLazyListState()
+ layoutInfoProvider = remember(state) { createLayoutInfo(state) }
+ LazyColumnOrRow(
+ state = state,
+ flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
+ ) {
+ items(200) {
+ Box(modifier = Modifier.size(200.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertEquals(
+ with(layoutInfoProvider) { density.calculateApproachOffset(10000f) },
+ calculateTargetOffset(10000f)
+ )
+ assertEquals(
+ with(layoutInfoProvider) { density.calculateApproachOffset(-10000f) },
+ calculateTargetOffset(-10000f)
+ )
+ }
+ }
+
+ @Test
+ fun calculateApproachOffset_lowVelocity_approachOffsetIsEqualToZero() {
lateinit var layoutInfoProvider: SnapLayoutInfoProvider
rule.setContent {
val state = rememberLazyListState()
@@ -166,8 +202,14 @@
}
rule.runOnIdle {
- assertEquals(with(layoutInfoProvider) { density.calculateApproachOffset(1000f) }, 0f)
- assertEquals(with(layoutInfoProvider) { density.calculateApproachOffset(-1000f) }, 0f)
+ assertEquals(
+ with(layoutInfoProvider) { density.calculateApproachOffset(1000f) },
+ 0f
+ )
+ assertEquals(
+ with(layoutInfoProvider) { density.calculateApproachOffset(-1000f) },
+ 0f
+ )
}
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
index 047223a..6c5dffe 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
@@ -257,7 +257,7 @@
) : SnapLayoutInfoProvider {
var calculateApproachOffsetCount = 0
- override fun Density.snapStepSize(): Float {
+ override fun Density.calculateSnapStepSize(): Float {
return snapStep
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
index 92acae1..9c6721f 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
@@ -28,6 +28,7 @@
import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.requiredWidthIn
+import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -1673,6 +1674,39 @@
}
}
+ @Test
+ fun itemWithSpecsIsMovingOut() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3))
+ rule.setContent {
+ LazyGrid(1, maxSize = itemSizeDp * 2) {
+ items(list, key = { it }) {
+ Item(it, animSpec = if (it == 1) AnimSpec else null)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(0, 2, 3, 1)
+ }
+
+ onAnimationFrame { fraction ->
+ val listSize = itemSize * 2
+ val item1Offset = itemSize + (itemSize * 2f * fraction).roundToInt()
+ val expected = mutableListOf<Pair<Any, IntOffset>>().apply {
+ add(0 to AxisIntOffset(0, 0))
+ if (item1Offset < listSize) {
+ add(1 to AxisIntOffset(0, item1Offset))
+ } else {
+ rule.onNodeWithTag("1").assertIsNotDisplayed()
+ }
+ }
+ assertPositions(
+ expected = expected.toTypedArray(),
+ fraction = fraction
+ )
+ }
+ }
+
private fun AxisIntOffset(crossAxis: Int, mainAxis: Int) =
if (isVertical) IntOffset(crossAxis, mainAxis) else IntOffset(mainAxis, crossAxis)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
index 44c7e34..bb58ce9 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
@@ -1219,6 +1219,39 @@
}
}
+ @Test
+ fun itemWithSpecsIsMovingOut() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3))
+ rule.setContent {
+ LazyList(maxSize = itemSizeDp * 2) {
+ items(list, key = { it }) {
+ Item(it, animSpec = if (it == 1) AnimSpec else null)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(0, 2, 3, 1)
+ }
+
+ onAnimationFrame { fraction ->
+ val listSize = itemSize * 2
+ val item1Offset = itemSize + (itemSize * 2f * fraction).roundToInt()
+ val expected = mutableListOf<Pair<Any, Int>>().apply {
+ add(0 to 0)
+ if (item1Offset < listSize) {
+ add(1 to item1Offset)
+ } else {
+ rule.onNodeWithTag("1").assertIsNotDisplayed()
+ }
+ }
+ assertPositions(
+ expected = expected.toTypedArray(),
+ fraction = fraction
+ )
+ }
+ }
+
private fun assertPositions(
vararg expected: Pair<Any, Int>,
crossAxis: List<Pair<Any, Int>>? = null,
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextMinLinesTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextMinLinesTest.kt
deleted file mode 100644
index c8a5fd5..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextMinLinesTest.kt
+++ /dev/null
@@ -1,111 +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.foundation.text
-
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.sp
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import kotlin.properties.Delegates
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@MediumTest
-@RunWith(Parameterized::class)
-class BasicTextMinLinesTest(private val useAnnotatedString: Boolean) {
- @get:Rule
- val rule = createComposeRule()
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "useAnnotatedString={0}")
- fun parameters() = arrayOf(true, false)
- }
-
- private val density = Density(1f)
- private val fontSize = 20
-
- @Test
- fun defaultMinLines_withEmptyText() {
- displayText("", 1) { height ->
- assertThat(height).isEqualTo(fontSize)
- }
- }
-
- @Test
- fun minLines_greater_thanEmptyText() {
- displayText("", 5) { height ->
- assertThat(height).isEqualTo(fontSize * 5)
- }
- }
-
- @Test
- fun minLines_smaller_thanTextLines() {
- displayText("Line1\nLine2", 1) { height ->
- assertThat(height).isEqualTo(fontSize * 2)
- }
- }
-
- @Test
- fun minLines_greater_thanTextLines() {
- displayText("Line1\nLine2", 5) { height ->
- assertThat(height).isEqualTo(fontSize * 5)
- }
- }
-
- private fun displayText(text: String, minLines: Int, verify: (Int) -> Unit) {
- var height by Delegates.notNull<Int>()
- val modifier = Modifier.fillMaxWidth().onSizeChanged { height = it.height }
- val style = TextStyle(
- fontSize = fontSize.sp,
- fontFamily = TEST_FONT_FAMILY,
- lineHeight = fontSize.sp
- )
-
- rule.setContent {
- CompositionLocalProvider(LocalDensity provides density) {
- if (useAnnotatedString) {
- BasicText(
- text = AnnotatedString(text),
- modifier = modifier,
- style = style,
- minLines = minLines
- )
- } else {
- BasicText(
- text = text,
- modifier = modifier,
- style = style,
- minLines = minLines
- )
- }
- }
- }
-
- rule.runOnIdle { verify(height) }
- }
-}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt
index 421bdef..019a837 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt
@@ -45,7 +45,6 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.click
@@ -55,6 +54,7 @@
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
+import androidx.test.filters.MediumTest
import org.junit.Assume.assumeTrue
import org.junit.Rule
import org.junit.Test
@@ -62,6 +62,7 @@
import org.junit.runners.Parameterized
import org.junit.runners.Parameterized.Parameters
+@MediumTest
@RunWith(Parameterized::class)
class CoreTextFieldKeyboardScrollableInteractionTest(
private val scrollableType: ScrollableType,
@@ -96,11 +97,11 @@
@Test
fun test() {
- // TODO(b/192043120) This is known broken when soft input mode is Resize.
- assumeTrue(softInputMode != AdjustResize)
+ // TODO(b/179203700) This is known broken for lazy lists.
+ assumeTrue(scrollableType != LazyList)
rule.setContent {
- keyboardHelper.view = LocalView.current
+ keyboardHelper.initialize()
TestContent()
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DefaultKeyboardActionsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DefaultKeyboardActionsTest.kt
index 630e70e..a9b6fe6 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DefaultKeyboardActionsTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DefaultKeyboardActionsTest.kt
@@ -24,19 +24,18 @@
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performImeAction
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.ImeAction.Companion.Done
import androidx.compose.ui.text.input.ImeAction.Companion.Go
+import androidx.compose.ui.text.input.ImeAction.Companion.Next
+import androidx.compose.ui.text.input.ImeAction.Companion.Previous
import androidx.compose.ui.text.input.ImeAction.Companion.Search
import androidx.compose.ui.text.input.ImeAction.Companion.Send
-import androidx.compose.ui.text.input.ImeAction.Companion.Previous
-import androidx.compose.ui.text.input.ImeAction.Companion.Next
-import androidx.compose.ui.text.input.ImeAction.Companion.Done
-import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.ImeOptions
import androidx.compose.ui.text.input.TextFieldValue
import androidx.test.filters.LargeTest
@@ -82,8 +81,7 @@
val keyboardHelper = KeyboardHelper(rule)
rule.setContent {
- keyboardHelper.view = LocalView.current
-
+ keyboardHelper.initialize()
Column {
CoreTextField(
value = value1,
@@ -164,8 +162,7 @@
val keyboardHelper = KeyboardHelper(rule)
rule.setContent {
- keyboardHelper.view = LocalView.current
-
+ keyboardHelper.initialize()
Column {
CoreTextField(
value = value1,
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardActionsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardActionsTest.kt
index 9d52ba0..0418c0e 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardActionsTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardActionsTest.kt
@@ -18,24 +18,23 @@
import android.os.Build
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performImeAction
import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.ImeAction.Companion.Done
import androidx.compose.ui.text.input.ImeAction.Companion.Go
+import androidx.compose.ui.text.input.ImeAction.Companion.Next
+import androidx.compose.ui.text.input.ImeAction.Companion.Previous
import androidx.compose.ui.text.input.ImeAction.Companion.Search
import androidx.compose.ui.text.input.ImeAction.Companion.Send
-import androidx.compose.ui.text.input.ImeAction.Companion.Previous
-import androidx.compose.ui.text.input.ImeAction.Companion.Next
-import androidx.compose.ui.text.input.ImeAction.Companion.Done
import androidx.compose.ui.text.input.ImeOptions
import androidx.compose.ui.text.input.TextFieldValue
-import com.google.common.truth.Truth.assertThat
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -113,8 +112,7 @@
var wasCallbackTriggered = false
rule.setContent {
- keyboardHelper.view = LocalView.current
-
+ keyboardHelper.initialize()
CoreTextField(
value = textFieldValue,
onValueChange = {},
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt
index 018cd82..f222389 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt
@@ -16,43 +16,60 @@
package androidx.compose.foundation.text
+import android.app.Activity
import android.content.Context
+import android.content.ContextWrapper
import android.os.Build
import android.view.View
+import android.view.Window
import android.view.WindowInsets
import android.view.WindowInsetsAnimation
import android.view.inputmethod.InputMethodManager
import androidx.annotation.RequiresApi
-import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.window.DialogWindowProvider
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/**
* Helper methods for hiding and showing the keyboard in tests.
- * Must set [view] before calling any methods on this class.
+ * Call [initialize] from your test rule's content before calling any other methods on this class.
*/
+@OptIn(ExperimentalComposeUiApi::class)
class KeyboardHelper(
- private val composeRule: ComposeTestRule,
+ private val composeRule: ComposeContentTestRule,
private val timeout: Long = 15_000L
) {
/**
* The [View] hosting the compose rule's content. Must be set before calling any methods on this
* class.
*/
- lateinit var view: View
+ private lateinit var view: View
private val imm by lazy {
view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
}
/**
+ * Call this at the top of your test composition before using the helper.
+ */
+ @Composable
+ fun initialize() {
+ view = LocalView.current
+ }
+
+ /**
* Requests the keyboard to be hidden without waiting for it.
- * Should be called from the main thread.
*/
fun hideKeyboard() {
- if (Build.VERSION.SDK_INT >= 30) {
+ composeRule.runOnIdle {
+ // Use both techniques to hide it, at least one of them will hopefully work.
hideKeyboardWithInsets()
- } else {
hideKeyboardWithImm()
}
}
@@ -68,26 +85,25 @@
}
fun hideKeyboardIfShown() {
- composeRule.runOnIdle {
- if (isSoftwareKeyboardShown()) {
- hideKeyboard()
- waitForKeyboardVisibility(visible = false)
- }
+ if (composeRule.runOnIdle { isSoftwareKeyboardShown() }) {
+ hideKeyboard()
+ waitForKeyboardVisibility(visible = false)
}
}
fun isSoftwareKeyboardShown(): Boolean {
- return if (Build.VERSION.SDK_INT >= 30) {
+ return if (Build.VERSION.SDK_INT >= 23) {
isSoftwareKeyboardShownWithInsets()
} else {
isSoftwareKeyboardShownWithImm()
}
}
- @RequiresApi(30)
+ @RequiresApi(23)
private fun isSoftwareKeyboardShownWithInsets(): Boolean {
- return view.rootWindowInsets != null &&
- view.rootWindowInsets.isVisible(WindowInsets.Type.ime())
+ val insets = view.rootWindowInsets ?: return false
+ val insetsCompat = WindowInsetsCompat.toWindowInsetsCompat(insets, view)
+ return insetsCompat.isVisible(WindowInsetsCompat.Type.ime())
}
private fun isSoftwareKeyboardShownWithImm(): Boolean {
@@ -97,35 +113,61 @@
}
private fun hideKeyboardWithImm() {
- imm.hideSoftInputFromWindow(view.windowToken, 0)
+ view.post {
+ imm.hideSoftInputFromWindow(view.windowToken, 0)
+ }
}
- @RequiresApi(30)
private fun hideKeyboardWithInsets() {
- view.windowInsetsController?.hide(WindowInsets.Type.ime())
+ view.findWindow()?.let { WindowInsetsControllerCompat(it, view) }
+ ?.hide(WindowInsetsCompat.Type.ime())
}
private fun waitUntil(timeout: Long, condition: () -> Boolean) {
if (Build.VERSION.SDK_INT >= 30) {
- view.waitUntil(timeout, condition)
+ view.waitForWindowInsetsUntil(timeout, condition)
} else {
composeRule.waitUntil(timeout, condition)
}
}
-}
-@RequiresApi(30)
-fun View.waitUntil(timeoutMillis: Long, condition: () -> Boolean) {
- val latch = CountDownLatch(1)
- rootView.setWindowInsetsAnimationCallback(
- InsetAnimationCallback {
+ // TODO(b/221889664) Replace with composition local when available.
+ private fun View.findWindow(): Window? =
+ (parent as? DialogWindowProvider)?.window
+ ?: context.findWindow()
+
+ private tailrec fun Context.findWindow(): Window? =
+ when (this) {
+ is Activity -> window
+ is ContextWrapper -> baseContext.findWindow()
+ else -> null
+ }
+
+ @RequiresApi(30)
+ fun View.waitForWindowInsetsUntil(timeoutMillis: Long, condition: () -> Boolean) {
+ val latch = CountDownLatch(1)
+ rootView.setOnApplyWindowInsetsListener { view, windowInsets ->
if (condition()) {
latch.countDown()
}
+ view.onApplyWindowInsets(windowInsets)
+ windowInsets
}
- )
- val conditionMet = latch.await(timeoutMillis, TimeUnit.MILLISECONDS)
- assertThat(conditionMet).isTrue()
+ rootView.setWindowInsetsAnimationCallback(
+ InsetAnimationCallback {
+ if (condition()) {
+ latch.countDown()
+ }
+ }
+ )
+
+ // if condition already met return
+ if (condition()) return
+
+ // else wait for condition to be met
+ val conditionMet = latch.await(timeoutMillis, TimeUnit.MILLISECONDS)
+ assertThat(conditionMet).isTrue()
+ }
}
@RequiresApi(30)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt
index 270cde8..c278e7b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt
@@ -18,15 +18,15 @@
import android.content.Context
import android.graphics.Typeface
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.text.CoreTextField
import androidx.compose.foundation.text.TEST_FONT
-import androidx.compose.foundation.text.TEST_FONT_FAMILY
import androidx.compose.foundation.text.heightInLines
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFontFamilyResolver
@@ -50,7 +50,8 @@
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
-import kotlin.properties.Delegates
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -63,8 +64,6 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
class HeightInLinesModifierTest {
- private val fontSize = 20
- private val defaultTextStyle = TextStyle(fontSize = 20.sp, fontFamily = TEST_FONT_FAMILY)
private val longText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " +
"eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam," +
@@ -89,23 +88,66 @@
@Test
fun minLines_shortInputText() {
- setTextFieldWithMinMaxLines(
- TextFieldValue("abc"),
- minLines = 5
- ) { height, textLayoutResult ->
- assertThat(textLayoutResult.lineCount).isEqualTo(1)
- assertThat(height).isGreaterThan(fontSize * 5)
+ var subjectLayout: TextLayoutResult? = null
+ var subjectHeight: Int? = null
+ var twoLineHeight: Int? = null
+ val positionedLatch = CountDownLatch(1)
+ val twoLinePositionedLatch = CountDownLatch(1)
+
+ rule.setContent {
+ HeightObservingText(
+ onGlobalHeightPositioned = {
+ subjectHeight = it
+ positionedLatch.countDown()
+ },
+ onTextLayoutResult = {
+ subjectLayout = it
+ },
+ textFieldValue = TextFieldValue("abc"),
+ minLines = 2
+ )
+ HeightObservingText(
+ onGlobalHeightPositioned = {
+ twoLineHeight = it
+ twoLinePositionedLatch.countDown()
+ },
+ onTextLayoutResult = {},
+ textFieldValue = TextFieldValue("1\n2"),
+ minLines = 2
+ )
+ }
+ assertThat(positionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
+ assertThat(twoLinePositionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
+
+ rule.runOnIdle {
+ assertThat(subjectLayout).isNotNull()
+ assertThat(subjectLayout!!.lineCount).isEqualTo(1)
+ assertThat(subjectHeight!!).isEqualTo(twoLineHeight)
}
}
@Test
fun maxLines_shortInputText() {
- setTextFieldWithMinMaxLines(
+ val (textLayoutResult, height) = setTextFieldWithMaxLines(
TextFieldValue("abc"),
maxLines = 5
- ) { height, textLayoutResult ->
- assertThat(textLayoutResult.lineCount).isEqualTo(1)
- assertThat(height).isGreaterThan(fontSize)
+ )
+
+ rule.runOnIdle {
+ assertThat(textLayoutResult).isNotNull()
+ assertThat(textLayoutResult!!.lineCount).isEqualTo(1)
+ assertThat(textLayoutResult.size.height).isEqualTo(height)
+ }
+ }
+
+ @Test
+ fun maxLines_notApplied_infiniteMaxLines() {
+ val (textLayoutResult, height) =
+ setTextFieldWithMaxLines(TextFieldValue(longText), Int.MAX_VALUE)
+
+ rule.runOnIdle {
+ assertThat(textLayoutResult).isNotNull()
+ assertThat(textLayoutResult!!.size.height).isEqualTo(height)
}
}
@@ -148,12 +190,16 @@
@Test
fun minLines_longInputText() {
- setTextFieldWithMinMaxLines(
+ val (textLayoutResult, height) = setTextFieldWithMaxLines(
TextFieldValue(longText),
minLines = 2
- ) { height, textLayoutResult ->
- assertThat(textLayoutResult.lineCount).isGreaterThan(2)
- assertThat(height).isGreaterThan(fontSize * 2)
+ )
+
+ rule.runOnIdle {
+ assertThat(textLayoutResult).isNotNull()
+ // should be in the 20s, but use this to create invariant for the next assertion
+ assertThat(textLayoutResult!!.lineCount).isGreaterThan(2)
+ assertThat(textLayoutResult.size.height).isEqualTo(height)
}
}
@@ -162,21 +208,33 @@
var subjectLayout: TextLayoutResult? = null
var subjectHeight: Int? = null
var twoLineHeight: Int? = null
+ val positionedLatch = CountDownLatch(1)
+ val twoLinePositionedLatch = CountDownLatch(1)
rule.setContent {
HeightObservingText(
- onHeightChanged = { subjectHeight = it },
- onTextLayoutResult = { subjectLayout = it },
+ onGlobalHeightPositioned = {
+ subjectHeight = it
+ positionedLatch.countDown()
+ },
+ onTextLayoutResult = {
+ subjectLayout = it
+ },
textFieldValue = TextFieldValue(longText),
maxLines = 2
)
HeightObservingText(
- onHeightChanged = { twoLineHeight = it },
+ onGlobalHeightPositioned = {
+ twoLineHeight = it
+ twoLinePositionedLatch.countDown()
+ },
onTextLayoutResult = {},
textFieldValue = TextFieldValue("1\n2"),
maxLines = 2
)
}
+ assertThat(positionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
+ assertThat(twoLinePositionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
rule.runOnIdle {
assertThat(subjectLayout).isNotNull()
@@ -217,12 +275,13 @@
LocalDensity provides Density(1.0f, 1f)
) {
HeightObservingText(
- onHeightChanged = {
+ onGlobalHeightPositioned = {
heights.add(it)
},
+ onTextLayoutResult = {},
textFieldValue = TextFieldValue(longText),
maxLines = 10,
- textStyle = defaultTextStyle.copy(
+ textStyle = TextStyle.Default.copy(
fontFamily = fontFamily,
fontSize = 80.sp
)
@@ -254,55 +313,61 @@
)
}
- private fun setTextFieldWithMinMaxLines(
+ private fun setTextFieldWithMaxLines(
textFieldValue: TextFieldValue,
minLines: Int = 1,
- maxLines: Int = Int.MAX_VALUE,
- verify: (Int, TextLayoutResult) -> Unit
- ) {
- var height by Delegates.notNull<Int>()
- lateinit var textLayoutResult: TextLayoutResult
+ maxLines: Int = Int.MAX_VALUE
+ ): Pair<TextLayoutResult?, Int?> {
+ var textLayoutResult: TextLayoutResult? = null
+ var height: Int? = null
+ val positionedLatch = CountDownLatch(1)
+
rule.setContent {
HeightObservingText(
- onHeightChanged = { height = it },
+ onGlobalHeightPositioned = {
+ height = it
+ positionedLatch.countDown()
+ },
+ onTextLayoutResult = {
+ textLayoutResult = it
+ },
textFieldValue = textFieldValue,
- onTextLayoutResult = { textLayoutResult = it },
minLines = minLines,
maxLines = maxLines
)
}
+ assertThat(positionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
- rule.runOnIdle {
- verify(height, textLayoutResult)
- }
+ return Pair(textLayoutResult, height)
}
@Composable
private fun HeightObservingText(
- onHeightChanged: (Int) -> Unit,
+ onGlobalHeightPositioned: (Int) -> Unit,
+ onTextLayoutResult: (TextLayoutResult) -> Unit,
textFieldValue: TextFieldValue,
- onTextLayoutResult: (TextLayoutResult) -> Unit = {},
minLines: Int = 1,
maxLines: Int = Int.MAX_VALUE,
- textStyle: TextStyle = defaultTextStyle
+ textStyle: TextStyle = TextStyle.Default
) {
- CoreTextField(
- value = textFieldValue,
- onValueChange = {},
- textStyle = textStyle,
- onTextLayout = onTextLayoutResult,
- modifier = Modifier
- .onSizeChanged {
- onHeightChanged(it.height)
- }
- .requiredWidth(100.dp)
- // we test modifier here so propagating min and max lines here instead of passing
- // them to the CoreTextField directly
- .heightInLines(
- textStyle = textStyle,
- minLines = minLines,
- maxLines = maxLines
- )
- )
+ Box(
+ Modifier.onGloballyPositioned {
+ onGlobalHeightPositioned(it.size.height)
+ }
+ ) {
+ CoreTextField(
+ value = textFieldValue,
+ onValueChange = {},
+ textStyle = textStyle,
+ modifier = Modifier
+ .requiredWidth(100.dp)
+ .heightInLines(
+ textStyle = textStyle,
+ minLines = minLines,
+ maxLines = maxLines
+ ),
+ onTextLayout = onTextLayoutResult
+ )
+ }
}
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt
index 25a01d3..e9a3958 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt
@@ -25,8 +25,11 @@
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.CoreTextField
+import androidx.compose.foundation.text.KeyboardHelper
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@@ -43,8 +46,10 @@
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
+import androidx.compose.ui.window.Dialog
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
@@ -113,7 +118,7 @@
}
@Test
- fun noCrushWhenSwitchingBetweenEnabledFocusedAndDisabledTextField() {
+ fun noCrashWhenSwitchingBetweenEnabledFocusedAndDisabledTextField() {
val enabled = mutableStateOf(true)
var focused = false
val tag = "textField"
@@ -122,9 +127,11 @@
value = TextFieldValue(),
onValueChange = {},
enabled = enabled.value,
- modifier = Modifier.testTag(tag).onFocusChanged {
- focused = it.isFocused
- }
+ modifier = Modifier
+ .testTag(tag)
+ .onFocusChanged {
+ focused = it.isFocused
+ }
)
}
// bring enabled text field into focus
@@ -198,4 +205,95 @@
).isEqualTo(innerCoordinates!!.size.toSize())
}
}
+
+ @Test
+ fun keyboardIsShown_forFieldInActivity_whenFocusRequestedImmediately_fromLaunchedEffect() {
+ keyboardIsShown_whenFocusRequestedImmediately_fromEffect(
+ runEffect = {
+ LaunchedEffect(Unit) {
+ it()
+ }
+ }
+ )
+ }
+
+ @Test
+ fun keyboardIsShown_forFieldInActivity_whenFocusRequestedImmediately_fromDisposableEffect() {
+ keyboardIsShown_whenFocusRequestedImmediately_fromEffect(
+ runEffect = {
+ DisposableEffect(Unit) {
+ it()
+ onDispose {}
+ }
+ }
+ )
+ }
+
+ // TODO(b/229378542) We can't accurately detect IME visibility from dialogs before API 30 so
+ // this test can't assert.
+ @SdkSuppress(minSdkVersion = 30)
+ @Test
+ fun keyboardIsShown_forFieldInDialog_whenFocusRequestedImmediately_fromLaunchedEffect() {
+ keyboardIsShown_whenFocusRequestedImmediately_fromEffect(
+ runEffect = {
+ LaunchedEffect(Unit) {
+ it()
+ }
+ },
+ wrapContent = {
+ Dialog(onDismissRequest = {}, content = it)
+ }
+ )
+ }
+
+ // TODO(b/229378542) We can't accurately detect IME visibility from dialogs before API 30 so
+ // this test can't assert.
+ @SdkSuppress(minSdkVersion = 30)
+ @Test
+ fun keyboardIsShown_forFieldInDialog_whenFocusRequestedImmediately_fromDisposableEffect() {
+ keyboardIsShown_whenFocusRequestedImmediately_fromEffect(
+ runEffect = {
+ DisposableEffect(Unit) {
+ it()
+ onDispose {}
+ }
+ },
+ wrapContent = {
+ Dialog(onDismissRequest = {}, content = it)
+ }
+ )
+ }
+
+ private fun keyboardIsShown_whenFocusRequestedImmediately_fromEffect(
+ runEffect: @Composable (body: () -> Unit) -> Unit,
+ wrapContent: @Composable (@Composable () -> Unit) -> Unit = { it() }
+ ) {
+ val focusRequester = FocusRequester()
+ val keyboardHelper = KeyboardHelper(rule)
+
+ rule.setContent {
+ wrapContent {
+ keyboardHelper.initialize()
+
+ runEffect {
+ assertThat(keyboardHelper.isSoftwareKeyboardShown()).isFalse()
+ focusRequester.requestFocus()
+ }
+
+ BasicTextField(
+ value = "",
+ onValueChange = {},
+ modifier = Modifier.focusRequester(focusRequester)
+ )
+ }
+ }
+
+ keyboardHelper.waitForKeyboardVisibility(visible = true)
+
+ // Ensure the keyboard doesn't leak in to the next test. Can't do this at the start of the
+ // test since the KeyboardHelper won't be initialized until composition runs, and this test
+ // is checking behavior that all happens on the first frame.
+ keyboardHelper.hideKeyboard()
+ keyboardHelper.waitForKeyboardVisibility(visible = false)
+ }
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldMinMaxLinesTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldMinMaxLinesTest.kt
deleted file mode 100644
index 9fc479c..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldMinMaxLinesTest.kt
+++ /dev/null
@@ -1,124 +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.foundation.textfield
-
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.text.CoreTextField
-import androidx.compose.foundation.text.TEST_FONT_FAMILY
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.text.TextLayoutResult
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.sp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import kotlin.properties.Delegates
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class TextFieldMinMaxLinesTest {
- private val fontSize = 20
-
- @get:Rule
- val rule = createComposeRule()
-
- @Test
- fun minLines_smaller_thanInput() {
- displayTextField(
- text = "abc\nabc\nabc",
- minLines = 1
- ) { height, textLayoutResult ->
- assertThat(textLayoutResult.lineCount).isEqualTo(3)
- assertThat(height).isEqualTo(fontSize * 3)
- }
- }
-
- @Test
- fun minLines_greater_thanInput() {
- displayTextField(
- text = "abc",
- minLines = 3
- ) { height, textLayoutResult ->
- assertThat(textLayoutResult.lineCount).isEqualTo(1)
- assertThat(height).isEqualTo(fontSize * 3)
- }
- }
-
- @Test
- fun maxLines_smaller_thanInput() {
- displayTextField(
- text = "abc\nabc\nabc",
- maxLines = 1
- ) { height, textLayoutResult ->
- assertThat(textLayoutResult.lineCount).isEqualTo(3)
- assertThat(height).isEqualTo(fontSize)
- }
- }
-
- @Test
- fun maxLines_greater_thanInput() {
- displayTextField(
- text = "abc",
- maxLines = 3
- ) { height, textLayoutResult ->
- assertThat(textLayoutResult.lineCount).isEqualTo(1)
- assertThat(height).isEqualTo(fontSize)
- }
- }
-
- private fun displayTextField(
- text: String,
- minLines: Int = 1,
- maxLines: Int = Int.MAX_VALUE,
- verify: (Int, TextLayoutResult) -> Unit
- ) {
- var height by Delegates.notNull<Int>()
- lateinit var textLayoutResult: TextLayoutResult
- rule.setContent {
- CompositionLocalProvider(LocalDensity provides Density(1f)) {
- CoreTextField(
- value = TextFieldValue(text),
- onValueChange = {},
- onTextLayout = { textLayoutResult = it },
- modifier = Modifier
- .onSizeChanged { height = it.height }
- .fillMaxWidth(),
- minLines = minLines,
- maxLines = maxLines,
- textStyle = TextStyle(
- fontSize = fontSize.sp,
- fontFamily = TEST_FONT_FAMILY,
- lineHeight = fontSize.sp
- )
- )
- }
- }
-
- rule.runOnIdle {
- verify(height, textLayoutResult)
- }
- }
-}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Clickable.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Clickable.android.kt
index 6b5cef6..79247de 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Clickable.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Clickable.android.kt
@@ -19,37 +19,13 @@
import android.view.KeyEvent.KEYCODE_DPAD_CENTER
import android.view.KeyEvent.KEYCODE_ENTER
import android.view.KeyEvent.KEYCODE_NUMPAD_ENTER
-import android.view.View
import android.view.ViewConfiguration
-import android.view.ViewGroup
-import androidx.compose.runtime.Composable
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
import androidx.compose.ui.input.key.KeyEventType.Companion.KeyUp
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.nativeKeyCode
import androidx.compose.ui.input.key.type
-import androidx.compose.ui.platform.LocalView
-
-@Composable
-internal actual fun isComposeRootInScrollableContainer(): () -> Boolean {
- val view = LocalView.current
- return {
- view.isInScrollableViewGroup()
- }
-}
-
-// Copied from View#isInScrollingContainer() which is @hide
-private fun View.isInScrollableViewGroup(): Boolean {
- var p = parent
- while (p != null && p is ViewGroup) {
- if (p.shouldDelayChildPressedState()) {
- return true
- }
- p = p.parent
- }
- return false
-}
internal actual val TapIndicationDelay: Long = ViewConfiguration.getTapTimeout().toLong()
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
index b249514..64643f1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
@@ -16,7 +16,6 @@
package androidx.compose.foundation
-import androidx.compose.foundation.gestures.ModifierLocalScrollableContainer
import androidx.compose.foundation.gestures.PressGestureScope
import androidx.compose.foundation.gestures.detectTapAndPress
import androidx.compose.foundation.gestures.detectTapGestures
@@ -38,8 +37,8 @@
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.modifier.ModifierLocalConsumer
-import androidx.compose.ui.modifier.ModifierLocalReadScope
+import androidx.compose.ui.input.canScroll
+import androidx.compose.ui.input.consumeScrollContainerInfo
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.disabled
@@ -144,11 +143,8 @@
currentKeyPressInteractions
)
}
- val isRootInScrollableContainer = isComposeRootInScrollableContainer()
- val isClickableInScrollableContainer = remember { mutableStateOf(true) }
- val delayPressInteraction = rememberUpdatedState {
- isClickableInScrollableContainer.value || isRootInScrollableContainer()
- }
+
+ val delayPressInteraction = remember { mutableStateOf({ true }) }
val centreOffset = remember { mutableStateOf(Offset.Zero) }
val gesture = Modifier.pointerInput(interactionSource, enabled) {
@@ -168,18 +164,9 @@
)
}
Modifier
- .then(
- remember {
- object : ModifierLocalConsumer {
- override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
- with(scope) {
- isClickableInScrollableContainer.value =
- ModifierLocalScrollableContainer.current
- }
- }
- }
- }
- )
+ .consumeScrollContainerInfo { scrollContainerInfo ->
+ delayPressInteraction.value = { scrollContainerInfo?.canScroll() == true }
+ }
.genericClickableWithoutGesture(
gestureModifiers = gesture,
interactionSource = interactionSource,
@@ -330,13 +317,9 @@
currentKeyPressInteractions
)
}
- val isRootInScrollableContainer = isComposeRootInScrollableContainer()
- val isClickableInScrollableContainer = remember { mutableStateOf(true) }
- val delayPressInteraction = rememberUpdatedState {
- isClickableInScrollableContainer.value || isRootInScrollableContainer()
- }
- val centreOffset = remember { mutableStateOf(Offset.Zero) }
+ val delayPressInteraction = remember { mutableStateOf({ true }) }
+ val centreOffset = remember { mutableStateOf(Offset.Zero) }
val gesture =
Modifier.pointerInput(interactionSource, hasLongClick, hasDoubleClick, enabled) {
centreOffset.value = size.center.toOffset()
@@ -365,18 +348,6 @@
)
}
Modifier
- .then(
- remember {
- object : ModifierLocalConsumer {
- override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
- with(scope) {
- isClickableInScrollableContainer.value =
- ModifierLocalScrollableContainer.current
- }
- }
- }
- }
- )
.genericClickableWithoutGesture(
gestureModifiers = gesture,
interactionSource = interactionSource,
@@ -391,6 +362,9 @@
onLongClick = onLongClick,
onClick = onClick
)
+ .consumeScrollContainerInfo { scrollContainerInfo ->
+ delayPressInteraction.value = { scrollContainerInfo?.canScroll() == true }
+ }
},
inspectorInfo = debugInspectorInfo {
name = "combinedClickable"
@@ -475,20 +449,6 @@
internal expect val TapIndicationDelay: Long
/**
- * Returns a lambda that calculates whether the root Compose layout node is hosted in a scrollable
- * container outside of Compose. On Android this will be whether the root View is in a scrollable
- * ViewGroup, as even if nothing in the Compose part of the hierarchy is scrollable, if the View
- * itself is in a scrollable container, we still want to delay presses in case presses in Compose
- * convert to a scroll outside of Compose.
- *
- * Combine this with [ModifierLocalScrollableContainer], which returns whether a [Modifier] is
- * within a scrollable Compose layout, to calculate whether this modifier is within some form of
- * scrollable container, and hence should delay presses.
- */
-@Composable
-internal expect fun isComposeRootInScrollableContainer(): () -> Boolean
-
-/**
* Whether the specified [KeyEvent] should trigger a press for a clickable component.
*/
internal expect val KeyEvent.isPress: Boolean
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ExperimentalFoundationApi.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ExperimentalFoundationApi.kt
index 9b19cf5..193eaab 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ExperimentalFoundationApi.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ExperimentalFoundationApi.kt
@@ -20,4 +20,5 @@
"This foundation API is experimental and is likely to change or be removed in the " +
"future."
)
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalFoundationApi
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/InternalFoundationApi.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/InternalFoundationApi.kt
index 0e0fe4a..19a79c5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/InternalFoundationApi.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/InternalFoundationApi.kt
@@ -21,4 +21,5 @@
AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY,
AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER
)
+@Retention(AnnotationRetention.BINARY)
annotation class InternalFoundationApi
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
index 6507207..161c854 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
@@ -24,6 +24,7 @@
import androidx.compose.foundation.gestures.DragEvent.DragStopped
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.internal.JvmDefaultWithCompatibility
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -52,7 +53,6 @@
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive
-import androidx.compose.foundation.internal.JvmDefaultWithCompatibility
/**
* State of [draggable]. Allows for a granular control of how deltas are consumed by the user as
@@ -255,6 +255,7 @@
}
}
}
+
Modifier.pointerInput(orientation, enabled, reverseDirection) {
if (!enabled) return@pointerInput
coroutineScope {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index 7aac595..b89a959 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -37,6 +37,7 @@
import androidx.compose.ui.MotionDurationScale
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.ScrollContainerInfo
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
@@ -48,8 +49,7 @@
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.modifier.ModifierLocalProvider
-import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.input.provideScrollContainerInfo
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Density
@@ -172,7 +172,6 @@
overscrollEffect,
enabled
)
- .then(if (enabled) ModifierLocalScrollableContainerProvider else Modifier)
}
)
@@ -266,6 +265,14 @@
val draggableState = remember { ScrollDraggableState(scrollLogic) }
val scrollConfig = platformScrollConfig()
+ val scrollContainerInfo = remember(orientation, enabled) {
+ object : ScrollContainerInfo {
+ override fun canScrollHorizontally() = enabled && orientation == Horizontal
+
+ override fun canScrollVertically() = enabled && orientation == Orientation.Vertical
+ }
+ }
+
return draggable(
draggableState,
orientation = orientation,
@@ -282,6 +289,7 @@
)
.mouseWheelScroll(scrollLogic, scrollConfig)
.nestedScroll(nestedScrollConnection, nestedScrollDispatcher.value)
+ .provideScrollContainerInfo(scrollContainerInfo)
}
private fun Modifier.mouseWheelScroll(
@@ -580,21 +588,9 @@
}
}
-// TODO: b/203141462 - make this public and move it to ui
-/**
- * Whether this modifier is inside a scrollable container, provided by [Modifier.scrollable].
- * Defaults to false.
- */
-internal val ModifierLocalScrollableContainer = modifierLocalOf { false }
-
-private object ModifierLocalScrollableContainerProvider : ModifierLocalProvider<Boolean> {
- override val key = ModifierLocalScrollableContainer
- override val value = true
-}
-
private const val DefaultScrollMotionDurationScaleFactor = 1f
private val DefaultScrollMotionDurationScale = object : MotionDurationScale {
override val scaleFactor: Float
get() = DefaultScrollMotionDurationScaleFactor
-}
\ No newline at end of file
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyListSnapLayoutInfoProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyListSnapLayoutInfoProvider.kt
index edb54bf..036babe 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyListSnapLayoutInfoProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyListSnapLayoutInfoProvider.kt
@@ -16,6 +16,9 @@
package androidx.compose.foundation.gestures.snapping
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.calculateTargetValue
+import androidx.compose.animation.splineBasedDecay
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.Orientation
@@ -27,6 +30,8 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastSumBy
+import kotlin.math.absoluteValue
+import kotlin.math.sign
/**
* A [SnapLayoutInfoProvider] for LazyLists.
@@ -48,8 +53,18 @@
private val layoutInfo: LazyListLayoutInfo
get() = lazyListState.layoutInfo
- // Single page snapping is the default
- override fun Density.calculateApproachOffset(initialVelocity: Float): Float = 0f
+ // Decayed page snapping is the default
+ override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
+ val decayAnimationSpec: DecayAnimationSpec<Float> = splineBasedDecay(this)
+ val offset =
+ decayAnimationSpec.calculateTargetValue(NoDistance, initialVelocity).absoluteValue
+ val finalDecayOffset = (offset - calculateSnapStepSize()).coerceAtLeast(0f)
+ return if (finalDecayOffset == 0f) {
+ finalDecayOffset
+ } else {
+ finalDecayOffset * initialVelocity.sign
+ }
+ }
override fun Density.calculateSnappingOffsetBounds(): ClosedFloatingPointRange<Float> {
var lowerBoundOffset = Float.NEGATIVE_INFINITY
@@ -73,7 +88,7 @@
return lowerBoundOffset.rangeTo(upperBoundOffset)
}
- override fun Density.snapStepSize(): Float = with(layoutInfo) {
+ override fun Density.calculateSnapStepSize(): Float = with(layoutInfo) {
if (visibleItemsInfo.isNotEmpty()) {
visibleItemsInfo.fastSumBy { it.size } / visibleItemsInfo.size.toFloat()
} else {
@@ -83,7 +98,7 @@
}
/**
- * Create and remember a FlingBehavior for single page snapping in Lazy Lists. This will snap
+ * Create and remember a FlingBehavior for decayed snapping in Lazy Lists. This will snap
* the item's center to the center of the viewport.
*
* @param lazyListState The [LazyListState] from the LazyList where this [FlingBehavior] will
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
index 0d9cfc7..9d632b2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
@@ -41,6 +41,7 @@
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlin.math.abs
+import kotlin.math.absoluteValue
import kotlin.math.sign
/**
@@ -100,12 +101,14 @@
}
private suspend fun ScrollScope.shortSnap(velocity: Float) {
+ debugLog { "Short Snapping" }
val closestOffset = findClosestOffset(0f, snapLayoutInfoProvider, density)
val animationState = AnimationState(NoDistance, velocity)
animateSnap(closestOffset, closestOffset, animationState, snapAnimationSpec)
}
private suspend fun ScrollScope.longSnap(initialVelocity: Float) {
+ debugLog { "Long Snapping" }
val initialOffset =
with(snapLayoutInfoProvider) { density.calculateApproachOffset(initialVelocity) }.let {
abs(it) * sign(initialVelocity) // ensure offset sign is correct
@@ -113,7 +116,14 @@
val (remainingOffset, animationState) = runApproach(initialOffset, initialVelocity)
- animateSnap(remainingOffset, remainingOffset, animationState, snapAnimationSpec)
+ debugLog { "Settling Final Bound=$remainingOffset" }
+
+ animateSnap(
+ remainingOffset,
+ remainingOffset,
+ animationState.copy(value = 0f),
+ snapAnimationSpec
+ )
}
private suspend fun ScrollScope.runApproach(
@@ -123,8 +133,10 @@
val animation =
if (isDecayApproachPossible(offset = initialTargetOffset, velocity = initialVelocity)) {
+ debugLog { "High Velocity Approach" }
HighVelocityApproachAnimation(highVelocityAnimationSpec)
} else {
+ debugLog { "Low Velocity Approach" }
LowVelocityApproachAnimation(
lowVelocityAnimationSpec,
snapLayoutInfoProvider,
@@ -149,7 +161,8 @@
velocity: Float
): Boolean {
val decayOffset = highVelocityAnimationSpec.calculateTargetValue(NoDistance, velocity)
- return abs(decayOffset) > abs(offset)
+ val snapStepSize = with(snapLayoutInfoProvider) { density.calculateSnapStepSize() }
+ return decayOffset.absoluteValue >= (offset.absoluteValue + snapStepSize)
}
override fun equals(other: Any?): Boolean {
@@ -267,6 +280,7 @@
lowerBound
}
}
+
1f -> upperBound
-1f -> lowerBound
else -> NoDistance
@@ -377,7 +391,9 @@
): AnimationState<Float, AnimationVector1D> {
val animationState = AnimationState(initialValue = 0f, initialVelocity = velocity)
val targetOffset =
- (abs(offset) + with(layoutInfoProvider) { density.snapStepSize() }) * sign(velocity)
+ (abs(offset) + with(layoutInfoProvider) { density.calculateSnapStepSize() }) * sign(
+ velocity
+ )
return with(scope) {
animateSnap(
targetOffset = targetOffset,
@@ -406,4 +422,11 @@
internal val MinFlingVelocityDp = 400.dp
internal const val NoDistance = 0f
-internal const val NoVelocity = 0f
\ No newline at end of file
+internal const val NoVelocity = 0f
+private const val DEBUG = false
+
+private inline fun debugLog(generateMsg: () -> String) {
+ if (DEBUG) {
+ println("SnapFlingBehavior: ${generateMsg()}")
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapLayoutInfoProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapLayoutInfoProvider.kt
index 9b6ca10..71dea06 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapLayoutInfoProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapLayoutInfoProvider.kt
@@ -29,9 +29,9 @@
@ExperimentalFoundationApi
interface SnapLayoutInfoProvider {
/**
- * The minimum offset that snapping will use to animate. (e.g. an item size)
+ * The minimum offset that snapping will use to animate.(e.g. an item size)
*/
- fun Density.snapStepSize(): Float
+ fun Density.calculateSnapStepSize(): Float
/**
* Calculate the distance to navigate before settling into the next snapping bound.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt
index b1e300d..eadd33f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt
@@ -71,7 +71,7 @@
positionedItems: MutableList<LazyListPositionedItem>,
itemProvider: LazyMeasuredItemProvider
) {
- if (!positionedItems.fastAny { it.hasAnimations }) {
+ if (!positionedItems.fastAny { it.hasAnimations } && keyToItemInfoMap.isEmpty()) {
// no animations specified - no work needed
reset()
return
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
index 351a8a1..d8f4f5f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
@@ -76,7 +76,7 @@
measuredItemProvider: LazyMeasuredItemProvider,
spanLayoutProvider: LazyGridSpanLayoutProvider
) {
- if (!positionedItems.fastAny { it.hasAnimations }) {
+ if (!positionedItems.fastAny { it.hasAnimations } && keyToItemInfoMap.isEmpty()) {
// no animations specified - no work needed
reset()
return
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
index b8ded54..d499986 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
@@ -220,7 +220,6 @@
softWrap = softWrap,
fontFamilyResolver = fontFamilyResolver,
overflow = overflow,
- minLines = minLines,
maxLines = maxLines,
placeholders = placeholders
),
@@ -239,7 +238,6 @@
softWrap = softWrap,
fontFamilyResolver = fontFamilyResolver,
overflow = overflow,
- minLines = minLines,
maxLines = maxLines,
placeholders = placeholders,
)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/InternalFoundationTextApi.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/InternalFoundationTextApi.kt
index f1ff38a..4221b97 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/InternalFoundationTextApi.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/InternalFoundationTextApi.kt
@@ -24,4 +24,5 @@
AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY,
AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER
)
+@Retention(AnnotationRetention.BINARY)
annotation class InternalFoundationTextApi
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt
index 6d663a8..48eb943 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt
@@ -19,7 +19,6 @@
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
-import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -52,9 +51,6 @@
import androidx.compose.ui.util.fastAll
import java.awt.event.KeyEvent.VK_ENTER
-@Composable
-internal actual fun isComposeRootInScrollableContainer(): () -> Boolean = { false }
-
// TODO: b/168524931 - should this depend on the input device?
internal actual val TapIndicationDelay: Long = 0L
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index 858095c..cfae285 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -369,10 +369,10 @@
@androidx.compose.runtime.Stable public final class DrawerState {
ctor public DrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
- method @androidx.compose.material.ExperimentalMaterialApi public suspend Object? animateTo(androidx.compose.material.DrawerValue targetValue, androidx.compose.animation.core.AnimationSpec<java.lang.Float> anim, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method @Deprecated @androidx.compose.material.ExperimentalMaterialApi public suspend Object? animateTo(androidx.compose.material.DrawerValue targetValue, androidx.compose.animation.core.AnimationSpec<java.lang.Float> anim, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public androidx.compose.material.DrawerValue getCurrentValue();
- method @androidx.compose.material.ExperimentalMaterialApi public androidx.compose.runtime.State<java.lang.Float> getOffset();
+ method @androidx.compose.material.ExperimentalMaterialApi public Float? getOffset();
method @androidx.compose.material.ExperimentalMaterialApi public androidx.compose.material.DrawerValue getTargetValue();
method public boolean isAnimationRunning();
method public boolean isClosed();
@@ -383,7 +383,7 @@
property public final boolean isAnimationRunning;
property public final boolean isClosed;
property public final boolean isOpen;
- property @androidx.compose.material.ExperimentalMaterialApi public final androidx.compose.runtime.State<java.lang.Float> offset;
+ property @androidx.compose.material.ExperimentalMaterialApi public final Float? offset;
property @androidx.compose.material.ExperimentalMaterialApi public final androidx.compose.material.DrawerValue targetValue;
field public static final androidx.compose.material.DrawerState.Companion Companion;
}
@@ -413,7 +413,7 @@
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material.ElevationOverlay> LocalElevationOverlay;
}
- @kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalMaterialApi {
+ @kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterialApi {
}
@androidx.compose.material.ExperimentalMaterialApi public interface ExposedDropdownMenuBoxScope {
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
index cd1e8a0..66665e6 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
@@ -17,7 +17,6 @@
package androidx.compose.material
import android.os.SystemClock.sleep
-import androidx.compose.animation.core.TweenSpec
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@@ -27,6 +26,8 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.canScroll
+import androidx.compose.ui.input.consumeScrollContainerInfo
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
@@ -319,12 +320,12 @@
rule.onNodeWithTag("drawer").assertLeftPositionInRootIsEqualTo(-width)
// When the drawer state is set to Opened
- drawerState.animateTo(DrawerValue.Open, TweenSpec())
+ drawerState.open()
// Then the drawer should be opened
rule.onNodeWithTag("drawer").assertLeftPositionInRootIsEqualTo(0.dp)
// When the drawer state is set to Closed
- drawerState.animateTo(DrawerValue.Closed, TweenSpec())
+ drawerState.close()
// Then the drawer should be closed
rule.onNodeWithTag("drawer").assertLeftPositionInRootIsEqualTo(-width)
}
@@ -1186,4 +1187,74 @@
topNode = rule.onNodeWithTag(topTag).fetchSemanticsNode()
assertEquals(2, topNode.children.size)
}
+
+ @Test
+ fun modalDrawer_providesScrollableContainerInfo_enabled() {
+ var actualValue = { false }
+ rule.setMaterialContent {
+ ModalDrawer(
+ drawerContent = {},
+ content = {
+ Box(Modifier.consumeScrollContainerInfo {
+ actualValue = { it!!.canScroll() }
+ })
+ }
+ )
+ }
+
+ assertThat(actualValue()).isTrue()
+ }
+
+ @Test
+ fun modalDrawer_providesScrollableContainerInfo_disabled() {
+ var actualValue = { false }
+ rule.setMaterialContent {
+ ModalDrawer(
+ drawerContent = {},
+ gesturesEnabled = false,
+ content = {
+ Box(Modifier.consumeScrollContainerInfo {
+ actualValue = { it!!.canScroll() }
+ })
+ }
+ )
+ }
+
+ assertThat(actualValue()).isFalse()
+ }
+
+ @Test
+ fun bottomDrawer_providesScrollableContainerInfo_enabled() {
+ var actualValue = { false }
+ rule.setMaterialContent {
+ BottomDrawer(
+ drawerContent = {},
+ content = {
+ Box(Modifier.consumeScrollContainerInfo {
+ actualValue = { it!!.canScroll() }
+ })
+ }
+ )
+ }
+
+ assertThat(actualValue()).isTrue()
+ }
+
+ @Test
+ fun bottomDrawer_providesScrollableContainerInfo_disabled() {
+ var actualValue = { false }
+ rule.setMaterialContent {
+ BottomDrawer(
+ drawerContent = {},
+ gesturesEnabled = false,
+ content = {
+ Box(Modifier.consumeScrollContainerInfo {
+ actualValue = { it!!.canScroll() }
+ })
+ }
+ )
+ }
+
+ assertThat(actualValue()).isFalse()
+ }
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
index 6d31620..8d311a0 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
@@ -25,6 +25,8 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.canScroll
+import androidx.compose.ui.input.consumeScrollContainerInfo
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.testTag
@@ -780,4 +782,44 @@
assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Expanded)
}
}
+
+ @Test
+ fun modalBottomSheet_providesScrollableContainerInfo_hidden() {
+ var actualValue = { false }
+ rule.setMaterialContent {
+ ModalBottomSheetLayout(
+ sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden),
+ content = { Box(Modifier.fillMaxSize().testTag(contentTag)) },
+ sheetContent = {
+ Box(
+ Modifier.fillMaxSize().consumeScrollContainerInfo {
+ actualValue = { it!!.canScroll() }
+ }
+ )
+ }
+ )
+ }
+
+ assertThat(actualValue()).isFalse()
+ }
+
+ @Test
+ fun modalBottomSheet_providesScrollableContainerInfo_expanded() {
+ var actualValue = { false }
+ rule.setMaterialContent {
+ ModalBottomSheetLayout(
+ sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Expanded),
+ content = { Box(Modifier.fillMaxSize().testTag(contentTag)) },
+ sheetContent = {
+ Box(
+ Modifier.fillMaxSize().consumeScrollContainerInfo {
+ actualValue = { it!!.canScroll() }
+ }
+ )
+ }
+ )
+ }
+
+ assertThat(actualValue()).isTrue()
+ }
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt
index a748549..303ed51 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt
@@ -350,9 +350,11 @@
) = SwipeableV2State(
initialValue = initialState,
positionalThreshold = positionalThreshold,
- velocityThreshold = velocityThreshold,
- density = density
- ).apply { if (anchors != null) updateAnchors(anchors) }
+ velocityThreshold = velocityThreshold
+ ).apply {
+ if (anchors != null) updateAnchors(anchors)
+ this.density = density
+ }
private fun TouchInjectionScope.endEdge(orientation: Orientation) =
if (orientation == Orientation.Horizontal) right else bottom
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
index 4099380..12f80a5 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
@@ -32,7 +32,6 @@
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
-import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -44,8 +43,10 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.isSpecified
+import androidx.compose.ui.input.ScrollContainerInfo
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.provideScrollContainerInfo
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
@@ -58,10 +59,10 @@
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.launch
import kotlin.math.max
import kotlin.math.roundToInt
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.launch
/**
* Possible values of [DrawerState].
@@ -113,10 +114,11 @@
confirmStateChange: (DrawerValue) -> Boolean = { true }
) {
- internal val swipeableState = SwipeableState(
+ internal val swipeableState = SwipeableV2State(
initialValue = initialValue,
animationSpec = AnimationSpec,
- confirmStateChange = confirmStateChange
+ confirmValueChange = confirmStateChange,
+ velocityThreshold = DrawerVelocityThreshold
)
/**
@@ -158,7 +160,7 @@
*
* @return the reason the open animation ended
*/
- suspend fun open() = animateTo(DrawerValue.Open, AnimationSpec)
+ suspend fun open() = swipeableState.animateTo(DrawerValue.Open)
/**
* Close the drawer with animation and suspend until it if fully closed or animation has been
@@ -167,17 +169,25 @@
*
* @return the reason the close animation ended
*/
- suspend fun close() = animateTo(DrawerValue.Closed, AnimationSpec)
+ suspend fun close() = swipeableState.animateTo(DrawerValue.Closed)
/**
* Set the state of the drawer with specific animation
*
* @param targetValue The new value to animate to.
- * @param anim The animation that will be used to animate to the new value.
+ * @param anim Set the state of the drawer with specific animation
*/
@ExperimentalMaterialApi
- suspend fun animateTo(targetValue: DrawerValue, anim: AnimationSpec<Float>) {
- swipeableState.animateTo(targetValue, anim)
+ @Deprecated(
+ message = "This method has been replaced by the open and close methods. The animation " +
+ "spec is now an implementation detail of ModalDrawer.",
+ level = DeprecationLevel.ERROR
+ )
+ suspend fun animateTo(
+ targetValue: DrawerValue,
+ @Suppress("UNUSED_PARAMETER") anim: AnimationSpec<Float>
+ ) {
+ swipeableState.animateTo(targetValue)
}
/**
@@ -204,14 +214,19 @@
get() = swipeableState.targetValue
/**
- * The current position (in pixels) of the drawer sheet.
+ * The current position (in pixels) of the drawer sheet, or null before the offset is
+ * initialized.
+ * @see [SwipeableV2State.offset] for more information.
*/
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:Suppress("AutoBoxing")
@ExperimentalMaterialApi
@get:ExperimentalMaterialApi
- val offset: State<Float>
+ val offset: Float?
get() = swipeableState.offset
+ internal fun requireOffset(): Float = swipeableState.requireOffset()
+
companion object {
/**
* The default [Saver] implementation for [DrawerState].
@@ -384,6 +399,14 @@
content: @Composable () -> Unit
) {
val scope = rememberCoroutineScope()
+
+ val containerInfo = remember(gesturesEnabled) {
+ object : ScrollContainerInfo {
+ override fun canScrollHorizontally() = gesturesEnabled
+
+ override fun canScrollVertically() = false
+ }
+ }
BoxWithConstraints(modifier.fillMaxSize()) {
val modalDrawerConstraints = constraints
// TODO : think about Infinite max bounds case
@@ -394,19 +417,25 @@
val minValue = -modalDrawerConstraints.maxWidth.toFloat()
val maxValue = 0f
- val anchors = mapOf(minValue to DrawerValue.Closed, maxValue to DrawerValue.Open)
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
Box(
- Modifier.swipeable(
- state = drawerState.swipeableState,
- anchors = anchors,
- thresholds = { _, _ -> FractionalThreshold(0.5f) },
- orientation = Orientation.Horizontal,
- enabled = gesturesEnabled,
- reverseDirection = isRtl,
- velocityThreshold = DrawerVelocityThreshold,
- resistance = null
- )
+ Modifier
+ .swipeableV2(
+ state = drawerState.swipeableState,
+ orientation = Orientation.Horizontal,
+ enabled = gesturesEnabled,
+ reverseDirection = isRtl
+ )
+ .swipeAnchors(
+ drawerState.swipeableState,
+ possibleValues = setOf(DrawerValue.Closed, DrawerValue.Open)
+ ) { value, _ ->
+ when (value) {
+ DrawerValue.Closed -> minValue
+ DrawerValue.Open -> maxValue
+ }
+ }
+ .provideScrollContainerInfo(containerInfo)
) {
Box {
content()
@@ -416,13 +445,13 @@
onClose = {
if (
gesturesEnabled &&
- drawerState.swipeableState.confirmStateChange(DrawerValue.Closed)
+ drawerState.swipeableState.confirmValueChange(DrawerValue.Closed)
) {
scope.launch { drawerState.close() }
}
},
fraction = {
- calculateFraction(minValue, maxValue, drawerState.offset.value)
+ calculateFraction(minValue, maxValue, drawerState.requireOffset())
},
color = scrimColor
)
@@ -437,7 +466,13 @@
maxHeight = modalDrawerConstraints.maxHeight.toDp()
)
}
- .offset { IntOffset(drawerState.offset.value.roundToInt(), 0) }
+ .offset {
+ IntOffset(
+ drawerState
+ .requireOffset()
+ .roundToInt(), 0
+ )
+ }
.padding(end = EndDrawerPadding)
.semantics {
paneTitle = navigationMenu
@@ -445,7 +480,7 @@
dismiss {
if (
drawerState.swipeableState
- .confirmStateChange(DrawerValue.Closed)
+ .confirmValueChange(DrawerValue.Closed)
) {
scope.launch { drawerState.close() }
}; true
@@ -541,6 +576,15 @@
} else {
Modifier
}
+
+ val containerInfo = remember(gesturesEnabled) {
+ object : ScrollContainerInfo {
+ override fun canScrollHorizontally() = gesturesEnabled
+
+ override fun canScrollVertically() = false
+ }
+ }
+
val swipeable = Modifier
.then(nestedScroll)
.swipeable(
@@ -550,6 +594,7 @@
enabled = gesturesEnabled,
resistance = null
)
+ .provideScrollContainerInfo(containerInfo)
Box(swipeable) {
content()
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ExperimentalMaterialApi.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ExperimentalMaterialApi.kt
index 553aafd..d0f78a0 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ExperimentalMaterialApi.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ExperimentalMaterialApi.kt
@@ -20,4 +20,5 @@
"This material API is experimental and is likely to change or to be removed in" +
" the future."
)
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalMaterialApi
\ No newline at end of file
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
index 3ba169b..919da41 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
@@ -44,8 +44,10 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.isSpecified
+import androidx.compose.ui.input.ScrollContainerInfo
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.provideScrollContainerInfo
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.semantics.collapse
import androidx.compose.ui.semantics.contentDescription
@@ -338,6 +340,15 @@
visible = sheetState.targetValue != Hidden
)
}
+
+ val containerInfo = remember(sheetState) {
+ object : ScrollContainerInfo {
+ override fun canScrollHorizontally() = false
+
+ override fun canScrollVertically() = sheetState.currentValue != Hidden
+ }
+ }
+
Surface(
Modifier
.fillMaxWidth()
@@ -353,6 +364,7 @@
IntOffset(0, y)
}
.bottomSheetSwipeable(sheetState, fullHeight, sheetHeightState)
+ .provideScrollContainerInfo(containerInfo)
.onGloballyPositioned {
sheetHeightState.value = it.size.height.toFloat()
}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
index 70c2766..9ee4ffb 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
@@ -33,8 +33,15 @@
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.OnRemeasuredModifier
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
@@ -98,22 +105,32 @@
possibleValues: Set<T>,
anchorsChanged: ((oldAnchors: Map<T, Float>, newAnchors: Map<T, Float>) -> Unit)? = null,
calculateAnchor: (value: T, layoutSize: IntSize) -> Float?,
-) = onSizeChanged { layoutSize ->
- val previousAnchors = state.anchors
- val newAnchors = mutableMapOf<T, Float>()
- possibleValues.forEach {
- val anchorValue = calculateAnchor(it, layoutSize)
- if (anchorValue != null) {
- newAnchors[it] = anchorValue
+) = this.then(SwipeAnchorsModifier(
+ onDensityChanged = { state.density = it },
+ onSizeChanged = { layoutSize ->
+ val previousAnchors = state.anchors
+ val newAnchors = mutableMapOf<T, Float>()
+ possibleValues.forEach {
+ val anchorValue = calculateAnchor(it, layoutSize)
+ if (anchorValue != null) {
+ newAnchors[it] = anchorValue
+ }
}
+ if (previousAnchors != newAnchors) {
+ state.updateAnchors(newAnchors)
+ if (previousAnchors.isNotEmpty()) {
+ anchorsChanged?.invoke(previousAnchors, newAnchors)
+ }
+ }
+ },
+ inspectorInfo = debugInspectorInfo {
+ name = "swipeAnchors"
+ properties["state"] = state
+ properties["possibleValues"] = possibleValues
+ properties["anchorsChanged"] = anchorsChanged
+ properties["calculateAnchor"] = calculateAnchor
}
- if (previousAnchors == newAnchors) return@onSizeChanged
- state.updateAnchors(newAnchors)
-
- if (previousAnchors.isNotEmpty()) {
- anchorsChanged?.invoke(previousAnchors, newAnchors)
- }
-}
+))
/**
* State of the [swipeableV2] modifier.
@@ -123,7 +140,6 @@
* [SwipeableV2State] use [rememberSwipeableV2State].
*
* @param initialValue The initial value of the state.
- * @param density The density used to convert thresholds from px to dp.
* @param animationSpec The default animation that will be used to animate to a new state.
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
* @param positionalThreshold The positional threshold to be used when calculating the target state
@@ -139,7 +155,6 @@
@ExperimentalMaterialApi
internal class SwipeableV2State<T>(
initialValue: T,
- internal val density: Density,
internal val animationSpec: AnimationSpec<Float> = SwipeableV2Defaults.AnimationSpec,
internal val confirmValueChange: (newValue: T) -> Boolean = { true },
internal val positionalThreshold: Density.(totalDistance: Float) -> Float =
@@ -178,6 +193,7 @@
*
* To guarantee stricter semantics, consider using [requireOffset].
*/
+ @get:Suppress("AutoBoxing")
val offset: Float? by derivedStateOf {
dragPosition?.coerceIn(minBound, maxBound)
}
@@ -228,14 +244,14 @@
private val minBound by derivedStateOf { anchors.minOrNull() ?: Float.NEGATIVE_INFINITY }
private val maxBound by derivedStateOf { anchors.maxOrNull() ?: Float.POSITIVE_INFINITY }
- private val velocityThresholdPx = with(density) { velocityThreshold.toPx() }
-
internal val draggableState = DraggableState {
dragPosition = (dragPosition ?: 0f) + it
}
internal var anchors by mutableStateOf(emptyMap<T, Float>())
+ internal var density: Density? = null
+
internal fun updateAnchors(newAnchors: Map<T, Float>) {
val previousAnchorsEmpty = anchors.isEmpty()
anchors = newAnchors
@@ -348,6 +364,8 @@
): T {
val currentAnchors = anchors
val currentAnchor = currentAnchors.requireAnchor(currentValue)
+ val currentDensity = requireDensity()
+ val velocityThresholdPx = with(currentDensity) { velocityThreshold.toPx() }
return if (currentAnchor <= offset) {
// Swiping from lower to upper (positive).
if (velocity >= velocityThresholdPx) {
@@ -355,7 +373,7 @@
} else {
val upper = currentAnchors.closestAnchor(offset, true)
val distance = abs(currentAnchors.getValue(upper) - currentAnchor)
- val relativeThreshold = abs(positionalThreshold(density, distance))
+ val relativeThreshold = abs(positionalThreshold(currentDensity, distance))
val absoluteThreshold = abs(currentAnchor + relativeThreshold)
if (offset < absoluteThreshold) currentValue else upper
}
@@ -366,7 +384,7 @@
} else {
val lower = currentAnchors.closestAnchor(offset, false)
val distance = abs(currentAnchor - currentAnchors.getValue(lower))
- val relativeThreshold = abs(positionalThreshold(density, distance))
+ val relativeThreshold = abs(positionalThreshold(currentDensity, distance))
val absoluteThreshold = abs(currentAnchor - relativeThreshold)
if (offset < 0) {
// For negative offsets, larger absolute thresholds are closer to lower anchors
@@ -379,6 +397,11 @@
}
}
+ private fun requireDensity() = requireNotNull(density) {
+ "SwipeableState did not have a density attached. Are you using Modifier.swipeable with " +
+ "this=$this SwipeableState?"
+ }
+
companion object {
/**
* The default [Saver] implementation for [SwipeableV2State].
@@ -388,8 +411,7 @@
animationSpec: AnimationSpec<Float>,
confirmValueChange: (T) -> Boolean,
positionalThreshold: Density.(distance: Float) -> Float,
- velocityThreshold: Dp,
- density: Density
+ velocityThreshold: Dp
) = Saver<SwipeableV2State<T>, T>(
save = { it.currentValue },
restore = {
@@ -398,8 +420,7 @@
animationSpec = animationSpec,
confirmValueChange = confirmValueChange,
positionalThreshold = positionalThreshold,
- velocityThreshold = velocityThreshold,
- density = density
+ velocityThreshold = velocityThreshold
)
}
)
@@ -420,15 +441,13 @@
animationSpec: AnimationSpec<Float> = SwipeableV2Defaults.AnimationSpec,
confirmValueChange: (newValue: T) -> Boolean = { true }
): SwipeableV2State<T> {
- val density = LocalDensity.current
return rememberSaveable(
- initialValue, animationSpec, confirmValueChange, density,
+ initialValue, animationSpec, confirmValueChange,
saver = SwipeableV2State.Saver(
animationSpec = animationSpec,
confirmValueChange = confirmValueChange,
positionalThreshold = SwipeableV2Defaults.PositionalThreshold,
- velocityThreshold = SwipeableV2Defaults.VelocityThreshold,
- density = density
+ velocityThreshold = SwipeableV2Defaults.VelocityThreshold
),
) {
SwipeableV2State(
@@ -436,8 +455,7 @@
animationSpec = animationSpec,
confirmValueChange = confirmValueChange,
positionalThreshold = SwipeableV2Defaults.PositionalThreshold,
- velocityThreshold = SwipeableV2Defaults.VelocityThreshold,
- density = density
+ velocityThreshold = SwipeableV2Defaults.VelocityThreshold
)
}
}
@@ -491,6 +509,37 @@
fixedPositionalThreshold(56.dp)
}
+@Stable
+private class SwipeAnchorsModifier(
+ private val onDensityChanged: (density: Density) -> Unit,
+ private val onSizeChanged: (layoutSize: IntSize) -> Unit,
+ inspectorInfo: InspectorInfo.() -> Unit,
+) : LayoutModifier, OnRemeasuredModifier, InspectorValueInfo(inspectorInfo) {
+
+ private var lastDensity: Float = -1f
+ private var lastFontScale: Float = -1f
+
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ if (density != lastDensity || fontScale != lastFontScale) {
+ onDensityChanged(Density(density, fontScale))
+ lastDensity = density
+ lastFontScale = fontScale
+ }
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.width, placeable.height) { placeable.place(0, 0) }
+ }
+
+ override fun onRemeasured(size: IntSize) {
+ onSizeChanged(size)
+ }
+
+ override fun toString() = "SwipeAnchorsModifierImpl(updateDensity=$onDensityChanged, " +
+ "onSizeChanged=$onSizeChanged)"
+}
+
private fun <T> Map<T, Float>.closestAnchor(
offset: Float = 0f,
searchUpwards: Boolean = false
diff --git a/compose/material3/material3-window-size-class/api/public_plus_experimental_current.txt b/compose/material3/material3-window-size-class/api/public_plus_experimental_current.txt
index e1590d8..81a9075 100644
--- a/compose/material3/material3-window-size-class/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3-window-size-class/api/public_plus_experimental_current.txt
@@ -5,7 +5,7 @@
method @androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi @androidx.compose.runtime.Composable public static androidx.compose.material3.windowsizeclass.WindowSizeClass calculateWindowSizeClass(android.app.Activity activity);
}
- @kotlin.RequiresOptIn(message="This material3-window-size-class API is experimental and is likely to change or to " + "be removed in the future.") public @interface ExperimentalMaterial3WindowSizeClassApi {
+ @kotlin.RequiresOptIn(message="This material3-window-size-class API is experimental and is likely to change or to " + "be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterial3WindowSizeClassApi {
}
public final class TestOnly_jvmKt {
diff --git a/compose/material3/material3-window-size-class/src/commonMain/kotlin/androidx/compose/material3/windowsizeclass/ExperimentalMaterial3WindowSizeClassApi.kt b/compose/material3/material3-window-size-class/src/commonMain/kotlin/androidx/compose/material3/windowsizeclass/ExperimentalMaterial3WindowSizeClassApi.kt
index 0c19899..8f405e8 100644
--- a/compose/material3/material3-window-size-class/src/commonMain/kotlin/androidx/compose/material3/windowsizeclass/ExperimentalMaterial3WindowSizeClassApi.kt
+++ b/compose/material3/material3-window-size-class/src/commonMain/kotlin/androidx/compose/material3/windowsizeclass/ExperimentalMaterial3WindowSizeClassApi.kt
@@ -20,4 +20,5 @@
"This material3-window-size-class API is experimental and is likely to change or to " +
"be removed in the future."
)
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalMaterial3WindowSizeClassApi
\ No newline at end of file
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index c815b0b..d92a1b2 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -350,7 +350,7 @@
public final class ElevationKt {
}
- @kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalMaterial3Api {
+ @kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterial3Api {
}
@androidx.compose.material3.ExperimentalMaterial3Api public interface ExposedDropdownMenuBoxScope {
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt
index 2edb2e5..6deaf4d 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt
@@ -26,6 +26,8 @@
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.canScroll
+import androidx.compose.ui.input.consumeScrollContainerInfo
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
@@ -558,6 +560,44 @@
.onParent()
.assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Dismiss))
}
+
+ @Test
+ fun dismissibleNavigationDrawer_providesScrollableContainerInfo_enabled() {
+ var actualValue = { false }
+ rule.setMaterialContent(lightColorScheme()) {
+
+ DismissibleNavigationDrawer(
+ gesturesEnabled = true,
+ drawerContent = {},
+ content = {
+ Box(Modifier.consumeScrollContainerInfo {
+ actualValue = { it!!.canScroll() }
+ })
+ }
+ )
+ }
+
+ assertThat(actualValue()).isTrue()
+ }
+
+ @Test
+ fun dismissibleNavigationDrawer_providesScrollableContainerInfo_disabled() {
+ var actualValue = { false }
+ rule.setMaterialContent(lightColorScheme()) {
+
+ DismissibleNavigationDrawer(
+ gesturesEnabled = false,
+ drawerContent = {},
+ content = {
+ Box(Modifier.consumeScrollContainerInfo {
+ actualValue = { it!!.canScroll() }
+ })
+ }
+ )
+ }
+
+ assertThat(actualValue()).isFalse()
+ }
}
private val DrawerTestTag = "drawer"
\ No newline at end of file
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt
index 409120f..f18376cc 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt
@@ -26,6 +26,8 @@
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.canScroll
+import androidx.compose.ui.input.consumeScrollContainerInfo
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
@@ -656,6 +658,45 @@
topNode = rule.onNodeWithTag(topTag).fetchSemanticsNode()
assertEquals(2, topNode.children.size)
}
+
+ @Test
+ fun navigationDrawer_providesScrollableContainerInfo_enabled() {
+ var actualValue = { false }
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawer(
+ drawerContent = { ModalDrawerSheet { } },
+ content = {
+ Box(
+ Modifier.consumeScrollContainerInfo {
+ actualValue = { it!!.canScroll() }
+ }
+ )
+ }
+ )
+ }
+
+ assertThat(actualValue()).isTrue()
+ }
+
+ @Test
+ fun navigationDrawer_providesScrollableContainerInfo_disabled() {
+ var actualValue = { false }
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawer(
+ gesturesEnabled = false,
+ drawerContent = { ModalDrawerSheet { } },
+ content = {
+ Box(
+ Modifier.consumeScrollContainerInfo {
+ actualValue = { it!!.canScroll() }
+ }
+ )
+ }
+ )
+ }
+
+ assertThat(actualValue()).isFalse()
+ }
}
private val DrawerTestTag = "drawer"
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ExperimentalMaterial3Api.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ExperimentalMaterial3Api.kt
index 19f4e59..60263cf 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ExperimentalMaterial3Api.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ExperimentalMaterial3Api.kt
@@ -20,4 +20,5 @@
"This material API is experimental and is likely to change or to be removed in" +
" the future."
)
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalMaterial3Api
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
index ae8de70..38b30e4 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
@@ -57,7 +57,9 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.input.ScrollContainerInfo
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.provideScrollContainerInfo
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
@@ -268,6 +270,15 @@
val anchors = mapOf(minValue to DrawerValue.Closed, maxValue to DrawerValue.Open)
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+
+ val containerInfo = remember(gesturesEnabled) {
+ object : ScrollContainerInfo {
+ override fun canScrollHorizontally() = gesturesEnabled
+
+ override fun canScrollVertically() = false
+ }
+ }
+
Box(
modifier
.fillMaxSize()
@@ -281,6 +292,7 @@
velocityThreshold = DrawerVelocityThreshold,
resistance = null
)
+ .provideScrollContainerInfo(containerInfo)
) {
Box {
content()
@@ -361,6 +373,14 @@
val anchors = mapOf(minValue to DrawerValue.Closed, maxValue to DrawerValue.Open)
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+ val containerInfo = remember(gesturesEnabled) {
+ object : ScrollContainerInfo {
+ override fun canScrollHorizontally() = gesturesEnabled
+
+ override fun canScrollVertically() = false
+ }
+ }
+
Box(
modifier.swipeable(
state = drawerState.swipeableState,
@@ -371,7 +391,7 @@
reverseDirection = isRtl,
velocityThreshold = DrawerVelocityThreshold,
resistance = null
- )
+ ).provideScrollContainerInfo(containerInfo)
) {
Layout(content = {
Box(Modifier.semantics {
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt
index cd738fe..1ed4cd9 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt
@@ -107,7 +107,7 @@
if (name != "content" && parameterInfo.functionType.parameters.isEmpty()) {
context.report(
ComposableLambdaParameterNaming,
- node,
+ uElement,
context.getNameLocation(uElement),
"Composable lambda parameter should be named `content`",
LintFix.create()
@@ -123,7 +123,7 @@
if (parameter !== node.uastParameters.last()) {
context.report(
ComposableLambdaParameterPosition,
- node,
+ uElement,
context.getNameLocation(uElement),
"Composable lambda parameter should be the last parameter so it can be used " +
"as a trailing lambda"
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetectorTest.kt
index 4ace0ae..463830d 100644
--- a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetectorTest.kt
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetectorTest.kt
@@ -59,7 +59,6 @@
Stubs.Composable
)
.skipTestModes(TestMode.TYPE_ALIAS)
- .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293458
.run()
.expect(
"""
@@ -97,7 +96,6 @@
Stubs.Composable
)
.skipTestModes(TestMode.TYPE_ALIAS)
- .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293458
.run()
.expect(
"""
@@ -127,7 +125,6 @@
Stubs.Composable
)
.skipTestModes(TestMode.TYPE_ALIAS)
- .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293458
.run()
.expect(
"""
@@ -169,7 +166,6 @@
),
Stubs.Composable
)
- .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293458
.run()
.expect(
"""
@@ -235,7 +231,6 @@
Stubs.Composable
)
.skipTestModes(TestMode.TYPE_ALIAS)
- .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293458
.run()
.expect(
"""
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index e75d073..82341f0 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -289,7 +289,7 @@
public final class ExpectKt {
}
- @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is an experimental API for Compose and is likely to change before becoming " + "stable.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface ExperimentalComposeApi {
+ @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is an experimental API for Compose and is likely to change before becoming " + "stable.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface ExperimentalComposeApi {
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface ExplicitGroupsComposable {
@@ -298,10 +298,10 @@
@androidx.compose.runtime.StableMarker @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface Immutable {
}
- @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API for Compose modules that may change frequently " + "and without warning.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface InternalComposeApi {
+ @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API for Compose modules that may change frequently " + "and without warning.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface InternalComposeApi {
}
- @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API that may change frequently and without warning.") public @interface InternalComposeTracingApi {
+ @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API that may change frequently and without warning.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface InternalComposeTracingApi {
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface MonotonicFrameClock extends kotlin.coroutines.CoroutineContext.Element {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ExperimentalComposeApi.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ExperimentalComposeApi.kt
index 21db3b7..1bfbd64 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ExperimentalComposeApi.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ExperimentalComposeApi.kt
@@ -27,4 +27,5 @@
AnnotationTarget.PROPERTY,
AnnotationTarget.PROPERTY_GETTER
)
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalComposeApi
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/InternalComposeApi.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/InternalComposeApi.kt
index 809a56d..c964187 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/InternalComposeApi.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/InternalComposeApi.kt
@@ -26,4 +26,5 @@
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY
)
+@Retention(AnnotationRetention.BINARY)
annotation class InternalComposeApi
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/InternalComposeTracingApi.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/InternalComposeTracingApi.kt
index 943df3a..b2e5633 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/InternalComposeTracingApi.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/InternalComposeTracingApi.kt
@@ -20,4 +20,5 @@
level = RequiresOptIn.Level.ERROR,
message = "This is internal API that may change frequently and without warning."
)
+@Retention(AnnotationRetention.BINARY)
annotation class InternalComposeTracingApi
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
index 5c32e29..285838a 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -54,7 +54,6 @@
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
-import org.junit.Ignore
@Composable
fun Container(content: @Composable () -> Unit) = content()
@@ -3235,7 +3234,6 @@
}
@Test
- @Ignore("b/255722247")
fun testNonLocalReturn_CM1_RetFunc_FalseTrue() = compositionTest {
var condition by mutableStateOf(false)
@@ -3255,7 +3253,6 @@
}
@Test
- @Ignore("b/255722247")
fun testNonLocalReturn_CM1_RetFunc_TrueFalse() = compositionTest {
var condition by mutableStateOf(true)
@@ -3275,7 +3272,6 @@
}
@Test
- @Ignore("b/255722247")
fun test_CM1_CCM1_RetFun_FalseTrue() = compositionTest {
var condition by mutableStateOf(false)
@@ -3295,7 +3291,6 @@
}
@Test
- @Ignore("b/255722247")
fun test_CM1_CCM1_RetFun_TrueFalse() = compositionTest {
var condition by mutableStateOf(true)
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index 47ef466..99ccb0c 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -43,7 +43,7 @@
implementation(projectOrArtifact(":compose:runtime:runtime"))
implementation(projectOrArtifact(":compose:ui:ui-unit"))
implementation(projectOrArtifact(":compose:ui:ui-graphics"))
- implementation(projectOrArtifact(":activity:activity-compose"))
+ implementation("androidx.activity:activity-compose:1.3.1")
// old version of common-java8 conflicts with newer version, because both have
// DefaultLifecycleEventObserver.
// Outside of androidx this is resolved via constraint added to lifecycle-common,
diff --git a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
index 08e2ebc..9b18442 100644
--- a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
@@ -399,7 +399,7 @@
public final class DegreesKt {
}
- @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalGraphicsApi {
+ @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalGraphicsApi {
}
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class FilterQuality {
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ExperimentalGraphicsApi.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ExperimentalGraphicsApi.kt
index aa38621..909c69d 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ExperimentalGraphicsApi.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ExperimentalGraphicsApi.kt
@@ -17,4 +17,5 @@
package androidx.compose.ui.graphics
@RequiresOptIn("This API is experimental and is likely to change in the future.")
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalGraphicsApi
\ No newline at end of file
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt
index d786c2f..bcc5884 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt
@@ -75,7 +75,7 @@
if (modifierParameter.name != ModifierParameterName) {
context.report(
ModifierParameter,
- node,
+ modifierParameterElement,
context.getNameLocation(modifierParameterElement),
"$modifierName parameter should be named $ModifierParameterName",
LintFix.create()
@@ -91,7 +91,7 @@
if (modifierParameter.type.canonicalText != Names.Ui.Modifier.javaFqn) {
context.report(
ModifierParameter,
- node,
+ modifierParameterElement,
context.getNameLocation(modifierParameterElement),
"$modifierName parameter should have a type of $modifierName",
LintFix.create()
@@ -113,7 +113,7 @@
if (referenceExpression?.getReferencedName() != modifierName) {
context.report(
ModifierParameter,
- node,
+ modifierParameterElement,
context.getNameLocation(modifierParameterElement),
"Optional $modifierName parameter should have a default value " +
"of `$modifierName`",
@@ -134,7 +134,7 @@
if (index != optionalParameterIndex) {
context.report(
ModifierParameter,
- node,
+ modifierParameterElement,
context.getNameLocation(modifierParameterElement),
"$modifierName parameter should be the first optional parameter",
// Hard to make a lint fix for this and keep parameter formatting, so
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierParameterDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierParameterDetectorTest.kt
index 0c5b55e..f3aba8d 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierParameterDetectorTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierParameterDetectorTest.kt
@@ -20,7 +20,6 @@
import androidx.compose.lint.test.Stubs
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestMode
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test
@@ -63,7 +62,6 @@
Stubs.Composable,
Stubs.Modifier
)
- .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293766
.run()
.expect(
"""
@@ -105,7 +103,6 @@
Stubs.Composable,
Stubs.Modifier
)
- .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293766
.run()
.expect(
"""
@@ -149,7 +146,6 @@
Stubs.Composable,
Stubs.Modifier
)
- .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293766
.run()
.expect(
"""
@@ -191,7 +187,6 @@
Stubs.Composable,
Stubs.Modifier
)
- .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293766
.run()
.expect(
"""
@@ -227,7 +222,6 @@
Stubs.Composable,
Stubs.Modifier
)
- .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293766
.run()
.expect(
"""
diff --git a/compose/ui/ui-test/api/public_plus_experimental_current.txt b/compose/ui/ui-test/api/public_plus_experimental_current.txt
index f3aa23b..08d0066 100644
--- a/compose/ui/ui-test/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test/api/public_plus_experimental_current.txt
@@ -88,7 +88,7 @@
public final class Expect_jvmKt {
}
- @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTestApi {
+ @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTestApi {
}
public final class FiltersKt {
@@ -245,7 +245,7 @@
property public default int width;
}
- @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestApi {
+ @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface InternalTestApi {
}
@androidx.compose.ui.test.ExperimentalTestApi @kotlin.jvm.JvmDefaultWithCompatibility public interface KeyInjectionScope extends androidx.compose.ui.test.InjectionScope {
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTestApi.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTestApi.kt
index 1b8cad3..561c112 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTestApi.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTestApi.kt
@@ -17,9 +17,11 @@
package androidx.compose.ui.test
@RequiresOptIn("This testing API is experimental and is likely to be changed or removed entirely")
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalTestApi
@RequiresOptIn(
"This is internal API for Compose modules that may change frequently and without warning."
)
+@Retention(AnnotationRetention.BINARY)
annotation class InternalTestApi
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index ed86b4a..226cedf 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -93,10 +93,10 @@
method public static inline <R> R withStyle(androidx.compose.ui.text.AnnotatedString.Builder, androidx.compose.ui.text.ParagraphStyle style, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,? extends R> block);
}
- @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalTextApi {
+ @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTextApi {
}
- @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API that may change frequently and without warning.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface InternalTextApi {
+ @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API that may change frequently and without warning.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface InternalTextApi {
}
public final class JvmAnnotatedString_jvmKt {
@@ -630,7 +630,7 @@
package androidx.compose.ui.text.android {
- @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API that may change frequently and without warning.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface InternalPlatformTextApi {
+ @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API that may change frequently and without warning.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface InternalPlatformTextApi {
}
public final class LayoutCompatKt {
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ExperimentalTextApi.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ExperimentalTextApi.kt
index 0c52428..b67456b 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ExperimentalTextApi.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ExperimentalTextApi.kt
@@ -17,4 +17,5 @@
package androidx.compose.ui.text
@RequiresOptIn("This API is experimental and is likely to change in the future.")
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalTextApi
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/InternalTextApi.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/InternalTextApi.kt
index e0b91a4..710ac39 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/InternalTextApi.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/InternalTextApi.kt
@@ -25,4 +25,5 @@
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY
)
+@Retention(AnnotationRetention.BINARY)
annotation class InternalTextApi
\ No newline at end of file
diff --git a/compose/ui/ui-tooling-data/api/public_plus_experimental_current.txt b/compose/ui/ui-tooling-data/api/public_plus_experimental_current.txt
index 092a10a..ffae455 100644
--- a/compose/ui/ui-tooling-data/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-tooling-data/api/public_plus_experimental_current.txt
@@ -116,7 +116,7 @@
property public final String? sourceFile;
}
- @kotlin.RequiresOptIn(message="This API is for tooling only and is likely to change in the future.") public @interface UiToolingDataApi {
+ @kotlin.RequiresOptIn(message="This API is for tooling only and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface UiToolingDataApi {
}
}
diff --git a/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/UiToolingDataApi.kt b/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/UiToolingDataApi.kt
index 45578f6..15b71a6 100644
--- a/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/UiToolingDataApi.kt
+++ b/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/UiToolingDataApi.kt
@@ -17,4 +17,5 @@
package androidx.compose.ui.tooling.data
@RequiresOptIn("This API is for tooling only and is likely to change in the future.")
+@Retention(AnnotationRetention.BINARY)
annotation class UiToolingDataApi
\ No newline at end of file
diff --git a/compose/ui/ui-unit/api/public_plus_experimental_current.txt b/compose/ui/ui-unit/api/public_plus_experimental_current.txt
index 35ad3e1..a529f45 100644
--- a/compose/ui/ui-unit/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-unit/api/public_plus_experimental_current.txt
@@ -194,7 +194,7 @@
property public final long Zero;
}
- @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalUnitApi {
+ @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalUnitApi {
}
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class IntOffset {
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/ExperimentalUnitApi.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/ExperimentalUnitApi.kt
index 84c3c67..3db6a8c 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/ExperimentalUnitApi.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/ExperimentalUnitApi.kt
@@ -17,4 +17,5 @@
package androidx.compose.ui.unit
@RequiresOptIn("This API is experimental and is likely to change in the future.")
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalUnitApi
\ No newline at end of file
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 732988f..3e2b1e6 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -828,6 +828,17 @@
property public abstract int inputMode;
}
+ public interface ScrollContainerInfo {
+ method public boolean canScrollHorizontally();
+ method public boolean canScrollVertically();
+ }
+
+ public final class ScrollContainerInfoKt {
+ method public static boolean canScroll(androidx.compose.ui.input.ScrollContainerInfo);
+ method public static androidx.compose.ui.Modifier consumeScrollContainerInfo(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.ScrollContainerInfo,kotlin.Unit> consumer);
+ method public static androidx.compose.ui.Modifier provideScrollContainerInfo(androidx.compose.ui.Modifier, androidx.compose.ui.input.ScrollContainerInfo scrollContainerInfo);
+ }
+
}
package androidx.compose.ui.input.key {
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 1d491d6..3c86dfd 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -123,10 +123,10 @@
method public static androidx.compose.ui.Modifier materialize(androidx.compose.runtime.Composer, androidx.compose.ui.Modifier modifier);
}
- @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalComposeUiApi {
+ @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalComposeUiApi {
}
- @kotlin.RequiresOptIn(message="Unstable API for use only between compose-ui modules sharing the same exact version, " + "subject to change without notice in major, minor, or patch releases.") public @interface InternalComposeUiApi {
+ @kotlin.RequiresOptIn(message="Unstable API for use only between compose-ui modules sharing the same exact version, " + "subject to change without notice in major, minor, or patch releases.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface InternalComposeUiApi {
}
@androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public interface Modifier {
@@ -954,6 +954,17 @@
property public abstract int inputMode;
}
+ public interface ScrollContainerInfo {
+ method public boolean canScrollHorizontally();
+ method public boolean canScrollVertically();
+ }
+
+ public final class ScrollContainerInfoKt {
+ method public static boolean canScroll(androidx.compose.ui.input.ScrollContainerInfo);
+ method public static androidx.compose.ui.Modifier consumeScrollContainerInfo(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.ScrollContainerInfo,kotlin.Unit> consumer);
+ method public static androidx.compose.ui.Modifier provideScrollContainerInfo(androidx.compose.ui.Modifier, androidx.compose.ui.input.ScrollContainerInfo scrollContainerInfo);
+ }
+
}
package androidx.compose.ui.input.key {
@@ -2474,7 +2485,7 @@
property public abstract long targetSize;
}
- @kotlin.RequiresOptIn(message="This API is internal to library.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InternalCoreApi {
+ @kotlin.RequiresOptIn(message="This API is internal to library.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InternalCoreApi {
}
@androidx.compose.ui.ExperimentalComposeUiApi public interface LayoutAwareModifierNode extends androidx.compose.ui.node.DelegatableNode {
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index db61359..87d6adf 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -828,6 +828,17 @@
property public abstract int inputMode;
}
+ public interface ScrollContainerInfo {
+ method public boolean canScrollHorizontally();
+ method public boolean canScrollVertically();
+ }
+
+ public final class ScrollContainerInfoKt {
+ method public static boolean canScroll(androidx.compose.ui.input.ScrollContainerInfo);
+ method public static androidx.compose.ui.Modifier consumeScrollContainerInfo(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.ScrollContainerInfo,kotlin.Unit> consumer);
+ method public static androidx.compose.ui.Modifier provideScrollContainerInfo(androidx.compose.ui.Modifier, androidx.compose.ui.input.ScrollContainerInfo scrollContainerInfo);
+ }
+
}
package androidx.compose.ui.input.key {
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index bd274e6..4944ff2 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -72,7 +72,7 @@
implementation(libs.kotlinCoroutinesAndroid)
implementation("androidx.activity:activity-ktx:1.5.1")
- implementation("androidx.core:core:1.5.0")
+ implementation("androidx.core:core:1.9.0")
implementation('androidx.collection:collection:1.0.0')
implementation("androidx.customview:customview-poolingcontainer:1.0.0")
implementation("androidx.savedstate:savedstate-ktx:1.2.0")
@@ -116,8 +116,8 @@
androidTestImplementation(project(":internal-testutils-runtime"))
androidTestImplementation(project(":test:screenshot:screenshot"))
androidTestImplementation("androidx.recyclerview:recyclerview:1.3.0-alpha02")
- androidTestImplementation("androidx.core:core-ktx:1.2.0")
- androidTestImplementation("androidx.activity:activity-compose:1.3.1")
+ androidTestImplementation("androidx.core:core-ktx:1.9.0")
+ androidTestImplementation("androidx.activity:activity-compose:1.5.1")
androidTestImplementation("androidx.appcompat:appcompat:1.3.0")
androidTestImplementation("androidx.fragment:fragment:1.3.0")
@@ -167,7 +167,7 @@
implementation(libs.kotlinCoroutinesAndroid)
implementation("androidx.activity:activity-ktx:1.5.1")
- implementation("androidx.core:core:1.5.0")
+ implementation("androidx.core:core:1.9.0")
implementation('androidx.collection:collection:1.0.0')
implementation("androidx.customview:customview-poolingcontainer:1.0.0")
implementation("androidx.savedstate:savedstate-ktx:1.2.0")
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ScrollableContainerSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ScrollableContainerSample.kt
new file mode 100644
index 0000000..7146d12
--- /dev/null
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ScrollableContainerSample.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+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.input.canScroll
+import androidx.compose.ui.input.consumeScrollContainerInfo
+import androidx.compose.ui.input.pointer.pointerInput
+import java.util.concurrent.TimeoutException
+import kotlinx.coroutines.withTimeout
+
+@Sampled
+@Composable
+fun ScrollableContainerSample() {
+ var isParentScrollable by remember { mutableStateOf({ false }) }
+
+ Column(Modifier.verticalScroll(rememberScrollState())) {
+
+ Box(modifier = Modifier.consumeScrollContainerInfo {
+ isParentScrollable = { it?.canScroll() == true }
+ }) {
+ Box(Modifier.pointerInput(Unit) {
+ detectTapGestures(
+ onPress = {
+ // If there is an ancestor that handles drag events, this press might
+ // become a drag so delay any work
+ val doWork = !isParentScrollable() || try {
+ withTimeout(100) { tryAwaitRelease() }
+ } catch (e: TimeoutException) {
+ true
+ }
+ if (doWork) println("Do work")
+ })
+ })
+ }
+ }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
index de8dd50..a7d7eba 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
@@ -26,6 +26,7 @@
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.GenericShape
import androidx.compose.runtime.Composable
@@ -91,15 +92,15 @@
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import kotlin.math.ceil
+import kotlin.math.roundToInt
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import kotlin.math.roundToInt
-import org.junit.Assert.assertNotNull
@MediumTest
@RunWith(AndroidJUnit4::class)
@@ -1349,4 +1350,37 @@
assertEquals(sizePx, drawScopeWidth)
assertEquals(sizePx, drawScopeHeight)
}
+ @RequiresApi(Build.VERSION_CODES.O)
+ @Test
+ fun removingGraphicsLayerInvalidatesParentLayer() {
+ var toggle by mutableStateOf(true)
+ val size = 100
+ rule.setContent {
+ val sizeDp = with(LocalDensity.current) { size.toDp() }
+ LazyColumn(Modifier.testTag("lazy").background(Color.Blue)) {
+ items(4) {
+ Box(
+ Modifier
+ .then(if (toggle) Modifier.graphicsLayer(alpha = 0f) else Modifier)
+ .background(Color.Red)
+ .size(sizeDp)
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag("lazy").captureToImage().asAndroidBitmap().apply {
+ assertEquals(Color.Blue.toArgb(), getPixel(10, (size * 1.5f).roundToInt()))
+ assertEquals(Color.Blue.toArgb(), getPixel(10, (size * 2.5f).roundToInt()))
+ }
+
+ rule.runOnIdle {
+ toggle = !toggle
+ }
+
+ rule.onNodeWithTag("lazy").captureToImage().asAndroidBitmap().apply {
+ assertEquals(Color.Red.toArgb(), getPixel(10, (size * 1.5f).roundToInt()))
+ assertEquals(Color.Red.toArgb(), getPixel(10, (size * 2.5f).roundToInt()))
+ }
+ }
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidOnStateUpdateTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidOnStateUpdateTest.kt
index 16f6740..94300f6 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidOnStateUpdateTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidOnStateUpdateTest.kt
@@ -70,8 +70,8 @@
newValue = newValue
)
- verify(inputMethodManager, times(1)).restartInput(any())
- verify(inputMethodManager, never()).updateSelection(any(), any(), any(), any(), any())
+ verify(inputMethodManager, times(1)).restartInput()
+ verify(inputMethodManager, never()).updateSelection(any(), any(), any(), any())
assertThat(inputConnection.mTextFieldValue).isEqualTo(newValue)
assertThat(textInputService.state).isEqualTo(newValue)
@@ -85,8 +85,8 @@
newValue = newValue
)
- verify(inputMethodManager, times(1)).restartInput(any())
- verify(inputMethodManager, never()).updateSelection(any(), any(), any(), any(), any())
+ verify(inputMethodManager, times(1)).restartInput()
+ verify(inputMethodManager, never()).updateSelection(any(), any(), any(), any())
assertThat(inputConnection.mTextFieldValue).isEqualTo(newValue)
assertThat(textInputService.state).isEqualTo(newValue)
@@ -100,8 +100,8 @@
newValue = newValue
)
- verify(inputMethodManager, never()).restartInput(any())
- verify(inputMethodManager, times(1)).updateSelection(any(), any(), any(), any(), any())
+ verify(inputMethodManager, never()).restartInput()
+ verify(inputMethodManager, times(1)).updateSelection(any(), any(), any(), any())
assertThat(inputConnection.mTextFieldValue).isEqualTo(newValue)
assertThat(textInputService.state).isEqualTo(newValue)
@@ -121,8 +121,8 @@
newValue = value
)
- verify(inputMethodManager, never()).restartInput(any())
- verify(inputMethodManager, times(1)).updateSelection(any(), any(), any(), any(), any())
+ verify(inputMethodManager, never()).restartInput()
+ verify(inputMethodManager, times(1)).updateSelection(any(), any(), any(), any())
assertThat(inputConnection.mTextFieldValue).isEqualTo(value)
assertThat(textInputService.state).isEqualTo(value)
@@ -148,9 +148,9 @@
newValue = value2
)
- verify(inputMethodManager, never()).restartInput(any())
+ verify(inputMethodManager, never()).restartInput()
verify(inputMethodManager, times(1)).updateSelection(
- any(), eq(value2.selection.min), eq(value2.selection.max), eq(-1), eq(-1)
+ eq(value2.selection.min), eq(value2.selection.max), eq(-1), eq(-1)
)
}
@@ -162,8 +162,8 @@
newValue = newValue
)
- verify(inputMethodManager, never()).restartInput(any())
- verify(inputMethodManager, times(1)).updateSelection(any(), any(), any(), any(), any())
+ verify(inputMethodManager, never()).restartInput()
+ verify(inputMethodManager, times(1)).updateSelection(any(), any(), any(), any())
assertThat(inputConnection.mTextFieldValue).isEqualTo(newValue)
assertThat(textInputService.state).isEqualTo(newValue)
@@ -177,8 +177,8 @@
newValue = value
)
- verify(inputMethodManager, never()).restartInput(any())
- verify(inputMethodManager, never()).updateSelection(any(), any(), any(), any(), any())
+ verify(inputMethodManager, never()).restartInput()
+ verify(inputMethodManager, never()).updateSelection(any(), any(), any(), any())
assertThat(inputConnection.mTextFieldValue).isEqualTo(value)
assertThat(textInputService.state).isEqualTo(value)
@@ -192,8 +192,8 @@
newValue = value
)
- verify(inputMethodManager, never()).restartInput(any())
- verify(inputMethodManager, never()).updateSelection(any(), any(), any(), any(), any())
+ verify(inputMethodManager, never()).restartInput()
+ verify(inputMethodManager, never()).updateSelection(any(), any(), any(), any())
// recreate the connection
inputConnection = textInputService.createInputConnection(EditorInfo())
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/ScrollContainerInfoTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/ScrollContainerInfoTest.kt
new file mode 100644
index 0000000..4bf39eb
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/ScrollContainerInfoTest.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+
+import androidx.compose.ui.input.ScrollContainerInfo
+import androidx.compose.ui.input.canScroll
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class ScrollContainerInfoTest {
+
+ @Test
+ fun canScroll_horizontal() {
+ val subject = Subject(horizontal = true)
+
+ assertThat(subject.canScroll()).isTrue()
+ }
+
+ @Test
+ fun canScroll_vertical() {
+ val subject = Subject(vertical = true)
+
+ assertThat(subject.canScroll()).isTrue()
+ }
+
+ @Test
+ fun canScroll_both() {
+ val subject = Subject(horizontal = true, vertical = true)
+
+ assertThat(subject.canScroll()).isTrue()
+ }
+
+ @Test
+ fun canScroll_neither() {
+ val subject = Subject(horizontal = false, vertical = false)
+
+ assertThat(subject.canScroll()).isFalse()
+ }
+
+ class Subject(
+ private val horizontal: Boolean = false,
+ private val vertical: Boolean = false,
+ ) : ScrollContainerInfo {
+ override fun canScrollHorizontally(): Boolean = horizontal
+ override fun canScrollVertically(): Boolean = vertical
+ }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
index 0885058..7919713 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
@@ -30,6 +30,7 @@
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
+import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.compose.foundation.background
@@ -38,6 +39,8 @@
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
@@ -52,6 +55,8 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.input.canScroll
+import androidx.compose.ui.input.consumeScrollContainerInfo
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
@@ -86,6 +91,7 @@
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
import org.hamcrest.CoreMatchers.endsWith
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.instanceOf
@@ -93,7 +99,6 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import kotlin.math.roundToInt
@MediumTest
@RunWith(AndroidJUnit4::class)
@@ -637,11 +642,19 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun androidView_noClip() {
rule.setContent {
- Box(Modifier.fillMaxSize().background(Color.White)) {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .background(Color.White)) {
with(LocalDensity.current) {
- Box(Modifier.requiredSize(150.toDp()).testTag("box")) {
+ Box(
+ Modifier
+ .requiredSize(150.toDp())
+ .testTag("box")) {
Box(
- Modifier.size(100.toDp(), 100.toDp()).align(AbsoluteAlignment.TopLeft)
+ Modifier
+ .size(100.toDp(), 100.toDp())
+ .align(AbsoluteAlignment.TopLeft)
) {
AndroidView(factory = { context ->
object : View(context) {
@@ -667,6 +680,92 @@
}
}
+ @Test
+ fun scrollableViewGroup_propagates_shouldDelay() {
+ val scrollContainerInfo = mutableStateOf({ false })
+ rule.activityRule.scenario.onActivity { activity ->
+ val parentComposeView = ScrollingViewGroup(activity).apply {
+ addView(
+ ComposeView(activity).apply {
+ setContent {
+ Box(modifier = Modifier.consumeScrollContainerInfo {
+ scrollContainerInfo.value = { it?.canScroll() == true }
+ })
+ }
+ })
+ }
+ activity.setContentView(parentComposeView)
+ }
+
+ rule.runOnIdle {
+ assertThat(scrollContainerInfo.value()).isTrue()
+ }
+ }
+
+ @Test
+ fun nonScrollableViewGroup_doesNotPropagate_shouldDelay() {
+ val scrollContainerInfo = mutableStateOf({ false })
+ rule.activityRule.scenario.onActivity { activity ->
+ val parentComposeView = FrameLayout(activity).apply {
+ addView(
+ ComposeView(activity).apply {
+ setContent {
+ Box(modifier = Modifier.consumeScrollContainerInfo {
+ scrollContainerInfo.value = { it?.canScroll() == true }
+ })
+ }
+ })
+ }
+ activity.setContentView(parentComposeView)
+ }
+
+ rule.runOnIdle {
+ assertThat(scrollContainerInfo.value()).isFalse()
+ }
+ }
+
+ @Test
+ fun viewGroup_propagates_shouldDelayTrue() {
+ lateinit var layout: View
+ rule.setContent {
+ Column(Modifier.verticalScroll(rememberScrollState())) {
+ AndroidView(
+ factory = {
+ layout = LinearLayout(it)
+ layout
+ }
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ // View#isInScrollingContainer is hidden, check the parent manually.
+ val shouldDelay = (layout.parent as ViewGroup).shouldDelayChildPressedState()
+ assertThat(shouldDelay).isTrue()
+ }
+ }
+
+ @Test
+ fun viewGroup_propagates_shouldDelayFalse() {
+ lateinit var layout: View
+ rule.setContent {
+ Column {
+ AndroidView(
+ factory = {
+ layout = LinearLayout(it)
+ layout
+ }
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ // View#isInScrollingContainer is hidden, check the parent manually.
+ val shouldDelay = (layout.parent as ViewGroup).shouldDelayChildPressedState()
+ assertThat(shouldDelay).isFalse()
+ }
+ }
+
private class StateSavingView(
private val key: String,
private val value: String,
@@ -698,4 +797,8 @@
value,
displayMetrics
).roundToInt()
+
+ class ScrollingViewGroup(context: Context) : FrameLayout(context) {
+ override fun shouldDelayChildPressedState() = true
+ }
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
index da9ee0f..02c2c59 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
@@ -73,6 +73,8 @@
import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import androidx.test.filters.SmallTest
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
import org.hamcrest.CoreMatchers.instanceOf
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -83,8 +85,6 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
@MediumTest
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index 2eb61e2..fefb238 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -126,6 +126,9 @@
import androidx.compose.ui.node.Owner
import androidx.compose.ui.node.OwnerSnapshotObserver
import androidx.compose.ui.node.RootForTest
+import androidx.compose.ui.input.ScrollContainerInfo
+import androidx.compose.ui.input.ModifierLocalScrollContainerInfo
+import androidx.compose.ui.modifier.ModifierLocalProvider
import androidx.compose.ui.semantics.SemanticsModifierCore
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsOwner
@@ -220,6 +223,34 @@
false
}
+ // We don't have a way to determine direction in Android, return true for both directions.
+ private val scrollContainerInfo = object : ModifierLocalProvider<ScrollContainerInfo?> {
+ override val key = ModifierLocalScrollContainerInfo
+ override val value = object : ScrollContainerInfo {
+ // Intentionally not using [View#canScrollHorizontally], to maintain semantics of
+ // View#isInScrollingContainer
+ override fun canScrollHorizontally(): Boolean =
+ view.isInScrollableViewGroup()
+
+ // Intentionally not using [View#canScrollVertically], to maintain semantics of
+ // View#isInScrollingContainer
+ override fun canScrollVertically(): Boolean =
+ view.isInScrollableViewGroup()
+
+ // Copied from View#isInScrollingContainer() which is @hide
+ private fun View.isInScrollableViewGroup(): Boolean {
+ var p = parent
+ while (p != null && p is ViewGroup) {
+ if (p.shouldDelayChildPressedState()) {
+ return true
+ }
+ p = p.parent
+ }
+ return false
+ }
+ }
+ }
+
private val canvasHolder = CanvasHolder()
override val root = LayoutNode().also {
@@ -231,6 +262,7 @@
.then(rotaryInputModifier)
.then(_focusManager.modifier)
.then(keyInputModifier)
+ .then(scrollContainerInfo)
}
override val rootForTest: RootForTest = this
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputMethodManager.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputMethodManager.kt
index 39071a16..50d67c6 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputMethodManager.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputMethodManager.kt
@@ -16,26 +16,33 @@
package androidx.compose.ui.text.input
+import android.app.Activity
import android.content.Context
-import android.os.IBinder
+import android.content.ContextWrapper
+import android.os.Build
+import android.util.Log
import android.view.View
+import android.view.Window
import android.view.inputmethod.ExtractedText
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.compose.ui.window.DialogWindowProvider
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
internal interface InputMethodManager {
- fun restartInput(view: View)
+ fun restartInput()
- fun showSoftInput(view: View)
+ fun showSoftInput()
- fun hideSoftInputFromWindow(windowToken: IBinder?)
+ fun hideSoftInput()
fun updateExtractedText(
- view: View,
token: Int,
extractedText: ExtractedText
)
fun updateSelection(
- view: View,
selectionStart: Int,
selectionEnd: Int,
compositionStart: Int,
@@ -47,27 +54,36 @@
* Wrapper class to prevent depending on getSystemService and final InputMethodManager.
* Let's us test TextInputServiceAndroid class.
*/
-internal class InputMethodManagerImpl(context: Context) : InputMethodManager {
+internal class InputMethodManagerImpl(private val view: View) : InputMethodManager {
private val imm by lazy(LazyThreadSafetyMode.NONE) {
- context.getSystemService(Context.INPUT_METHOD_SERVICE)
+ view.context.getSystemService(Context.INPUT_METHOD_SERVICE)
as android.view.inputmethod.InputMethodManager
}
- override fun restartInput(view: View) {
+ private val helper = if (Build.VERSION.SDK_INT < 30) {
+ ImmHelper21(view)
+ } else {
+ ImmHelper30(view)
+ }
+
+ override fun restartInput() {
imm.restartInput(view)
}
- override fun showSoftInput(view: View) {
- imm.showSoftInput(view, 0)
+ override fun showSoftInput() {
+ if (DEBUG && !view.hasWindowFocus()) {
+ Log.d(TAG, "InputMethodManagerImpl: requesting soft input on non focused field")
+ }
+
+ helper.showSoftInput(imm)
}
- override fun hideSoftInputFromWindow(windowToken: IBinder?) {
- imm.hideSoftInputFromWindow(windowToken, 0)
+ override fun hideSoftInput() {
+ helper.hideSoftInput(imm)
}
override fun updateExtractedText(
- view: View,
token: Int,
extractedText: ExtractedText
) {
@@ -75,7 +91,6 @@
}
override fun updateSelection(
- view: View,
selectionStart: Int,
selectionEnd: Int,
compositionStart: Int,
@@ -83,4 +98,71 @@
) {
imm.updateSelection(view, selectionStart, selectionEnd, compositionStart, compositionEnd)
}
-}
\ No newline at end of file
+}
+
+private interface ImmHelper {
+ fun showSoftInput(imm: android.view.inputmethod.InputMethodManager)
+ fun hideSoftInput(imm: android.view.inputmethod.InputMethodManager)
+}
+
+private class ImmHelper21(private val view: View) : ImmHelper {
+
+ @DoNotInline
+ override fun showSoftInput(imm: android.view.inputmethod.InputMethodManager) {
+ view.post {
+ imm.showSoftInput(view, 0)
+ }
+ }
+
+ @DoNotInline
+ override fun hideSoftInput(imm: android.view.inputmethod.InputMethodManager) {
+ imm.hideSoftInputFromWindow(view.windowToken, 0)
+ }
+}
+
+@RequiresApi(30)
+private class ImmHelper30(private val view: View) : ImmHelper {
+
+ /**
+ * Get a [WindowInsetsControllerCompat] for the view. This returns a new instance every time,
+ * since the view may return null or not null at different times depending on window attach
+ * state.
+ */
+ private val insetsControllerCompat
+ // This can return null when, for example, the view is not attached to a window.
+ get() = view.findWindow()?.let { WindowInsetsControllerCompat(it, view) }
+
+ /**
+ * This class falls back to the legacy implementation when the window insets controller isn't
+ * available.
+ */
+ private val immHelper21: ImmHelper21
+ get() = _immHelper21 ?: ImmHelper21(view).also { _immHelper21 = it }
+ private var _immHelper21: ImmHelper21? = null
+
+ @DoNotInline
+ override fun showSoftInput(imm: android.view.inputmethod.InputMethodManager) {
+ insetsControllerCompat?.apply {
+ show(WindowInsetsCompat.Type.ime())
+ } ?: immHelper21.showSoftInput(imm)
+ }
+
+ @DoNotInline
+ override fun hideSoftInput(imm: android.view.inputmethod.InputMethodManager) {
+ insetsControllerCompat?.apply {
+ hide(WindowInsetsCompat.Type.ime())
+ } ?: immHelper21.hideSoftInput(imm)
+ }
+
+ // TODO(b/221889664) Replace with composition local when available.
+ private fun View.findWindow(): Window? =
+ (parent as? DialogWindowProvider)?.window
+ ?: context.findWindow()
+
+ private tailrec fun Context.findWindow(): Window? =
+ when (this) {
+ is Activity -> window
+ is ContextWrapper -> baseContext.findWindow()
+ else -> null
+ }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.android.kt
index 97ee595..f234d53 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.android.kt
@@ -21,7 +21,6 @@
import android.text.TextUtils
import android.util.Log
import android.view.KeyEvent
-import android.view.View
import android.view.inputmethod.CompletionInfo
import android.view.inputmethod.CorrectionInfo
import android.view.inputmethod.EditorInfo
@@ -94,7 +93,6 @@
fun updateInputState(
state: TextFieldValue,
inputMethodManager: InputMethodManager,
- view: View
) {
if (!isActive) return
@@ -104,7 +102,6 @@
if (extractedTextMonitorMode) {
inputMethodManager.updateExtractedText(
- view,
currentExtractedTextRequestToken,
state.toExtractedText()
)
@@ -121,7 +118,7 @@
)
}
inputMethodManager.updateSelection(
- view, state.selection.min, state.selection.max, compositionStart, compositionEnd
+ state.selection.min, state.selection.max, compositionStart, compositionEnd
)
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
index 1dbb467..14cff951 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
@@ -95,10 +95,12 @@
*/
private val textInputCommandChannel = Channel<TextInputCommand>(Channel.UNLIMITED)
- internal constructor(view: View) : this(view, InputMethodManagerImpl(view.context))
+ internal constructor(view: View) : this(view, InputMethodManagerImpl(view))
init {
- if (DEBUG) { Log.d(TAG, "$DEBUG_CLASS.create") }
+ if (DEBUG) {
+ Log.d(TAG, "$DEBUG_CLASS.create")
+ }
}
/**
@@ -138,7 +140,9 @@
}
).also {
ics.add(WeakReference(it))
- if (DEBUG) { Log.d(TAG, "$DEBUG_CLASS.createInputConnection: $ics") }
+ if (DEBUG) {
+ Log.d(TAG, "$DEBUG_CLASS.createInputConnection: $ics")
+ }
}
}
@@ -324,7 +328,6 @@
if (needUpdateSelection) {
// updateSelection API requires -1 if there is no composition
inputMethodManager.updateSelection(
- view = view,
selectionStart = newValue.selection.min,
selectionEnd = newValue.selection.max,
compositionStart = state.composition?.min ?: -1,
@@ -348,7 +351,7 @@
restartInputImmediately()
} else {
for (i in 0 until ics.size) {
- ics[i].get()?.updateInputState(this.state, inputMethodManager, view)
+ ics[i].get()?.updateInputState(this.state, inputMethodManager)
}
}
}
@@ -380,16 +383,16 @@
/** Immediately restart the IME connection, bypassing the [textInputCommandChannel]. */
private fun restartInputImmediately() {
if (DEBUG) Log.d(TAG, "$DEBUG_CLASS.restartInputImmediately")
- inputMethodManager.restartInput(view)
+ inputMethodManager.restartInput()
}
/** Immediately show or hide the keyboard, bypassing the [textInputCommandChannel]. */
private fun setKeyboardVisibleImmediately(visible: Boolean) {
if (DEBUG) Log.d(TAG, "$DEBUG_CLASS.setKeyboardVisibleImmediately(visible=$visible)")
if (visible) {
- inputMethodManager.showSoftInput(view)
+ inputMethodManager.showSoftInput()
} else {
- inputMethodManager.hideSoftInputFromWindow(view.windowToken)
+ inputMethodManager.hideSoftInput()
}
}
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
index 9b725ee..6032a96 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
@@ -61,9 +61,9 @@
* Compose and there is no corresponding Compose API. Common examples for the moment are
* WebView, SurfaceView, AdView, etc.
*
- * [AndroidView] will clip its content to the layout bounds, as being clipped is a common
- * assumption made by [View]s - keeping clipping disabled might lead to unexpected drawing behavior.
- * Note this deviates from Compose's practice of keeping clipping opt-in, disabled by default.
+ * [AndroidView] will not clip its content to the layout bounds. Use [View.setClipToOutline] on
+ * the child View to clip the contents, if desired. Developers will likely want to do this with
+ * all subclasses of SurfaceView to keep its contents contained.
*
* [AndroidView] has nested scroll interop capabilities if the containing view has nested scroll
* enabled. This means this Composable can dispatch scroll deltas if it is placed inside a
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
index 7638528..4694d96 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
@@ -35,6 +35,8 @@
import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.pointer.pointerInteropFilter
+import androidx.compose.ui.input.canScroll
+import androidx.compose.ui.input.consumeScrollContainerInfo
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.Measurable
@@ -181,6 +183,8 @@
private val nestedScrollingParentHelper: NestedScrollingParentHelper =
NestedScrollingParentHelper(this)
+ private var isInScrollContainer: () -> Boolean = { true }
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
view?.measure(widthMeasureSpec, heightMeasureSpec)
setMeasuredDimension(view?.measuredWidth ?: 0, view?.measuredHeight ?: 0)
@@ -280,11 +284,15 @@
(layoutNode.owner as? AndroidComposeView)
?.drawAndroidView(this@AndroidViewHolder, canvas.nativeCanvas)
}
- }.onGloballyPositioned {
+ }
+ .onGloballyPositioned {
// The global position of this LayoutNode can change with it being replaced. For
// these cases, we need to inform the View.
layoutAccordingTo(layoutNode)
}
+ .consumeScrollContainerInfo { scrollContainerInfo ->
+ isInScrollContainer = { scrollContainerInfo?.canScroll() == true }
+ }
layoutNode.modifier = modifier.then(coreModifier)
onModifierChanged = { layoutNode.modifier = it.then(coreModifier) }
@@ -398,9 +406,7 @@
}
}
- // TODO: b/203141462 - consume whether the AndroidView() is inside a scrollable container, and
- // use that to set this. In the meantime set true as the defensive default.
- override fun shouldDelayChildPressedState(): Boolean = true
+ override fun shouldDelayChildPressedState(): Boolean = isInScrollContainer()
// NestedScrollingParent3
override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ExperimentalComposeUiApi.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ExperimentalComposeUiApi.kt
index 84259b4..00116b9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ExperimentalComposeUiApi.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ExperimentalComposeUiApi.kt
@@ -17,4 +17,5 @@
package androidx.compose.ui
@RequiresOptIn("This API is experimental and is likely to change in the future.")
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalComposeUiApi
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/InternalComposeUiApi.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/InternalComposeUiApi.kt
index d2977ca..476d2aa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/InternalComposeUiApi.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/InternalComposeUiApi.kt
@@ -24,4 +24,5 @@
"Unstable API for use only between compose-ui modules sharing the same exact version, " +
"subject to change without notice in major, minor, or patch releases."
)
+@Retention(AnnotationRetention.BINARY)
annotation class InternalComposeUiApi
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/ScrollContainerInfo.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/ScrollContainerInfo.kt
new file mode 100644
index 0000000..2e8afd7
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/ScrollContainerInfo.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.input
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.modifier.ModifierLocalConsumer
+import androidx.compose.ui.modifier.ModifierLocalProvider
+import androidx.compose.ui.modifier.ModifierLocalReadScope
+import androidx.compose.ui.modifier.ProvidableModifierLocal
+import androidx.compose.ui.modifier.modifierLocalConsumer
+import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.platform.debugInspectorInfo
+
+/**
+ * Represents a component that handles scroll events, so that other components in the hierarchy
+ * can adjust their behaviour.
+ * @See [provideScrollContainerInfo] and [consumeScrollContainerInfo]
+ */
+interface ScrollContainerInfo {
+ /** @return whether this component handles horizontal scroll events */
+ fun canScrollHorizontally(): Boolean
+ /** @return whether this component handles vertical scroll events */
+ fun canScrollVertically(): Boolean
+}
+
+/** @return whether this container handles either horizontal or vertical scroll events */
+fun ScrollContainerInfo.canScroll() = canScrollVertically() || canScrollHorizontally()
+
+/**
+ * A modifier to query whether there are any parents in the hierarchy that handle scroll events.
+ * The [ScrollContainerInfo] provided in [consumer] will recursively look for ancestors if the
+ * nearest parent does not handle scroll events in the queried direction.
+ * This can be used to delay UI changes in cases where a pointer event may later become a scroll,
+ * cancelling any existing press or other gesture.
+ *
+ * @sample androidx.compose.ui.samples.ScrollableContainerSample
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+fun Modifier.consumeScrollContainerInfo(consumer: (ScrollContainerInfo?) -> Unit): Modifier =
+ modifierLocalConsumer {
+ consumer(ModifierLocalScrollContainerInfo.current)
+ }
+
+/**
+ * A Modifier that indicates that this component handles scroll events. Use
+ * [consumeScrollContainerInfo] to query whether there is a parent in the hierarchy that is
+ * a [ScrollContainerInfo].
+ */
+fun Modifier.provideScrollContainerInfo(scrollContainerInfo: ScrollContainerInfo): Modifier =
+ composed(
+ inspectorInfo = debugInspectorInfo {
+ name = "provideScrollContainerInfo"
+ value = scrollContainerInfo
+ }) {
+ remember(scrollContainerInfo) {
+ ScrollContainerInfoModifierLocal(scrollContainerInfo)
+ }
+}
+
+/**
+ * ModifierLocal to propagate ScrollableContainer throughout the hierarchy.
+ * This Modifier will recursively check for ancestor ScrollableContainers,
+ * if the current ScrollableContainer does not handle scroll events in a particular direction.
+ */
+private class ScrollContainerInfoModifierLocal(
+ private val scrollContainerInfo: ScrollContainerInfo,
+) : ScrollContainerInfo, ModifierLocalProvider<ScrollContainerInfo?>, ModifierLocalConsumer {
+
+ private var parent: ScrollContainerInfo? by mutableStateOf(null)
+
+ override val key: ProvidableModifierLocal<ScrollContainerInfo?> =
+ ModifierLocalScrollContainerInfo
+ override val value: ScrollContainerInfoModifierLocal = this
+
+ override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) = with(scope) {
+ parent = ModifierLocalScrollContainerInfo.current
+ }
+
+ override fun canScrollHorizontally(): Boolean {
+ return scrollContainerInfo.canScrollHorizontally() ||
+ parent?.canScrollHorizontally() == true
+ }
+
+ override fun canScrollVertically(): Boolean {
+ return scrollContainerInfo.canScrollVertically() || parent?.canScrollVertically() == true
+ }
+}
+
+internal val ModifierLocalScrollContainerInfo = modifierLocalOf<ScrollContainerInfo?> {
+ null
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt
index e4f5e82..4edb9c4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt
@@ -38,7 +38,7 @@
* The input data is provided by calling [addPosition]. Adding data is cheap.
*
* To obtain a velocity, call [calculateVelocity]. This will compute the velocity
- * based on the data added so far. Only call this when you need to use the velocity,
+ * based on the data added so far. Only call this when you need to use the velocity,
* as it is comparatively expensive.
*
* The quality of the velocity estimation will be better if more data points
@@ -52,9 +52,9 @@
internal var currentPointerPositionAccumulator = Offset.Zero
/**
- * Adds a position as the given time to the tracker.
+ * Adds a position at the given time to the tracker.
*
- * Call resetTracking to remove added Offsets.
+ * Call [resetTracking] to remove added [Offset]s.
*
* @see resetTracking
*/
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InternalCoreApi.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InternalCoreApi.kt
index 3da8a82..57e2bfb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InternalCoreApi.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InternalCoreApi.kt
@@ -21,4 +21,5 @@
AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY,
AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER
)
+@Retention(AnnotationRetention.BINARY)
annotation class InternalCoreApi
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
index 9d0bed1..c56b027 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
@@ -234,7 +234,7 @@
headToTail {
if (!it.isAttached) {
it.attach()
- if (performInvalidations) autoInvalidateNode(it)
+ if (performInvalidations) autoInvalidateInsertedNode(it)
}
}
}
@@ -432,7 +432,7 @@
// for removing nodes, we always do the autoInvalidateNode call,
// regardless of whether or not it was a ModifierNodeElement with autoInvalidate
// true, or a BackwardsCompatNode, etc.
- autoInvalidateNode(node)
+ autoInvalidateRemovedNode(node)
node.detach()
}
return removeNode(node)
@@ -510,24 +510,26 @@
check(node is BackwardsCompatNode)
node.element = next
// we always autoInvalidate BackwardsCompatNode
- autoInvalidateNode(node)
+ autoInvalidateUpdatedNode(node)
return node
}
val updated = next.updateUnsafe(node)
- val result = if (updated !== node) {
+ if (updated !== node) {
// if a new instance is returned, we want to detach the old one
+ autoInvalidateRemovedNode(node)
node.detach()
- replaceNode(node, updated)
+ val result = replaceNode(node, updated)
+ autoInvalidateInsertedNode(updated)
+ return result
} else {
// the node was updated. we are done.
- updated
+ if (next.autoInvalidate) {
+ // the modifier element is labeled as "auto invalidate", which means that since the
+ // node was updated, we need to invalidate everything relevant to it
+ autoInvalidateUpdatedNode(updated)
+ }
+ return updated
}
- if (next.autoInvalidate) {
- // the modifier element is labeled as "auto invalidate", which means that since the
- // node was updated, we need to invalidate everything relevant to it
- autoInvalidateNode(result)
- }
- return result
}
// TRAVERSAL
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
index 4f8022f..04fef20 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
@@ -156,10 +156,29 @@
return mask
}
+private const val Updated = 0
+private const val Inserted = 1
+private const val Removed = 2
+
@OptIn(ExperimentalComposeUiApi::class)
-internal fun autoInvalidateNode(node: Modifier.Node) {
+internal fun autoInvalidateRemovedNode(node: Modifier.Node) = autoInvalidateNode(node, Removed)
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun autoInvalidateInsertedNode(node: Modifier.Node) = autoInvalidateNode(node, Inserted)
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun autoInvalidateUpdatedNode(node: Modifier.Node) = autoInvalidateNode(node, Updated)
+@OptIn(ExperimentalComposeUiApi::class)
+private fun autoInvalidateNode(node: Modifier.Node, phase: Int) {
if (node.isKind(Nodes.Layout) && node is LayoutModifierNode) {
node.invalidateMeasurements()
+ if (phase == Removed) {
+ val coordinator = node.requireCoordinator(Nodes.Layout)
+ val layer = coordinator.layer
+ if (layer != null) {
+ coordinator.onLayerBlockUpdated(null)
+ }
+ }
}
if (node.isKind(Nodes.GlobalPositionAware) && node is GlobalPositionAwareModifierNode) {
node.requireLayoutNode().invalidateMeasurements()
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/RecordingInputConnectionUpdateTextFieldValueTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/RecordingInputConnectionUpdateTextFieldValueTest.kt
index 7a9486c..4774c9f 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/RecordingInputConnectionUpdateTextFieldValueTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/RecordingInputConnectionUpdateTextFieldValueTest.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.input
-import android.view.View
import android.view.inputmethod.ExtractedText
import android.view.inputmethod.InputConnection
import androidx.compose.ui.text.TextRange
@@ -25,6 +24,10 @@
import androidx.compose.ui.text.input.RecordingInputConnection
import androidx.compose.ui.text.input.TextFieldValue
import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
@@ -32,10 +35,6 @@
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class RecordingInputConnectionUpdateTextFieldValueTest {
@@ -56,48 +55,45 @@
@Test
fun test_update_input_state() {
val imm: InputMethodManager = mock()
- val view: View = mock()
val inputState = TextFieldValue(text = "Hello, World.", selection = TextRange.Zero)
- ic.updateInputState(inputState, imm, view)
+ ic.updateInputState(inputState, imm)
- verify(imm, times(1)).updateSelection(eq(view), eq(0), eq(0), eq(-1), eq(-1))
- verify(imm, never()).updateExtractedText(any(), any(), any())
+ verify(imm, times(1)).updateSelection(eq(0), eq(0), eq(-1), eq(-1))
+ verify(imm, never()).updateExtractedText(any(), any())
}
@Test
fun test_update_input_state_inactive() {
val imm: InputMethodManager = mock()
- val view: View = mock()
val previousTextFieldValue = ic.mTextFieldValue
ic.closeConnection()
val inputState = TextFieldValue(text = "Hello, World.", selection = TextRange.Zero)
- ic.updateInputState(inputState, imm, view)
+ ic.updateInputState(inputState, imm)
assertThat(ic.mTextFieldValue).isEqualTo(previousTextFieldValue)
- verify(imm, never()).updateSelection(any(), any(), any(), any(), any())
- verify(imm, never()).updateExtractedText(any(), any(), any())
+ verify(imm, never()).updateSelection(any(), any(), any(), any())
+ verify(imm, never()).updateExtractedText(any(), any())
}
@Test
fun test_update_input_state_extracted_text_monitor() {
val imm: InputMethodManager = mock()
- val view: View = mock()
ic.getExtractedText(null, InputConnection.GET_EXTRACTED_TEXT_MONITOR)
val inputState = TextFieldValue(text = "Hello, World.", selection = TextRange.Zero)
- ic.updateInputState(inputState, imm, view)
+ ic.updateInputState(inputState, imm)
- verify(imm, times(1)).updateSelection(eq(view), eq(0), eq(0), eq(-1), eq(-1))
+ verify(imm, times(1)).updateSelection(eq(0), eq(0), eq(-1), eq(-1))
val captor = argumentCaptor<ExtractedText>()
- verify(imm, times(1)).updateExtractedText(any(), any(), captor.capture())
+ verify(imm, times(1)).updateExtractedText(any(), captor.capture())
assertThat(captor.allValues.size).isEqualTo(1)
assertThat(captor.firstValue.text).isEqualTo("Hello, World.")
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroidCommandDebouncingTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroidCommandDebouncingTest.kt
index 32854e2..9de574d 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroidCommandDebouncingTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroidCommandDebouncingTest.kt
@@ -16,12 +16,9 @@
package androidx.compose.ui.text.input
-import android.os.IBinder
import android.view.View
import android.view.inputmethod.ExtractedText
import com.google.common.truth.Truth.assertThat
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
@@ -32,6 +29,8 @@
import org.junit.After
import org.junit.Before
import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
@OptIn(ExperimentalCoroutinesApi::class)
class TextInputServiceAndroidCommandDebouncingTest {
@@ -59,9 +58,9 @@
service.showSoftwareKeyboard()
scope.advanceUntilIdle()
- assertThat(inputMethodManager.showSoftInputCalls).hasSize(1)
- assertThat(inputMethodManager.restartCalls).isEmpty()
- assertThat(inputMethodManager.hideSoftInputCalls).isEmpty()
+ assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(1)
+ assertThat(inputMethodManager.restartCalls).isEqualTo(0)
+ assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(0)
}
@Test
@@ -69,9 +68,9 @@
service.hideSoftwareKeyboard()
scope.advanceUntilIdle()
- assertThat(inputMethodManager.hideSoftInputCalls).hasSize(1)
- assertThat(inputMethodManager.restartCalls).isEmpty()
- assertThat(inputMethodManager.showSoftInputCalls).isEmpty()
+ assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(1)
+ assertThat(inputMethodManager.restartCalls).isEqualTo(0)
+ assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
}
@Test
@@ -79,7 +78,7 @@
service.startInput()
scope.advanceUntilIdle()
- assertThat(inputMethodManager.restartCalls).hasSize(1)
+ assertThat(inputMethodManager.restartCalls).isEqualTo(1)
}
@Test
@@ -87,7 +86,7 @@
service.startInput()
scope.advanceUntilIdle()
- assertThat(inputMethodManager.showSoftInputCalls).hasSize(1)
+ assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(1)
}
@Test
@@ -95,7 +94,7 @@
service.stopInput()
scope.advanceUntilIdle()
- assertThat(inputMethodManager.restartCalls).hasSize(1)
+ assertThat(inputMethodManager.restartCalls).isEqualTo(1)
}
@Test
@@ -103,7 +102,7 @@
service.stopInput()
scope.advanceUntilIdle()
- assertThat(inputMethodManager.hideSoftInputCalls).hasSize(1)
+ assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(1)
}
@Test
@@ -116,7 +115,7 @@
// in either order, should debounce to a single restart call. If they aren't de-duped, the
// keyboard may flicker if one of the calls configures the IME in a non-default way (e.g.
// number input).
- assertThat(inputMethodManager.restartCalls).hasSize(1)
+ assertThat(inputMethodManager.restartCalls).isEqualTo(1)
}
@Test
@@ -129,7 +128,7 @@
// in either order, should debounce to a single restart call. If they aren't de-duped, the
// keyboard may flicker if one of the calls configures the IME in a non-default way (e.g.
// number input).
- assertThat(inputMethodManager.restartCalls).hasSize(1)
+ assertThat(inputMethodManager.restartCalls).isEqualTo(1)
}
@Test
@@ -140,7 +139,7 @@
// After stopInput, there's no input connection, so any calls to show the keyboard should
// be ignored until the next call to startInput.
- assertThat(inputMethodManager.showSoftInputCalls).isEmpty()
+ assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
}
@Test
@@ -152,7 +151,7 @@
// stopInput will hide the keyboard implicitly, so both stopInput and hideSoftwareKeyboard
// have the effect "hide the keyboard". These two effects should be debounced and the IMM
// should only get a single hide call instead of two redundant calls.
- assertThat(inputMethodManager.hideSoftInputCalls).hasSize(1)
+ assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(1)
}
@Test
@@ -162,7 +161,7 @@
}
scope.advanceUntilIdle()
- assertThat(inputMethodManager.showSoftInputCalls).hasSize(1)
+ assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(1)
}
@Test
@@ -172,7 +171,7 @@
}
scope.advanceUntilIdle()
- assertThat(inputMethodManager.hideSoftInputCalls).hasSize(1)
+ assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(1)
}
@Test
@@ -181,8 +180,8 @@
service.hideSoftwareKeyboard()
scope.advanceUntilIdle()
- assertThat(inputMethodManager.showSoftInputCalls).hasSize(0)
- assertThat(inputMethodManager.hideSoftInputCalls).hasSize(1)
+ assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
+ assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(1)
}
@Test
@@ -191,40 +190,40 @@
service.showSoftwareKeyboard()
scope.advanceUntilIdle()
- assertThat(inputMethodManager.showSoftInputCalls).hasSize(1)
- assertThat(inputMethodManager.hideSoftInputCalls).hasSize(0)
+ assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(1)
+ assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(0)
}
@Test fun stopInput_isNotProcessedImmediately() {
service.stopInput()
- assertThat(inputMethodManager.restartCalls).isEmpty()
- assertThat(inputMethodManager.showSoftInputCalls).isEmpty()
- assertThat(inputMethodManager.hideSoftInputCalls).isEmpty()
+ assertThat(inputMethodManager.restartCalls).isEqualTo(0)
+ assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
+ assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(0)
}
@Test fun startInput_isNotProcessedImmediately() {
service.startInput()
- assertThat(inputMethodManager.restartCalls).isEmpty()
- assertThat(inputMethodManager.showSoftInputCalls).isEmpty()
- assertThat(inputMethodManager.hideSoftInputCalls).isEmpty()
+ assertThat(inputMethodManager.restartCalls).isEqualTo(0)
+ assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
+ assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(0)
}
@Test fun showSoftwareKeyboard_isNotProcessedImmediately() {
service.showSoftwareKeyboard()
- assertThat(inputMethodManager.restartCalls).isEmpty()
- assertThat(inputMethodManager.showSoftInputCalls).isEmpty()
- assertThat(inputMethodManager.hideSoftInputCalls).isEmpty()
+ assertThat(inputMethodManager.restartCalls).isEqualTo(0)
+ assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
+ assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(0)
}
@Test fun hideSoftwareKeyboard_isNotProcessedImmediately() {
service.hideSoftwareKeyboard()
- assertThat(inputMethodManager.restartCalls).isEmpty()
- assertThat(inputMethodManager.showSoftInputCalls).isEmpty()
- assertThat(inputMethodManager.hideSoftInputCalls).isEmpty()
+ assertThat(inputMethodManager.restartCalls).isEqualTo(0)
+ assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
+ assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(0)
}
@Test fun commandsAreIgnored_ifFocusLostBeforeProcessing() {
@@ -235,7 +234,7 @@
// Process the queued commands.
scope.advanceUntilIdle()
- assertThat(inputMethodManager.showSoftInputCalls).isEmpty()
+ assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
}
@Test fun commandsAreDrained_whenProcessedWithoutFocus() {
@@ -246,7 +245,7 @@
whenever(view.isFocused).thenReturn(true)
scope.advanceUntilIdle()
- assertThat(inputMethodManager.showSoftInputCalls).isEmpty()
+ assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
}
private fun TextInputServiceAndroid.startInput() {
@@ -259,27 +258,26 @@
}
private class TestInputMethodManager : InputMethodManager {
- val restartCalls = mutableListOf<View>()
- val showSoftInputCalls = mutableListOf<View>()
- val hideSoftInputCalls = mutableListOf<IBinder?>()
+ var restartCalls = 0
+ var showSoftInputCalls = 0
+ var hideSoftInputCalls = 0
- override fun restartInput(view: View) {
- restartCalls += view
+ override fun restartInput() {
+ restartCalls++
}
- override fun showSoftInput(view: View) {
- showSoftInputCalls += view
+ override fun showSoftInput() {
+ showSoftInputCalls++
}
- override fun hideSoftInputFromWindow(windowToken: IBinder?) {
- hideSoftInputCalls += windowToken
+ override fun hideSoftInput() {
+ hideSoftInputCalls++
}
- override fun updateExtractedText(view: View, token: Int, extractedText: ExtractedText) {
+ override fun updateExtractedText(token: Int, extractedText: ExtractedText) {
}
override fun updateSelection(
- view: View,
selectionStart: Int,
selectionEnd: Int,
compositionStart: Int,
diff --git a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
index fd40d95..96daef8 100644
--- a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
@@ -362,7 +362,7 @@
property public final androidx.constraintlayout.compose.Easing Standard;
}
- @kotlin.RequiresOptIn(message="MotionLayout API is experimental and it is likely to change.") public @interface ExperimentalMotionApi {
+ @kotlin.RequiresOptIn(message="MotionLayout API is experimental and it is likely to change.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMotionApi {
}
@androidx.compose.runtime.Immutable public final class FlowStyle {
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ExperimentalMotionApi.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ExperimentalMotionApi.kt
index 2e50d98..efa4195 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ExperimentalMotionApi.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ExperimentalMotionApi.kt
@@ -17,4 +17,5 @@
package androidx.constraintlayout.compose
@RequiresOptIn("MotionLayout API is experimental and it is likely to change.")
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalMotionApi
diff --git a/datastore/datastore-core/api/public_plus_experimental_current.txt b/datastore/datastore-core/api/public_plus_experimental_current.txt
index 4821c84..1c7216f 100644
--- a/datastore/datastore-core/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-core/api/public_plus_experimental_current.txt
@@ -34,7 +34,7 @@
field public static final androidx.datastore.core.DataStoreFactory INSTANCE;
}
- @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.WARNING, message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface ExperimentalMultiProcessDataStore {
+ @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.WARNING, message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface ExperimentalMultiProcessDataStore {
}
public final class MultiProcessDataStoreFactory {
diff --git a/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/ExperimentalMultiProcessDataStore.kt b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/ExperimentalMultiProcessDataStore.kt
index 5546222..49a2d2d 100644
--- a/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/ExperimentalMultiProcessDataStore.kt
+++ b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/ExperimentalMultiProcessDataStore.kt
@@ -21,4 +21,5 @@
message = "This API is experimental and is likely to change in the future."
)
@Target(AnnotationTarget.FUNCTION)
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalMultiProcessDataStore
\ No newline at end of file
diff --git a/fragment/fragment/api/current.txt b/fragment/fragment/api/current.txt
index d10b6de..ee8ccae 100644
--- a/fragment/fragment/api/current.txt
+++ b/fragment/fragment/api/current.txt
@@ -489,6 +489,7 @@
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectSetUserVisibleHint();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectTargetFragmentUsage();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectWrongFragmentContainer();
+ method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectWrongNestedHierarchy();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyDeath();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyListener(androidx.fragment.app.strictmode.FragmentStrictMode.OnViolationListener listener);
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyLog();
@@ -539,5 +540,12 @@
property public final android.view.ViewGroup container;
}
+ public final class WrongNestedHierarchyViolation extends androidx.fragment.app.strictmode.Violation {
+ method public int getContainerId();
+ method public androidx.fragment.app.Fragment getParentFragment();
+ property public final int containerId;
+ property public final androidx.fragment.app.Fragment parentFragment;
+ }
+
}
diff --git a/fragment/fragment/api/public_plus_experimental_current.txt b/fragment/fragment/api/public_plus_experimental_current.txt
index d10b6de..ee8ccae 100644
--- a/fragment/fragment/api/public_plus_experimental_current.txt
+++ b/fragment/fragment/api/public_plus_experimental_current.txt
@@ -489,6 +489,7 @@
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectSetUserVisibleHint();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectTargetFragmentUsage();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectWrongFragmentContainer();
+ method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectWrongNestedHierarchy();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyDeath();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyListener(androidx.fragment.app.strictmode.FragmentStrictMode.OnViolationListener listener);
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyLog();
@@ -539,5 +540,12 @@
property public final android.view.ViewGroup container;
}
+ public final class WrongNestedHierarchyViolation extends androidx.fragment.app.strictmode.Violation {
+ method public int getContainerId();
+ method public androidx.fragment.app.Fragment getParentFragment();
+ property public final int containerId;
+ property public final androidx.fragment.app.Fragment parentFragment;
+ }
+
}
diff --git a/fragment/fragment/api/restricted_current.txt b/fragment/fragment/api/restricted_current.txt
index 44528a7..29eb3b1 100644
--- a/fragment/fragment/api/restricted_current.txt
+++ b/fragment/fragment/api/restricted_current.txt
@@ -518,6 +518,7 @@
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectSetUserVisibleHint();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectTargetFragmentUsage();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectWrongFragmentContainer();
+ method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectWrongNestedHierarchy();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyDeath();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyListener(androidx.fragment.app.strictmode.FragmentStrictMode.OnViolationListener listener);
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyLog();
@@ -568,5 +569,12 @@
property public final android.view.ViewGroup container;
}
+ public final class WrongNestedHierarchyViolation extends androidx.fragment.app.strictmode.Violation {
+ method public int getContainerId();
+ method public androidx.fragment.app.Fragment getParentFragment();
+ property public final int containerId;
+ property public final androidx.fragment.app.Fragment parentFragment;
+ }
+
}
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/OnBackPressedCallbackTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OnBackPressedCallbackTest.kt
index e9428d1..abe5b23 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/OnBackPressedCallbackTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OnBackPressedCallbackTest.kt
@@ -44,7 +44,8 @@
@Test
fun testBackPressFinishesActivity() {
- withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+ // Since this activity finishes manually, we do not want to use withUse here
+ with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
val countDownLatch = withActivity {
onBackPressed()
finishCountDownLatch
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/strictmode/FragmentStrictModeTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/strictmode/FragmentStrictModeTest.kt
index 83a2f21..9ea6d81 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/strictmode/FragmentStrictModeTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/strictmode/FragmentStrictModeTest.kt
@@ -18,6 +18,7 @@
import android.os.Looper
import androidx.fragment.app.StrictFragment
+import androidx.fragment.app.StrictViewFragment
import androidx.fragment.app.executePendingTransactions
import androidx.fragment.app.test.FragmentTestActivity
import androidx.fragment.test.R
@@ -234,6 +235,87 @@
}
}
+ @Test
+ public fun detectWrongNestedHierarchyNoParent() {
+ var violation: Violation? = null
+ val policy = FragmentStrictMode.Policy.Builder()
+ .detectWrongNestedHierarchy()
+ .penaltyListener { violation = it }
+ .build()
+ FragmentStrictMode.defaultPolicy = policy
+
+ withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+ val fm = withActivity {
+ setContentView(R.layout.simple_container)
+ supportFragmentManager
+ }
+ val outerFragment = StrictViewFragment(R.layout.scene1)
+ val innerFragment = StrictViewFragment(R.layout.fragment_a)
+
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, outerFragment)
+ .setReorderingAllowed(false)
+ .commit()
+ // Here we add childFragment to a layout within parentFragment, but we
+ // specifically don't use parentFragment.childFragmentManager
+ fm.beginTransaction()
+ .add(R.id.squareContainer, innerFragment)
+ .setReorderingAllowed(false)
+ .commit()
+ executePendingTransactions()
+
+ assertThat(violation).isInstanceOf(WrongNestedHierarchyViolation::class.java)
+ assertThat(violation).hasMessageThat().contains(
+ "Attempting to nest fragment $innerFragment within the view " +
+ "of parent fragment $outerFragment via container with ID " +
+ "${R.id.squareContainer} without using parent's childFragmentManager"
+ )
+ }
+ }
+
+ @Test
+ public fun detectWrongNestedHierarchyWrongParent() {
+ var violation: Violation? = null
+ val policy = FragmentStrictMode.Policy.Builder()
+ .detectWrongNestedHierarchy()
+ .penaltyListener { violation = it }
+ .build()
+ FragmentStrictMode.defaultPolicy = policy
+
+ withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+ val fm = withActivity {
+ setContentView(R.layout.simple_container)
+ supportFragmentManager
+ }
+ val grandParent = StrictViewFragment(R.layout.scene1)
+ val parentFragment = StrictViewFragment(R.layout.scene5)
+ val childFragment = StrictViewFragment(R.layout.fragment_a)
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, grandParent)
+ .setReorderingAllowed(false)
+ .commit()
+ executePendingTransactions()
+ grandParent.childFragmentManager.beginTransaction()
+ .add(R.id.squareContainer, parentFragment)
+ .setReorderingAllowed(false)
+ .commit()
+ executePendingTransactions()
+ // Here we use the grandParent.childFragmentManager for the child
+ // fragment, though we should actually be using parentFragment.childFragmentManager
+ grandParent.childFragmentManager.beginTransaction()
+ .add(R.id.sharedElementContainer, childFragment)
+ .setReorderingAllowed(false)
+ .commit()
+ executePendingTransactions(parentFragment.childFragmentManager)
+ assertThat(violation).isInstanceOf(WrongNestedHierarchyViolation::class.java)
+ assertThat(violation).hasMessageThat().contains(
+ "Attempting to nest fragment $childFragment within the view " +
+ "of parent fragment $parentFragment via container with ID " +
+ "${R.id.sharedElementContainer} without using parent's childFragmentManager"
+ )
+ }
+ }
+
@Suppress("DEPRECATION")
@Test
public fun detectRetainInstanceUsage() {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index 22f4c63..1e0f070 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -1065,7 +1065,7 @@
* @return the locally scoped {@link Fragment} to the given view, if found
*/
@Nullable
- private static Fragment findViewFragment(@NonNull View view) {
+ static Fragment findViewFragment(@NonNull View view) {
while (view != null) {
Fragment fragment = getViewFragment(view);
if (fragment != null) {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
index 0b54927..859045b 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
@@ -860,6 +860,16 @@
}
void addViewToContainer() {
+ Fragment expectedParent = FragmentManager.findViewFragment(mFragment.mContainer);
+ Fragment actualParent = mFragment.getParentFragment();
+ // onFindViewById prevents any wrong nested hierarchies when expectedParent is null already
+ if (expectedParent != null) {
+ if (!expectedParent.equals(actualParent)) {
+ FragmentStrictMode.onWrongNestedHierarchy(mFragment, expectedParent,
+ mFragment.mContainerId);
+ }
+ }
+
// Ensure that our new Fragment is placed in the right index
// based on its relative position to Fragments already in the
// same container
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentStrictMode.kt b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentStrictMode.kt
index 1137313..04ef240 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentStrictMode.kt
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentStrictMode.kt
@@ -95,6 +95,27 @@
*/
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY)
+ fun onWrongNestedHierarchy(
+ fragment: Fragment,
+ parentFragment: Fragment,
+ containerId: Int
+ ) {
+ val violation: Violation =
+ WrongNestedHierarchyViolation(fragment, parentFragment, containerId)
+ logIfDebuggingEnabled(violation)
+ val policy = getNearestPolicy(fragment)
+ if (policy.flags.contains(Flag.DETECT_WRONG_NESTED_HIERARCHY) &&
+ shouldHandlePolicyViolation(policy, fragment.javaClass, violation.javaClass)
+ ) {
+ handlePolicyViolation(policy, violation)
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @JvmStatic
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
fun onSetRetainInstanceUsage(fragment: Fragment) {
val violation: Violation = SetRetainInstanceUsageViolation(fragment)
logIfDebuggingEnabled(violation)
@@ -284,6 +305,7 @@
PENALTY_DEATH,
DETECT_FRAGMENT_REUSE,
DETECT_FRAGMENT_TAG_USAGE,
+ DETECT_WRONG_NESTED_HIERARCHY,
DETECT_RETAIN_INSTANCE_USAGE,
DETECT_SET_USER_VISIBLE_HINT,
DETECT_TARGET_FRAGMENT_USAGE,
@@ -378,6 +400,13 @@
return this
}
+ /** Detects nested fragments that do not use the parent's childFragmentManager. */
+ @SuppressLint("BuilderSetStyle")
+ fun detectWrongNestedHierarchy(): Builder {
+ flags.add(Flag.DETECT_WRONG_NESTED_HIERARCHY)
+ return this
+ }
+
/**
* Detects calls to [Fragment.setRetainInstance] and [Fragment.getRetainInstance].
*/
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/WrongNestedHierarchyViolation.kt b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/WrongNestedHierarchyViolation.kt
new file mode 100644
index 0000000..3ec71c8
--- /dev/null
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/WrongNestedHierarchyViolation.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.fragment.app.strictmode
+
+import androidx.fragment.app.Fragment
+
+/**
+ * See [FragmentStrictMode.Policy.Builder.detectWrongNestedHierarchy].
+ */
+class WrongNestedHierarchyViolation internal constructor(
+ fragment: Fragment,
+ /**
+ * Gets the expected parent [Fragment] of the fragment causing the Violation.
+ */
+ val parentFragment: Fragment,
+ /**
+ * Gets the unique ID of the container that the [Fragment] causing
+ * the Violation would have been added to.
+ */
+ val containerId: Int
+) : Violation(
+ fragment,
+ "Attempting to nest fragment $fragment within the view " +
+ "of parent fragment $parentFragment via container with ID $containerId " +
+ "without using parent's childFragmentManager"
+)
diff --git a/glance/glance-appwidget/api/public_plus_experimental_current.txt b/glance/glance-appwidget/api/public_plus_experimental_current.txt
index b163bae..cd1190b 100644
--- a/glance/glance-appwidget/api/public_plus_experimental_current.txt
+++ b/glance/glance-appwidget/api/public_plus_experimental_current.txt
@@ -57,7 +57,7 @@
public final class CoroutineBroadcastReceiverKt {
}
- @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalGlanceRemoteViewsApi {
+ @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalGlanceRemoteViewsApi {
}
public final class GeneratedLayoutsKt {
diff --git a/glance/glance-appwidget/build.gradle b/glance/glance-appwidget/build.gradle
index c8aa9d9..1ad5b3b6 100644
--- a/glance/glance-appwidget/build.gradle
+++ b/glance/glance-appwidget/build.gradle
@@ -43,9 +43,9 @@
api(project(":glance:glance"))
api("androidx.annotation:annotation:1.1.0")
- api("androidx.compose.runtime:runtime:1.1.0-beta01")
- api("androidx.compose.ui:ui-graphics:1.1.0-beta01")
- api("androidx.compose.ui:ui-unit:1.1.0-beta01")
+ api("androidx.compose.runtime:runtime:1.1.1")
+ api("androidx.compose.ui:ui-graphics:1.1.1")
+ api("androidx.compose.ui:ui-unit:1.1.1")
implementation('androidx.core:core-ktx:1.7.0')
implementation("androidx.datastore:datastore:1.0.0")
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ExperimentalGlanceRemoteViewsApi.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ExperimentalGlanceRemoteViewsApi.kt
index c9c7d36..009e306 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ExperimentalGlanceRemoteViewsApi.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ExperimentalGlanceRemoteViewsApi.kt
@@ -17,4 +17,5 @@
package androidx.glance.appwidget
@RequiresOptIn("This API is experimental and is likely to change in the future.")
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalGlanceRemoteViewsApi
diff --git a/glance/glance-preview/api/public_plus_experimental_current.txt b/glance/glance-preview/api/public_plus_experimental_current.txt
index c536700..1fc4543 100644
--- a/glance/glance-preview/api/public_plus_experimental_current.txt
+++ b/glance/glance-preview/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.glance.preview {
- @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalGlancePreviewApi {
+ @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalGlancePreviewApi {
}
@androidx.glance.preview.ExperimentalGlancePreviewApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface GlancePreview {
diff --git a/glance/glance-preview/src/androidMain/kotlin/androidx/glance/preview/ExperimentalGlancePreviewApi.kt b/glance/glance-preview/src/androidMain/kotlin/androidx/glance/preview/ExperimentalGlancePreviewApi.kt
index b21e9d3..0a5ac68 100644
--- a/glance/glance-preview/src/androidMain/kotlin/androidx/glance/preview/ExperimentalGlancePreviewApi.kt
+++ b/glance/glance-preview/src/androidMain/kotlin/androidx/glance/preview/ExperimentalGlancePreviewApi.kt
@@ -17,4 +17,5 @@
package androidx.glance.preview
@RequiresOptIn("This API is experimental and is likely to change in the future.")
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalGlancePreviewApi
\ No newline at end of file
diff --git a/glance/glance-wear-tiles/api/public_plus_experimental_current.txt b/glance/glance-wear-tiles/api/public_plus_experimental_current.txt
index 117a296..d6febbf 100644
--- a/glance/glance-wear-tiles/api/public_plus_experimental_current.txt
+++ b/glance/glance-wear-tiles/api/public_plus_experimental_current.txt
@@ -18,7 +18,7 @@
public final class ErrorUiLayoutKt {
}
- @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalGlanceWearTilesApi {
+ @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalGlanceWearTilesApi {
}
public abstract class GlanceTileService extends androidx.wear.tiles.TileService {
diff --git a/glance/glance-wear-tiles/build.gradle b/glance/glance-wear-tiles/build.gradle
index 7c09cd6..eccfcf3 100644
--- a/glance/glance-wear-tiles/build.gradle
+++ b/glance/glance-wear-tiles/build.gradle
@@ -30,9 +30,9 @@
dependencies {
api(project(":glance:glance"))
- api("androidx.compose.runtime:runtime:1.1.0-beta01")
- api("androidx.compose.ui:ui-graphics:1.1.0-beta01")
- api("androidx.compose.ui:ui-unit:1.1.0-beta01")
+ api("androidx.compose.runtime:runtime:1.1.1")
+ api("androidx.compose.ui:ui-graphics:1.1.1")
+ api("androidx.compose.ui:ui-unit:1.1.1")
api("androidx.wear.tiles:tiles:1.0.0")
implementation(libs.kotlinStdlib)
diff --git a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/ExperimentalGlanceWearTilesApi.kt b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/ExperimentalGlanceWearTilesApi.kt
index 2587a4b..8966090 100644
--- a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/ExperimentalGlanceWearTilesApi.kt
+++ b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/ExperimentalGlanceWearTilesApi.kt
@@ -17,4 +17,5 @@
package androidx.glance.wear.tiles
@RequiresOptIn("This API is experimental and is likely to change in the future.")
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalGlanceWearTilesApi
\ No newline at end of file
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index ad6cf59..222ec6d 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -580,14 +580,12 @@
}
public final class SingleEntityTemplateData {
- ctor public SingleEntityTemplateData(optional boolean displayHeader, optional androidx.glance.template.HeaderBlock? headerBlock, optional androidx.glance.template.TextBlock? textBlock, optional androidx.glance.template.ImageBlock? imageBlock, optional androidx.glance.template.ActionBlock? actionBlock);
+ ctor public SingleEntityTemplateData(optional androidx.glance.template.HeaderBlock? headerBlock, optional androidx.glance.template.TextBlock? textBlock, optional androidx.glance.template.ImageBlock? imageBlock, optional androidx.glance.template.ActionBlock? actionBlock);
method public androidx.glance.template.ActionBlock? getActionBlock();
- method public boolean getDisplayHeader();
method public androidx.glance.template.HeaderBlock? getHeaderBlock();
method public androidx.glance.template.ImageBlock? getImageBlock();
method public androidx.glance.template.TextBlock? getTextBlock();
property public final androidx.glance.template.ActionBlock? actionBlock;
- property public final boolean displayHeader;
property public final androidx.glance.template.HeaderBlock? headerBlock;
property public final androidx.glance.template.ImageBlock? imageBlock;
property public final androidx.glance.template.TextBlock? textBlock;
diff --git a/glance/glance/api/public_plus_experimental_current.txt b/glance/glance/api/public_plus_experimental_current.txt
index ad6cf59..222ec6d 100644
--- a/glance/glance/api/public_plus_experimental_current.txt
+++ b/glance/glance/api/public_plus_experimental_current.txt
@@ -580,14 +580,12 @@
}
public final class SingleEntityTemplateData {
- ctor public SingleEntityTemplateData(optional boolean displayHeader, optional androidx.glance.template.HeaderBlock? headerBlock, optional androidx.glance.template.TextBlock? textBlock, optional androidx.glance.template.ImageBlock? imageBlock, optional androidx.glance.template.ActionBlock? actionBlock);
+ ctor public SingleEntityTemplateData(optional androidx.glance.template.HeaderBlock? headerBlock, optional androidx.glance.template.TextBlock? textBlock, optional androidx.glance.template.ImageBlock? imageBlock, optional androidx.glance.template.ActionBlock? actionBlock);
method public androidx.glance.template.ActionBlock? getActionBlock();
- method public boolean getDisplayHeader();
method public androidx.glance.template.HeaderBlock? getHeaderBlock();
method public androidx.glance.template.ImageBlock? getImageBlock();
method public androidx.glance.template.TextBlock? getTextBlock();
property public final androidx.glance.template.ActionBlock? actionBlock;
- property public final boolean displayHeader;
property public final androidx.glance.template.HeaderBlock? headerBlock;
property public final androidx.glance.template.ImageBlock? imageBlock;
property public final androidx.glance.template.TextBlock? textBlock;
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index ad6cf59..222ec6d 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -580,14 +580,12 @@
}
public final class SingleEntityTemplateData {
- ctor public SingleEntityTemplateData(optional boolean displayHeader, optional androidx.glance.template.HeaderBlock? headerBlock, optional androidx.glance.template.TextBlock? textBlock, optional androidx.glance.template.ImageBlock? imageBlock, optional androidx.glance.template.ActionBlock? actionBlock);
+ ctor public SingleEntityTemplateData(optional androidx.glance.template.HeaderBlock? headerBlock, optional androidx.glance.template.TextBlock? textBlock, optional androidx.glance.template.ImageBlock? imageBlock, optional androidx.glance.template.ActionBlock? actionBlock);
method public androidx.glance.template.ActionBlock? getActionBlock();
- method public boolean getDisplayHeader();
method public androidx.glance.template.HeaderBlock? getHeaderBlock();
method public androidx.glance.template.ImageBlock? getImageBlock();
method public androidx.glance.template.TextBlock? getTextBlock();
property public final androidx.glance.template.ActionBlock? actionBlock;
- property public final boolean displayHeader;
property public final androidx.glance.template.HeaderBlock? headerBlock;
property public final androidx.glance.template.ImageBlock? imageBlock;
property public final androidx.glance.template.TextBlock? textBlock;
diff --git a/glance/glance/build.gradle b/glance/glance/build.gradle
index 3d96ae5..ba94622 100644
--- a/glance/glance/build.gradle
+++ b/glance/glance/build.gradle
@@ -30,9 +30,9 @@
dependencies {
api("androidx.annotation:annotation:1.2.0")
- api("androidx.compose.runtime:runtime:1.1.0-beta01")
- api("androidx.compose.ui:ui-graphics:1.1.0-beta01")
- api("androidx.compose.ui:ui-unit:1.1.0-beta01")
+ api("androidx.compose.runtime:runtime:1.1.1")
+ api("androidx.compose.ui:ui-graphics:1.1.1")
+ api("androidx.compose.ui:ui-unit:1.1.1")
api("androidx.datastore:datastore-core:1.0.0")
api("androidx.datastore:datastore-preferences-core:1.0.0")
api("androidx.datastore:datastore-preferences:1.0.0")
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/template/SingleEntityTemplateData.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/template/SingleEntityTemplateData.kt
index c4a2b5f..a0a5308 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/template/SingleEntityTemplateData.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/template/SingleEntityTemplateData.kt
@@ -20,7 +20,6 @@
* The semantic data required to build Single Entity Template layouts. The template allows for
* a header, text section with up to three text items, main image, and single action button.
*
- * @param displayHeader True if the glanceable header should be displayed
* @param headerBlock The header block of the entity by [HeaderBlock].
* @param textBlock The text block for up to three types of texts for the entity.
* @param imageBlock The image block for the entity main image by [ImageBlock].
@@ -28,16 +27,13 @@
*/
class SingleEntityTemplateData(
- val displayHeader: Boolean = true,
val headerBlock: HeaderBlock? = null,
val textBlock: TextBlock? = null,
val imageBlock: ImageBlock? = null,
val actionBlock: ActionBlock? = null,
) {
-
override fun hashCode(): Int {
- var result = displayHeader.hashCode()
- result = 31 * result + (headerBlock?.hashCode() ?: 0)
+ var result = headerBlock?.hashCode() ?: 0
result = 31 * result + (textBlock?.hashCode() ?: 0)
result = 31 * result + (imageBlock?.hashCode() ?: 0)
result = 31 * result + (actionBlock?.hashCode() ?: 0)
@@ -50,7 +46,6 @@
other as SingleEntityTemplateData
- if (displayHeader != other.displayHeader) return false
if (headerBlock != other.headerBlock) return false
if (textBlock != other.textBlock) return false
if (imageBlock != other.imageBlock) return false
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
index 9650598..8f62713 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
@@ -30,7 +30,6 @@
import androidx.graphics.opengl.egl.EGLSpec
import androidx.graphics.opengl.egl.EGLVersion
import androidx.graphics.opengl.egl.supportsNativeAndroidFence
-import androidx.hardware.SyncFence
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -381,13 +380,6 @@
}
@Test
- fun testExtractSyncFenceFd() {
- val fileDescriptor = 7
- val syncFence = SyncFence(7)
- assertEquals(fileDescriptor, JniBindings.nExtractFenceFd(syncFence))
- }
-
- @Test
fun testTransactionSetBuffer_nullFence() {
val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
.moveToState(
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlWrapperTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlWrapperTest.kt
index c29a456..530c753 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlWrapperTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlWrapperTest.kt
@@ -25,7 +25,6 @@
import android.view.Surface
import android.view.SurfaceControl
import android.view.SurfaceHolder
-import androidx.hardware.SyncFence
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -410,13 +409,6 @@
}
@Test
- fun testExtractSyncFenceFd() {
- val fileDescriptor = 7
- val syncFence = SyncFence(7)
- assertEquals(fileDescriptor, JniBindings.nExtractFenceFd(syncFence))
- }
-
- @Test
fun testTransactionSetVisibility_show() {
val listener = TransactionOnCompleteListener()
val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceTest.kt
index 05b449d..4023ed0 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceTest.kt
@@ -17,6 +17,7 @@
package androidx.hardware
import android.os.Build
+import androidx.graphics.surface.JniBindings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
@@ -30,6 +31,23 @@
@SmallTest
class SyncFenceTest {
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+ @Test
+ fun testDupSyncFenceFd() {
+ val fileDescriptor = 7
+ val syncFence = SyncFence(7)
+ // If the file descriptor is valid dup'ing it should return a different fd
+ Assert.assertNotEquals(fileDescriptor, JniBindings.nDupFenceFd(syncFence))
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+ @Test
+ fun testDupSyncFenceFdWhenInvalid() {
+ // If the fence is invalid there should be no attempt to dup the fd it and -1
+ // should be returned
+ Assert.assertEquals(-1, JniBindings.nDupFenceFd(SyncFence(-1)))
+ }
+
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun testSignalTimeInvalid() {
diff --git a/graphics/graphics-core/src/main/cpp/graphics-core.cpp b/graphics/graphics-core/src/main/cpp/graphics-core.cpp
index ed2243e..98bc848 100644
--- a/graphics/graphics-core/src/main/cpp/graphics-core.cpp
+++ b/graphics/graphics-core/src/main/cpp/graphics-core.cpp
@@ -155,6 +155,12 @@
jmethodID onCommit{};
} gTransactionCommittedListenerClassInfo;
+static struct {
+ bool CLASS_INFO_INITIALIZED = false;
+ jclass clazz{};
+ jmethodID dupeFileDescriptor{};
+} gSyncFenceClassInfo;
+
#define NANO_SECONDS 1000000000LL
int64_t getSystemTime() {
@@ -297,20 +303,29 @@
}
}
-int extract_fence_fd(JNIEnv *env, jobject syncFence) {
- jclass sfClass = env->GetObjectClass(syncFence);
- jfieldID fid = env->GetFieldID(sfClass, "fd", "I");
- return env->GetIntField(syncFence, fid);
+void setupSyncFenceClassInfo(JNIEnv *env) {
+ if (!gSyncFenceClassInfo.CLASS_INFO_INITIALIZED) {
+ jclass syncFenceClazz = env->FindClass("androidx/hardware/SyncFence");
+ gSyncFenceClassInfo.clazz = static_cast<jclass>(env->NewGlobalRef(syncFenceClazz));
+ gSyncFenceClassInfo.dupeFileDescriptor =
+ env->GetMethodID(gSyncFenceClassInfo.clazz, "dupeFileDescriptor", "()I");
+ gSyncFenceClassInfo.CLASS_INFO_INITIALIZED = true;
+ }
+}
+
+int dup_fence_fd(JNIEnv *env, jobject syncFence) {
+ setupSyncFenceClassInfo(env);
+ return env->CallIntMethod(syncFence, gSyncFenceClassInfo.dupeFileDescriptor);
}
/* Helper method to extract the SyncFence file descriptor
*/
extern "C"
JNIEXPORT jint JNICALL
-Java_androidx_graphics_surface_JniBindings_00024Companion_nExtractFenceFd(JNIEnv *env,
- jobject thiz,
- jobject syncFence) {
- return extract_fence_fd(env, syncFence);
+Java_androidx_graphics_surface_JniBindings_00024Companion_nDupFenceFd(JNIEnv *env,
+ jobject thiz,
+ jobject syncFence) {
+ return dup_fence_fd(env, syncFence);
}
extern "C"
@@ -325,7 +340,7 @@
auto transaction = reinterpret_cast<ASurfaceTransaction *>(surfaceTransaction);
auto sc = reinterpret_cast<ASurfaceControl *>(surfaceControl);
auto hardwareBuffer = AHardwareBuffer_fromHardwareBuffer(env, hBuffer);
- auto fence_fd = extract_fence_fd(env, syncFence);
+ auto fence_fd = dup_fence_fd(env, syncFence);
ASurfaceTransaction_setBuffer(transaction, sc, hardwareBuffer, fence_fd);
}
}
@@ -500,4 +515,10 @@
auto src = ARect{0, 0, bufferWidth, bufferHeight};
auto dest = ARect{0, 0, dstWidth, dstHeight};
ASurfaceTransaction_setGeometry(st, sc, src, dest, transformation);
+}
+
+extern "C"
+JNIEXPORT jint JNICALL
+Java_androidx_hardware_SyncFence_nDup(JNIEnv *env, jobject thiz, jint fd) {
+ return static_cast<jint>(dup(static_cast<int>(fd)));
}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
index 6c30b4c..b191913 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
@@ -560,6 +560,7 @@
transaction
)
transaction.commit()
+ syncFenceCompat?.close()
}
},
mFrontBufferSyncStrategy
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt
index b9af9e7..27dbd7b 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt
@@ -52,7 +52,7 @@
listener: SurfaceControlCompat.TransactionCommittedListener
)
- external fun nExtractFenceFd(
+ external fun nDupFenceFd(
syncFence: SyncFence
): Int
diff --git a/graphics/graphics-core/src/main/java/androidx/hardware/SyncFence.kt b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFence.kt
index 3963976..97c6b74 100644
--- a/graphics/graphics-core/src/main/java/androidx/hardware/SyncFence.kt
+++ b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFence.kt
@@ -19,6 +19,8 @@
import android.os.Build
import androidx.annotation.RequiresApi
import java.util.concurrent.TimeUnit
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
/**
* A SyncFence represents a synchronization primitive which signals when hardware buffers have
@@ -33,11 +35,15 @@
@RequiresApi(Build.VERSION_CODES.KITKAT)
class SyncFence(private var fd: Int) : AutoCloseable {
+ private val fenceLock = ReentrantLock()
+
/**
* Checks if the SyncFence object is valid.
* @return `true` if it is valid, `false` otherwise
*/
- fun isValid(): Boolean = fd != -1
+ fun isValid(): Boolean = fenceLock.withLock {
+ fd != -1
+ }
/**
* Returns the time that the fence signaled in the [CLOCK_MONOTONIC] time domain.
@@ -45,12 +51,22 @@
*/
// Relies on NDK APIs sync_file_info/sync_file_info_free which were introduced in API level 26
@RequiresApi(Build.VERSION_CODES.O)
- fun getSignalTime(): Long =
+ fun getSignalTime(): Long = fenceLock.withLock {
if (isValid()) {
nGetSignalTime(fd)
} else {
SIGNAL_TIME_INVALID
}
+ }
+
+ // Accessed through JNI to obtain the dup'ed file descriptor in a thread safe manner
+ private fun dupeFileDescriptor(): Int = fenceLock.withLock {
+ return if (isValid()) {
+ nDup(fd)
+ } else {
+ -1
+ }
+ }
/**
* Waits for a SyncFence to signal for up to the [timeoutNanos] duration. An invalid SyncFence,
@@ -62,17 +78,19 @@
* @return `true` if the fence signaled or is not valid, `false` otherwise
*/
fun await(timeoutNanos: Long): Boolean {
- if (isValid()) {
- val timeout: Int
- if (timeoutNanos < 0) {
- timeout = -1
+ fenceLock.withLock {
+ if (isValid()) {
+ val timeout: Int
+ if (timeoutNanos < 0) {
+ timeout = -1
+ } else {
+ timeout = TimeUnit.NANOSECONDS.toMillis(timeoutNanos).toInt()
+ }
+ return nWait(fd, timeout)
} else {
- timeout = TimeUnit.NANOSECONDS.toMillis(timeoutNanos).toInt()
+ // invalid file descriptors will always return true
+ return true
}
- return nWait(fd, timeout)
- } else {
- // invalid file descriptors will always return true
- return true
}
}
@@ -89,8 +107,12 @@
* is subsequent calls to [isValid] will return `false`
*/
override fun close() {
- nClose(fd)
- fd = -1
+ fenceLock.withLock {
+ if (isValid()) {
+ nClose(fd)
+ fd = -1
+ }
+ }
}
protected fun finalize() {
@@ -104,6 +126,12 @@
private external fun nGetSignalTime(fd: Int): Long
private external fun nClose(fd: Int)
+ /**
+ * Dup the provided file descriptor, this method requires the caller to acquire the corresponding
+ * [fenceLock] before invoking
+ */
+ private external fun nDup(fd: Int): Int
+
companion object {
/**
diff --git a/health/health-services-client/api/1.0.0-beta02.txt b/health/health-services-client/api/1.0.0-beta02.txt
index 590e802..d65cfd6 100644
--- a/health/health-services-client/api/1.0.0-beta02.txt
+++ b/health/health-services-client/api/1.0.0-beta02.txt
@@ -17,6 +17,7 @@
method public void setUpdateCallback(androidx.health.services.client.ExerciseUpdateCallback callback);
method public void setUpdateCallback(java.util.concurrent.Executor executor, androidx.health.services.client.ExerciseUpdateCallback callback);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
+ method public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void> updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig);
}
public final class ExerciseClientExtensionKt {
@@ -33,6 +34,7 @@
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+ method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? updateExerciseTypeConfig(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
}
public interface ExerciseUpdateCallback {
@@ -308,12 +310,17 @@
}
public final class ExerciseConfig {
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams);
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled);
method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
method public android.os.Bundle getExerciseParams();
method public androidx.health.services.client.data.ExerciseType getExerciseType();
+ method public androidx.health.services.client.data.ExerciseTypeConfig? getExerciseTypeConfig();
method public float getSwimmingPoolLengthMeters();
method public boolean isAutoPauseAndResumeEnabled();
method public boolean isGpsEnabled();
@@ -321,6 +328,7 @@
property public final java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals;
property public final android.os.Bundle exerciseParams;
property public final androidx.health.services.client.data.ExerciseType exerciseType;
+ property public final androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig;
property public final boolean isAutoPauseAndResumeEnabled;
property public final boolean isGpsEnabled;
property public final float swimmingPoolLengthMeters;
@@ -334,6 +342,7 @@
method public androidx.health.services.client.data.ExerciseConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseGoals(java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
+ method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseTypeConfig(androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLengthMeters(float swimmingPoolLength);
@@ -556,6 +565,17 @@
property public final boolean supportsAutoPauseAndResume;
}
+ public final class ExerciseTypeConfig {
+ method public static androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+ method public int getGolfShotTrackingPlaceInfo();
+ property public final int golfShotTrackingPlaceInfo;
+ field public static final androidx.health.services.client.data.ExerciseTypeConfig.Companion Companion;
+ }
+
+ public static final class ExerciseTypeConfig.Companion {
+ method public androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+ }
+
public final class ExerciseUpdate {
method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.IntervalDataPoint<?> dataPoint);
method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.SampleDataPoint<?> dataPoint);
diff --git a/health/health-services-client/api/api_lint.ignore b/health/health-services-client/api/api_lint.ignore
index 4b2826d..844c583 100644
--- a/health/health-services-client/api/api_lint.ignore
+++ b/health/health-services-client/api/api_lint.ignore
@@ -1,4 +1,8 @@
// Baseline format: 1.0
+DocumentExceptions: androidx.health.services.client.ExerciseClient#updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig):
+ Method ExerciseClient.updateExerciseTypeConfigAsync appears to be throwing kotlin.NotImplementedError; this should be listed in the documentation; see https://android.github.io/kotlin-guides/interop.html#document-exceptions
+
+
ExecutorRegistration: androidx.health.services.client.ExerciseClient#clearUpdateCallbackAsync(androidx.health.services.client.ExerciseUpdateCallback):
Registration methods should have overload that accepts delivery Executor: `clearUpdateCallbackAsync`
@@ -7,8 +11,6 @@
Invalid nullability on method `onBind` return. Overrides of unannotated super method cannot be Nullable.
InvalidNullabilityOverride: androidx.health.services.client.PassiveListenerService#onBind(android.content.Intent) parameter #0:
Invalid nullability on parameter `intent` in method `onBind`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.health.services.client.VersionApiService#onBind(android.content.Intent):
- Invalid nullability on method `onBind` return. Overrides of unannotated super method cannot be Nullable.
PairedRegistration: androidx.health.services.client.MeasureClient#registerMeasureCallback(androidx.health.services.client.data.DeltaDataType<?,?>, androidx.health.services.client.MeasureCallback):
diff --git a/health/health-services-client/api/current.txt b/health/health-services-client/api/current.txt
index 590e802..d65cfd6 100644
--- a/health/health-services-client/api/current.txt
+++ b/health/health-services-client/api/current.txt
@@ -17,6 +17,7 @@
method public void setUpdateCallback(androidx.health.services.client.ExerciseUpdateCallback callback);
method public void setUpdateCallback(java.util.concurrent.Executor executor, androidx.health.services.client.ExerciseUpdateCallback callback);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
+ method public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void> updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig);
}
public final class ExerciseClientExtensionKt {
@@ -33,6 +34,7 @@
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+ method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? updateExerciseTypeConfig(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
}
public interface ExerciseUpdateCallback {
@@ -308,12 +310,17 @@
}
public final class ExerciseConfig {
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams);
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled);
method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
method public android.os.Bundle getExerciseParams();
method public androidx.health.services.client.data.ExerciseType getExerciseType();
+ method public androidx.health.services.client.data.ExerciseTypeConfig? getExerciseTypeConfig();
method public float getSwimmingPoolLengthMeters();
method public boolean isAutoPauseAndResumeEnabled();
method public boolean isGpsEnabled();
@@ -321,6 +328,7 @@
property public final java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals;
property public final android.os.Bundle exerciseParams;
property public final androidx.health.services.client.data.ExerciseType exerciseType;
+ property public final androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig;
property public final boolean isAutoPauseAndResumeEnabled;
property public final boolean isGpsEnabled;
property public final float swimmingPoolLengthMeters;
@@ -334,6 +342,7 @@
method public androidx.health.services.client.data.ExerciseConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseGoals(java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
+ method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseTypeConfig(androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLengthMeters(float swimmingPoolLength);
@@ -556,6 +565,17 @@
property public final boolean supportsAutoPauseAndResume;
}
+ public final class ExerciseTypeConfig {
+ method public static androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+ method public int getGolfShotTrackingPlaceInfo();
+ property public final int golfShotTrackingPlaceInfo;
+ field public static final androidx.health.services.client.data.ExerciseTypeConfig.Companion Companion;
+ }
+
+ public static final class ExerciseTypeConfig.Companion {
+ method public androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+ }
+
public final class ExerciseUpdate {
method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.IntervalDataPoint<?> dataPoint);
method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.SampleDataPoint<?> dataPoint);
diff --git a/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt b/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt
index 590e802..d65cfd6 100644
--- a/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt
+++ b/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt
@@ -17,6 +17,7 @@
method public void setUpdateCallback(androidx.health.services.client.ExerciseUpdateCallback callback);
method public void setUpdateCallback(java.util.concurrent.Executor executor, androidx.health.services.client.ExerciseUpdateCallback callback);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
+ method public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void> updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig);
}
public final class ExerciseClientExtensionKt {
@@ -33,6 +34,7 @@
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+ method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? updateExerciseTypeConfig(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
}
public interface ExerciseUpdateCallback {
@@ -308,12 +310,17 @@
}
public final class ExerciseConfig {
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams);
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled);
method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
method public android.os.Bundle getExerciseParams();
method public androidx.health.services.client.data.ExerciseType getExerciseType();
+ method public androidx.health.services.client.data.ExerciseTypeConfig? getExerciseTypeConfig();
method public float getSwimmingPoolLengthMeters();
method public boolean isAutoPauseAndResumeEnabled();
method public boolean isGpsEnabled();
@@ -321,6 +328,7 @@
property public final java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals;
property public final android.os.Bundle exerciseParams;
property public final androidx.health.services.client.data.ExerciseType exerciseType;
+ property public final androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig;
property public final boolean isAutoPauseAndResumeEnabled;
property public final boolean isGpsEnabled;
property public final float swimmingPoolLengthMeters;
@@ -334,6 +342,7 @@
method public androidx.health.services.client.data.ExerciseConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseGoals(java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
+ method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseTypeConfig(androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLengthMeters(float swimmingPoolLength);
@@ -556,6 +565,17 @@
property public final boolean supportsAutoPauseAndResume;
}
+ public final class ExerciseTypeConfig {
+ method public static androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+ method public int getGolfShotTrackingPlaceInfo();
+ property public final int golfShotTrackingPlaceInfo;
+ field public static final androidx.health.services.client.data.ExerciseTypeConfig.Companion Companion;
+ }
+
+ public static final class ExerciseTypeConfig.Companion {
+ method public androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+ }
+
public final class ExerciseUpdate {
method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.IntervalDataPoint<?> dataPoint);
method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.SampleDataPoint<?> dataPoint);
diff --git a/health/health-services-client/api/public_plus_experimental_current.txt b/health/health-services-client/api/public_plus_experimental_current.txt
index 590e802..d65cfd6 100644
--- a/health/health-services-client/api/public_plus_experimental_current.txt
+++ b/health/health-services-client/api/public_plus_experimental_current.txt
@@ -17,6 +17,7 @@
method public void setUpdateCallback(androidx.health.services.client.ExerciseUpdateCallback callback);
method public void setUpdateCallback(java.util.concurrent.Executor executor, androidx.health.services.client.ExerciseUpdateCallback callback);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
+ method public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void> updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig);
}
public final class ExerciseClientExtensionKt {
@@ -33,6 +34,7 @@
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+ method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? updateExerciseTypeConfig(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
}
public interface ExerciseUpdateCallback {
@@ -308,12 +310,17 @@
}
public final class ExerciseConfig {
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams);
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled);
method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
method public android.os.Bundle getExerciseParams();
method public androidx.health.services.client.data.ExerciseType getExerciseType();
+ method public androidx.health.services.client.data.ExerciseTypeConfig? getExerciseTypeConfig();
method public float getSwimmingPoolLengthMeters();
method public boolean isAutoPauseAndResumeEnabled();
method public boolean isGpsEnabled();
@@ -321,6 +328,7 @@
property public final java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals;
property public final android.os.Bundle exerciseParams;
property public final androidx.health.services.client.data.ExerciseType exerciseType;
+ property public final androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig;
property public final boolean isAutoPauseAndResumeEnabled;
property public final boolean isGpsEnabled;
property public final float swimmingPoolLengthMeters;
@@ -334,6 +342,7 @@
method public androidx.health.services.client.data.ExerciseConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseGoals(java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
+ method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseTypeConfig(androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLengthMeters(float swimmingPoolLength);
@@ -556,6 +565,17 @@
property public final boolean supportsAutoPauseAndResume;
}
+ public final class ExerciseTypeConfig {
+ method public static androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+ method public int getGolfShotTrackingPlaceInfo();
+ property public final int golfShotTrackingPlaceInfo;
+ field public static final androidx.health.services.client.data.ExerciseTypeConfig.Companion Companion;
+ }
+
+ public static final class ExerciseTypeConfig.Companion {
+ method public androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+ }
+
public final class ExerciseUpdate {
method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.IntervalDataPoint<?> dataPoint);
method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.SampleDataPoint<?> dataPoint);
diff --git a/health/health-services-client/api/restricted_1.0.0-beta02.txt b/health/health-services-client/api/restricted_1.0.0-beta02.txt
index 590e802..d65cfd6 100644
--- a/health/health-services-client/api/restricted_1.0.0-beta02.txt
+++ b/health/health-services-client/api/restricted_1.0.0-beta02.txt
@@ -17,6 +17,7 @@
method public void setUpdateCallback(androidx.health.services.client.ExerciseUpdateCallback callback);
method public void setUpdateCallback(java.util.concurrent.Executor executor, androidx.health.services.client.ExerciseUpdateCallback callback);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
+ method public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void> updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig);
}
public final class ExerciseClientExtensionKt {
@@ -33,6 +34,7 @@
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+ method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? updateExerciseTypeConfig(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
}
public interface ExerciseUpdateCallback {
@@ -308,12 +310,17 @@
}
public final class ExerciseConfig {
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams);
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled);
method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
method public android.os.Bundle getExerciseParams();
method public androidx.health.services.client.data.ExerciseType getExerciseType();
+ method public androidx.health.services.client.data.ExerciseTypeConfig? getExerciseTypeConfig();
method public float getSwimmingPoolLengthMeters();
method public boolean isAutoPauseAndResumeEnabled();
method public boolean isGpsEnabled();
@@ -321,6 +328,7 @@
property public final java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals;
property public final android.os.Bundle exerciseParams;
property public final androidx.health.services.client.data.ExerciseType exerciseType;
+ property public final androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig;
property public final boolean isAutoPauseAndResumeEnabled;
property public final boolean isGpsEnabled;
property public final float swimmingPoolLengthMeters;
@@ -334,6 +342,7 @@
method public androidx.health.services.client.data.ExerciseConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseGoals(java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
+ method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseTypeConfig(androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLengthMeters(float swimmingPoolLength);
@@ -556,6 +565,17 @@
property public final boolean supportsAutoPauseAndResume;
}
+ public final class ExerciseTypeConfig {
+ method public static androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+ method public int getGolfShotTrackingPlaceInfo();
+ property public final int golfShotTrackingPlaceInfo;
+ field public static final androidx.health.services.client.data.ExerciseTypeConfig.Companion Companion;
+ }
+
+ public static final class ExerciseTypeConfig.Companion {
+ method public androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+ }
+
public final class ExerciseUpdate {
method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.IntervalDataPoint<?> dataPoint);
method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.SampleDataPoint<?> dataPoint);
diff --git a/health/health-services-client/api/restricted_current.txt b/health/health-services-client/api/restricted_current.txt
index 590e802..d65cfd6 100644
--- a/health/health-services-client/api/restricted_current.txt
+++ b/health/health-services-client/api/restricted_current.txt
@@ -17,6 +17,7 @@
method public void setUpdateCallback(androidx.health.services.client.ExerciseUpdateCallback callback);
method public void setUpdateCallback(java.util.concurrent.Executor executor, androidx.health.services.client.ExerciseUpdateCallback callback);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
+ method public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void> updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig);
}
public final class ExerciseClientExtensionKt {
@@ -33,6 +34,7 @@
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+ method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? updateExerciseTypeConfig(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
}
public interface ExerciseUpdateCallback {
@@ -308,12 +310,17 @@
}
public final class ExerciseConfig {
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams);
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
+ ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled);
method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
method public android.os.Bundle getExerciseParams();
method public androidx.health.services.client.data.ExerciseType getExerciseType();
+ method public androidx.health.services.client.data.ExerciseTypeConfig? getExerciseTypeConfig();
method public float getSwimmingPoolLengthMeters();
method public boolean isAutoPauseAndResumeEnabled();
method public boolean isGpsEnabled();
@@ -321,6 +328,7 @@
property public final java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals;
property public final android.os.Bundle exerciseParams;
property public final androidx.health.services.client.data.ExerciseType exerciseType;
+ property public final androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig;
property public final boolean isAutoPauseAndResumeEnabled;
property public final boolean isGpsEnabled;
property public final float swimmingPoolLengthMeters;
@@ -334,6 +342,7 @@
method public androidx.health.services.client.data.ExerciseConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseGoals(java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
+ method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseTypeConfig(androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLengthMeters(float swimmingPoolLength);
@@ -556,6 +565,17 @@
property public final boolean supportsAutoPauseAndResume;
}
+ public final class ExerciseTypeConfig {
+ method public static androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+ method public int getGolfShotTrackingPlaceInfo();
+ property public final int golfShotTrackingPlaceInfo;
+ field public static final androidx.health.services.client.data.ExerciseTypeConfig.Companion Companion;
+ }
+
+ public static final class ExerciseTypeConfig.Companion {
+ method public androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+ }
+
public final class ExerciseUpdate {
method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.IntervalDataPoint<?> dataPoint);
method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.SampleDataPoint<?> dataPoint);
diff --git a/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/IExerciseApiService.aidl b/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/IExerciseApiService.aidl
index c4f225e..87f3b3e 100644
--- a/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/IExerciseApiService.aidl
+++ b/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/IExerciseApiService.aidl
@@ -25,12 +25,13 @@
import androidx.health.services.client.impl.request.ExerciseGoalRequest;
import androidx.health.services.client.impl.request.PrepareExerciseRequest;
import androidx.health.services.client.impl.request.StartExerciseRequest;
+import androidx.health.services.client.impl.request.UpdateExerciseTypeConfigRequest;
import androidx.health.services.client.impl.response.ExerciseCapabilitiesResponse;
/**
* Interface to make ipc calls for health services exercise api.
*
- * The next method added to the interface should use ID: 15
+ * The next method added to the interface should use ID: 17
* (this id needs to be incremented for each added method)
*
* @hide
@@ -41,7 +42,7 @@
* method is added.
*
*/
- const int API_VERSION = 1;
+ const int API_VERSION = 3;
/**
* Returns version of this AIDL interface.
@@ -127,4 +128,11 @@
/** Method to flush data metrics. */
void flushExercise(in FlushRequest request, in IStatusCallback statusCallback) = 12;
+
+ /**
+ * Handles a given request to update an exercise.
+
+ * <p>Added in API version 3.
+ */
+ void updateExerciseTypeConfigForActiveExercise(in UpdateExerciseTypeConfigRequest updateExerciseTypeConfigRequest, IStatusCallback statuscallback) = 16;
}
diff --git a/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.aidl b/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.aidl
new file mode 100644
index 0000000..3b3a016
--- /dev/null
+++ b/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.services.client.impl.request;
+
+/** @hide */
+parcelable UpdateExerciseTypeConfigRequest;
\ No newline at end of file
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
index fd572f3..311cdcd 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
@@ -25,6 +25,7 @@
import androidx.health.services.client.data.ExerciseState
import androidx.health.services.client.data.ExerciseEndReason
import androidx.health.services.client.data.ExerciseType
+import androidx.health.services.client.data.ExerciseTypeConfig
import androidx.health.services.client.data.ExerciseUpdate
import androidx.health.services.client.data.WarmUpConfig
import com.google.common.util.concurrent.ListenableFuture
@@ -281,4 +282,24 @@
* @return a [ListenableFuture] containing the [ExerciseCapabilities] for this device
*/
public fun getCapabilitiesAsync(): ListenableFuture<ExerciseCapabilities>
+
+ /**
+ * Updates the configurable exercise type attributes for the current exercise.
+ *
+ * This can be used to update the configurable attributes for the ongoing exercise, as defined
+ * in [ExerciseTypeConfig]. Minimum Exercise API version for this function is 3.
+ *
+ * @param exerciseTypeConfig a configuration containing the new values for the configurable
+ * attributes
+ * @return a [ListenableFuture] that completes when the configuration has been updated.
+ * @throws [NotImplementedError] if there is an existing [ExerciseClient] that has not
+ * implemented this method. Developers should use [ServiceBackedExerciseClient], which is
+ * guaranteed to have this method implemented.
+ * @see ServiceBackedExerciseClient
+ */
+ public fun updateExerciseTypeConfigAsync(
+ exerciseTypeConfig: ExerciseTypeConfig
+ ): ListenableFuture<Void> {
+ throw NotImplementedError()
+ }
}
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClientExtension.kt b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClientExtension.kt
index 2e54b70..13106ac 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClientExtension.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClientExtension.kt
@@ -25,6 +25,7 @@
import androidx.health.services.client.data.ExerciseInfo
import androidx.health.services.client.data.ExerciseState
import androidx.health.services.client.data.ExerciseType
+import androidx.health.services.client.data.ExerciseTypeConfig
import androidx.health.services.client.data.ExerciseUpdate
import androidx.health.services.client.data.WarmUpConfig
@@ -247,4 +248,20 @@
* @throws [android.os.RemoteException] if Health Service fails to process the call
*/
@kotlin.jvm.Throws(android.os.RemoteException::class)
-public suspend fun ExerciseClient.getCapabilities() = getCapabilitiesAsync().await()
\ No newline at end of file
+public suspend fun ExerciseClient.getCapabilities() = getCapabilitiesAsync().await()
+
+/**
+ * Updates the configurable exercise type attributes for the current exercise.
+ *
+ * This can be used to update the configurable attributes for the ongoing exercise, as defined
+ * in [ExerciseTypeConfig].
+ *
+ * @param exerciseTypeConfig a configuration containing the new values for the configurable
+ * attributes
+ *
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.updateExerciseTypeConfig(
+ exerciseTypeConfig: ExerciseTypeConfig
+) = updateExerciseTypeConfigAsync(exerciseTypeConfig).await()
\ No newline at end of file
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseConfig.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseConfig.kt
index 57ee614..a125add 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseConfig.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseConfig.kt
@@ -39,9 +39,11 @@
* on-going exercise which can be used to pre-populate a new exercise.
* @property swimmingPoolLengthMeters length (in meters) of the swimming pool, or 0 if not relevant to
* this exercise
+ * @property exerciseTypeConfig [ExerciseTypeConfig] containing attributes which may be
+ * modified after the exercise has started
*/
@Suppress("ParcelCreator")
-class ExerciseConfig(
+class ExerciseConfig @JvmOverloads constructor(
val exerciseType: ExerciseType,
val dataTypes: Set<DataType<*, *>>,
val isAutoPauseAndResumeEnabled: Boolean,
@@ -49,6 +51,7 @@
val exerciseGoals: List<ExerciseGoal<*>> = listOf(),
val exerciseParams: Bundle = Bundle(),
@FloatRange(from = 0.0) val swimmingPoolLengthMeters: Float = SWIMMING_POOL_LENGTH_UNSPECIFIED,
+ val exerciseTypeConfig: ExerciseTypeConfig? = null
) {
internal constructor(
@@ -65,7 +68,10 @@
proto.swimmingPoolLength
} else {
SWIMMING_POOL_LENGTH_UNSPECIFIED
- }
+ },
+ if (proto.hasExerciseTypeConfig()) {
+ ExerciseTypeConfig(proto.exerciseTypeConfig)
+ } else null
)
init {
@@ -98,6 +104,7 @@
private var exerciseGoals: List<ExerciseGoal<*>> = emptyList()
private var exerciseParams: Bundle = Bundle.EMPTY
private var swimmingPoolLength: Float = SWIMMING_POOL_LENGTH_UNSPECIFIED
+ private var exerciseTypeConfig: ExerciseTypeConfig? = null
/**
* Sets the requested [DataType]s that should be tracked during this exercise. If not
@@ -171,11 +178,22 @@
/** Sets the swimming pool length (in m). */
@Suppress("MissingGetterMatchingBuilder")
- public fun setSwimmingPoolLengthMeters(swimmingPoolLength: Float): Builder {
+ fun setSwimmingPoolLengthMeters(swimmingPoolLength: Float): Builder {
this.swimmingPoolLength = swimmingPoolLength
return this
}
+ /**
+ * Sets the [ExerciseTypeConfig] which are configurable attributes for the ongoing exercise.
+ *
+ * @param exerciseTypeConfig [ExerciseTypeConfig] specifying active exercise type
+ * configurations
+ */
+ fun setExerciseTypeConfig(exerciseTypeConfig: ExerciseTypeConfig?): Builder {
+ this.exerciseTypeConfig = exerciseTypeConfig
+ return this
+ }
+
/** Returns the built [ExerciseConfig]. */
fun build(): ExerciseConfig {
return ExerciseConfig(
@@ -185,7 +203,8 @@
isGpsEnabled,
exerciseGoals,
exerciseParams,
- swimmingPoolLength
+ swimmingPoolLength,
+ exerciseTypeConfig
)
}
}
@@ -197,19 +216,24 @@
"isAutoPauseAndResumeEnabled=$isAutoPauseAndResumeEnabled, " +
"isGpsEnabled=$isGpsEnabled, " +
"exerciseGoals=$exerciseGoals, " +
- "swimmingPoolLengthMeters=$swimmingPoolLengthMeters)"
+ "swimmingPoolLengthMeters=$swimmingPoolLengthMeters, " +
+ "exerciseTypeConfig=$exerciseTypeConfig)"
- internal fun toProto(): DataProto.ExerciseConfig =
- DataProto.ExerciseConfig.newBuilder()
- .setExerciseType(exerciseType.toProto())
- .addAllDataTypes(dataTypes.filter { !it.isAggregate }.map { it.proto })
- .addAllAggregateDataTypes(dataTypes.filter { it.isAggregate }.map { it.proto })
- .setIsAutoPauseAndResumeEnabled(isAutoPauseAndResumeEnabled)
- .setIsGpsUsageEnabled(isGpsEnabled)
- .addAllExerciseGoals(exerciseGoals.map { it.proto })
- .setExerciseParams(BundlesUtil.toProto(exerciseParams))
- .setSwimmingPoolLength(swimmingPoolLengthMeters)
- .build()
+ internal fun toProto(): DataProto.ExerciseConfig {
+ val builder = DataProto.ExerciseConfig.newBuilder()
+ .setExerciseType(exerciseType.toProto())
+ .addAllDataTypes(dataTypes.filter { !it.isAggregate }.map { it.proto })
+ .addAllAggregateDataTypes(dataTypes.filter { it.isAggregate }.map { it.proto })
+ .setIsAutoPauseAndResumeEnabled(isAutoPauseAndResumeEnabled)
+ .setIsGpsUsageEnabled(isGpsEnabled)
+ .addAllExerciseGoals(exerciseGoals.map { it.proto })
+ .setExerciseParams(BundlesUtil.toProto(exerciseParams))
+ .setSwimmingPoolLength(swimmingPoolLengthMeters)
+ if (exerciseTypeConfig != null) {
+ builder.exerciseTypeConfig = exerciseTypeConfig.toProto()
+ }
+ return builder.build()
+ }
companion object {
/**
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseTypeConfig.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseTypeConfig.kt
new file mode 100644
index 0000000..3d643b7
--- /dev/null
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseTypeConfig.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.services.client.data
+
+import androidx.health.services.client.data.GolfShotTrackingPlaceInfo.Companion.toProto
+import androidx.health.services.client.proto.DataProto
+
+/**
+ * Configuration attributes for a specific exercise type that may be modified after the exercise has
+ * started.
+ *
+ * @property golfShotTrackingPlaceInfo location where user takes [DataType.GOLF_SHOT_COUNT] during
+ * [ExerciseType.GOLF] activity
+ */
+class ExerciseTypeConfig private constructor(
+ @GolfShotTrackingPlaceInfo
+ val golfShotTrackingPlaceInfo: Int = GolfShotTrackingPlaceInfo.UNSPECIFIED
+) {
+
+ internal constructor(
+ proto: DataProto.ExerciseTypeConfig
+ ) : this (
+ GolfShotTrackingPlaceInfo.fromProto(proto.golfShotTrackingPlaceInfo)
+ )
+
+ internal fun toProto(): DataProto.ExerciseTypeConfig {
+ return DataProto.ExerciseTypeConfig.newBuilder()
+ .setGolfShotTrackingPlaceInfo(golfShotTrackingPlaceInfo.toProto())
+ .build()
+ }
+
+ override fun toString(): String =
+ "ExerciseTypeConfig(golfShotTrackingPlaceInfo=$golfShotTrackingPlaceInfo)"
+
+ companion object {
+ /**
+ * Creates golf-specific exercise type configuration.
+ *
+ * @param golfShotTrackingPlaceInfo location where user takes [DataType.GOLF_SHOT_COUNT] during
+ * [ExerciseType.GOLF] activity
+ *
+ * @return an instance of [ExerciseTypeConfig] with specific [GolfShotTrackingPlaceInfo]
+ */
+ @JvmStatic
+ fun createGolfExerciseTypeConfig(
+ @GolfShotTrackingPlaceInfo golfShotTrackingPlaceInfo: Int
+ ): ExerciseTypeConfig {
+ return ExerciseTypeConfig(golfShotTrackingPlaceInfo)
+ }
+ }
+}
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/GolfShotTrackingPlaceInfo.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/GolfShotTrackingPlaceInfo.kt
new file mode 100644
index 0000000..5cf89cb
--- /dev/null
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/GolfShotTrackingPlaceInfo.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.services.client.data
+
+import androidx.annotation.IntDef
+import androidx.health.services.client.proto.DataProto
+import kotlin.annotation.AnnotationRetention.SOURCE
+
+/**
+ * The tracking information for a golf shot used in [ExerciseTypeConfig]. It is the semantic
+ * location of a user while golfing to assist golf swing activity recognition algorithms.
+ *
+ * @hide
+ */
+@Retention(AnnotationRetention.SOURCE)
+@IntDef(
+ GolfShotTrackingPlaceInfo.UNSPECIFIED,
+ GolfShotTrackingPlaceInfo.FAIRWAY,
+ GolfShotTrackingPlaceInfo.PUTTING_GREEN,
+ GolfShotTrackingPlaceInfo.TEE_BOX
+)
+annotation class GolfShotTrackingPlaceInfo {
+ companion object {
+ /** The golf shot is being taken from an unknown place. */
+ const val UNSPECIFIED: Int = 0
+ /** The golf shot is being taken from the fairway. */
+ const val FAIRWAY: Int = 1
+ /** The golf shot is being taken from the putting green. */
+ const val PUTTING_GREEN: Int = 2
+ /** The golf shot is being taken from the tee box area. */
+ const val TEE_BOX: Int = 3
+
+ internal fun @receiver:GolfShotTrackingPlaceInfo Int.toProto():
+ DataProto.GolfShotTrackingPlaceInfoType =
+ when (this) {
+ FAIRWAY -> DataProto.GolfShotTrackingPlaceInfoType.GOLF_SHOT_TRACKING_PLACE_INFO_FAIRWAY
+ PUTTING_GREEN ->
+ DataProto.GolfShotTrackingPlaceInfoType.GOLF_SHOT_TRACKING_PLACE_INFO_PUTTING_GREEN
+ TEE_BOX -> DataProto.GolfShotTrackingPlaceInfoType.GOLF_SHOT_TRACKING_PLACE_INFO_TEE_BOX
+ else -> DataProto.GolfShotTrackingPlaceInfoType.GOLF_SHOT_TRACKING_PLACE_INFO_UNSPECIFIED
+ }
+
+ @GolfShotTrackingPlaceInfo
+ internal fun fromProto(proto: DataProto.GolfShotTrackingPlaceInfoType): Int =
+ when (proto) {
+ DataProto.GolfShotTrackingPlaceInfoType.GOLF_SHOT_TRACKING_PLACE_INFO_PUTTING_GREEN ->
+ PUTTING_GREEN
+ DataProto.GolfShotTrackingPlaceInfoType.GOLF_SHOT_TRACKING_PLACE_INFO_TEE_BOX -> TEE_BOX
+ DataProto.GolfShotTrackingPlaceInfoType.GOLF_SHOT_TRACKING_PLACE_INFO_FAIRWAY -> FAIRWAY
+ else -> UNSPECIFIED
+ }
+ }
+}
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt
index 786f636..9f92ad7 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt
@@ -27,6 +27,7 @@
import androidx.health.services.client.data.ExerciseConfig
import androidx.health.services.client.data.ExerciseGoal
import androidx.health.services.client.data.ExerciseInfo
+import androidx.health.services.client.data.ExerciseTypeConfig
import androidx.health.services.client.data.WarmUpConfig
import androidx.health.services.client.impl.IpcConstants.EXERCISE_API_BIND_ACTION
import androidx.health.services.client.impl.IpcConstants.SERVICE_PACKAGE_NAME
@@ -42,6 +43,7 @@
import androidx.health.services.client.impl.request.FlushRequest
import androidx.health.services.client.impl.request.PrepareExerciseRequest
import androidx.health.services.client.impl.request.StartExerciseRequest
+import androidx.health.services.client.impl.request.UpdateExerciseTypeConfigRequest
import com.google.common.util.concurrent.FutureCallback
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
@@ -216,6 +218,20 @@
ContextCompat.getMainExecutor(context)
)
+ override fun updateExerciseTypeConfigAsync(
+ exerciseTypeConfig: ExerciseTypeConfig
+ ): ListenableFuture<Void> {
+ return executeWithVersionCheck(
+ { service, resultFuture ->
+ service.updateExerciseTypeConfigForActiveExercise(
+ UpdateExerciseTypeConfigRequest(packageName, exerciseTypeConfig),
+ StatusCallback(resultFuture)
+ )
+ },
+ 3
+ )
+ }
+
internal companion object {
internal const val CLIENT = "HealthServicesExerciseClient"
internal val CLIENT_CONFIGURATION =
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.kt
new file mode 100644
index 0000000..a3820b5
--- /dev/null
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.services.client.impl.request
+
+import android.os.Parcelable
+import androidx.annotation.RestrictTo
+import androidx.health.services.client.data.ExerciseTypeConfig
+import androidx.health.services.client.data.ProtoParcelable
+import androidx.health.services.client.proto.RequestsProto
+
+/**
+ * Request for updating exercise type configuration in an [ExerciseTypeConfig].
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class UpdateExerciseTypeConfigRequest(
+ val packageName: String,
+ val exerciseTypeConfig: ExerciseTypeConfig,
+) : ProtoParcelable<RequestsProto.UpdateExerciseTypeConfigRequest>() {
+ override val proto: RequestsProto.UpdateExerciseTypeConfigRequest =
+ RequestsProto.UpdateExerciseTypeConfigRequest.newBuilder()
+ .setPackageName(packageName)
+ .setConfig(exerciseTypeConfig.toProto())
+ .build()
+
+ companion object {
+ @JvmField
+ val CREATOR: Parcelable.Creator<UpdateExerciseTypeConfigRequest> = newCreator { bytes ->
+ val proto = RequestsProto.UpdateExerciseTypeConfigRequest.parseFrom(bytes)
+ UpdateExerciseTypeConfigRequest(proto.packageName, ExerciseTypeConfig(proto.config))
+ }
+ }
+}
\ No newline at end of file
diff --git a/health/health-services-client/src/main/proto/data.proto b/health/health-services-client/src/main/proto/data.proto
index 420bbc6..65e2287 100644
--- a/health/health-services-client/src/main/proto/data.proto
+++ b/health/health-services-client/src/main/proto/data.proto
@@ -173,16 +173,31 @@
reserved 2 to max; // Next ID
}
+enum GolfShotTrackingPlaceInfoType {
+ GOLF_SHOT_TRACKING_PLACE_INFO_UNSPECIFIED = 0;
+ GOLF_SHOT_TRACKING_PLACE_INFO_FAIRWAY = 1;
+ GOLF_SHOT_TRACKING_PLACE_INFO_PUTTING_GREEN = 2;
+ GOLF_SHOT_TRACKING_PLACE_INFO_TEE_BOX = 3;
+ reserved 4 to max; // Next ID
+}
+
+message ExerciseTypeConfig {
+ optional GolfShotTrackingPlaceInfoType golf_shot_tracking_place_info = 1;
+ reserved 2 to max; // Next ID
+}
+
message ExerciseConfig {
optional ExerciseType exercise_type = 1;
repeated DataType data_types = 2;
repeated DataType aggregate_data_types = 3;
- optional bool is_auto_pause_and_resume_enabled = 4; // TODO(sarakato): Move to dynamicExericseConfig
+ optional bool is_auto_pause_and_resume_enabled = 4;
optional bool is_gps_usage_enabled = 5;
repeated ExerciseGoal exercise_goals = 6;
optional Bundle exercise_params = 7; // TODO(b/241015676): Deprecate
optional float swimming_pool_length = 8;
- reserved 9 to max; // Next ID
+ optional ExerciseTypeConfig exercise_type_config = 10;
+ reserved 9;
+ reserved 11 to max; // Next ID
}
message ExerciseInfo {
diff --git a/health/health-services-client/src/main/proto/requests.proto b/health/health-services-client/src/main/proto/requests.proto
index d71c3a7..39e0b7c 100644
--- a/health/health-services-client/src/main/proto/requests.proto
+++ b/health/health-services-client/src/main/proto/requests.proto
@@ -116,3 +116,9 @@
optional ExerciseConfig config = 2;
reserved 3 to max; // Next ID
}
+
+message UpdateExerciseTypeConfigRequest {
+ optional string package_name = 1;
+ optional ExerciseTypeConfig config = 2;
+ reserved 3 to max; // Next ID
+}
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/ExerciseClientTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/ExerciseClientTest.kt
index 8d2e322..41be3a0d 100644
--- a/health/health-services-client/src/test/java/androidx/health/services/client/ExerciseClientTest.kt
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/ExerciseClientTest.kt
@@ -34,7 +34,9 @@
import androidx.health.services.client.data.ExerciseTrackedStatus
import androidx.health.services.client.data.ExerciseType
import androidx.health.services.client.data.ExerciseTypeCapabilities
+import androidx.health.services.client.data.ExerciseTypeConfig
import androidx.health.services.client.data.ExerciseUpdate
+import androidx.health.services.client.data.GolfShotTrackingPlaceInfo
import androidx.health.services.client.data.WarmUpConfig
import androidx.health.services.client.impl.IExerciseApiService
import androidx.health.services.client.impl.IExerciseUpdateListener
@@ -51,6 +53,7 @@
import androidx.health.services.client.impl.request.FlushRequest
import androidx.health.services.client.impl.request.PrepareExerciseRequest
import androidx.health.services.client.impl.request.StartExerciseRequest
+import androidx.health.services.client.impl.request.UpdateExerciseTypeConfigRequest
import androidx.health.services.client.impl.response.AvailabilityResponse
import androidx.health.services.client.impl.response.ExerciseCapabilitiesResponse
import androidx.health.services.client.impl.response.ExerciseInfoResponse
@@ -644,6 +647,23 @@
.isEqualTo(passiveMonitoringCapabilities.toString())
}
+ @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+ @Test
+ fun updateExerciseTypeConfigForActiveExercise() = runTest {
+ service.exerciseConfig = ExerciseConfig.builder(ExerciseType.GOLF).build()
+ val exerciseTypeConfig =
+ ExerciseTypeConfig.createGolfExerciseTypeConfig(GolfShotTrackingPlaceInfo.FAIRWAY)
+ val request =
+ UpdateExerciseTypeConfigRequest(
+ CLIENT_CONFIGURATION.servicePackageName, exerciseTypeConfig
+ )
+ val statusCallback = IStatusCallback.Default()
+
+ service.updateExerciseTypeConfigForActiveExercise(request, statusCallback)
+
+ Truth.assertThat(service.exerciseConfig?.exerciseTypeConfig).isEqualTo(exerciseTypeConfig)
+ }
+
class FakeExerciseUpdateCallback : ExerciseUpdateCallback {
val availabilities = mutableMapOf<DataType<*, *>, Availability>()
val registrationFailureThrowables = mutableListOf<Throwable>()
@@ -855,6 +875,18 @@
statusCallbackAction.invoke(statusCallback)
}
+ override fun updateExerciseTypeConfigForActiveExercise(
+ updateExerciseTypeConfigRequest: UpdateExerciseTypeConfigRequest,
+ statuscallback: IStatusCallback
+ ) {
+ val newExerciseTypeConfig = updateExerciseTypeConfigRequest.exerciseTypeConfig
+ val newExerciseConfig =
+ ExerciseConfig.builder(
+ exerciseConfig!!.exerciseType
+ ).setExerciseTypeConfig(newExerciseTypeConfig).build()
+ this.exerciseConfig = newExerciseConfig
+ }
+
fun setException() {
throwException = true
}
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseConfigTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseConfigTest.kt
index 8567fa6..c23763f 100644
--- a/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseConfigTest.kt
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseConfigTest.kt
@@ -43,6 +43,8 @@
DataTypeCondition(DISTANCE_TOTAL, 150.0, GREATER_THAN)
),
),
+ exerciseTypeConfig = ExerciseTypeConfig.createGolfExerciseTypeConfig(
+ GolfShotTrackingPlaceInfo.FAIRWAY)
).toProto()
val config = ExerciseConfig(proto)
@@ -57,6 +59,51 @@
assertThat(config.exerciseGoals[1].dataTypeCondition.dataType).isEqualTo(DISTANCE_TOTAL)
assertThat(config.exerciseGoals[1].dataTypeCondition.threshold).isEqualTo(150.0)
assertThat(config.exerciseGoals[1].dataTypeCondition.comparisonType).isEqualTo(GREATER_THAN)
+ assertThat((config.exerciseTypeConfig)?.golfShotTrackingPlaceInfo).isEqualTo(
+ GolfShotTrackingPlaceInfo.FAIRWAY)
+ }
+
+ @Test
+ fun exerciseTypeConfigNull_protoRoundTrip() {
+ val proto = ExerciseConfig(
+ ExerciseType.RUNNING,
+ setOf(LOCATION, DISTANCE_TOTAL, HEART_RATE_BPM),
+ isAutoPauseAndResumeEnabled = true,
+ isGpsEnabled = true,
+ exerciseGoals = listOf(
+ ExerciseGoal.createOneTimeGoal(
+ DataTypeCondition(DISTANCE_TOTAL, 50.0, GREATER_THAN)
+ ),
+ ExerciseGoal.createOneTimeGoal(
+ DataTypeCondition(DISTANCE_TOTAL, 150.0, GREATER_THAN)
+ ),
+ )
+ ).toProto()
+
+ val config = ExerciseConfig(proto)
+
+ assertThat(config.exerciseType).isEqualTo(ExerciseType.RUNNING)
+ assertThat(config.dataTypes).containsExactly(LOCATION, HEART_RATE_BPM, DISTANCE_TOTAL)
+ assertThat(config.isAutoPauseAndResumeEnabled).isEqualTo(true)
+ assertThat(config.isGpsEnabled).isEqualTo(true)
+ assertThat(config.exerciseGoals[0].dataTypeCondition.dataType).isEqualTo(DISTANCE_TOTAL)
+ assertThat(config.exerciseGoals[0].dataTypeCondition.threshold).isEqualTo(50.0)
+ assertThat(config.exerciseGoals[0].dataTypeCondition.comparisonType).isEqualTo(GREATER_THAN)
+ assertThat(config.exerciseGoals[1].dataTypeCondition.dataType).isEqualTo(DISTANCE_TOTAL)
+ assertThat(config.exerciseGoals[1].dataTypeCondition.threshold).isEqualTo(150.0)
+ assertThat(config.exerciseGoals[1].dataTypeCondition.comparisonType).isEqualTo(GREATER_THAN)
+ assertThat((config.exerciseTypeConfig)).isNull()
+ }
+
+ @Test
+ fun builder_exerciseTypeConfigNull() {
+ val exerciseTypeConfigNotSetExerciseConfig =
+ ExerciseConfig.builder(ExerciseType.UNKNOWN).build()
+ val setNullExerciseTypeConfigExerciseConfig =
+ ExerciseConfig.builder(ExerciseType.UNKNOWN).setExerciseTypeConfig(null).build()
+
+ assertThat(exerciseTypeConfigNotSetExerciseConfig.exerciseTypeConfig).isNull()
+ assertThat(setNullExerciseTypeConfigExerciseConfig.exerciseTypeConfig).isNull()
}
@Test
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseTypeConfigTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseTypeConfigTest.kt
new file mode 100644
index 0000000..10cf21d
--- /dev/null
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseTypeConfigTest.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.services.client.data
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class ExerciseTypeConfigTest {
+ @Test
+ fun protoRoundTrip() {
+ val proto = ExerciseTypeConfig.createGolfExerciseTypeConfig(
+ GolfShotTrackingPlaceInfo.FAIRWAY).toProto()
+ val config = ExerciseTypeConfig(proto)
+
+ assertThat(config.golfShotTrackingPlaceInfo).isEqualTo(GolfShotTrackingPlaceInfo.FAIRWAY)
+ }
+
+ @Test
+ fun createGolfExerciseTypeConfigFromFactoryMethod() {
+ val golfExerciseStyleConfig = ExerciseTypeConfig.createGolfExerciseTypeConfig(
+ GolfShotTrackingPlaceInfo.FAIRWAY)
+
+ assertThat(golfExerciseStyleConfig.golfShotTrackingPlaceInfo).isEqualTo(
+ GolfShotTrackingPlaceInfo.FAIRWAY)
+ }
+}
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseUpdateTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseUpdateTest.kt
index e5c0a69..b4b0537 100644
--- a/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseUpdateTest.kt
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseUpdateTest.kt
@@ -59,9 +59,12 @@
exerciseConfig = ExerciseConfig(
WALKING,
setOf(CALORIES_TOTAL),
- isAutoPauseAndResumeEnabled = true,
+ isAutoPauseAndResumeEnabled = false,
isGpsEnabled = false,
- exerciseGoals = listOf(goal)
+ exerciseGoals = listOf(goal),
+ exerciseTypeConfig = ExerciseTypeConfig.createGolfExerciseTypeConfig(
+ GolfShotTrackingPlaceInfo.FAIRWAY
+ )
),
activeDurationCheckpoint = ActiveDurationCheckpoint(42.instant(), 30.duration()),
updateDurationFromBoot = 42.duration(),
@@ -81,7 +84,62 @@
.isEqualTo(CALORIES_TOTAL)
assertThat(markerSummary.achievedGoal.dataTypeCondition.dataType).isEqualTo(CALORIES_TOTAL)
assertThat(update.exerciseConfig!!.exerciseType).isEqualTo(WALKING)
+ assertThat(
+ update.exerciseConfig!!.exerciseTypeConfig!!.golfShotTrackingPlaceInfo).isEqualTo(
+ GolfShotTrackingPlaceInfo.FAIRWAY)
assertThat(update.activeDurationCheckpoint!!.activeDuration).isEqualTo(30.duration())
assertThat(update.exerciseStateInfo.state).isEqualTo(ExerciseState.ACTIVE)
}
-}
\ No newline at end of file
+
+ @Test
+ fun exerciseTypeConfigNull_protoRoundTrip() {
+ val goal = createOneTimeGoal(
+ DataTypeCondition(CALORIES_TOTAL, 125.0, GREATER_THAN_OR_EQUAL)
+ )
+ val proto = ExerciseUpdate(
+ latestMetrics = DataPointContainer(
+ listOf(DataPoints.calories(130.0, 15.duration(), 35.duration()))
+ ),
+ latestAchievedGoals = setOf(goal),
+ latestMilestoneMarkerSummaries = setOf(
+ MilestoneMarkerSummary(
+ 15.instant(),
+ 40.instant(),
+ 20.duration(),
+ goal,
+ DataPointContainer(
+ listOf(DataPoints.calories(130.0, 15.duration(), 35.duration()))
+ )
+ )
+ ),
+ exerciseStateInfo = ExerciseStateInfo(ExerciseState.ACTIVE, ExerciseEndReason.UNKNOWN),
+ exerciseConfig = ExerciseConfig(
+ WALKING,
+ setOf(CALORIES_TOTAL),
+ isAutoPauseAndResumeEnabled = false,
+ isGpsEnabled = false,
+ exerciseGoals = listOf(goal),
+ ),
+ activeDurationCheckpoint = ActiveDurationCheckpoint(42.instant(), 30.duration()),
+ updateDurationFromBoot = 42.duration(),
+ startTime = 10.instant()
+ ).proto
+
+ val update = ExerciseUpdate(proto)
+
+ val caloriesDataPoint = update.latestMetrics.getData(DataType.CALORIES).first()
+ val markerSummary = update.latestMilestoneMarkerSummaries.first()
+ assertThat(update.startTime).isEqualTo(10.instant())
+ assertThat(update.getUpdateDurationFromBoot()).isEqualTo(42.duration())
+ assertThat(caloriesDataPoint.value).isEqualTo(130.0)
+ assertThat(caloriesDataPoint.startDurationFromBoot).isEqualTo(15.duration())
+ assertThat(caloriesDataPoint.endDurationFromBoot).isEqualTo(35.duration())
+ assertThat(update.latestAchievedGoals.first().dataTypeCondition.dataType)
+ .isEqualTo(CALORIES_TOTAL)
+ assertThat(markerSummary.achievedGoal.dataTypeCondition.dataType).isEqualTo(CALORIES_TOTAL)
+ assertThat(update.exerciseConfig!!.exerciseType).isEqualTo(WALKING)
+ assertThat(update.exerciseConfig!!.exerciseTypeConfig).isNull()
+ assertThat(update.activeDurationCheckpoint!!.activeDuration).isEqualTo(30.duration())
+ assertThat(update.exerciseStateInfo.state).isEqualTo(ExerciseState.ACTIVE)
+ }
+}
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedExerciseClientTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedExerciseClientTest.kt
index 32e90cf..b519939 100644
--- a/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedExerciseClientTest.kt
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedExerciseClientTest.kt
@@ -31,6 +31,8 @@
import androidx.health.services.client.data.ExerciseType
import androidx.health.services.client.data.ExerciseUpdate
import androidx.health.services.client.data.WarmUpConfig
+import androidx.health.services.client.data.ExerciseTypeConfig
+import androidx.health.services.client.data.GolfShotTrackingPlaceInfo
import androidx.health.services.client.impl.event.ExerciseUpdateListenerEvent
import androidx.health.services.client.impl.internal.IExerciseInfoCallback
import androidx.health.services.client.impl.internal.IStatusCallback
@@ -41,6 +43,7 @@
import androidx.health.services.client.impl.request.FlushRequest
import androidx.health.services.client.impl.request.PrepareExerciseRequest
import androidx.health.services.client.impl.request.StartExerciseRequest
+import androidx.health.services.client.impl.request.UpdateExerciseTypeConfigRequest
import androidx.health.services.client.impl.response.AvailabilityResponse
import androidx.health.services.client.impl.response.ExerciseCapabilitiesResponse
import androidx.test.core.app.ApplicationProvider
@@ -107,7 +110,7 @@
ExerciseType.WALKING,
setOf(HEART_RATE_BPM),
isAutoPauseAndResumeEnabled = false,
- isGpsEnabled = false
+ isGpsEnabled = false,
)
val availabilityEvent = ExerciseUpdateListenerEvent.createAvailabilityUpdateEvent(
AvailabilityResponse(HEART_RATE_BPM, ACQUIRING)
@@ -171,6 +174,34 @@
}
@Test
+ fun withExerciseTypeConfig_statsAndSample_startExercise() {
+ val exerciseConfig = ExerciseConfig(
+ ExerciseType.GOLF,
+ setOf(HEART_RATE_BPM, HEART_RATE_BPM_STATS),
+ isAutoPauseAndResumeEnabled = false,
+ isGpsEnabled = false,
+ exerciseTypeConfig = ExerciseTypeConfig.createGolfExerciseTypeConfig(
+ GolfShotTrackingPlaceInfo.FAIRWAY
+ )
+ )
+ val availabilityEvent = ExerciseUpdateListenerEvent.createAvailabilityUpdateEvent(
+ // Currently the proto form of HEART_RATE_BPM and HEART_RATE_BPM_STATS is identical. The
+ // APK doesn't know about _STATS, so pass the sample type to mimic that behavior.
+ AvailabilityResponse(HEART_RATE_BPM, ACQUIRING)
+ )
+ client.setUpdateCallback(callback)
+ client.startExerciseAsync(exerciseConfig)
+ shadowOf(getMainLooper()).idle()
+
+ fakeService.listener!!.onExerciseUpdateListenerEvent(availabilityEvent)
+ shadowOf(getMainLooper()).idle()
+
+ // When both the sample type and stat type are requested, both should be notified
+ assertThat(callback.availabilities).containsEntry(HEART_RATE_BPM, ACQUIRING)
+ assertThat(callback.availabilities).containsEntry(HEART_RATE_BPM_STATS, ACQUIRING)
+ }
+
+ @Test
fun dataTypeInAvailabilityCallbackShouldMatchRequested_justSampleType_prepare() {
val warmUpConfig = WarmUpConfig(
ExerciseType.WALKING,
@@ -302,5 +333,12 @@
override fun flushExercise(request: FlushRequest?, statusCallback: IStatusCallback?) {
throw NotImplementedError()
}
+
+ override fun updateExerciseTypeConfigForActiveExercise(
+ updateExerciseTypeConfigRequest: UpdateExerciseTypeConfigRequest?,
+ statuscallback: IStatusCallback?
+ ) {
+ throw NotImplementedError()
+ }
}
-}
\ No newline at end of file
+}
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequestTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequestTest.kt
new file mode 100644
index 0000000..1e2a23c
--- /dev/null
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequestTest.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.services.client.impl.request
+
+import android.os.Parcel
+import androidx.health.services.client.data.ExerciseTypeConfig
+import androidx.health.services.client.data.GolfShotTrackingPlaceInfo
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class UpdateExerciseTypeConfigRequestTest {
+ @Test
+ fun parcelableRoundTrip() {
+ val request =
+ UpdateExerciseTypeConfigRequest(
+ "package",
+ ExerciseTypeConfig.createGolfExerciseTypeConfig(GolfShotTrackingPlaceInfo.TEE_BOX)
+ )
+ val parcel = Parcel.obtain()
+
+ request.writeToParcel(parcel, 0)
+ parcel.setDataPosition(0)
+ val fromParcel = UpdateExerciseTypeConfigRequest.CREATOR.createFromParcel(parcel)
+
+ Truth.assertThat(request).isEqualTo(fromParcel)
+ }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_current.txt
index 363fb58..5bebc62 100644
--- a/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.lifecycle.compose {
- @kotlin.RequiresOptIn(message="This is an experimental Lifecycle Compose API.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface ExperimentalLifecycleComposeApi {
+ @kotlin.RequiresOptIn(message="This is an experimental Lifecycle Compose API.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface ExperimentalLifecycleComposeApi {
}
public final class FlowExtKt {
diff --git a/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/ExperimentalLifecycleComposeApi.kt b/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/ExperimentalLifecycleComposeApi.kt
index 5b17e2f..3f54e9a 100644
--- a/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/ExperimentalLifecycleComposeApi.kt
+++ b/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/ExperimentalLifecycleComposeApi.kt
@@ -24,4 +24,5 @@
AnnotationTarget.FIELD,
AnnotationTarget.PROPERTY_GETTER,
)
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalLifecycleComposeApi
diff --git a/paging/paging-common/api/public_plus_experimental_current.txt b/paging/paging-common/api/public_plus_experimental_current.txt
index 8782062..20d1eb3 100644
--- a/paging/paging-common/api/public_plus_experimental_current.txt
+++ b/paging/paging-common/api/public_plus_experimental_current.txt
@@ -46,7 +46,7 @@
method @AnyThread public void onInvalidated();
}
- @kotlin.RequiresOptIn public @interface ExperimentalPagingApi {
+ @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalPagingApi {
}
public final class InvalidatingPagingSourceFactory<Key, Value> implements kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
diff --git a/paging/paging-common/src/main/kotlin/androidx/paging/ExperimentalPagingApi.kt b/paging/paging-common/src/main/kotlin/androidx/paging/ExperimentalPagingApi.kt
index c31ff68..f431c33 100644
--- a/paging/paging-common/src/main/kotlin/androidx/paging/ExperimentalPagingApi.kt
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/ExperimentalPagingApi.kt
@@ -21,4 +21,5 @@
* source-incompatible change in newer versions of the artifact that supplies it.
*/
@RequiresOptIn
+@Retention(AnnotationRetention.BINARY)
public annotation class ExperimentalPagingApi
\ No newline at end of file
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index a39575d..33c2b39 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -25,7 +25,7 @@
kotlin.code.style=official
# Disable docs
androidx.enableDocumentation=false
-androidx.playground.snapshotBuildId=9189666
+androidx.playground.snapshotBuildId=9282206
androidx.playground.metalavaBuildId=8993580
androidx.playground.dokkaBuildId=7472101
androidx.studio.type=playground
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
index 03390ee..17d262b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
@@ -28,11 +28,8 @@
import androidx.room.compiler.codegen.XTypeSpec
import androidx.room.compiler.codegen.asClassName
import androidx.room.compiler.codegen.asMutableClassName
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.ext.CommonTypeNames.STRING
import com.squareup.javapoet.ArrayTypeName
import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.CodeBlock
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.ParameterizedTypeName
import com.squareup.javapoet.TypeName
@@ -104,8 +101,7 @@
val AUTO_MIGRATION_SPEC = XClassName.get("$ROOM_PACKAGE.migration", "AutoMigrationSpec")
val UUID_UTIL: XClassName =
XClassName.get("$ROOM_PACKAGE.util", "UUIDUtil")
- val AMBIGUOUS_COLUMN_RESOLVER: ClassName =
- ClassName.get(ROOM_PACKAGE, "AmbiguousColumnResolver")
+ val AMBIGUOUS_COLUMN_RESOLVER = XClassName.get(ROOM_PACKAGE, "AmbiguousColumnResolver")
val RELATION_UTIL = XClassName.get("androidx.room.util", "RelationUtil")
}
@@ -143,19 +139,18 @@
object CollectionTypeNames {
val ARRAY_MAP = XClassName.get(COLLECTION_PACKAGE, "ArrayMap")
val LONG_SPARSE_ARRAY = XClassName.get(COLLECTION_PACKAGE, "LongSparseArray")
- val INT_SPARSE_ARRAY: ClassName = ClassName.get(COLLECTION_PACKAGE, "SparseArrayCompat")
-}
-
-object KotlinCollectionTypeNames {
- val MUTABLE_LIST = List::class.asMutableClassName()
+ val INT_SPARSE_ARRAY = XClassName.get(COLLECTION_PACKAGE, "SparseArrayCompat")
}
object CommonTypeNames {
val LIST = List::class.asClassName()
+ val MUTABLE_LIST = List::class.asMutableClassName()
val ARRAY_LIST = XClassName.get("java.util", "ArrayList")
val MAP = Map::class.asClassName()
+ val MUTABLE_MAP = Map::class.asMutableClassName()
val HASH_MAP = XClassName.get("java.util", "HashMap")
val SET = Set::class.asClassName()
+ val MUTABLE_SET = Set::class.asMutableClassName()
val HASH_SET = XClassName.get("java.util", "HashSet")
val STRING = String::class.asClassName()
val INTEGER = ClassName.get("java.lang", "Integer")
@@ -261,6 +256,10 @@
val DB_UTIL_DROP_FTS_SYNC_TRIGGERS = RoomTypeNames.DB_UTIL.packageMember("dropFtsSyncTriggers")
val CURSOR_UTIL_GET_COLUMN_INDEX =
RoomTypeNames.CURSOR_UTIL.packageMember("getColumnIndex")
+ val CURSOR_UTIL_GET_COLUMN_INDEX_OR_THROW =
+ RoomTypeNames.CURSOR_UTIL.packageMember("getColumnIndexOrThrow")
+ val CURSOR_UTIL_WRAP_MAPPED_COLUMNS =
+ RoomTypeNames.CURSOR_UTIL.packageMember("wrapMappedColumns")
val ROOM_SQL_QUERY_ACQUIRE =
RoomTypeNames.ROOM_SQL_QUERY.companionMember("acquire", isJvmStatic = true)
val ROOM_DATABASE_WITH_TRANSACTION =
@@ -291,10 +290,10 @@
ReactiveStreamsTypeNames.PUBLISHER
)
-fun TypeName.defaultValue(): String {
+fun XTypeName.defaultValue(): String {
return if (!isPrimitive) {
"null"
- } else if (this == TypeName.BOOLEAN) {
+ } else if (this == XTypeName.PRIMITIVE_BOOLEAN) {
"false"
} else {
"0"
@@ -368,44 +367,141 @@
}.build()
/**
+ * Generates an array literal with the given [values]
+ *
+ * Example: `ArrayLiteral(XTypeName.PRIMITIVE_INT, 1, 2, 3)`
+ *
+ * For Java will produce: `new int[] {1, 2, 3}`
+ *
+ * For Kotlin will produce: `intArrayOf(1, 2, 3)`,
+ */
+fun ArrayLiteral(
+ language: CodeLanguage,
+ type: XTypeName,
+ vararg values: Any
+): XCodeBlock {
+ val space = when (language) {
+ CodeLanguage.JAVA -> "%W"
+ CodeLanguage.KOTLIN -> " "
+ }
+ val initExpr = when (language) {
+ CodeLanguage.JAVA -> XCodeBlock.of(language, "new %T[] ", type)
+ CodeLanguage.KOTLIN -> XCodeBlock.of(language, getArrayOfFunction(type))
+ }
+ val openingChar = when (language) {
+ CodeLanguage.JAVA -> "{"
+ CodeLanguage.KOTLIN -> "("
+ }
+ val closingChar = when (language) {
+ CodeLanguage.JAVA -> "}"
+ CodeLanguage.KOTLIN -> ")"
+ }
+ return XCodeBlock.of(
+ language,
+ "%L$openingChar%L$closingChar",
+ initExpr,
+ XCodeBlock.builder(language).apply {
+ val joining = Array(values.size) { i ->
+ XCodeBlock.of(
+ language,
+ if (type == CommonTypeNames.STRING) "%S" else "%L",
+ values[i]
+ )
+ }
+ val placeholders = joining.joinToString(separator = ",$space") { "%L" }
+ add(placeholders, *joining)
+ }.build()
+ )
+}
+
+/**
* Generates a 2D array literal where the value at `i`,`j` will be produced by `valueProducer.
* For example:
* ```
- * DoubleArrayLiteral(TypeName.INT, 2, { _ -> 3 }, { i, j -> i + j })
+ * DoubleArrayLiteral(XTypeName.PRIMITIVE_INT, 2, { _ -> 3 }, { i, j -> i + j })
* ```
- * will produce:
+ * For Java will produce:
* ```
* new int[][] {
- * { 0, 1, 2 },
- * { 1, 2, 3 }
+ * {0, 1, 2},
+ * {1, 2, 3}
* }
* ```
+ * For Kotlin will produce:
+ * ```
+ * arrayOf(
+ * intArrayOf(0, 1, 2),
+ * intArrayOf(1, 2, 3)
+ * )
+ * ```
*/
fun DoubleArrayLiteral(
- type: TypeName,
+ language: CodeLanguage,
+ type: XTypeName,
rowSize: Int,
columnSizeProducer: (Int) -> Int,
valueProducer: (Int, Int) -> Any
-): CodeBlock = CodeBlock.of(
- "new $T[][] {$W$L$W}", type,
- CodeBlock.join(
- List(rowSize) { i ->
- CodeBlock.of(
- "{$W$L$W}",
- CodeBlock.join(
- List(columnSizeProducer(i)) { j ->
- CodeBlock.of(
- if (type == STRING.toJavaPoet()) S else L,
- valueProducer(i, j)
- )
- },
- ",$W"
- ),
- )
- },
- ",$W"
+): XCodeBlock {
+ val space = when (language) {
+ CodeLanguage.JAVA -> "%W"
+ CodeLanguage.KOTLIN -> " "
+ }
+ val outerInit = when (language) {
+ CodeLanguage.JAVA -> XCodeBlock.of(language, "new %T[][] ", type)
+ CodeLanguage.KOTLIN -> XCodeBlock.of(language, "arrayOf")
+ }
+ val innerInit = when (language) {
+ CodeLanguage.JAVA -> XCodeBlock.of(language, "", type)
+ CodeLanguage.KOTLIN -> XCodeBlock.of(language, getArrayOfFunction(type))
+ }
+ val openingChar = when (language) {
+ CodeLanguage.JAVA -> "{"
+ CodeLanguage.KOTLIN -> "("
+ }
+ val closingChar = when (language) {
+ CodeLanguage.JAVA -> "}"
+ CodeLanguage.KOTLIN -> ")"
+ }
+ return XCodeBlock.of(
+ language,
+ "%L$openingChar%L$closingChar",
+ outerInit,
+ XCodeBlock.builder(language).apply {
+ val joining = Array(rowSize) { i ->
+ XCodeBlock.of(
+ language,
+ "%L$openingChar%L$closingChar",
+ innerInit,
+ XCodeBlock.builder(language).apply {
+ val joining = Array(columnSizeProducer(i)) { j ->
+ XCodeBlock.of(
+ language,
+ if (type == CommonTypeNames.STRING) "%S" else "%L",
+ valueProducer(i, j)
+ )
+ }
+ val placeholders = joining.joinToString(separator = ",$space") { "%L" }
+ add(placeholders, *joining)
+ }.build()
+ )
+ }
+ val placeholders = joining.joinToString(separator = ",$space") { "%L" }
+ add(placeholders, *joining)
+ }.build()
)
-)
+}
+
+private fun getArrayOfFunction(type: XTypeName) = when (type) {
+ XTypeName.PRIMITIVE_BOOLEAN -> "booleanArrayOf"
+ XTypeName.PRIMITIVE_BYTE -> "byteArrayOf"
+ XTypeName.PRIMITIVE_SHORT -> "shortArrayOf"
+ XTypeName.PRIMITIVE_INT -> "intArrayOf"
+ XTypeName.PRIMITIVE_LONG -> "longArrayOf"
+ XTypeName.PRIMITIVE_CHAR -> "charArrayOf"
+ XTypeName.PRIMITIVE_FLOAT -> "floatArrayOf"
+ XTypeName.PRIMITIVE_DOUBLE -> "doubleArrayOf"
+ else -> "arrayOf"
+}
/**
* Code of expression for [Collection.size] in Kotlin, and [java.util.Collection.size] for Java.
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
index 140c869..6252b56 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
@@ -743,7 +743,8 @@
fieldName = field.name,
jvmName = field.name,
type = field.type,
- callType = CallType.FIELD
+ callType = CallType.FIELD,
+ isMutableField = !field.element.isFinal()
)
},
assignFromMethod = { match ->
@@ -756,7 +757,8 @@
CallType.SYNTHETIC_METHOD
} else {
CallType.METHOD
- }
+ },
+ isMutableField = !field.element.isFinal()
)
},
reportAmbiguity = { matching ->
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index 4f18b5d..ff8e11b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -73,7 +73,9 @@
import androidx.room.solver.query.result.ImmutableMapQueryResultAdapter
import androidx.room.solver.query.result.ListQueryResultAdapter
import androidx.room.solver.query.result.MapQueryResultAdapter
+import androidx.room.solver.query.result.MultimapQueryResultAdapter
import androidx.room.solver.query.result.MultimapQueryResultAdapter.Companion.validateMapTypeArgs
+import androidx.room.solver.query.result.MultimapQueryResultAdapter.MapType.Companion.isSparseArray
import androidx.room.solver.query.result.OptionalQueryResultAdapter
import androidx.room.solver.query.result.PojoRowAdapter
import androidx.room.solver.query.result.QueryResultAdapter
@@ -601,21 +603,24 @@
} else if (typeMirror.isTypeOf(java.util.Map::class) ||
typeMirror.rawType.typeName == ARRAY_MAP.toJavaPoet() ||
typeMirror.rawType.typeName == LONG_SPARSE_ARRAY.toJavaPoet() ||
- typeMirror.rawType.typeName == INT_SPARSE_ARRAY
+ typeMirror.rawType.typeName == INT_SPARSE_ARRAY.toJavaPoet()
) {
- val keyTypeArg = when (typeMirror.rawType.typeName) {
- LONG_SPARSE_ARRAY.toJavaPoet() -> context.processingEnv.requireType(TypeName.LONG)
- INT_SPARSE_ARRAY -> context.processingEnv.requireType(TypeName.INT)
- else -> typeMirror.typeArguments[0].extendsBoundOrSelf()
+ val mapType = when (typeMirror.rawType.asTypeName()) {
+ LONG_SPARSE_ARRAY -> MultimapQueryResultAdapter.MapType.LONG_SPARSE
+ INT_SPARSE_ARRAY -> MultimapQueryResultAdapter.MapType.INT_SPARSE
+ ARRAY_MAP -> MultimapQueryResultAdapter.MapType.ARRAY_MAP
+ else -> MultimapQueryResultAdapter.MapType.DEFAULT
+ }
+ val keyTypeArg = when (mapType) {
+ MultimapQueryResultAdapter.MapType.LONG_SPARSE ->
+ context.processingEnv.requireType(TypeName.LONG)
+ MultimapQueryResultAdapter.MapType.INT_SPARSE ->
+ context.processingEnv.requireType(TypeName.INT)
+ else ->
+ typeMirror.typeArguments[0].extendsBoundOrSelf()
}
- val isSparseArray = when (typeMirror.rawType.typeName) {
- LONG_SPARSE_ARRAY.toJavaPoet() -> LONG_SPARSE_ARRAY.toJavaPoet()
- INT_SPARSE_ARRAY -> INT_SPARSE_ARRAY
- else -> null
- }
-
- val mapValueTypeArg = if (isSparseArray != null) {
+ val mapValueTypeArg = if (mapType.isSparseArray()) {
typeMirror.typeArguments[0].extendsBoundOrSelf()
} else {
typeMirror.typeArguments[1].extendsBoundOrSelf()
@@ -636,48 +641,53 @@
if (collectionTypeRaw.isAssignableFrom(mapValueTypeArg.rawType)) {
// The Map's value type argument is assignable to a Collection, we need to make
// sure it is either a list or a set.
- if (
- mapValueTypeArg.isTypeOf(java.util.List::class) ||
- mapValueTypeArg.isTypeOf(java.util.Set::class)
- ) {
- val valueTypeArg = mapValueTypeArg.typeArguments.single().extendsBoundOrSelf()
-
- val keyRowAdapter = findRowAdapter(
- typeMirror = keyTypeArg,
- query = query,
- columnName = mapInfo?.keyColumnName
- ) ?: return null
-
- val valueRowAdapter = findRowAdapter(
- typeMirror = valueTypeArg,
- query = query,
- columnName = mapInfo?.valueColumnName
- ) ?: return null
-
- validateMapTypeArgs(
- keyTypeArg = keyTypeArg,
- valueTypeArg = valueTypeArg,
- keyReader = findCursorValueReader(keyTypeArg, null),
- valueReader = findCursorValueReader(valueTypeArg, null),
- mapInfo = mapInfo,
- logger = context.logger
- )
- return MapQueryResultAdapter(
- context = context,
- parsedQuery = query,
- keyTypeArg = keyTypeArg,
- valueTypeArg = valueTypeArg,
- keyRowAdapter = checkTypeOrNull(keyRowAdapter) ?: return null,
- valueRowAdapter = checkTypeOrNull(valueRowAdapter) ?: return null,
- valueCollectionType = mapValueTypeArg,
- isArrayMap = typeMirror.rawType.typeName == ARRAY_MAP.toJavaPoet(),
- isSparseArray = isSparseArray
- )
- } else {
- context.logger.e(
- valueCollectionMustBeListOrSet(mapValueTypeArg.typeName)
- )
+ val listTypeRaw = context.COMMON_TYPES.LIST.rawType
+ val setTypeRaw = context.COMMON_TYPES.SET.rawType
+ val collectionValueType = when {
+ mapValueTypeArg.rawType.isAssignableFrom(listTypeRaw) ->
+ MultimapQueryResultAdapter.CollectionValueType.LIST
+ mapValueTypeArg.rawType.isAssignableFrom(setTypeRaw) ->
+ MultimapQueryResultAdapter.CollectionValueType.SET
+ else -> {
+ context.logger.e(
+ valueCollectionMustBeListOrSet(mapValueTypeArg.typeName)
+ )
+ return null
+ }
}
+
+ val valueTypeArg = mapValueTypeArg.typeArguments.single().extendsBoundOrSelf()
+
+ val keyRowAdapter = findRowAdapter(
+ typeMirror = keyTypeArg,
+ query = query,
+ columnName = mapInfo?.keyColumnName
+ ) ?: return null
+
+ val valueRowAdapter = findRowAdapter(
+ typeMirror = valueTypeArg,
+ query = query,
+ columnName = mapInfo?.valueColumnName
+ ) ?: return null
+
+ validateMapTypeArgs(
+ keyTypeArg = keyTypeArg,
+ valueTypeArg = valueTypeArg,
+ keyReader = findCursorValueReader(keyTypeArg, null),
+ valueReader = findCursorValueReader(valueTypeArg, null),
+ mapInfo = mapInfo,
+ logger = context.logger
+ )
+ return MapQueryResultAdapter(
+ context = context,
+ parsedQuery = query,
+ keyTypeArg = keyTypeArg,
+ valueTypeArg = valueTypeArg,
+ keyRowAdapter = checkTypeOrNull(keyRowAdapter) ?: return null,
+ valueRowAdapter = checkTypeOrNull(valueRowAdapter) ?: return null,
+ valueCollectionType = collectionValueType,
+ mapType = mapType
+ )
} else {
val keyRowAdapter = findRowAdapter(
typeMirror = keyTypeArg,
@@ -706,8 +716,7 @@
keyRowAdapter = checkTypeOrNull(keyRowAdapter) ?: return null,
valueRowAdapter = checkTypeOrNull(valueRowAdapter) ?: return null,
valueCollectionType = null,
- isArrayMap = typeMirror.rawType.typeName == ARRAY_MAP.toJavaPoet(),
- isSparseArray = isSparseArray
+ mapType = mapType
)
}
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/AmbiguousColumnIndexAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/AmbiguousColumnIndexAdapter.kt
index 9da6a49..31c6b80 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/AmbiguousColumnIndexAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/AmbiguousColumnIndexAdapter.kt
@@ -17,17 +17,14 @@
package androidx.room.solver.query.result
import androidx.room.AmbiguousColumnResolver
-import androidx.room.compiler.codegen.toJavaPoet
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XTypeName
import androidx.room.ext.CommonTypeNames
import androidx.room.ext.DoubleArrayLiteral
-import androidx.room.ext.L
import androidx.room.ext.RoomTypeNames
-import androidx.room.ext.T
-import androidx.room.ext.W
import androidx.room.parser.ParsedQuery
import androidx.room.solver.CodeGenScope
import androidx.room.vo.ColumnIndexVar
-import com.squareup.javapoet.TypeName
/**
* An index adapter that uses [AmbiguousColumnResolver] to create the index variables for
@@ -47,7 +44,7 @@
*/
override fun onCursorReady(cursorVarName: String, scope: CodeGenScope) {
val cursorIndexMappingVarName = scope.getTmpVar("_cursorIndices")
- scope.builder().apply {
+ scope.builder.apply {
val resultInfo = query.resultInfo
if (resultInfo != null && query.hasTopStarProjection == false) {
// Query result columns are known, use ambiguous column resolver at compile-time
@@ -57,34 +54,42 @@
mappings = mappings.map { it.usedColumns.toTypedArray() }.toTypedArray()
)
val rowMappings = DoubleArrayLiteral(
- type = TypeName.INT,
+ language = language,
+ type = XTypeName.PRIMITIVE_INT,
rowSize = cursorIndices.size,
columnSizeProducer = { i -> cursorIndices[i].size },
valueProducer = { i, j -> cursorIndices[i][j] }
)
- addStatement(
- "final $T[][] $L = $L",
- TypeName.INT,
- cursorIndexMappingVarName,
- rowMappings
+ addLocalVariable(
+ name = cursorIndexMappingVarName,
+ typeName = XTypeName.getArrayName(
+ XTypeName.getArrayName(XTypeName.PRIMITIVE_INT)
+ ),
+ assignExpr = rowMappings
)
} else {
// Generate code that uses ambiguous column resolver at runtime, providing the
// query result column names from the Cursor and the result object column names in
// an array literal.
val rowMappings = DoubleArrayLiteral(
- type = CommonTypeNames.STRING.toJavaPoet(),
+ language = language,
+ type = CommonTypeNames.STRING,
rowSize = mappings.size,
columnSizeProducer = { i -> mappings[i].usedColumns.size },
valueProducer = { i, j -> mappings[i].usedColumns[j] }
)
- addStatement(
- "final $T[][] $L = $T.resolve($L.getColumnNames(),$W$L)",
- TypeName.INT,
- cursorIndexMappingVarName,
- RoomTypeNames.AMBIGUOUS_COLUMN_RESOLVER,
- cursorVarName,
- rowMappings
+ addLocalVariable(
+ name = cursorIndexMappingVarName,
+ typeName = XTypeName.getArrayName(
+ XTypeName.getArrayName(XTypeName.PRIMITIVE_INT)
+ ),
+ assignExpr = XCodeBlock.of(
+ language,
+ "%T.resolve(%L.getColumnNames(), %L)",
+ RoomTypeNames.AMBIGUOUS_COLUMN_RESOLVER,
+ cursorVarName,
+ rowMappings
+ )
)
}
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/EntityRowAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/EntityRowAdapter.kt
index 3f978e7..4bebca1 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/EntityRowAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/EntityRowAdapter.kt
@@ -16,23 +16,18 @@
package androidx.room.solver.query.result
+import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.codegen.XFunSpec
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.ext.AndroidTypeNames.CURSOR
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.ext.AndroidTypeNames
+import androidx.room.ext.ArrayLiteral
import androidx.room.ext.CommonTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.N
-import androidx.room.ext.RoomTypeNames.CURSOR_UTIL
-import androidx.room.ext.S
-import androidx.room.ext.T
-import androidx.room.ext.W
+import androidx.room.ext.RoomMemberNames
import androidx.room.solver.CodeGenScope
import androidx.room.vo.ColumnIndexVar
import androidx.room.vo.Entity
import androidx.room.vo.columnNames
import androidx.room.writer.EntityCursorConverterWriter
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.TypeName
class EntityRowAdapter(val entity: Entity) : QueryMappedRowAdapter(entity.type) {
@@ -47,12 +42,15 @@
private var indexVars: List<ColumnIndexVar>? = null
override fun onCursorReady(cursorVarName: String, scope: CodeGenScope) {
- indexVars = entity.columnNames.map {
+ indexVars = entity.columnNames.map { columnName ->
ColumnIndexVar(
- column = it,
- indexVar = CodeBlock.of(
- "$T.getColumnIndex($N, $S)",
- CURSOR_UTIL.toJavaPoet(), cursorVarName, it
+ column = columnName,
+ indexVar = XCodeBlock.of(
+ scope.language,
+ "%M(%L, %S)",
+ RoomMemberNames.CURSOR_UTIL_GET_COLUMN_INDEX,
+ cursorVarName,
+ columnName
).toString()
)
}
@@ -76,24 +74,27 @@
// solely used in the shared converter method and whose getColumnIndex() is overridden
// to return the resolved column index.
cursorDelegateVarName = scope.getTmpVar("_wrappedCursor")
- val entityColumnNamesParam = CodeBlock.of(
- "new $T[] { $L }",
- CommonTypeNames.STRING.toJavaPoet(),
- CodeBlock.join(entity.columnNames.map { CodeBlock.of(S, it) }, ",$W")
+ val entityColumnNamesParam = ArrayLiteral(
+ scope.language,
+ CommonTypeNames.STRING,
+ *entity.columnNames.toTypedArray()
)
- val entityColumnIndicesParam = CodeBlock.of(
- "new $T[] { $L }",
- TypeName.INT,
- CodeBlock.join(indices.map { CodeBlock.of(L, it.indexVar) }, ",$W")
+ val entityColumnIndicesParam = ArrayLiteral(
+ scope.language,
+ XTypeName.PRIMITIVE_INT,
+ *indices.map { it.indexVar }.toTypedArray()
)
- scope.builder().addStatement(
- "final $T $N = $T.wrapMappedColumns($N, $L, $L)",
- CURSOR.toJavaPoet(),
- cursorDelegateVarName,
- CURSOR_UTIL.toJavaPoet(),
- cursorVarName,
- entityColumnNamesParam,
- entityColumnIndicesParam
+ scope.builder.addLocalVariable(
+ checkNotNull(cursorDelegateVarName),
+ AndroidTypeNames.CURSOR,
+ assignExpr = XCodeBlock.of(
+ scope.language,
+ "%M(%L, %L, %L)",
+ RoomMemberNames.CURSOR_UTIL_WRAP_MAPPED_COLUMNS,
+ cursorVarName,
+ entityColumnNamesParam,
+ entityColumnIndicesParam
+ )
)
}
functionSpec = scope.writer.getOrCreateFunction(EntityCursorConverterWriter(entity))
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt
index 8698fae..5b991a1 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt
@@ -43,7 +43,7 @@
override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
val mapVarName = scope.getTmpVar("_mapBuilder")
- scope.builder().apply {
+ scope.builder.apply {
val dupeColumnsIndexAdapter: AmbiguousColumnIndexAdapter?
if (duplicateColumns.isNotEmpty()) {
// There are duplicate columns in the result objects, generate code that provides
@@ -86,6 +86,7 @@
dupeColumnsIndexAdapter?.getIndexVarsForMapping(valueRowAdapter.mapping)
?: valueRowAdapter.getDefaultIndexAdapter().getIndexVars()
val columnNullCheckCodeBlock = getColumnNullCheckCode(
+ language = language,
cursorVarName = cursorVarName,
indexVars = valueIndexVars
)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ListQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ListQueryResultAdapter.kt
index 2ef7f52..df6d058 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ListQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ListQueryResultAdapter.kt
@@ -18,8 +18,8 @@
import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.processing.XType
+import androidx.room.ext.CommonTypeNames
import androidx.room.ext.CommonTypeNames.ARRAY_LIST
-import androidx.room.ext.KotlinCollectionTypeNames.MUTABLE_LIST
import androidx.room.solver.CodeGenScope
class ListQueryResultAdapter(
@@ -29,7 +29,7 @@
override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
scope.builder.apply {
rowAdapter.onCursorReady(cursorVarName = cursorVarName, scope = scope)
- val listTypeName = MUTABLE_LIST.parametrizedBy(typeArg.asTypeName())
+ val listTypeName = CommonTypeNames.MUTABLE_LIST.parametrizedBy(typeArg.asTypeName())
addLocalVariable(
name = outVarName,
typeName = listTypeName,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt
index 9b05f4a..fd8d8f1 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt
@@ -16,16 +16,16 @@
package androidx.room.solver.query.result
-import androidx.room.compiler.codegen.toJavaPoet
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.asClassName
+import androidx.room.compiler.processing.XNullability
import androidx.room.compiler.processing.XType
-import androidx.room.ext.CollectionTypeNames.ARRAY_MAP
-import androidx.room.ext.L
-import androidx.room.ext.T
+import androidx.room.ext.CommonTypeNames
import androidx.room.parser.ParsedQuery
import androidx.room.processor.Context
import androidx.room.solver.CodeGenScope
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.ParameterizedTypeName
+import androidx.room.solver.query.result.MultimapQueryResultAdapter.MapType.Companion.isSparseArray
class MapQueryResultAdapter(
context: Context,
@@ -34,58 +34,54 @@
override val valueTypeArg: XType,
private val keyRowAdapter: QueryMappedRowAdapter,
private val valueRowAdapter: QueryMappedRowAdapter,
- private val valueCollectionType: XType?,
- isArrayMap: Boolean = false,
- private val isSparseArray: ClassName? = null,
+ private val valueCollectionType: CollectionValueType?,
+ private val mapType: MapType
) : MultimapQueryResultAdapter(context, parsedQuery, listOf(keyRowAdapter, valueRowAdapter)) {
- private val declaredValueType = if (valueCollectionType != null) {
- ParameterizedTypeName.get(
- valueCollectionType.typeElement?.className,
- valueTypeArg.typeName
- )
+ // The type name of the result map value
+ // For Map<Foo, Bar> it is Bar
+ // for Map<Foo, List<Bar> it is List<Bar>
+ private val valueTypeName = if (valueCollectionType != null) {
+ valueCollectionType.className.parametrizedBy(valueTypeArg.asTypeName())
} else {
- valueTypeArg.typeName
+ valueTypeArg.asTypeName()
}
- private val implValueType = if (valueCollectionType != null) {
- ParameterizedTypeName.get(
- declaredToImplCollection[valueCollectionType.typeElement?.className],
- valueTypeArg.typeName
- )
- } else {
- valueTypeArg.typeName
+ // The type name of the concrete result map value
+ // For Map<Foo, Bar> it is Bar
+ // For Map<Foo, List<Bar> it is ArrayList<Bar>
+ private val implValueTypeName = when (valueCollectionType) {
+ CollectionValueType.LIST ->
+ CommonTypeNames.ARRAY_LIST.parametrizedBy(valueTypeArg.asTypeName())
+ CollectionValueType.SET ->
+ CommonTypeNames.HASH_SET.parametrizedBy(valueTypeArg.asTypeName())
+ else ->
+ valueTypeArg.asTypeName()
}
- private val mapType = if (isSparseArray != null) {
- ParameterizedTypeName.get(
- isSparseArray,
- declaredValueType
- )
- } else {
- ParameterizedTypeName.get(
- if (isArrayMap) ARRAY_MAP.toJavaPoet() else ClassName.get(Map::class.java),
- keyTypeArg.typeName,
- declaredValueType
- )
+ // The type name of the result map
+ private val mapTypeName = when (mapType) {
+ MapType.DEFAULT, MapType.ARRAY_MAP ->
+ mapType.className.parametrizedBy(keyTypeArg.asTypeName(), valueTypeName)
+ MapType.LONG_SPARSE, MapType.INT_SPARSE ->
+ mapType.className.parametrizedBy(valueTypeName)
}
- private val implMapType = if (isSparseArray != null) {
- ParameterizedTypeName.get(
- isSparseArray,
- declaredValueType
- )
- } else {
- // LinkedHashMap is used as impl to preserve key ordering for ordered query results.
- ParameterizedTypeName.get(
- if (isArrayMap) ARRAY_MAP.toJavaPoet() else ClassName.get(LinkedHashMap::class.java),
- keyTypeArg.typeName,
- declaredValueType
- )
+ // The type name of the concrete result map
+ private val implMapTypeName = when (mapType) {
+ MapType.DEFAULT ->
+ // LinkedHashMap is used as impl to preserve key ordering for ordered query results.
+ LinkedHashMap::class.asClassName().parametrizedBy(
+ keyTypeArg.asTypeName(), valueTypeName
+ )
+ MapType.ARRAY_MAP ->
+ mapType.className.parametrizedBy(keyTypeArg.asTypeName(), valueTypeName)
+ MapType.LONG_SPARSE, MapType.INT_SPARSE ->
+ mapType.className.parametrizedBy(valueTypeName)
}
override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
- scope.builder().apply {
+ scope.builder.apply {
val dupeColumnsIndexAdapter: AmbiguousColumnIndexAdapter?
if (duplicateColumns.isNotEmpty()) {
// There are duplicate columns in the result objects, generate code that provides
@@ -109,18 +105,23 @@
}
}
- addStatement("final $T $L = new $T()", mapType, outVarName, implMapType)
+ addLocalVariable(
+ name = outVarName,
+ typeName = mapTypeName,
+ assignExpr = XCodeBlock.ofNewInstance(language, implMapTypeName)
+ )
val tmpKeyVarName = scope.getTmpVar("_key")
val tmpValueVarName = scope.getTmpVar("_value")
- beginControlFlow("while ($L.moveToNext())", cursorVarName).apply {
- addStatement("final $T $L", keyTypeArg.typeName, tmpKeyVarName)
+ beginControlFlow("while (%L.moveToNext())", cursorVarName).apply {
+ addLocalVariable(tmpKeyVarName, keyTypeArg.asTypeName())
keyRowAdapter.convert(tmpKeyVarName, cursorVarName, scope)
val valueIndexVars =
dupeColumnsIndexAdapter?.getIndexVarsForMapping(valueRowAdapter.mapping)
?: valueRowAdapter.getDefaultIndexAdapter().getIndexVars()
val columnNullCheckCodeBlock = getColumnNullCheckCode(
+ language = language,
cursorVarName = cursorVarName,
indexVars = valueIndexVars
)
@@ -129,23 +130,33 @@
// opposed to a 1-to-many mapping.
if (valueCollectionType != null) {
val tmpCollectionVarName = scope.getTmpVar("_values")
- addStatement("$T $L", declaredValueType, tmpCollectionVarName)
+ addLocalVariable(tmpCollectionVarName, valueTypeName)
- if (isSparseArray != null) {
- beginControlFlow("if ($L.get($L) != null)", outVarName, tmpKeyVarName)
+ if (mapType.isSparseArray()) {
+ beginControlFlow("if (%L.get(%L) != null)", outVarName, tmpKeyVarName)
} else {
- beginControlFlow("if ($L.containsKey($L))", outVarName, tmpKeyVarName)
+ beginControlFlow("if (%L.containsKey(%L))", outVarName, tmpKeyVarName)
}.apply {
+ val getFunction = when (language) {
+ CodeLanguage.JAVA -> "get"
+ CodeLanguage.KOTLIN ->
+ if (mapType.isSparseArray()) "get" else "getValue"
+ }
addStatement(
- "$L = $L.get($L)",
+ "%L = %L.%L(%L)",
tmpCollectionVarName,
outVarName,
+ getFunction,
tmpKeyVarName
)
}.nextControlFlow("else").apply {
- addStatement("$L = new $T()", tmpCollectionVarName, implValueType)
addStatement(
- "$L.put($L, $L)",
+ "%L = %L",
+ tmpCollectionVarName,
+ XCodeBlock.ofNewInstance(language, implValueTypeName)
+ )
+ addStatement(
+ "%L.put(%L, %L)",
outVarName,
tmpKeyVarName,
tmpCollectionVarName
@@ -154,37 +165,41 @@
// Perform value columns null check, in a 1-to-many mapping we still add the key
// with an empty collection as the value entry.
- beginControlFlow("if ($L)", columnNullCheckCodeBlock).apply {
+ beginControlFlow("if (%L)", columnNullCheckCodeBlock).apply {
addStatement("continue")
}.endControlFlow()
- addStatement("final $T $L", valueTypeArg.typeName, tmpValueVarName)
+ addLocalVariable(tmpValueVarName, valueTypeArg.asTypeName())
valueRowAdapter.convert(tmpValueVarName, cursorVarName, scope)
- addStatement("$L.add($L)", tmpCollectionVarName, tmpValueVarName)
+ addStatement("%L.add(%L)", tmpCollectionVarName, tmpValueVarName)
} else {
// Perform value columns null check, in a 1-to-1 mapping we still add the key
- // with a null value entry.
- beginControlFlow("if ($L)", columnNullCheckCodeBlock).apply {
- addStatement("$L.put($L, null)", outVarName, tmpKeyVarName)
- addStatement("continue")
+ // with a null value entry if permitted.
+ beginControlFlow("if (%L)", columnNullCheckCodeBlock).apply {
+ if (
+ language == CodeLanguage.KOTLIN &&
+ valueTypeArg.nullability == XNullability.NONNULL
+ ) {
+ // TODO(b/249984504): Generate / output a better message.
+ addStatement("error(%S)", "Missing value for a key.")
+ } else {
+ addStatement("%L.put(%L, null)", outVarName, tmpKeyVarName)
+ addStatement("continue")
+ }
}.endControlFlow()
- addStatement(
- "final $T $L",
- valueTypeArg.typeElement?.className,
- tmpValueVarName
- )
+ addLocalVariable(tmpValueVarName, valueTypeArg.asTypeName())
valueRowAdapter.convert(tmpValueVarName, cursorVarName, scope)
// For consistency purposes, in the one-to-one object mapping case, if
// multiple values are encountered for the same key, we will only consider
// the first ever encountered mapping.
- if (isSparseArray != null) {
- beginControlFlow("if ($L.get($L) == null)", outVarName, tmpKeyVarName)
+ if (mapType.isSparseArray()) {
+ beginControlFlow("if (%L.get(%L) == null)", outVarName, tmpKeyVarName)
} else {
- beginControlFlow("if (!$L.containsKey($L))", outVarName, tmpKeyVarName)
+ beginControlFlow("if (!%L.containsKey(%L))", outVarName, tmpKeyVarName)
}.apply {
- addStatement("$L.put($L, $L)", outVarName, tmpKeyVarName, tmpValueVarName)
+ addStatement("%L.put(%L, %L)", outVarName, tmpKeyVarName, tmpValueVarName)
}.endControlFlow()
}
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultimapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultimapQueryResultAdapter.kt
index 3b2c0f3..41f6df3 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultimapQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultimapQueryResultAdapter.kt
@@ -16,10 +16,13 @@
package androidx.room.solver.query.result
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XClassName
+import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.codegen.toJavaPoet
import androidx.room.compiler.processing.XType
-import androidx.room.ext.L
-import androidx.room.ext.W
+import androidx.room.ext.CollectionTypeNames
+import androidx.room.ext.CommonTypeNames
import androidx.room.ext.implementsEqualsAndHashcode
import androidx.room.log.RLog
import androidx.room.parser.ParsedQuery
@@ -32,8 +35,6 @@
import androidx.room.vo.ColumnIndexVar
import androidx.room.vo.MapInfo
import androidx.room.vo.Warning
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.CodeBlock
/**
* Abstract class for Map and Multimap result adapters.
@@ -91,12 +92,23 @@
}
}
- companion object {
+ enum class MapType(val className: XClassName) {
+ DEFAULT(CommonTypeNames.MUTABLE_MAP),
+ ARRAY_MAP(CollectionTypeNames.ARRAY_MAP),
+ LONG_SPARSE(CollectionTypeNames.LONG_SPARSE_ARRAY),
+ INT_SPARSE(CollectionTypeNames.INT_SPARSE_ARRAY);
- val declaredToImplCollection = mapOf<ClassName, ClassName>(
- ClassName.get(List::class.java) to ClassName.get(ArrayList::class.java),
- ClassName.get(Set::class.java) to ClassName.get(HashSet::class.java)
- )
+ companion object {
+ fun MapType.isSparseArray() = this == LONG_SPARSE || this == INT_SPARSE
+ }
+ }
+
+ enum class CollectionValueType(val className: XClassName) {
+ LIST(CommonTypeNames.MUTABLE_LIST),
+ SET(CommonTypeNames.MUTABLE_SET)
+ }
+
+ companion object {
/**
* Checks if the @MapInfo annotation is needed for clarification regarding the return type
@@ -144,16 +156,23 @@
* Generates a code expression that verifies if all matched fields are null.
*/
fun getColumnNullCheckCode(
+ language: CodeLanguage,
cursorVarName: String,
indexVars: List<ColumnIndexVar>
- ): CodeBlock {
+ ) = XCodeBlock.builder(language).apply {
+ val space = when (language) {
+ CodeLanguage.JAVA -> "%W"
+ CodeLanguage.KOTLIN -> " "
+ }
val conditions = indexVars.map {
- CodeBlock.of(
- "$L.isNull($L)",
+ XCodeBlock.of(
+ language,
+ "%L.isNull(%L)",
cursorVarName,
it.indexVar
)
}
- return CodeBlock.join(conditions, "$W&&$W")
- }
+ val placeholders = conditions.joinToString(separator = "$space&&$space") { "%L" }
+ add(placeholders, *conditions.toTypedArray())
+ }.build()
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/SingleNamedColumnRowAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/SingleNamedColumnRowAdapter.kt
index 57a3a43..cfc68ba 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/SingleNamedColumnRowAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/SingleNamedColumnRowAdapter.kt
@@ -16,17 +16,14 @@
package androidx.room.solver.query.result
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.ext.L
-import androidx.room.ext.RoomTypeNames.CURSOR_UTIL
-import androidx.room.ext.S
-import androidx.room.ext.T
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.ext.RoomMemberNames
import androidx.room.ext.capitalize
import androidx.room.ext.stripNonJava
import androidx.room.solver.CodeGenScope
import androidx.room.solver.types.CursorValueReader
import androidx.room.vo.ColumnIndexVar
-import com.squareup.javapoet.TypeName
import java.util.Locale
/**
@@ -47,14 +44,16 @@
override fun onCursorReady(cursorVarName: String, scope: CodeGenScope) {
indexVarName = scope.getTmpVar(indexVarNamePrefix)
- scope.builder().addStatement(
- "final $T $L = $T.$L($L, $S)",
- TypeName.INT,
- indexVarName,
- CURSOR_UTIL.toJavaPoet(),
- "getColumnIndexOrThrow",
- cursorVarName,
- columnName
+ scope.builder.addLocalVariable(
+ name = indexVarName,
+ typeName = XTypeName.PRIMITIVE_INT,
+ assignExpr = XCodeBlock.of(
+ scope.language,
+ "%M(%L, %S)",
+ RoomMemberNames.CURSOR_UTIL_GET_COLUMN_INDEX_OR_THROW,
+ cursorVarName,
+ columnName
+ )
)
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/FieldGetter.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/FieldGetter.kt
index 81678a9..05165a4 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/FieldGetter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/FieldGetter.kt
@@ -18,15 +18,19 @@
import androidx.room.compiler.codegen.CodeLanguage
import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.processing.XNullability
import androidx.room.compiler.processing.XType
+import androidx.room.ext.capitalize
import androidx.room.solver.CodeGenScope
import androidx.room.solver.types.StatementValueBinder
+import java.util.Locale
data class FieldGetter(
val fieldName: String,
val jvmName: String,
val type: XType,
- val callType: CallType
+ val callType: CallType,
+ val isMutableField: Boolean
) {
fun writeGet(ownerVar: String, outVar: String, builder: XCodeBlock.Builder) {
builder.addLocalVariable(
@@ -43,8 +47,25 @@
binder: StatementValueBinder,
scope: CodeGenScope
) {
- val varExpr = getterExpression(ownerVar, scope.language).toString()
- binder.bindToStmt(stmtParamVar, indexVar, varExpr, scope)
+ val varExpr = getterExpression(ownerVar, scope.language)
+ // A temporary local val is needed in Kotlin if the field or property is mutable (var)
+ // and is nullable since otherwise smart cast will fail indicating that the property
+ // might have changed when binding to statement.
+ val needTempVal = scope.language == CodeLanguage.KOTLIN &&
+ (callType == CallType.FIELD || callType == CallType.SYNTHETIC_METHOD) &&
+ type.nullability != XNullability.NONNULL &&
+ isMutableField
+ if (needTempVal) {
+ val tmpField = scope.getTmpVar("_tmp${fieldName.capitalize(Locale.US)}")
+ scope.builder.addLocalVariable(
+ name = tmpField,
+ typeName = type.asTypeName(),
+ assignExpr = varExpr
+ )
+ binder.bindToStmt(stmtParamVar, indexVar, tmpField, scope)
+ } else {
+ binder.bindToStmt(stmtParamVar, indexVar, varExpr.toString(), scope)
+ }
}
private fun getterExpression(ownerVar: String, codeLanguage: CodeLanguage): XCodeBlock {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/FieldReadWriteWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/FieldReadWriteWriter.kt
index c839722..8b856c3 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/FieldReadWriteWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/FieldReadWriteWriter.kt
@@ -16,9 +16,10 @@
package androidx.room.writer
+import androidx.room.compiler.codegen.CodeLanguage
import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.codegen.XTypeName
-import androidx.room.compiler.codegen.toJavaPoet
+import androidx.room.compiler.processing.XNullability
import androidx.room.ext.capitalize
import androidx.room.ext.defaultValue
import androidx.room.solver.CodeGenScope
@@ -366,7 +367,20 @@
field.cursorValueReader?.readFromCursor(tmpField, cursorVar, indexVar, scope)
} else {
beginControlFlow("if (%L == -1)", indexVar).apply {
- addStatement("%L = %L", tmpField, typeName.toJavaPoet().defaultValue())
+ val defaultValue = typeName.defaultValue()
+ if (
+ language == CodeLanguage.KOTLIN &&
+ typeName.nullability == XNullability.NONNULL &&
+ defaultValue == "null"
+ ) {
+ // TODO(b/249984504): Generate / output a better message.
+ addStatement(
+ "error(%S)",
+ "Missing column '${field.columnName}' for a non null value."
+ )
+ } else {
+ addStatement("%L = %L", tmpField, defaultValue)
+ }
}
nextControlFlow("else").apply {
field.cursorValueReader?.readFromCursor(tmpField, cursorVar, indexVar, scope)
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt
index cf952dd..1c3792d 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt
@@ -76,7 +76,7 @@
assertThat(field.setter)
.isEqualTo(FieldSetter(field.name, setterName, intType, CallType.METHOD))
assertThat(field.getter)
- .isEqualTo(FieldGetter(field.name, getterName, intType, CallType.METHOD))
+ .isEqualTo(FieldGetter(field.name, getterName, intType, CallType.METHOD, true))
}
}
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts3TableEntityProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts3TableEntityProcessorTest.kt
index 2f48c05..bf4a2d5 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts3TableEntityProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts3TableEntityProcessorTest.kt
@@ -82,7 +82,7 @@
assertThat(field.setter,
`is`(FieldSetter("rowId", "setRowId", intType, CallType.METHOD)))
assertThat(field.getter,
- `is`(FieldGetter("rowId", "getRowId", intType, CallType.METHOD)))
+ `is`(FieldGetter("rowId", "getRowId", intType, CallType.METHOD, true)))
assertThat(entity.primaryKey.fields, `is`(Fields(field)))
assertThat(entity.shadowTableName, `is`("MyEntity_content"))
assertThat(entity.ftsVersion, `is`(FtsVersion.FTS3))
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt
index 2b220be..3434fdf 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt
@@ -69,7 +69,7 @@
assertThat(field.setter,
`is`(FieldSetter("rowId", "setRowId", intType, CallType.METHOD)))
assertThat(field.getter,
- `is`(FieldGetter("rowId", "getRowId", intType, CallType.METHOD)))
+ `is`(FieldGetter("rowId", "getRowId", intType, CallType.METHOD, true)))
assertThat(entity.primaryKey.fields, `is`(Fields(field)))
assertThat(entity.shadowTableName, `is`("MyEntity_content"))
assertThat(entity.ftsVersion, `is`(FtsVersion.FTS4))
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
index b45185b..2785605 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
@@ -2100,7 +2100,8 @@
fieldName = "isbn",
jvmName = "getIsbn",
type = stringType,
- callType = CallType.SYNTHETIC_METHOD
+ callType = CallType.SYNTHETIC_METHOD,
+ isMutableField = true
)
)
Truth.assertThat(
@@ -2121,7 +2122,8 @@
fieldName = "isbn2",
jvmName = "getIsbn2",
type = stringType.makeNullable(),
- callType = CallType.SYNTHETIC_METHOD
+ callType = CallType.SYNTHETIC_METHOD,
+ isMutableField = true
)
)
Truth.assertThat(
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt
index 8d41129..7a4b1ae 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt
@@ -73,7 +73,8 @@
)
)
assertThat(field.setter, `is`(FieldSetter("id", "setId", intType, CallType.METHOD)))
- assertThat(field.getter, `is`(FieldGetter("id", "getId", intType, CallType.METHOD)))
+ assertThat(field.getter,
+ `is`(FieldGetter("id", "getId", intType, CallType.METHOD, true)))
assertThat(entity.primaryKey.fields, `is`(Fields(field)))
}
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
index 2be73c3..73af03b 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
@@ -435,7 +435,7 @@
}
private fun assignGetterSetter(f: Field, name: String, type: XType) {
- f.getter = FieldGetter(f.name, name, type, CallType.FIELD)
+ f.getter = FieldGetter(f.name, name, type, CallType.FIELD, true)
f.setter = FieldSetter(f.name, name, type, CallType.FIELD)
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
index 8c1d50c..3525dfcd 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
@@ -61,7 +61,9 @@
@PrimaryKey
var pk: Int
) {
- var variable: Long = 0
+ var variablePrimitive: Long = 0
+ var variableString: String = ""
+ var variableNullableString: String? = null
}
""".trimIndent()
)
@@ -1164,4 +1166,152 @@
expectedFilePath = getTestGoldenPath(testName)
)
}
+
+ @Test
+ fun queryResultAdapter_map() {
+ val testName = object {}.javaClass.enclosingMethod!!.name
+ val src = Source.kotlin(
+ "MyDao.kt",
+ """
+ import androidx.room.*
+
+ @Database(entities = [Artist::class, Song::class], version = 1, exportSchema = false)
+ abstract class MyDatabase : RoomDatabase() {
+ abstract fun getDao(): MyDao
+ }
+
+ @Dao
+ interface MyDao {
+ @Query("SELECT * FROM Song JOIN Artist ON Song.artistKey = Artist.artistId")
+ fun getSongsWithArtist(): Map<Song, Artist>
+
+ @Query("SELECT * FROM Song JOIN Artist ON Song.artistKey = Artist.artistId")
+ fun getSongsWithNullableArtist(): Map<Song, Artist?>
+
+ @Query("SELECT * FROM Artist JOIN Song ON Artist.artistId = Song.artistKey")
+ fun getArtistWithSongs(): Map<Artist, List<Song>>
+
+ @MapInfo(valueColumn = "songCount")
+ @Query(
+ "SELECT Artist.*, COUNT(songId) as songCount " +
+ "FROM Artist JOIN Song ON Artist.artistId = Song.artistKey " +
+ "GROUP BY artistId"
+ )
+ fun getArtistSongCount(): Map<Artist, Int>
+
+ @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
+ @MapInfo(valueColumn = "songId")
+ @Query("SELECT * FROM Artist JOIN Song ON Artist.artistId = Song.artistKey")
+ fun getArtistWithSongIds(): Map<Artist, List<String>>
+ }
+
+ @Entity
+ data class Artist(
+ @PrimaryKey
+ val artistId: String
+ )
+
+ @Entity
+ data class Song(
+ @PrimaryKey
+ val songId: String,
+ val artistKey: String
+ )
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(src),
+ expectedFilePath = getTestGoldenPath(testName)
+ )
+ }
+
+ @Test
+ fun queryResultAdapter_map_ambiguousIndexAdapter() {
+ val testName = object {}.javaClass.enclosingMethod!!.name
+ val src = Source.kotlin(
+ "MyDao.kt",
+ """
+ import androidx.room.*
+
+ @Database(entities = [User::class, Comment::class], version = 1, exportSchema = false)
+ abstract class MyDatabase : RoomDatabase() {
+ abstract fun getDao(): MyDao
+ }
+
+ @Dao
+ interface MyDao {
+ @Query("SELECT * FROM User JOIN Comment ON User.id = Comment.userId")
+ fun getUserCommentMap(): Map<User, List<Comment>>
+
+ @Query(
+ "SELECT User.id, name, Comment.id, userId, text " +
+ "FROM User JOIN Comment ON User.id = Comment.userId"
+ )
+ fun getUserCommentMapWithoutStarProjection(): Map<User, List<Comment>>
+
+ @SkipQueryVerification
+ @Query("SELECT * FROM User JOIN Comment ON User.id = Comment.userId")
+ fun getUserCommentMapWithoutQueryVerification(): Map<User, List<Comment>>
+ }
+
+ @Entity
+ data class User(
+ @PrimaryKey val id: Int,
+ val name: String,
+ )
+
+ @Entity
+ data class Comment(
+ @PrimaryKey val id: Int,
+ val userId: Int,
+ val text: String,
+ )
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(src),
+ expectedFilePath = getTestGoldenPath(testName)
+ )
+ }
+
+ @Test
+ fun entityRowAdapter() {
+ val testName = object {}.javaClass.enclosingMethod!!.name
+ val src = Source.kotlin(
+ "MyDao.kt",
+ """
+ import androidx.room.*
+
+ @Dao
+ interface MyDao {
+
+ @SkipQueryVerification // To make Room use EntityRowAdapter
+ @Query("SELECT * FROM MyEntity")
+ fun getEntity(): MyEntity
+
+ @SkipQueryVerification // To make Room use EntityRowAdapter
+ @Insert
+ fun addEntity(item: MyEntity)
+ }
+
+ @Entity
+ class MyEntity(
+ @PrimaryKey
+ val valuePrimitive: Long,
+ val valueBoolean: Boolean,
+ val valueString: String,
+ val valueNullableString: String?
+ ) {
+ var variablePrimitive: Long = 0
+ var variableNullableBoolean: Boolean? = null
+ var variableString: String = ""
+ var variableNullableString: String? = null
+ }
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(src, databaseSrc),
+ expectedFilePath = getTestGoldenPath(testName)
+ )
+ }
}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/entityRowAdapter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/entityRowAdapter.kt
new file mode 100644
index 0000000..a14eefe
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/entityRowAdapter.kt
@@ -0,0 +1,164 @@
+import android.database.Cursor
+import androidx.room.EntityInsertionAdapter
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndex
+import androidx.room.util.query
+import androidx.sqlite.db.SupportSQLiteStatement
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Boolean
+import kotlin.Int
+import kotlin.Long
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+ __db: RoomDatabase,
+) : MyDao {
+ private val __db: RoomDatabase
+
+ private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
+ init {
+ this.__db = __db
+ this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
+ public override fun createQuery(): String =
+ "INSERT OR ABORT INTO `MyEntity` (`valuePrimitive`,`valueBoolean`,`valueString`,`valueNullableString`,`variablePrimitive`,`variableNullableBoolean`,`variableString`,`variableNullableString`) VALUES (?,?,?,?,?,?,?,?)"
+
+ public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+ statement.bindLong(1, entity.valuePrimitive)
+ val _tmp: Int = if (entity.valueBoolean) 1 else 0
+ statement.bindLong(2, _tmp.toLong())
+ statement.bindString(3, entity.valueString)
+ if (entity.valueNullableString == null) {
+ statement.bindNull(4)
+ } else {
+ statement.bindString(4, entity.valueNullableString)
+ }
+ statement.bindLong(5, entity.variablePrimitive)
+ val _tmpVariableNullableBoolean: Boolean? = entity.variableNullableBoolean
+ val _tmp_1: Int? = _tmpVariableNullableBoolean?.let { if (it) 1 else 0 }
+ if (_tmp_1 == null) {
+ statement.bindNull(6)
+ } else {
+ statement.bindLong(6, _tmp_1.toLong())
+ }
+ statement.bindString(7, entity.variableString)
+ val _tmpVariableNullableString: String? = entity.variableNullableString
+ if (_tmpVariableNullableString == null) {
+ statement.bindNull(8)
+ } else {
+ statement.bindString(8, _tmpVariableNullableString)
+ }
+ }
+ }
+ }
+
+ public override fun addEntity(item: MyEntity): Unit {
+ __db.assertNotSuspendingTransaction()
+ __db.beginTransaction()
+ try {
+ __insertionAdapterOfMyEntity.insert(item)
+ __db.setTransactionSuccessful()
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public override fun getEntity(): MyEntity {
+ val _sql: String = "SELECT * FROM MyEntity"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _result: MyEntity
+ if (_cursor.moveToFirst()) {
+ _result = __entityCursorConverter_MyEntity(_cursor)
+ } else {
+ error("Cursor was empty, but expected a single item.")
+ }
+ return _result
+ } finally {
+ _cursor.close()
+ _statement.release()
+ }
+ }
+
+ private fun __entityCursorConverter_MyEntity(cursor: Cursor): MyEntity {
+ val _entity: MyEntity
+ val _cursorIndexOfValuePrimitive: Int = getColumnIndex(cursor, "valuePrimitive")
+ val _cursorIndexOfValueBoolean: Int = getColumnIndex(cursor, "valueBoolean")
+ val _cursorIndexOfValueString: Int = getColumnIndex(cursor, "valueString")
+ val _cursorIndexOfValueNullableString: Int = getColumnIndex(cursor, "valueNullableString")
+ val _cursorIndexOfVariablePrimitive: Int = getColumnIndex(cursor, "variablePrimitive")
+ val _cursorIndexOfVariableNullableBoolean: Int = getColumnIndex(cursor,
+ "variableNullableBoolean")
+ val _cursorIndexOfVariableString: Int = getColumnIndex(cursor, "variableString")
+ val _cursorIndexOfVariableNullableString: Int = getColumnIndex(cursor, "variableNullableString")
+ val _tmpValuePrimitive: Long
+ if (_cursorIndexOfValuePrimitive == -1) {
+ _tmpValuePrimitive = 0
+ } else {
+ _tmpValuePrimitive = cursor.getLong(_cursorIndexOfValuePrimitive)
+ }
+ val _tmpValueBoolean: Boolean
+ if (_cursorIndexOfValueBoolean == -1) {
+ _tmpValueBoolean = false
+ } else {
+ val _tmp: Int
+ _tmp = cursor.getInt(_cursorIndexOfValueBoolean)
+ _tmpValueBoolean = _tmp != 0
+ }
+ val _tmpValueString: String
+ if (_cursorIndexOfValueString == -1) {
+ error("Missing column 'valueString' for a non null value.")
+ } else {
+ _tmpValueString = cursor.getString(_cursorIndexOfValueString)
+ }
+ val _tmpValueNullableString: String?
+ if (_cursorIndexOfValueNullableString == -1) {
+ _tmpValueNullableString = null
+ } else {
+ if (cursor.isNull(_cursorIndexOfValueNullableString)) {
+ _tmpValueNullableString = null
+ } else {
+ _tmpValueNullableString = cursor.getString(_cursorIndexOfValueNullableString)
+ }
+ }
+ _entity = MyEntity(_tmpValuePrimitive,_tmpValueBoolean,_tmpValueString,_tmpValueNullableString)
+ if (_cursorIndexOfVariablePrimitive != -1) {
+ _entity.variablePrimitive = cursor.getLong(_cursorIndexOfVariablePrimitive)
+ }
+ if (_cursorIndexOfVariableNullableBoolean != -1) {
+ val _tmp_1: Int?
+ if (cursor.isNull(_cursorIndexOfVariableNullableBoolean)) {
+ _tmp_1 = null
+ } else {
+ _tmp_1 = cursor.getInt(_cursorIndexOfVariableNullableBoolean)
+ }
+ _entity.variableNullableBoolean = _tmp_1?.let { it != 0 }
+ }
+ if (_cursorIndexOfVariableString != -1) {
+ _entity.variableString = cursor.getString(_cursorIndexOfVariableString)
+ }
+ if (_cursorIndexOfVariableNullableString != -1) {
+ if (cursor.isNull(_cursorIndexOfVariableNullableString)) {
+ _entity.variableNullableString = null
+ } else {
+ _entity.variableNullableString = cursor.getString(_cursorIndexOfVariableNullableString)
+ }
+ }
+ return _entity
+ }
+
+ public companion object {
+ @JvmStatic
+ public fun getRequiredConverters(): List<Class<*>> = emptyList()
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt
index b34fd6a..ce7bf9f 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt
@@ -27,11 +27,18 @@
this.__db = __db
this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
public override fun createQuery(): String =
- "INSERT OR ABORT INTO `MyEntity` (`pk`,`variable`) VALUES (?,?)"
+ "INSERT OR ABORT INTO `MyEntity` (`pk`,`variablePrimitive`,`variableString`,`variableNullableString`) VALUES (?,?,?,?)"
public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
statement.bindLong(1, entity.pk.toLong())
- statement.bindLong(2, entity.variable)
+ statement.bindLong(2, entity.variablePrimitive)
+ statement.bindString(3, entity.variableString)
+ val _tmpVariableNullableString: String? = entity.variableNullableString
+ if (_tmpVariableNullableString == null) {
+ statement.bindNull(4)
+ } else {
+ statement.bindString(4, _tmpVariableNullableString)
+ }
}
}
}
@@ -54,13 +61,22 @@
val _cursor: Cursor = query(__db, _statement, false, null)
try {
val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
- val _cursorIndexOfVariable: Int = getColumnIndexOrThrow(_cursor, "variable")
+ val _cursorIndexOfVariablePrimitive: Int = getColumnIndexOrThrow(_cursor, "variablePrimitive")
+ val _cursorIndexOfVariableString: Int = getColumnIndexOrThrow(_cursor, "variableString")
+ val _cursorIndexOfVariableNullableString: Int = getColumnIndexOrThrow(_cursor,
+ "variableNullableString")
val _result: MyEntity
if (_cursor.moveToFirst()) {
val _tmpPk: Int
_tmpPk = _cursor.getInt(_cursorIndexOfPk)
_result = MyEntity(_tmpPk)
- _result.variable = _cursor.getLong(_cursorIndexOfVariable)
+ _result.variablePrimitive = _cursor.getLong(_cursorIndexOfVariablePrimitive)
+ _result.variableString = _cursor.getString(_cursorIndexOfVariableString)
+ if (_cursor.isNull(_cursorIndexOfVariableNullableString)) {
+ _result.variableNullableString = null
+ } else {
+ _result.variableNullableString = _cursor.getString(_cursorIndexOfVariableNullableString)
+ }
} else {
error("Cursor was empty, but expected a single item.")
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_map.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_map.kt
new file mode 100644
index 0000000..4d5d708
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_map.kt
@@ -0,0 +1,215 @@
+import android.database.Cursor
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.query
+import java.lang.Class
+import java.util.ArrayList
+import java.util.LinkedHashMap
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.collections.Map
+import kotlin.collections.MutableList
+import kotlin.collections.MutableMap
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+ __db: RoomDatabase,
+) : MyDao {
+ private val __db: RoomDatabase
+ init {
+ this.__db = __db
+ }
+
+ public override fun getSongsWithArtist(): Map<Song, Artist> {
+ val _sql: String = "SELECT * FROM Song JOIN Artist ON Song.artistKey = Artist.artistId"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+ val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+ val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+ val _result: MutableMap<Song, Artist> = LinkedHashMap<Song, Artist>()
+ while (_cursor.moveToNext()) {
+ val _key: Song
+ val _tmpSongId: String
+ _tmpSongId = _cursor.getString(_cursorIndexOfSongId)
+ val _tmpArtistKey: String
+ _tmpArtistKey = _cursor.getString(_cursorIndexOfArtistKey)
+ _key = Song(_tmpSongId,_tmpArtistKey)
+ if (_cursor.isNull(_cursorIndexOfArtistId)) {
+ error("Missing value for a key.")
+ }
+ val _value: Artist
+ val _tmpArtistId: String
+ _tmpArtistId = _cursor.getString(_cursorIndexOfArtistId)
+ _value = Artist(_tmpArtistId)
+ if (!_result.containsKey(_key)) {
+ _result.put(_key, _value)
+ }
+ }
+ return _result
+ } finally {
+ _cursor.close()
+ _statement.release()
+ }
+ }
+
+ public override fun getSongsWithNullableArtist(): Map<Song, Artist?> {
+ val _sql: String = "SELECT * FROM Song JOIN Artist ON Song.artistKey = Artist.artistId"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+ val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+ val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+ val _result: MutableMap<Song, Artist?> = LinkedHashMap<Song, Artist?>()
+ while (_cursor.moveToNext()) {
+ val _key: Song
+ val _tmpSongId: String
+ _tmpSongId = _cursor.getString(_cursorIndexOfSongId)
+ val _tmpArtistKey: String
+ _tmpArtistKey = _cursor.getString(_cursorIndexOfArtistKey)
+ _key = Song(_tmpSongId,_tmpArtistKey)
+ if (_cursor.isNull(_cursorIndexOfArtistId)) {
+ _result.put(_key, null)
+ continue
+ }
+ val _value: Artist?
+ val _tmpArtistId: String
+ _tmpArtistId = _cursor.getString(_cursorIndexOfArtistId)
+ _value = Artist(_tmpArtistId)
+ if (!_result.containsKey(_key)) {
+ _result.put(_key, _value)
+ }
+ }
+ return _result
+ } finally {
+ _cursor.close()
+ _statement.release()
+ }
+ }
+
+ public override fun getArtistWithSongs(): Map<Artist, List<Song>> {
+ val _sql: String = "SELECT * FROM Artist JOIN Song ON Artist.artistId = Song.artistKey"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+ val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+ val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+ val _result: MutableMap<Artist, MutableList<Song>> =
+ LinkedHashMap<Artist, MutableList<Song>>()
+ while (_cursor.moveToNext()) {
+ val _key: Artist
+ val _tmpArtistId: String
+ _tmpArtistId = _cursor.getString(_cursorIndexOfArtistId)
+ _key = Artist(_tmpArtistId)
+ val _values: MutableList<Song>
+ if (_result.containsKey(_key)) {
+ _values = _result.getValue(_key)
+ } else {
+ _values = ArrayList<Song>()
+ _result.put(_key, _values)
+ }
+ if (_cursor.isNull(_cursorIndexOfSongId) && _cursor.isNull(_cursorIndexOfArtistKey)) {
+ continue
+ }
+ val _value: Song
+ val _tmpSongId: String
+ _tmpSongId = _cursor.getString(_cursorIndexOfSongId)
+ val _tmpArtistKey: String
+ _tmpArtistKey = _cursor.getString(_cursorIndexOfArtistKey)
+ _value = Song(_tmpSongId,_tmpArtistKey)
+ _values.add(_value)
+ }
+ return _result
+ } finally {
+ _cursor.close()
+ _statement.release()
+ }
+ }
+
+ public override fun getArtistSongCount(): Map<Artist, Int> {
+ val _sql: String =
+ "SELECT Artist.*, COUNT(songId) as songCount FROM Artist JOIN Song ON Artist.artistId = Song.artistKey GROUP BY artistId"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+ val _columnIndexOfSongCount: Int = getColumnIndexOrThrow(_cursor, "songCount")
+ val _result: MutableMap<Artist, Int> = LinkedHashMap<Artist, Int>()
+ while (_cursor.moveToNext()) {
+ val _key: Artist
+ val _tmpArtistId: String
+ _tmpArtistId = _cursor.getString(_cursorIndexOfArtistId)
+ _key = Artist(_tmpArtistId)
+ if (_cursor.isNull(_columnIndexOfSongCount)) {
+ error("Missing value for a key.")
+ }
+ val _value: Int
+ val _tmp: Int
+ _tmp = _cursor.getInt(_columnIndexOfSongCount)
+ _value = _tmp
+ if (!_result.containsKey(_key)) {
+ _result.put(_key, _value)
+ }
+ }
+ return _result
+ } finally {
+ _cursor.close()
+ _statement.release()
+ }
+ }
+
+ public override fun getArtistWithSongIds(): Map<Artist, List<String>> {
+ val _sql: String = "SELECT * FROM Artist JOIN Song ON Artist.artistId = Song.artistKey"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+ val _columnIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+ val _result: MutableMap<Artist, MutableList<String>> =
+ LinkedHashMap<Artist, MutableList<String>>()
+ while (_cursor.moveToNext()) {
+ val _key: Artist
+ val _tmpArtistId: String
+ _tmpArtistId = _cursor.getString(_cursorIndexOfArtistId)
+ _key = Artist(_tmpArtistId)
+ val _values: MutableList<String>
+ if (_result.containsKey(_key)) {
+ _values = _result.getValue(_key)
+ } else {
+ _values = ArrayList<String>()
+ _result.put(_key, _values)
+ }
+ if (_cursor.isNull(_columnIndexOfSongId)) {
+ continue
+ }
+ val _value: String
+ _value = _cursor.getString(_columnIndexOfSongId)
+ _values.add(_value)
+ }
+ return _result
+ } finally {
+ _cursor.close()
+ _statement.release()
+ }
+ }
+
+ public companion object {
+ @JvmStatic
+ public fun getRequiredConverters(): List<Class<*>> = emptyList()
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_map_ambiguousIndexAdapter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_map_ambiguousIndexAdapter.kt
new file mode 100644
index 0000000..9217ac6
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_map_ambiguousIndexAdapter.kt
@@ -0,0 +1,216 @@
+import android.database.Cursor
+import androidx.room.AmbiguousColumnResolver
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndex
+import androidx.room.util.query
+import androidx.room.util.wrapMappedColumns
+import java.lang.Class
+import java.util.ArrayList
+import java.util.LinkedHashMap
+import javax.`annotation`.processing.Generated
+import kotlin.Array
+import kotlin.Int
+import kotlin.IntArray
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.collections.Map
+import kotlin.collections.MutableList
+import kotlin.collections.MutableMap
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+ __db: RoomDatabase,
+) : MyDao {
+ private val __db: RoomDatabase
+ init {
+ this.__db = __db
+ }
+
+ public override fun getUserCommentMap(): Map<User, List<Comment>> {
+ val _sql: String = "SELECT * FROM User JOIN Comment ON User.id = Comment.userId"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _cursorIndices: Array<IntArray> =
+ AmbiguousColumnResolver.resolve(_cursor.getColumnNames(), arrayOf(arrayOf("id", "name"),
+ arrayOf("id", "userId", "text")))
+ val _result: MutableMap<User, MutableList<Comment>> =
+ LinkedHashMap<User, MutableList<Comment>>()
+ while (_cursor.moveToNext()) {
+ val _key: User
+ val _tmpId: Int
+ _tmpId = _cursor.getInt(_cursorIndices[0][0])
+ val _tmpName: String
+ _tmpName = _cursor.getString(_cursorIndices[0][1])
+ _key = User(_tmpId,_tmpName)
+ val _values: MutableList<Comment>
+ if (_result.containsKey(_key)) {
+ _values = _result.getValue(_key)
+ } else {
+ _values = ArrayList<Comment>()
+ _result.put(_key, _values)
+ }
+ if (_cursor.isNull(_cursorIndices[1][0]) && _cursor.isNull(_cursorIndices[1][1]) &&
+ _cursor.isNull(_cursorIndices[1][2])) {
+ continue
+ }
+ val _value: Comment
+ val _tmpId_1: Int
+ _tmpId_1 = _cursor.getInt(_cursorIndices[1][0])
+ val _tmpUserId: Int
+ _tmpUserId = _cursor.getInt(_cursorIndices[1][1])
+ val _tmpText: String
+ _tmpText = _cursor.getString(_cursorIndices[1][2])
+ _value = Comment(_tmpId_1,_tmpUserId,_tmpText)
+ _values.add(_value)
+ }
+ return _result
+ } finally {
+ _cursor.close()
+ _statement.release()
+ }
+ }
+
+ public override fun getUserCommentMapWithoutStarProjection(): Map<User, List<Comment>> {
+ val _sql: String =
+ "SELECT User.id, name, Comment.id, userId, text FROM User JOIN Comment ON User.id = Comment.userId"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _cursorIndices: Array<IntArray> = arrayOf(intArrayOf(0, 1), intArrayOf(2, 3, 4))
+ val _result: MutableMap<User, MutableList<Comment>> =
+ LinkedHashMap<User, MutableList<Comment>>()
+ while (_cursor.moveToNext()) {
+ val _key: User
+ val _tmpId: Int
+ _tmpId = _cursor.getInt(_cursorIndices[0][0])
+ val _tmpName: String
+ _tmpName = _cursor.getString(_cursorIndices[0][1])
+ _key = User(_tmpId,_tmpName)
+ val _values: MutableList<Comment>
+ if (_result.containsKey(_key)) {
+ _values = _result.getValue(_key)
+ } else {
+ _values = ArrayList<Comment>()
+ _result.put(_key, _values)
+ }
+ if (_cursor.isNull(_cursorIndices[1][0]) && _cursor.isNull(_cursorIndices[1][1]) &&
+ _cursor.isNull(_cursorIndices[1][2])) {
+ continue
+ }
+ val _value: Comment
+ val _tmpId_1: Int
+ _tmpId_1 = _cursor.getInt(_cursorIndices[1][0])
+ val _tmpUserId: Int
+ _tmpUserId = _cursor.getInt(_cursorIndices[1][1])
+ val _tmpText: String
+ _tmpText = _cursor.getString(_cursorIndices[1][2])
+ _value = Comment(_tmpId_1,_tmpUserId,_tmpText)
+ _values.add(_value)
+ }
+ return _result
+ } finally {
+ _cursor.close()
+ _statement.release()
+ }
+ }
+
+ public override fun getUserCommentMapWithoutQueryVerification(): Map<User, List<Comment>> {
+ val _sql: String = "SELECT * FROM User JOIN Comment ON User.id = Comment.userId"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _cursorIndices: Array<IntArray> =
+ AmbiguousColumnResolver.resolve(_cursor.getColumnNames(), arrayOf(arrayOf("id", "name"),
+ arrayOf("id", "userId", "text")))
+ val _wrappedCursor: Cursor = wrapMappedColumns(_cursor, arrayOf("id", "name"),
+ intArrayOf(_cursorIndices[0][0], _cursorIndices[0][1]))
+ val _wrappedCursor_1: Cursor = wrapMappedColumns(_cursor, arrayOf("id", "userId", "text"),
+ intArrayOf(_cursorIndices[1][0], _cursorIndices[1][1], _cursorIndices[1][2]))
+ val _result: MutableMap<User, MutableList<Comment>> =
+ LinkedHashMap<User, MutableList<Comment>>()
+ while (_cursor.moveToNext()) {
+ val _key: User
+ _key = __entityCursorConverter_User(_wrappedCursor)
+ val _values: MutableList<Comment>
+ if (_result.containsKey(_key)) {
+ _values = _result.getValue(_key)
+ } else {
+ _values = ArrayList<Comment>()
+ _result.put(_key, _values)
+ }
+ if (_cursor.isNull(_cursorIndices[1][0]) && _cursor.isNull(_cursorIndices[1][1]) &&
+ _cursor.isNull(_cursorIndices[1][2])) {
+ continue
+ }
+ val _value: Comment
+ _value = __entityCursorConverter_Comment(_wrappedCursor_1)
+ _values.add(_value)
+ }
+ return _result
+ } finally {
+ _cursor.close()
+ _statement.release()
+ }
+ }
+
+ private fun __entityCursorConverter_User(cursor: Cursor): User {
+ val _entity: User
+ val _cursorIndexOfId: Int = getColumnIndex(cursor, "id")
+ val _cursorIndexOfName: Int = getColumnIndex(cursor, "name")
+ val _tmpId: Int
+ if (_cursorIndexOfId == -1) {
+ _tmpId = 0
+ } else {
+ _tmpId = cursor.getInt(_cursorIndexOfId)
+ }
+ val _tmpName: String
+ if (_cursorIndexOfName == -1) {
+ error("Missing column 'name' for a non null value.")
+ } else {
+ _tmpName = cursor.getString(_cursorIndexOfName)
+ }
+ _entity = User(_tmpId,_tmpName)
+ return _entity
+ }
+
+ private fun __entityCursorConverter_Comment(cursor: Cursor): Comment {
+ val _entity: Comment
+ val _cursorIndexOfId: Int = getColumnIndex(cursor, "id")
+ val _cursorIndexOfUserId: Int = getColumnIndex(cursor, "userId")
+ val _cursorIndexOfText: Int = getColumnIndex(cursor, "text")
+ val _tmpId: Int
+ if (_cursorIndexOfId == -1) {
+ _tmpId = 0
+ } else {
+ _tmpId = cursor.getInt(_cursorIndexOfId)
+ }
+ val _tmpUserId: Int
+ if (_cursorIndexOfUserId == -1) {
+ _tmpUserId = 0
+ } else {
+ _tmpUserId = cursor.getInt(_cursorIndexOfUserId)
+ }
+ val _tmpText: String
+ if (_cursorIndexOfText == -1) {
+ error("Missing column 'text' for a non null value.")
+ } else {
+ _tmpText = cursor.getString(_cursorIndexOfText)
+ }
+ _entity = Comment(_tmpId,_tmpUserId,_tmpText)
+ return _entity
+ }
+
+ public companion object {
+ @JvmStatic
+ public fun getRequiredConverters(): List<Class<*>> = emptyList()
+ }
+}
\ No newline at end of file
diff --git a/room/room-runtime/api/public_plus_experimental_current.txt b/room/room-runtime/api/public_plus_experimental_current.txt
index 68990e9..a7b3c40 100644
--- a/room/room-runtime/api/public_plus_experimental_current.txt
+++ b/room/room-runtime/api/public_plus_experimental_current.txt
@@ -27,7 +27,7 @@
public final class EntityUpsertionAdapterKt {
}
- @RequiresOptIn @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalRoomApi {
+ @RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalRoomApi {
}
public class InvalidationTracker {
diff --git a/room/room-runtime/src/main/java/androidx/room/ExperimentalRoomApi.kt b/room/room-runtime/src/main/java/androidx/room/ExperimentalRoomApi.kt
index 1535b4f..965bdde 100644
--- a/room/room-runtime/src/main/java/androidx/room/ExperimentalRoomApi.kt
+++ b/room/room-runtime/src/main/java/androidx/room/ExperimentalRoomApi.kt
@@ -26,4 +26,5 @@
)
@Suppress("UnsafeOptInUsageError")
@RequiresOptIn
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalRoomApi
\ No newline at end of file
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
index 5459765..3f64139 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
@@ -28,6 +28,7 @@
import android.view.KeyEvent;
import android.widget.TextView;
+import androidx.test.filters.FlakyTest;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -440,6 +441,8 @@
validateMainActivityXml(xml);
}
+
+ @FlakyTest(bugId = 259299647)
@Test
public void testWaitForWindowUpdate() {
launchTestActivity(WaitTestActivity.class);
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
index 6a1dc5d..c88a125 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
@@ -260,7 +260,7 @@
UiObject2 nestedObject = mDevice.findObject(By.res(TEST_APP, "nested_elements"));
List<UiObject2> children = nestedObject.getChildren();
assertEquals(2, children.size());
- Set<String> childrenClassNames = new HashSet<String>();
+ Set<String> childrenClassNames = new HashSet<>();
childrenClassNames.add(children.get(0).getClassName());
childrenClassNames.add(children.get(1).getClassName());
assertTrue(childrenClassNames.contains("android.widget.TextView"));
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoDumper.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoDumper.java
index 94ef7cf..cd4c2a2 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoDumper.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoDumper.java
@@ -166,11 +166,7 @@
}
private static String safeCharSeqToString(CharSequence cs) {
- if (cs == null)
- return "";
- else {
- return stripInvalidXMLChars(cs);
- }
+ return cs == null ? "" : stripInvalidXMLChars(cs);
}
private static String stripInvalidXMLChars(CharSequence cs) {
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
index ba9e851..2f7e288 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
@@ -34,9 +34,9 @@
private static final String TAG = ByMatcher.class.getSimpleName();
- private UiDevice mDevice;
- private BySelector mSelector;
- private boolean mShortCircuit;
+ private final UiDevice mDevice;
+ private final BySelector mSelector;
+ private final boolean mShortCircuit;
/**
* Constructs a new {@link ByMatcher} instance. Used by
@@ -89,7 +89,7 @@
static List<AccessibilityNodeInfo> findMatches(UiDevice device, BySelector selector,
AccessibilityNodeInfo... roots) {
- List<AccessibilityNodeInfo> ret = new ArrayList<AccessibilityNodeInfo>();
+ List<AccessibilityNodeInfo> ret = new ArrayList<>();
ByMatcher matcher = new ByMatcher(device, selector, false);
for (AccessibilityNodeInfo root : roots) {
ret.addAll(matcher.findMatches(root));
@@ -108,13 +108,13 @@
*/
private List<AccessibilityNodeInfo> findMatches(AccessibilityNodeInfo root) {
List<AccessibilityNodeInfo> ret =
- findMatches(root, 0, 0, new SinglyLinkedList<PartialMatch>());
+ findMatches(root, 0, 0, new SinglyLinkedList<>());
// If no matches were found
if (ret.isEmpty()) {
// Run watchers and retry
mDevice.runWatchers();
- ret = findMatches(root, 0, 0, new SinglyLinkedList<PartialMatch>());
+ ret = findMatches(root, 0, 0, new SinglyLinkedList<>());
}
return ret;
@@ -134,7 +134,7 @@
*/
private List<AccessibilityNodeInfo> findMatches(AccessibilityNodeInfo node,
int index, int depth, SinglyLinkedList<PartialMatch> partialMatches) {
- List<AccessibilityNodeInfo> ret = new ArrayList<AccessibilityNodeInfo>();
+ List<AccessibilityNodeInfo> ret = new ArrayList<>();
// Don't bother searching the subtree if it is not visible
if (!node.isVisibleToUser()) {
@@ -211,7 +211,7 @@
static private class PartialMatch {
private final int matchDepth;
private final BySelector matchSelector;
- private final List<PartialMatch> partialMatches = new ArrayList<PartialMatch>();
+ private final List<PartialMatch> partialMatches = new ArrayList<>();
/**
* Private constructor. Should be instanciated by calling the
@@ -315,7 +315,7 @@
*/
public boolean finalizeMatch() {
// Find out which of our child selectors were fully matched
- Set<BySelector> matches = new HashSet<BySelector>();
+ Set<BySelector> matches = new HashSet<>();
for (PartialMatch p : partialMatches) {
if (p.finalizeMatch()) {
matches.add(p.matchSelector);
@@ -346,7 +346,7 @@
/** Returns a new list obtained by prepending {@code data} to {@code rest}. */
public static <T> SinglyLinkedList<T> prepend(T data, SinglyLinkedList<T> rest) {
- return new SinglyLinkedList<T>(new Node<T>(data, rest.mHead));
+ return new SinglyLinkedList<>(new Node<>(data, rest.mHead));
}
@Override
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
index 684466e..8ee007d 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
@@ -51,7 +51,7 @@
Integer mMaxDepth;
// Child selectors
- List<BySelector> mChildSelectors = new LinkedList<BySelector>();
+ List<BySelector> mChildSelectors = new LinkedList<>();
/** Clients should not instanciate this class directly. Use the {@link By} factory class instead. */
@@ -79,6 +79,9 @@
mScrollable = original.mScrollable;
mSelected = original.mSelected;
+ mMinDepth = original.mMinDepth;
+ mMaxDepth = original.mMaxDepth;
+
for (BySelector childSelector : original.mChildSelectors) {
mChildSelectors.add(new BySelector(childSelector));
}
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
index 23add70..29aa8c0 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
@@ -57,7 +57,7 @@
}
}
- private UiDevice mDevice;
+ private final UiDevice mDevice;
/** Comparator for sorting PointerGestures by start times. */
private static final Comparator<PointerGesture> START_TIME_COMPARATOR =
@@ -108,19 +108,19 @@
public void performGesture(PointerGesture ... gestures) {
// Initialize pointers
int count = 0;
- Map<PointerGesture, Pointer> pointers = new HashMap<PointerGesture, Pointer>();
+ Map<PointerGesture, Pointer> pointers = new HashMap<>();
for (PointerGesture g : gestures) {
pointers.put(g, new Pointer(count++, g.start()));
}
// Initialize MotionEvent arrays
- List<PointerProperties> properties = new ArrayList<PointerProperties>();
- List<PointerCoords> coordinates = new ArrayList<PointerCoords>();
+ List<PointerProperties> properties = new ArrayList<>();
+ List<PointerCoords> coordinates = new ArrayList<>();
// Track active and pending gestures
- PriorityQueue<PointerGesture> active = new PriorityQueue<PointerGesture>(gestures.length,
+ PriorityQueue<PointerGesture> active = new PriorityQueue<>(gestures.length,
END_TIME_COMPARATOR);
- PriorityQueue<PointerGesture> pending = new PriorityQueue<PointerGesture>(gestures.length,
+ PriorityQueue<PointerGesture> pending = new PriorityQueue<>(gestures.length,
START_TIME_COMPARATOR);
pending.addAll(Arrays.asList(gestures));
@@ -204,8 +204,8 @@
private static MotionEvent getMotionEvent(long downTime, long eventTime, int action,
List<PointerProperties> properties, List<PointerCoords> coordinates, int displayId) {
- PointerProperties[] props = properties.toArray(new PointerProperties[properties.size()]);
- PointerCoords[] coords = coordinates.toArray(new PointerCoords[coordinates.size()]);
+ PointerProperties[] props = properties.toArray(new PointerProperties[0]);
+ PointerCoords[] coords = coordinates.toArray(new PointerCoords[0]);
final MotionEvent ev = MotionEvent.obtain(
downTime, eventTime, action, props.length, props, coords,
0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
@@ -254,7 +254,7 @@
/** Runnable wrapper around a {@link GestureController#performGesture} call. */
private class GestureRunnable implements Runnable {
- private PointerGesture[] mGestures;
+ private final PointerGesture[] mGestures;
public GestureRunnable(PointerGesture[] gestures) {
mGestures = gestures;
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InstrumentationAutomationSupport.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InstrumentationAutomationSupport.java
index 3390113..b69d9d2 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InstrumentationAutomationSupport.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InstrumentationAutomationSupport.java
@@ -30,7 +30,7 @@
*/
class InstrumentationAutomationSupport implements IAutomationSupport {
- private Instrumentation mInstrumentation;
+ private final Instrumentation mInstrumentation;
InstrumentationAutomationSupport(Instrumentation instrumentation) {
mInstrumentation = instrumentation;
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
index 2377af4..2881d27 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
@@ -135,11 +135,7 @@
mMask &= ~t.getEventType();
// Since we're waiting for all events to be matched at least once
- if (mMask != 0)
- return false;
-
- // all matched
- return true;
+ return mMask == 0;
}
// no match yet
@@ -186,19 +182,16 @@
*/
public boolean sendKeyAndWaitForEvent(final int keyCode, final int metaState,
final int eventType, long timeout) {
- Runnable command = new Runnable() {
- @Override
- public void run() {
- final long eventTime = SystemClock.uptimeMillis();
- KeyEvent downEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
+ Runnable command = () -> {
+ final long eventTime = SystemClock.uptimeMillis();
+ KeyEvent downEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
+ keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
+ InputDevice.SOURCE_KEYBOARD);
+ if (injectEventSync(downEvent)) {
+ KeyEvent upEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
InputDevice.SOURCE_KEYBOARD);
- if (injectEventSync(downEvent)) {
- KeyEvent upEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
- keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
- InputDevice.SOURCE_KEYBOARD);
- injectEventSync(upEvent);
- }
+ injectEventSync(upEvent);
}
};
@@ -270,13 +263,10 @@
* @return Runnable
*/
private Runnable clickRunnable(final int x, final int y) {
- return new Runnable() {
- @Override
- public void run() {
- if(touchDown(x, y)) {
- SystemClock.sleep(REGULAR_CLICK_LENGTH);
- touchUp(x, y);
- }
+ return () -> {
+ if (touchDown(x, y)) {
+ SystemClock.sleep(REGULAR_CLICK_LENGTH);
+ touchUp(x, y);
}
};
}
@@ -290,13 +280,10 @@
* @return Runnable
*/
private Runnable longTapRunnable(final int x, final int y) {
- return new Runnable() {
- @Override
- public void run() {
- if(touchDown(x, y)) {
- SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
- touchUp(x, y);
- }
+ return () -> {
+ if (touchDown(x, y)) {
+ SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+ touchUp(x, y);
}
};
}
@@ -384,16 +371,11 @@
Log.d(LOG_TAG, "scrollSwipe (" + downX + ", " + downY + ", " + upX + ", "
+ upY + ", " + steps +")");
- Runnable command = new Runnable() {
- @Override
- public void run() {
- swipe(downX, downY, upX, upY, steps);
- }
- };
+ Runnable command = () -> swipe(downX, downY, upX, upY, steps);
// Collect all accessibility events generated during the swipe command and get the
// last event
- ArrayList<AccessibilityEvent> events = new ArrayList<AccessibilityEvent>();
+ ArrayList<AccessibilityEvent> events = new ArrayList<>();
runAndWaitForEvents(command,
new EventCollectingPredicate(AccessibilityEvent.TYPE_VIEW_SCROLLED, events),
Configurator.getInstance().getScrollAcknowledgmentTimeout());
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/PointerGesture.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/PointerGesture.java
index 40883a1..9cc5399 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/PointerGesture.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/PointerGesture.java
@@ -26,7 +26,7 @@
*/
class PointerGesture {
// The list of actions that make up this gesture.
- private final Deque<PointerAction> mActions = new ArrayDeque<PointerAction>();
+ private final Deque<PointerAction> mActions = new ArrayDeque<>();
private final long mDelay;
private final int mDisplayId;
private long mDuration;
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/QueryController.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/QueryController.java
index 5ddfce9..d876a72 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/QueryController.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/QueryController.java
@@ -62,30 +62,31 @@
String mLastTraversedText = "";
- private OnAccessibilityEventListener mEventListener = new OnAccessibilityEventListener() {
- @Override
- public void onAccessibilityEvent(AccessibilityEvent event) {
- synchronized (mLock) {
- switch(event.getEventType()) {
- case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
- // don't trust event.getText(), check for nulls
- if (event.getText() != null && event.getText().size() > 0) {
- if(event.getText().get(0) != null)
- mLastActivityName = event.getText().get(0).toString();
+ private final OnAccessibilityEventListener mEventListener = event -> {
+ synchronized (mLock) {
+ switch(event.getEventType()) {
+ case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
+ // don't trust event.getText(), check for nulls
+ if (event.getText() != null && event.getText().size() > 0) {
+ if (event.getText().get(0) != null) {
+ mLastActivityName = event.getText().get(0).toString();
}
- break;
- case AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY:
- // don't trust event.getText(), check for nulls
- if (event.getText() != null && event.getText().size() > 0)
- if(event.getText().get(0) != null)
- mLastTraversedText = event.getText().get(0).toString();
- if (DEBUG)
- Log.d(LOG_TAG, "Last text selection reported: " +
- mLastTraversedText);
- break;
- }
- mLock.notifyAll();
+ }
+ break;
+ case AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY:
+ // don't trust event.getText(), check for nulls
+ if (event.getText() != null && event.getText().size() > 0) {
+ if (event.getText().get(0) != null) {
+ mLastTraversedText = event.getText().get(0).toString();
+ }
+ }
+ if (DEBUG) {
+ Log.d(LOG_TAG, "Last text selection reported: "
+ + mLastTraversedText);
+ }
+ break;
}
+ mLock.notifyAll();
}
};
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
index e330872..d2e6cbf 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
@@ -147,7 +147,7 @@
@Override
@NonNull
public List<UiObject2> findObjects(@NonNull BySelector selector) {
- List<UiObject2> ret = new ArrayList<UiObject2>();
+ List<UiObject2> ret = new ArrayList<>();
for (AccessibilityNodeInfo node : ByMatcher.findMatches(this, selector, getWindowRoots())) {
ret.add(new UiObject2(this, selector, node));
}
@@ -200,7 +200,7 @@
/** Proxy class which acts as an {@link AccessibilityEventFilter} and forwards calls to an
* {@link EventCondition} instance. */
private static class EventForwardingFilter implements AccessibilityEventFilter {
- private EventCondition<?> mCondition;
+ private final EventCondition<?> mCondition;
public EventForwardingFilter(EventCondition<?> condition) {
mCondition = condition;
@@ -898,20 +898,13 @@
return false;
}
}
- Runnable emptyRunnable = new Runnable() {
- @Override
- public void run() {
+ Runnable emptyRunnable = () -> {};
+ AccessibilityEventFilter checkWindowUpdate = t -> {
+ if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
+ return packageName == null || (t.getPackageName() != null
+ && packageName.contentEquals(t.getPackageName()));
}
- };
- AccessibilityEventFilter checkWindowUpdate = new AccessibilityEventFilter() {
- @Override
- public boolean accept(AccessibilityEvent t) {
- if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
- return packageName == null || (t.getPackageName() != null
- && packageName.contentEquals(t.getPackageName()));
- }
- return false;
- }
+ return false;
};
Log.d(TAG, String.format("Waiting %dms for window update of package %s.", timeout,
packageName));
@@ -1070,7 +1063,7 @@
roots.add(root);
}
}
- return roots.toArray(new AccessibilityNodeInfo[roots.size()]);
+ return roots.toArray(new AccessibilityNodeInfo[0]);
}
Instrumentation getInstrumentation() {
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
index 451f493..780f82d 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
@@ -895,10 +895,7 @@
*/
public boolean waitForExists(long timeout) {
Log.d(TAG, String.format("Waiting %dms for %s.", timeout, mUiSelector));
- if(findAccessibilityNodeInfo(timeout) != null) {
- return true;
- }
- return false;
+ return findAccessibilityNodeInfo(timeout) != null;
}
/**
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiScrollable.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiScrollable.java
index 8dcfbf5..1db9068 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiScrollable.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiScrollable.java
@@ -86,10 +86,7 @@
* @return true if found else false
*/
protected boolean exists(@NonNull UiSelector selector) {
- if(getQueryController().findAccessibilityNodeInfo(selector) != null) {
- return true;
- }
- return false;
+ return getQueryController().findAccessibilityNodeInfo(selector) != null;
}
/**
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java
index e652c7e..5cb978a 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java
@@ -63,7 +63,7 @@
static final int SELECTOR_CHECKABLE = 30;
static final int SELECTOR_RESOURCE_ID_REGEX = 31;
- private SparseArray<Object> mSelectorAttributes = new SparseArray<Object>();
+ private SparseArray<Object> mSelectorAttributes = new SparseArray<>();
public UiSelector() {
}
@@ -109,9 +109,7 @@
*/
@NonNull
public UiSelector text(@NonNull String text) {
- if (text == null) {
- throw new IllegalArgumentException("text cannot be null");
- }
+ checkNotNull(text, "text cannot be null");
return buildSelector(SELECTOR_TEXT, text);
}
@@ -127,9 +125,7 @@
*/
@NonNull
public UiSelector textMatches(@NonNull String regex) {
- if (regex == null) {
- throw new IllegalArgumentException("regex cannot be null");
- }
+ checkNotNull(regex, "regex cannot be null");
return buildSelector(SELECTOR_TEXT_REGEX, Pattern.compile(regex, Pattern.DOTALL));
}
@@ -144,9 +140,7 @@
*/
@NonNull
public UiSelector textStartsWith(@NonNull String text) {
- if (text == null) {
- throw new IllegalArgumentException("text cannot be null");
- }
+ checkNotNull(text, "text cannot be null");
return buildSelector(SELECTOR_START_TEXT, text);
}
@@ -161,9 +155,7 @@
*/
@NonNull
public UiSelector textContains(@NonNull String text) {
- if (text == null) {
- throw new IllegalArgumentException("text cannot be null");
- }
+ checkNotNull(text, "text cannot be null");
return buildSelector(SELECTOR_CONTAINS_TEXT, text);
}
@@ -176,9 +168,7 @@
*/
@NonNull
public UiSelector className(@NonNull String className) {
- if (className == null) {
- throw new IllegalArgumentException("className cannot be null");
- }
+ checkNotNull(className, "className cannot be null");
return buildSelector(SELECTOR_CLASS, className);
}
@@ -191,9 +181,7 @@
*/
@NonNull
public UiSelector classNameMatches(@NonNull String regex) {
- if (regex == null) {
- throw new IllegalArgumentException("regex cannot be null");
- }
+ checkNotNull(regex, "regex cannot be null");
return buildSelector(SELECTOR_CLASS_REGEX, Pattern.compile(regex));
}
@@ -206,9 +194,7 @@
*/
@NonNull
public <T> UiSelector className(@NonNull Class<T> type) {
- if (type == null) {
- throw new IllegalArgumentException("type cannot be null");
- }
+ checkNotNull(type, "type cannot be null");
return buildSelector(SELECTOR_CLASS, type.getName());
}
@@ -230,9 +216,7 @@
*/
@NonNull
public UiSelector description(@NonNull String desc) {
- if (desc == null) {
- throw new IllegalArgumentException("desc cannot be null");
- }
+ checkNotNull(desc, "desc cannot be null");
return buildSelector(SELECTOR_DESCRIPTION, desc);
}
@@ -252,9 +236,7 @@
*/
@NonNull
public UiSelector descriptionMatches(@NonNull String regex) {
- if (regex == null) {
- throw new IllegalArgumentException("regex cannot be null");
- }
+ checkNotNull(regex, "regex cannot be null");
return buildSelector(SELECTOR_DESCRIPTION_REGEX, Pattern.compile(regex, Pattern.DOTALL));
}
@@ -276,9 +258,7 @@
*/
@NonNull
public UiSelector descriptionStartsWith(@NonNull String desc) {
- if (desc == null) {
- throw new IllegalArgumentException("desc cannot be null");
- }
+ checkNotNull(desc, "desc cannot be null");
return buildSelector(SELECTOR_START_DESCRIPTION, desc);
}
@@ -300,9 +280,7 @@
*/
@NonNull
public UiSelector descriptionContains(@NonNull String desc) {
- if (desc == null) {
- throw new IllegalArgumentException("desc cannot be null");
- }
+ checkNotNull(desc, "desc cannot be null");
return buildSelector(SELECTOR_CONTAINS_DESCRIPTION, desc);
}
@@ -314,9 +292,7 @@
*/
@NonNull
public UiSelector resourceId(@NonNull String id) {
- if (id == null) {
- throw new IllegalArgumentException("id cannot be null");
- }
+ checkNotNull(id, "id cannot be null");
return buildSelector(SELECTOR_RESOURCE_ID, id);
}
@@ -329,9 +305,7 @@
*/
@NonNull
public UiSelector resourceIdMatches(@NonNull String regex) {
- if (regex == null) {
- throw new IllegalArgumentException("regex cannot be null");
- }
+ checkNotNull(regex, "regex cannot be null");
return buildSelector(SELECTOR_RESOURCE_ID_REGEX, Pattern.compile(regex));
}
@@ -563,9 +537,7 @@
*/
@NonNull
public UiSelector childSelector(@NonNull UiSelector selector) {
- if (selector == null) {
- throw new IllegalArgumentException("selector cannot be null");
- }
+ checkNotNull(selector, "selector cannot be null");
return buildSelector(SELECTOR_CHILD, selector);
}
@@ -589,9 +561,7 @@
*/
@NonNull
public UiSelector fromParent(@NonNull UiSelector selector) {
- if (selector == null) {
- throw new IllegalArgumentException("selector cannot be null");
- }
+ checkNotNull(selector, "selector cannot be null");
return buildSelector(SELECTOR_PARENT, selector);
}
@@ -604,9 +574,7 @@
*/
@NonNull
public UiSelector packageName(@NonNull String name) {
- if (name == null) {
- throw new IllegalArgumentException("name cannot be null");
- }
+ checkNotNull(name, "name cannot be null");
return buildSelector(SELECTOR_PACKAGE_NAME, name);
}
@@ -619,9 +587,7 @@
*/
@NonNull
public UiSelector packageNameMatches(@NonNull String regex) {
- if (regex == null) {
- throw new IllegalArgumentException("regex cannot be null");
- }
+ checkNotNull(regex, "regex cannot be null");
return buildSelector(SELECTOR_PACKAGE_NAME_REGEX, Pattern.compile(regex));
}
@@ -880,39 +846,24 @@
* @return true if is leaf.
*/
boolean isLeaf() {
- if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) < 0 &&
- mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) < 0) {
- return true;
- }
- return false;
+ return mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) < 0
+ && mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) < 0;
}
boolean hasChildSelector() {
- if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) < 0) {
- return false;
- }
- return true;
+ return mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) >= 0;
}
boolean hasPatternSelector() {
- if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PATTERN) < 0) {
- return false;
- }
- return true;
+ return mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PATTERN) >= 0;
}
boolean hasContainerSelector() {
- if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CONTAINER) < 0) {
- return false;
- }
- return true;
+ return mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CONTAINER) >= 0;
}
boolean hasParentSelector() {
- if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) < 0) {
- return false;
- }
- return true;
+ return mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) >= 0;
}
/**
@@ -1071,4 +1022,11 @@
builder.append("]");
return builder.toString();
}
+
+ private static <T> T checkNotNull(T value, @NonNull String message) {
+ if (value == null) {
+ throw new NullPointerException(message);
+ }
+ return value;
+ }
}
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/InternalPlatformTextApi.kt b/text/text/src/main/java/androidx/compose/ui/text/android/InternalPlatformTextApi.kt
index d87011e..5f975123 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/InternalPlatformTextApi.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/InternalPlatformTextApi.kt
@@ -25,4 +25,5 @@
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY
)
+@Retention(AnnotationRetention.BINARY)
annotation class InternalPlatformTextApi
\ No newline at end of file
diff --git a/tv/tv-foundation/api/public_plus_experimental_current.txt b/tv/tv-foundation/api/public_plus_experimental_current.txt
index ebce6ac..fe3724e 100644
--- a/tv/tv-foundation/api/public_plus_experimental_current.txt
+++ b/tv/tv-foundation/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.tv.foundation {
- @kotlin.RequiresOptIn(message="This tv-foundation API is experimental and likely to change or be removed in the future.") public @interface ExperimentalTvFoundationApi {
+ @kotlin.RequiresOptIn(message="This tv-foundation API is experimental and likely to change or be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTvFoundationApi {
}
public final class PivotOffsets {
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
index c0efb11..aed7e89 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
@@ -27,6 +27,7 @@
import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.requiredWidthIn
+import androidx.compose.foundation.lazy.grid.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -1142,6 +1143,39 @@
}
}
+ @Test
+ fun itemWithSpecsIsMovingOut() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3))
+ rule.setContent {
+ LazyGrid(1, maxSize = itemSizeDp * 2) {
+ items(list, key = { it }) {
+ Item(it, animSpec = if (it == 1) AnimSpec else null)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(0, 2, 3, 1)
+ }
+
+ onAnimationFrame { fraction ->
+ val listSize = itemSize * 2
+ val item1Offset = itemSize + (itemSize * 2f * fraction).roundToInt()
+ val expected = mutableListOf<Pair<Any, IntOffset>>().apply {
+ add(0 to AxisIntOffset(0, 0))
+ if (item1Offset < listSize) {
+ add(1 to AxisIntOffset(0, item1Offset))
+ } else {
+ rule.onNodeWithTag("1").assertIsNotDisplayed()
+ }
+ }
+ assertPositions(
+ expected = expected.toTypedArray(),
+ fraction = fraction
+ )
+ }
+ }
+
private fun AxisIntOffset(crossAxis: Int, mainAxis: Int) =
if (isVertical) IntOffset(crossAxis, mainAxis) else IntOffset(mainAxis, crossAxis)
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyColumnTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyColumnTest.kt
index 45825f43..d9f8e64 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyColumnTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyColumnTest.kt
@@ -56,6 +56,7 @@
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.tv.foundation.PivotOffsets
@@ -349,6 +350,7 @@
.assertPositionInRootIsEqualTo(30.dp, 50.dp)
}
+ @FlakyTest(bugId = 259297305)
@Test
fun removalWithMutableStateListOf() {
val items = mutableStateListOf("1", "2", "3")
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
index 07b5201..116fbea 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
@@ -111,7 +111,7 @@
rule.setContent {
LazyList {
items(list, key = { it }) {
- item(it)
+ Item(it)
}
}
}
@@ -137,7 +137,7 @@
rule.setContent {
LazyList {
items(list, key = { it }) {
- item(it)
+ Item(it)
}
}
}
@@ -160,7 +160,7 @@
rule.setContent {
LazyList {
items(list, key = { it }) {
- item(it)
+ Item(it)
}
}
}
@@ -195,7 +195,7 @@
rule.setContent {
LazyList {
items(list, key = { it }) {
- item(it)
+ Item(it)
}
}
}
@@ -233,7 +233,7 @@
maxSize = itemSizeDp * 5
) {
items(listOf(0, 1, 2, 3), key = { it }) {
- item(it, size = if (it == 1) size else itemSizeDp)
+ Item(it, size = if (it == 1) size else itemSizeDp)
}
}
}
@@ -275,7 +275,7 @@
rule.setContent {
LazyList {
items(list, key = { it }) {
- item(it, animSpec = if (it == 1 || it == 3) AnimSpec else null)
+ Item(it, animSpec = if (it == 1 || it == 3) AnimSpec else null)
}
}
}
@@ -303,7 +303,7 @@
LazyList {
items(list, key = { it }) {
val duration = if (it == 1 || it == 3) Duration * 2 else Duration
- item(it, animSpec = tween(duration.toInt(), easing = LinearEasing))
+ Item(it, animSpec = tween(duration.toInt(), easing = LinearEasing))
}
}
}
@@ -331,8 +331,8 @@
rule.setContent {
LazyList {
items(list, key = { it }) {
- item(it)
- item(it + 1)
+ Item(it)
+ Item(it + 1)
}
}
}
@@ -365,8 +365,8 @@
rule.setContent {
LazyList {
items(list, key = { it }) {
- item(it)
- item(it + 1, animSpec = null)
+ Item(it)
+ Item(it + 1, animSpec = null)
}
}
}
@@ -396,7 +396,7 @@
maxSize = itemSizeDp * 5
) {
items(listOf(1, 2, 3), key = { it }) {
- item(it)
+ Item(it)
}
}
}
@@ -428,7 +428,7 @@
rule.setContent {
LazyList(maxSize = itemSizeDp * 3) {
items(list, key = { it }) {
- item(it)
+ Item(it)
}
}
}
@@ -473,7 +473,7 @@
rule.setContent {
LazyList(maxSize = itemSizeDp * 3f, startIndex = 3) {
items(list, key = { it }) {
- item(it)
+ Item(it)
}
}
}
@@ -518,7 +518,7 @@
rule.setContent {
LazyList(arrangement = Arrangement.spacedBy(spacingDp)) {
items(list, key = { it }) {
- item(it)
+ Item(it)
}
}
}
@@ -547,7 +547,7 @@
arrangement = Arrangement.spacedBy(spacingDp)
) {
items(list, key = { it }) {
- item(it)
+ Item(it)
}
}
}
@@ -599,7 +599,7 @@
arrangement = Arrangement.spacedBy(spacingDp)
) {
items(list, key = { it }) {
- item(it)
+ Item(it)
}
}
}
@@ -648,7 +648,7 @@
items(list, key = { it }) {
val size =
if (it == 3) itemSize2Dp else if (it == 1) itemSize3Dp else itemSizeDp
- item(it, size = size)
+ Item(it, size = size)
}
}
}
@@ -708,7 +708,7 @@
items(list, key = { it }) {
val size =
if (it == 0) itemSize2Dp else if (it == 4) itemSize3Dp else itemSizeDp
- item(it, size = size)
+ Item(it, size = size)
}
}
}
@@ -769,7 +769,7 @@
items(listOf(1, 2, 3), key = { it }) {
val crossAxisSize =
if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
- item(it, crossAxisSize = crossAxisSize)
+ Item(it, crossAxisSize = crossAxisSize)
}
}
}
@@ -821,7 +821,7 @@
listOf(1, 2, 3).forEach {
val crossAxisSize =
if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
- item(it, crossAxisSize = crossAxisSize)
+ Item(it, crossAxisSize = crossAxisSize)
}
}
}
@@ -863,7 +863,7 @@
items(listOf(1, 2, 3), key = { it }) {
val crossAxisSize =
if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
- item(it, crossAxisSize = crossAxisSize)
+ Item(it, crossAxisSize = crossAxisSize)
}
}
}
@@ -911,7 +911,7 @@
rule.setContent {
LazyList(startPadding = startPaddingDp, endPadding = endPaddingDp) {
items(list, key = { it }) {
- item(it)
+ Item(it)
}
}
}
@@ -949,7 +949,7 @@
rule.setContent {
LazyList {
items(list, key = { it }) {
- item(it)
+ Item(it)
}
}
LaunchedEffect(Unit) {
@@ -982,7 +982,7 @@
rule.setContent {
LazyList(maxSize = itemSizeDp * 3) {
items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) {
- item(it)
+ Item(it)
}
}
}
@@ -1004,6 +1004,39 @@
}
}
+ @Test
+ fun itemWithSpecsIsMovingOut() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3))
+ rule.setContent {
+ LazyList(maxSize = itemSizeDp * 2) {
+ items(list, key = { it }) {
+ Item(it, animSpec = if (it == 1) AnimSpec else null)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(0, 2, 3, 1)
+ }
+
+ onAnimationFrame { fraction ->
+ val listSize = itemSize * 2
+ val item1Offset = itemSize + (itemSize * 2f * fraction).roundToInt()
+ val expected = mutableListOf<Pair<Any, Int>>().apply {
+ add(0 to 0)
+ if (item1Offset < listSize) {
+ add(1 to item1Offset)
+ } else {
+ rule.onNodeWithTag("1").assertIsNotDisplayed()
+ }
+ }
+ assertPositions(
+ expected = expected.toTypedArray(),
+ fraction = fraction
+ )
+ }
+ }
+
private fun assertPositions(
vararg expected: Pair<Any, Int>,
crossAxis: List<Pair<Any, Int>>? = null,
@@ -1161,7 +1194,7 @@
}
@Composable
- private fun TvLazyListItemScope.item(
+ private fun TvLazyListItemScope.Item(
tag: Int,
size: Dp = itemSizeDp,
crossAxisSize: Dp = size,
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/ExperimentalTvFoundationApi.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/ExperimentalTvFoundationApi.kt
index f33925a..b1402f3 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/ExperimentalTvFoundationApi.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/ExperimentalTvFoundationApi.kt
@@ -19,4 +19,5 @@
@RequiresOptIn(
"This tv-foundation API is experimental and likely to change or be removed in the future."
)
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalTvFoundationApi
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
index 223640a..6db01c4 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
@@ -81,7 +81,7 @@
positionedItems: MutableList<TvLazyGridPositionedItem>,
measuredItemProvider: LazyMeasuredItemProvider,
) {
- if (!positionedItems.fastAny { it.hasAnimations }) {
+ if (!positionedItems.fastAny { it.hasAnimations } && keyToItemInfoMap.isEmpty()) {
// no animations specified - no work needed
reset()
return
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt
index 317b454..a3ad3d9 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt
@@ -71,7 +71,7 @@
positionedItems: MutableList<LazyListPositionedItem>,
itemProvider: LazyMeasuredItemProvider,
) {
- if (!positionedItems.fastAny { it.hasAnimations }) {
+ if (!positionedItems.fastAny { it.hasAnimations } && keyToItemInfoMap.isEmpty()) {
// no animations specified - no work needed
reset()
return
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index 1180309..4970d54 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -189,7 +189,7 @@
public final class DefaultTimeSourceKt {
}
- @kotlin.RequiresOptIn(message="This Wear Material API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalWearMaterialApi {
+ @kotlin.RequiresOptIn(message="This Wear Material API is experimental and is likely to change or to be removed in" + " the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalWearMaterialApi {
}
@androidx.compose.runtime.Immutable @androidx.wear.compose.material.ExperimentalWearMaterialApi public final class FixedThreshold implements androidx.wear.compose.material.ThresholdConfig {
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScrollAwayTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScrollAwayTest.kt
index b43f0d2..c81b71a 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScrollAwayTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScrollAwayTest.kt
@@ -35,6 +35,7 @@
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import org.junit.Rule
import org.junit.Test
@@ -46,6 +47,7 @@
@get:Rule
val rule = createComposeRule()
+ @FlakyTest(bugId = 259134313)
@Test
fun hidesTimeTextWithScalingLazyColumn() {
lateinit var scrollState: ScalingLazyListState
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ExperimentalWearMaterialApi.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ExperimentalWearMaterialApi.kt
index 730437c..028dd52 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ExperimentalWearMaterialApi.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ExperimentalWearMaterialApi.kt
@@ -20,4 +20,5 @@
"This Wear Material API is experimental and is likely to change or to be removed in" +
" the future."
)
+@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalWearMaterialApi
\ No newline at end of file
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
index 7b38d00..84af177e 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
@@ -17,7 +17,6 @@
package androidx.wear.compose.material
import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.TweenSpec
import androidx.compose.foundation.background
@@ -154,12 +153,7 @@
val translationX = if (squeezeMode) squeezeOffset else slideOffset
- val backgroundAlpha =
- lerp(
- MAX_BACKGROUND_SCRIM_ALPHA,
- MIN_BACKGROUND_SCRIM_ALPHA,
- BACKGROUND_SCRIM_EASING.transform(progress)
- )
+ val backgroundAlpha = MAX_BACKGROUND_SCRIM_ALPHA * (1 - progress)
val contentScrimAlpha = min(MAX_CONTENT_SCRIM_ALPHA, progress / 2f)
Modifiers(
@@ -590,8 +584,6 @@
private const val SCALE_MAX = 1f
private const val SCALE_MIN = 0.7f
private const val MAX_CONTENT_SCRIM_ALPHA = 0.3f
-private const val MAX_BACKGROUND_SCRIM_ALPHA = 0.75f
-private const val MIN_BACKGROUND_SCRIM_ALPHA = 0f
-private val BACKGROUND_SCRIM_EASING = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)
+private const val MAX_BACKGROUND_SCRIM_ALPHA = 0.5f
private val SWIPE_TO_DISMISS_BOX_ANIMATION_SPEC =
TweenSpec<Float>(200, 0, LinearOutSlowInEasing)
diff --git a/wear/watchface/watchface-complications-data/api/current.txt b/wear/watchface/watchface-complications-data/api/current.txt
index 2f7342b..ff7ac43 100644
--- a/wear/watchface/watchface-complications-data/api/current.txt
+++ b/wear/watchface/watchface-complications-data/api/current.txt
@@ -86,6 +86,9 @@
public final class DataKt {
}
+ public final class DynamicFloatKt {
+ }
+
public final class EmptyComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
ctor public EmptyComplicationData();
field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
diff --git a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
index 565b9cd..c92a5a5 100644
--- a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
@@ -89,6 +89,14 @@
public final class DataKt {
}
+ @androidx.wear.watchface.complications.data.ComplicationExperimental public abstract class DynamicFloat {
+ ctor public DynamicFloat();
+ method public abstract byte[] asByteArray();
+ }
+
+ public final class DynamicFloatKt {
+ }
+
public final class EmptyComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
ctor public EmptyComplicationData();
field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
@@ -97,6 +105,7 @@
@RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class GoalProgressComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
+ method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.DynamicFloat? getDynamicValue();
method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
method public androidx.wear.watchface.complications.data.SmallImage? getSmallImage();
method public float getTargetValue();
@@ -105,6 +114,7 @@
method public float getValue();
property public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
+ property @androidx.wear.watchface.complications.data.ComplicationExperimental public final androidx.wear.watchface.complications.data.DynamicFloat? dynamicValue;
property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
property public final androidx.wear.watchface.complications.data.SmallImage? smallImage;
property public final float targetValue;
@@ -117,6 +127,7 @@
@RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final class GoalProgressComplicationData.Builder {
ctor public GoalProgressComplicationData.Builder(float value, float targetValue, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
+ ctor @androidx.wear.watchface.complications.data.ComplicationExperimental public GoalProgressComplicationData.Builder(androidx.wear.watchface.complications.data.DynamicFloat dynamicValue, float targetValue, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.GoalProgressComplicationData build();
method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
method public final T setDataSource(android.content.ComponentName? dataSource);
@@ -265,6 +276,7 @@
public final class RangedValueComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
+ method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.DynamicFloat? getDynamicValue();
method public float getMax();
method public float getMin();
method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
@@ -275,6 +287,7 @@
method public int getValueType();
property public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
+ property @androidx.wear.watchface.complications.data.ComplicationExperimental public final androidx.wear.watchface.complications.data.DynamicFloat? dynamicValue;
property public final float max;
property public final float min;
property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
@@ -292,6 +305,7 @@
public static final class RangedValueComplicationData.Builder {
ctor public RangedValueComplicationData.Builder(float value, float min, float max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
+ ctor @androidx.wear.watchface.complications.data.ComplicationExperimental public RangedValueComplicationData.Builder(androidx.wear.watchface.complications.data.DynamicFloat dynamicValue, float min, float max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData build();
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
method public final T setDataSource(android.content.ComponentName? dataSource);
diff --git a/wear/watchface/watchface-complications-data/api/restricted_current.txt b/wear/watchface/watchface-complications-data/api/restricted_current.txt
index 95ce87f..db4540b 100644
--- a/wear/watchface/watchface-complications-data/api/restricted_current.txt
+++ b/wear/watchface/watchface-complications-data/api/restricted_current.txt
@@ -86,6 +86,9 @@
public final class DataKt {
}
+ public final class DynamicFloatKt {
+ }
+
public final class EmptyComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
ctor public EmptyComplicationData();
field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
index 34de840..19f21c4 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
@@ -13,6 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+@file:OptIn(ComplicationExperimental::class)
+
package android.support.wearable.complications
import android.annotation.SuppressLint
@@ -31,8 +34,11 @@
import androidx.annotation.RestrictTo
import androidx.wear.watchface.complications.data.ComplicationDisplayPolicies
import androidx.wear.watchface.complications.data.ComplicationDisplayPolicy
+import androidx.wear.watchface.complications.data.ComplicationExperimental
import androidx.wear.watchface.complications.data.ComplicationPersistencePolicies
import androidx.wear.watchface.complications.data.ComplicationPersistencePolicy
+import androidx.wear.watchface.complications.data.DynamicFloat
+import androidx.wear.watchface.complications.data.toDynamicFloat
import java.io.IOException
import java.io.InvalidObjectException
import java.io.ObjectInputStream
@@ -171,6 +177,11 @@
if (isFieldValidForType(FIELD_VALUE, type)) {
oos.writeFloat(complicationData.rangedValue)
}
+ if (isFieldValidForType(FIELD_DYNAMIC_VALUE, type)) {
+ oos.writeNullable(complicationData.rangedDynamicValue) {
+ oos.writeByteArray(it.asByteArray())
+ }
+ }
if (isFieldValidForType(FIELD_VALUE_TYPE, type)) {
oos.writeInt(complicationData.rangedValueType)
}
@@ -184,49 +195,16 @@
oos.writeFloat(complicationData.targetValue)
}
if (isFieldValidForType(FIELD_COLOR_RAMP, type)) {
- val colors = complicationData.colorRamp
- if (colors != null) {
- oos.writeBoolean(true)
- oos.writeInt(colors.size)
- for (color in colors) {
- oos.writeInt(color)
- }
- } else {
- oos.writeBoolean(false)
- }
+ oos.writeNullable(complicationData.colorRamp, oos::writeIntArray)
}
if (isFieldValidForType(FIELD_COLOR_RAMP_INTERPOLATED, type)) {
- val isColorRampSmoothShaded = complicationData.isColorRampInterpolated
- if (isColorRampSmoothShaded != null) {
- oos.writeBoolean(true)
- oos.writeBoolean(isColorRampSmoothShaded)
- } else {
- oos.writeBoolean(false)
- }
+ oos.writeNullable(complicationData.isColorRampInterpolated, oos::writeBoolean)
}
if (isFieldValidForType(FIELD_ELEMENT_WEIGHTS, type)) {
- val weights = complicationData.elementWeights
- if (weights != null) {
- oos.writeBoolean(true)
- oos.writeInt(weights.size)
- for (weight in weights) {
- oos.writeFloat(weight)
- }
- } else {
- oos.writeBoolean(false)
- }
+ oos.writeNullable(complicationData.elementWeights, oos::writeFloatArray)
}
if (isFieldValidForType(FIELD_ELEMENT_COLORS, type)) {
- val colors = complicationData.elementColors
- if (colors != null) {
- oos.writeBoolean(true)
- oos.writeInt(colors.size)
- for (color in colors) {
- oos.writeInt(color)
- }
- } else {
- oos.writeBoolean(false)
- }
+ oos.writeNullable(complicationData.elementColors, oos::writeIntArray)
}
if (isFieldValidForType(FIELD_ELEMENT_BACKGROUND_COLOR, type)) {
oos.writeInt(complicationData.elementBackgroundColor)
@@ -242,31 +220,13 @@
oos.writeInt(complicationData.listStyleHint)
}
if (isFieldValidForType(EXP_FIELD_PROTO_LAYOUT_INTERACTIVE, type)) {
- val bytes = complicationData.interactiveLayout
- if (bytes == null) {
- oos.writeInt(0)
- } else {
- oos.writeInt(bytes.size)
- oos.write(bytes)
- }
+ oos.writeByteArray(complicationData.interactiveLayout ?: byteArrayOf())
}
if (isFieldValidForType(EXP_FIELD_PROTO_LAYOUT_AMBIENT, type)) {
- val bytes = complicationData.ambientLayout
- if (bytes == null) {
- oos.writeInt(0)
- } else {
- oos.writeInt(bytes.size)
- oos.write(bytes)
- }
+ oos.writeByteArray(complicationData.ambientLayout ?: byteArrayOf())
}
if (isFieldValidForType(EXP_FIELD_PROTO_LAYOUT_RESOURCES, type)) {
- val bytes = complicationData.layoutResources
- if (bytes == null) {
- oos.writeInt(0)
- } else {
- oos.writeInt(bytes.size)
- oos.write(bytes)
- }
+ oos.writeByteArray(complicationData.layoutResources ?: byteArrayOf())
}
if (isFieldValidForType(FIELD_DATA_SOURCE, type)) {
val componentName = complicationData.dataSource
@@ -286,32 +246,18 @@
val end = complicationData.fields.getLong(FIELD_TIMELINE_END_TIME, -1)
oos.writeLong(end)
oos.writeInt(complicationData.fields.getInt(FIELD_TIMELINE_ENTRY_TYPE))
- val listEntries = complicationData.listEntries
- val listEntriesLength = listEntries?.size ?: 0
- oos.writeInt(listEntriesLength)
- if (listEntries != null) {
- for (data in listEntries) {
- SerializedForm(data).writeObject(oos)
- }
+ oos.writeList(complicationData.listEntries ?: listOf()) {
+ SerializedForm(it).writeObject(oos)
}
if (isFieldValidForType(FIELD_PLACEHOLDER_FIELDS, type)) {
- val placeholder = complicationData.placeholder
- if (placeholder == null) {
- oos.writeBoolean(false)
- } else {
- oos.writeBoolean(true)
- SerializedForm(placeholder).writeObject(oos)
+ oos.writeNullable(complicationData.placeholder) {
+ SerializedForm(it).writeObject(oos)
}
}
// This has to be last, since it's recursive.
- val timeline = complicationData.timelineEntries
- val timelineLength = timeline?.size ?: 0
- oos.writeInt(timelineLength)
- if (timeline != null) {
- for (data in timeline) {
- SerializedForm(data).writeObject(oos)
- }
+ oos.writeList(complicationData.timelineEntries ?: listOf()) {
+ SerializedForm(it).writeObject(oos)
}
}
@@ -371,6 +317,11 @@
if (isFieldValidForType(FIELD_VALUE, type)) {
fields.putFloat(FIELD_VALUE, ois.readFloat())
}
+ if (isFieldValidForType(FIELD_DYNAMIC_VALUE, type)) {
+ ois.readNullable { ois.readByteArray() }?.let {
+ fields.putByteArray(FIELD_DYNAMIC_VALUE, it)
+ }
+ }
if (isFieldValidForType(FIELD_VALUE_TYPE, type)) {
fields.putInt(FIELD_VALUE_TYPE, ois.readInt())
}
@@ -383,32 +334,25 @@
if (isFieldValidForType(FIELD_TARGET_VALUE, type)) {
fields.putFloat(FIELD_TARGET_VALUE, ois.readFloat())
}
- if (isFieldValidForType(FIELD_COLOR_RAMP, type) && ois.readBoolean()) {
- val numColors = ois.readInt()
- val colors = IntArray(numColors)
- for (i in 0 until numColors) {
- colors[i] = ois.readInt()
+ if (isFieldValidForType(FIELD_COLOR_RAMP, type)) {
+ ois.readNullable { ois.readIntArray() }?.let {
+ fields.putIntArray(FIELD_COLOR_RAMP, it)
}
- fields.putIntArray(FIELD_COLOR_RAMP, colors)
}
- if (isFieldValidForType(FIELD_COLOR_RAMP_INTERPOLATED, type) && ois.readBoolean()) {
- fields.putBoolean(FIELD_COLOR_RAMP_INTERPOLATED, ois.readBoolean())
- }
- if (isFieldValidForType(FIELD_ELEMENT_WEIGHTS, type) && ois.readBoolean()) {
- val numWeights = ois.readInt()
- val weights = FloatArray(numWeights)
- for (i in 0 until numWeights) {
- weights[i] = ois.readFloat()
+ if (isFieldValidForType(FIELD_COLOR_RAMP_INTERPOLATED, type)) {
+ ois.readNullable { ois.readBoolean() }?.let {
+ fields.putBoolean(FIELD_COLOR_RAMP_INTERPOLATED, it)
}
- fields.putFloatArray(FIELD_ELEMENT_WEIGHTS, weights)
}
- if (isFieldValidForType(FIELD_ELEMENT_COLORS, type) && ois.readBoolean()) {
- val numColors = ois.readInt()
- val colors = IntArray(numColors)
- for (i in 0 until numColors) {
- colors[i] = ois.readInt()
+ if (isFieldValidForType(FIELD_ELEMENT_WEIGHTS, type)) {
+ ois.readNullable { ois.readFloatArray() }?.let {
+ fields.putFloatArray(FIELD_ELEMENT_WEIGHTS, it)
}
- fields.putIntArray(FIELD_ELEMENT_COLORS, colors)
+ }
+ if (isFieldValidForType(FIELD_ELEMENT_COLORS, type)) {
+ ois.readNullable { ois.readIntArray() }?.let {
+ fields.putIntArray(FIELD_ELEMENT_COLORS, it)
+ }
}
if (isFieldValidForType(FIELD_ELEMENT_BACKGROUND_COLOR, type)) {
fields.putInt(FIELD_ELEMENT_BACKGROUND_COLOR, ois.readInt())
@@ -475,47 +419,29 @@
if (timelineEntryType != 0) {
fields.putInt(FIELD_TIMELINE_ENTRY_TYPE, timelineEntryType)
}
- val listEntriesLength = ois.readInt()
- if (listEntriesLength != 0) {
- val parcels = arrayOfNulls<Parcelable>(listEntriesLength)
- for (i in 0 until listEntriesLength) {
- val entry = SerializedForm()
- entry.readObject(ois)
- parcels[i] = entry.complicationData!!.fields
- }
- fields.putParcelableArray(EXP_FIELD_LIST_ENTRIES, parcels)
- }
+ ois.readList { SerializedForm().apply { readObject(ois) } }
+ .map { it.complicationData!!.fields }
+ .takeIf { it.isNotEmpty() }
+ ?.let { fields.putParcelableArray(EXP_FIELD_LIST_ENTRIES, it.toTypedArray()) }
if (isFieldValidForType(FIELD_PLACEHOLDER_FIELDS, type)) {
- if (ois.readBoolean()) {
- val serializedPlaceholder = SerializedForm()
- serializedPlaceholder.readObject(ois)
- fields.putInt(
- FIELD_PLACEHOLDER_TYPE,
- serializedPlaceholder.complicationData!!.type
- )
- fields.putBundle(
- FIELD_PLACEHOLDER_FIELDS,
- serializedPlaceholder.complicationData!!.fields
- )
+ ois.readNullable { SerializedForm().apply { readObject(ois) } }?.let {
+ fields.putInt(FIELD_PLACEHOLDER_TYPE, it.complicationData!!.type)
+ fields.putBundle(FIELD_PLACEHOLDER_FIELDS, it.complicationData!!.fields)
}
}
- val timelineLength = ois.readInt()
- if (timelineLength != 0) {
- val parcels = arrayOfNulls<Parcelable>(timelineLength)
- for (i in 0 until timelineLength) {
- val entry = SerializedForm()
- entry.readObject(ois)
- parcels[i] = entry.complicationData!!.fields
+ ois.readList { SerializedForm().apply { readObject(ois) } }
+ .map { it.complicationData!!.fields }
+ .takeIf { it.isNotEmpty() }
+ ?.let {
+ fields.putParcelableArray(FIELD_TIMELINE_ENTRIES, it.toTypedArray())
}
- fields.putParcelableArray(FIELD_TIMELINE_ENTRIES, parcels)
- }
complicationData = ComplicationData(type, fields)
}
fun readResolve(): Any = complicationData!!
companion object {
- private const val VERSION_NUMBER = 19
+ private const val VERSION_NUMBER = 20
internal fun putIfNotNull(fields: Bundle, field: String, value: Parcelable?) {
if (value != null) {
fields.putParcelable(field, value)
@@ -650,7 +576,7 @@
}
/**
- * Returns true if the ComplicationData contains a ranged max value. I.e. if [rangedValue] can
+ * Returns true if the ComplicationData contains a ranged value. I.e. if [rangedValue] can
* succeed.
*/
fun hasRangedValue(): Boolean = isFieldValidForType(FIELD_VALUE, type)
@@ -658,8 +584,8 @@
/**
* Returns the *value* field for this complication.
*
- * Valid only if the type of this complication data is [TYPE_RANGED_VALUE], otherwise returns
- * zero.
+ * Valid only if the type of this complication data is [TYPE_RANGED_VALUE] and
+ * [TYPE_GOAL_PROGRESS], otherwise returns zero.
*/
val rangedValue: Float
get() {
@@ -668,6 +594,24 @@
}
/**
+ * Returns true if the ComplicationData contains a ranged dynamic value. I.e. if
+ * [rangedDynamicValue] can succeed.
+ */
+ fun hasRangedDynamicValue(): Boolean = isFieldValidForType(FIELD_DYNAMIC_VALUE, type)
+
+ /**
+ * Returns the *dynamicValue* field for this complication.
+ *
+ * Valid only if the type of this complication data is [TYPE_RANGED_VALUE] and
+ * [TYPE_GOAL_PROGRESS].
+ */
+ val rangedDynamicValue: DynamicFloat?
+ get() {
+ checkFieldValidForTypeWithoutThrowingException(FIELD_DYNAMIC_VALUE, type)
+ return fields.getByteArray(FIELD_DYNAMIC_VALUE)?.toDynamicFloat()
+ }
+
+ /**
* Returns true if the ComplicationData contains a ranged max type. I.e. if [rangedValueType]
* can succeed.
*/
@@ -1317,7 +1261,18 @@
*
* @throws IllegalStateException if this field is not valid for the complication type
*/
- fun setRangedValue(value: Float) = apply { putFloatField(FIELD_VALUE, value) }
+ fun setRangedValue(value: Float?) = apply { putOrRemoveField(FIELD_VALUE, value) }
+
+ /**
+ * Sets the *dynamicValue* field. It is evaluated to a value with the same limitations as
+ * [setRangedValue].
+ *
+ * Returns this Builder to allow chaining.
+ *
+ * @throws IllegalStateException if this field is not valid for the complication type
+ */
+ fun setRangedDynamicValue(value: DynamicFloat?) =
+ apply { putOrRemoveField(FIELD_DYNAMIC_VALUE, value?.asByteArray()) }
/**
* Sets the *value type* field which provides meta data about the value. This is
@@ -1709,6 +1664,11 @@
" is provided."
}
}
+ for (requiredOneOfFieldGroup in REQUIRED_ONE_OF_FIELDS[type]!!) {
+ check(requiredOneOfFieldGroup.count { fields.containsKey(it) } >= 1) {
+ "One of $requiredOneOfFieldGroup must be provided."
+ }
+ }
return ComplicationData(this)
}
@@ -1737,8 +1697,10 @@
when (obj) {
is Boolean -> fields.putBoolean(field, obj)
is Int -> fields.putInt(field, obj)
+ is Float -> fields.putFloat(field, obj)
is String -> fields.putString(field, obj)
is Parcelable -> fields.putParcelable(field, obj)
+ is ByteArray -> fields.putByteArray(field, obj)
is IntArray -> fields.putIntArray(field, obj)
is FloatArray -> fields.putFloatArray(field, obj)
else -> throw IllegalArgumentException("Unexpected object type: " + obj.javaClass)
@@ -1961,6 +1923,7 @@
private const val FIELD_TIMELINE_ENTRIES = "TIMELINE"
private const val FIELD_TIMELINE_ENTRY_TYPE = "TIMELINE_ENTRY_TYPE"
private const val FIELD_VALUE = "VALUE"
+ private const val FIELD_DYNAMIC_VALUE = "DYNAMIC_VALUE"
private const val FIELD_VALUE_TYPE = "VALUE_TYPE"
// Experimental fields, these are subject to change without notice.
@@ -1999,7 +1962,7 @@
TYPE_EMPTY to setOf(),
TYPE_SHORT_TEXT to setOf(FIELD_SHORT_TEXT),
TYPE_LONG_TEXT to setOf(FIELD_LONG_TEXT),
- TYPE_RANGED_VALUE to setOf(FIELD_VALUE, FIELD_MIN_VALUE, FIELD_MAX_VALUE),
+ TYPE_RANGED_VALUE to setOf(FIELD_MIN_VALUE, FIELD_MAX_VALUE),
TYPE_ICON to setOf(FIELD_ICON),
TYPE_SMALL_IMAGE to setOf(FIELD_SMALL_IMAGE, FIELD_IMAGE_STYLE),
TYPE_LARGE_IMAGE to setOf(FIELD_LARGE_IMAGE),
@@ -2008,18 +1971,39 @@
EXP_TYPE_PROTO_LAYOUT to setOf(
EXP_FIELD_PROTO_LAYOUT_AMBIENT,
EXP_FIELD_PROTO_LAYOUT_INTERACTIVE,
- EXP_FIELD_PROTO_LAYOUT_RESOURCES
+ EXP_FIELD_PROTO_LAYOUT_RESOURCES,
),
EXP_TYPE_LIST to setOf(EXP_FIELD_LIST_ENTRIES),
- TYPE_GOAL_PROGRESS to setOf(FIELD_VALUE, FIELD_TARGET_VALUE),
+ TYPE_GOAL_PROGRESS to setOf(FIELD_TARGET_VALUE),
TYPE_WEIGHTED_ELEMENTS to setOf(
FIELD_ELEMENT_WEIGHTS,
FIELD_ELEMENT_COLORS,
- FIELD_ELEMENT_BACKGROUND_COLOR
+ FIELD_ELEMENT_BACKGROUND_COLOR,
),
)
- // Used for validation. OPTIONAL_FIELDS[i] is an array containing all the fields which are
+ // Used for validation. REQUIRED_ONE_OF_FIELDS[i] is a list of field groups of which at
+ // least one field must be populated for @ComplicationType i.
+ // If a field is also in REQUIRED_FIELDS[i], it is not required if another field in the one
+ // of group is populated.
+ private val REQUIRED_ONE_OF_FIELDS: Map<Int, Set<Set<String>>> = mapOf(
+ TYPE_NOT_CONFIGURED to setOf(),
+ TYPE_EMPTY to setOf(),
+ TYPE_SHORT_TEXT to setOf(),
+ TYPE_LONG_TEXT to setOf(),
+ TYPE_RANGED_VALUE to setOf(setOf(FIELD_VALUE, FIELD_DYNAMIC_VALUE)),
+ TYPE_ICON to setOf(),
+ TYPE_SMALL_IMAGE to setOf(),
+ TYPE_LARGE_IMAGE to setOf(),
+ TYPE_NO_PERMISSION to setOf(),
+ TYPE_NO_DATA to setOf(),
+ EXP_TYPE_PROTO_LAYOUT to setOf(),
+ EXP_TYPE_LIST to setOf(),
+ TYPE_GOAL_PROGRESS to setOf(setOf(FIELD_VALUE, FIELD_DYNAMIC_VALUE)),
+ TYPE_WEIGHTED_ELEMENTS to setOf(),
+ )
+
+ // Used for validation. OPTIONAL_FIELDS[i] is a list containing all the fields which are
// valid but not required for type i.
private val OPTIONAL_FIELDS: Map<Int, Set<String>> = mapOf(
TYPE_NOT_CONFIGURED to setOf(),
@@ -2035,7 +2019,7 @@
FIELD_CONTENT_DESCRIPTION,
FIELD_DATA_SOURCE,
FIELD_PERSISTENCE_POLICY,
- FIELD_DISPLAY_POLICY
+ FIELD_DISPLAY_POLICY,
),
TYPE_LONG_TEXT to setOf(
FIELD_LONG_TITLE,
@@ -2048,7 +2032,7 @@
FIELD_CONTENT_DESCRIPTION,
FIELD_DATA_SOURCE,
FIELD_PERSISTENCE_POLICY,
- FIELD_DISPLAY_POLICY
+ FIELD_DISPLAY_POLICY,
),
TYPE_RANGED_VALUE to setOf(
FIELD_SHORT_TEXT,
@@ -2065,7 +2049,7 @@
FIELD_COLOR_RAMP_INTERPOLATED,
FIELD_PERSISTENCE_POLICY,
FIELD_DISPLAY_POLICY,
- FIELD_VALUE_TYPE
+ FIELD_VALUE_TYPE,
),
TYPE_ICON to setOf(
FIELD_TAP_ACTION,
@@ -2073,7 +2057,7 @@
FIELD_CONTENT_DESCRIPTION,
FIELD_DATA_SOURCE,
FIELD_PERSISTENCE_POLICY,
- FIELD_DISPLAY_POLICY
+ FIELD_DISPLAY_POLICY,
),
TYPE_SMALL_IMAGE to setOf(
FIELD_TAP_ACTION,
@@ -2081,14 +2065,14 @@
FIELD_CONTENT_DESCRIPTION,
FIELD_DATA_SOURCE,
FIELD_PERSISTENCE_POLICY,
- FIELD_DISPLAY_POLICY
+ FIELD_DISPLAY_POLICY,
),
TYPE_LARGE_IMAGE to setOf(
FIELD_TAP_ACTION,
FIELD_CONTENT_DESCRIPTION,
FIELD_DATA_SOURCE,
FIELD_PERSISTENCE_POLICY,
- FIELD_DISPLAY_POLICY
+ FIELD_DISPLAY_POLICY,
),
TYPE_NO_PERMISSION to setOf(
FIELD_SHORT_TEXT,
@@ -2101,7 +2085,7 @@
FIELD_CONTENT_DESCRIPTION,
FIELD_DATA_SOURCE,
FIELD_PERSISTENCE_POLICY,
- FIELD_DISPLAY_POLICY
+ FIELD_DISPLAY_POLICY,
),
TYPE_NO_DATA to setOf(
FIELD_CONTENT_DESCRIPTION,
@@ -2121,17 +2105,18 @@
FIELD_SMALL_IMAGE_BURN_IN_PROTECTION,
FIELD_TAP_ACTION,
FIELD_VALUE,
+ FIELD_DYNAMIC_VALUE,
FIELD_VALUE_TYPE,
FIELD_DATA_SOURCE,
FIELD_PERSISTENCE_POLICY,
- FIELD_DISPLAY_POLICY
+ FIELD_DISPLAY_POLICY,
),
EXP_TYPE_PROTO_LAYOUT to setOf(
FIELD_TAP_ACTION,
FIELD_CONTENT_DESCRIPTION,
FIELD_DATA_SOURCE,
FIELD_PERSISTENCE_POLICY,
- FIELD_DISPLAY_POLICY
+ FIELD_DISPLAY_POLICY,
),
EXP_TYPE_LIST to setOf(
FIELD_TAP_ACTION,
@@ -2139,7 +2124,7 @@
FIELD_CONTENT_DESCRIPTION,
FIELD_DATA_SOURCE,
FIELD_PERSISTENCE_POLICY,
- FIELD_DISPLAY_POLICY
+ FIELD_DISPLAY_POLICY,
),
TYPE_GOAL_PROGRESS to setOf(
FIELD_SHORT_TEXT,
@@ -2155,7 +2140,7 @@
FIELD_COLOR_RAMP,
FIELD_COLOR_RAMP_INTERPOLATED,
FIELD_PERSISTENCE_POLICY,
- FIELD_DISPLAY_POLICY
+ FIELD_DISPLAY_POLICY,
),
TYPE_WEIGHTED_ELEMENTS to setOf(
FIELD_SHORT_TEXT,
@@ -2169,7 +2154,7 @@
FIELD_CONTENT_DESCRIPTION,
FIELD_DATA_SOURCE,
FIELD_PERSISTENCE_POLICY,
- FIELD_DISPLAY_POLICY
+ FIELD_DISPLAY_POLICY,
),
)
@@ -2181,35 +2166,28 @@
}
fun isFieldValidForType(field: String, @ComplicationType type: Int): Boolean {
- val requiredFields = REQUIRED_FIELDS[type] ?: return false
- for (requiredField in requiredFields) {
- if (requiredField == field) {
- return true
- }
- }
- for (optionalField in OPTIONAL_FIELDS[type]!!) {
- if (optionalField == field) {
- return true
- }
- }
- return false
+ return REQUIRED_FIELDS[type]!!.contains(field) ||
+ REQUIRED_ONE_OF_FIELDS[type]!!.any { it.contains(field) } ||
+ OPTIONAL_FIELDS[type]!!.contains(field)
}
private fun isTypeSupported(type: Int) = type in VALID_TYPES
- /** The unparceling logic needs to remain backward compatible. */
+ /**
+ * The unparceling logic needs to remain backward compatible.
+ * Validates that a value of the given field type can be assigned
+ * to the given complication type.
+ */
internal fun checkFieldValidForTypeWithoutThrowingException(
- field: String,
- @ComplicationType type: Int,
+ fieldType: String,
+ @ComplicationType complicationType: Int,
) {
- if (!isTypeSupported(type)) {
- Log.w(TAG, "Type $type can not be recognized")
+ if (!isTypeSupported(complicationType)) {
+ Log.w(TAG, "Type $complicationType can not be recognized")
return
}
- if (!isFieldValidForType(field, type)) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Field $field is not supported for type $type")
- }
+ if (!isFieldValidForType(fieldType, complicationType)) {
+ Log.d(TAG, "Field $fieldType is not supported for type $complicationType")
}
}
@@ -2234,4 +2212,60 @@
if (!shouldRedact() || unredacted == PLACEHOLDER_STRING) unredacted
else "REDACTED"
}
-}
\ No newline at end of file
+}
+
+/** Writes a [ByteArray] by writing the size, then the bytes. To be used with [readByteArray]. */
+internal fun ObjectOutputStream.writeByteArray(value: ByteArray) {
+ writeInt(value.size)
+ write(value)
+}
+
+/** Reads a [ByteArray] written with [writeByteArray]. */
+internal fun ObjectInputStream.readByteArray() = ByteArray(readInt()).also { readFully(it) }
+
+/** Writes an [IntArray] by writing the size, then the bytes. To be used with [readIntArray]. */
+internal fun ObjectOutputStream.writeIntArray(value: IntArray) {
+ writeInt(value.size)
+ value.forEach(this::writeInt)
+}
+
+/** Reads an [IntArray] written with [writeIntArray]. */
+internal fun ObjectInputStream.readIntArray() = IntArray(readInt()).also {
+ for (i in it.indices) it[i] = readInt()
+}
+
+/** Writes a [FloatArray] by writing the size, then the bytes. To be used with [readFloatArray]. */
+internal fun ObjectOutputStream.writeFloatArray(value: FloatArray) {
+ writeInt(value.size)
+ value.forEach(this::writeFloat)
+}
+
+/** Reads a [FloatArray] written with [writeFloatArray]. */
+internal fun ObjectInputStream.readFloatArray() = FloatArray(readInt()).also {
+ for (i in it.indices) it[i] = readFloat()
+}
+
+/** Writes a generic [List] by writing the size, then the objects. To be used with [readList]. */
+internal fun <T> ObjectOutputStream.writeList(value: List<T>, writer: (T) -> Unit) {
+ writeInt(value.size)
+ value.forEach(writer)
+}
+
+/** Reads a list written with [readList]. */
+internal fun <T> ObjectInputStream.readList(reader: () -> T) = List(readInt()) { reader() }
+
+/**
+ * Writes a nullable object by writing a boolean, then the object. To be used with [readNullable].
+ */
+internal fun <T> ObjectOutputStream.writeNullable(value: T?, writer: (T) -> Unit) {
+ if (value != null) {
+ writeBoolean(true)
+ writer(value)
+ } else {
+ writeBoolean(false)
+ }
+}
+
+/** Reads a nullable value written with [writeNullable]. */
+internal fun <T> ObjectInputStream.readNullable(reader: () -> T): T? =
+ if (readBoolean()) reader() else null
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
index 73e985a..47f0028 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:OptIn(ComplicationExperimental::class)
+
package androidx.wear.watchface.complications.data
import android.app.PendingIntent
@@ -23,17 +25,14 @@
import android.os.Build
import android.util.Log
import androidx.annotation.ColorInt
-import androidx.annotation.IntDef
import androidx.annotation.FloatRange
+import androidx.annotation.IntDef
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import java.time.Instant
-/** The wire format for [ComplicationData]. */
-internal typealias WireComplicationData = android.support.wearable.complications.ComplicationData
-
-/** The builder for [WireComplicationData]. */
-internal typealias WireComplicationDataBuilder =
+typealias WireComplicationData = android.support.wearable.complications.ComplicationData
+typealias WireComplicationDataBuilder =
android.support.wearable.complications.ComplicationData.Builder
internal const val TAG = "Data.kt"
@@ -248,7 +247,7 @@
cachedWireComplicationData,
dataSource = null,
persistencePolicy =
- placeholder?.persistencePolicy ?: ComplicationPersistencePolicies.CACHING_ALLOWED,
+ placeholder?.persistencePolicy ?: ComplicationPersistencePolicies.CACHING_ALLOWED,
displayPolicy = placeholder?.displayPolicy ?: ComplicationDisplayPolicies.ALWAYS_DISPLAY
) {
@@ -907,8 +906,8 @@
* Type used for complications including a numerical value within a range, such as a percentage.
* The value may be accompanied by an icon and/or short text and title.
*
- * The [value], [min], and [max] fields are required for this type and the value within the
- * range is expected to always be displayed.
+ * The [min] and [max] fields are required for this type, as well as one of [value] or
+ * [dynamicValue]. The value within the range is expected to always be displayed.
*
* The icon, title, and text fields are optional and the watch face may choose which of these
* fields to display, if any.
@@ -923,6 +922,9 @@
* [PLACEHOLDER]. If it's equal to [PLACEHOLDER] the renderer must treat it as a placeholder rather
* than rendering normally, its suggested to be drawn as a grey arc with a percentage value selected
* by the renderer. The semantic meaning of value is described by [valueType].
+ * @property dynamicValue The [DynamicFloat] optionally set by the data source. If present the
+ * system will dynamically evaluate this and store the result in [value]. Watch faces can typically
+ * ignore this field.
* @property min The minimum [Float] value for this complication.
* @property max The maximum [Float] value for this complication.
* @property monochromaticImage A simple [MonochromaticImage] image that can be tinted by the watch
@@ -958,6 +960,10 @@
*/
public class RangedValueComplicationData internal constructor(
public val value: Float,
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @ComplicationExperimental
+ @get:ComplicationExperimental
+ public val dynamicValue: DynamicFloat?,
public val min: Float,
public val max: Float,
public val monochromaticImage: MonochromaticImage?,
@@ -989,22 +995,55 @@
/**
* Builder for [RangedValueComplicationData].
*
- * You must at a minimum set the [value], [min], [max] and [contentDescription] fields and at
- * least one of [monochromaticImage], [smallImage], [text] or [title].
- *
- * @param value The value of the ranged complication which should be in the range
- * [[min]] .. [[max]]. The semantic meaning of value can be specified via [setValueType].
- * @param min The minimum value. For [TYPE_PERCENTAGE] this must be 0f.
- * @param max The maximum value. This must be less than [Float.MAX_VALUE]. For [TYPE_PERCENTAGE]
- * this must be 100f.
- * @param contentDescription Localized description for use by screen readers
+ * You must at a minimum set the [min], [max] and [contentDescription] fields, at least one of
+ * [value] or [dynamicValue], and at least one of [monochromaticImage], [smallImage], [text]
+ * or [title].
*/
- public class Builder(
+ public class Builder
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public constructor(
private val value: Float,
+ private val dynamicValue: DynamicFloat?,
private val min: Float,
private val max: Float,
private var contentDescription: ComplicationText
) : BaseBuilder<Builder, RangedValueComplicationData>() {
+ /**
+ * Creates a [Builder] for a [RangedValueComplicationData] with a [Float] value.
+ *
+ * @param value The value of the ranged complication which should be in the range [[min]] ..
+ * [[max]]. The semantic meaning of value can be specified via [setValueType].
+ * @param min The minimum value. For [TYPE_PERCENTAGE] this must be 0f.
+ * @param max The maximum value. This must be less than [Float.MAX_VALUE]. For
+ * [TYPE_PERCENTAGE] this must be 0f.
+ * @param contentDescription Localized description for use by screen readers
+ */
+ public constructor(
+ value: Float,
+ min: Float,
+ max: Float,
+ contentDescription: ComplicationText
+ ) : this(value, dynamicValue = null, min, max, contentDescription)
+
+ /**
+ * Creates a [Builder] for a [RangedValueComplicationData] with a [DynamicFloat] value.
+ *
+ * @param dynamicValue The [DynamicFloat] of the ranged complication which will be evaluated
+ * into a value dynamically, and should be in the range [[min]] .. [[max]]. The semantic
+ * meaning of value can be specified via [setValueType].
+ * @param min The minimum value. For [TYPE_PERCENTAGE] this must be 0f.
+ * @param max The maximum value. This must be less than [Float.MAX_VALUE]. For
+ * [TYPE_PERCENTAGE] this must be 0f.
+ * @param contentDescription Localized description for use by screen readers
+ */
+ @ComplicationExperimental
+ public constructor(
+ dynamicValue: DynamicFloat,
+ min: Float,
+ max: Float,
+ contentDescription: ComplicationText
+ ) : this(value = min /* sensible default */, dynamicValue, min, max, contentDescription)
+
private var tapAction: PendingIntent? = null
private var validTimeRange: TimeRange? = null
private var monochromaticImage: MonochromaticImage? = null
@@ -1012,6 +1051,7 @@
private var title: ComplicationText? = null
private var text: ComplicationText? = null
private var colorRamp: ColorRamp? = null
+
@RangedValueType
private var valueType: Int = TYPE_UNDEFINED
@@ -1082,6 +1122,7 @@
}
return RangedValueComplicationData(
value,
+ dynamicValue,
min,
max,
monochromaticImage,
@@ -1114,6 +1155,7 @@
override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
builder.setRangedValue(value)
+ builder.setRangedDynamicValue(dynamicValue)
builder.setRangedMinValue(min)
builder.setRangedMaxValue(max)
monochromaticImage?.addToWireComplicationData(builder)
@@ -1143,6 +1185,7 @@
other as RangedValueComplicationData
if (value != other.value) return false
+ if (dynamicValue != other.dynamicValue) return false
if (valueType != other.valueType) return false
if (min != other.min) return false
if (max != other.max) return false
@@ -1164,6 +1207,7 @@
override fun hashCode(): Int {
var result = value.hashCode()
+ result = 31 * result + (dynamicValue?.hashCode() ?: 0)
result = 31 * result + valueType
result = 31 * result + min.hashCode()
result = 31 * result + max.hashCode()
@@ -1188,7 +1232,13 @@
} else {
value.toString()
}
- return "RangedValueComplicationData(value=$valueString, valueType=$valueType, min=$min, " +
+ val dynamicValueString = if (WireComplicationData.shouldRedact()) {
+ "REDACTED"
+ } else {
+ dynamicValue.toString()
+ }
+ return "RangedValueComplicationData(value=$valueString, " +
+ "dynamicValue=$dynamicValueString, valueType=$valueType, min=$min, " +
"max=$max, monochromaticImage=$monochromaticImage, smallImage=$smallImage, " +
"title=$title, text=$text, contentDescription=$contentDescription), " +
"tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
@@ -1255,8 +1305,8 @@
* color to indicate progress past the goal). The value may be accompanied by an icon and/or short
* text and title.
*
- * The [value], and [targetValue] fields are required for this type and the progress is expected to
- * always be displayed.
+ * The [targetValue] field is required for this type, as well as one of [value] or
+ * [dynamicValue]. The progress is expected to always be displayed.
*
* The icon, title, and text fields are optional and the watch face may choose which of these
* fields to display, if any.
@@ -1269,12 +1319,16 @@
*
* If you want to represent a score for something that's not based on the user (e.g. air quality
* index) then you should instead use a [RangedValueComplicationData] and pass
- * [RangedValueComplicationData.TYPE_RATING] into [RangedValueComplicationData.Builder.setValueType].
+ * [RangedValueComplicationData.TYPE_RATING] into
+ * [RangedValueComplicationData.Builder.setValueType].
*
* @property value The [Float] value of this complication which is >= 0f, this value may be larger
* than [targetValue]. If it's equal to [PLACEHOLDER] the renderer must treat it as a placeholder
* rather than rendering normally, its suggested to be drawn as a grey arc with a percentage value
* selected by the renderer.
+ * @property dynamicValue The [DynamicFloat] optionally set by the data source. If present the
+ * system will dynamically evaluate this and store the result in [value]. Watch faces can typically
+ * ignore this field.
* @property targetValue The target [Float] value for this complication.
* @property monochromaticImage A simple [MonochromaticImage] image that can be tinted by the watch
* face. If the monochromaticImage is equal to [MonochromaticImage.PLACEHOLDER] the renderer must
@@ -1308,6 +1362,10 @@
public class GoalProgressComplicationData
internal constructor(
public val value: Float,
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @ComplicationExperimental
+ @get:ComplicationExperimental
+ public val dynamicValue: DynamicFloat?,
public val targetValue: Float,
public val monochromaticImage: MonochromaticImage?,
public val smallImage: SmallImage?,
@@ -1333,19 +1391,52 @@
/**
* Builder for [GoalProgressComplicationData].
*
- * You must at a minimum set the [value], [targetValue] and [contentDescription] fields and at
- * least one of [monochromaticImage], [smallImage], [text] or [title].
- *
- * @param value The value of the ranged complication which should be >= 0.
- * @param targetValue The target value. This must be less than [Float.MAX_VALUE].
- * @param contentDescription Localized description for use by screen readers
+ * You must at a minimum set the [targetValue] and [contentDescription] fields, one of [value]
+ * or [dynamicValue], and at least one of [monochromaticImage], [smallImage], [text] or
+ * [title].
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
- public class Builder(
+ public class Builder
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public constructor(
private val value: Float,
+ private val dynamicValue: DynamicFloat?,
private val targetValue: Float,
private var contentDescription: ComplicationText
) : BaseBuilder<Builder, GoalProgressComplicationData>() {
+ /**
+ * Creates a [Builder] for a [GoalProgressComplicationData] with a [Float] value.
+ *
+ * @param value The value of the goal complication which should be >= 0.
+ * @param targetValue The target value. This must be less than [Float.MAX_VALUE].
+ * @param contentDescription Localized description for use by screen readers
+ */
+ public constructor(
+ value: Float,
+ targetValue: Float,
+ contentDescription: ComplicationText
+ ) : this(value, dynamicValue = null, targetValue, contentDescription)
+
+ /**
+ * Creates a [Builder] for a [GoalProgressComplicationData] with a [DynamicFloat] value.
+ *
+ * @param dynamicValue The [DynamicFloat] of the goal complication which will be evaluated
+ * into a value dynamically, and should be >= 0.
+ * @param targetValue The target value. This must be less than [Float.MAX_VALUE].
+ * @param contentDescription Localized description for use by screen readers
+ */
+ @ComplicationExperimental
+ public constructor(
+ dynamicValue: DynamicFloat,
+ targetValue: Float,
+ contentDescription: ComplicationText
+ ) : this(
+ value = 0f /* sensible default */,
+ dynamicValue,
+ targetValue,
+ contentDescription
+ )
+
private var tapAction: PendingIntent? = null
private var validTimeRange: TimeRange? = null
private var monochromaticImage: MonochromaticImage? = null
@@ -1408,6 +1499,7 @@
}
return GoalProgressComplicationData(
value,
+ dynamicValue,
targetValue,
monochromaticImage,
smallImage,
@@ -1438,6 +1530,7 @@
override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
builder.setRangedValue(value)
+ builder.setRangedDynamicValue(dynamicValue)
builder.setTargetValue(targetValue)
monochromaticImage?.addToWireComplicationData(builder)
smallImage?.addToWireComplicationData(builder)
@@ -1465,6 +1558,7 @@
other as GoalProgressComplicationData
if (value != other.value) return false
+ if (dynamicValue != other.dynamicValue) return false
if (targetValue != other.targetValue) return false
if (monochromaticImage != other.monochromaticImage) return false
if (smallImage != other.smallImage) return false
@@ -1484,6 +1578,7 @@
override fun hashCode(): Int {
var result = value.hashCode()
+ result = 31 * result + (dynamicValue?.hashCode() ?: 0)
result = 31 * result + targetValue.hashCode()
result = 31 * result + (monochromaticImage?.hashCode() ?: 0)
result = 31 * result + (smallImage?.hashCode() ?: 0)
@@ -1506,7 +1601,13 @@
} else {
value.toString()
}
- return "GoalProgressComplicationData(value=$valueString, targetValue=$targetValue, " +
+ val dynamicValueString = if (WireComplicationData.shouldRedact()) {
+ "REDACTED"
+ } else {
+ dynamicValue.toString()
+ }
+ return "GoalProgressComplicationData(value=$valueString, " +
+ "dynamicValue=$dynamicValueString, targetValue=$targetValue, " +
"monochromaticImage=$monochromaticImage, smallImage=$smallImage, title=$title, " +
"text=$text, contentDescription=$contentDescription), " +
"tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
@@ -1684,7 +1785,8 @@
elements: List<Element>,
private var contentDescription: ComplicationText
) : BaseBuilder<Builder, WeightedElementsComplicationData>() {
- @ColorInt private var elementBackgroundColor: Int = Color.TRANSPARENT
+ @ColorInt
+ private var elementBackgroundColor: Int = Color.TRANSPARENT
private var tapAction: PendingIntent? = null
private var validTimeRange: TimeRange? = null
private var monochromaticImage: MonochromaticImage? = null
@@ -2564,6 +2666,7 @@
RangedValueComplicationData.TYPE.toWireComplicationType() ->
RangedValueComplicationData.Builder(
value = rangedValue,
+ dynamicValue = rangedDynamicValue,
min = rangedMinValue,
max = rangedMaxValue,
contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
@@ -2622,6 +2725,7 @@
GoalProgressComplicationData.TYPE.toWireComplicationType() ->
GoalProgressComplicationData.Builder(
value = rangedValue,
+ dynamicValue = rangedDynamicValue,
targetValue = targetValue,
contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
).apply {
@@ -2738,7 +2842,9 @@
RangedValueComplicationData.TYPE.toWireComplicationType() ->
RangedValueComplicationData.Builder(
- value = rangedValue, min = rangedMinValue,
+ value = rangedValue,
+ dynamicValue = rangedDynamicValue,
+ min = rangedMinValue,
max = rangedMaxValue,
contentDescription = contentDescription?.toApiComplicationText()
?: ComplicationText.EMPTY
@@ -2813,6 +2919,7 @@
GoalProgressComplicationData.TYPE.toWireComplicationType() ->
GoalProgressComplicationData.Builder(
value = rangedValue,
+ dynamicValue = rangedDynamicValue,
targetValue = targetValue,
contentDescription = contentDescription?.toApiComplicationText()
?: ComplicationText.EMPTY
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/DynamicFloat.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/DynamicFloat.kt
new file mode 100644
index 0000000..0374f72
--- /dev/null
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/DynamicFloat.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.wear.watchface.complications.data
+
+import androidx.annotation.RestrictTo
+
+/** Placeholder for DynamicFloat implementation by tiles. */
+// TODO(b/257413268): Replace this with the real implementation.
+@ComplicationExperimental
+abstract class DynamicFloat {
+ abstract fun asByteArray(): ByteArray
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ // Not checking for exact same class because it's not implemented yet.
+ if (other !is DynamicFloat) return false
+ return asByteArray().contentEquals(other.asByteArray())
+ }
+
+ override fun hashCode() = asByteArray().contentHashCode()
+
+ override fun toString() = "DynamicFloatPlaceholder${asByteArray().contentToString()}"
+}
+
+/** Placeholder parser for [DynamicFloat] from [ByteArray]. */
+@ComplicationExperimental
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun ByteArray.toDynamicFloat() = object : DynamicFloat() {
+ override fun asByteArray() = this@toDynamicFloat
+}
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
index d264e3f..4494346 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:OptIn(ComplicationExperimental::class)
+
package android.support.wearable.complications
import android.app.PendingIntent
@@ -23,7 +25,9 @@
import android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder
import android.support.wearable.complications.ComplicationText.TimeFormatBuilder
import androidx.test.core.app.ApplicationProvider
+import androidx.wear.watchface.complications.data.ComplicationExperimental
import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
+import androidx.wear.watchface.complications.data.toDynamicFloat
import com.google.common.truth.Truth
import org.junit.Assert
import org.junit.Assert.assertThrows
@@ -79,7 +83,7 @@
}
@Test
- public fun testRangedValueFields() {
+ public fun testRangedValueFieldsWithFixedValue() {
// GIVEN complication data of the RANGED_VALUE type created by the Builder...
val data =
ComplicationData.Builder(ComplicationData.TYPE_RANGED_VALUE)
@@ -93,6 +97,30 @@
// WHEN the relevant getters are called on the resulting data
// THEN the correct values are returned.
Assert.assertEquals(data.rangedValue, 57f, 0f)
+ Assert.assertNull(data.rangedDynamicValue)
+ Assert.assertEquals(data.rangedMinValue, 5f, 0f)
+ Assert.assertEquals(data.rangedMaxValue, 150f, 0f)
+ Truth.assertThat(data.shortTitle!!.getTextAt(mResources, 0))
+ .isEqualTo("title")
+ Truth.assertThat(data.shortText!!.getTextAt(mResources, 0))
+ .isEqualTo("text")
+ }
+
+ @Test
+ public fun testRangedValueFieldsWithDynamicValue() {
+ // GIVEN complication data of the RANGED_VALUE type created by the Builder...
+ val data =
+ ComplicationData.Builder(ComplicationData.TYPE_RANGED_VALUE)
+ .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+ .setRangedMinValue(5f)
+ .setRangedMaxValue(150f)
+ .setShortTitle(ComplicationText.plainText("title"))
+ .setShortText(ComplicationText.plainText("text"))
+ .build()
+
+ // WHEN the relevant getters are called on the resulting data
+ // THEN the correct values are returned.
+ Truth.assertThat(data.rangedDynamicValue!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
Assert.assertEquals(data.rangedMinValue, 5f, 0f)
Assert.assertEquals(data.rangedMaxValue, 150f, 0f)
Truth.assertThat(data.shortTitle!!.getTextAt(mResources, 0))
@@ -128,7 +156,7 @@
}
@Test
- public fun testRangedValueMustContainValue() {
+ public fun testRangedValueMustContainFixedOrDynamicValue() {
// GIVEN a complication builder of the RANGED_VALUE type, with the value field not
// populated...
val builder =
@@ -1106,7 +1134,7 @@
.setLongText(
ComplicationText.plainText(ComplicationData.PLACEHOLDER_STRING)
)
- .build()
+ .build()
)
.build()
timelineEntry.timelineStartEpochSecond = 100
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
index 7251d4b..44c76f8 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:OptIn(ComplicationExperimental::class)
+
package androidx.wear.watchface.complications.data
import android.annotation.SuppressLint
@@ -399,7 +401,7 @@
}
@Test
- public fun rangedValueComplicationData() {
+ public fun rangedValueComplicationData_withFixedValue() {
val data = RangedValueComplicationData.Builder(
value = 95f, min = 0f, max = 100f,
contentDescription = "content description".complicationText
@@ -426,6 +428,7 @@
assertThat(deserialized.max).isEqualTo(100f)
assertThat(deserialized.min).isEqualTo(0f)
assertThat(deserialized.value).isEqualTo(95f)
+ assertThat(deserialized.dynamicValue).isNull()
assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
.isEqualTo("content description")
assertThat(deserialized.title!!.getTextAt(resources, Instant.EPOCH))
@@ -452,7 +455,95 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "RangedValueComplicationData(value=95.0, valueType=0, min=0.0, max=100.0, " +
+ "RangedValueComplicationData(value=95.0, dynamicValue=null, " +
+ "valueType=0, min=0.0, max=100.0, " +
+ "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
+ "mSurroundingText=battery, mTimeDependentText=null}, text=null, " +
+ "contentDescription=ComplicationText{mSurroundingText=content description, " +
+ "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
+ "tapAction=null, validTimeRange=TimeRange(" +
+ "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
+ "ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
+ "displayPolicy=0)"
+ )
+ }
+
+ @Test
+ public fun rangedValueComplicationData_withDynamicValue() {
+ val data = RangedValueComplicationData.Builder(
+ dynamicValue = byteArrayOf(42, 107).toDynamicFloat(),
+ min = 5f,
+ max = 100f,
+ contentDescription = "content description".complicationText
+ )
+ .setTitle("battery".complicationText)
+ .setDataSource(dataSourceA)
+ .build()
+ ParcelableSubject.assertThat(data.asWireComplicationData())
+ .hasSameSerializationAs(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+ .setRangedValue(5f) // min as a sensible default
+ .setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
+ .setRangedMinValue(5f)
+ .setRangedMaxValue(100f)
+ .setShortTitle(WireComplicationText.plainText("battery"))
+ .setContentDescription(WireComplicationText.plainText("content description"))
+ .setDataSource(dataSourceA)
+ .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
+ .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
+ .build()
+ )
+ testRoundTripConversions(data)
+ val deserialized = serializeAndDeserialize(data) as RangedValueComplicationData
+ assertThat(deserialized.max).isEqualTo(100f)
+ assertThat(deserialized.min).isEqualTo(5f)
+ assertThat(deserialized.dynamicValue!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
+ assertThat(deserialized.value).isEqualTo(5f) // min as a sensible default
+ assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
+ .isEqualTo("content description")
+ assertThat(deserialized.title!!.getTextAt(resources, Instant.EPOCH))
+ .isEqualTo("battery")
+
+ val sameData = RangedValueComplicationData.Builder(
+ dynamicValue = byteArrayOf(42, 107).toDynamicFloat(),
+ min = 5f,
+ max = 100f,
+ contentDescription = "content description".complicationText
+ )
+ .setTitle("battery".complicationText)
+ .setDataSource(dataSourceA)
+ .build()
+ val diffDataFixedValue = RangedValueComplicationData.Builder(
+ value = 5f, // Even though it's the sensible default
+ min = 5f,
+ max = 100f,
+ contentDescription = "content description".complicationText
+ )
+ .setTitle("battery".complicationText)
+ .setDataSource(dataSourceA)
+ .build()
+ val diffDataDynamicValue = RangedValueComplicationData.Builder(
+ dynamicValue = byteArrayOf(43, 108).toDynamicFloat(),
+ min = 5f,
+ max = 100f,
+ contentDescription = "content description".complicationText
+ )
+ .setTitle("battery".complicationText)
+ .setDataSource(dataSourceA)
+ .build()
+
+ assertThat(data).isEqualTo(sameData)
+ assertThat(data).isNotEqualTo(diffDataFixedValue)
+ assertThat(data).isNotEqualTo(diffDataDynamicValue)
+ assertThat(data.hashCode()).isEqualTo(sameData.hashCode())
+ assertThat(data.hashCode()).isNotEqualTo(diffDataFixedValue.hashCode())
+ assertThat(data.hashCode()).isNotEqualTo(diffDataDynamicValue.hashCode())
+ assertThat(data.toString()).isEqualTo(
+ "RangedValueComplicationData(value=5.0, " +
+ "dynamicValue=DynamicFloatPlaceholder[42, 107], " +
+ "valueType=0, min=5.0, max=100.0, " +
"monochromaticImage=null, smallImage=null, title=ComplicationText{" +
"mSurroundingText=battery, mTimeDependentText=null}, text=null, " +
"contentDescription=ComplicationText{mSurroundingText=content description, " +
@@ -535,7 +626,8 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "RangedValueComplicationData(value=95.0, valueType=1, min=0.0, max=100.0, " +
+ "RangedValueComplicationData(value=95.0, dynamicValue=null, " +
+ "valueType=1, min=0.0, max=100.0, " +
"monochromaticImage=MonochromaticImage(image=Icon(typ=URI uri=someuri), " +
"ambientImage=null), smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), " +
"type=PHOTO, ambientImage=null), title=ComplicationText{mSurroundingText=battery," +
@@ -550,7 +642,144 @@
}
@Test
- public fun goalProgressComplicationData_with_ColorRamp() {
+ public fun goalProgressComplicationData_withFixedValue() {
+ val data = GoalProgressComplicationData.Builder(
+ value = 1200f, targetValue = 10000f,
+ contentDescription = "content description".complicationText
+ )
+ .setTitle("steps".complicationText)
+ .setDataSource(dataSourceA)
+ .build()
+ ParcelableSubject.assertThat(data.asWireComplicationData())
+ .hasSameSerializationAs(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+ .setRangedValue(1200f)
+ .setTargetValue(10000f)
+ .setShortTitle(WireComplicationText.plainText("steps"))
+ .setContentDescription(WireComplicationText.plainText("content description"))
+ .setDataSource(dataSourceA)
+ .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
+ .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
+ .build()
+ )
+ testRoundTripConversions(data)
+ val deserialized = serializeAndDeserialize(data) as GoalProgressComplicationData
+ assertThat(deserialized.value).isEqualTo(1200f)
+ assertThat(deserialized.dynamicValue).isNull()
+ assertThat(deserialized.targetValue).isEqualTo(10000f)
+ assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
+ .isEqualTo("content description")
+ assertThat(deserialized.title!!.getTextAt(resources, Instant.EPOCH))
+ .isEqualTo("steps")
+
+ val sameData = GoalProgressComplicationData.Builder(
+ value = 1200f, targetValue = 10000f,
+ contentDescription = "content description".complicationText
+ )
+ .setTitle("steps".complicationText)
+ .setDataSource(dataSourceA)
+ .build()
+
+ val diffData = GoalProgressComplicationData.Builder(
+ value = 1201f, targetValue = 10000f,
+ contentDescription = "content description".complicationText
+ )
+ .setTitle("steps".complicationText)
+ .setDataSource(dataSourceB)
+ .build()
+
+ assertThat(data).isEqualTo(sameData)
+ assertThat(data).isNotEqualTo(diffData)
+ assertThat(data.hashCode()).isEqualTo(sameData.hashCode())
+ assertThat(data.hashCode()).isNotEqualTo(diffData.hashCode())
+ assertThat(data.toString()).isEqualTo(
+ "GoalProgressComplicationData(value=1200.0, dynamicValue=null, " +
+ "targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
+ "title=ComplicationText{mSurroundingText=steps, mTimeDependentText=null}, " +
+ "text=null, " +
+ "contentDescription=ComplicationText{mSurroundingText=content description, " +
+ "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
+ "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
+ "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
+ "+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
+ "ComponentInfo{com.pkg_a/com.a}, colorRamp=null, " +
+ "persistencePolicy=0, displayPolicy=0)"
+ )
+ }
+
+ @Test
+ public fun goalProgressComplicationData_withDynamicValue() {
+ val data = GoalProgressComplicationData.Builder(
+ dynamicValue = byteArrayOf(42, 107).toDynamicFloat(),
+ targetValue = 10000f,
+ contentDescription = "content description".complicationText
+ )
+ .setTitle("steps".complicationText)
+ .setDataSource(dataSourceA)
+ .build()
+ ParcelableSubject.assertThat(data.asWireComplicationData())
+ .hasSameSerializationAs(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+ .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+ .setRangedValue(0f) // sensible default
+ .setTargetValue(10000f)
+ .setShortTitle(WireComplicationText.plainText("steps"))
+ .setContentDescription(WireComplicationText.plainText("content description"))
+ .setDataSource(dataSourceA)
+ .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
+ .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
+ .build()
+ )
+ testRoundTripConversions(data)
+ val deserialized = serializeAndDeserialize(data) as GoalProgressComplicationData
+ assertThat(deserialized.dynamicValue!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
+ assertThat(deserialized.value).isEqualTo(0f) // sensible default
+ assertThat(deserialized.targetValue).isEqualTo(10000f)
+ assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
+ .isEqualTo("content description")
+ assertThat(deserialized.title!!.getTextAt(resources, Instant.EPOCH))
+ .isEqualTo("steps")
+
+ val sameData = GoalProgressComplicationData.Builder(
+ dynamicValue = byteArrayOf(42, 107).toDynamicFloat(),
+ targetValue = 10000f,
+ contentDescription = "content description".complicationText
+ )
+ .setTitle("steps".complicationText)
+ .setDataSource(dataSourceA)
+ .build()
+
+ val diffData = GoalProgressComplicationData.Builder(
+ dynamicValue = byteArrayOf(43, 108).toDynamicFloat(),
+ targetValue = 10000f,
+ contentDescription = "content description".complicationText
+ )
+ .setTitle("steps".complicationText)
+ .setDataSource(dataSourceB)
+ .build()
+
+ assertThat(data).isEqualTo(sameData)
+ assertThat(data).isNotEqualTo(diffData)
+ assertThat(data.hashCode()).isEqualTo(sameData.hashCode())
+ assertThat(data.hashCode()).isNotEqualTo(diffData.hashCode())
+ assertThat(data.toString()).isEqualTo(
+ "GoalProgressComplicationData(value=0.0, " +
+ "dynamicValue=DynamicFloatPlaceholder[42, 107], " +
+ "targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
+ "title=ComplicationText{mSurroundingText=steps, mTimeDependentText=null}, " +
+ "text=null, " +
+ "contentDescription=ComplicationText{mSurroundingText=content description, " +
+ "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
+ "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
+ "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
+ "+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
+ "ComponentInfo{com.pkg_a/com.a}, colorRamp=null, " +
+ "persistencePolicy=0, displayPolicy=0)"
+ )
+ }
+
+ @Test
+ public fun goalProgressComplicationData_withColorRamp() {
val data = GoalProgressComplicationData.Builder(
value = 1200f, targetValue = 10000f,
contentDescription = "content description".complicationText
@@ -605,9 +834,10 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "GoalProgressComplicationData(value=1200.0, targetValue=10000.0, " +
- "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
- "mSurroundingText=steps, mTimeDependentText=null}, text=null, " +
+ "GoalProgressComplicationData(value=1200.0, dynamicValue=null, " +
+ "targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
+ "title=ComplicationText{mSurroundingText=steps, mTimeDependentText=null}, " +
+ "text=null, " +
"contentDescription=ComplicationText{mSurroundingText=content description, " +
"mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
"tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
@@ -620,7 +850,7 @@
@RequiresApi(Build.VERSION_CODES.P)
@Test
- public fun goalProgressComplicationData_with_ColorRamp_and_Images() {
+ public fun goalProgressComplicationData_withColorRampAndImages() {
val data = GoalProgressComplicationData.Builder(
value = 1200f, targetValue = 10000f,
contentDescription = "content description".complicationText
@@ -687,7 +917,8 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "GoalProgressComplicationData(value=1200.0, targetValue=10000.0, " +
+ "GoalProgressComplicationData(value=1200.0, dynamicValue=null, " +
+ "targetValue=10000.0, " +
"monochromaticImage=MonochromaticImage(image=Icon(typ=URI uri=someuri), " +
"ambientImage=null), smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), " +
"type=PHOTO, ambientImage=null), title=ComplicationText{mSurroundingText=steps, " +
@@ -703,7 +934,7 @@
}
@Test
- public fun rangedValueComplicationData_with_ColorRamp() {
+ public fun rangedValueComplicationData_withColorRamp() {
val data = RangedValueComplicationData.Builder(
value = 95f, min = 0f, max = 100f,
contentDescription = "content description".complicationText
@@ -761,7 +992,8 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "RangedValueComplicationData(value=95.0, valueType=0, min=0.0, max=100.0, " +
+ "RangedValueComplicationData(value=95.0, dynamicValue=null, " +
+ "valueType=0, min=0.0, max=100.0, " +
"monochromaticImage=null, smallImage=null, title=ComplicationText{" +
"mSurroundingText=battery, mTimeDependentText=null}, text=null, " +
"contentDescription=ComplicationText{mSurroundingText=content description, " +
@@ -1093,6 +1325,7 @@
assertThat(deserialized.smallImage.image.type).isEqualTo(Icon.TYPE_BITMAP)
val getBitmap = deserialized.smallImage.image.javaClass.getDeclaredMethod("getBitmap")
+
@SuppressLint("BanUncheckedReflection")
val bitmap = getBitmap.invoke(deserialized.smallImage.image) as Bitmap
@@ -1455,8 +1688,8 @@
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
"NoDataComplicationData(placeholder=RangedValueComplicationData(" +
- "value=3.4028235E38, valueType=0, min=0.0, max=100.0, monochromaticImage=null, " +
- "smallImage=null, title=null, text=ComplicationText{" +
+ "value=3.4028235E38, dynamicValue=null, valueType=0, min=0.0, max=100.0, " +
+ "monochromaticImage=null, smallImage=null, title=null, text=ComplicationText{" +
"mSurroundingText=__placeholder__, mTimeDependentText=null}, " +
"contentDescription=ComplicationText{mSurroundingText=" +
"content description, mTimeDependentText=null}), " +
@@ -1538,8 +1771,8 @@
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
"NoDataComplicationData(placeholder=GoalProgressComplicationData(" +
- "value=3.4028235E38, targetValue=10000.0, monochromaticImage=null, " +
- "smallImage=null, title=null, text=ComplicationText{" +
+ "value=3.4028235E38, dynamicValue=null, targetValue=10000.0, " +
+ "monochromaticImage=null, smallImage=null, title=null, text=ComplicationText{" +
"mSurroundingText=__placeholder__, mTimeDependentText=null}, " +
"contentDescription=ComplicationText{mSurroundingText=content description, " +
"mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
@@ -1722,8 +1955,8 @@
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
"NoDataComplicationData(placeholder=RangedValueComplicationData(" +
- "value=3.4028235E38, valueType=1, min=0.0, max=100.0, monochromaticImage=null, " +
- "smallImage=null, title=null, text=ComplicationText{" +
+ "value=3.4028235E38, dynamicValue=null, valueType=1, min=0.0, max=100.0, " +
+ "monochromaticImage=null, smallImage=null, title=null, text=ComplicationText{" +
"mSurroundingText=__placeholder__, mTimeDependentText=null}, " +
"contentDescription=ComplicationText{mSurroundingText=" +
"content description, mTimeDependentText=null}), " +
@@ -2018,7 +2251,7 @@
}
@Test
- public fun rangedValueComplicationData() {
+ public fun rangedValueComplicationData_withFixedValue() {
assertRoundtrip(
WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(95f)
@@ -2034,6 +2267,22 @@
}
@Test
+ public fun rangedValueComplicationData_withDynamicValue() {
+ assertRoundtrip(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+ .setRangedMinValue(0f)
+ .setRangedMaxValue(100f)
+ .setShortTitle(WireComplicationText.plainText("battery"))
+ .setContentDescription(WireComplicationText.plainText("content description"))
+ .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
+ .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
+ .build(),
+ ComplicationType.RANGED_VALUE
+ )
+ }
+
+ @Test
public fun rangedValueComplicationData_drawSegmented() {
assertRoundtrip(
WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
@@ -2050,7 +2299,7 @@
}
@Test
- public fun goalProgressComplicationData() {
+ public fun goalProgressComplicationData_withFixedValue() {
assertRoundtrip(
WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
.setRangedValue(1200f)
@@ -2067,6 +2316,23 @@
}
@Test
+ public fun goalProgressComplicationData_withDynamicValue() {
+ assertRoundtrip(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+ .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+ .setTargetValue(10000f)
+ .setShortTitle(WireComplicationText.plainText("steps"))
+ .setContentDescription(WireComplicationText.plainText("content description"))
+ .setColorRamp(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
+ .setColorRampIsSmoothShaded(false)
+ .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
+ .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
+ .build(),
+ ComplicationType.GOAL_PROGRESS
+ )
+ }
+
+ @Test
public fun weightedElementsComplicationData() {
assertRoundtrip(
WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
@@ -3006,13 +3272,14 @@
.setTitle("title".complicationText)
.build()
- assertThat(data.toString()).isEqualTo("LongTextComplicationData(text=" +
- "ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, title=" +
- "ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
- "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText" +
- "{mSurroundingText=REDACTED, mTimeDependentText=null}), " +
- "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange" +
- "(REDACTED), dataSource=null, persistencePolicy=0, displayPolicy=0)"
+ assertThat(data.toString()).isEqualTo(
+ "LongTextComplicationData(text=" +
+ "ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, title=" +
+ "ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
+ "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText" +
+ "{mSurroundingText=REDACTED, mTimeDependentText=null}), " +
+ "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange" +
+ "(REDACTED), dataSource=null, persistencePolicy=0, displayPolicy=0)"
)
assertThat(data.asWireComplicationData().toString()).isEqualTo(
"ComplicationData{mType=4, mFields=REDACTED}"
@@ -3031,14 +3298,15 @@
.setTitle("title".complicationText)
.build()
- assertThat(data.toString()).isEqualTo("RangedValueComplicationData(value=REDACTED, " +
- "valueType=0, min=0.0, max=100.0, monochromaticImage=null, smallImage=null, " +
- "title=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
- "text=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
- "contentDescription=ComplicationText{mSurroundingText=REDACTED, " +
- "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(REDACTED), dataSource=null, " +
- "colorRamp=null, persistencePolicy=0, displayPolicy=0)"
+ assertThat(data.toString()).isEqualTo(
+ "RangedValueComplicationData(value=REDACTED, dynamicValue=REDACTED, " +
+ "valueType=0, min=0.0, max=100.0, monochromaticImage=null, smallImage=null, " +
+ "title=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
+ "text=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
+ "contentDescription=ComplicationText{mSurroundingText=REDACTED, " +
+ "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
+ "tapAction=null, validTimeRange=TimeRange(REDACTED), dataSource=null, " +
+ "colorRamp=null, persistencePolicy=0, displayPolicy=0)"
)
assertThat(data.asWireComplicationData().toString()).isEqualTo(
"ComplicationData{mType=5, mFields=REDACTED}"
@@ -3057,10 +3325,10 @@
.build()
assertThat(data.toString()).isEqualTo(
- "GoalProgressComplicationData(value=REDACTED, targetValue=10000.0, " +
- "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
- "mSurroundingText=REDACTED, mTimeDependentText=null}, text=null, " +
- "contentDescription=ComplicationText{mSurroundingText=REDACTED, " +
+ "GoalProgressComplicationData(value=REDACTED, dynamicValue=REDACTED, " +
+ "targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
+ "title=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
+ "text=null, contentDescription=ComplicationText{mSurroundingText=REDACTED, " +
"mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
"tapAction=null, validTimeRange=TimeRange(REDACTED), dataSource=null, " +
"colorRamp=ColorRamp(colors=[-65536, -16711936, -16776961], interpolated=true), " +
diff --git a/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java b/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java
index f5f9868..b014c3b 100644
--- a/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java
+++ b/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java
@@ -429,9 +429,9 @@
}
private class RangedArcsTestData {
- public int min;
- public int max;
- public int value;
+ public float min;
+ public float max;
+ public float value;
public float progress;
public float remaining;
public float gap;
@@ -498,9 +498,9 @@
@Test
public void rangedValueIsDrawnCorrectlyInActiveMode() {
// GIVEN a complication renderer with ranged value complication data
- int min = 0;
- int max = 100;
- int value = (max - min) / 2;
+ float min = 0;
+ float max = 100;
+ float value = (max - min) / 2;
mComplicationRenderer.setComplicationData(
new ComplicationData.Builder(TYPE_RANGED_VALUE)
.setRangedValue(value)
@@ -532,9 +532,9 @@
@Test
public void rangedValueIsDrawnCorrectlyInAmbientMode() {
// GIVEN a complication renderer with ranged value complication data
- int min = 0;
- int max = 100;
- int value = (max - min) / 2;
+ float min = 0;
+ float max = 100;
+ float value = (max - min) / 2;
mComplicationRenderer.setComplicationData(
new ComplicationData.Builder(TYPE_RANGED_VALUE)
.setRangedValue(value)
@@ -1005,9 +1005,9 @@
new ComplicationData.Builder(TYPE_RANGED_VALUE)
.setShortText(ComplicationText.plainText("foo"))
.setShortTitle(ComplicationText.plainText("bar"))
- .setRangedMinValue(1)
- .setRangedValue(5)
- .setRangedMaxValue(10)
+ .setRangedMinValue(1f)
+ .setRangedValue(5f)
+ .setRangedMaxValue(10f)
.build(),
true);
mComplicationRenderer.setRangedValueProgressHidden(true);
@@ -1029,9 +1029,9 @@
mComplicationRenderer.setComplicationData(
new ComplicationData.Builder(TYPE_RANGED_VALUE)
.setIcon(mMockIcon)
- .setRangedMinValue(1)
- .setRangedValue(5)
- .setRangedMaxValue(10)
+ .setRangedMinValue(1f)
+ .setRangedValue(5f)
+ .setRangedMaxValue(10f)
.build(),
true);
mComplicationRenderer.setRangedValueProgressHidden(true);
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
index 548403e..7ce080b 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
@@ -415,7 +415,7 @@
sendComplications()
handler.post {
- engineWrapper.draw()
+ engineWrapper.draw(engineWrapper.getWatchFaceImplOrNull())
}
assertThat(renderDoneLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue()
@@ -430,7 +430,7 @@
handler.post {
setAmbient(true)
- engineWrapper.draw()
+ engineWrapper.draw(engineWrapper.getWatchFaceImplOrNull())
}
assertThat(renderDoneLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue()
@@ -526,7 +526,7 @@
handler.post {
assertThat(engineWrapper.mutableWatchState.watchFaceInstanceId.value).isEqualTo(newId)
- engineWrapper.draw()
+ engineWrapper.draw(engineWrapper.getWatchFaceImplOrNull())
}
assertThat(renderDoneLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue()
@@ -544,7 +544,7 @@
)
handler.post {
- engineWrapper.draw()
+ engineWrapper.draw(engineWrapper.getWatchFaceImplOrNull())
}
assertThat(renderDoneLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue()
@@ -939,7 +939,7 @@
engineWrapper.deferredWatchFaceImpl.await()
}
- handler.post { engineWrapper.draw() }
+ handler.post { engineWrapper.draw(engineWrapper.getWatchFaceImplOrNull()) }
assertThat(renderDoneLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue()
try {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 9a10e9e3..073f8a4 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -1225,7 +1225,7 @@
* there's no point drawing.
*/
if (watchFaceImpl?.renderer?.shouldAnimate() != false) {
- draw()
+ draw(watchFaceImpl)
}
}
}
@@ -1415,7 +1415,8 @@
// loaded yet (if that did happen then draw would be a NOP). The watch face will
// render at least once upon loading so we don't need to do anything special
// here.
- draw()
+ val watchFaceImpl: WatchFaceImpl? = getWatchFaceImplOrNull()
+ draw(watchFaceImpl)
} catch (t: Throwable) {
Log.e(TAG, "ambientTickUpdate failed", t)
} finally {
@@ -2454,7 +2455,7 @@
}
}
- internal fun draw() {
+ internal fun draw(watchFaceImpl: WatchFaceImpl?) {
try {
if (TRACE_DRAW) {
Trace.beginSection("onDraw")
@@ -2462,8 +2463,6 @@
if (LOG_VERBOSE) {
Log.v(TAG, "drawing frame")
}
-
- val watchFaceImpl: WatchFaceImpl? = getWatchFaceImplOrNull()
watchFaceImpl?.onDraw()
} finally {
if (TRACE_DRAW) {